github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/user.go (about)

     1  /*
     2  *
     3  *	Gosora User File
     4  *	Copyright Azareal 2017 - 2020
     5  *
     6   */
     7  package common
     8  
     9  import (
    10  	"database/sql"
    11  	"errors"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	//"log"
    17  
    18  	qgen "github.com/Azareal/Gosora/query_gen"
    19  	"github.com/go-sql-driver/mysql"
    20  )
    21  
    22  // TODO: Replace any literals with this
    23  var BanGroup = 4
    24  
    25  // TODO: Use something else as the guest avatar, maybe a question mark of some sort?
    26  // GuestUser is an instance of user which holds guest data to avoid having to initialise a guest every time
    27  var GuestUser = User{ID: 0, Name: "Guest", Link: "#", Group: 6, Perms: GuestPerms, CreatedAt: StartTime} // BuildAvatar is done in site.go to make sure it's done after init
    28  var ErrNoTempGroup = errors.New("We couldn't find a temporary group for this user")
    29  
    30  type User struct {
    31  	ID           int
    32  	Link         string
    33  	Name         string
    34  	Email        string
    35  	Group        int
    36  	Active       bool
    37  	IsMod        bool
    38  	IsSuperMod   bool
    39  	IsAdmin      bool
    40  	IsSuperAdmin bool
    41  	IsBanned     bool
    42  	Perms        Perms
    43  	PluginPerms  map[string]bool
    44  	Session      string
    45  	//AuthToken    string
    46  	Loggedin    bool
    47  	RawAvatar   string
    48  	Avatar      string
    49  	MicroAvatar string
    50  	Message     string
    51  	// TODO: Implement something like this for profiles?
    52  	//URLPrefix   string // Move this to another table? Create a user lite?
    53  	//URLName     string
    54  	Tag       string
    55  	Level     int
    56  	Score     int
    57  	Posts     int
    58  	Liked     int
    59  	CreatedAt time.Time
    60  	LastIP    string // ! This part of the UserCache data might fall out of date
    61  	LastAgent int    // ! Temporary hack for http push, don't use
    62  	TempGroup int
    63  
    64  	ParseSettings *ParseSettings
    65  	Privacy       UserPrivacy
    66  }
    67  
    68  type UserPrivacy struct {
    69  	ShowComments int  // 0 = default, 1 = public, 2 = registered, 3 = friends, 4 = self, 5 = disabled / unused
    70  	AllowMessage int  // 0 = default, 1 = registered, 2 = friends, 3 = mods, 4 = disabled / unused
    71  	NoPresence   bool // false = default, true = true
    72  }
    73  
    74  func (u *User) WebSockets() *WsJSONUser {
    75  	groupID := u.Group
    76  	if u.TempGroup != 0 {
    77  		groupID = u.TempGroup
    78  	}
    79  	// TODO: Do we want to leak the user's permissions? Users will probably be able to see their status from the group tags, but still
    80  	return &WsJSONUser{u.ID, u.Link, u.Name, groupID, u.IsMod, u.Avatar, u.MicroAvatar, u.Level, u.Score, u.Liked}
    81  }
    82  
    83  // Use struct tags to avoid having to define this? It really depends on the circumstances, sometimes we want the whole thing, sometimes... not.
    84  type WsJSONUser struct {
    85  	ID          int
    86  	Link        string
    87  	Name        string
    88  	Group       int // Be sure to mask with TempGroup
    89  	IsMod       bool
    90  	Avatar      string
    91  	MicroAvatar string
    92  	Level       int
    93  	Score       int
    94  	Liked       int
    95  }
    96  
    97  func (u *User) Me() *MeUser {
    98  	groupID := u.Group
    99  	if u.TempGroup != 0 {
   100  		groupID = u.TempGroup
   101  	}
   102  	return &MeUser{u.ID, u.Link, u.Name, groupID, u.Active, u.IsMod, u.IsSuperMod, u.IsAdmin, u.IsBanned, u.Session, u.Avatar, u.MicroAvatar, u.Tag, u.Level, u.Score, u.Liked}
   103  }
   104  
   105  // For when users need to see their own data, I've omitted some redundancies and less useful items, so we don't wind up sending them on every request
   106  type MeUser struct {
   107  	ID         int
   108  	Link       string
   109  	Name       string
   110  	Group      int
   111  	Active     bool
   112  	IsMod      bool
   113  	IsSuperMod bool
   114  	IsAdmin    bool
   115  	IsBanned   bool
   116  
   117  	// TODO: Implement these as copies (might already be the case for Perms, but we'll want to look at it's definition anyway)
   118  	//Perms       Perms
   119  	//PluginPerms map[string]bool
   120  
   121  	S           string // Session
   122  	Avatar      string
   123  	MicroAvatar string
   124  	Tag         string
   125  	Level       int
   126  	Score       int
   127  	Liked       int
   128  }
   129  
   130  type UserStmts struct {
   131  	activate    *sql.Stmt
   132  	changeGroup *sql.Stmt
   133  	delete      *sql.Stmt
   134  	setAvatar   *sql.Stmt
   135  	setName     *sql.Stmt
   136  	update      *sql.Stmt
   137  
   138  	// TODO: Split these into a sub-struct
   139  	incScore         *sql.Stmt
   140  	incPosts         *sql.Stmt
   141  	incBigposts      *sql.Stmt
   142  	incMegaposts     *sql.Stmt
   143  	incPostStats     *sql.Stmt
   144  	incBigpostStats  *sql.Stmt
   145  	incMegapostStats *sql.Stmt
   146  	incLiked         *sql.Stmt
   147  	incTopics        *sql.Stmt
   148  	updateLevel      *sql.Stmt
   149  	resetStats       *sql.Stmt
   150  	setStats         *sql.Stmt
   151  
   152  	decLiked      *sql.Stmt
   153  	updateLastIP  *sql.Stmt
   154  	updatePrivacy *sql.Stmt
   155  
   156  	setPassword *sql.Stmt
   157  
   158  	scheduleAvatarResize *sql.Stmt
   159  
   160  	deletePosts            *sql.Stmt
   161  	deleteProfilePosts     *sql.Stmt
   162  	deleteReplyPosts       *sql.Stmt
   163  	getLikedRepliesOfTopic *sql.Stmt
   164  	getAttachmentsOfTopic  *sql.Stmt
   165  	getAttachmentsOfTopic2 *sql.Stmt
   166  	getRepliesOfTopic      *sql.Stmt
   167  }
   168  
   169  var userStmts UserStmts
   170  
   171  func init() {
   172  	DbInits.Add(func(acc *qgen.Accumulator) error {
   173  		u, w := "users", "uid=?"
   174  		set := func(s string) *sql.Stmt {
   175  			return acc.Update(u).Set(s).Where(w).Prepare()
   176  		}
   177  		userStmts = UserStmts{
   178  			activate:    set("active=1"),
   179  			changeGroup: set("group=?"), // TODO: Implement user_count for users_groups here
   180  			delete:      acc.Delete(u).Where(w).Prepare(),
   181  			setAvatar:   set("avatar=?"),
   182  			setName:     set("name=?"),
   183  			update:      set("name=?,email=?,group=?"), // TODO: Implement user_count for users_groups on things which use this
   184  
   185  			// Stat Statements
   186  			// TODO: Do +0 to avoid having as many statements?
   187  			incScore:         set("score=score+?"),
   188  			incPosts:         set("posts=posts+?"),
   189  			incBigposts:      set("posts=posts+?,bigposts=bigposts+?"),
   190  			incMegaposts:     set("posts=posts+?,bigposts=bigposts+?,megaposts=megaposts+?"),
   191  			incPostStats:     set("posts=posts+?,score=score+?,level=?"),
   192  			incBigpostStats:  set("posts=posts+?,bigposts=bigposts+?,score=score+?,level=?"),
   193  			incMegapostStats: set("posts=posts+?,bigposts=bigposts+?,megaposts=megaposts+?,score=score+?,level=?"),
   194  			incTopics:        set("topics=topics+?"),
   195  			updateLevel:      set("level=?"),
   196  			resetStats:       set("score=0,posts=0,bigposts=0,megaposts=0,topics=0,level=0"),
   197  			setStats:         set("score=?,posts=?,bigposts=?,megaposts=?,topics=?,level=?"),
   198  
   199  			incLiked: set("liked=liked+?,lastLiked=UTC_TIMESTAMP()"),
   200  			decLiked: set("liked=liked-?"),
   201  			//recalcLastLiked: acc...
   202  			updateLastIP:  set("last_ip=?"),
   203  			updatePrivacy: set("profile_comments=?,enable_embeds=?"),
   204  
   205  			setPassword: set("password=?,salt=?"),
   206  
   207  			scheduleAvatarResize: acc.Insert("users_avatar_queue").Columns("uid").Fields("?").Prepare(),
   208  
   209  			// Delete All Posts Statements
   210  			deletePosts:            acc.Select("topics").Columns("tid,parentID,postCount,poll").Where("createdBy=?").Prepare(),
   211  			deleteProfilePosts:     acc.Select("users_replies").Columns("rid,uid").Where("createdBy=?").Prepare(),
   212  			deleteReplyPosts:       acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(),
   213  			getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").Prepare(),
   214  			getAttachmentsOfTopic:  acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='topics'").Prepare(),
   215  			getAttachmentsOfTopic2: acc.Select("attachments").Columns("attachID").Where("extra=? AND originTable='replies'").Prepare(),
   216  			getRepliesOfTopic:      acc.Select("replies").Columns("words").Where("createdBy!=? AND tid=?").Prepare(),
   217  		}
   218  		return acc.FirstError()
   219  	})
   220  }
   221  
   222  func (u *User) Init() {
   223  	// TODO: Let admins configure the minimum default?
   224  	if u.Privacy.ShowComments < 1 {
   225  		u.Privacy.ShowComments = 1
   226  	}
   227  	u.Avatar, u.MicroAvatar = BuildAvatar(u.ID, u.RawAvatar)
   228  	u.Link = BuildProfileURL(NameToSlug(u.Name), u.ID)
   229  	u.Tag = Groups.DirtyGet(u.Group).Tag
   230  	u.InitPerms()
   231  }
   232  
   233  // TODO: Refactor this idiom into something shorter, maybe with a NullUserCache when one isn't set?
   234  func (u *User) CacheRemove() {
   235  	if uc := Users.GetCache(); uc != nil {
   236  		uc.Remove(u.ID)
   237  	}
   238  	TopicListThaw.Thaw()
   239  }
   240  
   241  func (u *User) Ban(dur time.Duration, issuedBy int) error {
   242  	return u.ScheduleGroupUpdate(BanGroup, issuedBy, dur)
   243  }
   244  
   245  func (u *User) Unban() error {
   246  	return u.RevertGroupUpdate()
   247  }
   248  
   249  func (u *User) deleteScheduleGroupTx(tx *sql.Tx) error {
   250  	deleteScheduleGroupStmt, e := qgen.Builder.SimpleDeleteTx(tx, "users_groups_scheduler", "uid=?")
   251  	if e != nil {
   252  		return e
   253  	}
   254  	_, e = deleteScheduleGroupStmt.Exec(u.ID)
   255  	return e
   256  }
   257  
   258  func (u *User) setTempGroupTx(tx *sql.Tx, tempGroup int) error {
   259  	setTempGroupStmt, e := qgen.Builder.SimpleUpdateTx(tx, "users", "temp_group=?", "uid=?")
   260  	if e != nil {
   261  		return e
   262  	}
   263  	_, e = setTempGroupStmt.Exec(tempGroup, u.ID)
   264  	return e
   265  }
   266  
   267  // Make this more stateless?
   268  func (u *User) ScheduleGroupUpdate(gid, issuedBy int, dur time.Duration) error {
   269  	var temp bool
   270  	if dur.Nanoseconds() != 0 {
   271  		temp = true
   272  	}
   273  	revertAt := time.Now().Add(dur)
   274  
   275  	tx, e := qgen.Builder.Begin()
   276  	if e != nil {
   277  		return e
   278  	}
   279  	defer tx.Rollback()
   280  
   281  	e = u.deleteScheduleGroupTx(tx)
   282  	if e != nil {
   283  		return e
   284  	}
   285  
   286  	createScheduleGroupTx, e := qgen.Builder.SimpleInsertTx(tx, "users_groups_scheduler", "uid,set_group,issued_by,issued_at,revert_at,temporary", "?,?,?,UTC_TIMESTAMP(),?,?")
   287  	if e != nil {
   288  		return e
   289  	}
   290  	_, e = createScheduleGroupTx.Exec(u.ID, gid, issuedBy, revertAt, temp)
   291  	if e != nil {
   292  		return e
   293  	}
   294  
   295  	e = u.setTempGroupTx(tx, gid)
   296  	if e != nil {
   297  		return e
   298  	}
   299  	e = tx.Commit()
   300  
   301  	u.CacheRemove()
   302  	return e
   303  }
   304  
   305  func (u *User) RevertGroupUpdate() error {
   306  	tx, e := qgen.Builder.Begin()
   307  	if e != nil {
   308  		return e
   309  	}
   310  	defer tx.Rollback()
   311  
   312  	e = u.deleteScheduleGroupTx(tx)
   313  	if e != nil {
   314  		return e
   315  	}
   316  
   317  	e = u.setTempGroupTx(tx, 0)
   318  	if e != nil {
   319  		return e
   320  	}
   321  	e = tx.Commit()
   322  
   323  	u.CacheRemove()
   324  	return e
   325  }
   326  
   327  // TODO: Use a transaction here
   328  // ? - Add a Deactivate method? Not really needed, if someone's been bad you could do a ban, I guess it might be useful, if someone says that email x isn't actually owned by the user in question?
   329  func (u *User) Activate() (e error) {
   330  	_, e = userStmts.activate.Exec(u.ID)
   331  	if e != nil {
   332  		return e
   333  	}
   334  	_, e = userStmts.changeGroup.Exec(Config.DefaultGroup, u.ID)
   335  	u.CacheRemove()
   336  	return e
   337  }
   338  
   339  // TODO: Write tests for this
   340  // TODO: Delete this user's content too?
   341  // TODO: Expose this to the admin?
   342  func (u *User) Delete() error {
   343  	_, e := userStmts.delete.Exec(u.ID)
   344  	u.CacheRemove()
   345  	return e
   346  }
   347  
   348  // TODO: dismiss-event
   349  func (u *User) DeletePosts() error {
   350  	rows, err := userStmts.deletePosts.Query(u.ID)
   351  	if err != nil {
   352  		return err
   353  	}
   354  	defer rows.Close()
   355  	defer TopicListThaw.Thaw()
   356  	defer u.CacheRemove()
   357  
   358  	updatedForums := make(map[int]int) // forum[count]
   359  	tc := Topics.GetCache()
   360  	umap := make(map[int]struct{})
   361  	for rows.Next() {
   362  		var tid, parentID, postCount, poll int
   363  		err := rows.Scan(&tid, &parentID, &postCount, &poll)
   364  		if err != nil {
   365  			return err
   366  		}
   367  		// TODO: Clear reply cache too
   368  		_, err = topicStmts.delete.Exec(tid)
   369  		if tc != nil {
   370  			tc.Remove(tid)
   371  		}
   372  		if err != nil {
   373  			return err
   374  		}
   375  		updatedForums[parentID] = updatedForums[parentID] + 1
   376  
   377  		_, err = topicStmts.deleteLikesForTopic.Exec(tid)
   378  		if err != nil {
   379  			return err
   380  		}
   381  		err = handleTopicAttachments(tid)
   382  		if err != nil {
   383  			return err
   384  		}
   385  		if postCount > 1 {
   386  			err = handleLikedTopicReplies(tid)
   387  			if err != nil {
   388  				return err
   389  			}
   390  			err = handleTopicReplies(umap, u.ID, tid)
   391  			if err != nil {
   392  				return err
   393  			}
   394  			_, err = topicStmts.deleteReplies.Exec(tid)
   395  			if err != nil {
   396  				return err
   397  			}
   398  		}
   399  		err = Subscriptions.DeleteResource(tid, "topic")
   400  		if err != nil {
   401  			return err
   402  		}
   403  		_, err = topicStmts.deleteActivity.Exec(tid)
   404  		if err != nil {
   405  			return err
   406  		}
   407  		if poll > 0 {
   408  			err = (&Poll{ID: poll}).Delete()
   409  			if err != nil {
   410  				return err
   411  			}
   412  		}
   413  	}
   414  	if err = rows.Err(); err != nil {
   415  		return err
   416  	}
   417  	err = u.ResetPostStats()
   418  	if err != nil {
   419  		return err
   420  	}
   421  	for uid, _ := range umap {
   422  		err = (&User{ID: uid}).RecalcPostStats()
   423  		if err != nil {
   424  			return err
   425  		}
   426  	}
   427  	for fid, count := range updatedForums {
   428  		err := Forums.RemoveTopics(fid, count)
   429  		if err != nil && err != ErrNoRows {
   430  			return err
   431  		}
   432  	}
   433  
   434  	rows, err = userStmts.deleteProfilePosts.Query(u.ID)
   435  	if err != nil {
   436  		return err
   437  	}
   438  	defer rows.Close()
   439  
   440  	for rows.Next() {
   441  		var rid, uid int
   442  		err := rows.Scan(&rid, &uid)
   443  		if err != nil {
   444  			return err
   445  		}
   446  		_, err = profileReplyStmts.delete.Exec(rid)
   447  		if err != nil {
   448  			return err
   449  		}
   450  		// TODO: Optimise this
   451  		// TODO: dismiss-event
   452  		err = Activity.DeleteByParamsExtra("reply", uid, "user", strconv.Itoa(rid))
   453  		if err != nil {
   454  			return err
   455  		}
   456  	}
   457  	if err = rows.Err(); err != nil {
   458  		return err
   459  	}
   460  
   461  	rows, err = userStmts.deleteReplyPosts.Query(u.ID)
   462  	if err != nil {
   463  		return err
   464  	}
   465  	defer rows.Close()
   466  
   467  	rc := Rstore.GetCache()
   468  	for rows.Next() {
   469  		var rid, tid int
   470  		err := rows.Scan(&rid, &tid)
   471  		if err != nil {
   472  			return err
   473  		}
   474  		_, err = replyStmts.delete.Exec(rid)
   475  		if err != nil {
   476  			return err
   477  		}
   478  		// TODO: Move this bit to *Topic
   479  		_, err = replyStmts.removeRepliesFromTopic.Exec(1, tid)
   480  		if err != nil {
   481  			return err
   482  		}
   483  		_, err = replyStmts.updateTopicReplies.Exec(tid)
   484  		if err != nil {
   485  			return err
   486  		}
   487  		_, err = replyStmts.updateTopicReplies2.Exec(tid)
   488  		if tc != nil {
   489  			tc.Remove(tid)
   490  		}
   491  		_ = rc.Remove(rid)
   492  		if err != nil {
   493  			return err
   494  		}
   495  
   496  		_, err = replyStmts.deleteLikesForReply.Exec(rid)
   497  		if err != nil {
   498  			return err
   499  		}
   500  		err = Activity.DeleteByParamsExtra("reply", tid, "topic", strconv.Itoa(rid))
   501  		if err != nil {
   502  			return err
   503  		}
   504  		_, err = replyStmts.deleteActivitySubs.Exec(rid)
   505  		if err != nil {
   506  			return err
   507  		}
   508  		_, err = replyStmts.deleteActivity.Exec(rid)
   509  		if err != nil {
   510  			return err
   511  		}
   512  		// TODO: Restructure alerts so we can delete the "x replied to topic" ones too.
   513  	}
   514  	return rows.Err()
   515  }
   516  
   517  func (u *User) bindStmt(stmt *sql.Stmt, params ...interface{}) (e error) {
   518  	params = append(params, u.ID)
   519  	_, e = stmt.Exec(params...)
   520  	u.CacheRemove()
   521  	return e
   522  }
   523  
   524  func (u *User) ChangeName(name string) error {
   525  	return u.bindStmt(userStmts.setName, name)
   526  }
   527  
   528  func (u *User) ChangeAvatar(avatar string) error {
   529  	return u.bindStmt(userStmts.setAvatar, avatar)
   530  }
   531  
   532  // TODO: Abstract this with an interface so we can scale this with an actual dedicated queue in a real cluster
   533  func (u *User) ScheduleAvatarResize() (e error) {
   534  	_, e = userStmts.scheduleAvatarResize.Exec(u.ID)
   535  	if e != nil {
   536  		// TODO: Do a more generic check so that we're not as tied to MySQL
   537  		me, ok := e.(*mysql.MySQLError)
   538  		if !ok {
   539  			return e
   540  		}
   541  		// If it's just telling us that the item already exists in the database, then we can ignore it, as it doesn't matter if it's this call or another which schedules the item in the queue
   542  		if me.Number != 1062 {
   543  			return e
   544  		}
   545  	}
   546  	return nil
   547  }
   548  
   549  func (u *User) ChangeGroup(group int) error {
   550  	return u.bindStmt(userStmts.changeGroup, group)
   551  }
   552  
   553  func (u *User) GetIP() string {
   554  	spl := strings.Split(u.LastIP, "-")
   555  	return spl[len(spl)-1]
   556  }
   557  
   558  // ! Only updates the database not the *User for safety reasons
   559  func (u *User) UpdateIP(ip string) error {
   560  	_, e := userStmts.updateLastIP.Exec(ip, u.ID)
   561  	if uc := Users.GetCache(); uc != nil {
   562  		uc.Remove(u.ID)
   563  	}
   564  	return e
   565  }
   566  
   567  //var ErrMalformedInteger = errors.New("malformed integer")
   568  var ErrProfileCommentsOutOfBounds = errors.New("profile_comments must be an integer between -1 and 4")
   569  var ErrEnableEmbedsOutOfBounds = errors.New("enable_embeds must be -1, 0 or 1")
   570  
   571  /*func (u *User) UpdatePrivacyS(sProfileComments, sEnableEmbeds string) error {
   572  	return u.UpdatePrivacy(profileComments, enableEmbeds)
   573  }*/
   574  
   575  func (u *User) UpdatePrivacy(profileComments, enableEmbeds int) error {
   576  	if profileComments < -1 || profileComments > 4 {
   577  		return ErrProfileCommentsOutOfBounds
   578  	}
   579  	if enableEmbeds < -1 || enableEmbeds > 1 {
   580  		return ErrEnableEmbedsOutOfBounds
   581  	}
   582  	_, e := userStmts.updatePrivacy.Exec(profileComments, enableEmbeds, u.ID)
   583  	if uc := Users.GetCache(); uc != nil {
   584  		uc.Remove(u.ID)
   585  	}
   586  	return e
   587  }
   588  
   589  func (u *User) Update(name, email string, group int) (err error) {
   590  	return u.bindStmt(userStmts.update, name, email, group)
   591  }
   592  
   593  func (u *User) IncreasePostStats(wcount int, topic bool) (err error) {
   594  	baseScore := 1
   595  	if topic {
   596  		_, err = userStmts.incTopics.Exec(1, u.ID)
   597  		if err != nil {
   598  			return err
   599  		}
   600  		baseScore = 2
   601  	}
   602  
   603  	settings := SettingBox.Load().(SettingMap)
   604  	var mod, level int
   605  	if wcount >= settings["megapost_min_words"].(int) {
   606  		mod = 4
   607  		level = GetLevel(u.Score + baseScore + mod)
   608  		_, err = userStmts.incMegapostStats.Exec(1, 1, 1, baseScore+mod, level, u.ID)
   609  	} else if wcount >= settings["bigpost_min_words"].(int) {
   610  		mod = 1
   611  		level = GetLevel(u.Score + baseScore + mod)
   612  		_, err = userStmts.incBigpostStats.Exec(1, 1, baseScore+mod, level, u.ID)
   613  	} else {
   614  		level = GetLevel(u.Score + baseScore + mod)
   615  		_, err = userStmts.incPostStats.Exec(1, baseScore+mod, level, u.ID)
   616  	}
   617  	if err != nil {
   618  		return err
   619  	}
   620  	err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1, u.CreatedAt)
   621  	u.CacheRemove()
   622  	return err
   623  }
   624  
   625  func (u *User) countf(stmt *sql.Stmt) (count int) {
   626  	e := stmt.QueryRow().Scan(&count)
   627  	if e != nil {
   628  		LogError(e)
   629  	}
   630  	return count
   631  }
   632  
   633  func (u *User) RecalcPostStats() error {
   634  	var score int
   635  	tcount := Topics.CountUser(u.ID)
   636  	rcount := Rstore.CountUser(u.ID)
   637  	//log.Print("tcount:", tcount)
   638  	//log.Print("rcount:", rcount)
   639  	score += tcount * 2
   640  	score += rcount
   641  
   642  	var tmega, tbig, rmega, rbig int
   643  	if tcount > 0 {
   644  		tmega = Topics.CountMegaUser(u.ID)
   645  		score += tmega * 3
   646  		tbig := Topics.CountBigUser(u.ID)
   647  		score += tbig
   648  	}
   649  	if rcount > 0 {
   650  		rmega = Rstore.CountMegaUser(u.ID)
   651  		score += rmega * 3
   652  		rbig = Rstore.CountBigUser(u.ID)
   653  		score += rbig
   654  	}
   655  
   656  	_, err := userStmts.setStats.Exec(score, tcount+rcount, tbig+rbig, tmega+rmega, tcount, GetLevel(score), u.ID)
   657  	u.CacheRemove()
   658  	return err
   659  }
   660  
   661  func (u *User) DecreasePostStats(wcount int, topic bool) (err error) {
   662  	baseScore := -1
   663  	if topic {
   664  		_, err = userStmts.incTopics.Exec(-1, u.ID)
   665  		if err != nil {
   666  			return err
   667  		}
   668  		baseScore = -2
   669  	}
   670  
   671  	// TODO: Use a transaction to prevent level desyncs?
   672  	var mod int
   673  	settings := SettingBox.Load().(SettingMap)
   674  	if wcount >= settings["megapost_min_words"].(int) {
   675  		mod = 4
   676  		_, err = userStmts.incMegapostStats.Exec(-1, -1, -1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
   677  	} else if wcount >= settings["bigpost_min_words"].(int) {
   678  		mod = 1
   679  		_, err = userStmts.incBigpostStats.Exec(-1, -1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
   680  	} else {
   681  		_, err = userStmts.incPostStats.Exec(-1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
   682  	}
   683  	u.CacheRemove()
   684  	return err
   685  }
   686  
   687  func (u *User) ResetPostStats() error {
   688  	_, err := userStmts.resetStats.Exec(u.ID)
   689  	u.CacheRemove()
   690  	return err
   691  }
   692  
   693  // Copy gives you a non-pointer concurrency safe copy of the user
   694  func (u *User) Copy() User {
   695  	return *u
   696  }
   697  
   698  // TODO: Write unit tests for this
   699  func (u *User) InitPerms() {
   700  	if u.TempGroup != 0 {
   701  		u.Group = u.TempGroup
   702  	}
   703  
   704  	group := Groups.DirtyGet(u.Group)
   705  	if u.IsSuperAdmin {
   706  		u.Perms = AllPerms
   707  		u.PluginPerms = AllPluginPerms
   708  	} else {
   709  		u.Perms = group.Perms
   710  		u.PluginPerms = group.PluginPerms
   711  	}
   712  	/*if len(group.CanSee) == 0 {
   713  		panic("should not be zero")
   714  	}*/
   715  
   716  	u.IsAdmin = u.IsSuperAdmin || group.IsAdmin
   717  	u.IsSuperMod = u.IsAdmin || group.IsMod
   718  	u.IsMod = u.IsSuperMod
   719  	u.IsBanned = group.IsBanned
   720  	if u.IsBanned && u.IsSuperMod {
   721  		u.IsBanned = false
   722  	}
   723  }
   724  
   725  // TODO: Write unit tests for this
   726  func InitPerms2(group int, superAdmin bool, tempGroup int) (perms *Perms, admin, superMod, banned bool) {
   727  	if tempGroup != 0 {
   728  		group = tempGroup
   729  	}
   730  
   731  	g := Groups.DirtyGet(group)
   732  	if superAdmin {
   733  		perms = &AllPerms
   734  	} else {
   735  		perms = &g.Perms
   736  	}
   737  
   738  	admin = superAdmin || g.IsAdmin
   739  	superMod = admin || g.IsMod
   740  	banned = g.IsBanned
   741  	if banned && superMod {
   742  		banned = false
   743  	}
   744  	return perms, admin, superMod, banned
   745  }
   746  
   747  // TODO: Write tests
   748  // TODO: Implement and use this
   749  // TODO: Implement friends
   750  func PrivacyAllowMessage(pu, u *User) (canMsg bool) {
   751  	switch pu.Privacy.AllowMessage {
   752  	case 4: // Unused
   753  		canMsg = false
   754  	case 3: // mods
   755  		canMsg = u.IsSuperMod
   756  	//case 2: // friends
   757  	case 1: // registered
   758  		canMsg = true
   759  	default: // 0
   760  		canMsg = true
   761  	}
   762  	return canMsg
   763  }
   764  
   765  // TODO: Implement friend system
   766  func PrivacyCommentsShow(pu, u *User) (showComments bool) {
   767  	switch pu.Privacy.ShowComments {
   768  	case 5: // Unused
   769  		showComments = false
   770  	case 4: // Self
   771  		showComments = u.ID == pu.ID
   772  	case 3: // friends
   773  		showComments = u.ID == pu.ID
   774  	case 2: // registered
   775  		showComments = u.Loggedin
   776  	case 1: // public
   777  		showComments = true
   778  	default: // 0
   779  		showComments = true
   780  	}
   781  	return showComments
   782  }
   783  
   784  var guestAvatar GuestAvatar
   785  
   786  type GuestAvatar struct {
   787  	Normal string
   788  	Micro  string
   789  }
   790  
   791  func buildNoavatar(uid, width int) string {
   792  	if !Config.DisableNoavatarRange {
   793  		// TODO: Find a faster algorithm
   794  		l := func(max int) {
   795  			for uid > max {
   796  				uid -= max
   797  			}
   798  		}
   799  		l(50000)
   800  		l(5000)
   801  		l(500)
   802  		l(50)
   803  		l(10)
   804  	}
   805  	if !Config.DisableDefaultNoavatar && uid < 11 {
   806  		/*if uid < 6 {
   807  			if width == 200 {
   808  				return noavatarCache200Avif[uid]
   809  			} else if width == 48 {
   810  				return noavatarCache48Avif[uid]
   811  			}
   812  			return StaticFiles.Prefix + "n" + strconv.Itoa(uid) + "-" + strconv.Itoa(width) + ".avif?i=0"
   813  		} else */if width == 200 {
   814  			return noavatarCache200[uid]
   815  		} else if width == 48 {
   816  			return noavatarCache48[uid]
   817  		}
   818  		return StaticFiles.Prefix + "n" + strconv.Itoa(uid) + "-" + strconv.Itoa(width) + ".png?i=0"
   819  	}
   820  	// ? - Add a prefix setting to make this faster?
   821  	return strings.Replace(strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1), "{width}", strconv.Itoa(width), 1)
   822  }
   823  
   824  // ? - Make this part of *User?
   825  // TODO: Write tests for this
   826  func BuildAvatar(uid int, avatar string) (normalAvatar, microAvatar string) {
   827  	if avatar == "" {
   828  		if uid == 0 {
   829  			return guestAvatar.Normal, guestAvatar.Micro
   830  		}
   831  		return buildNoavatar(uid, 200), buildNoavatar(uid, 48)
   832  	}
   833  	if avatar[0] == '.' {
   834  		if avatar[1] == '.' {
   835  			normalAvatar = Config.AvatarResBase + "avatar_" + strconv.Itoa(uid) + "_tmp" + avatar[1:]
   836  			return normalAvatar, normalAvatar
   837  		}
   838  		normalAvatar = Config.AvatarResBase + "avatar_" + strconv.Itoa(uid) + avatar
   839  		return normalAvatar, normalAvatar
   840  	}
   841  	return avatar, avatar
   842  }
   843  
   844  // TODO: Move this to *User
   845  func SetPassword(uid int, password string) error {
   846  	hashedPassword, salt, err := GeneratePassword(password)
   847  	if err != nil {
   848  		return err
   849  	}
   850  	_, err = userStmts.setPassword.Exec(hashedPassword, salt, uid)
   851  	return err
   852  }
   853  
   854  // TODO: Write units tests for this
   855  func wordsToScore(wcount int, topic bool) (score int) {
   856  	if topic {
   857  		score = 2
   858  	} else {
   859  		score = 1
   860  	}
   861  	settings := SettingBox.Load().(SettingMap)
   862  	if wcount >= settings["megapost_min_words"].(int) {
   863  		score += 4
   864  	} else if wcount >= settings["bigpost_min_words"].(int) {
   865  		score++
   866  	}
   867  	return score
   868  }
   869  
   870  // For use in tests and to help generate dummy users for forums which don't have last posters
   871  func BlankUser() *User {
   872  	return new(User)
   873  }
   874  
   875  // TODO: Write unit tests for this
   876  func BuildProfileURL(slug string, uid int) string {
   877  	if slug == "" || !Config.BuildSlugs {
   878  		return "/user/" + strconv.Itoa(uid)
   879  	}
   880  	return "/user/" + slug + "." + strconv.Itoa(uid)
   881  }
   882  
   883  func BuildProfileURLSb(sb *strings.Builder, slug string, uid int) {
   884  	if slug == "" || !Config.BuildSlugs {
   885  		sb.Grow(6 + 1)
   886  		sb.WriteString("/user/")
   887  		sb.WriteString(strconv.Itoa(uid))
   888  		return
   889  	}
   890  	sb.Grow(7 + 1 + len(slug))
   891  	sb.WriteString("/user/")
   892  	sb.WriteString(slug)
   893  	sb.WriteRune('.')
   894  	sb.WriteString(strconv.Itoa(uid))
   895  }