Status.php 8.6 KB

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