Status.php 8.5 KB

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