mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-10 16:44:50 +00:00
pulling latest files from upstream
This commit is contained in:
commit
5e83300d06
231 changed files with 23481 additions and 353 deletions
|
@ -2,12 +2,18 @@
|
|||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
|
||||
|
||||
### Added
|
||||
- Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021))
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Controller, add support for notification filter types ([f61159a1](https://github.com/pixelfed/pixelfed/commit/f61159a1))
|
||||
- Update ApiV1Dot1Controller, fix mutual api ([a8bb97b2](https://github.com/pixelfed/pixelfed/commit/a8bb97b2))
|
||||
- Update ApiV1Controller, fix /api/v1/favourits pagination ([72f68160](https://github.com/pixelfed/pixelfed/commit/72f68160))
|
||||
- Update RegisterController, update username constraints, require atleast one alpha char ([dd6e3cc2](https://github.com/pixelfed/pixelfed/commit/dd6e3cc2))
|
||||
- Update AdminUser, fix entity casting ([cb5620d4](https://github.com/pixelfed/pixelfed/commit/cb5620d4))
|
||||
- Update instance config, update network cache feed max_hours_old falloff to 90 days instead of 6 hours to allow for less active instances to have more results ([c042d135](https://github.com/pixelfed/pixelfed/commit/c042d135))
|
||||
- Update ApiV1Dot1Controller, add new single media status create endpoint ([b03f5cec](https://github.com/pixelfed/pixelfed/commit/b03f5cec))
|
||||
- Update AdminSettings component, add link to Custom CSS settings ([958daac4](https://github.com/pixelfed/pixelfed/commit/958daac4))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
|
|
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
49
app/Http/Controllers/Admin/AdminGroupsController.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait AdminGroupsController
|
||||
{
|
||||
public function groupsHome(Request $request)
|
||||
{
|
||||
$stats = $this->groupAdminStats();
|
||||
|
||||
return view('admin.groups.home', compact('stats'));
|
||||
}
|
||||
|
||||
protected function groupAdminStats()
|
||||
{
|
||||
return Cache::remember('admin:groups:stats', 3, function () {
|
||||
$res = [
|
||||
'total' => Group::count(),
|
||||
'local' => Group::whereLocal(true)->count(),
|
||||
];
|
||||
|
||||
$res['remote'] = $res['total'] - $res['local'];
|
||||
$res['categories'] = GroupCategory::count();
|
||||
$res['posts'] = GroupPost::count();
|
||||
$res['members'] = GroupMember::count();
|
||||
$res['interactions'] = GroupInteraction::count();
|
||||
$res['reports'] = GroupReport::count();
|
||||
|
||||
$res['local_30d'] = Cache::remember('admin:groups:stats:local_30d', 43200, function () {
|
||||
return Group::whereLocal(true)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
$res['remote_30d'] = Cache::remember('admin:groups:stats:remote_30d', 43200, function () {
|
||||
return Group::whereLocal(false)->where('created_at', '>', now()->subMonth())->count();
|
||||
});
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -531,6 +531,7 @@ trait AdminSettingsController
|
|||
'registration_status' => 'required|in:open,filtered,closed',
|
||||
'cloud_storage' => 'required',
|
||||
'activitypub_enabled' => 'required',
|
||||
'authorized_fetch' => 'required',
|
||||
'account_migration' => 'required',
|
||||
'mobile_apis' => 'required',
|
||||
'stories' => 'required',
|
||||
|
@ -555,6 +556,7 @@ trait AdminSettingsController
|
|||
}
|
||||
}
|
||||
}
|
||||
ConfigCacheService::put('federation.activitypub.authorized_fetch', $request->boolean('authorized_fetch'));
|
||||
ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
|
||||
ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
|
||||
ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
|
||||
|
|
38
app/Http/Controllers/Api/ApiController.php
Normal file
38
app/Http/Controllers/Api/ApiController.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class ApiController extends Controller {
|
||||
public function json($res, $headers = [], $code = 200) {
|
||||
return response()->json($res, $code, $this->filterHeaders($headers), JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function linksForCollection($paginator) {
|
||||
$link = null;
|
||||
|
||||
if ($paginator->onFirstPage()) {
|
||||
if ($paginator->hasMorePages()) {
|
||||
$link = '<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
} else {
|
||||
if ($paginator->previousPageUrl()) {
|
||||
$link = '<'.$paginator->previousPageUrl().'>; rel="next"';
|
||||
}
|
||||
|
||||
if ($paginator->hasMorePages()) {
|
||||
$link .= ($link ? ', ' : '').'<'.$paginator->nextPageUrl().'>; rel="prev"';
|
||||
}
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
private function filterHeaders($headers) {
|
||||
return array_filter($headers, function($v, $k) {
|
||||
return $v != null;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ use App\Services\SnowflakeService;
|
|||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use App\Transformer\Api\Mastodon\v1\AccountTransformer;
|
||||
|
@ -1806,12 +1807,16 @@ class ApiV1Controller extends Controller
|
|||
|
||||
$profile = $user->profile;
|
||||
|
||||
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;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
@ -1819,8 +1824,6 @@ class ApiV1Controller extends Controller
|
|||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
|
@ -1883,6 +1886,10 @@ class ApiV1Controller extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
@ -2023,12 +2030,16 @@ class ApiV1Controller extends Controller
|
|||
|
||||
$profile = $user->profile;
|
||||
|
||||
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;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
@ -2036,8 +2047,6 @@ class ApiV1Controller extends Controller
|
|||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
|
@ -2105,6 +2114,10 @@ class ApiV1Controller extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
|
|
@ -5,12 +5,17 @@ namespace App\Http\Controllers\Api;
|
|||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\StatusController;
|
||||
use App\Http\Resources\StatusStateless;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail;
|
||||
use App\Mail\ConfirmAppEmail;
|
||||
use App\Mail\PasswordChange;
|
||||
use App\Media;
|
||||
use App\Place;
|
||||
use App\Profile;
|
||||
use App\Report;
|
||||
|
@ -18,14 +23,18 @@ use App\Services\AccountService;
|
|||
use App\Services\BouncerService;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\ProfileStatusService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Status;
|
||||
use App\StatusArchived;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Cache;
|
||||
use DB;
|
||||
|
@ -37,6 +46,7 @@ use Jenssegers\Agent\Agent;
|
|||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use Mail;
|
||||
use NotificationChannels\Expo\ExpoPushToken;
|
||||
|
||||
class ApiV1Dot1Controller extends Controller
|
||||
{
|
||||
|
@ -1008,4 +1018,238 @@ class ApiV1Dot1Controller extends Controller
|
|||
|
||||
return $this->json($account, 200, $rateLimiting ? $limits : []);
|
||||
}
|
||||
|
||||
public function getExpoPushNotifications(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
|
||||
$user = $request->user();
|
||||
$res = [
|
||||
'expo_token' => (bool) $user->expo_token,
|
||||
'notify_like' => (bool) $user->notify_like,
|
||||
'notify_follow' => (bool) $user->notify_follow,
|
||||
'notify_mention' => (bool) $user->notify_mention,
|
||||
'notify_comment' => (bool) $user->notify_comment,
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
public function disableExpoPushNotifications(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
|
||||
$request->user()->update([
|
||||
'expo_token' => null,
|
||||
]);
|
||||
|
||||
return $this->json(['expo_token' => null]);
|
||||
}
|
||||
|
||||
public function updateExpoPushNotifications(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('push'), 403);
|
||||
abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.');
|
||||
$this->validate($request, [
|
||||
'expo_token' => ['required', ExpoPushToken::rule()],
|
||||
'notify_like' => 'sometimes',
|
||||
'notify_follow' => 'sometimes',
|
||||
'notify_mention' => 'sometimes',
|
||||
'notify_comment' => 'sometimes',
|
||||
]);
|
||||
|
||||
$user = $request->user()->update([
|
||||
'expo_token' => $request->input('expo_token'),
|
||||
'notify_like' => $request->has('notify_like') && $request->boolean('notify_like'),
|
||||
'notify_follow' => $request->has('notify_follow') && $request->boolean('notify_follow'),
|
||||
'notify_mention' => $request->has('notify_mention') && $request->boolean('notify_mention'),
|
||||
'notify_comment' => $request->has('notify_comment') && $request->boolean('notify_comment'),
|
||||
]);
|
||||
|
||||
$res = [
|
||||
'expo_token' => (bool) $request->user()->expo_token,
|
||||
'notify_like' => (bool) $request->user()->notify_like,
|
||||
'notify_follow' => (bool) $request->user()->notify_follow,
|
||||
'notify_mention' => (bool) $request->user()->notify_mention,
|
||||
'notify_comment' => (bool) $request->user()->notify_comment,
|
||||
];
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1.1/status/create
|
||||
*
|
||||
*
|
||||
* @return StatusTransformer
|
||||
*/
|
||||
public function statusCreate(Request $request)
|
||||
{
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'),
|
||||
'file' => [
|
||||
'required',
|
||||
'file',
|
||||
'mimetypes:'.config_cache('pixelfed.media_types'),
|
||||
'max:'.config_cache('pixelfed.max_photo_size'),
|
||||
function ($attribute, $value, $fail) {
|
||||
if (is_array($value) && count($value) > 1) {
|
||||
$fail('Only one file can be uploaded at a time.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'sensitive' => 'nullable',
|
||||
'visibility' => 'string|in:private,unlisted,public',
|
||||
'spoiler_text' => 'sometimes|max:140',
|
||||
]);
|
||||
|
||||
if ($request->hasHeader('idempotency-key')) {
|
||||
$key = 'pf:api:v1:status:idempotency-key:'.$request->user()->id.':'.hash('sha1', $request->header('idempotency-key'));
|
||||
$exists = Cache::has($key);
|
||||
abort_if($exists, 400, 'Duplicate idempotency key.');
|
||||
Cache::put($key, 1, 3600);
|
||||
}
|
||||
|
||||
if (config('costar.enabled') == true) {
|
||||
$blockedKeywords = config('costar.keyword.block');
|
||||
if ($blockedKeywords !== null && $request->status) {
|
||||
$keywords = config('costar.keyword.block');
|
||||
foreach ($keywords as $kw) {
|
||||
if (Str::contains($request->status, $kw) == true) {
|
||||
abort(400, 'Invalid object. Contains banned keyword.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->has_roles) {
|
||||
abort_if(! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
}
|
||||
|
||||
$profile = $user->profile;
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:'.$user->id;
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if ($settings && ! empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if (isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
$visibility = $profile->is_private ? 'private' : (
|
||||
$profile->unlisted == true &&
|
||||
$request->input('visibility', 'public') == 'public' ?
|
||||
'unlisted' :
|
||||
$request->input('visibility', 'public'));
|
||||
|
||||
if ($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = strip_tags($request->input('status'));
|
||||
$rendered = Autolink::create()->autolink($content);
|
||||
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
|
||||
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
|
||||
|
||||
$status = new Status;
|
||||
$status->caption = $content;
|
||||
$status->rendered = $rendered;
|
||||
$status->profile_id = $user->profile_id;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->cw_summary = $spoilerText;
|
||||
$status->scope = $visibility;
|
||||
$status->visibility = $visibility;
|
||||
$status->type = StatusController::mimeTypeCheck([$mime]);
|
||||
$status->save();
|
||||
|
||||
if (! $status) {
|
||||
abort(500, 'An error occured.');
|
||||
}
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->order = 1;
|
||||
$media->caption = $request->input('description');
|
||||
if ($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
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;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
|
||||
Cache::forget('profile:status_count:'.$user->profile_id);
|
||||
Cache::forget($user->storageUsedKey());
|
||||
Cache::forget('profile:embed:'.$status->profile_id);
|
||||
Cache::forget($limitKey);
|
||||
|
||||
$res = StatusService::getMastodon($status->id, false);
|
||||
$res['favourited'] = false;
|
||||
$res['language'] = 'en';
|
||||
$res['bookmarked'] = false;
|
||||
$res['card'] = null;
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,45 +2,32 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\VideoThumbnail;
|
||||
use App\Media;
|
||||
use App\UserSetting;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\SearchApiV2Service;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Transformer\Api\Mastodon\v1\MediaTransformer;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use App\Util\Media\Filter;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\Mastodon\v1\{
|
||||
AccountTransformer,
|
||||
MediaTransformer,
|
||||
NotificationTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use App\Transformer\Api\{
|
||||
RelationshipTransformer,
|
||||
};
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Services\UserRoleService;
|
||||
|
||||
class ApiV2Controller extends Controller
|
||||
{
|
||||
const PF_API_ENTITY_KEY = "_pe";
|
||||
const PF_API_ENTITY_KEY = '_pe';
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
|
@ -50,10 +37,11 @@ class ApiV2Controller extends Controller
|
|||
public function instance(Request $request)
|
||||
{
|
||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
if(config_cache('instance.admin.pid')) {
|
||||
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;
|
||||
|
@ -62,41 +50,42 @@ class ApiV2Controller extends Controller
|
|||
$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() : [];
|
||||
->map(function ($rule, $key) {
|
||||
$id = $key + 1;
|
||||
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule,
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use ($contact, $rules) {
|
||||
return [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
|
||||
'version' => '3.5.3 (compatible; Pixelfed '.config('pixelfed.version').')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly(),
|
||||
],
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
],
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => null,
|
||||
'status' => null
|
||||
'status' => null,
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
|
@ -107,7 +96,7 @@ class ApiV2Controller extends Controller
|
|||
'statuses' => [
|
||||
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
'characters_reserved_per_url' => 23,
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
|
@ -115,7 +104,7 @@ class ApiV2Controller extends Controller
|
|||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
'video_matrix_limit' => 3686400,
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 0,
|
||||
|
@ -135,14 +124,15 @@ class ApiV2Controller extends Controller
|
|||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
'account' => $contact,
|
||||
],
|
||||
'rules' => $rules
|
||||
'rules' => $rules,
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
@ -154,7 +144,7 @@ class ApiV2Controller extends Controller
|
|||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
|
@ -167,18 +157,19 @@ class ApiV2Controller extends Controller
|
|||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable'
|
||||
'following' => 'nullable',
|
||||
]);
|
||||
|
||||
if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
if ($request->user()->has_roles && ! UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
return [
|
||||
'accounts' => [],
|
||||
'hashtags' => [],
|
||||
'statuses' => []
|
||||
'statuses' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$mastodonMode = !$request->has('_pe');
|
||||
$mastodonMode = ! $request->has('_pe');
|
||||
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
|
||||
|
@ -194,7 +185,7 @@ class ApiV2Controller extends Controller
|
|||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster')
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster'),
|
||||
] : [];
|
||||
}
|
||||
|
||||
|
@ -206,39 +197,39 @@ class ApiV2Controller extends Controller
|
|||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_if(! $request->user() || ! $request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 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',
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes'
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if($user->last_active_at == null) {
|
||||
if ($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(empty($request->file('file'))) {
|
||||
if (empty($request->file('file'))) {
|
||||
return response('', 422);
|
||||
}
|
||||
|
||||
$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;
|
||||
|
@ -247,23 +238,25 @@ class ApiV2Controller extends Controller
|
|||
|
||||
$profile = $user->profile;
|
||||
|
||||
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;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
|
@ -275,24 +268,24 @@ class ApiV2Controller extends Controller
|
|||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if($settings && !empty($settings->compose_settings)) {
|
||||
if ($settings && ! empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
if (isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
if($request->has('replace_id')) {
|
||||
if ($request->has('replace_id')) {
|
||||
$rpid = $request->input('replace_id');
|
||||
$removeMedia = Media::whereNull('status_id')
|
||||
->whereUserId($user->id)
|
||||
->whereProfileId($profile->id)
|
||||
->where('created_at', '>', now()->subHours(2))
|
||||
->find($rpid);
|
||||
if($removeMedia) {
|
||||
if ($removeMedia) {
|
||||
MediaDeletePipeline::dispatch($removeMedia)
|
||||
->onQueue('mmo')
|
||||
->delay(now()->addMinutes(15));
|
||||
|
@ -310,7 +303,7 @@ class ApiV2Controller extends Controller
|
|||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if($license) {
|
||||
if ($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
@ -328,13 +321,18 @@ class ApiV2Controller extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url(). '?v=' . time();
|
||||
$res['preview_url'] = $media->url().'?v='.time();
|
||||
$res['url'] = null;
|
||||
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
}
|
||||
|
|
147
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
147
app/Http/Controllers/Api/V1/Admin/DomainBlocksController.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Http\Controllers\Api\ApiController;
|
||||
use App\Instance;
|
||||
use App\Services\InstanceService;
|
||||
use App\Http\Resources\MastoApi\Admin\DomainBlockResource;
|
||||
|
||||
class DomainBlocksController extends ApiController {
|
||||
|
||||
public function __construct() {
|
||||
$this->middleware(['auth:api', 'api.admin', 'scope:admin:read,admin:read:domain_blocks'])->only(['index', 'show']);
|
||||
$this->middleware(['auth:api', 'api.admin', 'scope:admin:write,admin:write:domain_blocks'])->only(['create', 'update', 'delete']);
|
||||
}
|
||||
|
||||
public function index(Request $request) {
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|max:100|min:1',
|
||||
]);
|
||||
|
||||
$limit = $request->input('limit', 100);
|
||||
|
||||
$res = Instance::moderated()
|
||||
->orderBy('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
return $this->json(DomainBlockResource::collection($res), [
|
||||
'Link' => $this->linksForCollection($res)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, $id) {
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function create(Request $request) {
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|string|min:1|max:120',
|
||||
'severity' => [
|
||||
'sometimes',
|
||||
Rule::in(['noop', 'silence', 'suspend'])
|
||||
],
|
||||
'reject_media' => 'sometimes|required|boolean',
|
||||
'reject_reports' => 'sometimes|required|boolean',
|
||||
'private_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'public_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'obfuscate' => 'sometimes|required|boolean'
|
||||
]);
|
||||
|
||||
$domain = $request->input('domain');
|
||||
$severity = $request->input('severity', 'silence');
|
||||
$private_comment = $request->input('private_comment');
|
||||
|
||||
abort_if(!strpos($domain, '.'), 400, 'Invalid domain');
|
||||
abort_if(!filter_var($domain, FILTER_VALIDATE_DOMAIN), 400, 'Invalid domain');
|
||||
|
||||
// This is because Pixelfed can't currently support wildcard domain blocks
|
||||
// We have to find something that could plausibly be an instance
|
||||
$parts = explode('.', $domain);
|
||||
if ($parts[0] == '*') {
|
||||
// If we only have two parts, e.g., "*", "example", then we want to fail:
|
||||
abort_if(count($parts) <= 2, 400, 'Invalid domain: This API does not support wildcard domain blocks yet');
|
||||
|
||||
// Otherwise we convert the *.foo.example to foo.example
|
||||
$domain = implode('.', array_slice($parts, 1));
|
||||
}
|
||||
|
||||
// Double check we definitely haven't let anything through:
|
||||
abort_if(str_contains($domain, '*'), 400, 'Invalid domain');
|
||||
|
||||
$existing_domain_block = Instance::moderated()->whereDomain($domain)->first();
|
||||
|
||||
if ($existing_domain_block) {
|
||||
return $this->json([
|
||||
'error' => 'A domain block already exists for this domain',
|
||||
'existing_domain_block' => new DomainBlockResource($existing_domain_block)
|
||||
], [], 422);
|
||||
}
|
||||
|
||||
$domain_block = Instance::updateOrCreate(
|
||||
[ 'domain' => $domain ],
|
||||
[ 'banned' => $severity === 'suspend', 'unlisted' => $severity === 'silence', 'notes' => [$private_comment]]
|
||||
);
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id) {
|
||||
$this->validate($request, [
|
||||
'severity' => [
|
||||
'sometimes',
|
||||
Rule::in(['noop', 'silence', 'suspend'])
|
||||
],
|
||||
'reject_media' => 'sometimes|required|boolean',
|
||||
'reject_reports' => 'sometimes|required|boolean',
|
||||
'private_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'public_comment' => 'sometimes|string|min:1|max:1000',
|
||||
'obfuscate' => 'sometimes|required|boolean'
|
||||
]);
|
||||
|
||||
$severity = $request->input('severity', 'silence');
|
||||
$private_comment = $request->input('private_comment');
|
||||
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
$domain_block->banned = $severity === 'suspend';
|
||||
$domain_block->unlisted = $severity === 'silence';
|
||||
$domain_block->notes = [$private_comment];
|
||||
$domain_block->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(new DomainBlockResource($domain_block));
|
||||
}
|
||||
|
||||
public function delete(Request $request, $id) {
|
||||
$domain_block = Instance::moderated()->find($id);
|
||||
|
||||
if (!$domain_block) {
|
||||
return $this->json([ 'error' => 'Record not found'], [], 404);
|
||||
}
|
||||
|
||||
$domain_block->banned = false;
|
||||
$domain_block->unlisted = false;
|
||||
$domain_block->save();
|
||||
|
||||
InstanceService::refresh();
|
||||
|
||||
return $this->json(null, [], 200);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ use App\Services\MediaStorageService;
|
|||
use App\Services\MediaTagService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\MediaTransformer;
|
||||
use App\UserFilter;
|
||||
|
@ -70,7 +71,7 @@ class ComposeController extends Controller
|
|||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$user = $request->user();
|
||||
$profile = $user->profile;
|
||||
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
|
||||
|
||||
|
@ -84,21 +85,22 @@ 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) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
|
||||
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
|
||||
|
@ -143,6 +145,10 @@ class ComposeController extends Controller
|
|||
break;
|
||||
}
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
@ -198,6 +204,7 @@ class ComposeController extends Controller
|
|||
];
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
Cache::forget($limitKey);
|
||||
UserStorageService::recalculateUpdateStorageUsed($request->user()->id);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
@ -218,6 +225,8 @@ class ComposeController extends Controller
|
|||
|
||||
MediaStorageService::delete($media, true);
|
||||
|
||||
UserStorageService::recalculateUpdateStorageUsed($request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'msg' => 'Successfully deleted',
|
||||
'code' => 200,
|
||||
|
@ -494,17 +503,17 @@ class ComposeController extends Controller
|
|||
|
||||
$limitKey = 'compose:rate-limit:store:'.$user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
|
||||
$dailyLimit = Status::whereProfileId($user->profile_id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->where('created_at', '>', now()->subDays(1))
|
||||
->count();
|
||||
// $limitReached = Cache::remember($limitKey, $limitTtl, function () use ($user) {
|
||||
// $dailyLimit = Status::whereProfileId($user->profile_id)
|
||||
// ->whereNull('in_reply_to_id')
|
||||
// ->whereNull('reblog_of_id')
|
||||
// ->where('created_at', '>', now()->subDays(1))
|
||||
// ->count();
|
||||
|
||||
return $dailyLimit >= 1000;
|
||||
});
|
||||
// return $dailyLimit >= 1000;
|
||||
// });
|
||||
|
||||
abort_if($limitReached == true, 429);
|
||||
// abort_if($limitReached == true, 429);
|
||||
|
||||
$license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
|
||||
|
||||
|
@ -626,7 +635,6 @@ class ComposeController extends Controller
|
|||
Cache::forget('_api:statuses:recent_9:'.$profile->id);
|
||||
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($limitKey);
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ use App\Services\MediaService;
|
|||
use App\Services\StatusService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Services\UserStorageService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Status;
|
||||
use App\UserFilter;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
@ -602,16 +602,19 @@ class DirectMessageController extends Controller
|
|||
$hidden = false;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
$accountSize = UserStorageService::get($user->id);
|
||||
abort_if($accountSize === -1, 403, 'Invalid request.');
|
||||
$photo = $request->file('file');
|
||||
$fileSize = $photo->getSize();
|
||||
$sizeInKbs = (int) ceil($fileSize / 1000);
|
||||
$updatedAccountSize = (int) $accountSize + (int) $sizeInKbs;
|
||||
|
||||
if ((bool) config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
if ($updatedAccountSize >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if (in_array($photo->getMimeType(), $mimes) == false) {
|
||||
|
@ -667,6 +670,10 @@ class DirectMessageController extends Controller
|
|||
]
|
||||
);
|
||||
|
||||
$user->storage_used = (int) $updatedAccountSize;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
if ($recipient->domain) {
|
||||
$this->remoteDeliver($dm);
|
||||
}
|
||||
|
|
671
app/Http/Controllers/GroupController.php
Normal file
671
app/Http/Controllers/GroupController.php
Normal file
|
@ -0,0 +1,671 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Instance;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupReport;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Storage;
|
||||
|
||||
class GroupController extends GroupFederationController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
abort_unless(config('groups.enabled'), 404);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function show(Request $request, $id, $path = false)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return $this->showGroupObject($group);
|
||||
}
|
||||
|
||||
return view('layouts.spa', compact('id', 'path'));
|
||||
}
|
||||
|
||||
public function showStatus(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::find($gid);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if ($group->is_private) {
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
}
|
||||
|
||||
$gp = GroupPost::whereGroupId($gid)
|
||||
->findOrFail($sid);
|
||||
|
||||
return view('layouts.spa', compact('group', 'gp'));
|
||||
}
|
||||
|
||||
public function getGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereNull('status')->findOrFail($id);
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return response()->json($group, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showStatusLikes(Request $request, $id, $sid)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = $user->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
$status = GroupPost::whereGroupId($id)->findOrFail($sid);
|
||||
$likes = GroupLike::whereStatusId($sid)
|
||||
->cursorPaginate(10)
|
||||
->map(function ($l) use ($group) {
|
||||
$account = AccountService::get($l->profile_id);
|
||||
$account['url'] = "/groups/{$group->id}/user/{$account['id']}";
|
||||
|
||||
return $account;
|
||||
})
|
||||
->filter(function ($l) {
|
||||
return $l && isset($l['id']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $likes;
|
||||
}
|
||||
|
||||
public function groupSettings(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return view('groups.settings', compact('group'));
|
||||
}
|
||||
|
||||
public function joinGroup(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if($group->isMember($pid), 404);
|
||||
|
||||
if (! $request->user()->is_admin) {
|
||||
abort_if(GroupService::getRejoinTimeout($group->id, $pid), 422, 'Cannot re-join this group for 24 hours after leaving or cancelling a request to join');
|
||||
}
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'member';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->join_request = $group->is_private;
|
||||
$member->save();
|
||||
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
$group = $this->toJson($group, $pid);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
public function updateGroup(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:all,local,private',
|
||||
'avatar' => 'nullable',
|
||||
'header' => 'nullable',
|
||||
'discoverable' => 'required',
|
||||
'activitypub' => 'required',
|
||||
'is_nsfw' => 'required',
|
||||
'category' => 'required|string|in:'.implode(',', GroupService::categories()),
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::whereProfileId($pid)->findOrFail($id);
|
||||
$member = GroupMember::whereGroupId($group->id)->whereProfileId($pid)->firstOrFail();
|
||||
|
||||
abort_if($member->role != 'founder', 403, 'Invalid group permission');
|
||||
|
||||
$metadata = $group->metadata;
|
||||
$len = $group->is_private ? 12 : 4;
|
||||
|
||||
if ($request->hasFile('avatar')) {
|
||||
$avatar = $request->file('avatar');
|
||||
|
||||
if ($avatar) {
|
||||
if (isset($metadata['avatar']) &&
|
||||
isset($metadata['avatar']['path']) &&
|
||||
Storage::exists($metadata['avatar']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['avatar']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'avatar_'.strtolower(str_random($len)).'.'.$avatar->extension();
|
||||
$path = $avatar->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['avatar'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->hasFile('header')) {
|
||||
$header = $request->file('header');
|
||||
|
||||
if ($header) {
|
||||
if (isset($metadata['header']) &&
|
||||
isset($metadata['header']['path']) &&
|
||||
Storage::exists($metadata['header']['path'])
|
||||
) {
|
||||
Storage::delete($metadata['header']['path']);
|
||||
}
|
||||
|
||||
$fileName = 'header_'.strtolower(str_random($len)).'.'.$header->extension();
|
||||
$path = $header->storePubliclyAs('public/g/'.$group->id.'/meta', $fileName);
|
||||
$url = url(Storage::url($path));
|
||||
$metadata['header'] = [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$cat = GroupService::categoryById($group->category_id);
|
||||
if ($request->category !== $cat['name']) {
|
||||
$group->category_id = GroupCategory::whereName($request->category)->first()->id;
|
||||
}
|
||||
|
||||
$changes = null;
|
||||
$group->description = e($request->input('description', null));
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->activitypub = $request->input('activitypub') == 'true';
|
||||
$group->discoverable = $request->input('discoverable') == 'true';
|
||||
$group->is_nsfw = $request->input('is_nsfw') == 'true';
|
||||
$group->metadata = $metadata;
|
||||
if ($group->isDirty()) {
|
||||
$changes = $group->getDirty();
|
||||
}
|
||||
$group->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:settings:updated',
|
||||
$changes
|
||||
);
|
||||
|
||||
GroupService::del($group->id);
|
||||
|
||||
$res = $this->toJson($group, $pid);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function groupLeave(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
|
||||
abort_if(! $group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function cancelJoinRequest(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
abort_if($pid == $group->profile_id, 422, 'Cannot leave a group you created');
|
||||
abort_if($group->isMember($pid), 422, 'Cannot cancel approved join request, please leave group instead.');
|
||||
|
||||
GroupMember::whereGroupId($group->id)->whereProfileId($pid)->delete();
|
||||
GroupService::del($group->id);
|
||||
GroupService::delSelf($group->id, $pid);
|
||||
GroupService::setRejoinTimeout($group->id, $pid);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function metaBlockSearch(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$type = $request->input('type');
|
||||
$item = $request->input('item');
|
||||
|
||||
switch ($type) {
|
||||
case 'instance':
|
||||
$res = Instance::whereDomain($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereInstanceId($res->id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$res = Profile::whereUsername($item)->first();
|
||||
if ($res) {
|
||||
abort_if(GroupBlock::whereGroupId($group->id)->whereProfileId($res->id)->exists(), 400);
|
||||
}
|
||||
if ($res->user_id != null) {
|
||||
abort_if(User::whereIsAdmin(true)->whereId($res->user_id)->exists(), 400);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json((bool) $res, ($res ? 200 : 404));
|
||||
}
|
||||
|
||||
public function reportCreate(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$type = $request->input('type');
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'violence',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism',
|
||||
];
|
||||
|
||||
$gp = GroupPost::whereGroupId($group->id)->find($id);
|
||||
abort_if(! $gp, 422, 'Cannot report an invalid or deleted post');
|
||||
abort_if(! in_array($type, $types), 422, 'Invalid report type');
|
||||
abort_if($gp->profile_id === $pid, 422, 'Cannot report your own post');
|
||||
abort_if(
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereProfileId($pid)
|
||||
->whereItemType(GroupPost::class)
|
||||
->whereItemId($id)
|
||||
->exists(),
|
||||
422,
|
||||
'You already reported this'
|
||||
);
|
||||
|
||||
$report = new GroupReport();
|
||||
$report->group_id = $group->id;
|
||||
$report->profile_id = $pid;
|
||||
$report->type = $type;
|
||||
$report->item_type = GroupPost::class;
|
||||
$report->item_id = $id;
|
||||
$report->open = true;
|
||||
$report->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:report:create',
|
||||
[
|
||||
'type' => $type,
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $gp->status_id,
|
||||
'profile_id' => $gp->profile_id,
|
||||
'username' => optional(AccountService::get($gp->profile_id))['acct'],
|
||||
'gpid' => $gp->id,
|
||||
'url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response([200]);
|
||||
}
|
||||
|
||||
public function reportAction(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:cw,delete,ignore',
|
||||
'id' => 'required|string',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$id = $request->input('id');
|
||||
|
||||
$report = GroupReport::whereGroupId($group->id)
|
||||
->findOrFail($id);
|
||||
$status = Status::findOrFail($report->item_id);
|
||||
$gp = GroupPost::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->firstOrFail();
|
||||
|
||||
switch ($action) {
|
||||
case 'cw':
|
||||
$status->is_nsfw = true;
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'cw',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
|
||||
case 'ignore':
|
||||
GroupReport::whereGroupId($group->id)
|
||||
->whereItemType($report->item_type)
|
||||
->whereItemId($report->item_id)
|
||||
->update(['open' => false]);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:moderation:action',
|
||||
[
|
||||
'type' => 'ignore',
|
||||
'report_id' => $report->id,
|
||||
'status_id' => $status->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_url' => $gp->url(),
|
||||
],
|
||||
GroupReport::class,
|
||||
$report->id
|
||||
);
|
||||
|
||||
return response()->json([200]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
abort_if(! $group->isMember($profile_id), 404);
|
||||
$limits = GroupService::getInteractionLimits($group->id, $profile_id);
|
||||
|
||||
return response()->json($limits);
|
||||
}
|
||||
|
||||
public function updateMemberInteractionLimits(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $group->isMember($pid), 404);
|
||||
abort_if(! in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|exists:profiles,id',
|
||||
'can_post' => 'required',
|
||||
'can_comment' => 'required',
|
||||
'can_like' => 'required',
|
||||
]);
|
||||
|
||||
$member = $request->input('profile_id');
|
||||
$can_post = $request->input('can_post');
|
||||
$can_comment = $request->input('can_comment');
|
||||
$can_like = $request->input('can_like');
|
||||
$account = AccountService::get($member);
|
||||
|
||||
abort_if(! $account, 422, 'Invalid profile');
|
||||
abort_if(! $group->isMember($member), 422, 'Invalid profile');
|
||||
|
||||
$limit = GroupLimit::firstOrCreate([
|
||||
'profile_id' => $member,
|
||||
'group_id' => $group->id,
|
||||
]);
|
||||
|
||||
if ($limit->wasRecentlyCreated) {
|
||||
abort_if(GroupLimit::whereGroupId($group->id)->count() >= 25, 422, 'limit_reached');
|
||||
}
|
||||
|
||||
$previousLimits = $limit->limits;
|
||||
|
||||
$limit->limits = [
|
||||
'can_post' => $can_post,
|
||||
'can_comment' => $can_comment,
|
||||
'can_like' => $can_like,
|
||||
];
|
||||
$limit->save();
|
||||
|
||||
GroupService::clearInteractionLimits($group->id, $member);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:member-limits:updated',
|
||||
[
|
||||
'profile_id' => $account['id'],
|
||||
'username' => $account['username'],
|
||||
'previousLimits' => $previousLimits,
|
||||
'newLimits' => $limit->limits,
|
||||
],
|
||||
GroupLimit::class,
|
||||
$limit->id
|
||||
);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function showProfile(Request $request, $id, $pid)
|
||||
{
|
||||
$group = Group::find($id);
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
return view('layouts.spa');
|
||||
}
|
||||
|
||||
public function showProfileByUsername(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
if (! $request->user()) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$group = Group::find($id);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
if (! $group || $group->status) {
|
||||
return response()->view('groups.unavailable')->setStatusCode(404);
|
||||
}
|
||||
|
||||
if (! $group->isMember($cid)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
$profile = Profile::whereUsername($pid)->first();
|
||||
|
||||
if (! $group->isMember($profile->id)) {
|
||||
return redirect("/{$pid}");
|
||||
}
|
||||
|
||||
if ($profile) {
|
||||
$url = url("/groups/{$id}/user/{$profile->id}");
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
abort(404, 'Invalid username');
|
||||
}
|
||||
|
||||
public function groupInviteLanding(Request $request, $id)
|
||||
{
|
||||
abort(404, 'Not yet implemented');
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
return view('groups.invite', compact('group'));
|
||||
}
|
||||
|
||||
public function groupShortLinkRedirect(Request $request, $hid)
|
||||
{
|
||||
$gid = HashidService::decode($hid);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
return redirect($group->url());
|
||||
}
|
||||
|
||||
public function groupInviteClaim(Request $request, $id)
|
||||
{
|
||||
$group = GroupService::get($id);
|
||||
abort_if(! $group || empty($group), 404);
|
||||
|
||||
return view('groups.invite-claim', compact('group'));
|
||||
}
|
||||
|
||||
public function groupMemberInviteCheck(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
$exists = GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists();
|
||||
|
||||
return response()->json([
|
||||
'gid' => $id,
|
||||
'can_join' => (bool) $exists,
|
||||
]);
|
||||
}
|
||||
|
||||
public function groupMemberInviteAccept(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
abort_if(! GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->exists(), 422);
|
||||
|
||||
$gm = new GroupMember;
|
||||
$gm->group_id = $id;
|
||||
$gm->profile_id = $pid;
|
||||
$gm->role = 'member';
|
||||
$gm->local_group = $group->local;
|
||||
$gm->local_profile = true;
|
||||
$gm->join_request = false;
|
||||
$gm->save();
|
||||
|
||||
GroupInvitation::whereGroupId($id)->whereToProfileId($pid)->delete();
|
||||
GroupService::del($id);
|
||||
GroupService::delSelf($id, $pid);
|
||||
|
||||
return ['next_url' => $group->url()];
|
||||
}
|
||||
|
||||
public function groupMemberInviteDecline(Request $request, $id)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if($group->isMember($pid), 422, 'Already a member');
|
||||
|
||||
return ['next_url' => '/'];
|
||||
}
|
||||
}
|
103
app/Http/Controllers/GroupFederationController.php
Normal file
103
app/Http/Controllers/GroupFederationController.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Status;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Services\MediaService;
|
||||
|
||||
class GroupFederationController extends Controller
|
||||
{
|
||||
public function getGroupObject(Request $request, $id)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
|
||||
$res = $this->showGroupObject($group);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showGroupObject($group)
|
||||
{
|
||||
return Cache::remember('ap:groups:object:' . $group->id, 3600, function() use($group) {
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $group->url(),
|
||||
'inbox' => $group->permalink('/inbox'),
|
||||
'name' => $group->name,
|
||||
'outbox' => $group->permalink('/outbox'),
|
||||
'summary' => $group->description,
|
||||
'type' => 'Group',
|
||||
'attributedTo' => [
|
||||
'type' => 'Person',
|
||||
'id' => $group->admin->permalink()
|
||||
],
|
||||
// 'endpoints' => [
|
||||
// 'sharedInbox' => config('app.url') . '/f/inbox'
|
||||
// ],
|
||||
'preferredUsername' => 'gid_' . $group->id,
|
||||
'publicKey' => [
|
||||
'id' => $group->permalink('#main-key'),
|
||||
'owner' => $group->permalink(),
|
||||
'publicKeyPem' => InstanceActor::first()->public_key,
|
||||
],
|
||||
'url' => $group->permalink()
|
||||
];
|
||||
|
||||
if($group->metadata && isset($group->metadata['avatar'])) {
|
||||
$res['icon'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['avatar']['url']
|
||||
];
|
||||
}
|
||||
|
||||
if($group->metadata && isset($group->metadata['header'])) {
|
||||
$res['image'] = [
|
||||
'type' => 'Image',
|
||||
'url' => $group->metadata['header']['url']
|
||||
];
|
||||
}
|
||||
ksort($res);
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public function getStatusObject(Request $request, $gid, $sid)
|
||||
{
|
||||
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid);
|
||||
$gp = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
$status = Status::findOrFail($gp->status_id);
|
||||
// permission check
|
||||
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $gp->url(),
|
||||
|
||||
'type' => 'Note',
|
||||
|
||||
'summary' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'inReplyTo' => null,
|
||||
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'url' => $gp->url(),
|
||||
'attributedTo' => $status->profile->permalink(),
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
$group->permalink('/followers'),
|
||||
],
|
||||
'cc' => [],
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'attachment' => MediaService::activitypub($status->id),
|
||||
'target' => [
|
||||
'type' => 'Collection',
|
||||
'id' => $group->permalink('/wall'),
|
||||
'attributedTo' => $group->permalink()
|
||||
]
|
||||
];
|
||||
// ksort($res);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
10
app/Http/Controllers/GroupPostController.php
Normal file
10
app/Http/Controllers/GroupPostController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GroupPostController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
83
app/Http/Controllers/Groups/CreateGroupsController.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
|
||||
class CreateGroupsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function checkCreatePermission(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$config = GroupService::config();
|
||||
if($request->user()->is_admin) {
|
||||
$allowed = true;
|
||||
} else {
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
$allowed = Group::whereProfileId($pid)->count() <= $max;
|
||||
}
|
||||
|
||||
return ['permission' => (bool) $allowed];
|
||||
}
|
||||
|
||||
public function storeGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'description' => 'nullable|max:500',
|
||||
'membership' => 'required|in:public,private,local'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$config = GroupService::config();
|
||||
abort_if($config['limits']['user']['create']['new'] == false && $request->user()->is_admin == false, 422, 'Invalid operation');
|
||||
$max = $config['limits']['user']['create']['max'];
|
||||
// abort_if(Group::whereProfileId($pid)->count() <= $max, 422, 'Group limit reached');
|
||||
|
||||
$group = new Group;
|
||||
$group->profile_id = $pid;
|
||||
$group->name = $request->input('name');
|
||||
$group->description = $request->input('description', null);
|
||||
$group->is_private = $request->input('membership') == 'private';
|
||||
$group->local_only = $request->input('membership') == 'local';
|
||||
$group->metadata = $request->input('configuration');
|
||||
$group->save();
|
||||
|
||||
GroupService::log($group->id, $pid, 'group:created');
|
||||
|
||||
$member = new GroupMember;
|
||||
$member->group_id = $group->id;
|
||||
$member->profile_id = $pid;
|
||||
$member->role = 'founder';
|
||||
$member->local_group = true;
|
||||
$member->local_profile = true;
|
||||
$member->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:joined',
|
||||
null,
|
||||
GroupMember::class,
|
||||
$member->id
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $group->id,
|
||||
'url' => $group->url()
|
||||
];
|
||||
}
|
||||
}
|
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
353
app/Http/Controllers/Groups/GroupsAdminController.php
Normal file
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupBlock;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupReport;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
|
||||
class GroupsAdminController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getAdminTabs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
abort_if($pid !== $group->profile_id, 404);
|
||||
|
||||
$reqs = GroupMember::whereGroupId($group->id)->whereJoinRequest(true)->count();
|
||||
$mods = GroupReport::whereGroupId($group->id)->whereOpen(true)->count();
|
||||
$tabs = [
|
||||
'moderation_count' => $mods > 99 ? '99+' : $mods,
|
||||
'request_count' => $reqs > 99 ? '99+' : $reqs
|
||||
];
|
||||
|
||||
return response()->json($tabs);
|
||||
}
|
||||
|
||||
public function getInteractionLogs(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$logs = GroupInteraction::whereGroupId($id)
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($log) use($group) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'profile' => GroupAccountService::get($group->id, $log->profile_id),
|
||||
'type' => $log->type,
|
||||
'metadata' => $log->metadata,
|
||||
'created_at' => $log->created_at->format('c')
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($logs, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->take(3)->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->take(3)->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->take(3)->pluck('name')
|
||||
];
|
||||
|
||||
return response()->json($blocks, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function exportBlocks(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$blocks = [
|
||||
'instances' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(false)->latest()->pluck('name'),
|
||||
'users' => GroupBlock::whereGroupId($group->id)->whereNotNull('profile_id')->whereIsUser(true)->latest()->pluck('name'),
|
||||
'moderated' => GroupBlock::whereGroupId($group->id)->whereNotNull('instance_id')->whereModerated(true)->latest()->pluck('name')
|
||||
];
|
||||
|
||||
$blocks['_created_at'] = now()->format('c');
|
||||
$blocks['_version'] = '1.0.0';
|
||||
ksort($blocks);
|
||||
|
||||
return response()->streamDownload(function() use($blocks) {
|
||||
echo json_encode($blocks, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
}
|
||||
|
||||
public function addBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->profile_id = $profile->id;
|
||||
$gb->name = $profile->username;
|
||||
$gb->is_user = true;
|
||||
$gb->moderated = false;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:block:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
$gb = new GroupBlock;
|
||||
$gb->group_id = $group->id;
|
||||
$gb->admin_id = $pid;
|
||||
$gb->instance_id = $instance->id;
|
||||
$gb->name = $instance->domain;
|
||||
$gb->is_user = false;
|
||||
$gb->moderated = true;
|
||||
$gb->save();
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function undoBlock(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required',
|
||||
'type' => 'required|in:instance,user,moderate'
|
||||
]);
|
||||
|
||||
$item = $request->input('item');
|
||||
$type = $request->input('type');
|
||||
|
||||
switch($type) {
|
||||
case 'instance':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(false)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
$profile = Profile::whereUsername($item)->first();
|
||||
abort_if(!$profile, 422, 'This user either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereProfileId($profile->id)
|
||||
->whereIsUser(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:unblock:user',
|
||||
[
|
||||
'username' => $profile->username,
|
||||
'domain' => $profile->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
case 'moderate':
|
||||
$instance = Instance::whereDomain($item)->first();
|
||||
abort_if(!$instance, 422, 'This domain either isn\'nt known or is invalid');
|
||||
|
||||
$gb = GroupBlock::whereGroupId($group->id)
|
||||
->whereInstanceId($instance->id)
|
||||
->whereModerated(true)
|
||||
->first();
|
||||
|
||||
abort_if(!$gb, 422, 'Invalid group block');
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:admin:moderate:instance',
|
||||
[
|
||||
'domain' => $instance->domain
|
||||
],
|
||||
GroupBlock::class,
|
||||
$gb->id
|
||||
);
|
||||
|
||||
$gb->delete();
|
||||
|
||||
return [200];
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([], 422, []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getReportList(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$scope = $request->input('scope', 'open');
|
||||
|
||||
$list = GroupReport::selectRaw('id, profile_id, item_type, item_id, type, created_at, count(*) as total')
|
||||
->whereGroupId($group->id)
|
||||
->groupBy('item_id')
|
||||
->when($scope == 'open', function($query, $scope) {
|
||||
return $query->whereOpen(true);
|
||||
})
|
||||
->latest()
|
||||
->simplePaginate(10)
|
||||
->map(function($report) use($group) {
|
||||
$res = [
|
||||
'id' => (string) $report->id,
|
||||
'profile' => GroupAccountService::get($group->id, $report->profile_id),
|
||||
'type' => $report->type,
|
||||
'created_at' => $report->created_at->format('c'),
|
||||
'total_count' => $report->total
|
||||
];
|
||||
|
||||
if($report->item_type === GroupPost::class) {
|
||||
$res['status'] = GroupPostService::get($group->id, $report->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
return response()->json($list, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
}
|
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
84
app/Http/Controllers/Groups/GroupsApiController.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
|
||||
class GroupsApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
protected function toJson($group, $pid = false)
|
||||
{
|
||||
return GroupService::get($group->id, $pid);
|
||||
}
|
||||
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
return GroupService::config();
|
||||
}
|
||||
|
||||
public function getGroupAccount(Request $request, $gid, $pid)
|
||||
{
|
||||
$res = GroupAccountService::get($gid, $pid);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function getGroupCategories(Request $request)
|
||||
{
|
||||
$res = GroupService::categories();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupsByCategory(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$category = GroupCategory::whereName($name)->firstOrFail();
|
||||
$groups = Group::whereCategoryId($category->id)
|
||||
->simplePaginate(6)
|
||||
->map(function($group) {
|
||||
return GroupService::get($group->id);
|
||||
})
|
||||
->filter(function($group) {
|
||||
return $group;
|
||||
})
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getRecommendedGroups(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getSelfGroups(Request $request)
|
||||
{
|
||||
$selfOnly = $request->input('self') == true;
|
||||
$memberOnly = $request->input('member') == true;
|
||||
$pid = $request->user()->profile_id;
|
||||
$res = GroupMember::whereProfileId($request->user()->profile_id)
|
||||
->when($selfOnly, function($q, $selfOnly) {
|
||||
return $q->whereRole('founder');
|
||||
})
|
||||
->when($memberOnly, function($q, $memberOnly) {
|
||||
return $q->whereRole('member');
|
||||
})
|
||||
->simplePaginate(4)
|
||||
->map(function($member) use($pid) {
|
||||
$group = $member->group;
|
||||
return $this->toJson($group, $pid);
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
361
app/Http/Controllers/Groups/GroupsCommentController.php
Normal file
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupCommentService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupComment;
|
||||
use Purify;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewCommentPipeline;
|
||||
use App\Jobs\GroupsPipeline\DeleteCommentPipeline;
|
||||
|
||||
class GroupsCommentController extends Controller
|
||||
{
|
||||
public function getComments(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required',
|
||||
'cid' => 'sometimes',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
$pid = optional($request->user())->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->has('cid') && $request->input('cid') == 1;
|
||||
$limit = $request->input('limit', 3);
|
||||
$maxId = $request->input('max_id', 0);
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if($group->is_private && !$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$status = $cid ? GroupComment::findOrFail($sid) : GroupPost::findOrFail($sid);
|
||||
|
||||
abort_if($status->group_id != $group->id, 400, 'Invalid group');
|
||||
|
||||
$replies = GroupComment::whereGroupId($group->id)
|
||||
->whereStatusId($status->id)
|
||||
->orderByDesc('id')
|
||||
->when($maxId, function($query, $maxId) {
|
||||
return $query->where('id', '<', $maxId);
|
||||
})
|
||||
->take($limit)
|
||||
->get()
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupCommentService::get($gp['group_id'], $gp['id']);
|
||||
$status['reply_count'] = $gp['reply_count'];
|
||||
$status['url'] = $gp->url();
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$gp['profile_id']}");
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $replies->toArray();
|
||||
}
|
||||
|
||||
public function storeComment(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'cid' => 'sometimes',
|
||||
'content' => 'required|string|min:1|max:1500'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$cid = $request->input('cid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = e($request->input('content'));
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
|
||||
|
||||
$parent = $cid == 1 ?
|
||||
GroupComment::findOrFail($sid) :
|
||||
GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
// $autolink = Purify::clean(Autolink::create()->autolink($caption));
|
||||
// $autolink = str_replace('/discover/tags/', '/groups/' . $gid . '/topics/', $autolink);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->status_id = $parent->id;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'public';
|
||||
$status->is_nsfw = false;
|
||||
$status->local = true;
|
||||
$status->save();
|
||||
|
||||
NewCommentPipeline::dispatch($parent, $status)->onQueue('groups');
|
||||
// todo: perform in job
|
||||
$parent->reply_count = $parent->reply_count ? $parent->reply_count + $parent->reply_count : 1;
|
||||
$parent->save();
|
||||
GroupPostService::del($parent->group_id, $parent->id);
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:comment:created',
|
||||
[
|
||||
'type' => 'group:post:comment',
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$status->id
|
||||
);
|
||||
|
||||
//GroupCommentPipeline::dispatch($parent, $status, $gp);
|
||||
//NewStatusPipeline::dispatch($status, $gp);
|
||||
//GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
$s['pf_type'] = 'text';
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $status->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function storeCommentPhoto(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required|exists:groups,id',
|
||||
'sid' => 'required|exists:group_posts,id',
|
||||
'photo' => 'required|image'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$caption = $request->input('content');
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
abort_if(!GroupService::canComment($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
$parent = GroupPost::whereGroupId($gid)->findOrFail($sid);
|
||||
|
||||
$status = new GroupComment;
|
||||
$status->status_id = $parent->id;
|
||||
$status->group_id = $group->id;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = Purify::clean($caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->is_nsfw = false;
|
||||
$status->save();
|
||||
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $parent->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
|
||||
// $gp = new GroupPost;
|
||||
// $gp->group_id = $group->id;
|
||||
// $gp->profile_id = $pid;
|
||||
// $gp->type = 'reply:photo';
|
||||
// $gp->status_id = $status->id;
|
||||
// $gp->in_reply_to_id = $parent->id;
|
||||
// $gp->save();
|
||||
|
||||
// GroupService::log(
|
||||
// $group->id,
|
||||
// $pid,
|
||||
// 'group:comment:created',
|
||||
// [
|
||||
// 'type' => $gp->type,
|
||||
// 'status_id' => $status->id
|
||||
// ],
|
||||
// GroupPost::class,
|
||||
// $gp->id
|
||||
// );
|
||||
|
||||
// todo: perform in job
|
||||
// $parent->reply_count = Status::whereInReplyToId($parent->id)->count();
|
||||
// $parent->save();
|
||||
// StatusService::del($parent->id);
|
||||
// GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
|
||||
// todo: perform in job
|
||||
$s = GroupCommentService::get($status->group_id, $status->id);
|
||||
|
||||
// $s['pf_type'] = 'text';
|
||||
// $s['visibility'] = 'public';
|
||||
// $s['url'] = $gp->url();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deleteComment(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupComment::whereGroupId($group->id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
|
||||
$parent = GroupPost::find($gp->status_id);
|
||||
abort_if(!$parent, 422, 'Invalid parent');
|
||||
|
||||
DeleteCommentPipeline::dispatch($parent, $gp)->onQueue('groups');
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $gp->id,
|
||||
],
|
||||
GroupComment::class,
|
||||
$gp->id
|
||||
);
|
||||
$gp->delete();
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect('/groups/feed');
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group || $gid != $group['id'], 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupCommentService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'comment_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupComment::find($sid);
|
||||
abort_if(!$parent || $parent->group_id != $gid, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupCommentService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
57
app/Http/Controllers/Groups/GroupsDiscoverController.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsDiscoverController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getDiscoverPopular(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::orderByDesc('member_count')
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getDiscoverNew(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$groups = Group::latest()
|
||||
->take(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return GroupService::get($id);
|
||||
})
|
||||
->filter(function($id) {
|
||||
return $id;
|
||||
})
|
||||
->take(6)
|
||||
->values();
|
||||
return $groups;
|
||||
}
|
||||
}
|
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
188
app/Http/Controllers/Groups/GroupsFeedController.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\RelationshipService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInvitation;
|
||||
|
||||
class GroupsFeedController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getSelfFeed(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$limit = $request->input('limit', 5);
|
||||
$page = $request->input('page');
|
||||
$initial = $request->has('initial');
|
||||
|
||||
if($initial) {
|
||||
$res = Cache::remember('groups:self:feed:' . $pid, 900, function() use($pid) {
|
||||
return $this->getSelfFeedV0($pid, 5, null);
|
||||
});
|
||||
} else {
|
||||
abort_if($page && $page > 5, 422);
|
||||
$res = $this->getSelfFeedV0($pid, $limit, $page);
|
||||
}
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
protected function getSelfFeedV0($pid, $limit, $page)
|
||||
{
|
||||
return GroupPost::join('group_members', 'group_posts.group_id', 'group_members.group_id')
|
||||
->select('group_posts.*', 'group_members.group_id', 'group_members.profile_id')
|
||||
->where('group_members.profile_id', $pid)
|
||||
->whereIn('group_posts.type', ['text', 'photo', 'video'])
|
||||
->orderByDesc('group_posts.id')
|
||||
->limit($limit)
|
||||
// ->pluck('group_posts.status_id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = url("/groups/{$gp['group_id']}/p/{$gp['id']}");
|
||||
$status['group'] = GroupService::get($gp['group_id']);
|
||||
$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
return $status;
|
||||
});
|
||||
}
|
||||
|
||||
public function getGroupProfileFeed(Request $request, $id, $pid)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
$group = Group::findOrFail($id);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$feed = GroupPost::whereGroupId($id)
|
||||
->whereProfileId($pid)
|
||||
->latest()
|
||||
->paginate(3)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
$status['account']['url'] = "/groups/{$gp['group_id']}/user/{$status['account']['id']}";
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
public function getGroupFeed(Request $request, $id)
|
||||
{
|
||||
$group = Group::findOrFail($id);
|
||||
$user = $request->user();
|
||||
$pid = optional($user)->profile_id ?? false;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$max = $request->input('max_id');
|
||||
$limit = $request->limit ?? 3;
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
|
||||
// $posts = GroupPost::whereGroupId($group->id)
|
||||
// ->when($maxId, function($q, $maxId) {
|
||||
// return $q->where('status_id', '<', $maxId);
|
||||
// })
|
||||
// ->whereNull('in_reply_to_id')
|
||||
// ->orderByDesc('status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
// $status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
|
||||
// return $status;
|
||||
// })->filter(function($status) {
|
||||
// return $status;
|
||||
// });
|
||||
// return $posts;
|
||||
|
||||
Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() use($id) {
|
||||
if(GroupFeedService::count($id) == 0) {
|
||||
GroupFeedService::warmCache($id, true, 400);
|
||||
}
|
||||
});
|
||||
|
||||
if ($max) {
|
||||
$feed = GroupFeedService::getRankedMaxId($id, $max, $limit);
|
||||
} else {
|
||||
$feed = GroupFeedService::get($id, 0, $limit);
|
||||
}
|
||||
|
||||
$res = collect($feed)
|
||||
->map(function($k) use($user, $id) {
|
||||
$status = GroupPostService::get($id, $k);
|
||||
if($status && $user) {
|
||||
$pid = $user->profile_id;
|
||||
$sid = $status['account']['id'];
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $status['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($status['id']);
|
||||
$status['relationship'] = $pid == $sid ? [] : RelationshipService::get($pid, $sid);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) use($filtered) {
|
||||
return $s && in_array($s['account']['id'], $filtered) == false;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
214
app/Http/Controllers/Groups/GroupsMemberController.php
Normal file
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupMember;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\Groups\GroupAccountService;
|
||||
use App\Services\Groups\GroupHashtagService;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinApprovedPipeline;
|
||||
use App\Jobs\GroupsPipeline\MemberJoinRejectedPipeline;
|
||||
|
||||
class GroupsMemberController extends Controller
|
||||
{
|
||||
public function getGroupMembers(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'limit' => 'nullable|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$members = GroupMember::whereGroupId($gid)
|
||||
->whereJoinRequest(false)
|
||||
->simplePaginate(10)
|
||||
->map(function($member) use($pid) {
|
||||
$account = AccountService::get($member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$member->group_id}/user/{$member['profile_id']}");
|
||||
return $account;
|
||||
});
|
||||
|
||||
return response()->json($members->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberJoinRequests(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
return GroupMember::whereGroupId($group->id)
|
||||
->whereJoinRequest(true)
|
||||
->whereNull('rejected_at')
|
||||
->paginate(10)
|
||||
->map(function($member) {
|
||||
return AccountService::get($member->profile_id);
|
||||
});
|
||||
}
|
||||
|
||||
public function handleGroupMemberJoinRequest(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
$mid = $request->input('pid');
|
||||
abort_if($group->isMember($mid), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required',
|
||||
'action' => 'required|in:approve,reject'
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
|
||||
$member = GroupMember::whereGroupId($group->id)
|
||||
->whereProfileId($mid)
|
||||
->firstOrFail();
|
||||
|
||||
if($action == 'approve') {
|
||||
MemberJoinApprovedPipeline::dispatch($member)->onQueue('groups');
|
||||
} else if ($action == 'reject') {
|
||||
MemberJoinRejectedPipeline::dispatch($member)->onQueue('groups');
|
||||
}
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function getGroupMember(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'pid' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$member_id = $request->input('pid');
|
||||
$member = GroupMember::whereGroupId($gid)
|
||||
->whereProfileId($member_id)
|
||||
->firstOrFail();
|
||||
|
||||
$account = GroupAccountService::get($group->id, $member['profile_id']);
|
||||
$account['role'] = $member['role'];
|
||||
$account['joined'] = $member['created_at'];
|
||||
$account['following'] = $pid != $member['profile_id'] ?
|
||||
FollowerService::follows($pid, $member['profile_id']) :
|
||||
null;
|
||||
$account['url'] = url("/groups/{$gid}/user/{$member_id}");
|
||||
|
||||
return response()->json($account, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getGroupMemberCommonIntersections(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$cid = $request->user()->profile_id;
|
||||
|
||||
// $this->validate($request, [
|
||||
// 'gid' => 'required',
|
||||
// 'pid' => 'required'
|
||||
// ]);
|
||||
|
||||
$gid = $request->input('gid');
|
||||
$pid = $request->input('pid');
|
||||
|
||||
if($pid === $cid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($cid), 404);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$self = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($cid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
$user = GroupPostHashtag::selectRaw('group_post_hashtags.*, count(*) as countr')
|
||||
->whereProfileId($pid)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('countr')
|
||||
->take(20)
|
||||
->pluck('hashtag_id');
|
||||
|
||||
$topics = $self->intersect($user)
|
||||
->values()
|
||||
->shuffle()
|
||||
->take(3)
|
||||
->map(function($id) use($group) {
|
||||
$tag = GroupHashtagService::get($id);
|
||||
$tag['url'] = url("/groups/{$group->id}/topics/{$tag['slug']}?src=upt");
|
||||
return $tag;
|
||||
});
|
||||
|
||||
// $friends = DB::table('followers as u')
|
||||
// ->join('followers as s', 'u.following_id', '=', 's.following_id')
|
||||
// ->where('s.profile_id', $cid)
|
||||
// ->where('u.profile_id', $pid)
|
||||
// ->inRandomOrder()
|
||||
// ->take(10)
|
||||
// ->pluck('s.following_id')
|
||||
// ->map(function($id) use($gid) {
|
||||
// $res = AccountService::get($id);
|
||||
// $res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
// return $res;
|
||||
// });
|
||||
$mutualGroups = GroupService::mutualGroups($cid, $pid, [$gid]);
|
||||
|
||||
$mutualFriends = collect(FollowerService::mutualIds($cid, $pid))
|
||||
->map(function($id) use($gid) {
|
||||
$res = AccountService::get($id);
|
||||
if(GroupService::isMember($gid, $id)) {
|
||||
$res['url'] = url("/groups/{$gid}/user/{$id}");
|
||||
} else if(!$res['local']) {
|
||||
$res['url'] = url("/i/web/profile/_/{$id}");
|
||||
}
|
||||
return $res;
|
||||
});
|
||||
$mutualFriendsCount = FollowerService::mutualCount($cid, $pid);
|
||||
|
||||
$res = [
|
||||
'groups_count' => $mutualGroups['count'],
|
||||
'groups' => $mutualGroups['groups'],
|
||||
'topics' => $topics,
|
||||
'friends_count' => $mutualFriendsCount,
|
||||
'friends' => $mutualFriends,
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
31
app/Http/Controllers/Groups/GroupsMetaController.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
|
||||
class GroupsMetaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function deleteGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$id = $request->input('gid');
|
||||
$group = Group::findOrFail($id);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(!in_array($group->selfRole($pid), ['founder', 'admin']), 404);
|
||||
|
||||
$group->status = "delete";
|
||||
$group->save();
|
||||
GroupService::del($group->id);
|
||||
return [200];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\GroupService;
|
||||
use App\Models\Group;
|
||||
use App\Notification;
|
||||
|
||||
class GroupsNotificationsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function selfGlobalNotifications(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$res = Notification::whereProfileId($pid)
|
||||
->where('action', 'like', 'group%')
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->map(function($n) {
|
||||
$res = [
|
||||
'id' => $n->id,
|
||||
'type' => $n->action,
|
||||
'account' => AccountService::get($n->actor_id),
|
||||
'object' => [
|
||||
'id' => $n->item_id,
|
||||
'type' => last(explode('\\', $n->item_type)),
|
||||
],
|
||||
'created_at' => $n->created_at->format('c')
|
||||
];
|
||||
|
||||
if($res['object']['type'] == 'Status' || in_array($n->action, ['group:comment'])) {
|
||||
$res['status'] = StatusService::get($n->item_id, false);
|
||||
$res['group'] = GroupService::get($res['status']['gid']);
|
||||
}
|
||||
|
||||
if($res['object']['type'] == 'Group') {
|
||||
$res['group'] = GroupService::get($n->item_id);
|
||||
}
|
||||
|
||||
return $res;
|
||||
});
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
420
app/Http/Controllers/Groups/GroupsPostController.php
Normal file
|
@ -0,0 +1,420 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupFeedService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupLike;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Jobs\GroupsPipeline\ImageResizePipeline;
|
||||
use App\Jobs\GroupsPipeline\ImageS3UploadPipeline;
|
||||
use App\Jobs\GroupsPipeline\NewPostPipeline;
|
||||
|
||||
class GroupsPostController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function storePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'group_id' => 'required|exists:groups,id',
|
||||
'caption' => 'sometimes|string|max:'.config_cache('pixelfed.max_caption_length', 500),
|
||||
'pollOptions' => 'sometimes|array|min:1|max:4'
|
||||
]);
|
||||
|
||||
$group = Group::findOrFail($request->input('group_id'));
|
||||
$pid = $request->user()->profile_id;
|
||||
$caption = $request->input('caption');
|
||||
$type = $request->input('type', 'text');
|
||||
|
||||
abort_if(!GroupService::canPost($group->id, $pid), 422, 'You cannot create new posts at this time');
|
||||
|
||||
if($type == 'text') {
|
||||
abort_if(strlen(e($caption)) == 0, 403);
|
||||
}
|
||||
|
||||
$gp = new GroupPost;
|
||||
$gp->group_id = $group->id;
|
||||
$gp->profile_id = $pid;
|
||||
$gp->caption = e($caption);
|
||||
$gp->type = $type;
|
||||
$gp->visibility = 'draft';
|
||||
$gp->save();
|
||||
|
||||
$status = $gp;
|
||||
|
||||
NewPostPipeline::dispatchSync($gp);
|
||||
|
||||
// NewStatusPipeline::dispatch($status, $gp);
|
||||
|
||||
if($type == 'poll') {
|
||||
// Polls not supported yet
|
||||
// $poll = new Poll;
|
||||
// $poll->status_id = $status->id;
|
||||
// $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) {
|
||||
// return 0;
|
||||
// })->toArray();
|
||||
// $poll->save();
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'photo') {
|
||||
$photo = $request->file('photo');
|
||||
$storagePath = GroupMediaService::path($group->id, $pid, $status->id);
|
||||
// $storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
// $hash = \hash_file('sha256', $photo);
|
||||
|
||||
$media = new GroupMedia();
|
||||
$media->group_id = $group->id;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $photo->getMimeType();
|
||||
$media->save();
|
||||
|
||||
// Bus::chain([
|
||||
// new ImageResizePipeline($media),
|
||||
// new ImageS3UploadPipeline($media),
|
||||
// ])->dispatch($media);
|
||||
|
||||
ImageResizePipeline::dispatchSync($media);
|
||||
ImageS3UploadPipeline::dispatchSync($media);
|
||||
// ImageOptimize::dispatch($media);
|
||||
// delay response while background job optimizes media
|
||||
// sleep(5);
|
||||
}
|
||||
if($type == 'video') {
|
||||
$video = $request->file('video');
|
||||
$storagePath = 'public/g/' . $group->id . '/p/' . $status->id;
|
||||
$path = $video->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $video);
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $request->user()->profile_id;
|
||||
$media->user_id = $request->user()->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $video->getSize();
|
||||
$media->mime = $video->getMimeType();
|
||||
$media->save();
|
||||
|
||||
VideoThumbnail::dispatch($media);
|
||||
sleep(15);
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$group->id,
|
||||
$pid,
|
||||
'group:status:created',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$s = GroupPostService::get($status->group_id, $status->id);
|
||||
GroupFeedService::add($group->id, $gp->id);
|
||||
Cache::forget('groups:self:feed:' . $pid);
|
||||
|
||||
$s['pf_type'] = $type;
|
||||
$s['visibility'] = 'public';
|
||||
$s['url'] = $gp->url();
|
||||
|
||||
if($type == 'poll') {
|
||||
$s['poll'] = PollService::get($status->id);
|
||||
}
|
||||
|
||||
$group->last_active_at = now();
|
||||
$group->save();
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function deletePost(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1',
|
||||
'gid' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$gp = GroupPost::whereGroupId($status->group_id)->findOrFail($request->input('id'));
|
||||
abort_if($gp->profile_id != $pid && $group->profile_id != $pid, 403);
|
||||
$cached = GroupPostService::get($status->group_id, $status->id);
|
||||
|
||||
if($cached) {
|
||||
$cached = collect($cached)->filter(function($r, $k) {
|
||||
return in_array($k, [
|
||||
'id',
|
||||
'sensitive',
|
||||
'pf_type',
|
||||
'media_attachments',
|
||||
'content_text',
|
||||
'created_at'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
GroupService::log(
|
||||
$status->group_id,
|
||||
$request->user()->profile_id,
|
||||
'group:status:deleted',
|
||||
[
|
||||
'type' => $gp->type,
|
||||
'status_id' => $status->id,
|
||||
'original' => $cached
|
||||
],
|
||||
GroupPost::class,
|
||||
$gp->id
|
||||
);
|
||||
|
||||
$user = $request->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 = GroupPost::find($status->in_reply_to_id);
|
||||
if($parent) {
|
||||
$parent->reply_count = GroupPost::whereInReplyToId($parent->id)->count();
|
||||
$parent->save();
|
||||
GroupPostService::del($group->id, GroupService::sidToGid($group->id, $parent->id));
|
||||
}
|
||||
}
|
||||
|
||||
GroupPostService::del($group->id, $gp->id);
|
||||
GroupFeedService::del($group->id, $gp->id);
|
||||
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
|
||||
// Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect($user->url());
|
||||
}
|
||||
}
|
||||
|
||||
public function likePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::firstOrCreate([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
]);
|
||||
|
||||
if($like->wasRecentlyCreated) {
|
||||
// update parent post like count
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count + 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::add($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count++;
|
||||
GroupService::log(
|
||||
$gid,
|
||||
$pid,
|
||||
'group:like',
|
||||
null,
|
||||
GroupLike::class,
|
||||
$like->id
|
||||
);
|
||||
}
|
||||
// if (GroupLike::whereGroupId($gid)->whereStatusId($sid)->whereProfileId($pid)->exists()) {
|
||||
// $like = GroupLike::whereProfileId($pid)->whereStatusId($sid)->firstOrFail();
|
||||
// // UnlikePipeline::dispatch($like);
|
||||
// $count = $gp->likes_count - 1;
|
||||
// $action = 'group:unlike';
|
||||
// } else {
|
||||
// $count = $gp->likes_count;
|
||||
// $like = GroupLike::firstOrCreate([
|
||||
// 'group_id' => $gid,
|
||||
// 'profile_id' => $pid,
|
||||
// 'status_id' => $sid
|
||||
// ]);
|
||||
// if($like->wasRecentlyCreated == true) {
|
||||
// $count++;
|
||||
// $gp->likes_count = $count;
|
||||
// $like->save();
|
||||
// $gp->save();
|
||||
// // LikePipeline::dispatch($like);
|
||||
// $action = 'group:like';
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Cache::forget('status:'.$status->id.':likedby:userid:'.$request->user()->id);
|
||||
// StatusService::del($status->id);
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function unlikePost(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'sid' => 'required'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$group = GroupService::get($gid);
|
||||
abort_if(!$group, 422, 'Invalid group');
|
||||
abort_if(!GroupService::canLike($gid, $pid), 422, 'You cannot interact with this content at this time');
|
||||
abort_if(!GroupService::isMember($gid, $pid), 403, 'Not a member of group');
|
||||
$gp = GroupPostService::get($gid, $sid);
|
||||
abort_if(!$gp, 422, 'Invalid status');
|
||||
$count = $gp['favourites_count'] ?? 0;
|
||||
|
||||
$like = GroupLike::where([
|
||||
'group_id' => $gid,
|
||||
'profile_id' => $pid,
|
||||
'status_id' => $sid,
|
||||
])->first();
|
||||
|
||||
if($like) {
|
||||
$like->delete();
|
||||
$parent = GroupPost::whereGroupId($gid)->find($sid);
|
||||
abort_if(!$parent, 422, 'Invalid status');
|
||||
$parent->likes_count = $parent->likes_count - 1;
|
||||
$parent->save();
|
||||
GroupsLikeService::remove($pid, $sid);
|
||||
// invalidate cache
|
||||
GroupPostService::del($gid, $sid);
|
||||
$count--;
|
||||
}
|
||||
|
||||
$response = ['code' => 200, 'msg' => 'Unliked post', 'count' => $count];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getGroupMedia(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'type' => 'required|in:photo,video'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$type = $request->input('type');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$media = GroupPost::whereGroupId($gid)
|
||||
->whereType($type)
|
||||
->latest()
|
||||
->simplePaginate(20)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
return $status;
|
||||
})->filter(function($status) {
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($media->toArray(), 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
221
app/Http/Controllers/Groups/GroupsSearchController.php
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\Groups\GroupActivityPubService;
|
||||
|
||||
class GroupsSearchController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function inviteFriendsToGroup(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'uids' => 'required',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$uid = $request->input('uids');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
abort_if(
|
||||
GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->count() >= 20,
|
||||
422,
|
||||
'Invite limit reached'
|
||||
);
|
||||
|
||||
$profiles = collect($uid)
|
||||
->map(function($u) {
|
||||
return Profile::find($u);
|
||||
})
|
||||
->filter(function($u) use($pid) {
|
||||
return $u &&
|
||||
$u->id != $pid &&
|
||||
isset($u->id) &&
|
||||
Follower::whereFollowingId($pid)
|
||||
->whereProfileId($u->id)
|
||||
->exists();
|
||||
})
|
||||
->filter(function($u) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($u->id)
|
||||
->exists() == false;
|
||||
})
|
||||
->each(function($u) use($gid, $pid) {
|
||||
$gi = new GroupInvitation;
|
||||
$gi->group_id = $gid;
|
||||
$gi->from_profile_id = $pid;
|
||||
$gi->to_profile_id = $u->id;
|
||||
$gi->to_local = true;
|
||||
$gi->from_local = $u->domain == null;
|
||||
$gi->save();
|
||||
// GroupMemberInvite::dispatch($gi);
|
||||
});
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function searchFriendsToInvite(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = Profile::where('username', 'like', "%{$q}%")
|
||||
->whereNull('profiles.domain')
|
||||
->join('followers', 'profiles.id', '=', 'followers.profile_id')
|
||||
->where('followers.following_id', $pid)
|
||||
->take(10)
|
||||
->get()
|
||||
->filter(function($p) use($group) {
|
||||
return $group->isMember($p->profile_id) == false;
|
||||
})
|
||||
->filter(function($p) use($group, $pid) {
|
||||
return GroupInvitation::whereGroupId($group->id)
|
||||
->whereFromProfileId($pid)
|
||||
->whereToProfileId($p->profile_id)
|
||||
->exists() == false;
|
||||
})
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'id' => (string) $gm->profile_id,
|
||||
'username' => $a['acct'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchGlobalResults(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:140',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
|
||||
if(str_starts_with($q, 'https://')) {
|
||||
$res = Helpers::getSignedFetch($q);
|
||||
if($res && $res = json_decode($res, true)) {
|
||||
|
||||
}
|
||||
if($res && isset($res['type']) && in_array($res['type'], ['Group', 'Note', 'Page'])) {
|
||||
if($res['type'] === 'Group') {
|
||||
return GroupActivityPubService::fetchGroup($q, true);
|
||||
}
|
||||
$resp = GroupActivityPubService::fetchGroupPost($q, true);
|
||||
$resp['name'] = 'Group Post';
|
||||
$resp['url'] = '/groups/' . $resp['group_id'] . '/p/' . $resp['id'];
|
||||
return [$resp];
|
||||
}
|
||||
}
|
||||
return Group::whereNull('status')
|
||||
->where('name', 'like', '%' . $q . '%')
|
||||
->orderBy('id')
|
||||
->take(10)
|
||||
->pluck('id')
|
||||
->map(function($group) {
|
||||
return GroupService::get($group);
|
||||
});
|
||||
}
|
||||
|
||||
public function searchLocalAutocomplete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
'v' => 'required|in:0.2'
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$res = GroupMember::whereGroupId($gid)
|
||||
->join('profiles', 'group_members.profile_id', '=', 'profiles.id')
|
||||
->where('profiles.username', 'like', "%{$q}%")
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($gm) use ($gid) {
|
||||
$a = AccountService::get($gm->profile_id);
|
||||
return [
|
||||
'username' => $a['username'],
|
||||
'url' => url("/groups/{$gid}/user/{$a['id']}?rf=group_search")
|
||||
];
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function searchAddRecent(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|min:2|max:40',
|
||||
'g' => 'required',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
$ttl = now()->addDays(14);
|
||||
$res = Cache::get($key);
|
||||
if(!$res) {
|
||||
$val = json_encode([$q]);
|
||||
} else {
|
||||
$ex = collect(json_decode($res))
|
||||
->prepend($q)
|
||||
->unique('value')
|
||||
->slice(0, 3)
|
||||
->values()
|
||||
->all();
|
||||
$val = json_encode($ex);
|
||||
}
|
||||
Cache::put($key, $val, $ttl);
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function searchGetRecent(Request $request)
|
||||
{
|
||||
$gid = $request->input('g');
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
$key = 'groups:search:recent:'.$gid.':pid:'.$pid;
|
||||
return Cache::get($key);
|
||||
}
|
||||
}
|
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
133
app/Http/Controllers/Groups/GroupsTopicController.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
use App\Services\Groups\GroupPostService;
|
||||
use App\Services\Groups\GroupsLikeService;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupPost;
|
||||
|
||||
class GroupsTopicController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function groupTopics(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$posts = GroupPostHashtag::join('group_hashtags', 'group_hashtags.id', '=', 'group_post_hashtags.hashtag_id')
|
||||
->selectRaw('group_hashtags.*, group_post_hashtags.*, count(group_post_hashtags.hashtag_id) as ht_count')
|
||||
->where('group_post_hashtags.group_id', $gid)
|
||||
->orderByDesc('ht_count')
|
||||
->limit(10)
|
||||
->pluck('group_post_hashtags.hashtag_id', 'ht_count')
|
||||
->map(function($id, $key) use ($gid) {
|
||||
$tag = GroupHashtag::find($id);
|
||||
return [
|
||||
'hid' => $id,
|
||||
'name' => $tag->name,
|
||||
'url' => url("/groups/{$gid}/topics/{$tag->slug}"),
|
||||
'count' => $key
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function groupTopicTag(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'gid' => 'required',
|
||||
'name' => 'required'
|
||||
]);
|
||||
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$gid = $request->input('gid');
|
||||
$limit = $request->input('limit', 3);
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
|
||||
$name = $request->input('name');
|
||||
$hashtag = GroupHashtag::whereName($name)->first();
|
||||
|
||||
if(!$hashtag) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// $posts = GroupPost::whereGroupId($gid)
|
||||
// ->select('status_hashtags.*', 'group_posts.*')
|
||||
// ->where('status_hashtags.hashtag_id', $hashtag->id)
|
||||
// ->join('status_hashtags', 'group_posts.status_id', '=', 'status_hashtags.status_id')
|
||||
// ->orderByDesc('group_posts.status_id')
|
||||
// ->simplePaginate($limit)
|
||||
// ->map(function($gp) use($pid) {
|
||||
// $status = StatusService::get($gp['status_id'], false);
|
||||
// if(!$status) {
|
||||
// return false;
|
||||
// }
|
||||
// $status['favourited'] = (bool) LikeService::liked($pid, $gp['status_id']);
|
||||
// $status['favourites_count'] = LikeService::count($gp['status_id']);
|
||||
// $status['pf_type'] = $gp['type'];
|
||||
// $status['visibility'] = 'public';
|
||||
// $status['url'] = $gp->url();
|
||||
// return $status;
|
||||
// });
|
||||
|
||||
$posts = GroupPostHashtag::whereGroupId($gid)
|
||||
->whereHashtagId($hashtag->id)
|
||||
->orderByDesc('id')
|
||||
->simplePaginate($limit)
|
||||
->map(function($gp) use($pid) {
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['status_id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['status_id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['status_id']);
|
||||
$status['pf_type'] = $status['pf_type'];
|
||||
$status['visibility'] = 'public';
|
||||
return $status;
|
||||
});
|
||||
|
||||
return response()->json($posts, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showTopicFeed(Request $request, $gid, $tag)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$group = Group::findOrFail($gid);
|
||||
$gid = $group->id;
|
||||
abort_if(!$group->isMember($pid), 403, 'Not a member of group.');
|
||||
return view('groups.topic-feed', compact('gid', 'tag'));
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ class Kernel extends HttpKernel
|
|||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'api.admin' => \App\Http\Middleware\Api\Admin::class,
|
||||
'admin' => \App\Http\Middleware\Admin::class,
|
||||
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
|
@ -68,6 +69,8 @@ class Kernel extends HttpKernel
|
|||
'twofactor' => \App\Http\Middleware\TwoFactorAuth::class,
|
||||
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
|
||||
'interstitial' => \App\Http\Middleware\AccountInterstitial::class,
|
||||
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
|
||||
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
|
||||
// 'restricted' => \App\Http\Middleware\RestrictedAccess::class,
|
||||
];
|
||||
}
|
||||
|
|
26
app/Http/Middleware/Api/Admin.php
Normal file
26
app/Http/Middleware/Api/Admin.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware\Api;
|
||||
|
||||
use Auth;
|
||||
use Closure;
|
||||
|
||||
class Admin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (Auth::check() == false || Auth::user()->is_admin == false) {
|
||||
return abort(403, "You must be an administrator to do that");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
42
app/Http/Resources/MastoApi/Admin/DomainBlockResource.php
Normal file
42
app/Http/Resources/MastoApi/Admin/DomainBlockResource.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\MastoApi\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class DomainBlockResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$severity = 'noop';
|
||||
if ($this->banned) {
|
||||
$severity = 'suspend';
|
||||
} else if ($this->unlisted) {
|
||||
$severity = 'silence';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'domain' => $this->domain,
|
||||
// This property is coming in Mastodon 4.3, although it'll only be
|
||||
// useful if Pixelfed supports obfuscating domains:
|
||||
'digest' => hash('sha256', $this->domain),
|
||||
'severity' => $severity,
|
||||
// Using the updated_at value as this is going to be the closest to
|
||||
// when the domain was banned
|
||||
'created_at' => $this->updated_at,
|
||||
// We don't have data for these fields
|
||||
'reject_media' => false,
|
||||
'reject_reports' => false,
|
||||
'private_comment' => $this->notes ? join('; ', $this->notes) : null,
|
||||
'public_comment' => $this->limit_reason,
|
||||
'obfuscate' => false
|
||||
];
|
||||
}
|
||||
}
|
|
@ -22,6 +22,13 @@ class Instance extends Model
|
|||
'notes'
|
||||
];
|
||||
|
||||
// To get all moderated instances, we need to search where (banned OR unlisted)
|
||||
public function scopeModerated($query): void {
|
||||
$query->where(function ($query) {
|
||||
$query->where('banned', true)->orWhere('unlisted', true);
|
||||
});
|
||||
}
|
||||
|
||||
public function profiles()
|
||||
{
|
||||
return $this->hasMany(Profile::class, 'domain', 'domain');
|
||||
|
|
99
app/Jobs/GroupPipeline/GroupCommentPipeline.php
Normal file
99
app/Jobs/GroupPipeline/GroupCommentPipeline.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class GroupCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $comment;
|
||||
protected $groupPost;
|
||||
|
||||
public function __construct(Status $status, Status $comment, $groupPost = null)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->comment = $comment;
|
||||
$this->groupPost = $groupPost;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if($this->status->group_id == null || $this->comment->group_id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateParentReplyCount();
|
||||
$this->generateNotification();
|
||||
|
||||
if($this->groupPost) {
|
||||
$this->updateChildReplyCount();
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateParentReplyCount()
|
||||
{
|
||||
$parent = $this->status;
|
||||
$parent->reply_count = Status::whereInReplyToId($parent->id)->count();
|
||||
$parent->save();
|
||||
StatusService::del($parent->id);
|
||||
}
|
||||
|
||||
protected function updateChildReplyCount()
|
||||
{
|
||||
$gp = $this->groupPost;
|
||||
if($gp->reply_child_id) {
|
||||
$parent = GroupPost::whereStatusId($gp->reply_child_id)->first();
|
||||
if($parent) {
|
||||
$parent->reply_count++;
|
||||
$parent->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateNotification()
|
||||
{
|
||||
$status = $this->status;
|
||||
$comment = $this->comment;
|
||||
|
||||
$target = $status->profile;
|
||||
$actor = $comment->profile;
|
||||
|
||||
if ($actor->id == $target->id || $status->comments_disabled == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = DB::transaction(function() use($target, $actor, $comment) {
|
||||
$actorName = $actor->username;
|
||||
$actorUrl = $actor->url();
|
||||
$text = "{$actorName} commented on your group post.";
|
||||
$html = "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> commented on your group post.";
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:comment';
|
||||
$notification->item_id = $comment->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
return $notification;
|
||||
});
|
||||
|
||||
NotificationService::setNotification($notification);
|
||||
NotificationService::set($notification->profile_id, $notification->id);
|
||||
}
|
||||
}
|
57
app/Jobs/GroupPipeline/GroupMediaPipeline.php
Normal file
57
app/Jobs/GroupPipeline/GroupMediaPipeline.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Media;
|
||||
use Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
|
||||
class GroupMediaPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
MediaStorageService::store($this->media);
|
||||
}
|
||||
|
||||
protected function localToCloud($media)
|
||||
{
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
$thumb = storage_path('app/'.$media->thumbnail_path);
|
||||
|
||||
$p = explode('/', $media->media_path);
|
||||
$name = array_pop($p);
|
||||
$pt = explode('/', $media->thumbnail_path);
|
||||
$thumbname = array_pop($pt);
|
||||
$storagePath = implode('/', $p);
|
||||
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
$url = $disk->url($file);
|
||||
$thumbFile = $disk->putFileAs($storagePath, new File($thumb), $thumbname, 'public');
|
||||
$thumbUrl = $disk->url($thumbFile);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
$media->cdn_url = $url;
|
||||
$media->optimized_url = $url;
|
||||
$media->replicated_at = now();
|
||||
$media->save();
|
||||
if($media->status_id) {
|
||||
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
54
app/Jobs/GroupPipeline/GroupMemberInvite.php
Normal file
54
app/Jobs/GroupPipeline/GroupMemberInvite.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupInvitation;
|
||||
use App\Notification;
|
||||
use App\Profile;
|
||||
|
||||
class GroupMemberInvite implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $invite;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupInvitation $invite)
|
||||
{
|
||||
$this->invite = $invite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$invite = $this->invite;
|
||||
$actor = Profile::find($invite->from_profile_id);
|
||||
$target = Profile::find($invite->to_profile_id);
|
||||
|
||||
if(!$actor || !$target) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:invite';
|
||||
$notification->item_id = $invite->group_id;
|
||||
$notification->item_type = 'App\Models\Group';
|
||||
$notification->save();
|
||||
}
|
||||
}
|
54
app/Jobs/GroupPipeline/JoinApproved.php
Normal file
54
app/Jobs/GroupPipeline/JoinApproved.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class JoinApproved implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->approved_at = now();
|
||||
$member->join_request = false;
|
||||
$member->role = 'member';
|
||||
$member->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $member->profile_id;
|
||||
$n->actor_id = $member->profile_id;
|
||||
$n->item_id = $member->group_id;
|
||||
$n->item_type = 'App\Models\Group';
|
||||
$n->save();
|
||||
|
||||
GroupService::del($member->group_id);
|
||||
GroupService::delSelf($member->group_id, $member->profile_id);
|
||||
}
|
||||
}
|
50
app/Jobs/GroupPipeline/JoinRejected.php
Normal file
50
app/Jobs/GroupPipeline/JoinRejected.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class JoinRejected implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->rejected_at = now();
|
||||
$member->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $member->profile_id;
|
||||
$n->actor_id = $member->profile_id;
|
||||
$n->item_id = $member->group_id;
|
||||
$n->item_type = 'App\Models\Group';
|
||||
$n->action = 'group.join.rejected';
|
||||
$n->save();
|
||||
}
|
||||
}
|
107
app/Jobs/GroupPipeline/LikePipeline.php
Normal file
107
app/Jobs/GroupPipeline/LikePipeline.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\Like as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class LikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if (!$status) {
|
||||
// Ignore notifications to deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
StatusService::refresh($status->id);
|
||||
|
||||
if($status->url && $actor->domain == null) {
|
||||
return $this->remoteLikeDeliver();
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('group:like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if ($actor->id === $status->profile_id || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $status->profile_id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'group:like';
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
public function remoteLikeDeliver()
|
||||
{
|
||||
$like = $this->like;
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($actor, $url, $activity);
|
||||
}
|
||||
}
|
130
app/Jobs/GroupPipeline/NewStatusPipeline.php
Normal file
130
app/Jobs/GroupPipeline/NewStatusPipeline.php
Normal file
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use App\Notification;
|
||||
use App\Hashtag;
|
||||
use App\Mention;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
|
||||
class NewStatusPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $gp;
|
||||
protected $tags;
|
||||
protected $mentions;
|
||||
|
||||
public function __construct(Status $status, GroupPost $gp)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->gp = $gp;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
|
||||
$autolink = Autolink::create()
|
||||
->setAutolinkActiveUsersOnly(true)
|
||||
->setBaseHashPath("/groups/{$status->group_id}/topics/")
|
||||
->setBaseUserPath("/groups/{$status->group_id}/username/")
|
||||
->autolink($status->caption);
|
||||
|
||||
$entities = Extractor::create()->extract($status->caption);
|
||||
|
||||
$autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink);
|
||||
|
||||
$status->rendered = nl2br($autolink);
|
||||
$status->entities = null;
|
||||
$status->save();
|
||||
|
||||
$this->tags = array_unique($entities['hashtags']);
|
||||
$this->mentions = array_unique($entities['mentions']);
|
||||
|
||||
if(count($this->tags)) {
|
||||
$this->storeHashtags();
|
||||
}
|
||||
|
||||
if(count($this->mentions)) {
|
||||
$this->storeMentions($this->mentions);
|
||||
}
|
||||
}
|
||||
|
||||
protected function storeHashtags()
|
||||
{
|
||||
$tags = $this->tags;
|
||||
$status = $this->status;
|
||||
$gp = $this->gp;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if(mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($status, $tag, $gp) {
|
||||
$slug = str_slug($tag, '-', false);
|
||||
$hashtag = Hashtag::firstOrCreate(
|
||||
['name' => $tag, 'slug' => $slug]
|
||||
);
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'group_id' => $status->group_id,
|
||||
'group_post_id' => $gp->id,
|
||||
'status_id' => $status->id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
]
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if(count($this->mentions)) {
|
||||
$this->storeMentions();
|
||||
}
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
|
||||
protected function storeMentions()
|
||||
{
|
||||
$mentions = $this->mentions;
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->first();
|
||||
|
||||
if (empty($mentioned) || !isset($mentioned->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($status, $mentioned) {
|
||||
$m = new Mention();
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
});
|
||||
}
|
||||
StatusService::del($status->id);
|
||||
}
|
||||
}
|
109
app/Jobs/GroupPipeline/UnlikePipeline.php
Normal file
109
app/Jobs/GroupPipeline/UnlikePipeline.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\UndoLike as LikeTransformer;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class UnlikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 5;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
if (!$status) {
|
||||
// Ignore notifications to deleted statuses
|
||||
return;
|
||||
}
|
||||
|
||||
$count = $status->likes_count > 1 ? $status->likes_count : $status->likes()->count();
|
||||
$status->likes_count = $count - 1;
|
||||
$status->save();
|
||||
|
||||
StatusService::del($status->id);
|
||||
|
||||
if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) {
|
||||
$this->remoteLikeDeliver();
|
||||
}
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('group:like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->first();
|
||||
|
||||
if($exists) {
|
||||
$exists->delete();
|
||||
}
|
||||
|
||||
$like = Like::whereProfileId($actor->id)->whereStatusId($status->id)->first();
|
||||
|
||||
if(!$like) {
|
||||
return;
|
||||
}
|
||||
|
||||
$like->forceDelete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public function remoteLikeDeliver()
|
||||
{
|
||||
$like = $this->like;
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($like, new LikeTransformer());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$url = $status->profile->sharedInbox ?? $status->profile->inbox_url;
|
||||
|
||||
Helpers::sendSignedObject($actor, $url, $activity);
|
||||
}
|
||||
}
|
58
app/Jobs/GroupsPipeline/DeleteCommentPipeline.php
Normal file
58
app/Jobs/GroupsPipeline/DeleteCommentPipeline.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupComment;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class DeleteCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $parent;
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($parent, $status)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$parent = $this->parent;
|
||||
$parent->reply_count = GroupComment::whereStatusId($parent->id)->count();
|
||||
$parent->save();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
89
app/Jobs/GroupsPipeline/ImageResizePipeline.php
Normal file
89
app/Jobs/GroupsPipeline/ImageResizePipeline.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Log;
|
||||
use Storage;
|
||||
use Image as Intervention;
|
||||
|
||||
class ImageResizePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Storage::exists($media->media_path) || $media->skip_optimize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $media->media_path;
|
||||
$file = storage_path('app/' . $path);
|
||||
$quality = config_cache('pixelfed.image_quality');
|
||||
|
||||
$orientations = [
|
||||
'square' => [
|
||||
'width' => 1080,
|
||||
'height' => 1080,
|
||||
],
|
||||
'landscape' => [
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
'portrait' => [
|
||||
'width' => 1080,
|
||||
'height' => 1350,
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$img = Intervention::make($file);
|
||||
$img->orientate();
|
||||
$width = $img->width();
|
||||
$height = $img->height();
|
||||
$aspect = $width / $height;
|
||||
$orientation = $aspect === 1 ? 'square' : ($aspect > 1 ? 'landscape' : 'portrait');
|
||||
$ratio = $orientations[$orientation];
|
||||
$img->resize($ratio['width'], $ratio['height']);
|
||||
$img->save($file, $quality);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
67
app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php
Normal file
67
app/Jobs/GroupsPipeline/ImageS3DeletePipeline.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Storage;
|
||||
use Illuminate\Http\File;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class ImageS3DeletePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
static $attempts = 1;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fs = Storage::disk(config('filesystems.cloud'));
|
||||
|
||||
if(!$fs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($fs->exists($media->media_path)) {
|
||||
$fs->delete($media->media_path);
|
||||
}
|
||||
}
|
||||
}
|
107
app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php
Normal file
107
app/Jobs/GroupsPipeline/ImageS3UploadPipeline.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Models\GroupMedia;
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Storage;
|
||||
use Illuminate\Http\File;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class ImageS3UploadPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $media;
|
||||
static $attempts = 1;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$media = $this->media;
|
||||
|
||||
if(!$media || (bool) config_cache('pixelfed.cloud_storage') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = storage_path('app/' . $media->media_path);
|
||||
|
||||
$p = explode('/', $media->media_path);
|
||||
$name = array_pop($p);
|
||||
$storagePath = implode('/', $p);
|
||||
|
||||
$url = (bool) config_cache('pixelfed.cloud_storage') && (bool) config('media.storage.remote.resilient_mode') ?
|
||||
self::handleResilientStore($storagePath, $path, $name) :
|
||||
self::handleStore($storagePath, $path, $name);
|
||||
|
||||
if($url && strlen($url) && str_starts_with($url, 'https://')) {
|
||||
$media->cdn_url = $url;
|
||||
$media->processed_at = now();
|
||||
$media->version = 11;
|
||||
$media->save();
|
||||
Storage::disk('local')->delete($media->media_path);
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleStore($storagePath, $path, $name)
|
||||
{
|
||||
return retry(3, function() use($storagePath, $path, $name) {
|
||||
$baseDisk = (bool) config_cache('pixelfed.cloud_storage') ? config('filesystems.cloud') : 'local';
|
||||
$disk = Storage::disk($baseDisk);
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
return $disk->url($file);
|
||||
}, random_int(100, 500));
|
||||
}
|
||||
|
||||
protected function handleResilientStore($storagePath, $path, $name)
|
||||
{
|
||||
$attempts = 0;
|
||||
return retry(4, function() use($storagePath, $path, $name, $attempts) {
|
||||
self::$attempts++;
|
||||
usleep(100000);
|
||||
$baseDisk = self::$attempts > 1 ? $this->getAltDriver() : config('filesystems.cloud');
|
||||
try {
|
||||
$disk = Storage::disk($baseDisk);
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
} catch (S3Exception | ClientException | ConnectException | UnableToWriteFile | Exception $e) {}
|
||||
return $disk->url($file);
|
||||
}, function (int $attempt, Exception $exception) {
|
||||
return $attempt * 200;
|
||||
});
|
||||
}
|
||||
|
||||
protected function getAltDriver()
|
||||
{
|
||||
return config('filesystems.cloud');
|
||||
}
|
||||
}
|
47
app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php
Normal file
47
app/Jobs/GroupsPipeline/MemberJoinApprovedPipeline.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class MemberJoinApprovedPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->approved_at = now();
|
||||
$member->join_request = false;
|
||||
$member->role = 'member';
|
||||
$member->save();
|
||||
|
||||
GroupService::del($member->group_id);
|
||||
GroupService::delSelf($member->group_id, $member->profile_id);
|
||||
}
|
||||
}
|
42
app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php
Normal file
42
app/Jobs/GroupsPipeline/MemberJoinRejectedPipeline.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\GroupMember;
|
||||
use App\Notification;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class MemberJoinRejectedPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupMember $member)
|
||||
{
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$member = $this->member;
|
||||
$member->rejected_at = now();
|
||||
$member->save();
|
||||
}
|
||||
}
|
115
app/Jobs/GroupsPipeline/NewCommentPipeline.php
Normal file
115
app/Jobs/GroupsPipeline/NewCommentPipeline.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupComment;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class NewCommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $parent;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($parent, GroupComment $status)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->status->profile;
|
||||
$status = $this->status;
|
||||
|
||||
$parent = $this->parent;
|
||||
$parent->reply_count = GroupComment::whereStatusId($parent->id)->count();
|
||||
$parent->save();
|
||||
|
||||
if ($profile->no_autolink == false) {
|
||||
$this->parseEntities();
|
||||
}
|
||||
}
|
||||
|
||||
public function parseEntities()
|
||||
{
|
||||
$this->extractEntities();
|
||||
}
|
||||
|
||||
public function extractEntities()
|
||||
{
|
||||
$this->entities = Extractor::create()->extract($this->status->caption);
|
||||
$this->autolinkStatus();
|
||||
}
|
||||
|
||||
public function autolinkStatus()
|
||||
{
|
||||
$this->autolink = Autolink::create()->autolink($this->status->caption);
|
||||
$this->storeHashtags();
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$hashtag = GroupHashtag::firstOrCreate([
|
||||
'name' => $tag,
|
||||
]);
|
||||
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'status_id' => $status->id,
|
||||
'group_id' => $status->group_id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_visibility' => $status->visibility,
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
$this->storeMentions();
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
}
|
108
app/Jobs/GroupsPipeline/NewPostPipeline.php
Normal file
108
app/Jobs/GroupsPipeline/NewPostPipeline.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\GroupsPipeline;
|
||||
|
||||
use App\Util\Media\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use DB;
|
||||
|
||||
class NewPostPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(GroupPost $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$profile = $this->status->profile;
|
||||
$status = $this->status;
|
||||
|
||||
if ($profile->no_autolink == false) {
|
||||
$this->parseEntities();
|
||||
}
|
||||
}
|
||||
|
||||
public function parseEntities()
|
||||
{
|
||||
$this->extractEntities();
|
||||
}
|
||||
|
||||
public function extractEntities()
|
||||
{
|
||||
$this->entities = Extractor::create()->extract($this->status->caption);
|
||||
$this->autolinkStatus();
|
||||
}
|
||||
|
||||
public function autolinkStatus()
|
||||
{
|
||||
$this->autolink = Autolink::create()->autolink($this->status->caption);
|
||||
$this->storeHashtags();
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (mb_strlen($tag) > 124) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($status, $tag) {
|
||||
$hashtag = GroupHashtag::firstOrCreate([
|
||||
'name' => $tag,
|
||||
]);
|
||||
|
||||
GroupPostHashtag::firstOrCreate(
|
||||
[
|
||||
'status_id' => $status->id,
|
||||
'group_id' => $status->group_id,
|
||||
'hashtag_id' => $hashtag->id,
|
||||
'profile_id' => $status->profile_id,
|
||||
'status_visibility' => $status->visibility,
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
$this->storeMentions();
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
}
|
67
app/Models/Group.php
Normal file
67
app/Models/Group.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Profile;
|
||||
use App\Services\GroupService;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Group extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'json'
|
||||
];
|
||||
|
||||
public function url()
|
||||
{
|
||||
return url("/groups/{$this->id}");
|
||||
}
|
||||
|
||||
public function permalink($suffix = null)
|
||||
{
|
||||
if(!$this->local) {
|
||||
return $this->remote_url;
|
||||
}
|
||||
return $this->url() . $suffix;
|
||||
}
|
||||
|
||||
public function members()
|
||||
{
|
||||
return $this->hasMany(GroupMember::class);
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function isMember($id = false)
|
||||
{
|
||||
$id = $id ?? request()->user()->profile_id;
|
||||
// return $this->members()->whereProfileId($id)->whereJoinRequest(false)->exists();
|
||||
return GroupService::isMember($this->id, $id);
|
||||
}
|
||||
|
||||
public function getMembershipType()
|
||||
{
|
||||
return $this->is_private ? 'private' : ($this->is_local ? 'local' : 'all');
|
||||
}
|
||||
|
||||
public function selfRole($id = false)
|
||||
{
|
||||
$id = $id ?? request()->user()->profile_id;
|
||||
return optional($this->members()->whereProfileId($id)->first())->role ?? null;
|
||||
}
|
||||
}
|
11
app/Models/GroupActivityGraph.php
Normal file
11
app/Models/GroupActivityGraph.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupActivityGraph extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupBlock.php
Normal file
11
app/Models/GroupBlock.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupBlock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupCategory.php
Normal file
11
app/Models/GroupCategory.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
24
app/Models/GroupComment.php
Normal file
24
app/Models/GroupComment.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class GroupComment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return '/group/' . $this->group_id . '/c/' . $this->id;
|
||||
}
|
||||
}
|
11
app/Models/GroupEvent.php
Normal file
11
app/Models/GroupEvent.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupEvent extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
13
app/Models/GroupHashtag.php
Normal file
13
app/Models/GroupHashtag.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupHashtag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['name'];
|
||||
}
|
15
app/Models/GroupInteraction.php
Normal file
15
app/Models/GroupInteraction.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupInteraction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array'
|
||||
];
|
||||
}
|
11
app/Models/GroupInvitation.php
Normal file
11
app/Models/GroupInvitation.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupInvitation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
13
app/Models/GroupLike.php
Normal file
13
app/Models/GroupLike.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupLike extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['group_id', 'status_id', 'profile_id', 'comment_id'];
|
||||
}
|
21
app/Models/GroupLimit.php
Normal file
21
app/Models/GroupLimit.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupLimit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'limits' => 'json',
|
||||
'metadata' => 'json'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'profile_id',
|
||||
'group_id'
|
||||
];
|
||||
}
|
39
app/Models/GroupMedia.php
Normal file
39
app/Models/GroupMedia.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
|
||||
class GroupMedia extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'json',
|
||||
'processed_at' => 'datetime',
|
||||
'thumbnail_generated' => 'datetime'
|
||||
];
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
if($this->cdn_url) {
|
||||
return $this->cdn_url;
|
||||
}
|
||||
return Storage::url($this->media_path);
|
||||
}
|
||||
|
||||
public function thumbnailUrl()
|
||||
{
|
||||
return $this->thumbnail_url;
|
||||
}
|
||||
}
|
16
app/Models/GroupMember.php
Normal file
16
app/Models/GroupMember.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupMember extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
}
|
57
app/Models/GroupPost.php
Normal file
57
app/Models/GroupPost.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Services\HashidService;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
|
||||
class GroupPost extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, HasFactory;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $fillable = [
|
||||
'remote_url',
|
||||
'group_id',
|
||||
'profile_id',
|
||||
'type',
|
||||
'caption',
|
||||
'visibility',
|
||||
'is_nsfw'
|
||||
];
|
||||
|
||||
public function mediaPath()
|
||||
{
|
||||
return 'public/g/_v1/' . $this->group_id . '/' . $this->id;
|
||||
}
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return '/groups/' . $this->group_id . '/p/' . $this->id;
|
||||
}
|
||||
}
|
22
app/Models/GroupPostHashtag.php
Normal file
22
app/Models/GroupPostHashtag.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupPostHashtag extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = [
|
||||
'group_id',
|
||||
'group_post_id',
|
||||
'status_id',
|
||||
'hashtag_id',
|
||||
'profile_id',
|
||||
'nsfw'
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
}
|
11
app/Models/GroupReport.php
Normal file
11
app/Models/GroupReport.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupReport extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupRole.php
Normal file
11
app/Models/GroupRole.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupRole extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
11
app/Models/GroupStore.php
Normal file
11
app/Models/GroupStore.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GroupStore extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
|
@ -5,6 +5,7 @@ namespace App\Providers;
|
|||
use App\Observers\{
|
||||
AvatarObserver,
|
||||
FollowerObserver,
|
||||
HashtagFollowObserver,
|
||||
LikeObserver,
|
||||
NotificationObserver,
|
||||
ModLogObserver,
|
||||
|
@ -17,6 +18,7 @@ use App\Observers\{
|
|||
use App\{
|
||||
Avatar,
|
||||
Follower,
|
||||
HashtagFollow,
|
||||
Like,
|
||||
Notification,
|
||||
ModLog,
|
||||
|
@ -32,6 +34,7 @@ use Illuminate\Support\Facades\Schema;
|
|||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -50,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
Paginator::useBootstrap();
|
||||
Avatar::observe(AvatarObserver::class);
|
||||
Follower::observe(FollowerObserver::class);
|
||||
HashtagFollow::observe(HashtagFollowObserver::class);
|
||||
Like::observe(LikeObserver::class);
|
||||
Notification::observe(NotificationObserver::class);
|
||||
ModLog::observe(ModLogObserver::class);
|
||||
|
@ -62,6 +66,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
return Auth::check() && $request->user()->is_admin;
|
||||
});
|
||||
Validator::includeUnvalidatedArrayKeys();
|
||||
|
||||
// Model::preventLazyLoading(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
if (config('app.env') === 'production' && (bool) config_cache('pixelfed.oauth_enabled') == true) {
|
||||
if(config('pixelfed.oauth_enabled') == true) {
|
||||
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
|
||||
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
|
||||
Passport::enableImplicitGrant();
|
||||
|
@ -37,8 +37,10 @@ class AuthServiceProvider extends ServiceProvider
|
|||
'write' => 'Full write access to your account',
|
||||
'follow' => 'Ability to follow other profiles',
|
||||
'admin:read' => 'Read all data on the server',
|
||||
'admin:read:domain_blocks' => 'Read sensitive information of all domain blocks',
|
||||
'admin:write' => 'Modify all data on the server',
|
||||
'push' => 'Receive your push notifications',
|
||||
'admin:write:domain_blocks' => 'Perform moderation actions on domain blocks',
|
||||
'push' => 'Receive your push notifications'
|
||||
]);
|
||||
|
||||
Passport::setDefaultScope([
|
||||
|
|
21
app/Rules/ValidUrl.php
Normal file
21
app/Rules/ValidUrl.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class ValidUrl implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (!str_starts_with(strtolower($value), 'https://')) {
|
||||
$fail('The :attribute must start with https://.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ class AdminSettingsService
|
|||
'registration_status' => $regState,
|
||||
'cloud_storage' => $cloud_ready && $cloud_storage,
|
||||
'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'),
|
||||
'authorized_fetch' => (bool) config_cache('federation.activitypub.authorized_fetch'),
|
||||
'account_migration' => (bool) config_cache('federation.migration'),
|
||||
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'stories' => (bool) config_cache('instance.stories.enabled'),
|
||||
|
|
|
@ -46,6 +46,7 @@ class ConfigCacheService
|
|||
'pixelfed.oauth_enabled',
|
||||
'pixelfed.import.instagram.enabled',
|
||||
'pixelfed.bouncer.enabled',
|
||||
'federation.activitypub.authorized_fetch',
|
||||
|
||||
'pixelfed.enforce_email_verification',
|
||||
'pixelfed.max_account_size',
|
||||
|
|
88
app/Services/GroupFeedService.php
Normal file
88
app/Services/GroupFeedService.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class GroupFeedService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:feed:';
|
||||
|
||||
const FEED_LIMIT = 400;
|
||||
|
||||
public static function get($gid, $start = 0, $stop = 10)
|
||||
{
|
||||
if ($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_KEY.$gid, $start, $stop);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($gid, $start = null, $limit = 10)
|
||||
{
|
||||
if (! $start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRankedMinId($gid, $end = null, $limit = 10)
|
||||
{
|
||||
if (! $end) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY.$gid, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function add($gid, $val)
|
||||
{
|
||||
if (self::count($gid) > self::FEED_LIMIT) {
|
||||
if (config('database.redis.client') === 'phpredis') {
|
||||
Redis::zpopmin(self::CACHE_KEY.$gid);
|
||||
}
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_KEY.$gid, $val, $val);
|
||||
}
|
||||
|
||||
public static function rem($gid, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY.$gid, $val);
|
||||
}
|
||||
|
||||
public static function del($gid, $val)
|
||||
{
|
||||
return self::rem($gid, $val);
|
||||
}
|
||||
|
||||
public static function count($gid)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_KEY.$gid);
|
||||
}
|
||||
|
||||
public static function warmCache($gid, $force = false, $limit = 100)
|
||||
{
|
||||
if (self::count($gid) == 0 || $force == true) {
|
||||
Redis::del(self::CACHE_KEY.$gid);
|
||||
$ids = GroupPost::whereGroupId($gid)
|
||||
->orderByDesc('id')
|
||||
->limit($limit)
|
||||
->pluck('id');
|
||||
foreach ($ids as $id) {
|
||||
self::add($gid, $id);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
49
app/Services/GroupPostService.php
Normal file
49
app/Services/GroupPostService.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
use Cache;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class GroupPostService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:post:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY.$gid.':'.$pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function () use ($gid, $pid) {
|
||||
$gp = GroupPost::whereGroupId($gid)->find($pid);
|
||||
|
||||
if (! $gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = $gp['type'];
|
||||
$res['url'] = $gp->url();
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
}
|
366
app/Services/GroupService.php
Normal file
366
app/Services/GroupService.php
Normal file
|
@ -0,0 +1,366 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use App\Http\Resources\Groups\GroupResource;
|
||||
|
||||
class GroupService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:';
|
||||
|
||||
protected static function key($name)
|
||||
{
|
||||
return self::CACHE_KEY . $name;
|
||||
}
|
||||
|
||||
public static function get($id, $pid = false)
|
||||
{
|
||||
$res = Cache::remember(
|
||||
self::key($id),
|
||||
1209600,
|
||||
function() use($id, $pid) {
|
||||
$group = (new Group)->withoutRelations()->whereNull('status')->find($id);
|
||||
|
||||
if(!$group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$admin = $group->profile_id ? AccountService::get($group->profile_id) : null;
|
||||
|
||||
return [
|
||||
'id' => (string) $group->id,
|
||||
'name' => $group->name,
|
||||
'description' => $group->description,
|
||||
'short_description' => str_limit(strip_tags($group->description), 120),
|
||||
'category' => self::categoryById($group->category_id),
|
||||
'local' => (bool) $group->local,
|
||||
'url' => $group->url(),
|
||||
'shorturl' => url('/g/'.HashidService::encode($group->id)),
|
||||
'membership' => $group->getMembershipType(),
|
||||
'member_count' => $group->members()->whereJoinRequest(false)->count(),
|
||||
'verified' => false,
|
||||
'self' => null,
|
||||
'admin' => $admin,
|
||||
'config' => [
|
||||
'recommended' => (bool) $group->recommended,
|
||||
'discoverable' => (bool) $group->discoverable,
|
||||
'activitypub' => (bool) $group->activitypub,
|
||||
'is_nsfw' => (bool) $group->is_nsfw,
|
||||
'dms' => (bool) $group->dms
|
||||
],
|
||||
'metadata' => $group->metadata,
|
||||
'created_at' => $group->created_at->toAtomString(),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
if($pid) {
|
||||
$res['self'] = self::getSelf($id, $pid);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public static function del($id)
|
||||
{
|
||||
Cache::forget('ap:groups:object:' . $id);
|
||||
return Cache::forget(self::key($id));
|
||||
}
|
||||
|
||||
public static function getSelf($gid, $pid)
|
||||
{
|
||||
return Cache::remember(
|
||||
self::key('self:gid-' . $gid . ':pid-' . $pid),
|
||||
3600,
|
||||
function() use($gid, $pid) {
|
||||
$group = Group::find($gid);
|
||||
|
||||
if(!$gid || !$pid) {
|
||||
return [
|
||||
'is_member' => false,
|
||||
'role' => null,
|
||||
'is_requested' => null
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'is_member' => $group->isMember($pid),
|
||||
'role' => $group->selfRole($pid),
|
||||
'is_requested' => optional($group->members()->whereProfileId($pid)->first())->join_request ?? false
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function delSelf($gid, $pid)
|
||||
{
|
||||
Cache::forget(self::key("is_member:{$gid}:{$pid}"));
|
||||
return Cache::forget(self::key('self:gid-' . $gid . ':pid-' . $pid));
|
||||
}
|
||||
|
||||
public static function sidToGid($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key('s2gid:' . $gid . ':' . $pid), 3600, function() use($gid, $pid) {
|
||||
return optional(GroupPost::whereGroupId($gid)->whereStatusId($pid)->first())->id;
|
||||
});
|
||||
}
|
||||
|
||||
public static function membershipsByPid($pid)
|
||||
{
|
||||
return Cache::remember(self::key("mbpid:{$pid}"), 3600, function() use($pid) {
|
||||
return GroupMember::whereProfileId($pid)->pluck('group_id');
|
||||
});
|
||||
}
|
||||
|
||||
public static function config()
|
||||
{
|
||||
return [
|
||||
'enabled' => config('exp.gps') ?? false,
|
||||
'limits' => [
|
||||
'group' => [
|
||||
'max' => 999,
|
||||
'federation' => false,
|
||||
],
|
||||
|
||||
'user' => [
|
||||
'create' => [
|
||||
'new' => true,
|
||||
'max' => 10
|
||||
],
|
||||
'join' => [
|
||||
'max' => 10
|
||||
],
|
||||
'invite' => [
|
||||
'max' => 20
|
||||
]
|
||||
]
|
||||
],
|
||||
'guest' => [
|
||||
'public' => false
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function fetchRemote($url)
|
||||
{
|
||||
// todo: refactor this demo
|
||||
$res = Helpers::fetchFromUrl($url);
|
||||
|
||||
if(!$res || !isset($res['type']) || $res['type'] != 'Group') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group = Group::whereRemoteUrl($url)->first();
|
||||
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$group = new Group;
|
||||
$group->remote_url = $res['url'];
|
||||
$group->name = $res['name'];
|
||||
$group->inbox_url = $res['inbox'];
|
||||
$group->metadata = [
|
||||
'header' => [
|
||||
'url' => $res['icon']['image']['url']
|
||||
]
|
||||
];
|
||||
$group->description = Purify::clean($res['summary']);
|
||||
$group->local = false;
|
||||
$group->save();
|
||||
|
||||
return $group->url();
|
||||
}
|
||||
|
||||
public static function log(
|
||||
string $groupId,
|
||||
string $profileId,
|
||||
string $type = null,
|
||||
array $meta = null,
|
||||
string $itemType = null,
|
||||
string $itemId = null
|
||||
)
|
||||
{
|
||||
// todo: truncate (some) metadata after XX days in cron/queue
|
||||
$log = new GroupInteraction;
|
||||
$log->group_id = $groupId;
|
||||
$log->profile_id = $profileId;
|
||||
$log->type = $type;
|
||||
$log->item_type = $itemType;
|
||||
$log->item_id = $itemId;
|
||||
$log->metadata = $meta;
|
||||
$log->save();
|
||||
}
|
||||
|
||||
public static function getRejoinTimeout($gid, $pid)
|
||||
{
|
||||
$key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid);
|
||||
return Cache::has($key);
|
||||
}
|
||||
|
||||
public static function setRejoinTimeout($gid, $pid)
|
||||
{
|
||||
// todo: allow group admins to manually remove timeout
|
||||
$key = self::key('rejoin-timeout:gid-' . $gid . ':pid-' . $pid);
|
||||
return Cache::put($key, 1, 86400);
|
||||
}
|
||||
|
||||
public static function getMemberInboxes($id)
|
||||
{
|
||||
// todo: cache this, maybe add join/leave methods to this service to handle cache invalidation
|
||||
$group = (new Group)->withoutRelations()->findOrFail($id);
|
||||
if(!$group->local) {
|
||||
return [];
|
||||
}
|
||||
$members = GroupMember::whereGroupId($id)->whereLocalProfile(false)->pluck('profile_id');
|
||||
return Profile::find($members)->map(function($u) {
|
||||
return $u->sharedInbox ?? $u->inbox_url;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public static function getInteractionLimits($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key(":il:{$gid}:{$pid}"), 3600, function() use($gid, $pid) {
|
||||
$limit = GroupLimit::whereGroupId($gid)->whereProfileId($pid)->first();
|
||||
if(!$limit) {
|
||||
return [
|
||||
'limits' => [
|
||||
'can_post' => true,
|
||||
'can_comment' => true,
|
||||
'can_like' => true
|
||||
],
|
||||
'updated_at' => null
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'limits' => $limit->limits,
|
||||
'updated_at' => $limit->updated_at->format('c')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public static function clearInteractionLimits($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key(":il:{$gid}:{$pid}"));
|
||||
}
|
||||
|
||||
public static function canPost($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_post'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function canComment($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_comment'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function canLike($gid, $pid)
|
||||
{
|
||||
$limits = self::getInteractionLimits($gid, $pid);
|
||||
if($limits) {
|
||||
return (bool) $limits['limits']['can_like'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function categories($onlyActive = true)
|
||||
{
|
||||
return Cache::remember(self::key(':categories'), 2678400, function() use($onlyActive) {
|
||||
return GroupCategory::when($onlyActive, function($q, $onlyActive) {
|
||||
return $q->whereActive(true);
|
||||
})
|
||||
->orderBy('order')
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function categoryById($id)
|
||||
{
|
||||
return Cache::remember(self::key(':categorybyid:'.$id), 2678400, function() use($id) {
|
||||
$category = GroupCategory::find($id);
|
||||
if($category) {
|
||||
return [
|
||||
'name' => $category->name,
|
||||
'url' => url("/groups/explore/category/{$category->slug}")
|
||||
];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public static function isMember($gid = false, $pid = false)
|
||||
{
|
||||
if(!$gid || !$pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = self::key("is_member:{$gid}:{$pid}");
|
||||
return Cache::remember($key, 3600, function() use($gid, $pid) {
|
||||
return GroupMember::whereGroupId($gid)
|
||||
->whereProfileId($pid)
|
||||
->whereJoinRequest(false)
|
||||
->exists();
|
||||
});
|
||||
}
|
||||
|
||||
public static function mutualGroups($cid = false, $pid = false, $exclude = [])
|
||||
{
|
||||
if(!$cid || !$pid) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'groups' => []
|
||||
];
|
||||
}
|
||||
|
||||
$self = self::membershipsByPid($cid);
|
||||
$user = self::membershipsByPid($pid);
|
||||
|
||||
if(!$self->count() || !$user->count()) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'groups' => []
|
||||
];
|
||||
}
|
||||
|
||||
$intersect = $self->intersect($user);
|
||||
$count = $intersect->count();
|
||||
$groups = $intersect
|
||||
->values()
|
||||
->filter(function($id) use($exclude) {
|
||||
return !in_array($id, $exclude);
|
||||
})
|
||||
->shuffle()
|
||||
->take(1)
|
||||
->map(function($id) {
|
||||
return self::get($id);
|
||||
});
|
||||
|
||||
return [
|
||||
'count' => $count,
|
||||
'groups' => $groups
|
||||
];
|
||||
}
|
||||
}
|
51
app/Services/Groups/GroupAccountService.php
Normal file
51
app/Services/Groups/GroupAccountService.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupMember;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\GroupService;
|
||||
|
||||
class GroupAccountService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:accounts-v0:';
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
$group = GroupService::get($gid);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account = AccountService::get($pid, true);
|
||||
if(!$account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = self::CACHE_KEY . $gid . ':' . $pid;
|
||||
$account['group'] = Cache::remember($key, 3600, function() use($gid, $pid) {
|
||||
$membership = GroupMember::whereGroupId($gid)->whereProfileId($pid)->first();
|
||||
if(!$membership) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'joined' => $membership->created_at->format('c'),
|
||||
'role' => $membership->role,
|
||||
'local_group' => (bool) $membership->local_group,
|
||||
'local_profile' => (bool) $membership->local_profile,
|
||||
];
|
||||
});
|
||||
return $account;
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
$key = self::CACHE_KEY . $gid . ':' . $pid;
|
||||
return Cache::forget($key);
|
||||
}
|
||||
}
|
311
app/Services/Groups/GroupActivityPubService.php
Normal file
311
app/Services/Groups/GroupActivityPubService.php
Normal file
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupComment;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
use App\Services\ActivityPubFetchService;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Rules\ValidUrl;
|
||||
|
||||
class GroupActivityPubService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:ap:';
|
||||
|
||||
public static function fetchGroup($url, $saveOnFetch = true)
|
||||
{
|
||||
$group = Group::where('remote_url', $url)->first();
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$res = ActivityPubFetchService::get($url);
|
||||
if(!$res) {
|
||||
return $res;
|
||||
}
|
||||
$json = json_decode($res, true);
|
||||
$group = self::validateGroup($json);
|
||||
if(!$group) {
|
||||
return false;
|
||||
}
|
||||
if($saveOnFetch) {
|
||||
return self::storeGroup($group);
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function fetchGroupPost($url, $saveOnFetch = true)
|
||||
{
|
||||
$group = GroupPost::where('remote_url', $url)->first();
|
||||
|
||||
if($group) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
$res = ActivityPubFetchService::get($url);
|
||||
if(!$res) {
|
||||
return 'invalid res';
|
||||
}
|
||||
$json = json_decode($res, true);
|
||||
if(!$json) {
|
||||
return 'invalid json';
|
||||
}
|
||||
if(isset($json['inReplyTo'])) {
|
||||
$comment = self::validateGroupComment($json);
|
||||
return self::storeGroupComment($comment);
|
||||
}
|
||||
|
||||
$group = self::validateGroupPost($json);
|
||||
if($saveOnFetch) {
|
||||
return self::storeGroupPost($group);
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function validateGroup($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Group',
|
||||
'preferredUsername' => 'required',
|
||||
'name' => 'required',
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'inbox' => ['required', 'url', new ValidUrl],
|
||||
'outbox' => ['required', 'url', new ValidUrl],
|
||||
'followers' => ['required', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'summary' => 'sometimes',
|
||||
'publicKey' => 'required',
|
||||
'publicKey.id' => 'required',
|
||||
'publicKey.owner' => ['required', 'url', 'same:id', new ValidUrl],
|
||||
'publicKey.publicKeyPem' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function validateGroupPost($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Page,Note',
|
||||
'to' => 'required|array',
|
||||
'to.*' => ['required', 'url', new ValidUrl],
|
||||
'cc' => 'sometimes|array',
|
||||
'cc.*' => ['sometimes', 'url', new ValidUrl],
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'name' => 'sometimes',
|
||||
'target' => 'sometimes',
|
||||
'audience' => 'sometimes',
|
||||
'inReplyTo' => 'sometimes',
|
||||
'content' => 'sometimes',
|
||||
'mediaType' => 'sometimes',
|
||||
'sensitive' => 'sometimes',
|
||||
'attachment' => 'sometimes',
|
||||
'published' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function validateGroupComment($obj)
|
||||
{
|
||||
$validator = Validator::make($obj, [
|
||||
'@context' => 'required',
|
||||
'id' => ['required', 'url', new ValidUrl],
|
||||
'type' => 'required|in:Note',
|
||||
'to' => 'required|array',
|
||||
'to.*' => ['required', 'url', new ValidUrl],
|
||||
'cc' => 'sometimes|array',
|
||||
'cc.*' => ['sometimes', 'url', new ValidUrl],
|
||||
'url' => ['sometimes', 'url', new ValidUrl],
|
||||
'attributedTo' => 'required',
|
||||
'name' => 'sometimes',
|
||||
'target' => 'sometimes',
|
||||
'audience' => 'sometimes',
|
||||
'inReplyTo' => 'sometimes',
|
||||
'content' => 'sometimes',
|
||||
'mediaType' => 'sometimes',
|
||||
'sensitive' => 'sometimes',
|
||||
'published' => 'required',
|
||||
]);
|
||||
|
||||
if($validator->fails()) {
|
||||
return $validator->errors();
|
||||
return false;
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public static function getGroupFromPostActivity($groupPost)
|
||||
{
|
||||
if(isset($groupPost['audience']) && is_string($groupPost['audience'])) {
|
||||
return $groupPost['audience'];
|
||||
}
|
||||
|
||||
if(
|
||||
isset(
|
||||
$groupPost['target'],
|
||||
$groupPost['target']['type'],
|
||||
$groupPost['target']['attributedTo']
|
||||
) && $groupPost['target']['type'] == 'Collection'
|
||||
) {
|
||||
return $groupPost['target']['attributedTo'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getActorFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['attributedTo'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$field = $groupPost['attributedTo'];
|
||||
|
||||
if(is_string($field)) {
|
||||
return $field;
|
||||
}
|
||||
|
||||
if(is_array($field) && count($field) === 1) {
|
||||
if(
|
||||
isset(
|
||||
$field[0]['id'],
|
||||
$field[0]['type']
|
||||
) &&
|
||||
$field[0]['type'] === 'Person' &&
|
||||
is_string($field[0]['id'])
|
||||
) {
|
||||
return $field[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getCaptionFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['name']) && isset($groupPost['content'])) {
|
||||
return Purify::clean(strip_tags($groupPost['content']));
|
||||
}
|
||||
|
||||
if(isset($groupPost['name'], $groupPost['content'])) {
|
||||
return Purify::clean(strip_tags($groupPost['name'])) . Purify::clean(strip_tags($groupPost['content']));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSensitiveFromPostActivity($groupPost)
|
||||
{
|
||||
if(!isset($groupPost['sensitive'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(isset($groupPost['sensitive']) && !is_bool($groupPost['sensitive'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return boolval($groupPost['sensitive']);
|
||||
}
|
||||
|
||||
public static function storeGroup($activity)
|
||||
{
|
||||
$group = new Group;
|
||||
$group->profile_id = null;
|
||||
$group->category_id = 1;
|
||||
$group->name = $activity['name'] ?? 'Untitled Group';
|
||||
$group->description = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
|
||||
$group->is_private = false;
|
||||
$group->local_only = false;
|
||||
$group->metadata = [];
|
||||
$group->local = false;
|
||||
$group->remote_url = $activity['id'];
|
||||
$group->inbox_url = $activity['inbox'];
|
||||
$group->activitypub = true;
|
||||
$group->save();
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
public static function storeGroupPost($groupPost)
|
||||
{
|
||||
$groupUrl = self::getGroupFromPostActivity($groupPost);
|
||||
if(!$groupUrl) {
|
||||
return;
|
||||
}
|
||||
$group = self::fetchGroup($groupUrl, true);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
$actorUrl = self::getActorFromPostActivity($groupPost);
|
||||
$actor = Helpers::profileFetch($actorUrl);
|
||||
$caption = self::getCaptionFromPostActivity($groupPost);
|
||||
$sensitive = self::getSensitiveFromPostActivity($groupPost);
|
||||
$model = GroupPost::firstOrCreate(
|
||||
[
|
||||
'remote_url' => $groupPost['id'],
|
||||
], [
|
||||
'group_id' => $group->id,
|
||||
'profile_id' => $actor->id,
|
||||
'type' => 'text',
|
||||
'caption' => $caption,
|
||||
'visibility' => 'public',
|
||||
'is_nsfw' => $sensitive,
|
||||
]
|
||||
);
|
||||
return $model;
|
||||
}
|
||||
|
||||
public static function storeGroupComment($groupPost)
|
||||
{
|
||||
$groupUrl = self::getGroupFromPostActivity($groupPost);
|
||||
if(!$groupUrl) {
|
||||
return;
|
||||
}
|
||||
$group = self::fetchGroup($groupUrl, true);
|
||||
if(!$group) {
|
||||
return;
|
||||
}
|
||||
$actorUrl = self::getActorFromPostActivity($groupPost);
|
||||
$actor = Helpers::profileFetch($actorUrl);
|
||||
$caption = self::getCaptionFromPostActivity($groupPost);
|
||||
$sensitive = self::getSensitiveFromPostActivity($groupPost);
|
||||
$parentPost = self::fetchGroupPost($groupPost['inReplyTo']);
|
||||
$model = GroupComment::firstOrCreate(
|
||||
[
|
||||
'remote_url' => $groupPost['id'],
|
||||
], [
|
||||
'group_id' => $group->id,
|
||||
'profile_id' => $actor->id,
|
||||
'status_id' => $parentPost->id,
|
||||
'type' => 'text',
|
||||
'caption' => $caption,
|
||||
'visibility' => 'public',
|
||||
'is_nsfw' => $sensitive,
|
||||
'local' => $actor->private_key != null
|
||||
]
|
||||
);
|
||||
return $model;
|
||||
}
|
||||
}
|
50
app/Services/Groups/GroupCommentService.php
Normal file
50
app/Services/Groups/GroupCommentService.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\GroupComment;
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
|
||||
class GroupCommentService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:comment:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY . $gid . ':' . $pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function() use($gid, $pid) {
|
||||
$gp = GroupComment::whereGroupId($gid)->find($pid);
|
||||
|
||||
if(!$gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = 'group:post:comment';
|
||||
$res['url'] = $gp->url();
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
}
|
95
app/Services/Groups/GroupFeedService.php
Normal file
95
app/Services/Groups/GroupFeedService.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Profile;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupCategory;
|
||||
use App\Models\GroupMember;
|
||||
use App\Models\GroupPost;
|
||||
use App\Models\GroupInteraction;
|
||||
use App\Models\GroupLimit;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Cache;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class GroupFeedService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:feed:';
|
||||
const FEED_LIMIT = 400;
|
||||
|
||||
public static function get($gid, $start = 0, $stop = 10)
|
||||
{
|
||||
if($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_KEY . $gid, $start, $stop);
|
||||
}
|
||||
|
||||
public static function getRankedMaxId($gid, $start = null, $limit = 10)
|
||||
{
|
||||
if(!$start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, $start, '-inf', [
|
||||
'withscores' => true,
|
||||
'limit' => [1, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function getRankedMinId($gid, $end = null, $limit = 10)
|
||||
{
|
||||
if(!$end) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys(Redis::zrevrangebyscore(self::CACHE_KEY . $gid, '+inf', $end, [
|
||||
'withscores' => true,
|
||||
'limit' => [0, $limit]
|
||||
]));
|
||||
}
|
||||
|
||||
public static function add($gid, $val)
|
||||
{
|
||||
if(self::count($gid) > self::FEED_LIMIT) {
|
||||
if(config('database.redis.client') === 'phpredis') {
|
||||
Redis::zpopmin(self::CACHE_KEY . $gid);
|
||||
}
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_KEY . $gid, $val, $val);
|
||||
}
|
||||
|
||||
public static function rem($gid, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_KEY . $gid, $val);
|
||||
}
|
||||
|
||||
public static function del($gid, $val)
|
||||
{
|
||||
return self::rem($gid, $val);
|
||||
}
|
||||
|
||||
public static function count($gid)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_KEY . $gid);
|
||||
}
|
||||
|
||||
public static function warmCache($gid, $force = false, $limit = 100)
|
||||
{
|
||||
if(self::count($gid) == 0 || $force == true) {
|
||||
Redis::del(self::CACHE_KEY . $gid);
|
||||
$ids = GroupPost::whereGroupId($gid)
|
||||
->orderByDesc('id')
|
||||
->limit($limit)
|
||||
->pluck('id');
|
||||
foreach($ids as $id) {
|
||||
self::add($gid, $id);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
28
app/Services/Groups/GroupHashtagService.php
Normal file
28
app/Services/Groups/GroupHashtagService.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\GroupHashtag;
|
||||
use App\Models\GroupPostHashtag;
|
||||
|
||||
class GroupHashtagService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups-v1:hashtags:';
|
||||
|
||||
public static function get($id)
|
||||
{
|
||||
return Cache::remember(self::CACHE_KEY . $id, 3600, function() use($id) {
|
||||
$tag = GroupHashtag::find($id);
|
||||
if(!$tag) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'slug' => Str::slug($tag->name),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
114
app/Services/Groups/GroupMediaService.php
Normal file
114
app/Services/Groups/GroupMediaService.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\GroupMedia;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\HashidService;
|
||||
|
||||
class GroupMediaService
|
||||
{
|
||||
const CACHE_KEY = 'groups:media:';
|
||||
|
||||
public static function path($gid, $pid, $sid = false)
|
||||
{
|
||||
if(!$gid || !$pid) {
|
||||
return;
|
||||
}
|
||||
$groupHashid = HashidService::encode($gid);
|
||||
$monthHash = HashidService::encode(date('Y').date('n'));
|
||||
$pid = HashidService::encode($pid);
|
||||
$sid = $sid ? HashidService::encode($sid) : false;
|
||||
$path = $sid ?
|
||||
"public/g1/{$groupHashid}/{$pid}/{$monthHash}/{$sid}" :
|
||||
"public/g1/{$groupHashid}/{$pid}/{$monthHash}";
|
||||
return $path;
|
||||
}
|
||||
|
||||
public static function get($statusId)
|
||||
{
|
||||
return Cache::remember(self::CACHE_KEY.$statusId, 21600, function() use($statusId) {
|
||||
$media = GroupMedia::whereStatusId($statusId)->orderBy('order')->get();
|
||||
if(!$media) {
|
||||
return [];
|
||||
}
|
||||
$medias = $media->map(function($media) {
|
||||
return [
|
||||
'id' => (string) $media->id,
|
||||
'type' => 'Document',
|
||||
'url' => $media->url(),
|
||||
'preview_url' => $media->url(),
|
||||
'remote_url' => $media->url,
|
||||
'description' => $media->cw_summary,
|
||||
'blurhash' => $media->blurhash ?? 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay'
|
||||
];
|
||||
});
|
||||
return $medias->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getMastodon($id)
|
||||
{
|
||||
$media = self::get($id);
|
||||
if(!$media) {
|
||||
return [];
|
||||
}
|
||||
$medias = collect($media)
|
||||
->map(function($media) {
|
||||
$mime = $media['mime'] ? explode('/', $media['mime']) : false;
|
||||
unset(
|
||||
$media['optimized_url'],
|
||||
$media['license'],
|
||||
$media['is_nsfw'],
|
||||
$media['orientation'],
|
||||
$media['filter_name'],
|
||||
$media['filter_class'],
|
||||
$media['mime'],
|
||||
$media['hls_manifest']
|
||||
);
|
||||
|
||||
$media['type'] = $mime ? strtolower($mime[0]) : 'unknown';
|
||||
return $media;
|
||||
})
|
||||
->filter(function($m) {
|
||||
return $m && isset($m['url']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $medias->toArray();
|
||||
}
|
||||
|
||||
public static function del($statusId)
|
||||
{
|
||||
return Cache::forget(self::CACHE_KEY . $statusId);
|
||||
}
|
||||
|
||||
public static function activitypub($statusId)
|
||||
{
|
||||
$status = self::get($statusId);
|
||||
if(!$status) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($status)->map(function($s) {
|
||||
$license = isset($s['license']) && $s['license']['title'] ? $s['license']['title'] : null;
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $s['mime'],
|
||||
'url' => $s['url'],
|
||||
'name' => $s['description'],
|
||||
'summary' => $s['description'],
|
||||
'blurhash' => $s['blurhash'],
|
||||
'license' => $license
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
83
app/Services/Groups/GroupPostService.php
Normal file
83
app/Services/Groups/GroupPostService.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\GroupPostTransformer;
|
||||
|
||||
class GroupPostService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:groups:post:';
|
||||
|
||||
public static function key($gid, $pid)
|
||||
{
|
||||
return self::CACHE_KEY . $gid . ':' . $pid;
|
||||
}
|
||||
|
||||
public static function get($gid, $pid)
|
||||
{
|
||||
return Cache::remember(self::key($gid, $pid), 604800, function() use($gid, $pid) {
|
||||
$gp = GroupPost::whereGroupId($gid)->find($pid);
|
||||
|
||||
if(!$gp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($gp, new GroupPostTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
$res['pf_type'] = $gp['type'];
|
||||
$res['url'] = $gp->url();
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
//$status['account']['url'] = url("/groups/{$gp['group_id']}/user/{$status['account']['id']}");
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($gid, $pid)
|
||||
{
|
||||
return Cache::forget(self::key($gid, $pid));
|
||||
}
|
||||
|
||||
public function getStatus(Request $request)
|
||||
{
|
||||
$gid = $request->input('gid');
|
||||
$sid = $request->input('sid');
|
||||
$pid = optional($request->user())->profile_id ?? false;
|
||||
|
||||
$group = Group::findOrFail($gid);
|
||||
|
||||
if($group->is_private) {
|
||||
abort_if(!$group->isMember($pid), 404);
|
||||
}
|
||||
|
||||
$gp = GroupPost::whereGroupId($group->id)->whereId($sid)->firstOrFail();
|
||||
|
||||
$status = GroupPostService::get($gp['group_id'], $gp['id']);
|
||||
if(!$status) {
|
||||
return false;
|
||||
}
|
||||
$status['reply_count'] = $gp['reply_count'];
|
||||
$status['favourited'] = (bool) GroupsLikeService::liked($pid, $gp['id']);
|
||||
$status['favourites_count'] = GroupsLikeService::count($gp['id']);
|
||||
$status['pf_type'] = $gp['type'];
|
||||
$status['visibility'] = 'public';
|
||||
$status['url'] = $gp->url();
|
||||
$status['account']['url'] = url("/groups/{$gp->group_id}/user/{$gp->profile_id}");
|
||||
|
||||
// if($gp['type'] == 'poll') {
|
||||
// $status['poll'] = PollService::get($status['id']);
|
||||
// }
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
85
app/Services/Groups/GroupsLikeService.php
Normal file
85
app/Services/Groups/GroupsLikeService.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Groups;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Models\GroupLike;
|
||||
|
||||
class GroupsLikeService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:group-likes:ids:';
|
||||
const CACHE_SET_KEY = 'pf:services:group-likes:set:';
|
||||
const CACHE_POST_KEY = 'pf:services:group-likes:count:';
|
||||
|
||||
public static function add($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
Cache::increment(self::CACHE_POST_KEY . $statusId);
|
||||
//Cache::forget('pf:services:likes:liked_by:'.$statusId);
|
||||
self::setAdd($profileId, $statusId);
|
||||
return Cache::put($key, true, 86400);
|
||||
}
|
||||
|
||||
public static function setAdd($profileId, $statusId)
|
||||
{
|
||||
if(self::setCount($profileId) > 400) {
|
||||
Redis::zpopmin(self::CACHE_SET_KEY . $profileId);
|
||||
}
|
||||
|
||||
return Redis::zadd(self::CACHE_SET_KEY . $profileId, $statusId, $statusId);
|
||||
}
|
||||
|
||||
public static function setCount($id)
|
||||
{
|
||||
return Redis::zcard(self::CACHE_SET_KEY . $id);
|
||||
}
|
||||
|
||||
public static function setRem($profileId, $val)
|
||||
{
|
||||
return Redis::zrem(self::CACHE_SET_KEY . $profileId, $val);
|
||||
}
|
||||
|
||||
public static function get($profileId, $start = 0, $stop = 10)
|
||||
{
|
||||
if($stop > 100) {
|
||||
$stop = 100;
|
||||
}
|
||||
|
||||
return Redis::zrevrange(self::CACHE_SET_KEY . $profileId, $start, $stop);
|
||||
}
|
||||
|
||||
public static function remove($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
Cache::decrement(self::CACHE_POST_KEY . $statusId);
|
||||
//Cache::forget('pf:services:likes:liked_by:'.$statusId);
|
||||
self::setRem($profileId, $statusId);
|
||||
return Cache::put($key, false, 86400);
|
||||
}
|
||||
|
||||
public static function liked($profileId, $statusId)
|
||||
{
|
||||
$key = self::CACHE_KEY . $profileId . ':' . $statusId;
|
||||
return Cache::remember($key, 900, function() use($profileId, $statusId) {
|
||||
return GroupLike::whereProfileId($profileId)->whereStatusId($statusId)->exists();
|
||||
});
|
||||
}
|
||||
|
||||
public static function likedBy($status)
|
||||
{
|
||||
$empty = [
|
||||
'username' => null,
|
||||
'others' => false
|
||||
];
|
||||
|
||||
return $empty;
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
return Cache::get(self::CACHE_POST_KEY . $id, 0);
|
||||
}
|
||||
|
||||
}
|
48
app/Services/UserStorageService.php
Normal file
48
app/Services/UserStorageService.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Media;
|
||||
use App\User;
|
||||
|
||||
class UserStorageService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:user-storage:byId:';
|
||||
|
||||
public static function get($id)
|
||||
{
|
||||
$user = User::find($id);
|
||||
if (! $user || $user->status) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($user->storage_used_updated_at) {
|
||||
return (int) $user->storage_used;
|
||||
}
|
||||
$updatedVal = self::calculateStorageUsed($id);
|
||||
$user->storage_used = $updatedVal;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
return $user->storage_used;
|
||||
}
|
||||
|
||||
public static function calculateStorageUsed($id)
|
||||
{
|
||||
return (int) floor(Media::whereUserId($id)->sum('size') / 1000);
|
||||
}
|
||||
|
||||
public static function recalculateUpdateStorageUsed($id)
|
||||
{
|
||||
$user = User::find($id);
|
||||
if (! $user || $user->status) {
|
||||
return;
|
||||
}
|
||||
$updatedVal = (int) floor(Media::whereUserId($id)->sum('size') / 1000);
|
||||
$user->storage_used = $updatedVal;
|
||||
$user->storage_used_updated_at = now();
|
||||
$user->save();
|
||||
|
||||
return $updatedVal;
|
||||
}
|
||||
}
|
59
app/Transformer/Api/GroupPostTransformer.php
Normal file
59
app/Transformer/Api/GroupPostTransformer.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\Groups\GroupMediaService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusLabelService;
|
||||
use App\Services\StatusMentionService;
|
||||
use App\Services\PollService;
|
||||
use App\Models\CustomEmoji;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Purify;
|
||||
|
||||
class GroupPostTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform($status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'gid' => $status->group_id ? (string) $status->group_id : null,
|
||||
'url' => '/groups/' . $status->group_id . '/p/' . $status->id,
|
||||
'content' => $status->caption,
|
||||
'content_text' => $status->caption,
|
||||
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'reblogs_count' => $status->reblogs_count ?? 0,
|
||||
'favourites_count' => $status->likes_count ?? 0,
|
||||
'reblogged' => null,
|
||||
'favourited' => null,
|
||||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'language' => null,
|
||||
'pf_type' => $status->type,
|
||||
'reply_count' => (int) $status->reply_count ?? 0,
|
||||
'comments_disabled' => (bool) $status->comments_disabled,
|
||||
'thread' => false,
|
||||
'media_attachments' => GroupMediaService::get($status->id),
|
||||
'replies' => [],
|
||||
'parent' => [],
|
||||
'place' => null,
|
||||
'local' => (bool) !$status->remote_url,
|
||||
'account' => AccountService::get($status->profile_id, true),
|
||||
'poll' => [],
|
||||
];
|
||||
}
|
||||
}
|
47
app/User.php
47
app/User.php
|
@ -2,32 +2,34 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use App\Services\AvatarService;
|
||||
use App\Util\RateLimit\User as UserRateLimit;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use App\Util\RateLimit\User as UserRateLimit;
|
||||
use App\Services\AvatarService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use NotificationChannels\Expo\ExpoPushToken;
|
||||
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Notifiable, SoftDeletes, HasApiTokens, UserRateLimit, HasFactory, HasPushSubscriptions;
|
||||
use HasApiTokens, HasFactory, HasPushSubscriptions, Notifiable, SoftDeletes, UserRateLimit;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'deleted_at' => 'datetime',
|
||||
'email_verified_at' => 'datetime',
|
||||
'2fa_setup_at' => 'datetime',
|
||||
'last_active_at' => 'datetime',
|
||||
'expo_token' => ExpoPushToken::class
|
||||
];
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'deleted_at' => 'datetime',
|
||||
'email_verified_at' => 'datetime',
|
||||
'2fa_setup_at' => 'datetime',
|
||||
'last_active_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
@ -42,7 +44,12 @@ class User extends Authenticatable
|
|||
'app_register_ip',
|
||||
'email_verified_at',
|
||||
'last_active_at',
|
||||
'register_source'
|
||||
'register_source',
|
||||
'expo_token',
|
||||
'notify_like',
|
||||
'notify_follow',
|
||||
'notify_mention',
|
||||
'notify_comment',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -54,7 +61,7 @@ class User extends Authenticatable
|
|||
'email', 'password', 'is_admin', 'remember_token',
|
||||
'email_verified_at', '2fa_enabled', '2fa_secret',
|
||||
'2fa_backup_codes', '2fa_setup_at', 'deleted_at',
|
||||
'updated_at'
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function profile()
|
||||
|
@ -97,7 +104,7 @@ class User extends Authenticatable
|
|||
|
||||
public function storageUsedKey()
|
||||
{
|
||||
return 'profile:storage:used:' . $this->id;
|
||||
return 'profile:storage:used:'.$this->id;
|
||||
}
|
||||
|
||||
public function accountLog()
|
||||
|
@ -112,14 +119,14 @@ class User extends Authenticatable
|
|||
|
||||
public function avatarUrl()
|
||||
{
|
||||
if(!$this->profile_id || $this->status) {
|
||||
return config('app.url') . '/storage/avatars/default.jpg';
|
||||
if (! $this->profile_id || $this->status) {
|
||||
return config('app.url').'/storage/avatars/default.jpg';
|
||||
}
|
||||
|
||||
return AvatarService::get($this->profile_id);
|
||||
}
|
||||
|
||||
public function routeNotificationForExpo(): ?ExpoPushToken
|
||||
public function routeNotificationForExpo()
|
||||
{
|
||||
return $this->expo_token;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ use Cache;
|
|||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\Uri\Exceptions\UriException;
|
||||
use League\Uri\Uri;
|
||||
use Purify;
|
||||
use Validator;
|
||||
|
||||
|
@ -153,61 +155,74 @@ class Helpers
|
|||
return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
|
||||
}
|
||||
|
||||
public static function validateUrl($url)
|
||||
public static function validateUrl($url = null, $disableDNSCheck = false)
|
||||
{
|
||||
if (is_array($url)) {
|
||||
if (is_array($url) && ! empty($url)) {
|
||||
$url = $url[0];
|
||||
}
|
||||
if (! $url || strlen($url) === 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$uri = Uri::new($url);
|
||||
|
||||
$hash = hash('sha256', $url);
|
||||
$key = "helpers:url:valid:sha256-{$hash}";
|
||||
if (! $uri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($uri->getScheme() !== 'https') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = $uri->getHost();
|
||||
|
||||
if (! $host || $host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! str_contains($host, '.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid = Cache::remember($key, 900, function () use ($url) {
|
||||
$localhosts = [
|
||||
'127.0.0.1', 'localhost', '::1',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
'broadcasthost',
|
||||
'ip6-localhost',
|
||||
'ip6-loopback',
|
||||
];
|
||||
|
||||
if (strtolower(mb_substr($url, 0, 8)) !== 'https://') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (substr_count($url, '://') !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mb_substr($url, 0, 8) !== 'https://') {
|
||||
$url = 'https://'.substr($url, 8);
|
||||
}
|
||||
|
||||
$valid = filter_var($url, FILTER_VALIDATE_URL);
|
||||
|
||||
if (! $valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = parse_url($valid, PHP_URL_HOST);
|
||||
|
||||
if (in_array($host, $localhosts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config('security.url.verify_dns')) {
|
||||
if (DomainService::hasValidDns($host) === false) {
|
||||
if ($disableDNSCheck !== true && app()->environment() === 'production' && (bool) config('security.url.verify_dns')) {
|
||||
$hash = hash('sha256', $host);
|
||||
$key = "helpers:url:valid-dns:sha256-{$hash}";
|
||||
$domainValidDns = Cache::remember($key, 14440, function () use ($host) {
|
||||
return DomainService::hasValidDns($host);
|
||||
});
|
||||
if (! $domainValidDns) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (app()->environment() === 'production') {
|
||||
if ($disableDNSCheck !== true && app()->environment() === 'production') {
|
||||
$bannedInstances = InstanceService::getBannedDomains();
|
||||
if (in_array($host, $bannedInstances)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
});
|
||||
|
||||
return $valid;
|
||||
return $uri->toString();
|
||||
} catch (UriException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateLocalUrl($url)
|
||||
|
@ -215,7 +230,12 @@ class Helpers
|
|||
$url = self::validateUrl($url);
|
||||
if ($url == true) {
|
||||
$domain = config('pixelfed.domain.app');
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
$uri = Uri::new($url);
|
||||
$host = $uri->getHost();
|
||||
if (! $host || empty($host)) {
|
||||
return false;
|
||||
}
|
||||
$url = strtolower($domain) === strtolower($host) ? $url : false;
|
||||
|
||||
return $url;
|
||||
|
@ -858,6 +878,11 @@ class Helpers
|
|||
return self::profileFirstOrNew($url);
|
||||
}
|
||||
|
||||
public static function getSignedFetch($url)
|
||||
{
|
||||
return ActivityPubFetchService::get($url);
|
||||
}
|
||||
|
||||
public static function sendSignedObject($profile, $url, $body)
|
||||
{
|
||||
if (app()->environment() !== 'production') {
|
||||
|
|
|
@ -2,146 +2,159 @@
|
|||
|
||||
namespace App\Util\ActivityPub;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Models\InstanceActor;
|
||||
use App\Profile;
|
||||
use \DateTime;
|
||||
use Cache;
|
||||
use DateTime;
|
||||
|
||||
class HttpSignature {
|
||||
class HttpSignature
|
||||
{
|
||||
/*
|
||||
* source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
|
||||
* thanks aaronpk!
|
||||
*/
|
||||
|
||||
/*
|
||||
* source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
|
||||
* thanks aaronpk!
|
||||
*/
|
||||
public static function sign(Profile $profile, $url, $body = false, $addlHeaders = [])
|
||||
{
|
||||
if ($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$user = $profile;
|
||||
$headers = self::_headersToSign($url, $body ? $digest : false);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
$key = openssl_pkey_get_private($user->private_key);
|
||||
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
$signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
|
||||
unset($headers['(request-target)']);
|
||||
$headers['Signature'] = $signatureHeader;
|
||||
|
||||
public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) {
|
||||
if($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$user = $profile;
|
||||
$headers = self::_headersToSign($url, $body ? $digest : false);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
$key = openssl_pkey_get_private($user->private_key);
|
||||
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
$signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
|
||||
unset($headers['(request-target)']);
|
||||
$headers['Signature'] = $signatureHeader;
|
||||
|
||||
return self::_headersToCurlArray($headers);
|
||||
}
|
||||
|
||||
public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
|
||||
{
|
||||
$keyId = config('app.url') . '/i/actor#main-key';
|
||||
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() {
|
||||
return InstanceActor::first()->private_key;
|
||||
});
|
||||
if($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$headers = self::_headersToSign($url, $body ? $digest : false, $method);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
$signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
|
||||
unset($headers['(request-target)']);
|
||||
$headers['Signature'] = $signatureHeader;
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public static function parseSignatureHeader($signature) {
|
||||
$parts = explode(',', $signature);
|
||||
$signatureData = [];
|
||||
|
||||
foreach($parts as $part) {
|
||||
if(preg_match('/(.+)="(.+)"/', $part, $match)) {
|
||||
$signatureData[$match[1]] = $match[2];
|
||||
}
|
||||
return self::_headersToCurlArray($headers);
|
||||
}
|
||||
|
||||
if(!isset($signatureData['keyId'])) {
|
||||
return [
|
||||
'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData))
|
||||
];
|
||||
public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
|
||||
{
|
||||
$keyId = config('app.url').'/i/actor#main-key';
|
||||
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
|
||||
return InstanceActor::first()->private_key;
|
||||
});
|
||||
if ($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$headers = self::_headersToSign($url, $body ? $digest : false, $method);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
$signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
|
||||
unset($headers['(request-target)']);
|
||||
$headers['Signature'] = $signatureHeader;
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
|
||||
return [
|
||||
'error' => 'keyId is not a URL: '.$signatureData['keyId']
|
||||
];
|
||||
public static function parseSignatureHeader($signature)
|
||||
{
|
||||
$parts = explode(',', $signature);
|
||||
$signatureData = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (preg_match('/(.+)="(.+)"/', $part, $match)) {
|
||||
$signatureData[$match[1]] = $match[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($signatureData['keyId'])) {
|
||||
return [
|
||||
'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)),
|
||||
];
|
||||
}
|
||||
|
||||
if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
|
||||
return [
|
||||
'error' => 'keyId is not a URL: '.$signatureData['keyId'],
|
||||
];
|
||||
}
|
||||
|
||||
if (! Helpers::validateUrl($signatureData['keyId'])) {
|
||||
return [
|
||||
'error' => 'keyId is not a URL: '.$signatureData['keyId'],
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) {
|
||||
return [
|
||||
'error' => 'Signature is missing headers or signature parts',
|
||||
];
|
||||
}
|
||||
|
||||
return $signatureData;
|
||||
}
|
||||
|
||||
if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
|
||||
return [
|
||||
'error' => 'Signature is missing headers or signature parts'
|
||||
];
|
||||
public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
|
||||
{
|
||||
$digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
|
||||
$headersToSign = [];
|
||||
foreach (explode(' ', $signatureData['headers']) as $h) {
|
||||
if ($h == '(request-target)') {
|
||||
$headersToSign[$h] = 'post '.$path;
|
||||
} elseif ($h == 'digest') {
|
||||
$headersToSign[$h] = $digest;
|
||||
} elseif (isset($inputHeaders[$h][0])) {
|
||||
$headersToSign[$h] = $inputHeaders[$h][0];
|
||||
}
|
||||
}
|
||||
$signingString = self::_headersToSigningString($headersToSign);
|
||||
|
||||
$verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
|
||||
|
||||
return [$verified, $signingString];
|
||||
}
|
||||
|
||||
return $signatureData;
|
||||
}
|
||||
|
||||
public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) {
|
||||
$digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
|
||||
$headersToSign = [];
|
||||
foreach(explode(' ',$signatureData['headers']) as $h) {
|
||||
if($h == '(request-target)') {
|
||||
$headersToSign[$h] = 'post '.$path;
|
||||
} elseif($h == 'digest') {
|
||||
$headersToSign[$h] = $digest;
|
||||
} elseif(isset($inputHeaders[$h][0])) {
|
||||
$headersToSign[$h] = $inputHeaders[$h][0];
|
||||
}
|
||||
}
|
||||
$signingString = self::_headersToSigningString($headersToSign);
|
||||
|
||||
$verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
|
||||
|
||||
return [$verified, $signingString];
|
||||
}
|
||||
|
||||
private static function _headersToSigningString($headers) {
|
||||
return implode("\n", array_map(function($k, $v){
|
||||
return strtolower($k).': '.$v;
|
||||
}, array_keys($headers), $headers));
|
||||
}
|
||||
|
||||
private static function _headersToCurlArray($headers) {
|
||||
return array_map(function($k, $v){
|
||||
return "$k: $v";
|
||||
}, array_keys($headers), $headers);
|
||||
}
|
||||
|
||||
private static function _digest($body) {
|
||||
if(is_array($body)) {
|
||||
$body = json_encode($body);
|
||||
}
|
||||
return base64_encode(hash('sha256', $body, true));
|
||||
}
|
||||
|
||||
protected static function _headersToSign($url, $digest = false, $method = 'post') {
|
||||
$date = new DateTime('UTC');
|
||||
|
||||
if(!in_array($method, ['post', 'get'])) {
|
||||
throw new \Exception('Invalid method used to sign headers in HttpSignature');
|
||||
}
|
||||
$headers = [
|
||||
'(request-target)' => $method . ' '.parse_url($url, PHP_URL_PATH),
|
||||
'Host' => parse_url($url, PHP_URL_HOST),
|
||||
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
|
||||
];
|
||||
|
||||
if($digest) {
|
||||
$headers['Digest'] = 'SHA-256='.$digest;
|
||||
private static function _headersToSigningString($headers)
|
||||
{
|
||||
return implode("\n", array_map(function ($k, $v) {
|
||||
return strtolower($k).': '.$v;
|
||||
}, array_keys($headers), $headers));
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
private static function _headersToCurlArray($headers)
|
||||
{
|
||||
return array_map(function ($k, $v) {
|
||||
return "$k: $v";
|
||||
}, array_keys($headers), $headers);
|
||||
}
|
||||
|
||||
private static function _digest($body)
|
||||
{
|
||||
if (is_array($body)) {
|
||||
$body = json_encode($body);
|
||||
}
|
||||
|
||||
return base64_encode(hash('sha256', $body, true));
|
||||
}
|
||||
|
||||
protected static function _headersToSign($url, $digest = false, $method = 'post')
|
||||
{
|
||||
$date = new DateTime('UTC');
|
||||
|
||||
if (! in_array($method, ['post', 'get'])) {
|
||||
throw new \Exception('Invalid method used to sign headers in HttpSignature');
|
||||
}
|
||||
$headers = [
|
||||
'(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH),
|
||||
'Host' => parse_url($url, PHP_URL_HOST),
|
||||
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
|
||||
];
|
||||
|
||||
if ($digest) {
|
||||
$headers['Digest'] = 'SHA-256='.$digest;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use Illuminate\Support\Str;
|
|||
|
||||
class Config
|
||||
{
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.8';
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.9';
|
||||
|
||||
public static function get()
|
||||
{
|
||||
|
@ -97,6 +97,7 @@ class Config
|
|||
],
|
||||
],
|
||||
'hls' => $hls,
|
||||
'groups' => (bool) config('groups.enabled'),
|
||||
],
|
||||
];
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"doctrine/dbal": "^3.0",
|
||||
"intervention/image": "^2.4",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laravel-notification-channels/expo": "^1.3.0|^2.0",
|
||||
"laravel-notification-channels/expo": "~1.3.0|~2.0.0",
|
||||
"laravel-notification-channels/webpush": "^8.0",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/helpers": "^1.1",
|
||||
|
@ -29,6 +29,7 @@
|
|||
"laravel/ui": "^4.2",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/iso3166": "^2.1|^4.0",
|
||||
"league/uri": "^7.4",
|
||||
"pbmedia/laravel-ffmpeg": "^8.0",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
"pixelfed/fractal": "^0.18.0",
|
||||
|
|
41
composer.lock
generated
41
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fbadeaf1fbbd9e0f64feaa1433ca7dd0",
|
||||
"content-hash": "fecc0efcc40880a422690feefedef584",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
@ -2249,29 +2249,34 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel-notification-channels/expo",
|
||||
"version": "1.3.1",
|
||||
"version": "v2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel-notification-channels/expo.git",
|
||||
"reference": "d718a89dfc4997aba69b673f5db416ac833188e9"
|
||||
"reference": "29d038b6409077ac4c671cc5587a4dc7986260b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel-notification-channels/expo/zipball/d718a89dfc4997aba69b673f5db416ac833188e9",
|
||||
"reference": "d718a89dfc4997aba69b673f5db416ac833188e9",
|
||||
"url": "https://api.github.com/repos/laravel-notification-channels/expo/zipball/29d038b6409077ac4c671cc5587a4dc7986260b0",
|
||||
"reference": "29d038b6409077ac4c671cc5587a4dc7986260b0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "~6.0 || ~7.0",
|
||||
"illuminate/notifications": "~6.0 || ~7.0 || ~8.0 || ~9.0 || ~10.0 || ~11.0",
|
||||
"illuminate/support": "~6.0 || ~7.0 || ~8.0 || ~9.0 || ~10.0 || ~11.0",
|
||||
"php": ">=7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3"
|
||||
"guzzlehttp/guzzle": "^7.1",
|
||||
"illuminate/contracts": "^11.0",
|
||||
"illuminate/notifications": "^11.0",
|
||||
"illuminate/support": "^11.0",
|
||||
"php": "~8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^6.18",
|
||||
"phpunit/phpunit": "^8.0"
|
||||
"larastan/larastan": "^2.0",
|
||||
"laravel/pint": "^1.0",
|
||||
"orchestra/testbench": "^9.0",
|
||||
"phpunit/phpunit": "^11.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-zlib": "Required for compressing payloads exceeding 1 KiB in size."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
@ -2292,19 +2297,19 @@
|
|||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nick Pratley",
|
||||
"email": "nick@npratley.net",
|
||||
"homepage": "https://npratley.net/",
|
||||
"name": "Muhammed Sari",
|
||||
"email": "muhammed@dive.be",
|
||||
"homepage": "https://dive.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Expo Notifications driver for Laravel",
|
||||
"description": "Expo Notifications Channel for Laravel",
|
||||
"homepage": "https://github.com/laravel-notification-channels/expo",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel-notification-channels/expo/issues",
|
||||
"source": "https://github.com/laravel-notification-channels/expo/tree/1.3.1"
|
||||
"source": "https://github.com/laravel-notification-channels/expo/tree/v2.0.0"
|
||||
},
|
||||
"time": "2024-03-15T00:24:58+00:00"
|
||||
"time": "2024-03-18T07:49:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel-notification-channels/webpush",
|
||||
|
|
|
@ -30,6 +30,8 @@ return [
|
|||
'ingest' => [
|
||||
'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false),
|
||||
],
|
||||
|
||||
'authorized_fetch' => env('AUTHORIZED_FETCH', false),
|
||||
],
|
||||
|
||||
'atom' => [
|
||||
|
|
13
config/groups.php
Normal file
13
config/groups.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => env('GROUPS_ENABLED', false),
|
||||
'federation' => env('GROUPS_FEDERATION', true),
|
||||
|
||||
'acl' => [
|
||||
'create_group' => [
|
||||
'admins' => env('GROUPS_ACL_CREATE_ADMINS', true),
|
||||
'users' => env('GROUPS_ACL_CREATE_USERS', true),
|
||||
]
|
||||
]
|
||||
];
|
|
@ -14,8 +14,8 @@ class AddSnowflakeidsToUsersTable extends Migration
|
|||
public function up()
|
||||
{
|
||||
Schema::table('statuses', function (Blueprint $table) {
|
||||
$table->dropPrimary('id');
|
||||
$table->bigInteger('id')->unsigned()->primary()->change();
|
||||
$table->dropPrimary('id');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,13 @@ class AddSnowflakeIdsToCollectionsTable extends Migration
|
|||
public function up()
|
||||
{
|
||||
Schema::table('collections', function (Blueprint $table) {
|
||||
$table->dropPrimary('id');
|
||||
$table->bigInteger('id')->unsigned()->primary()->change();
|
||||
$table->dropPrimary('id');
|
||||
});
|
||||
|
||||
Schema::table('collection_items', function (Blueprint $table) {
|
||||
$table->dropPrimary('id');
|
||||
$table->bigInteger('id')->unsigned()->primary()->change();
|
||||
$table->dropPrimary('id');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupRolesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->string('name');
|
||||
$table->string('slug')->nullable();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->unique(['group_id', 'slug']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_roles');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupInteractionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_interactions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('item_type')->nullable()->index();
|
||||
$table->string('item_id')->nullable()->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_interactions');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupReportsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('item_type')->nullable()->index();
|
||||
$table->string('item_id')->nullable()->index();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->boolean('open')->default(true)->index();
|
||||
$table->unique(['group_id', 'profile_id', 'item_type', 'item_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_reports');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupBlocksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_blocks', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('admin_id')->unsigned()->nullable();
|
||||
$table->bigInteger('profile_id')->nullable()->unsigned()->index();
|
||||
$table->bigInteger('instance_id')->nullable()->unsigned()->index();
|
||||
$table->string('name')->nullable()->index();
|
||||
$table->string('reason')->nullable();
|
||||
$table->boolean('is_user')->index();
|
||||
$table->boolean('moderated')->default(false)->index();
|
||||
$table->unique(['group_id', 'profile_id', 'instance_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_blocks');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupLimitsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_limits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->json('limits')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->unique(['group_id', 'profile_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_limits');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\GroupCategory;
|
||||
|
||||
class CreateGroupCategoriesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('group_categories');
|
||||
|
||||
Schema::create('group_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique()->index();
|
||||
$table->string('slug')->unique()->index();
|
||||
$table->boolean('active')->default(true)->index();
|
||||
$table->tinyInteger('order')->unsigned()->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$default = [
|
||||
'General',
|
||||
'Photography',
|
||||
'Fediverse',
|
||||
'CompSci & Programming',
|
||||
'Causes & Movements',
|
||||
'Humor',
|
||||
'Science & Tech',
|
||||
'Travel',
|
||||
'Buy & Sell',
|
||||
'Business',
|
||||
'Style',
|
||||
'Animals',
|
||||
'Sports & Fitness',
|
||||
'Education',
|
||||
'Arts',
|
||||
'Entertainment',
|
||||
'Faith & Spirituality',
|
||||
'Relationships & Identity',
|
||||
'Parenting',
|
||||
'Hobbies & Interests',
|
||||
'Food & Drink',
|
||||
'Vehicles & Commutes',
|
||||
'Civics & Community',
|
||||
];
|
||||
|
||||
for ($i=1; $i <= 23; $i++) {
|
||||
$cat = new GroupCategory;
|
||||
$cat->name = $default[$i - 1];
|
||||
$cat->slug = str_slug($cat->name);
|
||||
$cat->active = true;
|
||||
$cat->order = $i;
|
||||
$cat->save();
|
||||
}
|
||||
|
||||
Schema::table('groups', function (Blueprint $table) {
|
||||
$table->unsignedInteger('category_id')->default(1)->index()->after('id');
|
||||
$table->unsignedInteger('member_count')->nullable();
|
||||
$table->boolean('recommended')->default(false)->index();
|
||||
$table->boolean('discoverable')->default(false)->index();
|
||||
$table->boolean('activitypub')->default(false);
|
||||
$table->boolean('is_nsfw')->default(false);
|
||||
$table->boolean('dms')->default(false);
|
||||
$table->boolean('autospam')->default(false);
|
||||
$table->boolean('verified')->default(false);
|
||||
$table->timestamp('last_active_at')->nullable();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_categories');
|
||||
|
||||
Schema::table('groups', function (Blueprint $table) {
|
||||
$table->dropColumn('category_id');
|
||||
$table->dropColumn('member_count');
|
||||
$table->dropColumn('recommended');
|
||||
$table->dropColumn('activitypub');
|
||||
$table->dropColumn('is_nsfw');
|
||||
$table->dropColumn('discoverable');
|
||||
$table->dropColumn('dms');
|
||||
$table->dropColumn('autospam');
|
||||
$table->dropColumn('verified');
|
||||
$table->dropColumn('last_active_at');
|
||||
$table->dropColumn('deleted_at');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupHashtagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_hashtags', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('name')->unique()->index();
|
||||
$table->string('formatted')->nullable();
|
||||
$table->boolean('recommended')->default(false);
|
||||
$table->boolean('sensitive')->default(false);
|
||||
$table->boolean('banned')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_hashtags');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupPostHashtagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_post_hashtags', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('hashtag_id')->unsigned()->index();
|
||||
$table->bigInteger('group_id')->unsigned()->index();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->bigInteger('status_id')->unsigned()->nullable();
|
||||
$table->string('status_visibility')->nullable();
|
||||
$table->boolean('nsfw')->default(false);
|
||||
$table->unique(['hashtag_id', 'group_id', 'profile_id', 'status_id'], 'group_post_hashtags_gda_unique');
|
||||
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
|
||||
$table->foreign('profile_id')->references('id')->on('profiles')->onDelete('cascade');
|
||||
$table->foreign('hashtag_id')->references('id')->on('group_hashtags')->onDelete('cascade');
|
||||
$table->foreign('status_id')->references('id')->on('group_posts')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_post_hashtags');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupStoresTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_stores', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('group_id')->unsigned()->nullable()->index();
|
||||
$table->string('store_key')->index();
|
||||
$table->json('store_value')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->unique(['group_id', 'store_key']);
|
||||
$table->foreign('group_id')->references('id')->on('groups')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('group_stores');
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue