Status.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. namespace App;
  3. use App\HasSnowflakePrimary;
  4. use App\Http\Controllers\StatusController;
  5. use App\Models\Poll;
  6. use App\Models\StatusEdit;
  7. use App\Services\AccountService;
  8. use App\Services\StatusService;
  9. use Auth;
  10. use Illuminate\Database\Eloquent\Model;
  11. use Illuminate\Database\Eloquent\SoftDeletes;
  12. use Illuminate\Support\Str;
  13. use Storage;
  14. class Status extends Model
  15. {
  16. use HasSnowflakePrimary, SoftDeletes;
  17. /**
  18. * Indicates if the IDs are auto-incrementing.
  19. *
  20. * @var bool
  21. */
  22. public $incrementing = false;
  23. /**
  24. * The attributes that should be mutated to dates.
  25. *
  26. * @var array
  27. */
  28. protected $casts = [
  29. 'deleted_at' => 'datetime',
  30. 'edited_at' => 'datetime',
  31. ];
  32. protected $guarded = [];
  33. const STATUS_TYPES = [
  34. 'text',
  35. 'photo',
  36. 'photo:album',
  37. 'video',
  38. 'video:album',
  39. 'photo:video:album',
  40. 'share',
  41. 'reply',
  42. 'story',
  43. 'story:reply',
  44. 'story:reaction',
  45. 'story:live',
  46. 'loop',
  47. ];
  48. const MAX_MENTIONS = 20;
  49. const MAX_HASHTAGS = 60;
  50. const MAX_LINKS = 5;
  51. public function profile()
  52. {
  53. return $this->belongsTo(Profile::class);
  54. }
  55. public function media()
  56. {
  57. return $this->hasMany(Media::class);
  58. }
  59. public function firstMedia()
  60. {
  61. return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
  62. }
  63. public function viewType()
  64. {
  65. if ($this->type) {
  66. return $this->type;
  67. }
  68. return $this->setType();
  69. }
  70. public function setType()
  71. {
  72. if (in_array($this->type, self::STATUS_TYPES)) {
  73. return $this->type;
  74. }
  75. $mimes = $this->media->pluck('mime')->toArray();
  76. $type = StatusController::mimeTypeCheck($mimes);
  77. if ($type) {
  78. $this->type = $type;
  79. $this->save();
  80. return $type;
  81. }
  82. }
  83. public function thumb($showNsfw = false)
  84. {
  85. $entity = StatusService::get($this->id, false);
  86. if (! $entity || ! isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
  87. return url(Storage::url('public/no-preview.png'));
  88. }
  89. if ((! isset($entity['sensitive']) || $entity['sensitive']) && ! $showNsfw) {
  90. return url(Storage::url('public/no-preview.png'));
  91. }
  92. if (! isset($entity['visibility']) || ! in_array($entity['visibility'], ['public', 'unlisted'])) {
  93. return url(Storage::url('public/no-preview.png'));
  94. }
  95. return collect($entity['media_attachments'])
  96. ->filter(fn ($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png', 'image/jpg']))
  97. ->map(function ($media) {
  98. if (! Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) {
  99. return $media['preview_url'];
  100. }
  101. return $media['url'];
  102. })
  103. ->first() ?? url(Storage::url('public/no-preview.png'));
  104. }
  105. public function url($forceLocal = false)
  106. {
  107. if ($this->uri) {
  108. return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
  109. } else {
  110. $id = $this->id;
  111. $account = AccountService::get($this->profile_id, true);
  112. if (! $account || ! isset($account['username'])) {
  113. return '/404';
  114. }
  115. $path = url(config('app.url')."/p/{$account['username']}/{$id}");
  116. return $path;
  117. }
  118. }
  119. public function permalink($suffix = '/activity')
  120. {
  121. $id = $this->id;
  122. $username = $this->profile->username;
  123. $path = config('app.url')."/p/{$username}/{$id}{$suffix}";
  124. return url($path);
  125. }
  126. public function editUrl()
  127. {
  128. return $this->url().'/edit';
  129. }
  130. public function mediaUrl()
  131. {
  132. $media = $this->firstMedia();
  133. $path = $media->media_path;
  134. $hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
  135. $url = $media->cdn_url ? $media->cdn_url."?v={$hash}" : url(Storage::url($path)."?v={$hash}");
  136. return $url;
  137. }
  138. public function likes()
  139. {
  140. return $this->hasMany(Like::class);
  141. }
  142. public function liked(): bool
  143. {
  144. if (! Auth::check()) {
  145. return false;
  146. }
  147. $pid = Auth::user()->profile_id;
  148. return Like::select('status_id', 'profile_id')
  149. ->whereStatusId($this->id)
  150. ->whereProfileId($pid)
  151. ->exists();
  152. }
  153. public function likedBy()
  154. {
  155. return $this->hasManyThrough(
  156. Profile::class,
  157. Like::class,
  158. 'status_id',
  159. 'id',
  160. 'id',
  161. 'profile_id'
  162. );
  163. }
  164. public function comments()
  165. {
  166. return $this->hasMany(self::class, 'in_reply_to_id');
  167. }
  168. public function bookmarked()
  169. {
  170. if (! Auth::check()) {
  171. return false;
  172. }
  173. $profile = Auth::user()->profile;
  174. return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
  175. }
  176. public function shares()
  177. {
  178. return $this->hasMany(self::class, 'reblog_of_id');
  179. }
  180. public function shared(): bool
  181. {
  182. if (! Auth::check()) {
  183. return false;
  184. }
  185. $pid = Auth::user()->profile_id;
  186. return $this->select('profile_id', 'reblog_of_id')
  187. ->whereProfileId($pid)
  188. ->whereReblogOfId($this->id)
  189. ->exists();
  190. }
  191. public function sharedBy()
  192. {
  193. return $this->hasManyThrough(
  194. Profile::class,
  195. Status::class,
  196. 'reblog_of_id',
  197. 'id',
  198. 'id',
  199. 'profile_id'
  200. );
  201. }
  202. public function parent()
  203. {
  204. $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
  205. if (! empty($parent)) {
  206. return $this->findOrFail($parent);
  207. } else {
  208. return false;
  209. }
  210. }
  211. public function conversation()
  212. {
  213. return $this->hasOne(Conversation::class);
  214. }
  215. public function hashtags()
  216. {
  217. return $this->hasManyThrough(
  218. Hashtag::class,
  219. StatusHashtag::class,
  220. 'status_id',
  221. 'id',
  222. 'id',
  223. 'hashtag_id'
  224. );
  225. }
  226. public function mentions()
  227. {
  228. return $this->hasManyThrough(
  229. Profile::class,
  230. Mention::class,
  231. 'status_id',
  232. 'id',
  233. 'id',
  234. 'profile_id'
  235. );
  236. }
  237. public function reportUrl()
  238. {
  239. return route('report.form')."?type=post&id={$this->id}";
  240. }
  241. public function toActivityStream()
  242. {
  243. $media = $this->media;
  244. $mediaCollection = [];
  245. foreach ($media as $image) {
  246. $mediaCollection[] = [
  247. 'type' => 'Link',
  248. 'href' => $image->url(),
  249. 'mediaType' => $image->mime,
  250. ];
  251. }
  252. $obj = [
  253. '@context' => 'https://www.w3.org/ns/activitystreams',
  254. 'type' => 'Document',
  255. 'name' => null,
  256. 'url' => $mediaCollection,
  257. ];
  258. return $obj;
  259. }
  260. public function recentComments()
  261. {
  262. return $this->comments()->orderBy('created_at', 'desc')->take(3);
  263. }
  264. public function scopeToAudience($audience)
  265. {
  266. if (! in_array($audience, ['to', 'cc']) || $this->local == false) {
  267. return;
  268. }
  269. $res = [];
  270. $res['to'] = [];
  271. $res['cc'] = [];
  272. $scope = $this->scope;
  273. $mentions = $this->mentions->map(function ($mention) {
  274. return $mention->permalink();
  275. })->toArray();
  276. if ($this->in_reply_to_id != null) {
  277. $parent = $this->parent();
  278. if ($parent) {
  279. $mentions = array_merge([$parent->profile->permalink()], $mentions);
  280. }
  281. }
  282. switch ($scope) {
  283. case 'public':
  284. $res['to'] = [
  285. 'https://www.w3.org/ns/activitystreams#Public',
  286. ];
  287. $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
  288. break;
  289. case 'unlisted':
  290. $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
  291. $res['cc'] = [
  292. 'https://www.w3.org/ns/activitystreams#Public',
  293. ];
  294. break;
  295. case 'private':
  296. $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
  297. $res['cc'] = [];
  298. break;
  299. // TODO: Update scope when DMs are supported
  300. case 'direct':
  301. $res['to'] = [];
  302. $res['cc'] = [];
  303. break;
  304. }
  305. return $res[$audience];
  306. }
  307. public function place()
  308. {
  309. return $this->belongsTo(Place::class);
  310. }
  311. public function directMessage()
  312. {
  313. return $this->hasOne(DirectMessage::class);
  314. }
  315. public function poll()
  316. {
  317. return $this->hasOne(Poll::class);
  318. }
  319. public function edits()
  320. {
  321. return $this->hasMany(StatusEdit::class);
  322. }
  323. }