Helpers.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use App\Instance;
  4. use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
  5. use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline;
  6. use App\Jobs\MediaPipeline\MediaStoragePipeline;
  7. use App\Jobs\StatusPipeline\StatusReplyPipeline;
  8. use App\Jobs\StatusPipeline\StatusTagsPipeline;
  9. use App\Media;
  10. use App\Models\Poll;
  11. use App\Profile;
  12. use App\Services\Account\AccountStatService;
  13. use App\Services\ActivityPubDeliveryService;
  14. use App\Services\ActivityPubFetchService;
  15. use App\Services\DomainService;
  16. use App\Services\InstanceService;
  17. use App\Services\MediaPathService;
  18. use App\Services\NetworkTimelineService;
  19. use App\Services\UserFilterService;
  20. use App\Status;
  21. use App\Util\Media\License;
  22. use Cache;
  23. use Carbon\Carbon;
  24. use Illuminate\Support\Str;
  25. use Illuminate\Validation\Rule;
  26. use League\Uri\Exceptions\UriException;
  27. use League\Uri\Uri;
  28. use Purify;
  29. use Validator;
  30. class Helpers
  31. {
  32. public static function validateObject($data)
  33. {
  34. $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
  35. $valid = Validator::make($data, [
  36. 'type' => [
  37. 'required',
  38. 'string',
  39. Rule::in($verbs),
  40. ],
  41. 'id' => 'required|string',
  42. 'actor' => 'required|string|url',
  43. 'object' => 'required',
  44. 'object.type' => 'required_if:type,Create',
  45. 'object.attributedTo' => 'required_if:type,Create|url',
  46. 'published' => 'required_if:type,Create|date',
  47. ])->passes();
  48. return $valid;
  49. }
  50. public static function verifyAttachments($data)
  51. {
  52. if (! isset($data['object']) || empty($data['object'])) {
  53. $data = ['object' => $data];
  54. }
  55. $activity = $data['object'];
  56. $mimeTypes = explode(',', config_cache('pixelfed.media_types'));
  57. $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
  58. // Peertube
  59. // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image'];
  60. if (! isset($activity['attachment']) || empty($activity['attachment'])) {
  61. return false;
  62. }
  63. // peertube
  64. // $attachment = is_array($activity['url']) ?
  65. // collect($activity['url'])
  66. // ->filter(function($media) {
  67. // return $media['type'] == 'Link' && $media['mediaType'] == 'video/mp4';
  68. // })
  69. // ->take(1)
  70. // ->values()
  71. // ->toArray()[0] : $activity['attachment'];
  72. $attachment = $activity['attachment'];
  73. $valid = Validator::make($attachment, [
  74. '*.type' => [
  75. 'required',
  76. 'string',
  77. Rule::in($mediaTypes),
  78. ],
  79. '*.url' => 'required|url',
  80. '*.mediaType' => [
  81. 'required',
  82. 'string',
  83. Rule::in($mimeTypes),
  84. ],
  85. '*.name' => 'sometimes|nullable|string',
  86. '*.blurhash' => 'sometimes|nullable|string|min:6|max:164',
  87. '*.width' => 'sometimes|nullable|integer|min:1|max:5000',
  88. '*.height' => 'sometimes|nullable|integer|min:1|max:5000',
  89. ])->passes();
  90. return $valid;
  91. }
  92. public static function normalizeAudience($data, $localOnly = true)
  93. {
  94. if (! isset($data['to'])) {
  95. return;
  96. }
  97. $audience = [];
  98. $audience['to'] = [];
  99. $audience['cc'] = [];
  100. $scope = 'private';
  101. if (is_array($data['to']) && ! empty($data['to'])) {
  102. foreach ($data['to'] as $to) {
  103. if ($to == 'https://www.w3.org/ns/activitystreams#Public') {
  104. $scope = 'public';
  105. continue;
  106. }
  107. $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to);
  108. if ($url != false) {
  109. array_push($audience['to'], $url);
  110. }
  111. }
  112. }
  113. if (is_array($data['cc']) && ! empty($data['cc'])) {
  114. foreach ($data['cc'] as $cc) {
  115. if ($cc == 'https://www.w3.org/ns/activitystreams#Public') {
  116. $scope = 'unlisted';
  117. continue;
  118. }
  119. $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc);
  120. if ($url != false) {
  121. array_push($audience['cc'], $url);
  122. }
  123. }
  124. }
  125. $audience['scope'] = $scope;
  126. return $audience;
  127. }
  128. public static function userInAudience($profile, $data)
  129. {
  130. $audience = self::normalizeAudience($data);
  131. $url = $profile->permalink();
  132. return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
  133. }
  134. public static function validateUrl($url = null, $disableDNSCheck = false, $forceBanCheck = false)
  135. {
  136. if (is_array($url) && ! empty($url)) {
  137. $url = $url[0];
  138. }
  139. if (! $url || strlen($url) === 0) {
  140. return false;
  141. }
  142. try {
  143. $uri = Uri::new($url);
  144. if (! $uri) {
  145. return false;
  146. }
  147. if ($uri->getScheme() !== 'https') {
  148. return false;
  149. }
  150. $host = $uri->getHost();
  151. if (! $host || $host === '') {
  152. return false;
  153. }
  154. if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
  155. return false;
  156. }
  157. if (! str_contains($host, '.')) {
  158. return false;
  159. }
  160. $localhosts = [
  161. 'localhost',
  162. '127.0.0.1',
  163. '::1',
  164. 'broadcasthost',
  165. 'ip6-localhost',
  166. 'ip6-loopback',
  167. ];
  168. if (in_array($host, $localhosts)) {
  169. return false;
  170. }
  171. if ($disableDNSCheck !== true && app()->environment() === 'production' && (bool) config('security.url.verify_dns')) {
  172. $hash = hash('sha256', $host);
  173. $key = "helpers:url:valid-dns:sha256-{$hash}";
  174. $domainValidDns = Cache::remember($key, 14440, function () use ($host) {
  175. return DomainService::hasValidDns($host);
  176. });
  177. if (! $domainValidDns) {
  178. return false;
  179. }
  180. }
  181. if ($forceBanCheck || $disableDNSCheck !== true && app()->environment() === 'production') {
  182. $bannedInstances = InstanceService::getBannedDomains();
  183. if (in_array($host, $bannedInstances)) {
  184. return false;
  185. }
  186. }
  187. return $uri->toString();
  188. } catch (UriException $e) {
  189. return false;
  190. }
  191. }
  192. public static function validateLocalUrl($url)
  193. {
  194. $url = self::validateUrl($url);
  195. if ($url == true) {
  196. $domain = config('pixelfed.domain.app');
  197. $uri = Uri::new($url);
  198. $host = $uri->getHost();
  199. if (! $host || empty($host)) {
  200. return false;
  201. }
  202. $url = strtolower($domain) === strtolower($host) ? $url : false;
  203. return $url;
  204. }
  205. return false;
  206. }
  207. public static function zttpUserAgent()
  208. {
  209. $version = config('pixelfed.version');
  210. $url = config('app.url');
  211. return [
  212. 'Accept' => 'application/activity+json',
  213. 'User-Agent' => "(Pixelfed/{$version}; +{$url})",
  214. ];
  215. }
  216. public static function fetchFromUrl($url = false)
  217. {
  218. if (self::validateUrl($url) == false) {
  219. return;
  220. }
  221. $hash = hash('sha256', $url);
  222. $key = "helpers:url:fetcher:sha256-{$hash}";
  223. $ttl = now()->addMinutes(15);
  224. return Cache::remember($key, $ttl, function () use ($url) {
  225. $res = ActivityPubFetchService::get($url);
  226. if (! $res || empty($res)) {
  227. return false;
  228. }
  229. $res = json_decode($res, true, 8);
  230. if (json_last_error() == JSON_ERROR_NONE) {
  231. return $res;
  232. } else {
  233. return false;
  234. }
  235. });
  236. }
  237. public static function fetchProfileFromUrl($url)
  238. {
  239. return self::fetchFromUrl($url);
  240. }
  241. public static function pluckval($val)
  242. {
  243. if (is_string($val)) {
  244. return $val;
  245. }
  246. if (is_array($val)) {
  247. return ! empty($val) ? head($val) : null;
  248. }
  249. return null;
  250. }
  251. public static function statusFirstOrFetch($url, $replyTo = false)
  252. {
  253. $url = self::validateUrl($url);
  254. if ($url == false) {
  255. return;
  256. }
  257. $host = parse_url($url, PHP_URL_HOST);
  258. $local = config('pixelfed.domain.app') == $host ? true : false;
  259. if ($local) {
  260. $id = (int) last(explode('/', $url));
  261. return Status::whereNotIn('scope', ['draft', 'archived'])->findOrFail($id);
  262. }
  263. $cached = Status::whereNotIn('scope', ['draft', 'archived'])
  264. ->whereUri($url)
  265. ->orWhere('object_url', $url)
  266. ->first();
  267. if ($cached) {
  268. return $cached;
  269. }
  270. $res = self::fetchFromUrl($url);
  271. if (! $res || empty($res) || isset($res['error']) || ! isset($res['@context']) || ! isset($res['published'])) {
  272. return;
  273. }
  274. if (config('autospam.live_filters.enabled')) {
  275. $filters = config('autospam.live_filters.filters');
  276. if (! empty($filters) && isset($res['content']) && ! empty($res['content']) && strlen($filters) > 3) {
  277. $filters = array_map('trim', explode(',', $filters));
  278. $content = $res['content'];
  279. foreach ($filters as $filter) {
  280. $filter = trim(strtolower($filter));
  281. if (! $filter || ! strlen($filter)) {
  282. continue;
  283. }
  284. if (str_contains(strtolower($content), $filter)) {
  285. return;
  286. }
  287. }
  288. }
  289. }
  290. if (isset($res['object'])) {
  291. $activity = $res;
  292. } else {
  293. $activity = ['object' => $res];
  294. }
  295. $scope = 'private';
  296. $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
  297. if (isset($res['to']) == true) {
  298. if (is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
  299. $scope = 'public';
  300. }
  301. if (is_string($res['to']) && $res['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
  302. $scope = 'public';
  303. }
  304. }
  305. if (isset($res['cc']) == true) {
  306. if (is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
  307. $scope = 'unlisted';
  308. }
  309. if (is_string($res['cc']) && $res['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
  310. $scope = 'unlisted';
  311. }
  312. }
  313. if (config('costar.enabled') == true) {
  314. $blockedKeywords = config('costar.keyword.block');
  315. if ($blockedKeywords !== null) {
  316. $keywords = config('costar.keyword.block');
  317. foreach ($keywords as $kw) {
  318. if (Str::contains($res['content'], $kw) == true) {
  319. return;
  320. }
  321. }
  322. }
  323. $unlisted = config('costar.domain.unlisted');
  324. if (in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
  325. $unlisted = true;
  326. $scope = 'unlisted';
  327. } else {
  328. $unlisted = false;
  329. }
  330. $cwDomains = config('costar.domain.cw');
  331. if (in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
  332. $cw = true;
  333. }
  334. }
  335. $id = isset($res['id']) ? self::pluckval($res['id']) : self::pluckval($url);
  336. $idDomain = parse_url($id, PHP_URL_HOST);
  337. $urlDomain = parse_url($url, PHP_URL_HOST);
  338. if ($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) {
  339. return;
  340. }
  341. if (! self::validateUrl($id)) {
  342. return;
  343. }
  344. if (! isset($activity['object']['attributedTo'])) {
  345. return;
  346. }
  347. $attributedTo = is_string($activity['object']['attributedTo']) ?
  348. $activity['object']['attributedTo'] :
  349. (is_array($activity['object']['attributedTo']) ?
  350. collect($activity['object']['attributedTo'])
  351. ->filter(function ($o) {
  352. return $o && isset($o['type']) && $o['type'] == 'Person';
  353. })
  354. ->pluck('id')
  355. ->first() : null
  356. );
  357. if ($attributedTo) {
  358. $actorDomain = parse_url($attributedTo, PHP_URL_HOST);
  359. if (! self::validateUrl($attributedTo) ||
  360. $idDomain !== $actorDomain ||
  361. $actorDomain !== $urlDomain
  362. ) {
  363. return;
  364. }
  365. }
  366. if ($idDomain !== $urlDomain) {
  367. return;
  368. }
  369. $profile = self::profileFirstOrNew($attributedTo);
  370. if (! $profile) {
  371. return;
  372. }
  373. if (isset($activity['object']['inReplyTo']) && ! empty($activity['object']['inReplyTo']) || $replyTo == true) {
  374. $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false);
  375. if ($reply_to) {
  376. $blocks = UserFilterService::blocks($reply_to->profile_id);
  377. if (in_array($profile->id, $blocks)) {
  378. return;
  379. }
  380. }
  381. $reply_to = optional($reply_to)->id;
  382. } else {
  383. $reply_to = null;
  384. }
  385. $ts = self::pluckval($res['published']);
  386. if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
  387. $scope = 'unlisted';
  388. }
  389. if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
  390. $cw = true;
  391. }
  392. if ($res['type'] === 'Question') {
  393. $status = self::storePoll(
  394. $profile,
  395. $res,
  396. $url,
  397. $ts,
  398. $reply_to,
  399. $cw,
  400. $scope,
  401. $id
  402. );
  403. return $status;
  404. } else {
  405. $status = self::storeStatus($url, $profile, $res);
  406. }
  407. return $status;
  408. }
  409. public static function storeStatus($url, $profile, $activity)
  410. {
  411. $originalUrl = $url;
  412. $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']);
  413. $url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
  414. $idDomain = parse_url($id, PHP_URL_HOST);
  415. $urlDomain = parse_url($url, PHP_URL_HOST);
  416. $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST);
  417. if (! self::validateUrl($id) || ! self::validateUrl($url)) {
  418. return;
  419. }
  420. if (strtolower($originalUrlDomain) !== strtolower($idDomain) ||
  421. strtolower($originalUrlDomain) !== strtolower($urlDomain)) {
  422. return;
  423. }
  424. $reply_to = self::getReplyTo($activity);
  425. $ts = self::pluckval($activity['published']);
  426. $scope = self::getScope($activity, $url);
  427. $cw = self::getSensitive($activity, $url);
  428. $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
  429. $isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false);
  430. $commentsDisabled = isset($activity['commentsEnabled']) ? ! boolval($activity['commentsEnabled']) : false;
  431. if (! $pid) {
  432. return;
  433. }
  434. if ($scope == 'public') {
  435. if ($isUnlisted == true) {
  436. $scope = 'unlisted';
  437. }
  438. }
  439. $status = Status::updateOrCreate(
  440. [
  441. 'uri' => $url,
  442. ], [
  443. 'profile_id' => $pid,
  444. 'url' => $url,
  445. 'object_url' => $id,
  446. 'caption' => isset($activity['content']) ? Purify::clean(strip_tags($activity['content'])) : null,
  447. 'rendered' => isset($activity['content']) ? Purify::clean($activity['content']) : null,
  448. 'created_at' => Carbon::parse($ts)->tz('UTC'),
  449. 'in_reply_to_id' => $reply_to,
  450. 'local' => false,
  451. 'is_nsfw' => $cw,
  452. 'scope' => $scope,
  453. 'visibility' => $scope,
  454. 'cw_summary' => ($cw == true && isset($activity['summary']) ?
  455. Purify::clean(strip_tags($activity['summary'])) : null),
  456. 'comments_disabled' => $commentsDisabled,
  457. ]
  458. );
  459. if ($reply_to == null) {
  460. self::importNoteAttachment($activity, $status);
  461. } else {
  462. if (isset($activity['attachment']) && ! empty($activity['attachment'])) {
  463. self::importNoteAttachment($activity, $status);
  464. }
  465. StatusReplyPipeline::dispatch($status);
  466. }
  467. if (isset($activity['tag']) && is_array($activity['tag']) && ! empty($activity['tag'])) {
  468. StatusTagsPipeline::dispatch($activity, $status);
  469. }
  470. if (config('instance.timeline.network.cached') &&
  471. $status->in_reply_to_id === null &&
  472. $status->reblog_of_id === null &&
  473. in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
  474. $status->created_at->gt(now()->subHours(config('instance.timeline.network.max_hours_old'))) &&
  475. (config('instance.hide_nsfw_on_public_feeds') == true ? $status->is_nsfw == false : true)
  476. ) {
  477. $filteredDomains = collect(InstanceService::getBannedDomains())
  478. ->merge(InstanceService::getUnlistedDomains())
  479. ->unique()
  480. ->values()
  481. ->toArray();
  482. if (! in_array($urlDomain, $filteredDomains)) {
  483. if (! $isUnlisted) {
  484. NetworkTimelineService::add($status->id);
  485. }
  486. }
  487. }
  488. AccountStatService::incrementPostCount($pid);
  489. if ($status->in_reply_to_id === null &&
  490. in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
  491. ) {
  492. FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed');
  493. }
  494. return $status;
  495. }
  496. public static function getSensitive($activity, $url)
  497. {
  498. if (! $url || ! strlen($url)) {
  499. return true;
  500. }
  501. $urlDomain = parse_url($url, PHP_URL_HOST);
  502. $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
  503. if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
  504. $cw = true;
  505. }
  506. return $cw;
  507. }
  508. public static function getReplyTo($activity)
  509. {
  510. $reply_to = null;
  511. $inReplyTo = isset($activity['inReplyTo']) && ! empty($activity['inReplyTo']) ?
  512. self::pluckval($activity['inReplyTo']) :
  513. false;
  514. if ($inReplyTo) {
  515. $reply_to = self::statusFirstOrFetch($inReplyTo);
  516. if ($reply_to) {
  517. $reply_to = optional($reply_to)->id;
  518. }
  519. } else {
  520. $reply_to = null;
  521. }
  522. return $reply_to;
  523. }
  524. public static function getScope($activity, $url)
  525. {
  526. $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url);
  527. $url = isset($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
  528. $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST);
  529. $scope = 'private';
  530. if (isset($activity['to']) == true) {
  531. if (is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
  532. $scope = 'public';
  533. }
  534. if (is_string($activity['to']) && $activity['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
  535. $scope = 'public';
  536. }
  537. }
  538. if (isset($activity['cc']) == true) {
  539. if (is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
  540. $scope = 'unlisted';
  541. }
  542. if (is_string($activity['cc']) && $activity['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
  543. $scope = 'unlisted';
  544. }
  545. }
  546. if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
  547. $scope = 'unlisted';
  548. }
  549. return $scope;
  550. }
  551. private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
  552. {
  553. if (! isset($res['endTime']) || ! isset($res['oneOf']) || ! is_array($res['oneOf']) || count($res['oneOf']) > 4) {
  554. return;
  555. }
  556. $options = collect($res['oneOf'])->map(function ($option) {
  557. return $option['name'];
  558. })->toArray();
  559. $cachedTallies = collect($res['oneOf'])->map(function ($option) {
  560. return $option['replies']['totalItems'] ?? 0;
  561. })->toArray();
  562. $status = new Status;
  563. $status->profile_id = $profile->id;
  564. $status->url = isset($res['url']) ? $res['url'] : $url;
  565. $status->uri = isset($res['url']) ? $res['url'] : $url;
  566. $status->object_url = $id;
  567. $status->caption = strip_tags($res['content']);
  568. $status->rendered = Purify::clean($res['content']);
  569. $status->created_at = Carbon::parse($ts)->tz('UTC');
  570. $status->in_reply_to_id = null;
  571. $status->local = false;
  572. $status->is_nsfw = $cw;
  573. $status->scope = 'draft';
  574. $status->visibility = 'draft';
  575. $status->cw_summary = $cw == true && isset($res['summary']) ?
  576. Purify::clean(strip_tags($res['summary'])) : null;
  577. $status->save();
  578. $poll = new Poll;
  579. $poll->status_id = $status->id;
  580. $poll->profile_id = $status->profile_id;
  581. $poll->poll_options = $options;
  582. $poll->cached_tallies = $cachedTallies;
  583. $poll->votes_count = array_sum($cachedTallies);
  584. $poll->expires_at = now()->parse($res['endTime']);
  585. $poll->last_fetched_at = now();
  586. $poll->save();
  587. $status->type = 'poll';
  588. $status->scope = $scope;
  589. $status->visibility = $scope;
  590. $status->save();
  591. return $status;
  592. }
  593. public static function statusFetch($url)
  594. {
  595. return self::statusFirstOrFetch($url);
  596. }
  597. public static function importNoteAttachment($data, Status $status)
  598. {
  599. if (self::verifyAttachments($data) == false) {
  600. // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]);
  601. $status->viewType();
  602. return;
  603. }
  604. $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
  605. // peertube
  606. // if(!$attachments) {
  607. // $obj = isset($data['object']) ? $data['object'] : $data;
  608. // $attachments = is_array($obj['url']) ? $obj['url'] : null;
  609. // }
  610. $user = $status->profile;
  611. $storagePath = MediaPathService::get($user, 2);
  612. $allowed = explode(',', config_cache('pixelfed.media_types'));
  613. foreach ($attachments as $key => $media) {
  614. $type = $media['mediaType'];
  615. $url = $media['url'];
  616. $valid = self::validateUrl($url);
  617. if (in_array($type, $allowed) == false || $valid == false) {
  618. continue;
  619. }
  620. $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
  621. $license = isset($media['license']) ? License::nameToId($media['license']) : null;
  622. $caption = isset($media['name']) ? Purify::clean($media['name']) : null;
  623. $width = isset($media['width']) ? $media['width'] : false;
  624. $height = isset($media['height']) ? $media['height'] : false;
  625. $media = new Media;
  626. $media->blurhash = $blurhash;
  627. $media->remote_media = true;
  628. $media->status_id = $status->id;
  629. $media->profile_id = $status->profile_id;
  630. $media->user_id = null;
  631. $media->media_path = $url;
  632. $media->remote_url = $url;
  633. $media->caption = $caption;
  634. $media->order = $key + 1;
  635. if ($width) {
  636. $media->width = $width;
  637. }
  638. if ($height) {
  639. $media->height = $height;
  640. }
  641. if ($license) {
  642. $media->license = $license;
  643. }
  644. $media->mime = $type;
  645. $media->version = 3;
  646. $media->save();
  647. if ((bool) config_cache('pixelfed.cloud_storage') == true) {
  648. MediaStoragePipeline::dispatch($media);
  649. }
  650. }
  651. $status->viewType();
  652. }
  653. public static function profileFirstOrNew($url)
  654. {
  655. $url = self::validateUrl($url);
  656. if ($url == false) {
  657. return;
  658. }
  659. $host = parse_url($url, PHP_URL_HOST);
  660. $local = config('pixelfed.domain.app') == $host ? true : false;
  661. if ($local == true) {
  662. $id = last(explode('/', $url));
  663. return Profile::whereNull('status')
  664. ->whereNull('domain')
  665. ->whereUsername($id)
  666. ->firstOrFail();
  667. }
  668. if ($profile = Profile::whereRemoteUrl($url)->first()) {
  669. if ($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
  670. return self::profileUpdateOrCreate($url);
  671. }
  672. return $profile;
  673. }
  674. return self::profileUpdateOrCreate($url);
  675. }
  676. public static function profileUpdateOrCreate($url, $movedToCheck = false)
  677. {
  678. $movedToPid = null;
  679. $res = self::fetchProfileFromUrl($url);
  680. if (! $res || isset($res['id']) == false) {
  681. return;
  682. }
  683. if (! self::validateUrl($res['inbox'])) {
  684. return;
  685. }
  686. if (! self::validateUrl($res['id'])) {
  687. return;
  688. }
  689. $urlDomain = parse_url($url, PHP_URL_HOST);
  690. $domain = parse_url($res['id'], PHP_URL_HOST);
  691. if (strtolower($urlDomain) !== strtolower($domain)) {
  692. return;
  693. }
  694. if (! isset($res['preferredUsername']) && ! isset($res['nickname'])) {
  695. return;
  696. }
  697. // skip invalid usernames
  698. if (! ctype_alnum($res['preferredUsername'])) {
  699. $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']);
  700. if (! ctype_alnum($tmpUsername)) {
  701. return;
  702. }
  703. }
  704. $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']);
  705. if (empty($username)) {
  706. return;
  707. }
  708. $remoteUsername = $username;
  709. $webfinger = "@{$username}@{$domain}";
  710. $instance = Instance::updateOrCreate([
  711. 'domain' => $domain,
  712. ]);
  713. if ($instance->wasRecentlyCreated == true) {
  714. \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
  715. }
  716. if (! $movedToCheck && isset($res['movedTo']) && Helpers::validateUrl($res['movedTo'])) {
  717. $movedTo = self::profileUpdateOrCreate($res['movedTo'], true);
  718. if ($movedTo) {
  719. $movedToPid = $movedTo->id;
  720. }
  721. }
  722. $profile = Profile::updateOrCreate(
  723. [
  724. 'domain' => strtolower($domain),
  725. 'username' => Purify::clean($webfinger),
  726. ],
  727. [
  728. 'webfinger' => Purify::clean($webfinger),
  729. 'key_id' => $res['publicKey']['id'],
  730. 'remote_url' => $res['id'],
  731. 'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user',
  732. 'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null,
  733. 'sharedInbox' => isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null,
  734. 'inbox_url' => $res['inbox'],
  735. 'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null,
  736. 'public_key' => $res['publicKey']['publicKeyPem'],
  737. 'indexable' => isset($res['indexable']) && is_bool($res['indexable']) ? $res['indexable'] : false,
  738. 'moved_to_profile_id' => $movedToPid,
  739. ]
  740. );
  741. if ($profile->last_fetched_at == null ||
  742. $profile->last_fetched_at->lt(now()->subMonths(3))
  743. ) {
  744. RemoteAvatarFetch::dispatch($profile);
  745. }
  746. $profile->last_fetched_at = now();
  747. $profile->save();
  748. return $profile;
  749. }
  750. public static function profileFetch($url)
  751. {
  752. return self::profileFirstOrNew($url);
  753. }
  754. public static function getSignedFetch($url)
  755. {
  756. return ActivityPubFetchService::get($url);
  757. }
  758. public static function sendSignedObject($profile, $url, $body)
  759. {
  760. if (app()->environment() !== 'production') {
  761. return;
  762. }
  763. ActivityPubDeliveryService::queue()
  764. ->from($profile)
  765. ->to($url)
  766. ->payload($body)
  767. ->send();
  768. }
  769. }