1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace Database\Factories;
9
10use App\Libraries\Fulfillments\ApplySupporterTag;
11use App\Libraries\User\CountryChangeTarget;
12use App\Models\Beatmap;
13use App\Models\Country;
14use App\Models\User;
15use App\Models\UserAccountHistory;
16use App\Models\UserCountryHistory;
17use App\Models\UserStatistics\Model as UserStatisticsModel;
18
19class UserFactory extends Factory
20{
21 const DEFAULT_PASSWORD = 'password';
22
23 public static function createRecentCountryHistory(User $user, ?string $country, ?int $months): void
24 {
25 $months ??= CountryChangeTarget::minMonths();
26 $country ??= Country::factory()->create()->getKey();
27 $currentMonth = CountryChangeTarget::currentMonth();
28 $userId = $user->getKey();
29 for ($i = 0; $i < $months; $i++) {
30 UserCountryHistory::create([
31 'country_acronym' => $country,
32 'user_id' => $userId,
33 'year_month' => $currentMonth->subMonths($i),
34 ]);
35 }
36 }
37
38 private static function defaultPasswordHash()
39 {
40 static $password;
41
42 return $password ??= password_hash(md5(static::DEFAULT_PASSWORD), PASSWORD_BCRYPT);
43 }
44
45 protected $model = User::class;
46
47 public function configure()
48 {
49 return $this->afterCreating(function (User $user) {
50 $user->addToGroup(app('groups')->byIdOrFail($user->group_id));
51 });
52 }
53
54 public function definition(): array
55 {
56 // Get or create a random country
57 $countryAcronym = fn () => Country::inRandomOrder()->first() ?? Country::factory()->create();
58
59 return [
60 'username' => fn () => substr(str_replace('.', ' ', $this->faker->unique()->userName()), 0, 15),
61 'user_password' => static::defaultPasswordHash(),
62 'user_email' => fn () => $this->faker->unique()->safeEmail(),
63 'group_id' => fn () => app('groups')->byIdentifier('default'),
64 'user_lastvisit' => time(),
65 'user_posts' => rand(1, 500),
66 'user_warnings' => 0,
67 'user_type' => 0,
68 'osu_kudosavailable' => rand(1, 500),
69 'osu_kudosdenied' => rand(1, 500),
70 'osu_kudostotal' => rand(1, 500),
71 'country_acronym' => $countryAcronym,
72 'osu_playstyle' => [array_rand(User::PLAYSTYLES)],
73 'user_website' => 'http://www.google.com/',
74 'user_twitter' => 'ppy',
75 'user_permissions' => '',
76 'user_interests' => fn () => mb_substr($this->faker->bs(), 0, 30),
77 'user_occ' => fn () => mb_substr($this->faker->catchPhrase(), 0, 30),
78 'user_sig' => fn () => $this->faker->realText(155),
79 'user_from' => fn () => mb_substr($this->faker->country(), 0, 25),
80 'user_regdate' => fn () => $this->faker->dateTimeBetween('-6 years'),
81 ];
82 }
83
84 // convenience for dataProviders so null checks don't have to be called when creating with named state.
85 public function default()
86 {
87 return $this;
88 }
89
90 public function restricted()
91 {
92 return $this
93 ->state(['user_warnings' => 1])
94 ->has(UserAccountHistory::factory()->restriction(), 'accountHistories');
95 }
96
97 public function silenced()
98 {
99 return $this->has(UserAccountHistory::factory()->silence(), 'accountHistories');
100 }
101
102 public function supporter()
103 {
104 return $this->state([
105 'osu_subscriber' => true,
106 'osu_subscriptionexpiry' => ApplySupporterTag::addDuration(now()->floorSecond(), 1),
107 ]);
108 }
109
110 public function tournamentBanned()
111 {
112 return $this->has(UserAccountHistory::factory()->tournamentBan(), 'accountHistories');
113 }
114
115 public function withGroup(?string $groupIdentifier, ?array $playmodes = null)
116 {
117 if ($groupIdentifier === null) {
118 return $this;
119 }
120
121 $group = app('groups')->byIdentifier($groupIdentifier);
122
123 return $this
124 ->state(['group_id' => $group])
125 ->afterCreating(function (User $user) use ($group, $playmodes) {
126 $user->addToGroup($group);
127
128 if ($playmodes !== null) {
129 if (!$group->has_playmodes) {
130 $group->update(['has_playmodes' => true]);
131
132 // TODO: This shouldn't have to be called here, since it's already
133 // called by `Group::afterCommit`, but `Group::afterCommit` isn't
134 // running in tests when creating/saving `Group`s.
135 app('groups')->resetMemoized();
136 }
137
138 $user->findUserGroup($group, true)->update(['playmodes' => $playmodes]);
139 }
140 });
141 }
142
143 public function withNote()
144 {
145 return $this->has(UserAccountHistory::factory(), 'accountHistories');
146 }
147
148 public function withPlays(?int $count = null, ?string $ruleset = 'osu'): static
149 {
150 $state = [
151 'playcount' => $count ?? $GLOBALS['cfg']['osu']['user']['min_plays_for_posting'],
152 ];
153
154 $ret = $this->has(
155 UserStatisticsModel::getClass($ruleset)::factory()->state($state),
156 'statistics'.studly_case($ruleset),
157 );
158
159 foreach (Beatmap::VARIANTS[$ruleset] ?? [] as $variant) {
160 $ret = $ret->has(
161 UserStatisticsModel::getClass($ruleset, $variant)::factory()->state($state),
162 'statistics'.studly_case("{$ruleset}_{$variant}"),
163 );
164 }
165
166 return $ret;
167 }
168}