ComposeController.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use Auth, Cache, Storage, URL;
  5. use Carbon\Carbon;
  6. use App\{
  7. Avatar,
  8. Hashtag,
  9. Like,
  10. Media,
  11. MediaTag,
  12. Notification,
  13. Profile,
  14. Place,
  15. Status,
  16. UserFilter,
  17. UserSetting
  18. };
  19. use App\Models\Poll;
  20. use App\Transformer\Api\{
  21. MediaTransformer,
  22. MediaDraftTransformer,
  23. StatusTransformer,
  24. StatusStatelessTransformer
  25. };
  26. use League\Fractal;
  27. use App\Util\Media\Filter;
  28. use League\Fractal\Serializer\ArraySerializer;
  29. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  30. use App\Jobs\AvatarPipeline\AvatarOptimize;
  31. use App\Jobs\ImageOptimizePipeline\ImageOptimize;
  32. use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
  33. use App\Jobs\StatusPipeline\NewStatusPipeline;
  34. use App\Jobs\VideoPipeline\{
  35. VideoOptimize,
  36. VideoPostProcess,
  37. VideoThumbnail
  38. };
  39. use App\Services\AccountService;
  40. use App\Services\NotificationService;
  41. use App\Services\MediaPathService;
  42. use App\Services\MediaBlocklistService;
  43. use App\Services\MediaStorageService;
  44. use App\Services\MediaTagService;
  45. use App\Services\StatusService;
  46. use Illuminate\Support\Str;
  47. use App\Util\Lexer\Autolink;
  48. use App\Util\Lexer\Extractor;
  49. use App\Util\Media\License;
  50. class ComposeController extends Controller
  51. {
  52. protected $fractal;
  53. public function __construct()
  54. {
  55. $this->middleware('auth');
  56. $this->fractal = new Fractal\Manager();
  57. $this->fractal->setSerializer(new ArraySerializer());
  58. }
  59. public function show(Request $request)
  60. {
  61. return view('status.compose');
  62. }
  63. public function mediaUpload(Request $request)
  64. {
  65. abort_if(!$request->user(), 403);
  66. $this->validate($request, [
  67. 'file.*' => function() {
  68. return [
  69. 'required',
  70. 'mimes:' . config_cache('pixelfed.media_types'),
  71. 'max:' . config_cache('pixelfed.max_photo_size'),
  72. ];
  73. },
  74. 'filter_name' => 'nullable|string|max:24',
  75. 'filter_class' => 'nullable|alpha_dash|max:24'
  76. ]);
  77. $user = Auth::user();
  78. $profile = $user->profile;
  79. $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
  80. $limitTtl = now()->addMinutes(15);
  81. $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
  82. $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
  83. return $dailyLimit >= 250;
  84. });
  85. abort_if($limitReached == true, 429);
  86. if(config_cache('pixelfed.enforce_account_limit') == true) {
  87. $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
  88. return Media::whereUserId($user->id)->sum('size') / 1000;
  89. });
  90. $limit = (int) config_cache('pixelfed.max_account_size');
  91. if ($size >= $limit) {
  92. abort(403, 'Account size limit reached.');
  93. }
  94. }
  95. $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
  96. $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
  97. $photo = $request->file('file');
  98. $mimes = explode(',', config_cache('pixelfed.media_types'));
  99. abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
  100. $storagePath = MediaPathService::get($user, 2);
  101. $path = $photo->store($storagePath);
  102. $hash = \hash_file('sha256', $photo);
  103. $mime = $photo->getMimeType();
  104. abort_if(MediaBlocklistService::exists($hash) == true, 451);
  105. $media = new Media();
  106. $media->status_id = null;
  107. $media->profile_id = $profile->id;
  108. $media->user_id = $user->id;
  109. $media->media_path = $path;
  110. $media->original_sha256 = $hash;
  111. $media->size = $photo->getSize();
  112. $media->mime = $mime;
  113. $media->filter_class = $filterClass;
  114. $media->filter_name = $filterName;
  115. $media->version = 3;
  116. $media->save();
  117. $preview_url = $media->url() . '?v=' . time();
  118. $url = $media->url() . '?v=' . time();
  119. switch ($media->mime) {
  120. case 'image/jpeg':
  121. case 'image/png':
  122. case 'image/webp':
  123. ImageOptimize::dispatch($media);
  124. break;
  125. case 'video/mp4':
  126. VideoThumbnail::dispatch($media);
  127. $preview_url = '/storage/no-preview.png';
  128. $url = '/storage/no-preview.png';
  129. break;
  130. default:
  131. break;
  132. }
  133. Cache::forget($limitKey);
  134. $resource = new Fractal\Resource\Item($media, new MediaTransformer());
  135. $res = $this->fractal->createData($resource)->toArray();
  136. $res['preview_url'] = $preview_url;
  137. $res['url'] = $url;
  138. return response()->json($res);
  139. }
  140. public function mediaUpdate(Request $request)
  141. {
  142. $this->validate($request, [
  143. 'id' => 'required',
  144. 'file' => function() {
  145. return [
  146. 'required',
  147. 'mimes:' . config_cache('pixelfed.media_types'),
  148. 'max:' . config_cache('pixelfed.max_photo_size'),
  149. ];
  150. },
  151. ]);
  152. $user = Auth::user();
  153. $limitKey = 'compose:rate-limit:media-updates:' . $user->id;
  154. $limitTtl = now()->addMinutes(15);
  155. $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
  156. $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
  157. return $dailyLimit >= 500;
  158. });
  159. abort_if($limitReached == true, 429);
  160. $photo = $request->file('file');
  161. $id = $request->input('id');
  162. $media = Media::whereUserId($user->id)
  163. ->whereProfileId($user->profile_id)
  164. ->whereNull('status_id')
  165. ->findOrFail($id);
  166. $media->save();
  167. $fragments = explode('/', $media->media_path);
  168. $name = last($fragments);
  169. array_pop($fragments);
  170. $dir = implode('/', $fragments);
  171. $path = $photo->storeAs($dir, $name);
  172. $res = [
  173. 'url' => $media->url() . '?v=' . time()
  174. ];
  175. ImageOptimize::dispatch($media);
  176. Cache::forget($limitKey);
  177. return $res;
  178. }
  179. public function mediaDelete(Request $request)
  180. {
  181. abort_if(!$request->user(), 403);
  182. $this->validate($request, [
  183. 'id' => 'required|integer|min:1|exists:media,id'
  184. ]);
  185. $media = Media::whereNull('status_id')
  186. ->whereUserId(Auth::id())
  187. ->findOrFail($request->input('id'));
  188. MediaStorageService::delete($media, true);
  189. $media->forceDelete();
  190. return response()->json([
  191. 'msg' => 'Successfully deleted',
  192. 'code' => 200
  193. ]);
  194. }
  195. public function searchTag(Request $request)
  196. {
  197. abort_if(!$request->user(), 403);
  198. $this->validate($request, [
  199. 'q' => 'required|string|min:1|max:50'
  200. ]);
  201. $q = $request->input('q');
  202. if(Str::of($q)->startsWith('@')) {
  203. if(strlen($q) < 3) {
  204. return [];
  205. }
  206. $q = mb_substr($q, 1);
  207. }
  208. $blocked = UserFilter::whereFilterableType('App\Profile')
  209. ->whereFilterType('block')
  210. ->whereFilterableId($request->user()->profile_id)
  211. ->pluck('user_id');
  212. $blocked->push($request->user()->profile_id);
  213. $results = Profile::select('id','domain','username')
  214. ->whereNotIn('id', $blocked)
  215. ->whereNull('domain')
  216. ->where('username','like','%'.$q.'%')
  217. ->limit(15)
  218. ->get()
  219. ->map(function($r) {
  220. return [
  221. 'id' => (string) $r->id,
  222. 'name' => $r->username,
  223. 'privacy' => true,
  224. 'avatar' => $r->avatarUrl()
  225. ];
  226. });
  227. return $results;
  228. }
  229. public function searchUntag(Request $request)
  230. {
  231. abort_if(!$request->user(), 403);
  232. $this->validate($request, [
  233. 'status_id' => 'required',
  234. 'profile_id' => 'required'
  235. ]);
  236. $user = $request->user();
  237. $status_id = $request->input('status_id');
  238. $profile_id = (int) $request->input('profile_id');
  239. abort_if((int) $user->profile_id !== $profile_id, 400);
  240. $tag = MediaTag::whereStatusId($status_id)
  241. ->whereProfileId($profile_id)
  242. ->first();
  243. if(!$tag) {
  244. return [];
  245. }
  246. Notification::whereItemType('App\MediaTag')
  247. ->whereItemId($tag->id)
  248. ->whereProfileId($profile_id)
  249. ->whereAction('tagged')
  250. ->delete();
  251. MediaTagService::untag($status_id, $profile_id);
  252. return [200];
  253. }
  254. public function searchLocation(Request $request)
  255. {
  256. abort_if(!Auth::check(), 403);
  257. $this->validate($request, [
  258. 'q' => 'required|string|max:100'
  259. ]);
  260. $q = filter_var($request->input('q'), FILTER_SANITIZE_STRING);
  261. $hash = hash('sha256', $q);
  262. $key = 'search:location:id:' . $hash;
  263. $places = Cache::remember($key, now()->addMinutes(15), function() use($q) {
  264. $q = '%' . $q . '%';
  265. return Place::where('name', 'like', $q)
  266. ->take(80)
  267. ->get()
  268. ->map(function($r) {
  269. return [
  270. 'id' => $r->id,
  271. 'name' => $r->name,
  272. 'country' => $r->country,
  273. 'url' => $r->url()
  274. ];
  275. });
  276. });
  277. return $places;
  278. }
  279. public function searchMentionAutocomplete(Request $request)
  280. {
  281. abort_if(!$request->user(), 403);
  282. $this->validate($request, [
  283. 'q' => 'required|string|min:2|max:50'
  284. ]);
  285. $q = $request->input('q');
  286. if(Str::of($q)->startsWith('@')) {
  287. if(strlen($q) < 3) {
  288. return [];
  289. }
  290. }
  291. $blocked = UserFilter::whereFilterableType('App\Profile')
  292. ->whereFilterType('block')
  293. ->whereFilterableId($request->user()->profile_id)
  294. ->pluck('user_id');
  295. $blocked->push($request->user()->profile_id);
  296. $results = Profile::select('id','domain','username')
  297. ->whereNotIn('id', $blocked)
  298. ->where('username','like','%'.$q.'%')
  299. ->groupBy('domain')
  300. ->limit(15)
  301. ->get()
  302. ->map(function($profile) {
  303. $username = $profile->domain ? substr($profile->username, 1) : $profile->username;
  304. return [
  305. 'key' => '@' . str_limit($username, 30),
  306. 'value' => $username,
  307. ];
  308. });
  309. return $results;
  310. }
  311. public function searchHashtagAutocomplete(Request $request)
  312. {
  313. abort_if(!$request->user(), 403);
  314. $this->validate($request, [
  315. 'q' => 'required|string|min:2|max:50'
  316. ]);
  317. $q = $request->input('q');
  318. $results = Hashtag::select('slug')
  319. ->where('slug', 'like', '%'.$q.'%')
  320. ->whereIsNsfw(false)
  321. ->whereIsBanned(false)
  322. ->limit(5)
  323. ->get()
  324. ->map(function($tag) {
  325. return [
  326. 'key' => '#' . $tag->slug,
  327. 'value' => $tag->slug
  328. ];
  329. });
  330. return $results;
  331. }
  332. public function store(Request $request)
  333. {
  334. $this->validate($request, [
  335. 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
  336. 'media.*' => 'required',
  337. 'media.*.id' => 'required|integer|min:1',
  338. 'media.*.filter_class' => 'nullable|alpha_dash|max:30',
  339. 'media.*.license' => 'nullable|string|max:140',
  340. 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
  341. 'cw' => 'nullable|boolean',
  342. 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
  343. 'place' => 'nullable',
  344. 'comments_disabled' => 'nullable',
  345. 'tagged' => 'nullable',
  346. 'license' => 'nullable|integer|min:1|max:16'
  347. // 'optimize_media' => 'nullable'
  348. ]);
  349. if(config('costar.enabled') == true) {
  350. $blockedKeywords = config('costar.keyword.block');
  351. if($blockedKeywords !== null && $request->caption) {
  352. $keywords = config('costar.keyword.block');
  353. foreach($keywords as $kw) {
  354. if(Str::contains($request->caption, $kw) == true) {
  355. abort(400, 'Invalid object');
  356. }
  357. }
  358. }
  359. }
  360. $user = Auth::user();
  361. $profile = $user->profile;
  362. $limitKey = 'compose:rate-limit:store:' . $user->id;
  363. $limitTtl = now()->addMinutes(15);
  364. $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
  365. $dailyLimit = Status::whereProfileId($user->profile_id)
  366. ->whereNull('in_reply_to_id')
  367. ->whereNull('reblog_of_id')
  368. ->where('created_at', '>', now()->subDays(1))
  369. ->count();
  370. return $dailyLimit >= 100;
  371. });
  372. abort_if($limitReached == true, 429);
  373. $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
  374. $visibility = $request->input('visibility');
  375. $medias = $request->input('media');
  376. $attachments = [];
  377. $status = new Status;
  378. $mimes = [];
  379. $place = $request->input('place');
  380. $cw = $request->input('cw');
  381. $tagged = $request->input('tagged');
  382. $optimize_media = (bool) $request->input('optimize_media');
  383. foreach($medias as $k => $media) {
  384. if($k + 1 > config_cache('pixelfed.max_album_length')) {
  385. continue;
  386. }
  387. $m = Media::findOrFail($media['id']);
  388. if($m->profile_id !== $profile->id || $m->status_id) {
  389. abort(403, 'Invalid media id');
  390. }
  391. $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
  392. $m->license = $license;
  393. $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
  394. $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
  395. // if($optimize_media == false) {
  396. // $m->skip_optimize = true;
  397. // ImageThumbnail::dispatch($m);
  398. // } else {
  399. // ImageOptimize::dispatch($m);
  400. // }
  401. if($cw == true || $profile->cw == true) {
  402. $m->is_nsfw = $cw;
  403. $status->is_nsfw = $cw;
  404. }
  405. $m->save();
  406. $attachments[] = $m;
  407. array_push($mimes, $m->mime);
  408. }
  409. abort_if(empty($attachments), 422);
  410. $mediaType = StatusController::mimeTypeCheck($mimes);
  411. if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
  412. abort(400, __('exception.compose.invalid.album'));
  413. }
  414. if($place && is_array($place)) {
  415. $status->place_id = $place['id'];
  416. }
  417. if($request->filled('comments_disabled')) {
  418. $status->comments_disabled = (bool) $request->input('comments_disabled');
  419. }
  420. $status->caption = strip_tags($request->caption);
  421. $status->rendered = Autolink::create()->autolink($status->caption);
  422. $status->scope = 'draft';
  423. $status->profile_id = $profile->id;
  424. $status->save();
  425. foreach($attachments as $media) {
  426. $media->status_id = $status->id;
  427. $media->save();
  428. }
  429. $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
  430. $visibility = $profile->is_private ? 'private' : $visibility;
  431. $cw = $profile->cw == true ? true : $cw;
  432. $status->is_nsfw = $cw;
  433. $status->visibility = $visibility;
  434. $status->scope = $visibility;
  435. $status->type = $mediaType;
  436. $status->save();
  437. foreach($tagged as $tg) {
  438. $mt = new MediaTag;
  439. $mt->status_id = $status->id;
  440. $mt->media_id = $status->media->first()->id;
  441. $mt->profile_id = $tg['id'];
  442. $mt->tagged_username = $tg['name'];
  443. $mt->is_public = true;
  444. $mt->metadata = json_encode([
  445. '_v' => 1,
  446. ]);
  447. $mt->save();
  448. MediaTagService::set($mt->status_id, $mt->profile_id);
  449. MediaTagService::sendNotification($mt);
  450. }
  451. NewStatusPipeline::dispatch($status);
  452. Cache::forget('user:account:id:'.$profile->user_id);
  453. Cache::forget('_api:statuses:recent_9:'.$profile->id);
  454. Cache::forget('profile:status_count:'.$profile->id);
  455. Cache::forget('status:transformer:media:attachments:'.$status->id);
  456. Cache::forget($user->storageUsedKey());
  457. Cache::forget('profile:embed:' . $status->profile_id);
  458. Cache::forget($limitKey);
  459. return $status->url();
  460. }
  461. public function storeText(Request $request)
  462. {
  463. abort_unless(config('exp.top'), 404);
  464. $this->validate($request, [
  465. 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
  466. 'cw' => 'nullable|boolean',
  467. 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
  468. 'place' => 'nullable',
  469. 'comments_disabled' => 'nullable',
  470. 'tagged' => 'nullable',
  471. ]);
  472. if(config('costar.enabled') == true) {
  473. $blockedKeywords = config('costar.keyword.block');
  474. if($blockedKeywords !== null && $request->caption) {
  475. $keywords = config('costar.keyword.block');
  476. foreach($keywords as $kw) {
  477. if(Str::contains($request->caption, $kw) == true) {
  478. abort(400, 'Invalid object');
  479. }
  480. }
  481. }
  482. }
  483. $user = Auth::user();
  484. $profile = $user->profile;
  485. $visibility = $request->input('visibility');
  486. $status = new Status;
  487. $place = $request->input('place');
  488. $cw = $request->input('cw');
  489. $tagged = $request->input('tagged');
  490. if($place && is_array($place)) {
  491. $status->place_id = $place['id'];
  492. }
  493. if($request->filled('comments_disabled')) {
  494. $status->comments_disabled = (bool) $request->input('comments_disabled');
  495. }
  496. $status->caption = strip_tags($request->caption);
  497. $status->profile_id = $profile->id;
  498. $entities = Extractor::create()->extract($status->caption);
  499. $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
  500. $cw = $profile->cw == true ? true : $cw;
  501. $status->is_nsfw = $cw;
  502. $status->visibility = $visibility;
  503. $status->scope = $visibility;
  504. $status->type = 'text';
  505. $status->rendered = Autolink::create()->autolink($status->caption);
  506. $status->entities = json_encode(array_merge([
  507. 'timg' => [
  508. 'version' => 0,
  509. 'bg_id' => 1,
  510. 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
  511. 'length' => strlen($status->caption),
  512. ]
  513. ], $entities), JSON_UNESCAPED_SLASHES);
  514. $status->save();
  515. foreach($tagged as $tg) {
  516. $mt = new MediaTag;
  517. $mt->status_id = $status->id;
  518. $mt->media_id = $status->media->first()->id;
  519. $mt->profile_id = $tg['id'];
  520. $mt->tagged_username = $tg['name'];
  521. $mt->is_public = true;
  522. $mt->metadata = json_encode([
  523. '_v' => 1,
  524. ]);
  525. $mt->save();
  526. MediaTagService::set($mt->status_id, $mt->profile_id);
  527. MediaTagService::sendNotification($mt);
  528. }
  529. Cache::forget('user:account:id:'.$profile->user_id);
  530. Cache::forget('_api:statuses:recent_9:'.$profile->id);
  531. Cache::forget('profile:status_count:'.$profile->id);
  532. return $status->url();
  533. }
  534. public function mediaProcessingCheck(Request $request)
  535. {
  536. $this->validate($request, [
  537. 'id' => 'required|integer|min:1'
  538. ]);
  539. $media = Media::whereUserId($request->user()->id)
  540. ->whereNull('status_id')
  541. ->findOrFail($request->input('id'));
  542. if(config('pixelfed.media_fast_process')) {
  543. return [
  544. 'finished' => true
  545. ];
  546. }
  547. $finished = false;
  548. switch ($media->mime) {
  549. case 'image/jpeg':
  550. case 'image/png':
  551. case 'video/mp4':
  552. $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
  553. break;
  554. default:
  555. # code...
  556. break;
  557. }
  558. return [
  559. 'finished' => $finished
  560. ];
  561. }
  562. public function composeSettings(Request $request)
  563. {
  564. $uid = $request->user()->id;
  565. $default = [
  566. 'default_license' => 1,
  567. 'media_descriptions' => false,
  568. 'max_altext_length' => config_cache('pixelfed.max_altext_length')
  569. ];
  570. $settings = AccountService::settings($uid);
  571. if(isset($settings['other']) && isset($settings['other']['scope'])) {
  572. $s = $settings['compose_settings'];
  573. $s['default_scope'] = $settings['other']['scope'];
  574. $settings['compose_settings'] = $s;
  575. }
  576. return array_merge($default, $settings['compose_settings']);
  577. }
  578. public function createPoll(Request $request)
  579. {
  580. $this->validate($request, [
  581. 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
  582. 'cw' => 'nullable|boolean',
  583. 'visibility' => 'required|string|in:public,private',
  584. 'comments_disabled' => 'nullable',
  585. 'expiry' => 'required|in:60,360,1440,10080',
  586. 'pollOptions' => 'required|array|min:1|max:4'
  587. ]);
  588. abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
  589. abort_if(Status::whereType('poll')
  590. ->whereProfileId($request->user()->profile_id)
  591. ->whereCaption($request->input('caption'))
  592. ->where('created_at', '>', now()->subDays(2))
  593. ->exists()
  594. , 422, 'Duplicate detected.');
  595. $status = new Status;
  596. $status->profile_id = $request->user()->profile_id;
  597. $status->caption = $request->input('caption');
  598. $status->rendered = Autolink::create()->autolink($status->caption);
  599. $status->visibility = 'draft';
  600. $status->scope = 'draft';
  601. $status->type = 'poll';
  602. $status->local = true;
  603. $status->save();
  604. $poll = new Poll;
  605. $poll->status_id = $status->id;
  606. $poll->profile_id = $status->profile_id;
  607. $poll->poll_options = $request->input('pollOptions');
  608. $poll->expires_at = now()->addMinutes($request->input('expiry'));
  609. $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
  610. return 0;
  611. })->toArray();
  612. $poll->save();
  613. $status->visibility = $request->input('visibility');
  614. $status->scope = $request->input('visibility');
  615. $status->save();
  616. NewStatusPipeline::dispatch($status);
  617. return ['url' => $status->url()];
  618. }
  619. }