Status.php 11 KB

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