kopia lustrzana https://github.com/pixelfed/pixelfed
				
				
				
			Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork
						commit
						ca7c2d34f2
					
				|  | @ -10,6 +10,15 @@ | |||
| - Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a)) | ||||
| - Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c)) | ||||
| - Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8)) | ||||
| - Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7)) | ||||
| - Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06)) | ||||
| - Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9)) | ||||
| - Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4)) | ||||
| - Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595)) | ||||
| - Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f)) | ||||
| - Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843)) | ||||
| - Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6)) | ||||
| - Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f)) | ||||
| -  ([](https://github.com/pixelfed/pixelfed/commit/)) | ||||
| 
 | ||||
| ## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13) | ||||
|  |  | |||
|  | @ -0,0 +1,123 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Console\Commands; | ||||
| 
 | ||||
| use App\Instance; | ||||
| use App\Profile; | ||||
| use App\Transformer\ActivityPub\Verb\DeleteActor; | ||||
| use App\User; | ||||
| use App\Util\ActivityPub\HttpSignature; | ||||
| use GuzzleHttp\Client; | ||||
| use GuzzleHttp\Pool; | ||||
| use Illuminate\Console\Command; | ||||
| use League\Fractal; | ||||
| use League\Fractal\Serializer\ArraySerializer; | ||||
| 
 | ||||
| use function Laravel\Prompts\confirm; | ||||
| use function Laravel\Prompts\search; | ||||
| use function Laravel\Prompts\table; | ||||
| 
 | ||||
| class UserAccountDelete extends Command | ||||
| { | ||||
|     /** | ||||
|      * The name and signature of the console command. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $signature = 'app:user-account-delete'; | ||||
| 
 | ||||
|     /** | ||||
|      * The console command description. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $description = 'Federate Account Deletion'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle() | ||||
|     { | ||||
|         $id = search( | ||||
|             label: 'Search for the account to delete by username', | ||||
|             placeholder: 'john.appleseed', | ||||
|             options: fn (string $value) => strlen($value) > 0 | ||||
|                 ? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all() | ||||
|                 : [], | ||||
|         ); | ||||
| 
 | ||||
|         $user = User::withTrashed()->find($id); | ||||
| 
 | ||||
|         table( | ||||
|             ['Username', 'Name', 'Email', 'Created'], | ||||
|             [[$user->username, $user->name, $user->email, $user->created_at]] | ||||
|         ); | ||||
| 
 | ||||
|         $confirmed = confirm( | ||||
|             label: 'Do you want to federate this account deletion?', | ||||
|             default: false, | ||||
|             yes: 'Proceed', | ||||
|             no: 'Cancel', | ||||
|             hint: 'This action is irreversible' | ||||
|         ); | ||||
| 
 | ||||
|         if (! $confirmed) { | ||||
|             $this->error('Aborting...'); | ||||
|             exit; | ||||
|         } | ||||
| 
 | ||||
|         $profile = Profile::withTrashed()->find($user->profile_id); | ||||
| 
 | ||||
|         $fractal = new Fractal\Manager(); | ||||
|         $fractal->setSerializer(new ArraySerializer()); | ||||
|         $resource = new Fractal\Resource\Item($profile, new DeleteActor()); | ||||
|         $activity = $fractal->createData($resource)->toArray(); | ||||
| 
 | ||||
|         $audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched']) | ||||
|             ->where('nodeinfo_last_fetched', '>', now()->subHours(12)) | ||||
|             ->distinct() | ||||
|             ->pluck('shared_inbox'); | ||||
| 
 | ||||
|         $payload = json_encode($activity); | ||||
| 
 | ||||
|         $client = new Client([ | ||||
|             'timeout' => 10, | ||||
|         ]); | ||||
| 
 | ||||
|         $version = config('pixelfed.version'); | ||||
|         $appUrl = config('app.url'); | ||||
|         $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; | ||||
| 
 | ||||
|         $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) { | ||||
|             foreach ($audience as $url) { | ||||
|                 $headers = HttpSignature::sign($profile, $url, $activity, [ | ||||
|                     'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | ||||
|                     'User-Agent' => $userAgent, | ||||
|                 ]); | ||||
|                 yield function () use ($client, $url, $headers, $payload) { | ||||
|                     return $client->postAsync($url, [ | ||||
|                         'curl' => [ | ||||
|                             CURLOPT_HTTPHEADER => $headers, | ||||
|                             CURLOPT_POSTFIELDS => $payload, | ||||
|                             CURLOPT_HEADER => true, | ||||
|                             CURLOPT_SSL_VERIFYPEER => false, | ||||
|                             CURLOPT_SSL_VERIFYHOST => false, | ||||
|                         ], | ||||
|                     ]); | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         $pool = new Pool($client, $requests($audience), [ | ||||
|             'concurrency' => 50, | ||||
|             'fulfilled' => function ($response, $index) { | ||||
|             }, | ||||
|             'rejected' => function ($reason, $index) { | ||||
|             }, | ||||
|         ]); | ||||
| 
 | ||||
|         $promise = $pool->promise(); | ||||
| 
 | ||||
|         $promise->wait(); | ||||
|     } | ||||
| } | ||||
|  | @ -1664,7 +1664,7 @@ class ApiV1Controller extends Controller | |||
|                     ], | ||||
|                     'statuses' => [ | ||||
|                         'characters_reserved_per_url' => 23, | ||||
|                         'max_characters' => (int) config('pixelfed.max_caption_length'), | ||||
|                         'max_characters' => (int) config_cache('pixelfed.max_caption_length'), | ||||
|                         'max_media_attachments' => (int) config('pixelfed.max_album_length'), | ||||
|                     ], | ||||
|                 ], | ||||
|  | @ -3308,7 +3308,7 @@ class ApiV1Controller extends Controller | |||
|         abort_unless($request->user()->tokenCan('write'), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'status' => 'nullable|string', | ||||
|             'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'), | ||||
|             'in_reply_to_id' => 'nullable', | ||||
|             'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'), | ||||
|             'sensitive' => 'nullable', | ||||
|  | @ -4066,7 +4066,7 @@ class ApiV1Controller extends Controller | |||
| 
 | ||||
|         $pid = $request->user()->profile_id; | ||||
| 
 | ||||
|         $ids = Cache::remember('api:v1.1:discover:accounts:popular', 3600, function () { | ||||
|         $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { | ||||
|             return DB::table('profiles') | ||||
|                 ->where('is_private', false) | ||||
|                 ->whereNull('status') | ||||
|  | @ -4075,6 +4075,7 @@ class ApiV1Controller extends Controller | |||
|                 ->get(); | ||||
|         }); | ||||
|         $filters = UserFilterService::filters($pid); | ||||
|         $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); | ||||
|         $ids = $ids->map(function ($profile) { | ||||
|             return AccountService::get($profile->id, true); | ||||
|         }) | ||||
|  | @ -4087,6 +4088,9 @@ class ApiV1Controller extends Controller | |||
|             ->filter(function ($profile) use ($pid) { | ||||
|                 return ! FollowerService::follows($pid, $profile['id'], true); | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($asf) { | ||||
|                 return ! in_array($profile['id'], $asf); | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($filters) { | ||||
|                 return ! in_array($profile['id'], $filters); | ||||
|             }) | ||||
|  |  | |||
|  | @ -473,15 +473,15 @@ class ApiV1Dot1Controller extends Controller | |||
| 	{ | ||||
| 		return [ | ||||
| 			'open' => (bool) config_cache('pixelfed.open_registration'), | ||||
| 			'iara' => config('pixelfed.allow_app_registration') | ||||
| 			'iara' => (bool) config_cache('pixelfed.allow_app_registration'), | ||||
| 		]; | ||||
| 	} | ||||
| 
 | ||||
| 	public function inAppRegistration(Request $request) | ||||
| 	{ | ||||
| 		abort_if($request->user(), 404); | ||||
| 		abort_unless(config_cache('pixelfed.open_registration'), 404); | ||||
| 		abort_unless(config('pixelfed.allow_app_registration'), 404); | ||||
| 		abort_unless((bool) config_cache('pixelfed.open_registration'), 404); | ||||
| 		abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); | ||||
| 		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); | ||||
| 		if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { | ||||
| 			abort_if(BouncerService::checkIp($request->ip()), 404); | ||||
|  | @ -609,8 +609,8 @@ class ApiV1Dot1Controller extends Controller | |||
| 	public function inAppRegistrationConfirm(Request $request) | ||||
| 	{ | ||||
| 		abort_if($request->user(), 404); | ||||
| 		abort_unless(config_cache('pixelfed.open_registration'), 404); | ||||
| 		abort_unless(config('pixelfed.allow_app_registration'), 404); | ||||
| 		abort_unless((bool) config_cache('pixelfed.open_registration'), 404); | ||||
| 		abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404); | ||||
| 		abort_unless($request->hasHeader('X-PIXELFED-APP'), 403); | ||||
| 		if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { | ||||
| 			abort_if(BouncerService::checkIp($request->ip()), 404); | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ class ApiV2Controller extends Controller | |||
|                         'max_featured_tags' => 0, | ||||
|                     ], | ||||
|                     'statuses' => [ | ||||
|                         'max_characters' => (int) config('pixelfed.max_caption_length'), | ||||
|                         'max_characters' => (int) config_cache('pixelfed.max_caption_length'), | ||||
|                         'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), | ||||
|                         'characters_reserved_per_url' => 23 | ||||
|                     ], | ||||
|  |  | |||
|  | @ -2,23 +2,18 @@ | |||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use Auth; | ||||
| use DB; | ||||
| use Cache; | ||||
| 
 | ||||
| use App\Comment; | ||||
| use App\Jobs\CommentPipeline\CommentPipeline; | ||||
| use App\Jobs\StatusPipeline\NewStatusPipeline; | ||||
| use App\Util\Lexer\Autolink; | ||||
| use App\Profile; | ||||
| use App\Status; | ||||
| use App\UserFilter; | ||||
| use League\Fractal; | ||||
| use App\Transformer\Api\StatusTransformer; | ||||
| use League\Fractal\Serializer\ArraySerializer; | ||||
| use League\Fractal\Pagination\IlluminatePaginatorAdapter; | ||||
| use App\Services\StatusService; | ||||
| use App\Status; | ||||
| use App\Transformer\Api\StatusTransformer; | ||||
| use App\UserFilter; | ||||
| use App\Util\Lexer\Autolink; | ||||
| use Auth; | ||||
| use DB; | ||||
| use Illuminate\Http\Request; | ||||
| use League\Fractal; | ||||
| use League\Fractal\Serializer\ArraySerializer; | ||||
| 
 | ||||
| class CommentController extends Controller | ||||
| { | ||||
|  | @ -33,9 +28,9 @@ class CommentController extends Controller | |||
|             abort(403); | ||||
|         } | ||||
|         $this->validate($request, [ | ||||
|             'item'    => 'required|integer|min:1', | ||||
|             'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'), | ||||
|             'sensitive' => 'nullable|boolean' | ||||
|             'item' => 'required|integer|min:1', | ||||
|             'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'), | ||||
|             'sensitive' => 'nullable|boolean', | ||||
|         ]); | ||||
|         $comment = $request->input('comment'); | ||||
|         $statusId = $request->input('item'); | ||||
|  | @ -45,7 +40,7 @@ class CommentController extends Controller | |||
|         $profile = $user->profile; | ||||
|         $status = Status::findOrFail($statusId); | ||||
| 
 | ||||
|         if($status->comments_disabled == true) { | ||||
|         if ($status->comments_disabled == true) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -55,11 +50,11 @@ class CommentController extends Controller | |||
|             ->whereFilterableId($profile->id) | ||||
|             ->exists(); | ||||
| 
 | ||||
|         if($filtered == true) { | ||||
|         if ($filtered == true) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) { | ||||
|         $reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) { | ||||
|             $scope = $profile->is_private == true ? 'private' : 'public'; | ||||
|             $autolink = Autolink::create()->autolink($comment); | ||||
|             $reply = new Status(); | ||||
|  |  | |||
|  | @ -2,59 +2,38 @@ | |||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use Auth, Cache, DB, Storage, URL; | ||||
| use Carbon\Carbon; | ||||
| use App\{ | ||||
|     Avatar, | ||||
|     Collection, | ||||
|     CollectionItem, | ||||
|     Hashtag, | ||||
|     Like, | ||||
|     Media, | ||||
|     MediaTag, | ||||
|     Notification, | ||||
|     Profile, | ||||
|     Place, | ||||
|     Status, | ||||
|     UserFilter, | ||||
|     UserSetting | ||||
| }; | ||||
| use App\Models\Poll; | ||||
| use App\Transformer\Api\{ | ||||
|     MediaTransformer, | ||||
|     MediaDraftTransformer, | ||||
|     StatusTransformer, | ||||
|     StatusStatelessTransformer | ||||
| }; | ||||
| use League\Fractal; | ||||
| use App\Util\Media\Filter; | ||||
| use League\Fractal\Serializer\ArraySerializer; | ||||
| use League\Fractal\Pagination\IlluminatePaginatorAdapter; | ||||
| use App\Jobs\AvatarPipeline\AvatarOptimize; | ||||
| use App\Collection; | ||||
| use App\CollectionItem; | ||||
| use App\Hashtag; | ||||
| use App\Jobs\ImageOptimizePipeline\ImageOptimize; | ||||
| use App\Jobs\ImageOptimizePipeline\ImageThumbnail; | ||||
| use App\Jobs\StatusPipeline\NewStatusPipeline; | ||||
| use App\Jobs\VideoPipeline\{ | ||||
|     VideoOptimize, | ||||
|     VideoPostProcess, | ||||
|     VideoThumbnail | ||||
| }; | ||||
| use App\Jobs\VideoPipeline\VideoThumbnail; | ||||
| use App\Media; | ||||
| use App\MediaTag; | ||||
| use App\Models\Poll; | ||||
| use App\Notification; | ||||
| use App\Profile; | ||||
| use App\Services\AccountService; | ||||
| use App\Services\CollectionService; | ||||
| use App\Services\NotificationService; | ||||
| use App\Services\MediaPathService; | ||||
| use App\Services\MediaBlocklistService; | ||||
| use App\Services\MediaPathService; | ||||
| use App\Services\MediaStorageService; | ||||
| use App\Services\MediaTagService; | ||||
| use App\Services\StatusService; | ||||
| use App\Services\SnowflakeService; | ||||
| use Illuminate\Support\Str; | ||||
| use App\Util\Lexer\Autolink; | ||||
| use App\Util\Lexer\Extractor; | ||||
| use App\Util\Media\License; | ||||
| use Image; | ||||
| use App\Services\UserRoleService; | ||||
| use App\Status; | ||||
| use App\Transformer\Api\MediaTransformer; | ||||
| use App\UserFilter; | ||||
| use App\Util\Lexer\Autolink; | ||||
| use App\Util\Media\Filter; | ||||
| use App\Util\Media\License; | ||||
| use Auth; | ||||
| use Cache; | ||||
| use DB; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Str; | ||||
| use League\Fractal; | ||||
| use League\Fractal\Serializer\ArraySerializer; | ||||
| 
 | ||||
| class ComposeController extends Controller | ||||
| { | ||||
|  | @ -74,30 +53,30 @@ class ComposeController extends Controller | |||
| 
 | ||||
|     public function mediaUpload(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'file.*' => [ | ||||
|                 'required_without:file', | ||||
|                 'mimetypes:' . config_cache('pixelfed.media_types'), | ||||
|                 'max:' . config_cache('pixelfed.max_photo_size'), | ||||
|                 'mimetypes:'.config_cache('pixelfed.media_types'), | ||||
|                 'max:'.config_cache('pixelfed.max_photo_size'), | ||||
|             ], | ||||
|             'file' => [ | ||||
|                 'required_without:file.*', | ||||
|                 'mimetypes:' . config_cache('pixelfed.media_types'), | ||||
|                 'max:' . config_cache('pixelfed.max_photo_size'), | ||||
|                 'mimetypes:'.config_cache('pixelfed.media_types'), | ||||
|                 'max:'.config_cache('pixelfed.max_photo_size'), | ||||
|             ], | ||||
|             'filter_name' => 'nullable|string|max:24', | ||||
|             'filter_class' => 'nullable|alpha_dash|max:24' | ||||
|             'filter_class' => 'nullable|alpha_dash|max:24', | ||||
|         ]); | ||||
| 
 | ||||
|         $user = Auth::user(); | ||||
|         $profile = $user->profile; | ||||
|         abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $limitKey = 'compose:rate-limit:media-upload:' . $user->id; | ||||
|         $limitKey = 'compose:rate-limit:media-upload:'.$user->id; | ||||
|         $limitTtl = now()->addMinutes(15); | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { | ||||
|             $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); | ||||
| 
 | ||||
|             return $dailyLimit >= 1250; | ||||
|  | @ -105,8 +84,8 @@ class ComposeController extends Controller | |||
| 
 | ||||
|         abort_if($limitReached == true, 429); | ||||
| 
 | ||||
|         if(config_cache('pixelfed.enforce_account_limit') == true) { | ||||
|             $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { | ||||
|         if (config_cache('pixelfed.enforce_account_limit') == true) { | ||||
|             $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function () use ($user) { | ||||
|                 return Media::whereUserId($user->id)->sum('size') / 1000; | ||||
|             }); | ||||
|             $limit = (int) config_cache('pixelfed.max_account_size'); | ||||
|  | @ -144,24 +123,24 @@ class ComposeController extends Controller | |||
|         $media->version = 3; | ||||
|         $media->save(); | ||||
| 
 | ||||
|         $preview_url = $media->url() . '?v=' . time(); | ||||
|         $url = $media->url() . '?v=' . time(); | ||||
|         $preview_url = $media->url().'?v='.time(); | ||||
|         $url = $media->url().'?v='.time(); | ||||
| 
 | ||||
|         switch ($media->mime) { | ||||
|             case 'image/jpeg': | ||||
|             case 'image/png': | ||||
|             case 'image/webp': | ||||
|             ImageOptimize::dispatch($media)->onQueue('mmo'); | ||||
|             break; | ||||
|                 ImageOptimize::dispatch($media)->onQueue('mmo'); | ||||
|                 break; | ||||
| 
 | ||||
|             case 'video/mp4': | ||||
|             VideoThumbnail::dispatch($media)->onQueue('mmo'); | ||||
|             $preview_url = '/storage/no-preview.png'; | ||||
|             $url = '/storage/no-preview.png'; | ||||
|             break; | ||||
|                 VideoThumbnail::dispatch($media)->onQueue('mmo'); | ||||
|                 $preview_url = '/storage/no-preview.png'; | ||||
|                 $url = '/storage/no-preview.png'; | ||||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|             break; | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         Cache::forget($limitKey); | ||||
|  | @ -169,6 +148,7 @@ class ComposeController extends Controller | |||
|         $res = $this->fractal->createData($resource)->toArray(); | ||||
|         $res['preview_url'] = $preview_url; | ||||
|         $res['url'] = $url; | ||||
| 
 | ||||
|         return response()->json($res); | ||||
|     } | ||||
| 
 | ||||
|  | @ -176,21 +156,21 @@ class ComposeController extends Controller | |||
|     { | ||||
|         $this->validate($request, [ | ||||
|             'id' => 'required', | ||||
|             'file' => function() { | ||||
|             'file' => function () { | ||||
|                 return [ | ||||
|                     'required', | ||||
|                     'mimetypes:' . config_cache('pixelfed.media_types'), | ||||
|                     'max:' . config_cache('pixelfed.max_photo_size'), | ||||
|                     'mimetypes:'.config_cache('pixelfed.media_types'), | ||||
|                     'max:'.config_cache('pixelfed.max_photo_size'), | ||||
|                 ]; | ||||
|             }, | ||||
|         ]); | ||||
| 
 | ||||
|         $user = Auth::user(); | ||||
|         abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $limitKey = 'compose:rate-limit:media-updates:' . $user->id; | ||||
|         $limitKey = 'compose:rate-limit:media-updates:'.$user->id; | ||||
|         $limitTtl = now()->addMinutes(15); | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { | ||||
|             $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count(); | ||||
| 
 | ||||
|             return $dailyLimit >= 1500; | ||||
|  | @ -202,9 +182,9 @@ class ComposeController extends Controller | |||
|         $id = $request->input('id'); | ||||
| 
 | ||||
|         $media = Media::whereUserId($user->id) | ||||
|         ->whereProfileId($user->profile_id) | ||||
|         ->whereNull('status_id') | ||||
|         ->findOrFail($id); | ||||
|             ->whereProfileId($user->profile_id) | ||||
|             ->whereNull('status_id') | ||||
|             ->findOrFail($id); | ||||
| 
 | ||||
|         $media->save(); | ||||
| 
 | ||||
|  | @ -214,47 +194,48 @@ class ComposeController extends Controller | |||
|         $dir = implode('/', $fragments); | ||||
|         $path = $photo->storePubliclyAs($dir, $name); | ||||
|         $res = [ | ||||
|             'url' => $media->url() . '?v=' . time() | ||||
|             'url' => $media->url().'?v='.time(), | ||||
|         ]; | ||||
|         ImageOptimize::dispatch($media)->onQueue('mmo'); | ||||
|         Cache::forget($limitKey); | ||||
| 
 | ||||
|         return $res; | ||||
|     } | ||||
| 
 | ||||
|     public function mediaDelete(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'id' => 'required|integer|min:1|exists:media,id' | ||||
|             'id' => 'required|integer|min:1|exists:media,id', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $media = Media::whereNull('status_id') | ||||
|         ->whereUserId(Auth::id()) | ||||
|         ->findOrFail($request->input('id')); | ||||
|             ->whereUserId(Auth::id()) | ||||
|             ->findOrFail($request->input('id')); | ||||
| 
 | ||||
|         MediaStorageService::delete($media, true); | ||||
| 
 | ||||
|         return response()->json([ | ||||
|             'msg' => 'Successfully deleted', | ||||
|             'code' => 200 | ||||
|             'code' => 200, | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     public function searchTag(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'q' => 'required|string|min:1|max:50' | ||||
|             'q' => 'required|string|min:1|max:50', | ||||
|         ]); | ||||
| 
 | ||||
|         $q = $request->input('q'); | ||||
| 
 | ||||
|         if(Str::of($q)->startsWith('@')) { | ||||
|             if(strlen($q) < 3) { | ||||
|         if (Str::of($q)->startsWith('@')) { | ||||
|             if (strlen($q) < 3) { | ||||
|                 return []; | ||||
|             } | ||||
|             $q = mb_substr($q, 1); | ||||
|  | @ -262,7 +243,7 @@ class ComposeController extends Controller | |||
| 
 | ||||
|         $user = $request->user(); | ||||
| 
 | ||||
|         abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $blocked = UserFilter::whereFilterableType('App\Profile') | ||||
|             ->whereFilterType('block') | ||||
|  | @ -271,34 +252,34 @@ class ComposeController extends Controller | |||
| 
 | ||||
|         $blocked->push($request->user()->profile_id); | ||||
| 
 | ||||
|         $results = Profile::select('id','domain','username') | ||||
|         $results = Profile::select('id', 'domain', 'username') | ||||
|             ->whereNotIn('id', $blocked) | ||||
|             ->whereNull('domain') | ||||
|             ->where('username','like','%'.$q.'%') | ||||
|             ->where('username', 'like', '%'.$q.'%') | ||||
|             ->limit(15) | ||||
|             ->get() | ||||
|             ->map(function($r) { | ||||
|             ->map(function ($r) { | ||||
|                 return [ | ||||
|                     'id' => (string) $r->id, | ||||
|                     'name' => $r->username, | ||||
|                     'privacy' => true, | ||||
|                     'avatar' => $r->avatarUrl() | ||||
|                     'avatar' => $r->avatarUrl(), | ||||
|                 ]; | ||||
|         }); | ||||
|             }); | ||||
| 
 | ||||
|         return $results; | ||||
|     } | ||||
| 
 | ||||
|     public function searchUntag(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'status_id' => 'required', | ||||
|             'profile_id' => 'required' | ||||
|             'profile_id' => 'required', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $user = $request->user(); | ||||
|         $status_id = $request->input('status_id'); | ||||
|  | @ -310,7 +291,7 @@ class ComposeController extends Controller | |||
|             ->whereProfileId($profile_id) | ||||
|             ->first(); | ||||
| 
 | ||||
|         if(!$tag) { | ||||
|         if (! $tag) { | ||||
|             return []; | ||||
|         } | ||||
|         Notification::whereItemType('App\MediaTag') | ||||
|  | @ -326,37 +307,38 @@ class ComposeController extends Controller | |||
| 
 | ||||
|     public function searchLocation(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
|         $this->validate($request, [ | ||||
|             'q' => 'required|string|max:100' | ||||
|             'q' => 'required|string|max:100', | ||||
|         ]); | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         $pid = $request->user()->profile_id; | ||||
|         abort_if(!$pid, 400); | ||||
|         abort_if(! $pid, 400); | ||||
|         $q = e($request->input('q')); | ||||
| 
 | ||||
|         $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() { | ||||
|         $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function () { | ||||
|             $minId = SnowflakeService::byDate(now()->subDays(290)); | ||||
|             if(config('database.default') == 'pgsql') { | ||||
|             if (config('database.default') == 'pgsql') { | ||||
|                 return Status::selectRaw('id, place_id, count(place_id) as pc') | ||||
|                 ->whereNotNull('place_id') | ||||
|                 ->where('id', '>', $minId) | ||||
|                 ->orderByDesc('pc') | ||||
|                 ->groupBy(['place_id', 'id']) | ||||
|                 ->limit(400) | ||||
|                 ->get() | ||||
|                 ->filter(function($post) { | ||||
|                     return $post; | ||||
|                 }) | ||||
|                 ->map(function($place) { | ||||
|                     return [ | ||||
|                         'id' => $place->place_id, | ||||
|                         'count' => $place->pc | ||||
|                     ]; | ||||
|                 }) | ||||
|                 ->unique('id') | ||||
|                 ->values(); | ||||
|                     ->whereNotNull('place_id') | ||||
|                     ->where('id', '>', $minId) | ||||
|                     ->orderByDesc('pc') | ||||
|                     ->groupBy(['place_id', 'id']) | ||||
|                     ->limit(400) | ||||
|                     ->get() | ||||
|                     ->filter(function ($post) { | ||||
|                         return $post; | ||||
|                     }) | ||||
|                     ->map(function ($place) { | ||||
|                         return [ | ||||
|                             'id' => $place->place_id, | ||||
|                             'count' => $place->pc, | ||||
|                         ]; | ||||
|                     }) | ||||
|                     ->unique('id') | ||||
|                     ->values(); | ||||
|             } | ||||
| 
 | ||||
|             return Status::selectRaw('id, place_id, count(place_id) as pc') | ||||
|                 ->whereNotNull('place_id') | ||||
|                 ->where('id', '>', $minId) | ||||
|  | @ -364,57 +346,58 @@ class ComposeController extends Controller | |||
|                 ->orderByDesc('pc') | ||||
|                 ->limit(400) | ||||
|                 ->get() | ||||
|                 ->filter(function($post) { | ||||
|                 ->filter(function ($post) { | ||||
|                     return $post; | ||||
|                 }) | ||||
|                 ->map(function($place) { | ||||
|                 ->map(function ($place) { | ||||
|                     return [ | ||||
|                         'id' => $place->place_id, | ||||
|                         'count' => $place->pc | ||||
|                         'count' => $place->pc, | ||||
|                     ]; | ||||
|                 }); | ||||
|         }); | ||||
|         $q = '%' . $q . '%'; | ||||
|         $q = '%'.$q.'%'; | ||||
|         $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like'; | ||||
| 
 | ||||
|         $places = DB::table('places') | ||||
|         ->where('name', $wildcard, $q) | ||||
|         ->limit((strlen($q) > 5 ? 360 : 30)) | ||||
|         ->get() | ||||
|         ->sortByDesc(function($place, $key) use($popular) { | ||||
|             return $popular->filter(function($p) use($place) { | ||||
|                 return $p['id'] == $place->id; | ||||
|             })->map(function($p) use($place) { | ||||
|                 return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; | ||||
|             })->values(); | ||||
|         }) | ||||
|         ->map(function($r) { | ||||
|             return [ | ||||
|                 'id' => $r->id, | ||||
|                 'name' => $r->name, | ||||
|                 'country' => $r->country, | ||||
|                 'url'   => url('/discover/places/' . $r->id . '/' . $r->slug) | ||||
|             ]; | ||||
|         }) | ||||
|         ->values() | ||||
|         ->all(); | ||||
|             ->where('name', $wildcard, $q) | ||||
|             ->limit((strlen($q) > 5 ? 360 : 30)) | ||||
|             ->get() | ||||
|             ->sortByDesc(function ($place, $key) use ($popular) { | ||||
|                 return $popular->filter(function ($p) use ($place) { | ||||
|                     return $p['id'] == $place->id; | ||||
|                 })->map(function ($p) use ($place) { | ||||
|                     return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1; | ||||
|                 })->values(); | ||||
|             }) | ||||
|             ->map(function ($r) { | ||||
|                 return [ | ||||
|                     'id' => $r->id, | ||||
|                     'name' => $r->name, | ||||
|                     'country' => $r->country, | ||||
|                     'url' => url('/discover/places/'.$r->id.'/'.$r->slug), | ||||
|                 ]; | ||||
|             }) | ||||
|             ->values() | ||||
|             ->all(); | ||||
| 
 | ||||
|         return $places; | ||||
|     } | ||||
| 
 | ||||
|     public function searchMentionAutocomplete(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'q' => 'required|string|min:2|max:50' | ||||
|             'q' => 'required|string|min:2|max:50', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $q = $request->input('q'); | ||||
| 
 | ||||
|         if(Str::of($q)->startsWith('@')) { | ||||
|             if(strlen($q) < 3) { | ||||
|         if (Str::of($q)->startsWith('@')) { | ||||
|             if (strlen($q) < 3) { | ||||
|                 return []; | ||||
|             } | ||||
|         } | ||||
|  | @ -426,32 +409,33 @@ class ComposeController extends Controller | |||
| 
 | ||||
|         $blocked->push($request->user()->profile_id); | ||||
| 
 | ||||
|         $results = Profile::select('id','domain','username') | ||||
|         $results = Profile::select('id', 'domain', 'username') | ||||
|             ->whereNotIn('id', $blocked) | ||||
|             ->where('username','like','%'.$q.'%') | ||||
|             ->where('username', 'like', '%'.$q.'%') | ||||
|             ->groupBy('id', 'domain') | ||||
|             ->limit(15) | ||||
|             ->get() | ||||
|             ->map(function($profile) { | ||||
|             ->map(function ($profile) { | ||||
|                 $username = $profile->domain ? substr($profile->username, 1) : $profile->username; | ||||
| 
 | ||||
|                 return [ | ||||
|                     'key' => '@' . str_limit($username, 30), | ||||
|                     'key' => '@'.str_limit($username, 30), | ||||
|                     'value' => $username, | ||||
|                 ]; | ||||
|         }); | ||||
|             }); | ||||
| 
 | ||||
|         return $results; | ||||
|     } | ||||
| 
 | ||||
|     public function searchHashtagAutocomplete(Request $request) | ||||
|     { | ||||
|         abort_if(!$request->user(), 403); | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'q' => 'required|string|min:2|max:50' | ||||
|             'q' => 'required|string|min:2|max:50', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $q = $request->input('q'); | ||||
| 
 | ||||
|  | @ -461,12 +445,12 @@ class ComposeController extends Controller | |||
|             ->whereIsBanned(false) | ||||
|             ->limit(5) | ||||
|             ->get() | ||||
|             ->map(function($tag) { | ||||
|             ->map(function ($tag) { | ||||
|                 return [ | ||||
|                     'key' => '#' . $tag->slug, | ||||
|                     'value' => $tag->slug | ||||
|                     'key' => '#'.$tag->slug, | ||||
|                     'value' => $tag->slug, | ||||
|                 ]; | ||||
|         }); | ||||
|             }); | ||||
| 
 | ||||
|         return $results; | ||||
|     } | ||||
|  | @ -474,8 +458,8 @@ class ComposeController extends Controller | |||
|     public function store(Request $request) | ||||
|     { | ||||
|         $this->validate($request, [ | ||||
|             'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), | ||||
|             'media.*'   => 'required', | ||||
|             'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), | ||||
|             'media.*' => 'required', | ||||
|             'media.*.id' => 'required|integer|min:1', | ||||
|             'media.*.filter_class' => 'nullable|alpha_dash|max:30', | ||||
|             'media.*.license' => 'nullable|string|max:140', | ||||
|  | @ -491,14 +475,14 @@ class ComposeController extends Controller | |||
|             // 'optimize_media' => 'nullable'
 | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         if(config('costar.enabled') == true) { | ||||
|         if (config('costar.enabled') == true) { | ||||
|             $blockedKeywords = config('costar.keyword.block'); | ||||
|             if($blockedKeywords !== null && $request->caption) { | ||||
|             if ($blockedKeywords !== null && $request->caption) { | ||||
|                 $keywords = config('costar.keyword.block'); | ||||
|                 foreach($keywords as $kw) { | ||||
|                     if(Str::contains($request->caption, $kw) == true) { | ||||
|                 foreach ($keywords as $kw) { | ||||
|                     if (Str::contains($request->caption, $kw) == true) { | ||||
|                         abort(400, 'Invalid object'); | ||||
|                     } | ||||
|                 } | ||||
|  | @ -508,9 +492,9 @@ class ComposeController extends Controller | |||
|         $user = $request->user(); | ||||
|         $profile = $user->profile; | ||||
| 
 | ||||
|         $limitKey = 'compose:rate-limit:store:' . $user->id; | ||||
|         $limitKey = 'compose:rate-limit:store:'.$user->id; | ||||
|         $limitTtl = now()->addMinutes(15); | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) { | ||||
|         $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) { | ||||
|             $dailyLimit = Status::whereProfileId($user->profile_id) | ||||
|                 ->whereNull('in_reply_to_id') | ||||
|                 ->whereNull('reblog_of_id') | ||||
|  | @ -534,12 +518,12 @@ class ComposeController extends Controller | |||
|         $tagged = $request->input('tagged'); | ||||
|         $optimize_media = (bool) $request->input('optimize_media'); | ||||
| 
 | ||||
|         foreach($medias as $k => $media) { | ||||
|             if($k + 1 > config_cache('pixelfed.max_album_length')) { | ||||
|         foreach ($medias as $k => $media) { | ||||
|             if ($k + 1 > config_cache('pixelfed.max_album_length')) { | ||||
|                 continue; | ||||
|             } | ||||
|             $m = Media::findOrFail($media['id']); | ||||
|             if($m->profile_id !== $profile->id || $m->status_id) { | ||||
|             if ($m->profile_id !== $profile->id || $m->status_id) { | ||||
|                 abort(403, 'Invalid media id'); | ||||
|             } | ||||
|             $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null; | ||||
|  | @ -547,7 +531,7 @@ class ComposeController extends Controller | |||
|             $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null; | ||||
|             $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k; | ||||
| 
 | ||||
|             if($cw == true || $profile->cw == true) { | ||||
|             if ($cw == true || $profile->cw == true) { | ||||
|                 $m->is_nsfw = $cw; | ||||
|                 $status->is_nsfw = $cw; | ||||
|             } | ||||
|  | @ -560,19 +544,19 @@ class ComposeController extends Controller | |||
| 
 | ||||
|         $mediaType = StatusController::mimeTypeCheck($mimes); | ||||
| 
 | ||||
|         if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { | ||||
|         if (in_array($mediaType, ['photo', 'video', 'photo:album']) == false) { | ||||
|             abort(400, __('exception.compose.invalid.album')); | ||||
|         } | ||||
| 
 | ||||
|         if($place && is_array($place)) { | ||||
|         if ($place && is_array($place)) { | ||||
|             $status->place_id = $place['id']; | ||||
|         } | ||||
| 
 | ||||
|         if($request->filled('comments_disabled')) { | ||||
|         if ($request->filled('comments_disabled')) { | ||||
|             $status->comments_disabled = (bool) $request->input('comments_disabled'); | ||||
|         } | ||||
| 
 | ||||
|         if($request->filled('spoiler_text') && $cw) { | ||||
|         if ($request->filled('spoiler_text') && $cw) { | ||||
|             $status->cw_summary = $request->input('spoiler_text'); | ||||
|         } | ||||
| 
 | ||||
|  | @ -583,7 +567,7 @@ class ComposeController extends Controller | |||
|         $status->profile_id = $profile->id; | ||||
|         $status->save(); | ||||
| 
 | ||||
|         foreach($attachments as $media) { | ||||
|         foreach ($attachments as $media) { | ||||
|             $media->status_id = $status->id; | ||||
|             $media->save(); | ||||
|         } | ||||
|  | @ -597,7 +581,7 @@ class ComposeController extends Controller | |||
|         $status->type = $mediaType; | ||||
|         $status->save(); | ||||
| 
 | ||||
|         foreach($tagged as $tg) { | ||||
|         foreach ($tagged as $tg) { | ||||
|             $mt = new MediaTag; | ||||
|             $mt->status_id = $status->id; | ||||
|             $mt->media_id = $status->media->first()->id; | ||||
|  | @ -612,17 +596,17 @@ class ComposeController extends Controller | |||
|             MediaTagService::sendNotification($mt); | ||||
|         } | ||||
| 
 | ||||
|         if($request->filled('collections')) { | ||||
|         if ($request->filled('collections')) { | ||||
|             $collections = Collection::whereProfileId($profile->id) | ||||
|                 ->find($request->input('collections')) | ||||
|                 ->each(function($collection) use($status) { | ||||
|                 ->each(function ($collection) use ($status) { | ||||
|                     $count = $collection->items()->count(); | ||||
|                     CollectionItem::firstOrCreate([ | ||||
|                         'collection_id' => $collection->id, | ||||
|                         'object_type' => 'App\Status', | ||||
|                         'object_id' => $status->id | ||||
|                         'object_id' => $status->id, | ||||
|                     ], [ | ||||
|                         'order' => $count | ||||
|                         'order' => $count, | ||||
|                     ]); | ||||
| 
 | ||||
|                     CollectionService::addItem( | ||||
|  | @ -643,7 +627,7 @@ class ComposeController extends Controller | |||
|         Cache::forget('profile:status_count:'.$profile->id); | ||||
|         Cache::forget('status:transformer:media:attachments:'.$status->id); | ||||
|         Cache::forget($user->storageUsedKey()); | ||||
|         Cache::forget('profile:embed:' . $status->profile_id); | ||||
|         Cache::forget('profile:embed:'.$status->profile_id); | ||||
|         Cache::forget($limitKey); | ||||
| 
 | ||||
|         return $status->url(); | ||||
|  | @ -653,7 +637,7 @@ class ComposeController extends Controller | |||
|     { | ||||
|         abort_unless(config('exp.top'), 404); | ||||
|         $this->validate($request, [ | ||||
|             'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), | ||||
|             'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), | ||||
|             'cw' => 'nullable|boolean', | ||||
|             'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10', | ||||
|             'place' => 'nullable', | ||||
|  | @ -661,14 +645,14 @@ class ComposeController extends Controller | |||
|             'tagged' => 'nullable', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         if(config('costar.enabled') == true) { | ||||
|         if (config('costar.enabled') == true) { | ||||
|             $blockedKeywords = config('costar.keyword.block'); | ||||
|             if($blockedKeywords !== null && $request->caption) { | ||||
|             if ($blockedKeywords !== null && $request->caption) { | ||||
|                 $keywords = config('costar.keyword.block'); | ||||
|                 foreach($keywords as $kw) { | ||||
|                     if(Str::contains($request->caption, $kw) == true) { | ||||
|                 foreach ($keywords as $kw) { | ||||
|                     if (Str::contains($request->caption, $kw) == true) { | ||||
|                         abort(400, 'Invalid object'); | ||||
|                     } | ||||
|                 } | ||||
|  | @ -683,11 +667,11 @@ class ComposeController extends Controller | |||
|         $cw = $request->input('cw'); | ||||
|         $tagged = $request->input('tagged'); | ||||
| 
 | ||||
|         if($place && is_array($place)) { | ||||
|         if ($place && is_array($place)) { | ||||
|             $status->place_id = $place['id']; | ||||
|         } | ||||
| 
 | ||||
|         if($request->filled('comments_disabled')) { | ||||
|         if ($request->filled('comments_disabled')) { | ||||
|             $status->comments_disabled = (bool) $request->input('comments_disabled'); | ||||
|         } | ||||
| 
 | ||||
|  | @ -707,11 +691,11 @@ class ComposeController extends Controller | |||
|                 'bg_id' => 1, | ||||
|                 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3', | ||||
|                 'length' => strlen($status->caption), | ||||
|             ] | ||||
|             ], | ||||
|         ], $entities), JSON_UNESCAPED_SLASHES); | ||||
|         $status->save(); | ||||
| 
 | ||||
|         foreach($tagged as $tg) { | ||||
|         foreach ($tagged as $tg) { | ||||
|             $mt = new MediaTag; | ||||
|             $mt->status_id = $status->id; | ||||
|             $mt->media_id = $status->media->first()->id; | ||||
|  | @ -726,7 +710,6 @@ class ComposeController extends Controller | |||
|             MediaTagService::sendNotification($mt); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         Cache::forget('user:account:id:'.$profile->user_id); | ||||
|         Cache::forget('_api:statuses:recent_9:'.$profile->id); | ||||
|         Cache::forget('profile:status_count:'.$profile->id); | ||||
|  | @ -737,18 +720,18 @@ class ComposeController extends Controller | |||
|     public function mediaProcessingCheck(Request $request) | ||||
|     { | ||||
|         $this->validate($request, [ | ||||
|             'id' => 'required|integer|min:1' | ||||
|             'id' => 'required|integer|min:1', | ||||
|         ]); | ||||
| 
 | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $media = Media::whereUserId($request->user()->id) | ||||
|             ->whereNull('status_id') | ||||
|             ->findOrFail($request->input('id')); | ||||
| 
 | ||||
|         if(config('pixelfed.media_fast_process')) { | ||||
|         if (config('pixelfed.media_fast_process')) { | ||||
|             return [ | ||||
|                 'finished' => true | ||||
|                 'finished' => true, | ||||
|             ]; | ||||
|         } | ||||
| 
 | ||||
|  | @ -762,27 +745,27 @@ class ComposeController extends Controller | |||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|                 # code...
 | ||||
|                 // code...
 | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         return [ | ||||
|             'finished' => $finished | ||||
|             'finished' => $finished, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     public function composeSettings(Request $request) | ||||
|     { | ||||
|         $uid = $request->user()->id; | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         $default = [ | ||||
|             'default_license' => 1, | ||||
|             'media_descriptions' => false, | ||||
|             'max_altext_length' => config_cache('pixelfed.max_altext_length') | ||||
|             'max_altext_length' => config_cache('pixelfed.max_altext_length'), | ||||
|         ]; | ||||
|         $settings = AccountService::settings($uid); | ||||
|         if(isset($settings['other']) && isset($settings['other']['scope'])) { | ||||
|         if (isset($settings['other']) && isset($settings['other']['scope'])) { | ||||
|             $s = $settings['compose_settings']; | ||||
|             $s['default_scope'] = $settings['other']['scope']; | ||||
|             $settings['compose_settings'] = $s; | ||||
|  | @ -794,23 +777,22 @@ class ComposeController extends Controller | |||
|     public function createPoll(Request $request) | ||||
|     { | ||||
|         $this->validate($request, [ | ||||
|             'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500), | ||||
|             'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500), | ||||
|             'cw' => 'nullable|boolean', | ||||
|             'visibility' => 'required|string|in:public,private', | ||||
|             'comments_disabled' => 'nullable', | ||||
|             'expiry' => 'required|in:60,360,1440,10080', | ||||
|             'pollOptions' => 'required|array|min:1|max:4' | ||||
|             'pollOptions' => 'required|array|min:1|max:4', | ||||
|         ]); | ||||
|         abort(404); | ||||
|         abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled'); | ||||
|         abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
|         abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action'); | ||||
| 
 | ||||
|         abort_if(Status::whereType('poll') | ||||
|             ->whereProfileId($request->user()->profile_id) | ||||
|             ->whereCaption($request->input('caption')) | ||||
|             ->where('created_at', '>', now()->subDays(2)) | ||||
|             ->exists() | ||||
|         , 422, 'Duplicate detected.'); | ||||
|             ->exists(), 422, 'Duplicate detected.'); | ||||
| 
 | ||||
|         $status = new Status; | ||||
|         $status->profile_id = $request->user()->profile_id; | ||||
|  | @ -827,7 +809,7 @@ class ComposeController extends Controller | |||
|         $poll->profile_id = $status->profile_id; | ||||
|         $poll->poll_options = $request->input('pollOptions'); | ||||
|         $poll->expires_at = now()->addMinutes($request->input('expiry')); | ||||
|         $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { | ||||
|         $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) { | ||||
|             return 0; | ||||
|         })->toArray(); | ||||
|         $poll->save(); | ||||
|  |  | |||
|  | @ -5,8 +5,11 @@ namespace App\Http\Controllers; | |||
| use App\Hashtag; | ||||
| use App\Instance; | ||||
| use App\Like; | ||||
| use App\Services\AccountService; | ||||
| use App\Services\AdminShadowFilterService; | ||||
| use App\Services\BookmarkService; | ||||
| use App\Services\ConfigCacheService; | ||||
| use App\Services\FollowerService; | ||||
| use App\Services\HashtagService; | ||||
| use App\Services\LikeService; | ||||
| use App\Services\ReblogService; | ||||
|  | @ -377,4 +380,44 @@ class DiscoverController extends Controller | |||
| 
 | ||||
|         return $res; | ||||
|     } | ||||
| 
 | ||||
|     public function discoverAccountsPopular(Request $request) | ||||
|     { | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $pid = $request->user()->profile_id; | ||||
| 
 | ||||
|         $ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () { | ||||
|             return DB::table('profiles') | ||||
|                 ->where('is_private', false) | ||||
|                 ->whereNull('status') | ||||
|                 ->orderByDesc('profiles.followers_count') | ||||
|                 ->limit(30) | ||||
|                 ->get(); | ||||
|         }); | ||||
|         $filters = UserFilterService::filters($pid); | ||||
|         $asf = AdminShadowFilterService::getHideFromPublicFeedsList(); | ||||
|         $ids = $ids->map(function ($profile) { | ||||
|             return AccountService::get($profile->id, true); | ||||
|         }) | ||||
|             ->filter(function ($profile) { | ||||
|                 return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked']; | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($pid) { | ||||
|                 return $profile['id'] != $pid; | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($pid) { | ||||
|                 return ! FollowerService::follows($pid, $profile['id'], true); | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($asf) { | ||||
|                 return ! in_array($profile['id'], $asf); | ||||
|             }) | ||||
|             ->filter(function ($profile) use ($filters) { | ||||
|                 return ! in_array($profile['id'], $filters); | ||||
|             }) | ||||
|             ->take(16) | ||||
|             ->values(); | ||||
| 
 | ||||
|         return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,356 +2,385 @@ | |||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use Auth; | ||||
| use Cache; | ||||
| use DB; | ||||
| use View; | ||||
| use App\AccountInterstitial; | ||||
| use App\Follower; | ||||
| use App\FollowRequest; | ||||
| use App\Profile; | ||||
| use App\Story; | ||||
| use App\Status; | ||||
| use App\User; | ||||
| use App\UserSetting; | ||||
| use App\UserFilter; | ||||
| use League\Fractal; | ||||
| use App\Services\AccountService; | ||||
| use App\Services\FollowerService; | ||||
| use App\Services\StatusService; | ||||
| use App\Util\Lexer\Nickname; | ||||
| use App\Util\Webfinger\Webfinger; | ||||
| use App\Transformer\ActivityPub\ProfileOutbox; | ||||
| use App\Status; | ||||
| use App\Story; | ||||
| use App\Transformer\ActivityPub\ProfileTransformer; | ||||
| use App\User; | ||||
| use App\UserFilter; | ||||
| use App\UserSetting; | ||||
| use Auth; | ||||
| use Cache; | ||||
| use Illuminate\Http\Request; | ||||
| use League\Fractal; | ||||
| use View; | ||||
| 
 | ||||
| class ProfileController extends Controller | ||||
| { | ||||
| 	public function show(Request $request, $username) | ||||
| 	{ | ||||
| 		// redirect authed users to Metro 2.0
 | ||||
| 		if($request->user()) { | ||||
| 			// unless they force static view
 | ||||
| 			if(!$request->has('fs') || $request->input('fs') != '1') { | ||||
| 				$pid = AccountService::usernameToId($username); | ||||
| 				if($pid) { | ||||
| 					return redirect('/i/web/profile/' . $pid); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|     public function show(Request $request, $username) | ||||
|     { | ||||
|         if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { | ||||
|             $user = $this->getCachedUser($username, true); | ||||
|             abort_if(! $user, 404, 'Not found'); | ||||
| 
 | ||||
| 		$user = Profile::whereNull('domain') | ||||
| 			->whereNull('status') | ||||
| 			->whereUsername($username) | ||||
| 			->firstOrFail(); | ||||
|             return $this->showActivityPub($request, $user); | ||||
|         } | ||||
| 
 | ||||
|         // redirect authed users to Metro 2.0
 | ||||
|         if ($request->user()) { | ||||
|             // unless they force static view
 | ||||
|             if (! $request->has('fs') || $request->input('fs') != '1') { | ||||
|                 $pid = AccountService::usernameToId($username); | ||||
|                 if ($pid) { | ||||
|                     return redirect('/i/web/profile/'.$pid); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 		if($request->wantsJson() && config_cache('federation.activitypub.enabled')) { | ||||
| 			return $this->showActivityPub($request, $user); | ||||
| 		} | ||||
|         $user = $this->getCachedUser($username); | ||||
| 
 | ||||
| 		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) { | ||||
| 			$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); | ||||
| 			if($exists) { | ||||
| 				return true; | ||||
| 			} | ||||
|         abort_unless($user, 404); | ||||
| 
 | ||||
| 			return false; | ||||
| 		}); | ||||
| 		if($aiCheck) { | ||||
| 			return redirect('/login'); | ||||
| 		} | ||||
| 		return $this->buildProfile($request, $user); | ||||
| 	} | ||||
|         $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) { | ||||
|             $exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count(); | ||||
|             if ($exists) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
| 	protected function buildProfile(Request $request, $user) | ||||
| 	{ | ||||
| 		$username = $user->username; | ||||
| 		$loggedIn = Auth::check(); | ||||
| 		$isPrivate = false; | ||||
| 		$isBlocked = false; | ||||
| 		if(!$loggedIn) { | ||||
| 			$key = 'profile:settings:' . $user->id; | ||||
| 			$ttl = now()->addHours(6); | ||||
| 			$settings = Cache::remember($key, $ttl, function() use($user) { | ||||
| 				return $user->user->settings; | ||||
| 			}); | ||||
|             return false; | ||||
|         }); | ||||
|         if ($aiCheck) { | ||||
|             return redirect('/login'); | ||||
|         } | ||||
| 
 | ||||
| 			if ($user->is_private == true) { | ||||
| 				$profile = null; | ||||
| 				return view('profile.private', compact('user')); | ||||
| 			} | ||||
|         return $this->buildProfile($request, $user); | ||||
|     } | ||||
| 
 | ||||
| 			$owner = false; | ||||
| 			$is_following = false; | ||||
|     protected function buildProfile(Request $request, $user) | ||||
|     { | ||||
|         $username = $user->username; | ||||
|         $loggedIn = Auth::check(); | ||||
|         $isPrivate = false; | ||||
|         $isBlocked = false; | ||||
|         if (! $loggedIn) { | ||||
|             $key = 'profile:settings:'.$user->id; | ||||
|             $ttl = now()->addHours(6); | ||||
|             $settings = Cache::remember($key, $ttl, function () use ($user) { | ||||
|                 return $user->user->settings; | ||||
|             }); | ||||
| 
 | ||||
| 			$profile = $user; | ||||
| 			$settings = [ | ||||
| 				'crawlable' => $settings->crawlable, | ||||
| 				'following' => [ | ||||
| 					'count' => $settings->show_profile_following_count, | ||||
| 					'list' => $settings->show_profile_following | ||||
| 				], | ||||
| 				'followers' => [ | ||||
| 					'count' => $settings->show_profile_follower_count, | ||||
| 					'list' => $settings->show_profile_followers | ||||
| 				] | ||||
| 			]; | ||||
| 			return view('profile.show', compact('profile', 'settings')); | ||||
| 		} else { | ||||
| 			$key = 'profile:settings:' . $user->id; | ||||
| 			$ttl = now()->addHours(6); | ||||
| 			$settings = Cache::remember($key, $ttl, function() use($user) { | ||||
| 				return $user->user->settings; | ||||
| 			}); | ||||
|             if ($user->is_private == true) { | ||||
|                 $profile = null; | ||||
| 
 | ||||
| 			if ($user->is_private == true) { | ||||
| 				$isPrivate = $this->privateProfileCheck($user, $loggedIn); | ||||
| 			} | ||||
|                 return view('profile.private', compact('user')); | ||||
|             } | ||||
| 
 | ||||
| 			$isBlocked = $this->blockedProfileCheck($user); | ||||
|             $owner = false; | ||||
|             $is_following = false; | ||||
| 
 | ||||
| 			$owner = $loggedIn && Auth::id() === $user->user_id; | ||||
| 			$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; | ||||
|             $profile = $user; | ||||
|             $settings = [ | ||||
|                 'crawlable' => $settings->crawlable, | ||||
|                 'following' => [ | ||||
|                     'count' => $settings->show_profile_following_count, | ||||
|                     'list' => $settings->show_profile_following, | ||||
|                 ], | ||||
|                 'followers' => [ | ||||
|                     'count' => $settings->show_profile_follower_count, | ||||
|                     'list' => $settings->show_profile_followers, | ||||
|                 ], | ||||
|             ]; | ||||
| 
 | ||||
| 			if ($isPrivate == true || $isBlocked == true) { | ||||
| 				$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) | ||||
| 					->whereFollowingId($user->id) | ||||
| 					->exists() : false; | ||||
| 				return view('profile.private', compact('user', 'is_following', 'requested')); | ||||
| 			} | ||||
|             return view('profile.show', compact('profile', 'settings')); | ||||
|         } else { | ||||
|             $key = 'profile:settings:'.$user->id; | ||||
|             $ttl = now()->addHours(6); | ||||
|             $settings = Cache::remember($key, $ttl, function () use ($user) { | ||||
|                 return $user->user->settings; | ||||
|             }); | ||||
| 
 | ||||
| 			$is_admin = is_null($user->domain) ? $user->user->is_admin : false; | ||||
| 			$profile = $user; | ||||
| 			$settings = [ | ||||
| 				'crawlable' => $settings->crawlable, | ||||
| 				'following' => [ | ||||
| 					'count' => $settings->show_profile_following_count, | ||||
| 					'list' => $settings->show_profile_following | ||||
| 				], | ||||
| 				'followers' => [ | ||||
| 					'count' => $settings->show_profile_follower_count, | ||||
| 					'list' => $settings->show_profile_followers | ||||
| 				] | ||||
| 			]; | ||||
| 			return view('profile.show', compact('profile', 'settings')); | ||||
| 		} | ||||
| 	} | ||||
|             if ($user->is_private == true) { | ||||
|                 $isPrivate = $this->privateProfileCheck($user, $loggedIn); | ||||
|             } | ||||
| 
 | ||||
| 	public function permalinkRedirect(Request $request, $username) | ||||
| 	{ | ||||
| 		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
|             $isBlocked = $this->blockedProfileCheck($user); | ||||
| 
 | ||||
| 		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { | ||||
| 			return $this->showActivityPub($request, $user); | ||||
| 		} | ||||
|             $owner = $loggedIn && Auth::id() === $user->user_id; | ||||
|             $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false; | ||||
| 
 | ||||
| 		return redirect($user->url()); | ||||
| 	} | ||||
|             if ($isPrivate == true || $isBlocked == true) { | ||||
|                 $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) | ||||
|                     ->whereFollowingId($user->id) | ||||
|                     ->exists() : false; | ||||
| 
 | ||||
| 	protected function privateProfileCheck(Profile $profile, $loggedIn) | ||||
| 	{ | ||||
| 		if (!Auth::check()) { | ||||
| 			return true; | ||||
| 		} | ||||
|                 return view('profile.private', compact('user', 'is_following', 'requested')); | ||||
|             } | ||||
| 
 | ||||
| 		$user = Auth::user()->profile; | ||||
| 		if($user->id == $profile->id || !$profile->is_private) { | ||||
| 			return false; | ||||
| 		} | ||||
|             $is_admin = is_null($user->domain) ? $user->user->is_admin : false; | ||||
|             $profile = $user; | ||||
|             $settings = [ | ||||
|                 'crawlable' => $settings->crawlable, | ||||
|                 'following' => [ | ||||
|                     'count' => $settings->show_profile_following_count, | ||||
|                     'list' => $settings->show_profile_following, | ||||
|                 ], | ||||
|                 'followers' => [ | ||||
|                     'count' => $settings->show_profile_follower_count, | ||||
|                     'list' => $settings->show_profile_followers, | ||||
|                 ], | ||||
|             ]; | ||||
| 
 | ||||
| 		$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); | ||||
| 		if ($follows == false) { | ||||
| 			return true; | ||||
| 		} | ||||
|             return view('profile.show', compact('profile', 'settings')); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 		return false; | ||||
| 	} | ||||
|     protected function getCachedUser($username, $withTrashed = false) | ||||
|     { | ||||
|         $val = str_replace(['_', '.', '-'], '', $username); | ||||
|         if (! ctype_alnum($val)) { | ||||
|             return; | ||||
|         } | ||||
|         $hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username); | ||||
| 
 | ||||
| 	public static function accountCheck(Profile $profile) | ||||
| 	{ | ||||
| 		switch ($profile->status) { | ||||
| 			case 'disabled': | ||||
| 			case 'suspended': | ||||
| 			case 'delete': | ||||
| 				return view('profile.disabled'); | ||||
| 				break; | ||||
|         return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) { | ||||
|             if (! $withTrashed) { | ||||
|                 return Profile::whereNull(['domain', 'status']) | ||||
|                     ->whereUsername($username) | ||||
|                     ->first(); | ||||
|             } else { | ||||
|                 return Profile::withTrashed() | ||||
|                     ->whereNull('domain') | ||||
|                     ->whereUsername($username) | ||||
|                     ->first(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 			default: | ||||
| 				break; | ||||
| 		} | ||||
| 		return abort(404); | ||||
| 	} | ||||
|     public function permalinkRedirect(Request $request, $username) | ||||
|     { | ||||
|         if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) { | ||||
|             $user = $this->getCachedUser($username, true); | ||||
| 
 | ||||
| 	protected function blockedProfileCheck(Profile $profile) | ||||
| 	{ | ||||
| 		$pid = Auth::user()->profile->id; | ||||
| 		$blocks = UserFilter::whereUserId($profile->id) | ||||
| 				->whereFilterType('block') | ||||
| 				->whereFilterableType('App\Profile') | ||||
| 				->pluck('filterable_id') | ||||
| 				->toArray(); | ||||
| 		if (in_array($pid, $blocks)) { | ||||
| 			return true; | ||||
| 		} | ||||
|             return $this->showActivityPub($request, $user); | ||||
|         } | ||||
| 
 | ||||
| 		return false; | ||||
| 	} | ||||
|         $user = $this->getCachedUser($username); | ||||
| 
 | ||||
| 	public function showActivityPub(Request $request, $user) | ||||
| 	{ | ||||
| 		abort_if(!config_cache('federation.activitypub.enabled'), 404); | ||||
| 		abort_if($user->domain, 404); | ||||
|         return redirect($user->url()); | ||||
|     } | ||||
| 
 | ||||
| 		return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) { | ||||
| 			$fractal = new Fractal\Manager(); | ||||
| 			$resource = new Fractal\Resource\Item($user, new ProfileTransformer); | ||||
| 			$res = $fractal->createData($resource)->toArray(); | ||||
| 			return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); | ||||
| 		}); | ||||
| 	} | ||||
|     protected function privateProfileCheck(Profile $profile, $loggedIn) | ||||
|     { | ||||
|         if (! Auth::check()) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
| 	public function showAtomFeed(Request $request, $user) | ||||
| 	{ | ||||
| 		abort_if(!config('federation.atom.enabled'), 404); | ||||
|         $user = Auth::user()->profile; | ||||
|         if ($user->id == $profile->id || ! $profile->is_private) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
| 		$pid = AccountService::usernameToId($user); | ||||
|         $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); | ||||
|         if ($follows == false) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
| 		abort_if(!$pid, 404); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| 		$profile = AccountService::get($pid, true); | ||||
|     public static function accountCheck(Profile $profile) | ||||
|     { | ||||
|         switch ($profile->status) { | ||||
|             case 'disabled': | ||||
|             case 'suspended': | ||||
|             case 'delete': | ||||
|                 return view('profile.disabled'); | ||||
|                 break; | ||||
| 
 | ||||
| 		abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
| 		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) { | ||||
| 			$uid = User::whereProfileId($profile['id'])->first(); | ||||
| 			if(!$uid) { | ||||
| 				return true; | ||||
| 			} | ||||
| 			$exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); | ||||
| 			if($exists) { | ||||
| 				return true; | ||||
| 			} | ||||
|         return abort(404); | ||||
|     } | ||||
| 
 | ||||
| 			return false; | ||||
| 		}); | ||||
|     protected function blockedProfileCheck(Profile $profile) | ||||
|     { | ||||
|         $pid = Auth::user()->profile->id; | ||||
|         $blocks = UserFilter::whereUserId($profile->id) | ||||
|             ->whereFilterType('block') | ||||
|             ->whereFilterableType('App\Profile') | ||||
|             ->pluck('filterable_id') | ||||
|             ->toArray(); | ||||
|         if (in_array($pid, $blocks)) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
| 		abort_if($aiCheck, 404); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| 		$enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) { | ||||
| 			$uid = User::whereProfileId($profile['id'])->first(); | ||||
| 			if(!$uid) { | ||||
| 				return false; | ||||
| 			} | ||||
| 			$settings = UserSetting::whereUserId($uid->id)->first(); | ||||
| 			if(!$settings) { | ||||
| 				return false; | ||||
| 			} | ||||
|     public function showActivityPub(Request $request, $user) | ||||
|     { | ||||
|         abort_if(! config_cache('federation.activitypub.enabled'), 404); | ||||
|         abort_if(! $user, 404, 'Not found'); | ||||
|         abort_if($user->domain, 404); | ||||
| 
 | ||||
| 			return $settings->show_atom; | ||||
| 		}); | ||||
|         return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) { | ||||
|             $fractal = new Fractal\Manager(); | ||||
|             $resource = new Fractal\Resource\Item($user, new ProfileTransformer); | ||||
|             $res = $fractal->createData($resource)->toArray(); | ||||
| 
 | ||||
| 		abort_if(!$enabled, 404); | ||||
|             return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 		$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) { | ||||
| 			$items = Status::whereProfileId($pid) | ||||
| 				->whereScope('public') | ||||
| 				->whereIn('type', ['photo', 'photo:album']) | ||||
| 				->orderByDesc('id') | ||||
| 				->take(10) | ||||
| 				->get() | ||||
| 				->map(function($status) { | ||||
| 					return StatusService::get($status->id, true); | ||||
| 				}) | ||||
| 				->filter(function($status) { | ||||
| 					return $status && | ||||
| 						isset($status['account']) && | ||||
| 						isset($status['media_attachments']) && | ||||
| 						count($status['media_attachments']); | ||||
| 				}) | ||||
| 				->values(); | ||||
| 			$permalink = config('app.url') . "/users/{$profile['username']}.atom"; | ||||
| 			$headers = ['Content-Type' => 'application/atom+xml']; | ||||
|     public function showAtomFeed(Request $request, $user) | ||||
|     { | ||||
|         abort_if(! config('federation.atom.enabled'), 404); | ||||
| 
 | ||||
| 			if($items && $items->count()) { | ||||
| 				$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); | ||||
| 			} | ||||
|         $pid = AccountService::usernameToId($user); | ||||
| 
 | ||||
| 			return compact('items', 'permalink', 'headers'); | ||||
| 		}); | ||||
| 		abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404); | ||||
| 		return response() | ||||
| 			->view('atom.user', | ||||
| 				[ | ||||
| 					'profile' => $profile, | ||||
| 					'items' => $data['items'], | ||||
| 					'permalink' => $data['permalink'] | ||||
| 				] | ||||
| 			) | ||||
| 			->withHeaders($data['headers']); | ||||
| 	} | ||||
|         abort_if(! $pid, 404); | ||||
| 
 | ||||
| 	public function meRedirect() | ||||
| 	{ | ||||
| 		abort_if(!Auth::check(), 404); | ||||
| 		return redirect(Auth::user()->url()); | ||||
| 	} | ||||
|         $profile = AccountService::get($pid, true); | ||||
| 
 | ||||
| 	public function embed(Request $request, $username) | ||||
| 	{ | ||||
| 		$res = view('profile.embed-removed'); | ||||
|         abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404); | ||||
| 
 | ||||
| 		if(!config('instance.embed.profile')) { | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
|         $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 86400, function () use ($profile) { | ||||
|             $uid = User::whereProfileId($profile['id'])->first(); | ||||
|             if (! $uid) { | ||||
|                 return true; | ||||
|             } | ||||
|             $exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count(); | ||||
|             if ($exists) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
| 		if(strlen($username) > 15 || strlen($username) < 2) { | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
| 		$profile = Profile::whereUsername($username) | ||||
| 			->whereIsPrivate(false) | ||||
| 			->whereNull('status') | ||||
| 			->whereNull('domain') | ||||
| 			->first(); | ||||
|         abort_if($aiCheck, 404); | ||||
| 
 | ||||
| 		if(!$profile) { | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
|         $enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) { | ||||
|             $uid = User::whereProfileId($profile['id'])->first(); | ||||
|             if (! $uid) { | ||||
|                 return false; | ||||
|             } | ||||
|             $settings = UserSetting::whereUserId($uid->id)->first(); | ||||
|             if (! $settings) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
| 		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { | ||||
| 			$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); | ||||
| 			if($exists) { | ||||
| 				return true; | ||||
| 			} | ||||
|             return $settings->show_atom; | ||||
|         }); | ||||
| 
 | ||||
| 			return false; | ||||
| 		}); | ||||
|         abort_if(! $enabled, 404); | ||||
| 
 | ||||
| 		if($aiCheck) { | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
|         $data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) { | ||||
|             $items = Status::whereProfileId($pid) | ||||
|                 ->whereScope('public') | ||||
|                 ->whereIn('type', ['photo', 'photo:album']) | ||||
|                 ->orderByDesc('id') | ||||
|                 ->take(10) | ||||
|                 ->get() | ||||
|                 ->map(function ($status) { | ||||
|                     return StatusService::get($status->id, true); | ||||
|                 }) | ||||
|                 ->filter(function ($status) { | ||||
|                     return $status && | ||||
|                         isset($status['account']) && | ||||
|                         isset($status['media_attachments']) && | ||||
|                         count($status['media_attachments']); | ||||
|                 }) | ||||
|                 ->values(); | ||||
|             $permalink = config('app.url')."/users/{$profile['username']}.atom"; | ||||
|             $headers = ['Content-Type' => 'application/atom+xml']; | ||||
| 
 | ||||
| 		if(AccountService::canEmbed($profile->user_id) == false) { | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
|             if ($items && $items->count()) { | ||||
|                 $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); | ||||
|             } | ||||
| 
 | ||||
| 		$profile = AccountService::get($profile->id); | ||||
| 		$res = view('profile.embed', compact('profile')); | ||||
| 		return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 	} | ||||
|             return compact('items', 'permalink', 'headers'); | ||||
|         }); | ||||
|         abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404); | ||||
| 
 | ||||
| 	public function stories(Request $request, $username) | ||||
| 	{ | ||||
| 		abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); | ||||
| 		$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
| 		$pid = $profile->id; | ||||
| 		$authed = Auth::user()->profile_id; | ||||
| 		abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404); | ||||
| 		$exists = Story::whereProfileId($pid) | ||||
| 			->whereActive(true) | ||||
| 			->exists(); | ||||
| 		abort_unless($exists, 404); | ||||
| 		return view('profile.story', compact('pid', 'profile')); | ||||
| 	} | ||||
|         return response() | ||||
|             ->view('atom.user', | ||||
|                 [ | ||||
|                     'profile' => $profile, | ||||
|                     'items' => $data['items'], | ||||
|                     'permalink' => $data['permalink'], | ||||
|                 ] | ||||
|             ) | ||||
|             ->withHeaders($data['headers']); | ||||
|     } | ||||
| 
 | ||||
|     public function meRedirect() | ||||
|     { | ||||
|         abort_if(! Auth::check(), 404); | ||||
| 
 | ||||
|         return redirect(Auth::user()->url()); | ||||
|     } | ||||
| 
 | ||||
|     public function embed(Request $request, $username) | ||||
|     { | ||||
|         $res = view('profile.embed-removed'); | ||||
| 
 | ||||
|         if (! (bool) config_cache('instance.embed.profile')) { | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         if (strlen($username) > 15 || strlen($username) < 2) { | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         $profile = $this->getCachedUser($username); | ||||
| 
 | ||||
|         if (! $profile || $profile->is_private) { | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) { | ||||
|             $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); | ||||
|             if ($exists) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|         if ($aiCheck) { | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         if (AccountService::canEmbed($profile->user_id) == false) { | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         $profile = AccountService::get($profile->id); | ||||
|         $res = view('profile.embed', compact('profile')); | ||||
| 
 | ||||
|         return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|     } | ||||
| 
 | ||||
|     public function stories(Request $request, $username) | ||||
|     { | ||||
|         abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404); | ||||
|         $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
|         $pid = $profile->id; | ||||
|         $authed = Auth::user()->profile_id; | ||||
|         abort_if($pid != $authed && ! FollowerService::follows($authed, $pid), 404); | ||||
|         $exists = Story::whereProfileId($pid) | ||||
|             ->whereActive(true) | ||||
|             ->exists(); | ||||
|         abort_unless($exists, 404); | ||||
| 
 | ||||
|         return view('profile.story', compact('pid', 'profile')); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -95,6 +95,8 @@ trait PrivacySettings | |||
|         Cache::forget('pf:acct:settings:hidden-following:' . $pid); | ||||
|         Cache::forget('pf:acct-trans:hideFollowing:' . $pid); | ||||
|         Cache::forget('pf:acct-trans:hideFollowers:' . $pid); | ||||
|         Cache::forget('pfc:cached-user:wt:' . strtolower($profile->username)); | ||||
|         Cache::forget('pfc:cached-user:wot:' . strtolower($profile->username)); | ||||
|         return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!'); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,166 +2,202 @@ | |||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use App\Page; | ||||
| use App\Profile; | ||||
| use App\Services\FollowerService; | ||||
| use App\Status; | ||||
| use App\User; | ||||
| use App\Util\ActivityPub\Helpers; | ||||
| use App\Util\Localization\Localization; | ||||
| use Auth; | ||||
| use Cache; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Str; | ||||
| use App, Auth, Cache, View; | ||||
| use App\Util\Lexer\PrettyNumber; | ||||
| use App\{Follower, Page, Profile, Status, User, UserFilter}; | ||||
| use App\Util\Localization\Localization; | ||||
| use App\Services\FollowerService; | ||||
| use App\Util\ActivityPub\Helpers; | ||||
| use View; | ||||
| 
 | ||||
| class SiteController extends Controller | ||||
| { | ||||
| 	public function home(Request $request) | ||||
| 	{ | ||||
| 		if (Auth::check()) { | ||||
| 			return $this->homeTimeline($request); | ||||
| 		} else { | ||||
| 			return $this->homeGuest(); | ||||
| 		} | ||||
| 	} | ||||
|     public function home(Request $request) | ||||
|     { | ||||
|         if (Auth::check()) { | ||||
|             return $this->homeTimeline($request); | ||||
|         } else { | ||||
|             return $this->homeGuest(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 	public function homeGuest() | ||||
| 	{ | ||||
| 		return view('site.index'); | ||||
| 	} | ||||
|     public function homeGuest() | ||||
|     { | ||||
|         return view('site.index'); | ||||
|     } | ||||
| 
 | ||||
| 	public function homeTimeline(Request $request) | ||||
| 	{ | ||||
| 		if($request->has('force_old_ui')) { | ||||
| 			return view('timeline.home', ['layout' => 'feed']); | ||||
| 		} | ||||
|     public function homeTimeline(Request $request) | ||||
|     { | ||||
|         if ($request->has('force_old_ui')) { | ||||
|             return view('timeline.home', ['layout' => 'feed']); | ||||
|         } | ||||
| 
 | ||||
| 		return redirect('/i/web'); | ||||
| 	} | ||||
|         return redirect('/i/web'); | ||||
|     } | ||||
| 
 | ||||
| 	public function changeLocale(Request $request, $locale) | ||||
| 	{ | ||||
| 		// todo: add other locales after pushing new l10n strings
 | ||||
| 		$locales = Localization::languages(); | ||||
| 		if(in_array($locale, $locales)) { | ||||
| 			if($request->user()) { | ||||
| 				$user = $request->user(); | ||||
| 				$user->language = $locale; | ||||
| 				$user->save(); | ||||
| 			} | ||||
| 		  session()->put('locale', $locale); | ||||
| 		} | ||||
|     public function changeLocale(Request $request, $locale) | ||||
|     { | ||||
|         // todo: add other locales after pushing new l10n strings
 | ||||
|         $locales = Localization::languages(); | ||||
|         if (in_array($locale, $locales)) { | ||||
|             if ($request->user()) { | ||||
|                 $user = $request->user(); | ||||
|                 $user->language = $locale; | ||||
|                 $user->save(); | ||||
|             } | ||||
|             session()->put('locale', $locale); | ||||
|         } | ||||
| 
 | ||||
| 		return redirect(route('site.language')); | ||||
| 	} | ||||
|         return redirect(route('site.language')); | ||||
|     } | ||||
| 
 | ||||
| 	public function about() | ||||
| 	{ | ||||
| 		return Cache::remember('site.about_v2', now()->addMinutes(15), function() { | ||||
| 			$user_count = number_format(User::count()); | ||||
| 			$post_count = number_format(Status::count()); | ||||
| 			$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; | ||||
| 			return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); | ||||
| 		}); | ||||
| 	} | ||||
|     public function about() | ||||
|     { | ||||
|         return Cache::remember('site.about_v2', now()->addMinutes(15), function () { | ||||
|             $user_count = number_format(User::count()); | ||||
|             $post_count = number_format(Status::count()); | ||||
|             $rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null; | ||||
| 
 | ||||
| 	public function language() | ||||
| 	{ | ||||
| 	  return view('site.language'); | ||||
| 	} | ||||
|             return view('site.about', compact('rules', 'user_count', 'post_count'))->render(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 	public function communityGuidelines(Request $request) | ||||
| 	{ | ||||
| 		return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() { | ||||
| 			$slug = '/site/kb/community-guidelines'; | ||||
| 			$page = Page::whereSlug($slug)->whereActive(true)->first(); | ||||
| 			return View::make('site.help.community-guidelines')->with(compact('page'))->render(); | ||||
| 		}); | ||||
| 	} | ||||
|     public function language() | ||||
|     { | ||||
|         return view('site.language'); | ||||
|     } | ||||
| 
 | ||||
| 	public function privacy(Request $request) | ||||
| 	{ | ||||
| 		$page = Cache::remember('site:privacy', now()->addDays(120), function() { | ||||
| 			$slug = '/site/privacy'; | ||||
| 			return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
| 		}); | ||||
| 		return View::make('site.privacy')->with(compact('page'))->render(); | ||||
| 	} | ||||
|     public function communityGuidelines(Request $request) | ||||
|     { | ||||
|         return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () { | ||||
|             $slug = '/site/kb/community-guidelines'; | ||||
|             $page = Page::whereSlug($slug)->whereActive(true)->first(); | ||||
| 
 | ||||
| 	public function terms(Request $request) | ||||
| 	{ | ||||
| 		$page = Cache::remember('site:terms', now()->addDays(120), function() { | ||||
| 			$slug = '/site/terms'; | ||||
| 			return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
| 		}); | ||||
| 		return View::make('site.terms')->with(compact('page'))->render(); | ||||
| 	} | ||||
|             return View::make('site.help.community-guidelines')->with(compact('page'))->render(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 	public function redirectUrl(Request $request) | ||||
| 	{ | ||||
| 		abort_if(!$request->user(), 404); | ||||
| 		$this->validate($request, [ | ||||
| 			'url' => 'required|url' | ||||
| 		]); | ||||
| 		$url = request()->input('url'); | ||||
| 		abort_if(Helpers::validateUrl($url) == false, 404); | ||||
| 		return view('site.redirect', compact('url')); | ||||
| 	} | ||||
|     public function privacy(Request $request) | ||||
|     { | ||||
|         $page = Cache::remember('site:privacy', now()->addDays(120), function () { | ||||
|             $slug = '/site/privacy'; | ||||
| 
 | ||||
| 	public function followIntent(Request $request) | ||||
| 	{ | ||||
| 		$this->validate($request, [ | ||||
| 			'user' => 'string|min:1|max:15|exists:users,username', | ||||
| 		]); | ||||
| 		$profile = Profile::whereUsername($request->input('user'))->firstOrFail(); | ||||
| 		$user = $request->user(); | ||||
| 		abort_if($user && $profile->id == $user->profile_id, 404); | ||||
| 		$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; | ||||
| 		return view('site.intents.follow', compact('profile', 'user', 'following')); | ||||
| 	} | ||||
|             return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
|         }); | ||||
| 
 | ||||
| 	public function legacyProfileRedirect(Request $request, $username) | ||||
| 	{ | ||||
| 		$username = Str::contains($username, '@') ? '@' . $username : $username; | ||||
| 		if(str_contains($username, '@')) { | ||||
| 			$profile = Profile::whereUsername($username) | ||||
| 				->firstOrFail(); | ||||
|         return View::make('site.privacy')->with(compact('page'))->render(); | ||||
|     } | ||||
| 
 | ||||
| 			if($profile->domain == null) { | ||||
| 				$url = "/$profile->username"; | ||||
| 			} else { | ||||
| 				$url = "/i/web/profile/_/{$profile->id}"; | ||||
| 			} | ||||
|     public function terms(Request $request) | ||||
|     { | ||||
|         $page = Cache::remember('site:terms', now()->addDays(120), function () { | ||||
|             $slug = '/site/terms'; | ||||
| 
 | ||||
| 		} else { | ||||
| 			$profile = Profile::whereUsername($username) | ||||
| 				->whereNull('domain') | ||||
| 				->firstOrFail(); | ||||
| 			$url = "/$profile->username"; | ||||
| 		} | ||||
|             return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
|         }); | ||||
| 
 | ||||
| 		return redirect($url); | ||||
| 	} | ||||
|         return View::make('site.terms')->with(compact('page'))->render(); | ||||
|     } | ||||
| 
 | ||||
| 	public function legacyWebfingerRedirect(Request $request, $username, $domain) | ||||
| 	{ | ||||
| 		$un = '@'.$username.'@'.$domain; | ||||
| 		$profile = Profile::whereUsername($un) | ||||
| 			->firstOrFail(); | ||||
|     public function redirectUrl(Request $request) | ||||
|     { | ||||
|         abort_if(! $request->user(), 404); | ||||
|         $this->validate($request, [ | ||||
|             'url' => 'required|url', | ||||
|         ]); | ||||
|         $url = request()->input('url'); | ||||
|         abort_if(Helpers::validateUrl($url) == false, 404); | ||||
| 
 | ||||
| 		if($profile->domain == null) { | ||||
| 			$url = "/$profile->username"; | ||||
| 		} else { | ||||
| 			$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); | ||||
| 		} | ||||
|         return view('site.redirect', compact('url')); | ||||
|     } | ||||
| 
 | ||||
| 		return redirect($url); | ||||
| 	} | ||||
|     public function followIntent(Request $request) | ||||
|     { | ||||
|         $this->validate($request, [ | ||||
|             'user' => 'string|min:1|max:15|exists:users,username', | ||||
|         ]); | ||||
|         $profile = Profile::whereUsername($request->input('user'))->firstOrFail(); | ||||
|         $user = $request->user(); | ||||
|         abort_if($user && $profile->id == $user->profile_id, 404); | ||||
|         $following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false; | ||||
| 
 | ||||
| 	public function legalNotice(Request $request) | ||||
| 	{ | ||||
| 		$page = Cache::remember('site:legal-notice', now()->addDays(120), function() { | ||||
| 			$slug = '/site/legal-notice'; | ||||
| 			return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
| 		}); | ||||
| 		abort_if(!$page, 404); | ||||
| 		return View::make('site.legal-notice')->with(compact('page'))->render(); | ||||
| 	} | ||||
|         return view('site.intents.follow', compact('profile', 'user', 'following')); | ||||
|     } | ||||
| 
 | ||||
|     public function legacyProfileRedirect(Request $request, $username) | ||||
|     { | ||||
|         $username = Str::contains($username, '@') ? '@'.$username : $username; | ||||
|         if (str_contains($username, '@')) { | ||||
|             $profile = Profile::whereUsername($username) | ||||
|                 ->firstOrFail(); | ||||
| 
 | ||||
|             if ($profile->domain == null) { | ||||
|                 $url = "/$profile->username"; | ||||
|             } else { | ||||
|                 $url = "/i/web/profile/_/{$profile->id}"; | ||||
|             } | ||||
| 
 | ||||
|         } else { | ||||
|             $profile = Profile::whereUsername($username) | ||||
|                 ->whereNull('domain') | ||||
|                 ->firstOrFail(); | ||||
|             $url = "/$profile->username"; | ||||
|         } | ||||
| 
 | ||||
|         return redirect($url); | ||||
|     } | ||||
| 
 | ||||
|     public function legacyWebfingerRedirect(Request $request, $username, $domain) | ||||
|     { | ||||
|         $un = '@'.$username.'@'.$domain; | ||||
|         $profile = Profile::whereUsername($un) | ||||
|             ->firstOrFail(); | ||||
| 
 | ||||
|         if ($profile->domain == null) { | ||||
|             $url = "/$profile->username"; | ||||
|         } else { | ||||
|             $url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url(); | ||||
|         } | ||||
| 
 | ||||
|         return redirect($url); | ||||
|     } | ||||
| 
 | ||||
|     public function legalNotice(Request $request) | ||||
|     { | ||||
|         $page = Cache::remember('site:legal-notice', now()->addDays(120), function () { | ||||
|             $slug = '/site/legal-notice'; | ||||
| 
 | ||||
|             return Page::whereSlug($slug)->whereActive(true)->first(); | ||||
|         }); | ||||
|         abort_if(! $page, 404); | ||||
| 
 | ||||
|         return View::make('site.legal-notice')->with(compact('page'))->render(); | ||||
|     } | ||||
| 
 | ||||
|     public function curatedOnboarding(Request $request) | ||||
|     { | ||||
|         if ($request->user()) { | ||||
|             return redirect('/i/web'); | ||||
|         } | ||||
| 
 | ||||
|         $regOpen = (bool) config_cache('pixelfed.open_registration'); | ||||
|         $curOnboarding = (bool) config_cache('instance.curated_registration.enabled'); | ||||
|         $curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg'); | ||||
|         if ($regOpen) { | ||||
|             if ($curOnlyClosed) { | ||||
|                 return redirect('/register'); | ||||
|             } | ||||
|         } else { | ||||
|             if (! $curOnboarding) { | ||||
|                 return redirect('/'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return view('auth.curated-register.index', ['step' => 1]); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,458 +2,466 @@ | |||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use App\Jobs\ImageOptimizePipeline\ImageOptimize; | ||||
| use App\Jobs\StatusPipeline\NewStatusPipeline; | ||||
| use App\Jobs\StatusPipeline\StatusDelete; | ||||
| use App\Jobs\StatusPipeline\RemoteStatusDelete; | ||||
| use App\AccountInterstitial; | ||||
| use App\Jobs\SharePipeline\SharePipeline; | ||||
| use App\Jobs\SharePipeline\UndoSharePipeline; | ||||
| use App\AccountInterstitial; | ||||
| use App\Media; | ||||
| use App\Jobs\StatusPipeline\RemoteStatusDelete; | ||||
| use App\Jobs\StatusPipeline\StatusDelete; | ||||
| use App\Profile; | ||||
| use App\Services\HashidService; | ||||
| use App\Services\ReblogService; | ||||
| use App\Services\StatusService; | ||||
| use App\Status; | ||||
| use App\StatusArchived; | ||||
| use App\StatusView; | ||||
| use App\Transformer\ActivityPub\StatusTransformer; | ||||
| use App\Transformer\ActivityPub\Verb\Note; | ||||
| use App\Transformer\ActivityPub\Verb\Question; | ||||
| use App\User; | ||||
| use Auth, DB, Cache; | ||||
| use App\Util\Media\License; | ||||
| use Auth; | ||||
| use Cache; | ||||
| use DB; | ||||
| use Illuminate\Http\Request; | ||||
| use League\Fractal; | ||||
| use App\Util\Media\Filter; | ||||
| use Illuminate\Support\Str; | ||||
| use App\Services\HashidService; | ||||
| use App\Services\StatusService; | ||||
| use App\Util\Media\License; | ||||
| use App\Services\ReblogService; | ||||
| 
 | ||||
| class StatusController extends Controller | ||||
| { | ||||
| 	public function show(Request $request, $username, $id) | ||||
| 	{ | ||||
| 		// redirect authed users to Metro 2.0
 | ||||
| 		if($request->user()) { | ||||
| 			// unless they force static view
 | ||||
| 			if(!$request->has('fs') || $request->input('fs') != '1') { | ||||
| 				return redirect('/i/web/post/' . $id); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
| 
 | ||||
| 		if($user->status != null) { | ||||
| 			return ProfileController::accountCheck($user); | ||||
| 		} | ||||
| 
 | ||||
| 		$status = Status::whereProfileId($user->id) | ||||
| 				->whereNull('reblog_of_id') | ||||
| 				->whereIn('scope', ['public','unlisted', 'private']) | ||||
| 				->findOrFail($id); | ||||
| 
 | ||||
| 		if($status->uri || $status->url) { | ||||
| 			$url = $status->uri ?? $status->url; | ||||
| 			if(ends_with($url, '/activity')) { | ||||
| 				$url = str_replace('/activity', '', $url); | ||||
| 			} | ||||
| 			return redirect($url); | ||||
| 		} | ||||
| 
 | ||||
| 		if($status->visibility == 'private' || $user->is_private) { | ||||
| 			if(!Auth::check()) { | ||||
| 				abort(404); | ||||
| 			} | ||||
| 			$pid = Auth::user()->profile; | ||||
| 			if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { | ||||
| 				abort(404); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if($status->type == 'archived') { | ||||
| 			if(Auth::user()->profile_id !== $status->profile_id) { | ||||
| 				abort(404); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if($request->user() && $request->user()->profile_id != $status->profile_id) { | ||||
| 			StatusView::firstOrCreate([ | ||||
| 				'status_id' => $status->id, | ||||
| 				'status_profile_id' => $status->profile_id, | ||||
| 				'profile_id' => $request->user()->profile_id | ||||
| 			]); | ||||
| 		} | ||||
| 
 | ||||
| 		if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { | ||||
| 			return $this->showActivityPub($request, $status); | ||||
| 		} | ||||
| 
 | ||||
| 		$template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; | ||||
| 		return view($template, compact('user', 'status')); | ||||
| 	} | ||||
| 
 | ||||
| 	public function shortcodeRedirect(Request $request, $id) | ||||
| 	{ | ||||
| 		abort(404); | ||||
| 	} | ||||
| 
 | ||||
| 	public function showId(int $id) | ||||
| 	{ | ||||
| 		abort(404); | ||||
| 		$status = Status::whereNull('reblog_of_id') | ||||
| 				->whereIn('scope', ['public', 'unlisted']) | ||||
| 				->findOrFail($id); | ||||
| 		return redirect($status->url()); | ||||
| 	} | ||||
| 
 | ||||
| 	public function showEmbed(Request $request, $username, int $id) | ||||
| 	{ | ||||
| 		if(!config('instance.embed.post')) { | ||||
| 			$res = view('status.embed-removed'); | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
| 
 | ||||
| 		$profile = Profile::whereNull(['domain','status']) | ||||
| 			->whereIsPrivate(false) | ||||
| 			->whereUsername($username) | ||||
| 			->first(); | ||||
| 
 | ||||
| 		if(!$profile) { | ||||
| 			$content = view('status.embed-removed'); | ||||
| 			return response($content)->header('X-Frame-Options', 'ALLOWALL'); | ||||
| 		} | ||||
| 
 | ||||
| 		$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) { | ||||
| 			$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); | ||||
| 			if($exists) { | ||||
| 				return true; | ||||
| 			} | ||||
| 
 | ||||
| 			return false; | ||||
| 		}); | ||||
| 
 | ||||
| 		if($aiCheck) { | ||||
| 			$res = view('status.embed-removed'); | ||||
| 			return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 		} | ||||
| 		$status = Status::whereProfileId($profile->id) | ||||
| 			->whereNull('uri') | ||||
| 			->whereScope('public') | ||||
| 			->whereIsNsfw(false) | ||||
| 			->whereIn('type', ['photo', 'video','photo:album']) | ||||
| 			->find($id); | ||||
| 		if(!$status) { | ||||
| 			$content = view('status.embed-removed'); | ||||
| 			return response($content)->header('X-Frame-Options', 'ALLOWALL'); | ||||
| 		} | ||||
| 		$showLikes = $request->filled('likes') && $request->likes == true; | ||||
| 		$showCaption = $request->filled('caption') && $request->caption !== false; | ||||
| 		$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; | ||||
| 		$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); | ||||
| 		return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
| 	} | ||||
| 
 | ||||
| 	public function showObject(Request $request, $username, int $id) | ||||
| 	{ | ||||
| 		$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
| 
 | ||||
| 		if($user->status != null) { | ||||
| 			return ProfileController::accountCheck($user); | ||||
| 		} | ||||
| 
 | ||||
| 		$status = Status::whereProfileId($user->id) | ||||
| 				->whereNotIn('visibility',['draft','direct']) | ||||
| 				->findOrFail($id); | ||||
| 
 | ||||
| 		abort_if($status->uri, 404); | ||||
| 
 | ||||
| 		if($status->visibility == 'private' || $user->is_private) { | ||||
| 			if(!Auth::check()) { | ||||
| 				abort(403); | ||||
| 			} | ||||
| 			$pid = Auth::user()->profile; | ||||
| 			if($user->followedBy($pid) == false && $user->id !== $pid->id) { | ||||
| 				abort(403); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return $this->showActivityPub($request, $status); | ||||
| 	} | ||||
| 
 | ||||
| 	public function compose() | ||||
| 	{ | ||||
| 		$this->authCheck(); | ||||
| 
 | ||||
| 		return view('status.compose'); | ||||
| 	} | ||||
| 
 | ||||
| 	public function store(Request $request) | ||||
| 	{ | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	public function delete(Request $request) | ||||
| 	{ | ||||
| 		$this->authCheck(); | ||||
| 
 | ||||
| 		$this->validate($request, [ | ||||
| 		  'item'  => 'required|integer|min:1', | ||||
| 		]); | ||||
| 
 | ||||
| 		$status = Status::findOrFail($request->input('item')); | ||||
| 
 | ||||
| 		$user = Auth::user(); | ||||
| 
 | ||||
| 		if($status->profile_id != $user->profile->id && | ||||
| 			$user->is_admin == true && | ||||
| 			$status->uri == null | ||||
| 		) { | ||||
| 			$media = $status->media; | ||||
| 
 | ||||
| 			$ai = new AccountInterstitial; | ||||
| 			$ai->user_id = $status->profile->user_id; | ||||
| 			$ai->type = 'post.removed'; | ||||
| 			$ai->view = 'account.moderation.post.removed'; | ||||
| 			$ai->item_type = 'App\Status'; | ||||
| 			$ai->item_id = $status->id; | ||||
| 			$ai->has_media = (bool) $media->count(); | ||||
| 			$ai->blurhash = $media->count() ? $media->first()->blurhash : null; | ||||
| 			$ai->meta = json_encode([ | ||||
| 				'caption' => $status->caption, | ||||
| 				'created_at' => $status->created_at, | ||||
| 				'type' => $status->type, | ||||
| 				'url' => $status->url(), | ||||
| 				'is_nsfw' => $status->is_nsfw, | ||||
| 				'scope' => $status->scope, | ||||
| 				'reblog' => $status->reblog_of_id, | ||||
| 				'likes_count' => $status->likes_count, | ||||
| 				'reblogs_count' => $status->reblogs_count, | ||||
| 			]); | ||||
| 			$ai->save(); | ||||
| 
 | ||||
| 			$u = $status->profile->user; | ||||
| 			$u->has_interstitial = true; | ||||
| 			$u->save(); | ||||
| 		} | ||||
| 
 | ||||
| 		if($status->in_reply_to_id) { | ||||
| 			$parent = Status::find($status->in_reply_to_id); | ||||
| 			if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { | ||||
| 				Cache::forget('_api:statuses:recent_9:' . $status->profile_id); | ||||
| 				Cache::forget('profile:status_count:' . $status->profile_id); | ||||
| 				Cache::forget('profile:embed:' . $status->profile_id); | ||||
| 				StatusService::del($status->id, true); | ||||
| 				Cache::forget('profile:status_count:'.$status->profile_id); | ||||
| 				$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); | ||||
| 			} | ||||
| 		} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) { | ||||
| 			Cache::forget('_api:statuses:recent_9:' . $status->profile_id); | ||||
| 			Cache::forget('profile:status_count:' . $status->profile_id); | ||||
| 			Cache::forget('profile:embed:' . $status->profile_id); | ||||
| 			StatusService::del($status->id, true); | ||||
| 			Cache::forget('profile:status_count:'.$status->profile_id); | ||||
| 			$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); | ||||
| 		} | ||||
| 
 | ||||
| 		if($request->wantsJson()) { | ||||
| 			return response()->json(['Status successfully deleted.']); | ||||
| 		} else { | ||||
| 			return redirect($user->url()); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function storeShare(Request $request) | ||||
| 	{ | ||||
| 		$this->authCheck(); | ||||
| 
 | ||||
| 		$this->validate($request, [ | ||||
| 		  'item'    => 'required|integer|min:1', | ||||
| 		]); | ||||
| 
 | ||||
| 		$user = Auth::user(); | ||||
| 		$profile = $user->profile; | ||||
| 		$status = Status::whereScope('public') | ||||
| 			->findOrFail($request->input('item')); | ||||
| 
 | ||||
| 		$count = $status->reblogs_count; | ||||
| 
 | ||||
| 		$exists = Status::whereProfileId(Auth::user()->profile->id) | ||||
| 				  ->whereReblogOfId($status->id) | ||||
| 				  ->exists(); | ||||
| 		if ($exists == true) { | ||||
| 			$shares = Status::whereProfileId(Auth::user()->profile->id) | ||||
| 				  ->whereReblogOfId($status->id) | ||||
| 				  ->get(); | ||||
| 			foreach ($shares as $share) { | ||||
| 				UndoSharePipeline::dispatch($share); | ||||
| 				ReblogService::del($profile->id, $status->id); | ||||
| 				$count--; | ||||
| 			} | ||||
| 		} else { | ||||
| 			$share = new Status(); | ||||
| 			$share->profile_id = $profile->id; | ||||
| 			$share->reblog_of_id = $status->id; | ||||
| 			$share->in_reply_to_profile_id = $status->profile_id; | ||||
| 			$share->type = 'share'; | ||||
| 			$share->save(); | ||||
| 			$count++; | ||||
| 			SharePipeline::dispatch($share); | ||||
| 			ReblogService::add($profile->id, $status->id); | ||||
| 		} | ||||
| 
 | ||||
| 		Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); | ||||
| 		StatusService::del($status->id); | ||||
| 
 | ||||
| 		if ($request->ajax()) { | ||||
| 			$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; | ||||
| 		} else { | ||||
| 			$response = redirect($status->url()); | ||||
| 		} | ||||
| 
 | ||||
| 		return $response; | ||||
| 	} | ||||
| 
 | ||||
| 	public function showActivityPub(Request $request, $status) | ||||
| 	{ | ||||
| 		$object = $status->type == 'poll' ? new Question() : new Note(); | ||||
| 		$fractal = new Fractal\Manager(); | ||||
| 		$resource = new Fractal\Resource\Item($status, $object); | ||||
| 		$res = $fractal->createData($resource)->toArray(); | ||||
| 
 | ||||
| 		return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); | ||||
| 	} | ||||
| 
 | ||||
| 	public function edit(Request $request, $username, $id) | ||||
| 	{ | ||||
| 		$this->authCheck(); | ||||
| 		$user = Auth::user()->profile; | ||||
| 		$status = Status::whereProfileId($user->id) | ||||
| 				->with(['media']) | ||||
| 				->findOrFail($id); | ||||
| 		$licenses = License::get(); | ||||
| 		return view('status.edit', compact('user', 'status', 'licenses')); | ||||
| 	} | ||||
| 
 | ||||
| 	public function editStore(Request $request, $username, $id) | ||||
| 	{ | ||||
| 		$this->authCheck(); | ||||
| 		$user = Auth::user()->profile; | ||||
| 		$status = Status::whereProfileId($user->id) | ||||
| 				->with(['media']) | ||||
| 				->findOrFail($id); | ||||
| 
 | ||||
| 		$this->validate($request, [ | ||||
| 		  'license'      => 'nullable|integer|min:1|max:16', | ||||
| 		]); | ||||
| 
 | ||||
| 		$licenseId = $request->input('license'); | ||||
| 
 | ||||
| 		$status->media->each(function($media) use($licenseId) { | ||||
| 			$media->license = $licenseId; | ||||
| 			$media->save(); | ||||
| 			Cache::forget('status:transformer:media:attachments:'.$media->status_id); | ||||
| 		}); | ||||
| 
 | ||||
| 		return redirect($status->url()); | ||||
| 	} | ||||
| 
 | ||||
| 	protected function authCheck() | ||||
| 	{ | ||||
| 		if (Auth::check() == false) { | ||||
| 			abort(403); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	protected function validateVisibility($visibility) | ||||
| 	{ | ||||
| 		$allowed = ['public', 'unlisted', 'private']; | ||||
| 		return in_array($visibility, $allowed) ? $visibility : 'public'; | ||||
| 	} | ||||
| 
 | ||||
| 	public static function mimeTypeCheck($mimes) | ||||
| 	{ | ||||
| 		$allowed = explode(',', config_cache('pixelfed.media_types')); | ||||
| 		$count = count($mimes); | ||||
| 		$photos = 0; | ||||
| 		$videos = 0; | ||||
| 		foreach($mimes as $mime) { | ||||
| 			if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') { | ||||
| 				continue; | ||||
| 			} | ||||
| 			if(str_contains($mime, 'image/')) { | ||||
| 				$photos++; | ||||
| 			} | ||||
| 			if(str_contains($mime, 'video/')) { | ||||
| 				$videos++; | ||||
| 			} | ||||
| 		} | ||||
| 		if($photos == 1 && $videos == 0) { | ||||
| 			return 'photo'; | ||||
| 		} | ||||
| 		if($videos == 1 && $photos == 0) { | ||||
| 			return 'video'; | ||||
| 		} | ||||
| 		if($photos > 1 && $videos == 0) { | ||||
| 			return 'photo:album'; | ||||
| 		} | ||||
| 		if($videos > 1 && $photos == 0) { | ||||
| 			return 'video:album'; | ||||
| 		} | ||||
| 		if($photos >= 1 && $videos >= 1) { | ||||
| 			return 'photo:video:album'; | ||||
| 		} | ||||
| 
 | ||||
| 		return 'text'; | ||||
| 	} | ||||
| 
 | ||||
| 	public function toggleVisibility(Request $request) { | ||||
| 		$this->authCheck(); | ||||
| 		$this->validate($request, [ | ||||
| 			'item' => 'required|string|min:1|max:20', | ||||
| 			'disableComments' => 'required|boolean' | ||||
| 		]); | ||||
| 
 | ||||
| 		$user = Auth::user(); | ||||
| 		$id = $request->input('item'); | ||||
| 		$state = $request->input('disableComments'); | ||||
| 
 | ||||
| 		$status = Status::findOrFail($id); | ||||
| 
 | ||||
| 		if($status->profile_id != $user->profile->id && $user->is_admin == false) { | ||||
| 			abort(403); | ||||
| 		} | ||||
| 
 | ||||
| 		$status->comments_disabled = $status->comments_disabled == true ? false : true; | ||||
| 		$status->save(); | ||||
| 
 | ||||
| 		return response()->json([200]); | ||||
| 	} | ||||
| 
 | ||||
| 	public function storeView(Request $request) | ||||
| 	{ | ||||
| 		abort_if(!$request->user(), 403); | ||||
| 
 | ||||
| 		$views = $request->input('_v'); | ||||
| 		$uid = $request->user()->profile_id; | ||||
| 
 | ||||
| 		if(empty($views) || !is_array($views)) { | ||||
| 			return response()->json(0); | ||||
| 		} | ||||
| 
 | ||||
| 		Cache::forget('profile:home-timeline-cursor:' . $request->user()->id); | ||||
| 
 | ||||
| 		foreach($views as $view) { | ||||
| 			if(!isset($view['sid']) || !isset($view['pid'])) { | ||||
| 				continue; | ||||
| 			} | ||||
| 			DB::transaction(function () use($view, $uid) { | ||||
| 				StatusView::firstOrCreate([ | ||||
| 						'status_id' => $view['sid'], | ||||
| 						'status_profile_id' => $view['pid'], | ||||
| 						'profile_id' => $uid | ||||
| 				]); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return response()->json(1); | ||||
| 	} | ||||
|     public function show(Request $request, $username, $id) | ||||
|     { | ||||
|         // redirect authed users to Metro 2.0
 | ||||
|         if ($request->user()) { | ||||
|             // unless they force static view
 | ||||
|             if (! $request->has('fs') || $request->input('fs') != '1') { | ||||
|                 return redirect('/i/web/post/'.$id); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
| 
 | ||||
|         if ($user->status != null) { | ||||
|             return ProfileController::accountCheck($user); | ||||
|         } | ||||
| 
 | ||||
|         $status = Status::whereProfileId($user->id) | ||||
|             ->whereNull('reblog_of_id') | ||||
|             ->whereIn('scope', ['public', 'unlisted', 'private']) | ||||
|             ->findOrFail($id); | ||||
| 
 | ||||
|         if ($status->uri || $status->url) { | ||||
|             $url = $status->uri ?? $status->url; | ||||
|             if (ends_with($url, '/activity')) { | ||||
|                 $url = str_replace('/activity', '', $url); | ||||
|             } | ||||
| 
 | ||||
|             return redirect($url); | ||||
|         } | ||||
| 
 | ||||
|         if ($status->visibility == 'private' || $user->is_private) { | ||||
|             if (! Auth::check()) { | ||||
|                 abort(404); | ||||
|             } | ||||
|             $pid = Auth::user()->profile; | ||||
|             if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { | ||||
|                 abort(404); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ($status->type == 'archived') { | ||||
|             if (Auth::user()->profile_id !== $status->profile_id) { | ||||
|                 abort(404); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ($request->user() && $request->user()->profile_id != $status->profile_id) { | ||||
|             StatusView::firstOrCreate([ | ||||
|                 'status_id' => $status->id, | ||||
|                 'status_profile_id' => $status->profile_id, | ||||
|                 'profile_id' => $request->user()->profile_id, | ||||
|             ]); | ||||
|         } | ||||
| 
 | ||||
|         if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { | ||||
|             return $this->showActivityPub($request, $status); | ||||
|         } | ||||
| 
 | ||||
|         $template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; | ||||
| 
 | ||||
|         return view($template, compact('user', 'status')); | ||||
|     } | ||||
| 
 | ||||
|     public function shortcodeRedirect(Request $request, $id) | ||||
|     { | ||||
|         $hid = HashidService::decode($id); | ||||
|         abort_if(! $hid, 404); | ||||
| 
 | ||||
|         return redirect('/i/web/post/'.$hid); | ||||
|     } | ||||
| 
 | ||||
|     public function showId(int $id) | ||||
|     { | ||||
|         abort(404); | ||||
|         $status = Status::whereNull('reblog_of_id') | ||||
|             ->whereIn('scope', ['public', 'unlisted']) | ||||
|             ->findOrFail($id); | ||||
| 
 | ||||
|         return redirect($status->url()); | ||||
|     } | ||||
| 
 | ||||
|     public function showEmbed(Request $request, $username, int $id) | ||||
|     { | ||||
|         if (! (bool) config_cache('instance.embed.post')) { | ||||
|             $res = view('status.embed-removed'); | ||||
| 
 | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
| 
 | ||||
|         $profile = Profile::whereNull(['domain', 'status']) | ||||
|             ->whereIsPrivate(false) | ||||
|             ->whereUsername($username) | ||||
|             ->first(); | ||||
| 
 | ||||
|         if (! $profile) { | ||||
|             $content = view('status.embed-removed'); | ||||
| 
 | ||||
|             return response($content)->header('X-Frame-Options', 'ALLOWALL'); | ||||
|         } | ||||
| 
 | ||||
|         $aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 86400, function () use ($profile) { | ||||
|             $exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count(); | ||||
|             if ($exists) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|         if ($aiCheck) { | ||||
|             $res = view('status.embed-removed'); | ||||
| 
 | ||||
|             return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|         } | ||||
|         $status = Status::whereProfileId($profile->id) | ||||
|             ->whereNull('uri') | ||||
|             ->whereScope('public') | ||||
|             ->whereIsNsfw(false) | ||||
|             ->whereIn('type', ['photo', 'video', 'photo:album']) | ||||
|             ->find($id); | ||||
|         if (! $status) { | ||||
|             $content = view('status.embed-removed'); | ||||
| 
 | ||||
|             return response($content)->header('X-Frame-Options', 'ALLOWALL'); | ||||
|         } | ||||
|         $showLikes = $request->filled('likes') && $request->likes == true; | ||||
|         $showCaption = $request->filled('caption') && $request->caption !== false; | ||||
|         $layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full'; | ||||
|         $content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout')); | ||||
| 
 | ||||
|         return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); | ||||
|     } | ||||
| 
 | ||||
|     public function showObject(Request $request, $username, int $id) | ||||
|     { | ||||
|         $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); | ||||
| 
 | ||||
|         if ($user->status != null) { | ||||
|             return ProfileController::accountCheck($user); | ||||
|         } | ||||
| 
 | ||||
|         $status = Status::whereProfileId($user->id) | ||||
|             ->whereNotIn('visibility', ['draft', 'direct']) | ||||
|             ->findOrFail($id); | ||||
| 
 | ||||
|         abort_if($status->uri, 404); | ||||
| 
 | ||||
|         if ($status->visibility == 'private' || $user->is_private) { | ||||
|             if (! Auth::check()) { | ||||
|                 abort(403); | ||||
|             } | ||||
|             $pid = Auth::user()->profile; | ||||
|             if ($user->followedBy($pid) == false && $user->id !== $pid->id) { | ||||
|                 abort(403); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $this->showActivityPub($request, $status); | ||||
|     } | ||||
| 
 | ||||
|     public function compose() | ||||
|     { | ||||
|         $this->authCheck(); | ||||
| 
 | ||||
|         return view('status.compose'); | ||||
|     } | ||||
| 
 | ||||
|     public function store(Request $request) | ||||
|     { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function delete(Request $request) | ||||
|     { | ||||
|         $this->authCheck(); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'item' => 'required|integer|min:1', | ||||
|         ]); | ||||
| 
 | ||||
|         $status = Status::findOrFail($request->input('item')); | ||||
| 
 | ||||
|         $user = Auth::user(); | ||||
| 
 | ||||
|         if ($status->profile_id != $user->profile->id && | ||||
|             $user->is_admin == true && | ||||
|             $status->uri == null | ||||
|         ) { | ||||
|             $media = $status->media; | ||||
| 
 | ||||
|             $ai = new AccountInterstitial; | ||||
|             $ai->user_id = $status->profile->user_id; | ||||
|             $ai->type = 'post.removed'; | ||||
|             $ai->view = 'account.moderation.post.removed'; | ||||
|             $ai->item_type = 'App\Status'; | ||||
|             $ai->item_id = $status->id; | ||||
|             $ai->has_media = (bool) $media->count(); | ||||
|             $ai->blurhash = $media->count() ? $media->first()->blurhash : null; | ||||
|             $ai->meta = json_encode([ | ||||
|                 'caption' => $status->caption, | ||||
|                 'created_at' => $status->created_at, | ||||
|                 'type' => $status->type, | ||||
|                 'url' => $status->url(), | ||||
|                 'is_nsfw' => $status->is_nsfw, | ||||
|                 'scope' => $status->scope, | ||||
|                 'reblog' => $status->reblog_of_id, | ||||
|                 'likes_count' => $status->likes_count, | ||||
|                 'reblogs_count' => $status->reblogs_count, | ||||
|             ]); | ||||
|             $ai->save(); | ||||
| 
 | ||||
|             $u = $status->profile->user; | ||||
|             $u->has_interstitial = true; | ||||
|             $u->save(); | ||||
|         } | ||||
| 
 | ||||
|         if ($status->in_reply_to_id) { | ||||
|             $parent = Status::find($status->in_reply_to_id); | ||||
|             if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) { | ||||
|                 Cache::forget('_api:statuses:recent_9:'.$status->profile_id); | ||||
|                 Cache::forget('profile:status_count:'.$status->profile_id); | ||||
|                 Cache::forget('profile:embed:'.$status->profile_id); | ||||
|                 StatusService::del($status->id, true); | ||||
|                 Cache::forget('profile:status_count:'.$status->profile_id); | ||||
|                 $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); | ||||
|             } | ||||
|         } elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) { | ||||
|             Cache::forget('_api:statuses:recent_9:'.$status->profile_id); | ||||
|             Cache::forget('profile:status_count:'.$status->profile_id); | ||||
|             Cache::forget('profile:embed:'.$status->profile_id); | ||||
|             StatusService::del($status->id, true); | ||||
|             Cache::forget('profile:status_count:'.$status->profile_id); | ||||
|             $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status); | ||||
|         } | ||||
| 
 | ||||
|         if ($request->wantsJson()) { | ||||
|             return response()->json(['Status successfully deleted.']); | ||||
|         } else { | ||||
|             return redirect($user->url()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function storeShare(Request $request) | ||||
|     { | ||||
|         $this->authCheck(); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'item' => 'required|integer|min:1', | ||||
|         ]); | ||||
| 
 | ||||
|         $user = Auth::user(); | ||||
|         $profile = $user->profile; | ||||
|         $status = Status::whereScope('public') | ||||
|             ->findOrFail($request->input('item')); | ||||
| 
 | ||||
|         $count = $status->reblogs_count; | ||||
| 
 | ||||
|         $exists = Status::whereProfileId(Auth::user()->profile->id) | ||||
|             ->whereReblogOfId($status->id) | ||||
|             ->exists(); | ||||
|         if ($exists == true) { | ||||
|             $shares = Status::whereProfileId(Auth::user()->profile->id) | ||||
|                 ->whereReblogOfId($status->id) | ||||
|                 ->get(); | ||||
|             foreach ($shares as $share) { | ||||
|                 UndoSharePipeline::dispatch($share); | ||||
|                 ReblogService::del($profile->id, $status->id); | ||||
|                 $count--; | ||||
|             } | ||||
|         } else { | ||||
|             $share = new Status(); | ||||
|             $share->profile_id = $profile->id; | ||||
|             $share->reblog_of_id = $status->id; | ||||
|             $share->in_reply_to_profile_id = $status->profile_id; | ||||
|             $share->type = 'share'; | ||||
|             $share->save(); | ||||
|             $count++; | ||||
|             SharePipeline::dispatch($share); | ||||
|             ReblogService::add($profile->id, $status->id); | ||||
|         } | ||||
| 
 | ||||
|         Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id); | ||||
|         StatusService::del($status->id); | ||||
| 
 | ||||
|         if ($request->ajax()) { | ||||
|             $response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count]; | ||||
|         } else { | ||||
|             $response = redirect($status->url()); | ||||
|         } | ||||
| 
 | ||||
|         return $response; | ||||
|     } | ||||
| 
 | ||||
|     public function showActivityPub(Request $request, $status) | ||||
|     { | ||||
|         $object = $status->type == 'poll' ? new Question() : new Note(); | ||||
|         $fractal = new Fractal\Manager(); | ||||
|         $resource = new Fractal\Resource\Item($status, $object); | ||||
|         $res = $fractal->createData($resource)->toArray(); | ||||
| 
 | ||||
|         return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | ||||
|     } | ||||
| 
 | ||||
|     public function edit(Request $request, $username, $id) | ||||
|     { | ||||
|         $this->authCheck(); | ||||
|         $user = Auth::user()->profile; | ||||
|         $status = Status::whereProfileId($user->id) | ||||
|             ->with(['media']) | ||||
|             ->findOrFail($id); | ||||
|         $licenses = License::get(); | ||||
| 
 | ||||
|         return view('status.edit', compact('user', 'status', 'licenses')); | ||||
|     } | ||||
| 
 | ||||
|     public function editStore(Request $request, $username, $id) | ||||
|     { | ||||
|         $this->authCheck(); | ||||
|         $user = Auth::user()->profile; | ||||
|         $status = Status::whereProfileId($user->id) | ||||
|             ->with(['media']) | ||||
|             ->findOrFail($id); | ||||
| 
 | ||||
|         $this->validate($request, [ | ||||
|             'license' => 'nullable|integer|min:1|max:16', | ||||
|         ]); | ||||
| 
 | ||||
|         $licenseId = $request->input('license'); | ||||
| 
 | ||||
|         $status->media->each(function ($media) use ($licenseId) { | ||||
|             $media->license = $licenseId; | ||||
|             $media->save(); | ||||
|             Cache::forget('status:transformer:media:attachments:'.$media->status_id); | ||||
|         }); | ||||
| 
 | ||||
|         return redirect($status->url()); | ||||
|     } | ||||
| 
 | ||||
|     protected function authCheck() | ||||
|     { | ||||
|         if (Auth::check() == false) { | ||||
|             abort(403); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected function validateVisibility($visibility) | ||||
|     { | ||||
|         $allowed = ['public', 'unlisted', 'private']; | ||||
| 
 | ||||
|         return in_array($visibility, $allowed) ? $visibility : 'public'; | ||||
|     } | ||||
| 
 | ||||
|     public static function mimeTypeCheck($mimes) | ||||
|     { | ||||
|         $allowed = explode(',', config_cache('pixelfed.media_types')); | ||||
|         $count = count($mimes); | ||||
|         $photos = 0; | ||||
|         $videos = 0; | ||||
|         foreach ($mimes as $mime) { | ||||
|             if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') { | ||||
|                 continue; | ||||
|             } | ||||
|             if (str_contains($mime, 'image/')) { | ||||
|                 $photos++; | ||||
|             } | ||||
|             if (str_contains($mime, 'video/')) { | ||||
|                 $videos++; | ||||
|             } | ||||
|         } | ||||
|         if ($photos == 1 && $videos == 0) { | ||||
|             return 'photo'; | ||||
|         } | ||||
|         if ($videos == 1 && $photos == 0) { | ||||
|             return 'video'; | ||||
|         } | ||||
|         if ($photos > 1 && $videos == 0) { | ||||
|             return 'photo:album'; | ||||
|         } | ||||
|         if ($videos > 1 && $photos == 0) { | ||||
|             return 'video:album'; | ||||
|         } | ||||
|         if ($photos >= 1 && $videos >= 1) { | ||||
|             return 'photo:video:album'; | ||||
|         } | ||||
| 
 | ||||
|         return 'text'; | ||||
|     } | ||||
| 
 | ||||
|     public function toggleVisibility(Request $request) | ||||
|     { | ||||
|         $this->authCheck(); | ||||
|         $this->validate($request, [ | ||||
|             'item' => 'required|string|min:1|max:20', | ||||
|             'disableComments' => 'required|boolean', | ||||
|         ]); | ||||
| 
 | ||||
|         $user = Auth::user(); | ||||
|         $id = $request->input('item'); | ||||
|         $state = $request->input('disableComments'); | ||||
| 
 | ||||
|         $status = Status::findOrFail($id); | ||||
| 
 | ||||
|         if ($status->profile_id != $user->profile->id && $user->is_admin == false) { | ||||
|             abort(403); | ||||
|         } | ||||
| 
 | ||||
|         $status->comments_disabled = $status->comments_disabled == true ? false : true; | ||||
|         $status->save(); | ||||
| 
 | ||||
|         return response()->json([200]); | ||||
|     } | ||||
| 
 | ||||
|     public function storeView(Request $request) | ||||
|     { | ||||
|         abort_if(! $request->user(), 403); | ||||
| 
 | ||||
|         $views = $request->input('_v'); | ||||
|         $uid = $request->user()->profile_id; | ||||
| 
 | ||||
|         if (empty($views) || ! is_array($views)) { | ||||
|             return response()->json(0); | ||||
|         } | ||||
| 
 | ||||
|         Cache::forget('profile:home-timeline-cursor:'.$request->user()->id); | ||||
| 
 | ||||
|         foreach ($views as $view) { | ||||
|             if (! isset($view['sid']) || ! isset($view['pid'])) { | ||||
|                 continue; | ||||
|             } | ||||
|             DB::transaction(function () use ($view, $uid) { | ||||
|                 StatusView::firstOrCreate([ | ||||
|                     'status_id' => $view['sid'], | ||||
|                     'status_profile_id' => $view['pid'], | ||||
|                     'profile_id' => $uid, | ||||
|                 ]); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return response()->json(1); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -75,6 +75,20 @@ class ConfigCacheService | |||
|                 'instance.curated_registration.enabled', | ||||
| 
 | ||||
|                 'federation.migration', | ||||
| 
 | ||||
|                 'pixelfed.max_caption_length', | ||||
|                 'pixelfed.max_bio_length', | ||||
|                 'pixelfed.max_name_length', | ||||
|                 'pixelfed.min_password_length', | ||||
|                 'pixelfed.max_avatar_size', | ||||
|                 'pixelfed.max_altext_length', | ||||
|                 'pixelfed.allow_app_registration', | ||||
|                 'pixelfed.app_registration_rate_limit_attempts', | ||||
|                 'pixelfed.app_registration_rate_limit_decay', | ||||
|                 'pixelfed.app_registration_confirm_rate_limit_attempts', | ||||
|                 'pixelfed.app_registration_confirm_rate_limit_decay', | ||||
|                 'instance.embed.profile', | ||||
|                 'instance.embed.post', | ||||
|                 // 'system.user_mode'
 | ||||
|             ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,54 +2,38 @@ | |||
| 
 | ||||
| namespace App\Services; | ||||
| 
 | ||||
| use Cache; | ||||
| class HashidService | ||||
| { | ||||
|     public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; | ||||
| 
 | ||||
| class HashidService { | ||||
|     public static function encode($id, $minLimit = true) | ||||
|     { | ||||
|         if (! is_numeric($id) || $id > PHP_INT_MAX) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
| 	public const MIN_LIMIT = 15; | ||||
| 	public const CMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; | ||||
|         $cmap = self::CMAP; | ||||
|         $base = strlen($cmap); | ||||
|         $shortcode = ''; | ||||
|         while ($id) { | ||||
|             $id = ($id - ($r = $id % $base)) / $base; | ||||
|             $shortcode = $cmap[$r].$shortcode; | ||||
|         } | ||||
| 
 | ||||
| 	public static function encode($id, $minLimit = true) | ||||
| 	{ | ||||
| 		if(!is_numeric($id) || $id > PHP_INT_MAX) { | ||||
| 			return null; | ||||
| 		} | ||||
|         return $shortcode; | ||||
|     } | ||||
| 
 | ||||
| 		if($minLimit && strlen($id) < self::MIN_LIMIT) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		$key = "hashids:{$id}"; | ||||
| 		return Cache::remember($key, now()->hours(48), function() use($id) { | ||||
| 			$cmap = self::CMAP; | ||||
| 			$base = strlen($cmap); | ||||
| 			$shortcode = ''; | ||||
| 			while($id) { | ||||
| 				$id = ($id - ($r = $id % $base)) / $base; | ||||
| 				$shortcode = $cmap[$r] . $shortcode; | ||||
| 			} | ||||
| 			return $shortcode; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public static function decode($short) | ||||
| 	{ | ||||
| 		$len = strlen($short); | ||||
| 		if($len < 3 || $len > 11) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		$id = 0; | ||||
| 		foreach(str_split($short) as $needle) { | ||||
| 			$pos = strpos(self::CMAP, $needle); | ||||
| 			// if(!$pos) {
 | ||||
| 			// 	return null;
 | ||||
| 			// }
 | ||||
| 			$id = ($id*64) + $pos; | ||||
| 		} | ||||
| 		if(strlen($id) < self::MIN_LIMIT) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return $id; | ||||
| 	} | ||||
|     public static function decode($short = false) | ||||
|     { | ||||
|         if (! $short) { | ||||
|             return; | ||||
|         } | ||||
|         $id = 0; | ||||
|         foreach (str_split($short) as $needle) { | ||||
|             $pos = strpos(self::CMAP, $needle); | ||||
|             $id = ($id * 64) + $pos; | ||||
|         } | ||||
| 
 | ||||
|         return $id; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,105 +2,104 @@ | |||
| 
 | ||||
| namespace App\Services; | ||||
| 
 | ||||
| use App\Util\ActivityPub\Helpers; | ||||
| use Illuminate\Support\Str; | ||||
| use Illuminate\Support\Facades\Cache; | ||||
| use Illuminate\Support\Facades\Redis; | ||||
| use App\Status; | ||||
| use App\User; | ||||
| use App\Services\AccountService; | ||||
| use App\Util\Site\Nodeinfo; | ||||
| use Illuminate\Support\Facades\Cache; | ||||
| use Illuminate\Support\Str; | ||||
| 
 | ||||
| class LandingService | ||||
| { | ||||
| 	public static function get($json = true) | ||||
| 	{ | ||||
| 		$activeMonth = Nodeinfo::activeUsersMonthly(); | ||||
|     public static function get($json = true) | ||||
|     { | ||||
|         $activeMonth = Nodeinfo::activeUsersMonthly(); | ||||
| 
 | ||||
| 		$totalUsers = Cache::remember('api:nodeinfo:users', 43200, function() { | ||||
| 			return User::count(); | ||||
| 		}); | ||||
|         $totalUsers = Cache::remember('api:nodeinfo:users', 43200, function () { | ||||
|             return User::count(); | ||||
|         }); | ||||
| 
 | ||||
| 		$postCount = Cache::remember('api:nodeinfo:statuses', 21600, function() { | ||||
| 			return Status::whereLocal(true)->count(); | ||||
| 		}); | ||||
|         $postCount = Cache::remember('api:nodeinfo:statuses', 21600, function () { | ||||
|             return Status::whereLocal(true)->count(); | ||||
|         }); | ||||
| 
 | ||||
| 		$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { | ||||
| 			if(config_cache('instance.admin.pid')) { | ||||
| 				return AccountService::getMastodon(config_cache('instance.admin.pid'), true); | ||||
| 			} | ||||
| 			$admin = User::whereIsAdmin(true)->first(); | ||||
| 			return $admin && isset($admin->profile_id) ? | ||||
| 				AccountService::getMastodon($admin->profile_id, true) : | ||||
| 				null; | ||||
| 		}); | ||||
|         $contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () { | ||||
|             if (config_cache('instance.admin.pid')) { | ||||
|                 return AccountService::getMastodon(config_cache('instance.admin.pid'), true); | ||||
|             } | ||||
|             $admin = User::whereIsAdmin(true)->first(); | ||||
| 
 | ||||
| 		$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { | ||||
| 			return config_cache('app.rules') ? | ||||
| 				collect(json_decode(config_cache('app.rules'), true)) | ||||
| 				->map(function($rule, $key) { | ||||
| 					$id = $key + 1; | ||||
| 					return [ | ||||
| 						'id' => "{$id}", | ||||
| 						'text' => $rule | ||||
| 					]; | ||||
| 				}) | ||||
| 				->toArray() : []; | ||||
| 		}); | ||||
|             return $admin && isset($admin->profile_id) ? | ||||
|                 AccountService::getMastodon($admin->profile_id, true) : | ||||
|                 null; | ||||
|         }); | ||||
| 
 | ||||
| 		$openReg = (bool) config_cache('pixelfed.open_registration'); | ||||
|         $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () { | ||||
|             return config_cache('app.rules') ? | ||||
|                 collect(json_decode(config_cache('app.rules'), true)) | ||||
|                     ->map(function ($rule, $key) { | ||||
|                         $id = $key + 1; | ||||
| 
 | ||||
| 		$res = [ | ||||
| 			'name' => config_cache('app.name'), | ||||
| 			'url' => config_cache('app.url'), | ||||
| 			'domain' => config('pixelfed.domain.app'), | ||||
| 			'show_directory' => config_cache('instance.landing.show_directory'), | ||||
| 			'show_explore_feed' => config_cache('instance.landing.show_explore'), | ||||
| 			'open_registration' => (bool) $openReg, | ||||
| 			'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), | ||||
| 			'version' => config('pixelfed.version'), | ||||
| 			'about' => [ | ||||
| 				'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), | ||||
| 				'short_description' => config_cache('app.short_description'), | ||||
| 				'description' => config_cache('app.description'), | ||||
| 			], | ||||
| 			'stats' => [ | ||||
| 				'active_users' => (int) $activeMonth, | ||||
| 				'posts_count' => (int) $postCount, | ||||
| 				'total_users' => (int) $totalUsers | ||||
| 			], | ||||
| 			'contact' => [ | ||||
| 				'account' => $contactAccount, | ||||
| 				'email' => config('instance.email') | ||||
| 			], | ||||
| 			'rules' => $rules, | ||||
| 			'uploader' => [ | ||||
| 				'max_photo_size' => (int) (config('pixelfed.max_photo_size') * 1024), | ||||
| 				'max_caption_length' => (int) config('pixelfed.max_caption_length'), | ||||
| 				'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), | ||||
| 				'album_limit' => (int) config_cache('pixelfed.max_album_length'), | ||||
| 				'image_quality' => (int) config_cache('pixelfed.image_quality'), | ||||
| 				'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), | ||||
| 				'optimize_image' => (bool) config('pixelfed.optimize_image'), | ||||
| 				'optimize_video' => (bool) config('pixelfed.optimize_video'), | ||||
| 				'media_types' => config_cache('pixelfed.media_types'), | ||||
| 			], | ||||
| 			'features' => [ | ||||
| 				'federation' => config_cache('federation.activitypub.enabled'), | ||||
| 				'timelines' => [ | ||||
| 					'local' => true, | ||||
| 					'network' => (bool) config('federation.network_timeline'), | ||||
| 				], | ||||
| 				'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), | ||||
| 				'stories' => (bool) config_cache('instance.stories.enabled'), | ||||
| 				'video'	=> Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), | ||||
| 			] | ||||
| 		]; | ||||
|                         return [ | ||||
|                             'id' => "{$id}", | ||||
|                             'text' => $rule, | ||||
|                         ]; | ||||
|                     }) | ||||
|                     ->toArray() : []; | ||||
|         }); | ||||
| 
 | ||||
| 		if($json) { | ||||
| 			return json_encode($res); | ||||
| 		} | ||||
|         $openReg = (bool) config_cache('pixelfed.open_registration'); | ||||
| 
 | ||||
| 		return $res; | ||||
| 	} | ||||
|         $res = [ | ||||
|             'name' => config_cache('app.name'), | ||||
|             'url' => config_cache('app.url'), | ||||
|             'domain' => config('pixelfed.domain.app'), | ||||
|             'show_directory' => config_cache('instance.landing.show_directory'), | ||||
|             'show_explore_feed' => config_cache('instance.landing.show_explore'), | ||||
|             'open_registration' => (bool) $openReg, | ||||
|             'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), | ||||
|             'version' => config('pixelfed.version'), | ||||
|             'about' => [ | ||||
|                 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), | ||||
|                 'short_description' => config_cache('app.short_description'), | ||||
|                 'description' => config_cache('app.description'), | ||||
|             ], | ||||
|             'stats' => [ | ||||
|                 'active_users' => (int) $activeMonth, | ||||
|                 'posts_count' => (int) $postCount, | ||||
|                 'total_users' => (int) $totalUsers, | ||||
|             ], | ||||
|             'contact' => [ | ||||
|                 'account' => $contactAccount, | ||||
|                 'email' => config('instance.email'), | ||||
|             ], | ||||
|             'rules' => $rules, | ||||
|             'uploader' => [ | ||||
|                 'max_photo_size' => (int) (config_cache('pixelfed.max_photo_size') * 1024), | ||||
|                 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), | ||||
|                 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), | ||||
|                 'album_limit' => (int) config_cache('pixelfed.max_album_length'), | ||||
|                 'image_quality' => (int) config_cache('pixelfed.image_quality'), | ||||
|                 'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), | ||||
|                 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'), | ||||
|                 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'), | ||||
|                 'media_types' => config_cache('pixelfed.media_types'), | ||||
|             ], | ||||
|             'features' => [ | ||||
|                 'federation' => config_cache('federation.activitypub.enabled'), | ||||
|                 'timelines' => [ | ||||
|                     'local' => true, | ||||
|                     'network' => (bool) config_cache('federation.network_timeline'), | ||||
|                 ], | ||||
|                 'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'), | ||||
|                 'stories' => (bool) config_cache('instance.stories.enabled'), | ||||
|                 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'), | ||||
|             ], | ||||
|         ]; | ||||
| 
 | ||||
|         if ($json) { | ||||
|             return json_encode($res); | ||||
|         } | ||||
| 
 | ||||
|         return $res; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,67 +3,80 @@ | |||
| namespace App\Transformer\ActivityPub; | ||||
| 
 | ||||
| use App\Profile; | ||||
| use League\Fractal; | ||||
| use App\Services\AccountService; | ||||
| use League\Fractal; | ||||
| 
 | ||||
| class ProfileTransformer extends Fractal\TransformerAbstract | ||||
| { | ||||
|     public function transform(Profile $profile) | ||||
|     { | ||||
|         $res = [ | ||||
|           '@context' => [ | ||||
|             'https://w3id.org/security/v1', | ||||
|             'https://www.w3.org/ns/activitystreams', | ||||
|             [ | ||||
|               'toot' => 'http://joinmastodon.org/ns#', | ||||
|               'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', | ||||
|               'alsoKnownAs' => [ | ||||
|                     '@id' => 'as:alsoKnownAs', | ||||
|                     '@type' => '@id' | ||||
|               ], | ||||
|               'movedTo' => [ | ||||
|                     '@id' => 'as:movedTo', | ||||
|                     '@type' => '@id' | ||||
|               ], | ||||
|               'indexable' => 'toot:indexable', | ||||
|             '@context' => [ | ||||
|                 'https://w3id.org/security/v1', | ||||
|                 'https://www.w3.org/ns/activitystreams', | ||||
|                 [ | ||||
|                     'toot' => 'http://joinmastodon.org/ns#', | ||||
|                     'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', | ||||
|                     'alsoKnownAs' => [ | ||||
|                         '@id' => 'as:alsoKnownAs', | ||||
|                         '@type' => '@id', | ||||
|                     ], | ||||
|                     'movedTo' => [ | ||||
|                         '@id' => 'as:movedTo', | ||||
|                         '@type' => '@id', | ||||
|                     ], | ||||
|                     'indexable' => 'toot:indexable', | ||||
|                     'suspended' => 'toot:suspended', | ||||
|                 ], | ||||
|             ], | ||||
|           ], | ||||
|           'id'                        => $profile->permalink(), | ||||
|           'type'                      => 'Person', | ||||
|           'following'                 => $profile->permalink('/following'), | ||||
|           'followers'                 => $profile->permalink('/followers'), | ||||
|           'inbox'                     => $profile->permalink('/inbox'), | ||||
|           'outbox'                    => $profile->permalink('/outbox'), | ||||
|           'preferredUsername'         => $profile->username, | ||||
|           'name'                      => $profile->name, | ||||
|           'summary'                   => $profile->bio, | ||||
|           'url'                       => $profile->url(), | ||||
|           'manuallyApprovesFollowers' => (bool) $profile->is_private, | ||||
|           'indexable'                 => (bool) $profile->indexable, | ||||
|           'published'                 => $profile->created_at->format('Y-m-d') . 'T00:00:00Z', | ||||
|           'publicKey' => [ | ||||
|             'id'           => $profile->permalink().'#main-key', | ||||
|             'owner'        => $profile->permalink(), | ||||
|             'publicKeyPem' => $profile->public_key, | ||||
|           ], | ||||
|           'icon' => [ | ||||
|             'type'      => 'Image', | ||||
|             'mediaType' => 'image/jpeg', | ||||
|             'url'       => $profile->avatarUrl(), | ||||
|           ], | ||||
|           'endpoints' => [ | ||||
|             'sharedInbox' => config('app.url') . '/f/inbox' | ||||
|           ] | ||||
|       ]; | ||||
|             'id' => $profile->permalink(), | ||||
|             'type' => 'Person', | ||||
|             'following' => $profile->permalink('/following'), | ||||
|             'followers' => $profile->permalink('/followers'), | ||||
|             'inbox' => $profile->permalink('/inbox'), | ||||
|             'outbox' => $profile->permalink('/outbox'), | ||||
|             'preferredUsername' => $profile->username, | ||||
|             'name' => $profile->name, | ||||
|             'summary' => $profile->bio, | ||||
|             'url' => $profile->url(), | ||||
|             'manuallyApprovesFollowers' => (bool) $profile->is_private, | ||||
|             'indexable' => (bool) $profile->indexable, | ||||
|             'published' => $profile->created_at->format('Y-m-d').'T00:00:00Z', | ||||
|             'publicKey' => [ | ||||
|                 'id' => $profile->permalink().'#main-key', | ||||
|                 'owner' => $profile->permalink(), | ||||
|                 'publicKeyPem' => $profile->public_key, | ||||
|             ], | ||||
|             'icon' => [ | ||||
|                 'type' => 'Image', | ||||
|                 'mediaType' => 'image/jpeg', | ||||
|                 'url' => $profile->avatarUrl(), | ||||
|             ], | ||||
|             'endpoints' => [ | ||||
|                 'sharedInbox' => config('app.url').'/f/inbox', | ||||
|             ], | ||||
|         ]; | ||||
| 
 | ||||
|       if($profile->aliases->count()) { | ||||
|         $res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri); | ||||
|       } | ||||
|         if ($profile->status === 'delete' || $profile->deleted_at != null) { | ||||
|             $res['suspended'] = true; | ||||
|             $res['name'] = ''; | ||||
|             unset($res['icon']); | ||||
|             $res['summary'] = ''; | ||||
|             $res['indexable'] = false; | ||||
|             $res['manuallyApprovesFollowers'] = false; | ||||
|         } else { | ||||
|             if ($profile->aliases->count()) { | ||||
|                 $res['alsoKnownAs'] = $profile->aliases->map(fn ($alias) => $alias->uri); | ||||
|             } | ||||
| 
 | ||||
|       if($profile->moved_to_profile_id) { | ||||
|         $res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url']; | ||||
|       } | ||||
|             if ($profile->moved_to_profile_id) { | ||||
|                 $movedTo = AccountService::get($profile->moved_to_profile_id); | ||||
|                 if ($movedTo && isset($movedTo['url'], $movedTo['id'])) { | ||||
|                     $res['movedTo'] = $movedTo['url']; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|       return $res; | ||||
|         return $res; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,24 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Transformer\ActivityPub\Verb; | ||||
| 
 | ||||
| use App\Profile; | ||||
| use League\Fractal; | ||||
| 
 | ||||
| class DeleteActor extends Fractal\TransformerAbstract | ||||
| { | ||||
|     public function transform(Profile $profile) | ||||
|     { | ||||
|         return [ | ||||
|             '@context' => 'https://www.w3.org/ns/activitystreams', | ||||
|             'id' => $profile->permalink('#delete'), | ||||
|             'type' => 'Delete', | ||||
|             'actor' => $profile->permalink(), | ||||
|             'to' => [ | ||||
|                 'https://www.w3.org/ns/activitystreams#Public' | ||||
|             ], | ||||
|             'object' => $profile->permalink() | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -5,32 +5,34 @@ namespace App\Util\Site; | |||
| use Cache; | ||||
| use Illuminate\Support\Str; | ||||
| 
 | ||||
| class Config { | ||||
| 
 | ||||
| class Config | ||||
| { | ||||
|     const CACHE_KEY = 'api:site:configuration:_v0.8'; | ||||
| 
 | ||||
|     public static function get() { | ||||
|         return Cache::remember(self::CACHE_KEY, 900, function() { | ||||
|     public static function get() | ||||
|     { | ||||
|         return Cache::remember(self::CACHE_KEY, 900, function () { | ||||
|             $hls = [ | ||||
|                 'enabled' => config('media.hls.enabled'), | ||||
|             ]; | ||||
|             if(config('media.hls.enabled')) { | ||||
|             if (config('media.hls.enabled')) { | ||||
|                 $hls = [ | ||||
|                     'enabled' => true, | ||||
|                     'debug' => (bool) config('media.hls.debug'), | ||||
|                     'p2p' => (bool) config('media.hls.p2p'), | ||||
|                     'p2p_debug' => (bool) config('media.hls.p2p_debug'), | ||||
|                     'tracker' => config('media.hls.tracker'), | ||||
|                     'ice' => config('media.hls.ice') | ||||
|                     'ice' => config('media.hls.ice'), | ||||
|                 ]; | ||||
|             } | ||||
| 
 | ||||
|             return [ | ||||
|                 'version' => config('pixelfed.version'), | ||||
|                 'open_registration' => (bool) config_cache('pixelfed.open_registration'), | ||||
|                 'uploader' => [ | ||||
|                     'max_photo_size' => (int) config('pixelfed.max_photo_size'), | ||||
|                     'max_caption_length' => (int) config('pixelfed.max_caption_length'), | ||||
|                     'max_altext_length' => (int) config('pixelfed.max_altext_length', 150), | ||||
|                     'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), | ||||
|                     'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), | ||||
|                     'album_limit' => (int) config_cache('pixelfed.max_album_length'), | ||||
|                     'image_quality' => (int) config_cache('pixelfed.image_quality'), | ||||
| 
 | ||||
|  | @ -41,12 +43,12 @@ class Config { | |||
| 
 | ||||
|                     'media_types' => config_cache('pixelfed.media_types'), | ||||
|                     'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [], | ||||
|                     'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit') | ||||
|                     'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'), | ||||
|                 ], | ||||
| 
 | ||||
|                 'activitypub' => [ | ||||
|                     'enabled' => (bool) config_cache('federation.activitypub.enabled'), | ||||
|                     'remote_follow' => config('federation.activitypub.remoteFollow') | ||||
|                     'remote_follow' => config('federation.activitypub.remoteFollow'), | ||||
|                 ], | ||||
| 
 | ||||
|                 'ab' => config('exp'), | ||||
|  | @ -54,8 +56,8 @@ class Config { | |||
|                 'site' => [ | ||||
|                     'name' => config_cache('app.name'), | ||||
|                     'domain' => config('pixelfed.domain.app'), | ||||
|                     'url'    => config('app.url'), | ||||
|                     'description' => config_cache('app.short_description') | ||||
|                     'url' => config('app.url'), | ||||
|                     'description' => config_cache('app.short_description'), | ||||
|                 ], | ||||
| 
 | ||||
|                 'account' => [ | ||||
|  | @ -63,15 +65,15 @@ class Config { | |||
|                     'max_bio_length' => config('pixelfed.max_bio_length'), | ||||
|                     'max_name_length' => config('pixelfed.max_name_length'), | ||||
|                     'min_password_length' => config('pixelfed.min_password_length'), | ||||
|                     'max_account_size' => config('pixelfed.max_account_size') | ||||
|                     'max_account_size' => config('pixelfed.max_account_size'), | ||||
|                 ], | ||||
| 
 | ||||
|                 'username' => [ | ||||
|                     'remote' => [ | ||||
|                         'formats' => config('instance.username.remote.formats'), | ||||
|                         'format' => config('instance.username.remote.format'), | ||||
|                         'custom' => config('instance.username.remote.custom') | ||||
|                     ] | ||||
|                         'custom' => config('instance.username.remote.custom'), | ||||
|                     ], | ||||
|                 ], | ||||
| 
 | ||||
|                 'features' => [ | ||||
|  | @ -85,22 +87,29 @@ class Config { | |||
|                     'import' => [ | ||||
|                         'instagram' => (bool) config_cache('pixelfed.import.instagram.enabled'), | ||||
|                         'mastodon' => false, | ||||
|                         'pixelfed' => false | ||||
|                         'pixelfed' => false, | ||||
|                     ], | ||||
|                     'label' => [ | ||||
|                         'covid' => [ | ||||
|                             'enabled' => (bool) config('instance.label.covid.enabled'), | ||||
|                             'org' => config('instance.label.covid.org'), | ||||
|                             'url' => config('instance.label.covid.url'), | ||||
|                         ] | ||||
|                         ], | ||||
|                     ], | ||||
|                     'hls' => $hls | ||||
|                 ] | ||||
|                     'hls' => $hls, | ||||
|                 ], | ||||
|             ]; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public static function json() { | ||||
|     public static function refresh() | ||||
|     { | ||||
|         Cache::forget(self::CACHE_KEY); | ||||
|         return self::get(); | ||||
|     } | ||||
| 
 | ||||
|     public static function json() | ||||
|     { | ||||
|         return json_encode(self::get(), JSON_FORCE_OBJECT); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,28 @@ | |||
| <?php | ||||
| 
 | ||||
| use Illuminate\Database\Migrations\Migration; | ||||
| use Illuminate\Database\Schema\Blueprint; | ||||
| use Illuminate\Support\Facades\Schema; | ||||
| 
 | ||||
| return new class extends Migration | ||||
| { | ||||
|     /** | ||||
|      * Run the migrations. | ||||
|      */ | ||||
|     public function up(): void | ||||
|     { | ||||
|         Schema::table('instances', function (Blueprint $table) { | ||||
|             $table->string('shared_inbox')->nullable()->index(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reverse the migrations. | ||||
|      */ | ||||
|     public function down(): void | ||||
|     { | ||||
|         Schema::table('instances', function (Blueprint $table) { | ||||
|             $table->dropColumn('shared_inbox'); | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
|  | @ -0,0 +1,32 @@ | |||
| <?php | ||||
| 
 | ||||
| use Illuminate\Database\Migrations\Migration; | ||||
| use Illuminate\Database\Schema\Blueprint; | ||||
| use Illuminate\Support\Facades\Schema; | ||||
| use App\Instance; | ||||
| use App\Profile; | ||||
| 
 | ||||
| return new class extends Migration | ||||
| { | ||||
|     /** | ||||
|      * Run the migrations. | ||||
|      */ | ||||
|     public function up(): void | ||||
|     { | ||||
|         foreach(Instance::lazyById(50, 'id') as $instance) { | ||||
|             $si = Profile::whereDomain($instance->domain)->whereNotNull('sharedInbox')->first(); | ||||
|             if($si && $si->sharedInbox) { | ||||
|                 $instance->shared_inbox = $si->sharedInbox; | ||||
|                 $instance->save(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reverse the migrations. | ||||
|      */ | ||||
|     public function down(): void | ||||
|     { | ||||
| 
 | ||||
|     } | ||||
| }; | ||||
|  | @ -25,7 +25,7 @@ | |||
|             <hr class="border-dark"> | ||||
|             <p>From our Admins:</p> | ||||
|             <div class="card card-body mb-1 bg-dark border border-secondary" style="border-style: dashed !important;"> | ||||
|                 <p class="lead mb-0" style="white-space: pre; opacity: 0.8">{{ $activity->message }}</p> | ||||
|                 <p class="lead mb-0" style="white-space: pre-wrap; opacity: 0.8;">{{ $activity->message }}</p> | ||||
|             </div> | ||||
|             <p class="mb-3 small text-muted">If you don't understand this request, or need additional context you should request clarification from the admin team.</p> | ||||
|             {{-- <hr class="border-dark"> --}} | ||||
|  |  | |||
|  | @ -50,7 +50,7 @@ | |||
| 		</a> | ||||
| 		<div class="collapse" id="collapse3"> | ||||
| 			<div> | ||||
| 				During the compose process, you will see the <span class="font-weight-bold">Caption</span> input. Captions are optional and limited to <span class="font-weight-bold">{{config('pixelfed.max_caption_length')}}</span> characters. | ||||
| 				During the compose process, you will see the <span class="font-weight-bold">Caption</span> input. Captions are optional and limited to <span class="font-weight-bold">{{config_cache('pixelfed.max_caption_length')}}</span> characters. | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</p> | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact | |||
|                 Route::post('discover/admin/features', 'DiscoverController@updateFeatures'); | ||||
|             }); | ||||
| 
 | ||||
|             Route::get('discover/accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular'); | ||||
|             Route::get('discover/accounts/popular', 'DiscoverController@discoverAccountsPopular'); | ||||
|             Route::post('web/change-language.json', 'SpaController@updateLanguage'); | ||||
|         }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact | |||
|     Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister'); | ||||
|     Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore'); | ||||
| 
 | ||||
|     Route::get('auth/sign_up', 'CuratedRegisterController@index')->name('auth.curated-onboarding'); | ||||
|     Route::get('auth/sign_up', 'SiteController@curatedOnboarding')->name('auth.curated-onboarding'); | ||||
|     Route::post('auth/sign_up', 'CuratedRegisterController@proceed'); | ||||
|     Route::get('auth/sign_up/concierge/response-sent', 'CuratedRegisterController@conciergeResponseSent'); | ||||
|     Route::get('auth/sign_up/concierge', 'CuratedRegisterController@concierge'); | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Christian Winther
						Christian Winther