AccountService.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <?php
  2. namespace App\Services;
  3. use App\Models\UserDomainBlock;
  4. use App\Profile;
  5. use App\Status;
  6. use App\Transformer\Api\AccountTransformer;
  7. use App\User;
  8. use App\UserSetting;
  9. use Cache;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Str;
  12. use League\Fractal;
  13. use League\Fractal\Serializer\ArraySerializer;
  14. use NumberFormatter;
  15. class AccountService
  16. {
  17. const CACHE_KEY = 'pf:services:account:';
  18. const CACHE_PF_ACCT_SETTINGS_KEY = 'pf:services:account-settings:';
  19. public static function get($id, $softFail = false)
  20. {
  21. $res = Cache::remember(self::CACHE_KEY.$id, 43200, function () use ($id) {
  22. $fractal = new Fractal\Manager;
  23. $fractal->setSerializer(new ArraySerializer);
  24. $profile = Profile::find($id);
  25. if (! $profile || $profile->status === 'delete') {
  26. return null;
  27. }
  28. $resource = new Fractal\Resource\Item($profile, new AccountTransformer);
  29. return $fractal->createData($resource)->toArray();
  30. });
  31. if (! $res) {
  32. return $softFail ? null : abort(404);
  33. }
  34. return $res;
  35. }
  36. public static function getMastodon($id, $softFail = false)
  37. {
  38. $account = self::get($id, $softFail);
  39. if (! $account) {
  40. return null;
  41. }
  42. if (config('exp.emc') == false) {
  43. return $account;
  44. }
  45. unset(
  46. $account['header_bg'],
  47. $account['is_admin'],
  48. $account['last_fetched_at'],
  49. $account['local'],
  50. $account['location'],
  51. $account['note_text'],
  52. $account['pronouns'],
  53. $account['website']
  54. );
  55. $account['avatar_static'] = $account['avatar'];
  56. $account['bot'] = false;
  57. $account['emojis'] = [];
  58. $account['fields'] = [];
  59. $account['header'] = url('/storage/headers/missing.png');
  60. $account['header_static'] = url('/storage/headers/missing.png');
  61. $account['last_status_at'] = null;
  62. return $account;
  63. }
  64. public static function del($id)
  65. {
  66. Cache::forget('pf:activitypub:user-object:by-id:'.$id);
  67. return Cache::forget(self::CACHE_KEY.$id);
  68. }
  69. public static function settings($id)
  70. {
  71. return Cache::remember('profile:compose:settings:'.$id, 604800, function () use ($id) {
  72. $settings = UserSetting::whereUserId($id)->first();
  73. if (! $settings) {
  74. return self::defaultSettings();
  75. }
  76. return collect($settings)
  77. ->filter(function ($item, $key) {
  78. return in_array($key, array_keys(self::defaultSettings())) == true;
  79. })
  80. ->map(function ($item, $key) {
  81. if ($key == 'compose_settings') {
  82. $cs = self::defaultSettings()['compose_settings'];
  83. $ms = is_array($item) ? $item : [];
  84. return array_merge($cs, $ms);
  85. }
  86. if ($key == 'other') {
  87. $other = self::defaultSettings()['other'];
  88. $mo = is_array($item) ? $item : [];
  89. return array_merge($other, $mo);
  90. }
  91. return $item;
  92. });
  93. });
  94. }
  95. public static function getAccountSettings($pid)
  96. {
  97. $key = self::CACHE_PF_ACCT_SETTINGS_KEY.$pid;
  98. return Cache::remember($key, 14400, function () use ($pid) {
  99. $user = User::with('profile')->whereProfileId($pid)->whereNull('status')->first();
  100. if (! $user) {
  101. return [];
  102. }
  103. $settings = $user->settings;
  104. $other = array_merge(self::defaultSettings()['other'], $settings->other ?? []);
  105. return [
  106. 'reduce_motion' => (bool) $settings->reduce_motion,
  107. 'high_contrast_mode' => (bool) $settings->high_contrast_mode,
  108. 'video_autoplay' => (bool) $settings->video_autoplay,
  109. 'media_descriptions' => (bool) $settings->media_descriptions,
  110. 'crawlable' => (bool) $settings->crawlable,
  111. 'show_profile_follower_count' => (bool) $settings->show_profile_follower_count,
  112. 'show_profile_following_count' => (bool) $settings->show_profile_following_count,
  113. 'public_dm' => (bool) $settings->public_dm,
  114. 'disable_embeds' => (bool) $other['disable_embeds'],
  115. 'show_atom' => (bool) $settings->show_atom,
  116. 'is_suggestable' => (bool) $user->profile->is_suggestable,
  117. 'indexable' => (bool) $user->profile->indexable,
  118. ];
  119. });
  120. }
  121. public static function forgetAccountSettings($pid)
  122. {
  123. return Cache::forget(self::CACHE_PF_ACCT_SETTINGS_KEY.$pid);
  124. }
  125. public static function canEmbed($id)
  126. {
  127. $res = self::getAccountSettings($id);
  128. if (! $res || ! isset($res['disable_embeds'])) {
  129. return false;
  130. }
  131. return ! $res['disable_embeds'];
  132. }
  133. public static function defaultSettings()
  134. {
  135. return [
  136. 'crawlable' => true,
  137. 'public_dm' => false,
  138. 'reduce_motion' => false,
  139. 'high_contrast_mode' => false,
  140. 'video_autoplay' => false,
  141. 'show_profile_follower_count' => true,
  142. 'show_profile_following_count' => true,
  143. 'compose_settings' => [
  144. 'default_scope' => 'public',
  145. 'default_license' => 1,
  146. 'media_descriptions' => false,
  147. ],
  148. 'other' => [
  149. 'advanced_atom' => false,
  150. 'disable_embeds' => false,
  151. 'mutual_mention_notifications' => false,
  152. 'hide_collections' => false,
  153. 'hide_like_counts' => false,
  154. 'hide_groups' => false,
  155. 'hide_stories' => false,
  156. 'disable_cw' => false,
  157. ],
  158. ];
  159. }
  160. public static function syncPostCount($id)
  161. {
  162. $profile = Profile::find($id);
  163. if (! $profile) {
  164. return false;
  165. }
  166. $key = self::CACHE_KEY.'pcs:'.$id;
  167. if (Cache::has($key)) {
  168. return;
  169. }
  170. $count = Status::whereProfileId($id)
  171. ->whereNull(['in_reply_to_id', 'reblog_of_id'])
  172. ->whereIn('scope', ['public', 'unlisted', 'private'])
  173. ->count();
  174. $profile->status_count = $count;
  175. $profile->save();
  176. Cache::put($key, 1, 259200);
  177. return true;
  178. }
  179. public static function usernameToId($username)
  180. {
  181. $key = self::CACHE_KEY.'u2id:'.hash('sha256', $username);
  182. return Cache::remember($key, 14400, function () use ($username) {
  183. $s = Str::of($username);
  184. if ($s->contains('@') && ! $s->startsWith('@')) {
  185. $username = "@{$username}";
  186. }
  187. $profile = DB::table('profiles')
  188. ->whereUsername($username)
  189. ->first();
  190. if (! $profile) {
  191. return null;
  192. }
  193. return (string) $profile->id;
  194. });
  195. }
  196. public static function hiddenFollowers($id)
  197. {
  198. $account = self::get($id, true);
  199. if (! $account || ! isset($account['local']) || $account['local'] == false) {
  200. return false;
  201. }
  202. return Cache::remember('pf:acct:settings:hidden-followers:'.$id, 43200, function () use ($id) {
  203. $user = User::whereProfileId($id)->first();
  204. if (! $user) {
  205. return false;
  206. }
  207. $settings = UserSetting::whereUserId($user->id)->first();
  208. if ($settings) {
  209. return $settings->show_profile_follower_count == false;
  210. }
  211. return false;
  212. });
  213. }
  214. public static function hiddenFollowing($id)
  215. {
  216. $account = self::get($id, true);
  217. if (! $account || ! isset($account['local']) || $account['local'] == false) {
  218. return false;
  219. }
  220. return Cache::remember('pf:acct:settings:hidden-following:'.$id, 43200, function () use ($id) {
  221. $user = User::whereProfileId($id)->first();
  222. if (! $user) {
  223. return false;
  224. }
  225. $settings = UserSetting::whereUserId($user->id)->first();
  226. if ($settings) {
  227. return $settings->show_profile_following_count == false;
  228. }
  229. return false;
  230. });
  231. }
  232. public static function setLastActive($id = false)
  233. {
  234. if (! $id) {
  235. return;
  236. }
  237. $key = 'user:last_active_at:id:'.$id;
  238. if (! Cache::has($key)) {
  239. $user = User::find($id);
  240. if (! $user) {
  241. return;
  242. }
  243. $user->last_active_at = now();
  244. $user->save();
  245. Cache::put($key, 1, 14400);
  246. }
  247. }
  248. public static function blocksDomain($pid, $domain = false)
  249. {
  250. if (! $domain) {
  251. return;
  252. }
  253. return UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->exists();
  254. }
  255. public static function formatNumber($num)
  256. {
  257. if (! $num || $num < 1) {
  258. return '0';
  259. }
  260. $num = intval($num);
  261. $formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
  262. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1);
  263. if ($num >= 1000000000) {
  264. return $formatter->format($num / 1000000000).'B';
  265. } elseif ($num >= 1000000) {
  266. return $formatter->format($num / 1000000).'M';
  267. } elseif ($num >= 1000) {
  268. return $formatter->format($num / 1000).'K';
  269. } else {
  270. return $formatter->format($num);
  271. }
  272. }
  273. public static function getMetaDescription($id)
  274. {
  275. $account = self::get($id, true);
  276. if (! $account) {
  277. return '';
  278. }
  279. $posts = self::formatNumber($account['statuses_count']).' Posts, ';
  280. $following = self::formatNumber($account['following_count']).' Following, ';
  281. $followers = self::formatNumber($account['followers_count']).' Followers';
  282. $note = $account['note'] && strlen($account['note']) ?
  283. ' · '.\Purify::clean(strip_tags(str_replace("\n", '', str_replace("\r", '', $account['note'])))) :
  284. '';
  285. return $posts.$following.$followers.$note;
  286. }
  287. }