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 }