diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49d6c00a4..df79210a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,38 @@
# Release Notes
-## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.8...dev)
+## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.9...dev)
+
+### Added
+- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
+
+### Federation
+- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
+- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
+- ([](https://github.com/pixelfed/pixelfed/commit/))
+
+### Updates
+- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
+- Update FollowServiceWarmCache, improve handling larger following/follower lists ([61a6d904](https://github.com/pixelfed/pixelfed/commit/61a6d904))
+- Update StoryApiV1Controller, add viewers route to view story viewers ([941736ce](https://github.com/pixelfed/pixelfed/commit/941736ce))
+- Update NotificationService, improve cache warming query ([2496386d](https://github.com/pixelfed/pixelfed/commit/2496386d))
+- Update StatusService, hydrate accounts on request instead of caching them along with status objects ([223661ec](https://github.com/pixelfed/pixelfed/commit/223661ec))
+- Update profile embed, fix resize ([dc23c21d](https://github.com/pixelfed/pixelfed/commit/dc23c21d))
+- Update Status model, improve thumb logic ([d969a973](https://github.com/pixelfed/pixelfed/commit/d969a973))
+- Update Status model, allow unlisted thumbnails ([1f0a45b7](https://github.com/pixelfed/pixelfed/commit/1f0a45b7))
+- Update StatusTagsPipeline, fix object tags and slug normalization ([d295e605](https://github.com/pixelfed/pixelfed/commit/d295e605))
+- Update Note and CreateNote transformers, include attachment blurhash, width and height ([ce1afe27](https://github.com/pixelfed/pixelfed/commit/ce1afe27))
+- Update ap helpers, store media attachment width and height if present ([8c969191](https://github.com/pixelfed/pixelfed/commit/8c969191))
+- Update Sign-in with Mastodon, allow usage when registrations are closed ([895dc4fa](https://github.com/pixelfed/pixelfed/commit/895dc4fa))
+- ([](https://github.com/pixelfed/pixelfed/commit/))
+
+## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
### Added
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
+- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e))
+- Health check endpoint at /api/service/health-check ([ff58f970](https://github.com/pixelfed/pixelfed/commit/ff58f970))
+- Reblogs in home feed ([#4563](https://github.com/pixelfed/pixelfed/pull/4563)) ([b86d47bf](https://github.com/pixelfed/pixelfed/commit/b86d47bf))
+- Account Migrations ([#4578](https://github.com/pixelfed/pixelfed/pull/4578)) ([a9220e4e](https://github.com/pixelfed/pixelfed/commit/a9220e4e))
### Updates
- Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f))
@@ -26,7 +55,32 @@
- Update ComposeModal.vue, fix scroll issue and dont hide scrollbar ([2d959fb3](https://github.com/pixelfed/pixelfed/commit/2d959fb3))
- Update AccountImport, add select first 100 posts button ([625a76a5](https://github.com/pixelfed/pixelfed/commit/625a76a5))
- Update ApiV1Controller, add include_reblogs attribute to home timeline ([37fd0342](https://github.com/pixelfed/pixelfed/commit/37fd0342))
-- ([](https://github.com/pixelfed/pixelfed/commit/))
+- Update rate limits, fixes #4537 ([1cc6274a](https://github.com/pixelfed/pixelfed/commit/1cc6274a))
+- Update Services, use zpopmin on predis ([4b2c66f5](https://github.com/pixelfed/pixelfed/commit/4b2c66f5))
+- Update Inbox, allow storing Create->Note activities without any local followers, disabled by default ([9fa6b3f7](https://github.com/pixelfed/pixelfed/commit/9fa6b3f7))
+- Update AP Helpers, preserve admin unlisted state before adding to NetworkTimelineService ([0704c7e0](https://github.com/pixelfed/pixelfed/commit/0704c7e0))
+- Update SearchApiV2Service, improve resolve query logic to better handle remote posts/profiles and local posts/profiles ([c61d0b91](https://github.com/pixelfed/pixelfed/commit/c61d0b91))
+- Update FollowPipeline, improve follower/following count calculation ([0b515767](https://github.com/pixelfed/pixelfed/commit/0b515767))
+- Update TransformImports command, increment status_count on profile model ([ba7551d8](https://github.com/pixelfed/pixelfed/commit/ba7551d8))
+- Update AP Helpers, improve url validation and add optional dns verification, disabled by default ([2bef3e41](https://github.com/pixelfed/pixelfed/commit/2bef3e41))
+- Update admin users blade view, show last_active_at and other info ([e0b48b29](https://github.com/pixelfed/pixelfed/commit/e0b48b29))
+- Update MediaStorageService, improve head header handling ([3590adbd](https://github.com/pixelfed/pixelfed/commit/3590adbd))
+- Update admin user view, improve previews ([ff2c16fe](https://github.com/pixelfed/pixelfed/commit/ff2c16fe))
+- Update FanoutDeletePipeline, fix AP object ([0d802c31](https://github.com/pixelfed/pixelfed/commit/0d802c31))
+- Update Remote Auth feature, fix custom domain bug and enforce banned domains ([acabf603](https://github.com/pixelfed/pixelfed/commit/acabf603))
+- Update StatusService, reduce cache ttl from 7 days to 6 hours ([59b64378](https://github.com/pixelfed/pixelfed/commit/59b64378))
+- Update ProfileController, allow albums in atom feed. Closes #4561. Fixes #4526 ([1c105a6c](https://github.com/pixelfed/pixelfed/commit/1c105a6c))
+- Update admin users view, fix website value. Closes #4557 ([c469d475](https://github.com/pixelfed/pixelfed/commit/c469d475))
+- Update StatusStatelessTransformer, allow unlisted reblogs ([1c13b518](https://github.com/pixelfed/pixelfed/commit/1c13b518))
+- Update ApiV1Controller, hydrate reblog state in home timeline ([13bdaa2e](https://github.com/pixelfed/pixelfed/commit/13bdaa2e))
+- Update Timeline component, improve reblog support ([29de91e5](https://github.com/pixelfed/pixelfed/commit/29de91e5))
+- Update timeline settings, add photo reblogs only option ([e2705b9a](https://github.com/pixelfed/pixelfed/commit/e2705b9a))
+- Update PostContent, add text cw warning ([911504fa](https://github.com/pixelfed/pixelfed/commit/911504fa))
+- Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances ([3d1b6516](https://github.com/pixelfed/pixelfed/commit/3d1b6516))
+- Update RemoteStatusDelete pipeline ([71e92261](https://github.com/pixelfed/pixelfed/commit/71e92261))
+- Update RemoteStatusDelete pipeline ([fab8f25e](https://github.com/pixelfed/pixelfed/commit/fab8f25e))
+- Update RemoteStatusPipeline, fix reply check ([618b6727](https://github.com/pixelfed/pixelfed/commit/618b6727))
+- Update ApiV1Controller, add bookmarked to timeline entities ([ca746717](https://github.com/pixelfed/pixelfed/commit/ca746717))
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)
diff --git a/app/Console/Commands/TransformImports.php b/app/Console/Commands/TransformImports.php
index cd63985ac..b88401178 100644
--- a/app/Console/Commands/TransformImports.php
+++ b/app/Console/Commands/TransformImports.php
@@ -9,6 +9,7 @@ use App\Media;
use App\Profile;
use App\Status;
use Storage;
+use App\Services\AccountService;
use App\Services\MediaPathService;
use Illuminate\Support\Str;
use App\Util\Lexer\Autolink;
@@ -38,7 +39,7 @@ class TransformImports extends Command
return;
}
- $ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(200)->get();
+ $ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
if(!$ips->count()) {
return;
@@ -135,6 +136,11 @@ class TransformImports extends Command
$ip->creation_id = $idk['incr'];
$ip->save();
+ $profile->status_count = $profile->status_count + 1;
+ $profile->save();
+
+ AccountService::del($profile->id);
+
ImportService::clearAttempts($profile->id);
ImportService::getPostCount($profile->id, true);
}
diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php
index d045edd55..311eac220 100644
--- a/app/Http/Controllers/Admin/AdminReportController.php
+++ b/app/Http/Controllers/Admin/AdminReportController.php
@@ -27,8 +27,8 @@ use App\Services\StoryService;
use App\Services\ModLogService;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
-use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Http\Resources\AdminReport;
use App\Http\Resources\AdminSpamReport;
use App\Services\NotificationService;
@@ -643,7 +643,7 @@ trait AdminReportController
$q->whereNull('admin_seen') :
$q->whereNotNull('admin_seen');
})
- ->groupBy(['object_id', 'object_type'])
+ ->groupBy(['id', 'object_id', 'object_type'])
->cursorPaginate(6)
->withQueryString()
);
@@ -1049,7 +1049,7 @@ trait AdminReportController
StatusDelete::dispatch($status)->onQueue('high');
} else {
NetworkTimelineService::del($status->id);
- DeleteRemoteStatusPipeline::dispatch($status)->onQueue('high');
+ RemoteStatusDelete::dispatch($status)->onQueue('high');
}
Report::whereObjectId($report->object_id)
diff --git a/app/Http/Controllers/AdminShadowFilterController.php b/app/Http/Controllers/AdminShadowFilterController.php
new file mode 100644
index 000000000..461e1d0c2
--- /dev/null
+++ b/app/Http/Controllers/AdminShadowFilterController.php
@@ -0,0 +1,122 @@
+middleware(['auth','admin']);
+ }
+
+ public function home(Request $request)
+ {
+ $filter = $request->input('filter');
+ $searchQuery = $request->input('q');
+ $filters = AdminShadowFilter::when($filter, function($q, $filter) {
+ if($filter == 'all') {
+ return $q;
+ } else if($filter == 'inactive') {
+ return $q->whereActive(false);
+ } else {
+ return $q;
+ }
+ }, function($q, $filter) {
+ return $q->whereActive(true);
+ })
+ ->when($searchQuery, function($q, $searchQuery) {
+ $ids = Profile::where('username', 'like', '%' . $searchQuery . '%')
+ ->limit(100)
+ ->pluck('id')
+ ->toArray();
+ return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids);
+ })
+ ->latest()
+ ->paginate(10)
+ ->withQueryString();
+
+ return view('admin.asf.home', compact('filters'));
+ }
+
+ public function create(Request $request)
+ {
+ return view('admin.asf.create');
+ }
+
+ public function edit(Request $request, $id)
+ {
+ $filter = AdminShadowFilter::findOrFail($id);
+ $profile = AccountService::get($filter->item_id);
+ return view('admin.asf.edit', compact('filter', 'profile'));
+ }
+
+ public function store(Request $request)
+ {
+ $this->validate($request, [
+ 'username' => 'required',
+ 'active' => 'sometimes',
+ 'note' => 'sometimes',
+ 'hide_from_public_feeds' => 'sometimes'
+ ]);
+
+ $profile = Profile::whereUsername($request->input('username'))->first();
+
+ if(!$profile) {
+ return back()->withErrors(['Invalid account']);
+ }
+
+ if($profile->user && $profile->user->is_admin) {
+ return back()->withErrors(['Cannot filter an admin account']);
+ }
+
+ $active = $request->has('active') && $request->has('hide_from_public_feeds');
+
+ AdminShadowFilter::updateOrCreate([
+ 'item_id' => $profile->id,
+ 'item_type' => get_class($profile)
+ ], [
+ 'is_local' => $profile->domain === null,
+ 'note' => $request->input('note'),
+ 'hide_from_public_feeds' => $request->has('hide_from_public_feeds'),
+ 'admin_id' => $request->user()->profile_id,
+ 'active' => $active
+ ]);
+
+ AdminShadowFilterService::refresh();
+
+ return redirect('/i/admin/asf/home');
+ }
+
+ public function storeEdit(Request $request, $id)
+ {
+ $this->validate($request, [
+ 'active' => 'sometimes',
+ 'note' => 'sometimes',
+ 'hide_from_public_feeds' => 'sometimes'
+ ]);
+
+ $filter = AdminShadowFilter::findOrFail($id);
+
+ $profile = Profile::findOrFail($filter->item_id);
+
+ if($profile->user && $profile->user->is_admin) {
+ return back()->withErrors(['Cannot filter an admin account']);
+ }
+
+ $active = $request->has('active');
+ $filter->active = $active;
+ $filter->hide_from_public_feeds = $request->has('hide_from_public_feeds');
+ $filter->note = $request->input('note');
+ $filter->save();
+
+ AdminShadowFilterService::refresh();
+
+ return redirect('/i/admin/asf/home');
+ }
+}
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index 3e67632a7..92d5d43ee 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -1621,7 +1621,7 @@ class ApiV1Controller extends Controller
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 250;
+ return $dailyLimit >= 1250;
});
abort_if($limitReached == true, 429);
@@ -1826,7 +1826,7 @@ class ApiV1Controller extends Controller
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 250;
+ return $dailyLimit >= 1250;
});
abort_if($limitReached == true, 429);
@@ -2193,12 +2193,22 @@ class ApiV1Controller extends Controller
if($pid) {
$status['favourited'] = (bool) LikeService::liked($pid, $s['id']);
$status['reblogged'] = (bool) ReblogService::get($pid, $status['id']);
+ $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']);
}
return $status;
})
->filter(function($status) {
return $status && isset($status['account']);
})
+ ->map(function($status) use($pid) {
+ if(!empty($status['reblog'])) {
+ $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']);
+ $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']);
+ $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']);
+ }
+
+ return $status;
+ })
->take($limit)
->values();
} else {
@@ -2236,12 +2246,22 @@ class ApiV1Controller extends Controller
if($pid) {
$status['favourited'] = (bool) LikeService::liked($pid, $s['id']);
$status['reblogged'] = (bool) ReblogService::get($pid, $status['id']);
+ $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']);
}
return $status;
})
->filter(function($status) {
return $status && isset($status['account']);
})
+ ->map(function($status) use($pid) {
+ if(!empty($status['reblog'])) {
+ $status['reblog']['favourited'] = (bool) LikeService::liked($pid, $status['reblog']['id']);
+ $status['reblog']['reblogged'] = (bool) ReblogService::get($pid, $status['reblog']['id']);
+ $status['bookmarked'] = (bool) BookmarkService::get($pid, $status['id']);
+ }
+
+ return $status;
+ })
->take($limit)
->values();
}
@@ -2362,6 +2382,7 @@ class ApiV1Controller extends Controller
if($user) {
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $k);
$status['reblogged'] = (bool) ReblogService::get($user->profile_id, $status['id']);
+ $status['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $status['id']);
}
return $status;
})
@@ -2838,7 +2859,7 @@ class ApiV1Controller extends Controller
->where('created_at', '>', now()->subDays(1))
->count();
- return $dailyLimit >= 100;
+ return $dailyLimit >= 1000;
});
abort_if($limitReached == true, 429);
@@ -3599,8 +3620,8 @@ class ApiV1Controller extends Controller
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
- $home = $request->input('home.last_read_id');
- $notifications = $request->input('notifications.last_read_id');
+ $home = $request->input('home[last_read_id]');
+ $notifications = $request->input('notifications[last_read_id]');
if($home) {
return $this->json(MarkerService::set($pid, 'home', $home));
diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php
index 4c41a8a77..6ed047af9 100644
--- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php
@@ -17,6 +17,7 @@ use App\Report;
use App\Profile;
use App\StatusArchived;
use App\User;
+use App\UserSetting;
use App\Services\AccountService;
use App\Services\StatusService;
use App\Services\ProfileStatusService;
@@ -33,6 +34,7 @@ use App\Mail\PasswordChange;
use App\Mail\ConfirmAppEmail;
use App\Http\Resources\StatusStateless;
use App\Jobs\StatusPipeline\StatusDelete;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
use Illuminate\Support\Facades\RateLimiter;
@@ -837,7 +839,7 @@ class ApiV1Dot1Controller extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
- StatusDelete::dispatch($status);
+ $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
return [];
}
@@ -845,4 +847,41 @@ class ApiV1Dot1Controller extends Controller
return StatusService::get($status->id, false);
}
+
+ public function getWebSettings(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ $uid = $request->user()->id;
+ $settings = UserSetting::firstOrCreate([
+ 'user_id' => $uid
+ ]);
+ if(!$settings->other) {
+ return [];
+ }
+ return $settings->other;
+ }
+
+ public function setWebSettings(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ $this->validate($request, [
+ 'field' => 'required|in:enable_reblogs,hide_reblog_banner',
+ 'value' => 'required'
+ ]);
+ $field = $request->input('field');
+ $value = $request->input('value');
+ $settings = UserSetting::firstOrCreate([
+ 'user_id' => $request->user()->id
+ ]);
+ if(!$settings->other) {
+ $other = [];
+ } else {
+ $other = $settings->other;
+ }
+ $other[$field] = $value;
+ $settings->other = $other;
+ $settings->save();
+
+ return [200];
+ }
}
diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php
index 63c63c56f..757e14dce 100644
--- a/app/Http/Controllers/Api/ApiV2Controller.php
+++ b/app/Http/Controllers/Api/ApiV2Controller.php
@@ -225,7 +225,7 @@ class ApiV2Controller extends Controller
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 250;
+ return $dailyLimit >= 1250;
});
abort_if($limitReached == true, 429);
diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php
index 7a3614f3d..9be50f346 100644
--- a/app/Http/Controllers/ComposeController.php
+++ b/app/Http/Controllers/ComposeController.php
@@ -98,7 +98,7 @@ class ComposeController extends Controller
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 250;
+ return $dailyLimit >= 1250;
});
abort_if($limitReached == true, 429);
@@ -190,7 +190,7 @@ class ComposeController extends Controller
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 500;
+ return $dailyLimit >= 1500;
});
abort_if($limitReached == true, 429);
@@ -415,7 +415,7 @@ class ComposeController extends Controller
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->where('username','like','%'.$q.'%')
- ->groupBy('domain')
+ ->groupBy('id', 'domain')
->limit(15)
->get()
->map(function($profile) {
@@ -499,7 +499,7 @@ class ComposeController extends Controller
->where('created_at', '>', now()->subDays(1))
->count();
- return $dailyLimit >= 100;
+ return $dailyLimit >= 1000;
});
abort_if($limitReached == true, 429);
diff --git a/app/Http/Controllers/HealthCheckController.php b/app/Http/Controllers/HealthCheckController.php
new file mode 100644
index 000000000..0af2f63f4
--- /dev/null
+++ b/app/Http/Controllers/HealthCheckController.php
@@ -0,0 +1,16 @@
+withHeaders([
+ 'Content-Type' => 'text/plain',
+ 'Cache-Control' => 'max-age=0, must-revalidate, no-cache, no-store'
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/ProfileAliasController.php b/app/Http/Controllers/ProfileAliasController.php
new file mode 100644
index 000000000..024005a8e
--- /dev/null
+++ b/app/Http/Controllers/ProfileAliasController.php
@@ -0,0 +1,64 @@
+middleware('auth');
+ }
+
+ public function index(Request $request)
+ {
+ $aliases = $request->user()->profile->aliases;
+ return view('settings.aliases.index', compact('aliases'));
+ }
+
+ public function store(Request $request)
+ {
+ $this->validate($request, [
+ 'acct' => 'required'
+ ]);
+
+ $acct = $request->input('acct');
+
+ if($request->user()->profile->aliases->count() >= 3) {
+ return back()->with('error', 'You can only add 3 account aliases.');
+ }
+
+ $webfingerService = WebfingerService::lookup($acct);
+ if(!$webfingerService || !isset($webfingerService['url'])) {
+ return back()->with('error', 'Invalid account, cannot add alias at this time.');
+ }
+ $alias = new ProfileAlias;
+ $alias->profile_id = $request->user()->profile_id;
+ $alias->acct = $acct;
+ $alias->uri = $webfingerService['url'];
+ $alias->save();
+
+ return back()->with('status', 'Successfully added alias!');
+ }
+
+ public function delete(Request $request)
+ {
+ $this->validate($request, [
+ 'acct' => 'required',
+ 'id' => 'required|exists:profile_aliases'
+ ]);
+
+ $alias = ProfileAlias::where('profile_id', $request->user()->profile_id)
+ ->where('acct', $request->input('acct'))
+ ->findOrFail($request->input('id'));
+
+ $alias->delete();
+
+ return back()->with('status', 'Successfully deleted alias!');
+ }
+}
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
index df25b2d7b..26e9e5398 100644
--- a/app/Http/Controllers/ProfileController.php
+++ b/app/Http/Controllers/ProfileController.php
@@ -12,6 +12,7 @@ use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\Story;
+use App\Status;
use App\User;
use App\UserSetting;
use App\UserFilter;
@@ -253,15 +254,14 @@ class ProfileController extends Controller
abort_if(!$enabled, 404);
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) {
- $items = DB::table('statuses')
- ->whereProfileId($pid)
- ->whereVisibility('public')
- ->whereType('photo')
+ $items = Status::whereProfileId($pid)
+ ->whereScope('public')
+ ->whereIn('type', ['photo', 'photo:album'])
->orderByDesc('id')
->take(10)
->get()
->map(function($status) {
- return StatusService::get($status->id);
+ return StatusService::get($status->id, true);
})
->filter(function($status) {
return $status &&
diff --git a/app/Http/Controllers/RemoteAuthController.php b/app/Http/Controllers/RemoteAuthController.php
new file mode 100644
index 000000000..e068f5d75
--- /dev/null
+++ b/app/Http/Controllers/RemoteAuthController.php
@@ -0,0 +1,718 @@
+user()) {
+ return redirect('/');
+ }
+ return view('auth.remote.start');
+ }
+
+ public function startRedirect(Request $request)
+ {
+ return redirect('/login');
+ }
+
+ public function getAuthDomains(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+
+ if(config('remote-auth.mastodon.domains.only_custom')) {
+ $res = config('remote-auth.mastodon.domains.custom');
+ if(!$res || !strlen($res)) {
+ return [];
+ }
+ $res = explode(',', $res);
+ return response()->json($res);
+ }
+
+ if( config('remote-auth.mastodon.domains.custom') &&
+ !config('remote-auth.mastodon.domains.only_default') &&
+ strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
+ strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
+ ) {
+ $res = config('remote-auth.mastodon.domains.custom');
+ if(!$res || !strlen($res)) {
+ return [];
+ }
+ $res = explode(',', $res);
+ return response()->json($res);
+ }
+
+ $res = config('remote-auth.mastodon.domains.default');
+ $res = explode(',', $res);
+
+ return response()->json($res);
+ }
+
+ public function redirect(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+
+ $this->validate($request, ['domain' => 'required']);
+
+ $domain = $request->input('domain');
+
+ if(str_starts_with(strtolower($domain), 'http')) {
+ $res = [
+ 'domain' => $domain,
+ 'ready' => false,
+ 'action' => 'incompatible_domain'
+ ];
+ return response()->json($res);
+ }
+
+ $validateInstance = Helpers::validateUrl('https://' . $domain . '/?block-check=' . time());
+
+ if(!$validateInstance) {
+ $res = [
+ 'domain' => $domain,
+ 'ready' => false,
+ 'action' => 'blocked_domain'
+ ];
+ return response()->json($res);
+ }
+
+ $compatible = RemoteAuthService::isDomainCompatible($domain);
+
+ if(!$compatible) {
+ $res = [
+ 'domain' => $domain,
+ 'ready' => false,
+ 'action' => 'incompatible_domain'
+ ];
+ return response()->json($res);
+ }
+
+ if(config('remote-auth.mastodon.domains.only_default')) {
+ $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
+ if(!in_array($domain, $defaultDomains)) {
+ $res = [
+ 'domain' => $domain,
+ 'ready' => false,
+ 'action' => 'incompatible_domain'
+ ];
+ return response()->json($res);
+ }
+ }
+
+ if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
+ $customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
+ if(!in_array($domain, $customDomains)) {
+ $res = [
+ 'domain' => $domain,
+ 'ready' => false,
+ 'action' => 'incompatible_domain'
+ ];
+ return response()->json($res);
+ }
+ }
+
+ $client = RemoteAuthService::getMastodonClient($domain);
+
+ abort_unless($client, 422, 'Invalid mastodon client');
+
+ $request->session()->put('state', $state = Str::random(40));
+ $request->session()->put('oauth_domain', $domain);
+
+ $query = http_build_query([
+ 'client_id' => $client->client_id,
+ 'redirect_uri' => $client->redirect_uri,
+ 'response_type' => 'code',
+ 'scope' => 'read',
+ 'state' => $state,
+ ]);
+
+ $request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query);
+
+ $dsh = Str::random(17);
+ $res = [
+ 'domain' => $domain,
+ 'ready' => true,
+ 'dsh' => $dsh
+ ];
+
+ return response()->json($res);
+ }
+
+ public function preflight(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+
+ if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) {
+ return redirect('/login');
+ }
+
+ return redirect()->away($request->session()->pull('oauth_redirect_to'));
+ }
+
+ public function handleCallback(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+
+ $domain = $request->session()->get('oauth_domain');
+
+ if($request->filled('code')) {
+ $code = $request->input('code');
+ $state = $request->session()->pull('state');
+
+ throw_unless(
+ strlen($state) > 0 && $state === $request->state,
+ InvalidArgumentException::class,
+ 'Invalid state value.'
+ );
+
+ $res = RemoteAuthService::getToken($domain, $code);
+
+ if(!$res || !isset($res['access_token'])) {
+ $request->session()->regenerate();
+ return redirect('/login');
+ }
+
+ $request->session()->put('oauth_remote_session_token', $res['access_token']);
+ return redirect('/auth/mastodon/getting-started');
+ }
+
+ return redirect('/login');
+ }
+
+ public function onboarding(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ if($request->user()) {
+ return redirect('/');
+ }
+ return view('auth.remote.onboarding');
+ }
+
+ public function sessionCheck(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 403);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+
+ $domain = $request->session()->get('oauth_domain');
+ $token = $request->session()->get('oauth_remote_session_token');
+
+ $res = RemoteAuthService::getVerifyCredentials($domain, $token);
+
+ abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials');
+
+ $webfinger = strtolower('@' . $res['acct'] . '@' . $domain);
+ $request->session()->put('oauth_masto_webfinger', $webfinger);
+
+ if(config('remote-auth.mastodon.max_uses.enabled')) {
+ $limit = config('remote-auth.mastodon.max_uses.limit');
+ $uses = RemoteAuthService::lookupWebfingerUses($webfinger);
+ if($uses >= $limit) {
+ return response()->json([
+ 'code' => 200,
+ 'msg' => 'Success!',
+ 'action' => 'max_uses_reached'
+ ]);
+ }
+ }
+
+ $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
+ if($exists && $exists->user_id) {
+ return response()->json([
+ 'code' => 200,
+ 'msg' => 'Success!',
+ 'action' => 'redirect_existing_user'
+ ]);
+ }
+
+ return response()->json([
+ 'code' => 200,
+ 'msg' => 'Success!',
+ 'action' => 'onboard'
+ ]);
+ }
+
+ public function sessionGetMastodonData(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 403);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+
+ $domain = $request->session()->get('oauth_domain');
+ $token = $request->session()->get('oauth_remote_session_token');
+
+ $res = RemoteAuthService::getVerifyCredentials($domain, $token);
+ $res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain);
+ $res['_domain'] = strtolower($domain);
+ $request->session()->put('oauth_remasto_id', $res['id']);
+
+ $ra = RemoteAuth::updateOrCreate([
+ 'domain' => $domain,
+ 'webfinger' => $res['_webfinger'],
+ ], [
+ 'software' => 'mastodon',
+ 'ip_address' => $request->ip(),
+ 'bearer_token' => $token,
+ 'verify_credentials' => $res,
+ 'last_verify_credentials_at' => now(),
+ 'last_successful_login_at' => now()
+ ]);
+
+ $request->session()->put('oauth_masto_raid', $ra->id);
+
+ return response()->json($res);
+ }
+
+ public function sessionValidateUsername(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 403);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+
+ $this->validate($request, [
+ 'username' => [
+ 'required',
+ 'min:2',
+ 'max:15',
+ function ($attribute, $value, $fail) {
+ $dash = substr_count($value, '-');
+ $underscore = substr_count($value, '_');
+ $period = substr_count($value, '.');
+
+ if(ends_with($value, ['.php', '.js', '.css'])) {
+ return $fail('Username is invalid.');
+ }
+
+ if(($dash + $underscore + $period) > 1) {
+ return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+ }
+
+ if (!ctype_alnum($value[0])) {
+ return $fail('Username is invalid. Must start with a letter or number.');
+ }
+
+ if (!ctype_alnum($value[strlen($value) - 1])) {
+ return $fail('Username is invalid. Must end with a letter or number.');
+ }
+
+ $val = str_replace(['_', '.', '-'], '', $value);
+ if(!ctype_alnum($val)) {
+ return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+ }
+
+ $restricted = RestrictedNames::get();
+ if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+ return $fail('Username cannot be used.');
+ }
+ }
+ ]
+ ]);
+ $username = strtolower($request->input('username'));
+
+ $exists = User::where('username', $username)->exists();
+
+ return response()->json([
+ 'code' => 200,
+ 'username' => $username,
+ 'exists' => $exists
+ ]);
+ }
+
+ public function sessionValidateEmail(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 403);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+
+ $this->validate($request, [
+ 'email' => [
+ 'required',
+ 'email:strict,filter_unicode,dns,spoof',
+ ]
+ ]);
+
+ $email = $request->input('email');
+ $banned = EmailService::isBanned($email);
+ $exists = User::where('email', $email)->exists();
+
+ return response()->json([
+ 'code' => 200,
+ 'email' => $email,
+ 'exists' => $exists,
+ 'banned' => $banned
+ ]);
+ }
+
+ public function sessionGetMastodonFollowers(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+ abort_unless($request->session()->exists('oauth_remasto_id'), 403);
+
+ $domain = $request->session()->get('oauth_domain');
+ $token = $request->session()->get('oauth_remote_session_token');
+ $id = $request->session()->get('oauth_remasto_id');
+
+ $res = RemoteAuthService::getFollowing($domain, $token, $id);
+
+ if(!$res) {
+ return response()->json([
+ 'code' => 200,
+ 'following' => []
+ ]);
+ }
+
+ $res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
+
+ return response()->json([
+ 'code' => 200,
+ 'following' => $res
+ ]);
+ }
+
+ public function handleSubmit(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+ abort_unless($request->session()->exists('oauth_remasto_id'), 403);
+ abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
+ abort_unless($request->session()->exists('oauth_masto_raid'), 403);
+
+ $this->validate($request, [
+ 'email' => 'required|email:strict,filter_unicode,dns,spoof',
+ 'username' => [
+ 'required',
+ 'min:2',
+ 'max:15',
+ 'unique:users,username',
+ function ($attribute, $value, $fail) {
+ $dash = substr_count($value, '-');
+ $underscore = substr_count($value, '_');
+ $period = substr_count($value, '.');
+
+ if(ends_with($value, ['.php', '.js', '.css'])) {
+ return $fail('Username is invalid.');
+ }
+
+ if(($dash + $underscore + $period) > 1) {
+ return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
+ }
+
+ if (!ctype_alnum($value[0])) {
+ return $fail('Username is invalid. Must start with a letter or number.');
+ }
+
+ if (!ctype_alnum($value[strlen($value) - 1])) {
+ return $fail('Username is invalid. Must end with a letter or number.');
+ }
+
+ $val = str_replace(['_', '.', '-'], '', $value);
+ if(!ctype_alnum($val)) {
+ return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
+ }
+
+ $restricted = RestrictedNames::get();
+ if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
+ return $fail('Username cannot be used.');
+ }
+ }
+ ],
+ 'password' => 'required|string|min:8|confirmed',
+ 'name' => 'nullable|max:30'
+ ]);
+
+ $email = $request->input('email');
+ $username = $request->input('username');
+ $password = $request->input('password');
+ $name = $request->input('name');
+
+ $user = $this->createUser([
+ 'name' => $name,
+ 'username' => $username,
+ 'password' => $password,
+ 'email' => $email
+ ]);
+
+ $raid = $request->session()->pull('oauth_masto_raid');
+ $webfinger = $request->session()->pull('oauth_masto_webfinger');
+ $token = $user->createToken('Onboarding')->accessToken;
+
+ $ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail();
+ $ra->user_id = $user->id;
+ $ra->save();
+
+ return [
+ 'code' => 200,
+ 'msg' => 'Success',
+ 'token' => $token
+ ];
+ }
+
+ public function storeBio(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_unless($request->user(), 404);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+ abort_unless($request->session()->exists('oauth_remasto_id'), 403);
+
+ $this->validate($request, [
+ 'bio' => 'required|nullable|max:500',
+ ]);
+
+ $profile = $request->user()->profile;
+ $profile->bio = Purify::clean($request->input('bio'));
+ $profile->save();
+
+ return [200];
+ }
+
+ public function accountToId(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 404);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+ abort_unless($request->session()->exists('oauth_remasto_id'), 403);
+
+ $this->validate($request, [
+ 'account' => 'required|url'
+ ]);
+
+ $account = $request->input('account');
+ abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404);
+
+ $host = strtolower(config('pixelfed.domain.app'));
+ $domain = strtolower(parse_url($account, PHP_URL_HOST));
+
+ if($domain == $host) {
+ $username = Str::of($account)->explode('/')->last();
+ $user = User::where('username', $username)->first();
+ if($user) {
+ return ['id' => (string) $user->profile_id];
+ } else {
+ return [];
+ }
+ } else {
+ try {
+ $profile = Helpers::profileFetch($account);
+ if($profile) {
+ return ['id' => (string) $profile->id];
+ } else {
+ return [];
+ }
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
+ return;
+ } catch (Exception $e) {
+ return [];
+ }
+ }
+ }
+
+ public function storeAvatar(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_unless($request->user(), 404);
+ $this->validate($request, [
+ 'avatar_url' => 'required|active_url',
+ ]);
+
+ $user = $request->user();
+ $profile = $user->profile;
+
+ abort_if(!$profile->avatar, 404, 'Missing avatar');
+
+ $avatar = $profile->avatar;
+ $avatar->remote_url = $request->input('avatar_url');
+ $avatar->save();
+
+ MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
+
+ return [200];
+ }
+
+ public function finishUp(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_unless($request->user(), 404);
+
+ $currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app');
+ $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
+ RemoteAuthService::submitToBeagle(
+ $ra->webfinger,
+ $ra->verify_credentials['url'],
+ $currentWebfinger,
+ $request->user()->url()
+ );
+
+ return [200];
+ }
+
+ public function handleLogin(Request $request)
+ {
+ abort_unless((
+ config_cache('pixelfed.open_registration') &&
+ config('remote-auth.mastodon.enabled')
+ ) || (
+ config('remote-auth.mastodon.ignore_closed_state') &&
+ config('remote-auth.mastodon.enabled')
+ ), 404);
+ abort_if($request->user(), 404);
+ abort_unless($request->session()->exists('oauth_domain'), 403);
+ abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
+ abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
+
+ $domain = $request->session()->get('oauth_domain');
+ $wf = $request->session()->get('oauth_masto_webfinger');
+
+ $ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail();
+
+ $user = User::findOrFail($ra->user_id);
+ abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
+ Auth::loginUsingId($ra->user_id);
+ return [200];
+ }
+
+ protected function createUser($data)
+ {
+ event(new Registered($user = User::create([
+ 'name' => Purify::clean($data['name']),
+ 'username' => $data['username'],
+ 'email' => $data['email'],
+ 'password' => Hash::make($data['password']),
+ 'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
+ 'app_register_ip' => request()->ip(),
+ 'register_source' => 'mastodon'
+ ])));
+
+ $this->guarder()->login($user);
+
+ return $user;
+ }
+
+ protected function guarder()
+ {
+ return Auth::guard();
+ }
+}
diff --git a/app/Http/Controllers/Settings/PrivacySettings.php b/app/Http/Controllers/Settings/PrivacySettings.php
index 3d1cd4515..9a5febe83 100644
--- a/app/Http/Controllers/Settings/PrivacySettings.php
+++ b/app/Http/Controllers/Settings/PrivacySettings.php
@@ -20,13 +20,13 @@ trait PrivacySettings
public function privacy()
{
- $user = Auth::user();
- $settings = $user->settings;
- $profile = $user->profile;
- $is_private = $profile->is_private;
- $settings['is_private'] = (bool) $is_private;
+ $user = Auth::user();
+ $settings = $user->settings;
+ $profile = $user->profile;
+ $is_private = $profile->is_private;
+ $settings['is_private'] = (bool) $is_private;
- return view('settings.privacy', compact('settings', 'profile'));
+ return view('settings.privacy', compact('settings', 'profile'));
}
public function privacyStore(Request $request)
@@ -39,11 +39,13 @@ trait PrivacySettings
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
+ 'indexable',
'show_atom',
];
- $profile->is_suggestable = $request->input('is_suggestable') == 'on';
- $profile->save();
+ $profile->indexable = $request->input('indexable') == 'on';
+ $profile->is_suggestable = $request->input('is_suggestable') == 'on';
+ $profile->save();
foreach ($fields as $field) {
$form = $request->input($field);
@@ -70,6 +72,8 @@ trait PrivacySettings
} else {
$settings->{$field} = false;
}
+ } elseif ($field == 'indexable') {
+
} else {
if ($form == 'on') {
$settings->{$field} = true;
diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php
index a2459f2d1..2eb9df65f 100644
--- a/app/Http/Controllers/SettingsController.php
+++ b/app/Http/Controllers/SettingsController.php
@@ -230,29 +230,51 @@ class SettingsController extends Controller
public function timelineSettings(Request $request)
{
+ $uid = $request->user()->id;
$pid = $request->user()->profile_id;
$top = Redis::zscore('pf:tl:top', $pid) != false;
$replies = Redis::zscore('pf:tl:replies', $pid) != false;
- return view('settings.timeline', compact('top', 'replies'));
+ $userSettings = UserSetting::firstOrCreate([
+ 'user_id' => $uid
+ ]);
+ if(!$userSettings || !$userSettings->other) {
+ $userSettings = [
+ 'enable_reblogs' => false,
+ 'photo_reblogs_only' => false
+ ];
+ } else {
+ $userSettings = array_merge([
+ 'enable_reblogs' => false,
+ 'photo_reblogs_only' => false
+ ],
+ $userSettings->other);
+ }
+ return view('settings.timeline', compact('top', 'replies', 'userSettings'));
}
public function updateTimelineSettings(Request $request)
{
- $pid = $request->user()->profile_id;
- $top = $request->has('top') && $request->input('top') === 'on';
- $replies = $request->has('replies') && $request->input('replies') === 'on';
-
- if($top) {
- Redis::zadd('pf:tl:top', $pid, $pid);
- } else {
- Redis::zrem('pf:tl:top', $pid);
- }
-
- if($replies) {
- Redis::zadd('pf:tl:replies', $pid, $pid);
- } else {
- Redis::zrem('pf:tl:replies', $pid);
- }
+ $pid = $request->user()->profile_id;
+ $uid = $request->user()->id;
+ $this->validate($request, [
+ 'enable_reblogs' => 'sometimes',
+ 'photo_reblogs_only' => 'sometimes'
+ ]);
+ Redis::zrem('pf:tl:top', $pid);
+ Redis::zrem('pf:tl:replies', $pid);
+ $userSettings = UserSetting::firstOrCreate([
+ 'user_id' => $uid
+ ]);
+ if($userSettings->other) {
+ $other = $userSettings->other;
+ $other['enable_reblogs'] = $request->has('enable_reblogs');
+ $other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
+ } else {
+ $other['enable_reblogs'] = $request->has('enable_reblogs');
+ $other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
+ }
+ $userSettings->other = $other;
+ $userSettings->save();
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');
}
diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php
index 43ac03263..873f5eace 100644
--- a/app/Http/Controllers/StatusController.php
+++ b/app/Http/Controllers/StatusController.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\Jobs\SharePipeline\UndoSharePipeline;
use App\AccountInterstitial;
@@ -242,7 +243,7 @@ class StatusController extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
- StatusDelete::dispatch($status);
+ $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
@@ -250,7 +251,7 @@ class StatusController extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
- StatusDelete::dispatch($status);
+ $status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
}
if($request->wantsJson()) {
diff --git a/app/Http/Controllers/Stories/StoryApiV1Controller.php b/app/Http/Controllers/Stories/StoryApiV1Controller.php
index e32fffa26..db2b1f533 100644
--- a/app/Http/Controllers/Stories/StoryApiV1Controller.php
+++ b/app/Http/Controllers/Stories/StoryApiV1Controller.php
@@ -20,6 +20,7 @@ use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Services\AccountService;
use App\Services\MediaPathService;
use App\Services\StoryService;
+use App\Http\Resources\StoryView as StoryViewResource;
class StoryApiV1Controller extends Controller
{
@@ -355,4 +356,26 @@ class StoryApiV1Controller extends Controller
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
return $path;
}
+
+ public function viewers(Request $request)
+ {
+ abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
+
+ $this->validate($request, [
+ 'sid' => 'required|string|min:1|max:50'
+ ]);
+
+ $pid = $request->user()->profile_id;
+ $sid = $request->input('sid');
+
+ $story = Story::whereProfileId($pid)
+ ->whereActive(true)
+ ->findOrFail($sid);
+
+ $viewers = StoryView::whereStoryId($story->id)
+ ->orderByDesc('id')
+ ->cursorPaginate(10);
+
+ return StoryViewResource::collection($viewers);
+ }
}
diff --git a/app/Http/Resources/AdminProfile.php b/app/Http/Resources/AdminProfile.php
new file mode 100644
index 000000000..50db2d5ca
--- /dev/null
+++ b/app/Http/Resources/AdminProfile.php
@@ -0,0 +1,30 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ $res = AccountService::get($this->id, true);
+ $res['domain'] = $this->domain;
+ $res['status'] = $this->status;
+ $res['limits'] = [
+ 'exist' => $this->cw || $this->unlisted || $this->no_autolink,
+ 'autocw' => (bool) $this->cw,
+ 'unlisted' => (bool) $this->unlisted,
+ 'no_autolink' => (bool) $this->no_autolink,
+ 'banned' => (bool) $this->status == 'banned'
+ ];
+ return $res;
+ }
+}
diff --git a/app/Http/Resources/StoryView.php b/app/Http/Resources/StoryView.php
new file mode 100644
index 000000000..891bf2eee
--- /dev/null
+++ b/app/Http/Resources/StoryView.php
@@ -0,0 +1,20 @@
+
+ */
+ public function toArray(Request $request)
+ {
+ return AccountService::get($this->profile_id, true);
+ }
+}
diff --git a/app/Jobs/AdminPipeline/AdminProfileActionPipeline.php b/app/Jobs/AdminPipeline/AdminProfileActionPipeline.php
new file mode 100644
index 000000000..24b24ba6e
--- /dev/null
+++ b/app/Jobs/AdminPipeline/AdminProfileActionPipeline.php
@@ -0,0 +1,139 @@
+profile = $profile;
+ $this->action = $action;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $profile = $this->profile;
+ $action = $this->action;
+
+ switch($action) {
+ case 'mark-all-cw':
+ return $this->markAllPostsWithContentWarnings();
+ break;
+ case 'unlist-all':
+ return $this->unlistAllPosts();
+ break;
+ case 'purge':
+ return $this->purgeAllPosts();
+ break;
+ case 'refetch':
+ return $this->refetchAllPosts();
+ break;
+ }
+ }
+
+ protected function markAllPostsWithContentWarnings()
+ {
+ $profile = $this->profile;
+
+ foreach(Status::whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
+ if($status->scope == 'direct') {
+ continue;
+ }
+ $status->is_nsfw = true;
+ $status->save();
+ StatusService::del($status->id);
+ }
+ }
+
+ protected function unlistAllPosts()
+ {
+ $profile = $this->profile;
+
+ foreach(Status::whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
+ if($status->scope != 'public') {
+ continue;
+ }
+ $status->scope = 'unlisted';
+ $status->visibility = 'unlisted';
+ $status->save();
+ StatusService::del($status->id);
+ }
+ }
+
+ protected function purgeAllPosts()
+ {
+ $profile = $this->profile;
+
+ foreach(Status::withTrashed()->whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
+ RemoteStatusDelete::dispatch($status)->onQueue('delete');
+ }
+ }
+
+ protected function refetchAllPosts()
+ {
+ $profile = $this->profile;
+ $res = ActivityPubFetchService::get($profile->remote_url, false);
+ if(!$res) {
+ return;
+ }
+ $res = json_decode($res, true);
+ $profile->following_count = Follower::whereProfileId($profile->id)->count();
+ $profile->followers_count = Follower::whereFollowingId($profile->id)->count();
+ $profile->name = isset($res['name']) ? Purify::clean($res['name']) : $profile->username;
+ $profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null;
+ if(isset($res['publicKey'])) {
+ $profile->public_key = $res['publicKey']['publicKeyPem'];
+ }
+ if(
+ isset($res['icon']) &&
+ isset(
+ $res['icon']['type'],
+ $res['icon']['mediaType'],
+ $res['icon']['url']) && $res['icon']['type'] == 'Image'
+ ) {
+ if(in_array($res['icon']['mediaType'], ['image/jpeg', 'image/png'])) {
+ $profile->avatar->remote_url = $res['icon']['url'];
+ $profile->push();
+ MediaStorageService::avatar($profile->avatar);
+ }
+ }
+ $profile->save();
+ AccountService::del($profile->id);
+ }
+}
diff --git a/app/Jobs/DeletePipeline/DeleteRemoteProfilePipeline.php b/app/Jobs/DeletePipeline/DeleteRemoteProfilePipeline.php
index 095cf98e3..7dce73fdc 100644
--- a/app/Jobs/DeletePipeline/DeleteRemoteProfilePipeline.php
+++ b/app/Jobs/DeletePipeline/DeleteRemoteProfilePipeline.php
@@ -51,6 +51,7 @@ use App\Models\Conversation;
use App\Models\Poll;
use App\Models\PollVote;
use App\Services\AccountService;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
class DeleteRemoteProfilePipeline implements ShouldQueue
{
@@ -86,7 +87,7 @@ class DeleteRemoteProfilePipeline implements ShouldQueue
Status::whereProfileId($pid)
->chunk(50, function($statuses) {
foreach($statuses as $status) {
- DeleteRemoteStatusPipeline::dispatch($status)->onQueue('delete');
+ RemoteStatusDelete::dispatch($status)->onQueue('delete');
}
});
diff --git a/app/Jobs/DeletePipeline/FanoutDeletePipeline.php b/app/Jobs/DeletePipeline/FanoutDeletePipeline.php
index a5ee65ac3..0ccb9d5c4 100644
--- a/app/Jobs/DeletePipeline/FanoutDeletePipeline.php
+++ b/app/Jobs/DeletePipeline/FanoutDeletePipeline.php
@@ -53,10 +53,10 @@ class FanoutDeletePipeline implements ShouldQueue
"id" => $profile->permalink('#delete'),
"type" => "Delete",
"actor" => $profile->permalink(),
- "to" => [
- "https://www.w3.org/ns/activitystreams#Public",
+ "object" => [
+ "type" => "Person",
+ "id" => $profile->permalink()
],
- "object" => $profile->permalink(),
];
$payload = json_encode($activity);
diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php
index 6db85fa6a..225334304 100644
--- a/app/Jobs/FollowPipeline/FollowPipeline.php
+++ b/app/Jobs/FollowPipeline/FollowPipeline.php
@@ -17,91 +17,71 @@ use App\Services\FollowerService;
class FollowPipeline implements ShouldQueue
{
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- protected $follower;
+ protected $follower;
- /**
- * Delete the job if its models no longer exist.
- *
- * @var bool
- */
- public $deleteWhenMissingModels = true;
-
- /**
- * Create a new job instance.
- *
- * @return void
- */
- public function __construct($follower)
- {
- $this->follower = $follower;
- }
+ /**
+ * Delete the job if its models no longer exist.
+ *
+ * @var bool
+ */
+ public $deleteWhenMissingModels = true;
- /**
- * Execute the job.
- *
- * @return void
- */
- public function handle()
- {
- $follower = $this->follower;
- $actor = $follower->actor;
- $target = $follower->target;
+ /**
+ * Create a new job instance.
+ *
+ * @return void
+ */
+ public function __construct($follower)
+ {
+ $this->follower = $follower;
+ }
- if(!$actor || !$target) {
- return;
- }
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $follower = $this->follower;
+ $actor = $follower->actor;
+ $target = $follower->target;
- Cache::forget('profile:following:' . $actor->id);
- Cache::forget('profile:following:' . $target->id);
+ if(!$actor || !$target) {
+ return;
+ }
- FollowerService::add($actor->id, $target->id);
+ if($target->domain || !$target->private_key) {
+ return;
+ }
- $actorProfileSync = Cache::get(FollowerService::FOLLOWING_SYNC_KEY . $actor->id);
- if(!$actorProfileSync) {
- FollowServiceWarmCache::dispatch($actor->id)->onQueue('low');
- } else {
- if($actor->following_count) {
- $actor->increment('following_count');
- } else {
- $count = Follower::whereProfileId($actor->id)->count();
- $actor->following_count = $count;
- $actor->save();
- }
- Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $actor->id, 1, 604800);
- AccountService::del($actor->id);
- }
+ Cache::forget('profile:following:' . $actor->id);
+ Cache::forget('profile:following:' . $target->id);
- $targetProfileSync = Cache::get(FollowerService::FOLLOWERS_SYNC_KEY . $target->id);
- if(!$targetProfileSync) {
- FollowServiceWarmCache::dispatch($target->id)->onQueue('low');
- } else {
- if($target->followers_count) {
- $target->increment('followers_count');
- } else {
- $count = Follower::whereFollowingId($target->id)->count();
- $target->followers_count = $count;
- $target->save();
- }
- Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $target->id, 1, 604800);
- AccountService::del($target->id);
- }
+ FollowerService::add($actor->id, $target->id);
- if($target->domain || !$target->private_key) {
- return;
- }
+ $count = Follower::whereProfileId($actor->id)->count();
+ $actor->following_count = $count;
+ $actor->save();
+ AccountService::del($actor->id);
- try {
- $notification = new Notification();
- $notification->profile_id = $target->id;
- $notification->actor_id = $actor->id;
- $notification->action = 'follow';
- $notification->item_id = $target->id;
- $notification->item_type = "App\Profile";
- $notification->save();
- } catch (Exception $e) {
- Log::error($e);
- }
- }
+ $count = Follower::whereFollowingId($target->id)->count();
+ $target->followers_count = $count;
+ $target->save();
+ AccountService::del($target->id);
+
+ try {
+ $notification = new Notification();
+ $notification->profile_id = $target->id;
+ $notification->actor_id = $actor->id;
+ $notification->action = 'follow';
+ $notification->item_id = $target->id;
+ $notification->item_type = "App\Profile";
+ $notification->save();
+ } catch (Exception $e) {
+ Log::error($e);
+ }
+ }
}
diff --git a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php
index cabea9958..990236f69 100644
--- a/app/Jobs/FollowPipeline/FollowServiceWarmCache.php
+++ b/app/Jobs/FollowPipeline/FollowServiceWarmCache.php
@@ -8,10 +8,13 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
use App\Services\AccountService;
use App\Services\FollowerService;
use Cache;
use DB;
+use Storage;
+use App\Follower;
use App\Profile;
class FollowServiceWarmCache implements ShouldQueue
@@ -23,6 +26,16 @@ class FollowServiceWarmCache implements ShouldQueue
public $timeout = 5000;
public $failOnTimeout = false;
+ /**
+ * Get the middleware the job should pass through.
+ *
+ * @return array
+ */
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->profileId))->dontRelease()];
+ }
+
/**
* Create a new job instance.
*
@@ -42,6 +55,10 @@ class FollowServiceWarmCache implements ShouldQueue
{
$id = $this->profileId;
+ if(Cache::has(FollowerService::FOLLOWERS_SYNC_KEY . $id) && Cache::has(FollowerService::FOLLOWING_SYNC_KEY . $id)) {
+ return;
+ }
+
$account = AccountService::get($id, true);
if(!$account) {
@@ -50,25 +67,43 @@ class FollowServiceWarmCache implements ShouldQueue
return;
}
- DB::table('followers')
- ->select('id', 'following_id', 'profile_id')
- ->whereFollowingId($id)
- ->orderBy('id')
- ->chunk(200, function($followers) use($id) {
- foreach($followers as $follow) {
- FollowerService::add($follow->profile_id, $id);
- }
- });
+ $hasFollowerPostProcessing = false;
+ $hasFollowingPostProcessing = false;
- DB::table('followers')
- ->select('id', 'following_id', 'profile_id')
- ->whereProfileId($id)
- ->orderBy('id')
- ->chunk(200, function($followers) use($id) {
- foreach($followers as $follow) {
- FollowerService::add($id, $follow->following_id);
- }
- });
+ if(Follower::whereProfileId($id)->orWhere('following_id', $id)->count()) {
+ $following = [];
+ $followers = [];
+ foreach(Follower::lazy() as $follow) {
+ if($follow->following_id != $id && $follow->profile_id != $id) {
+ continue;
+ }
+ if($follow->profile_id == $id) {
+ $following[] = $follow->following_id;
+ } else {
+ $followers[] = $follow->profile_id;
+ }
+ }
+
+ if(count($followers) > 100) {
+ // store follower ids and process in another job
+ Storage::put('follow-warm-cache/' . $id . '/followers.json', json_encode($followers));
+ $hasFollowerPostProcessing = true;
+ } else {
+ foreach($followers as $follower) {
+ FollowerService::add($follower, $id);
+ }
+ }
+
+ if(count($following) > 100) {
+ // store following ids and process in another job
+ Storage::put('follow-warm-cache/' . $id . '/following.json', json_encode($following));
+ $hasFollowingPostProcessing = true;
+ } else {
+ foreach($following as $following) {
+ FollowerService::add($id, $following);
+ }
+ }
+ }
Cache::put(FollowerService::FOLLOWERS_SYNC_KEY . $id, 1, 604800);
Cache::put(FollowerService::FOLLOWING_SYNC_KEY . $id, 1, 604800);
@@ -82,6 +117,14 @@ class FollowServiceWarmCache implements ShouldQueue
AccountService::del($id);
+ if($hasFollowingPostProcessing) {
+ FollowServiceWarmCacheLargeIngestPipeline::dispatch($id, 'following')->onQueue('follow');
+ }
+
+ if($hasFollowerPostProcessing) {
+ FollowServiceWarmCacheLargeIngestPipeline::dispatch($id, 'followers')->onQueue('follow');
+ }
+
return;
}
}
diff --git a/app/Jobs/FollowPipeline/FollowServiceWarmCacheLargeIngestPipeline.php b/app/Jobs/FollowPipeline/FollowServiceWarmCacheLargeIngestPipeline.php
new file mode 100644
index 000000000..3299bf7a4
--- /dev/null
+++ b/app/Jobs/FollowPipeline/FollowServiceWarmCacheLargeIngestPipeline.php
@@ -0,0 +1,88 @@
+profileId = $profileId;
+ $this->followType = $followType;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $pid = $this->profileId;
+ $type = $this->followType;
+
+ if($type === 'followers') {
+ $key = 'follow-warm-cache/' . $pid . '/followers.json';
+ if(!Storage::exists($key)) {
+ return;
+ }
+ $file = Storage::get($key);
+ $json = json_decode($file, true);
+
+ foreach($json as $id) {
+ FollowerService::add($id, $pid, false);
+ usleep(random_int(500, 3000));
+ }
+ sleep(5);
+ Storage::delete($key);
+ }
+
+ if($type === 'following') {
+ $key = 'follow-warm-cache/' . $pid . '/following.json';
+ if(!Storage::exists($key)) {
+ return;
+ }
+ $file = Storage::get($key);
+ $json = json_decode($file, true);
+
+ foreach($json as $id) {
+ FollowerService::add($pid, $id, false);
+ usleep(random_int(500, 3000));
+ }
+ sleep(5);
+ Storage::delete($key);
+ }
+
+ sleep(random_int(2, 5));
+ $files = Storage::files('follow-warm-cache/' . $pid);
+ if(empty($files)) {
+ Storage::deleteDirectory('follow-warm-cache/' . $pid);
+ }
+ }
+}
diff --git a/app/Jobs/SharePipeline/SharePipeline.php b/app/Jobs/SharePipeline/SharePipeline.php
index 0b580500b..ae184957e 100644
--- a/app/Jobs/SharePipeline/SharePipeline.php
+++ b/app/Jobs/SharePipeline/SharePipeline.php
@@ -49,7 +49,10 @@ class SharePipeline implements ShouldQueue
public function handle()
{
$status = $this->status;
- $parent = $this->status->parent();
+ $parent = Status::find($this->status->reblog_of_id);
+ if(!$parent) {
+ return;
+ }
$actor = $status->profile;
$target = $parent->profile;
@@ -84,7 +87,7 @@ class SharePipeline implements ShouldQueue
public function remoteAnnounceDeliver()
{
- if(config_cache('federation.activitypub.enabled') == false) {
+ if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
return true;
}
$status = $this->status;
diff --git a/app/Jobs/SharePipeline/UndoSharePipeline.php b/app/Jobs/SharePipeline/UndoSharePipeline.php
index 6d39dfa39..3850a4752 100644
--- a/app/Jobs/SharePipeline/UndoSharePipeline.php
+++ b/app/Jobs/SharePipeline/UndoSharePipeline.php
@@ -61,7 +61,7 @@ class UndoSharePipeline implements ShouldQueue
return;
}
- if(config_cache('federation.activitypub.enabled') == false) {
+ if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
return $status->delete();
} else {
return $this->remoteAnnounceDeliver();
@@ -70,7 +70,8 @@ class UndoSharePipeline implements ShouldQueue
public function remoteAnnounceDeliver()
{
- if(config_cache('federation.activitypub.enabled') == false) {
+ if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
+ $status->delete();
return 1;
}
diff --git a/app/Jobs/StatusPipeline/RemoteStatusDelete.php b/app/Jobs/StatusPipeline/RemoteStatusDelete.php
new file mode 100644
index 000000000..19c17b54c
--- /dev/null
+++ b/app/Jobs/StatusPipeline/RemoteStatusDelete.php
@@ -0,0 +1,142 @@
+status = $status;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $status = $this->status;
+
+ if($status->deleted_at) {
+ return;
+ }
+ $profile = $this->status->profile;
+
+ StatusService::del($status->id, true);
+
+ if($profile->status_count && $profile->status_count > 0) {
+ $profile->status_count = $profile->status_count - 1;
+ $profile->save();
+ }
+
+ return $this->unlinkRemoveMedia($status);
+ }
+
+ public function unlinkRemoveMedia($status)
+ {
+
+ if($status->in_reply_to_id) {
+ $parent = Status::find($status->in_reply_to_id);
+ if($parent) {
+ --$parent->reply_count;
+ $parent->save();
+ StatusService::del($parent->id);
+ }
+ }
+
+ AccountInterstitial::where('item_type', 'App\Status')
+ ->where('item_id', $status->id)
+ ->delete();
+ Bookmark::whereStatusId($status->id)->delete();
+ CollectionItem::whereObjectType('App\Status')
+ ->whereObjectId($status->id)
+ ->get()
+ ->each(function($col) {
+ CollectionService::removeItem($col->collection_id, $col->object_id);
+ $col->delete();
+ });
+ DirectMessage::whereStatusId($status->id)->delete();
+ Like::whereStatusId($status->id)->forceDelete();
+ Media::whereStatusId($status->id)
+ ->get()
+ ->each(function($media) {
+ MediaDeletePipeline::dispatch($media)->onQueue('mmo');
+ });
+ MediaTag::where('status_id', $status->id)->delete();
+ Mention::whereStatusId($status->id)->forceDelete();
+ Notification::whereItemType('App\Status')
+ ->whereItemId($status->id)
+ ->forceDelete();
+ Report::whereObjectType('App\Status')
+ ->whereObjectId($status->id)
+ ->delete();
+ StatusArchived::whereStatusId($status->id)->delete();
+ StatusHashtag::whereStatusId($status->id)->delete();
+ StatusView::whereStatusId($status->id)->delete();
+ Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
+
+ $status->delete();
+
+ StatusService::del($status->id, true);
+ AccountService::del($status->profile_id);
+
+ return 1;
+ }
+}
diff --git a/app/Jobs/StatusPipeline/StatusEntityLexer.php b/app/Jobs/StatusPipeline/StatusEntityLexer.php
index d205f1e21..2bbc92102 100644
--- a/app/Jobs/StatusPipeline/StatusEntityLexer.php
+++ b/app/Jobs/StatusPipeline/StatusEntityLexer.php
@@ -20,6 +20,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Services\UserFilterService;
+use App\Services\AdminShadowFilterService;
class StatusEntityLexer implements ShouldQueue
{
@@ -176,7 +177,9 @@ class StatusEntityLexer implements ShouldQueue
$status->reblog_of_id === null &&
($hideNsfw ? $status->is_nsfw == false : true)
) {
- PublicTimelineService::add($status->id);
+ if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
+ PublicTimelineService::add($status->id);
+ }
}
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
diff --git a/app/Jobs/StatusPipeline/StatusTagsPipeline.php b/app/Jobs/StatusPipeline/StatusTagsPipeline.php
index a72e6d50e..893fa6a83 100644
--- a/app/Jobs/StatusPipeline/StatusTagsPipeline.php
+++ b/app/Jobs/StatusPipeline/StatusTagsPipeline.php
@@ -45,6 +45,11 @@ class StatusTagsPipeline implements ShouldQueue
{
$res = $this->activity;
$status = $this->status;
+
+ if(isset($res['tag']['type'], $res['tag']['name'])) {
+ $res['tag'] = [$res['tag']];
+ }
+
$tags = collect($res['tag']);
// Emoji
@@ -73,19 +78,18 @@ class StatusTagsPipeline implements ShouldQueue
if(config('database.default') === 'pgsql') {
$hashtag = Hashtag::where('name', 'ilike', $name)
- ->orWhere('slug', 'ilike', str_slug($name))
+ ->orWhere('slug', 'ilike', str_slug($name, '-', false))
->first();
- if(!$hashtag) {
- $hashtag = new Hashtag;
- $hashtag->name = $name;
- $hashtag->slug = str_slug($name);
- $hashtag->save();
- }
+ if(!$hashtag) {
+ $hashtag = Hashtag::updateOrCreate([
+ 'slug' => str_slug($name, '-', false),
+ 'name' => $name
+ ]);
+ }
} else {
- $hashtag = Hashtag::firstOrCreate([
- 'slug' => str_slug($name)
- ], [
+ $hashtag = Hashtag::updateOrCreate([
+ 'slug' => str_slug($name, '-', false),
'name' => $name
]);
}
diff --git a/app/Media.php b/app/Media.php
index 16448dbbf..b3f9ccba0 100644
--- a/app/Media.php
+++ b/app/Media.php
@@ -20,8 +20,8 @@ class Media extends Model
protected $guarded = [];
protected $casts = [
- 'srcset' => 'array',
- 'deleted_at' => 'datetime'
+ 'srcset' => 'array',
+ 'deleted_at' => 'datetime'
];
public function status()
@@ -63,7 +63,7 @@ class Media extends Model
}
if($this->media_path && $this->mime && in_array($this->mime, ['image/jpeg', 'image/png'])) {
- return $this->remote_media || Str::startsWith($this->media_path, 'http') ?
+ return $this->remote_media || Str::startsWith($this->media_path, 'http') ?
$this->media_path :
url(Storage::url($this->media_path));
}
@@ -78,6 +78,9 @@ class Media extends Model
public function mimeType()
{
+ if(!$this->mime) {
+ return;
+ }
return explode('/', $this->mime)[0];
}
diff --git a/app/Models/AdminShadowFilter.php b/app/Models/AdminShadowFilter.php
new file mode 100644
index 000000000..f98086f7f
--- /dev/null
+++ b/app/Models/AdminShadowFilter.php
@@ -0,0 +1,27 @@
+ 'datetime'
+ ];
+
+ public function account()
+ {
+ if($this->item_type === 'App\Profile') {
+ return AccountService::get($this->item_id, true);
+ }
+
+ return;
+ }
+}
diff --git a/app/Models/ProfileAlias.php b/app/Models/ProfileAlias.php
new file mode 100644
index 000000000..b7a3bdc9c
--- /dev/null
+++ b/app/Models/ProfileAlias.php
@@ -0,0 +1,17 @@
+belongsTo(Profile::class);
+ }
+}
diff --git a/app/Models/RemoteAuth.php b/app/Models/RemoteAuth.php
new file mode 100644
index 000000000..98909f09b
--- /dev/null
+++ b/app/Models/RemoteAuth.php
@@ -0,0 +1,19 @@
+ 'array',
+ 'last_successful_login_at' => 'datetime',
+ 'last_verify_credentials_at' => 'datetime'
+ ];
+}
diff --git a/app/Models/RemoteAuthInstance.php b/app/Models/RemoteAuthInstance.php
new file mode 100644
index 000000000..bdc03fcb2
--- /dev/null
+++ b/app/Models/RemoteAuthInstance.php
@@ -0,0 +1,13 @@
+hasMany(Story::class);
}
-
public function reported()
{
return $this->hasMany(Report::class, 'object_id');
}
+
+ public function aliases()
+ {
+ return $this->hasMany(ProfileAlias::class);
+ }
}
diff --git a/app/Services/Account/RemoteAuthService.php b/app/Services/Account/RemoteAuthService.php
new file mode 100644
index 000000000..4412352a5
--- /dev/null
+++ b/app/Services/Account/RemoteAuthService.php
@@ -0,0 +1,191 @@
+ config('remote-auth.mastodon.domains.only_default'),
+ 'custom_only' => config('remote-auth.mastodon.domains.only_custom'),
+ ]);
+ }
+
+ public static function getMastodonClient($domain)
+ {
+ if(RemoteAuthInstance::whereDomain($domain)->exists()) {
+ return RemoteAuthInstance::whereDomain($domain)->first();
+ }
+
+ try {
+ $url = 'https://' . $domain . '/api/v1/apps';
+ $res = Http::asForm()->throw()->timeout(10)->post($url, [
+ 'client_name' => config('pixelfed.domain.app', 'pixelfed'),
+ 'redirect_uris' => url('/auth/mastodon/callback'),
+ 'scopes' => 'read',
+ 'website' => 'https://pixelfed.org'
+ ]);
+
+ if(!$res->ok()) {
+ return false;
+ }
+ } catch (RequestException $e) {
+ return false;
+ } catch (ConnectionException $e) {
+ return false;
+ } catch (Exception $e) {
+ return false;
+ }
+
+ $body = $res->json();
+
+ if(!$body || !isset($body['client_id'])) {
+ return false;
+ }
+
+ $raw = RemoteAuthInstance::updateOrCreate([
+ 'domain' => $domain
+ ], [
+ 'client_id' => $body['client_id'],
+ 'client_secret' => $body['client_secret'],
+ 'redirect_uri' => $body['redirect_uri'],
+ ]);
+
+ return $raw;
+ }
+
+ public static function getToken($domain, $code)
+ {
+ $raw = RemoteAuthInstance::whereDomain($domain)->first();
+ if(!$raw || !$raw->active || $raw->banned) {
+ return false;
+ }
+
+ $url = 'https://' . $domain . '/oauth/token';
+ $res = Http::asForm()->post($url, [
+ 'code' => $code,
+ 'grant_type' => 'authorization_code',
+ 'client_id' => $raw->client_id,
+ 'client_secret' => $raw->client_secret,
+ 'redirect_uri' => $raw->redirect_uri,
+ 'scope' => 'read'
+ ]);
+
+ return $res;
+ }
+
+ public static function getVerifyCredentials($domain, $code)
+ {
+ $raw = RemoteAuthInstance::whereDomain($domain)->first();
+ if(!$raw || !$raw->active || $raw->banned) {
+ return false;
+ }
+
+ $url = 'https://' . $domain . '/api/v1/accounts/verify_credentials';
+
+ $res = Http::withToken($code)->get($url);
+
+ return $res->json();
+ }
+
+ public static function getFollowing($domain, $code, $id)
+ {
+ $raw = RemoteAuthInstance::whereDomain($domain)->first();
+ if(!$raw || !$raw->active || $raw->banned) {
+ return false;
+ }
+
+ $url = 'https://' . $domain . '/api/v1/accounts/' . $id . '/following?limit=80';
+ $key = self::CACHE_KEY . 'get-following:code:' . substr($code, 0, 16) . substr($code, -5) . ':domain:' . $domain. ':id:' .$id;
+
+ return Cache::remember($key, 3600, function() use($url, $code) {
+ $res = Http::withToken($code)->get($url);
+ return $res->json();
+ });
+ }
+
+ public static function isDomainCompatible($domain = false)
+ {
+ if(!$domain) {
+ return false;
+ }
+
+ return Cache::remember(self::CACHE_KEY . 'domain-compatible:' . $domain, 14400, function() use($domain) {
+ try {
+ $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/domain?domain=' . $domain);
+ if(!$res->ok()) {
+ return false;
+ }
+ } catch (RequestException $e) {
+ return false;
+ } catch (ConnectionException $e) {
+ return false;
+ } catch (Exception $e) {
+ return false;
+ }
+ $json = $res->json();
+
+ if(!in_array('compatible', $json)) {
+ return false;
+ }
+
+ return $res['compatible'];
+ });
+ }
+
+ public static function lookupWebfingerUses($wf)
+ {
+ try {
+ $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/lookup?webfinger=' . $wf);
+ if(!$res->ok()) {
+ return false;
+ }
+ } catch (RequestException $e) {
+ return false;
+ } catch (ConnectionException $e) {
+ return false;
+ } catch (Exception $e) {
+ return false;
+ }
+ $json = $res->json();
+ if(!$json || !isset($json['count'])) {
+ return false;
+ }
+
+ return $json['count'];
+ }
+
+ public static function submitToBeagle($ow, $ou, $dw, $du)
+ {
+ try {
+ $url = 'https://beagle.pixelfed.net/api/v1/raa/submit';
+ $res = Http::throw()->timeout(10)->get($url, [
+ 'ow' => $ow,
+ 'ou' => $ou,
+ 'dw' => $dw,
+ 'du' => $du,
+ ]);
+
+ if(!$res->ok()) {
+ return;
+ }
+ } catch (RequestException $e) {
+ return;
+ } catch (ConnectionException $e) {
+ return;
+ } catch (Exception $e) {
+ return;
+ }
+
+ return;
+ }
+}
diff --git a/app/Services/ActivityPubFetchService.php b/app/Services/ActivityPubFetchService.php
index d0accdcdc..3d1980a11 100644
--- a/app/Services/ActivityPubFetchService.php
+++ b/app/Services/ActivityPubFetchService.php
@@ -11,11 +11,13 @@ use Illuminate\Http\Client\RequestException;
class ActivityPubFetchService
{
- public static function get($url)
+ public static function get($url, $validateUrl = true)
{
- if(!Helpers::validateUrl($url)) {
- return 0;
- }
+ if($validateUrl === true) {
+ if(!Helpers::validateUrl($url)) {
+ return 0;
+ }
+ }
$baseHeaders = [
'Accept' => 'application/activity+json, application/ld+json',
diff --git a/app/Services/AdminShadowFilterService.php b/app/Services/AdminShadowFilterService.php
new file mode 100644
index 000000000..a5933508a
--- /dev/null
+++ b/app/Services/AdminShadowFilterService.php
@@ -0,0 +1,51 @@
+whereActive(1)
+ ->where('hide_from_public_feeds', true)
+ ->pluck('item_id')
+ ->toArray();
+ }
+
+ public static function getHideFromPublicFeedsList($refresh = false)
+ {
+ $key = self::CACHE_KEY . 'list:hide_from_public_feeds';
+ if($refresh) {
+ Cache::forget($key);
+ }
+ return Cache::remember($key, 86400, function() {
+ return AdminShadowFilter::whereItemType('App\Profile')
+ ->whereActive(1)
+ ->where('hide_from_public_feeds', true)
+ ->pluck('item_id')
+ ->toArray();
+ });
+ }
+
+ public static function canAddToPublicFeedByProfileId($profileId)
+ {
+ return !in_array($profileId, self::getHideFromPublicFeedsList());
+ }
+
+ public static function refresh()
+ {
+ $keys = [
+ self::CACHE_KEY . 'list:hide_from_public_feeds'
+ ];
+
+ foreach($keys as $key) {
+ Cache::forget($key);
+ }
+ }
+}
diff --git a/app/Services/DomainService.php b/app/Services/DomainService.php
new file mode 100644
index 000000000..01f050ca0
--- /dev/null
+++ b/app/Services/DomainService.php
@@ -0,0 +1,28 @@
+ 0;
+ });
+ }
+}
diff --git a/app/Services/FollowerService.php b/app/Services/FollowerService.php
index 9398fa53f..1c00a6f49 100644
--- a/app/Services/FollowerService.php
+++ b/app/Services/FollowerService.php
@@ -20,10 +20,14 @@ class FollowerService
const FOLLOWING_KEY = 'pf:services:follow:following:id:';
const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
- public static function add($actor, $target)
+ public static function add($actor, $target, $refresh = true)
{
$ts = (int) microtime(true);
- RelationshipService::refresh($actor, $target);
+ if($refresh) {
+ RelationshipService::refresh($actor, $target);
+ } else {
+ RelationshipService::forget($actor, $target);
+ }
Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target);
Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor);
Cache::forget('profile:following:' . $actor);
diff --git a/app/Services/InstanceService.php b/app/Services/InstanceService.php
index 1cead8d48..2ad991063 100644
--- a/app/Services/InstanceService.php
+++ b/app/Services/InstanceService.php
@@ -120,6 +120,9 @@ class InstanceService
$pixels[] = $row;
}
+ // Free the allocated GdImage object from memory:
+ imagedestroy($image);
+
$components_x = 4;
$components_y = 4;
$blurhash = Blurhash::encode($pixels, $components_x, $components_y);
diff --git a/app/Services/LikeService.php b/app/Services/LikeService.php
index 34a2417d0..f0ea1ac57 100644
--- a/app/Services/LikeService.php
+++ b/app/Services/LikeService.php
@@ -24,9 +24,7 @@ class LikeService {
public static function setAdd($profileId, $statusId)
{
if(self::setCount($profileId) > 400) {
- if(config('database.redis.client') === 'phpredis') {
- Redis::zpopmin(self::CACHE_SET_KEY . $profileId);
- }
+ Redis::zpopmin(self::CACHE_SET_KEY . $profileId);
}
return Redis::zadd(self::CACHE_SET_KEY . $profileId, $statusId, $statusId);
diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php
index fd70e3a0f..b547ee39c 100644
--- a/app/Services/MediaStorageService.php
+++ b/app/Services/MediaStorageService.php
@@ -16,6 +16,7 @@ use App\Services\AccountService;
use App\Http\Controllers\AvatarController;
use GuzzleHttp\Exception\RequestException;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
+use Illuminate\Support\Arr;
class MediaStorageService {
@@ -42,27 +43,16 @@ class MediaStorageService {
return false;
}
- $h = $r->getHeaders();
+ $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) {
+ return [strtolower($key) => last($item)];
+ });
- if (isset($h['content-length']) && isset($h['content-type'])) {
- if(empty($h['content-length']) || empty($h['content-type'])) {
- return false;
- }
- $len = is_array($h['content-length']) ? $h['content-length'][0] : $h['content-length'];
- $mime = is_array($h['content-type']) ? $h['content-type'][0] : $h['content-type'];
- } else {
- if (isset($h['Content-Length'], $h['Content-Type']) == false) {
- return false;
- }
-
- if(empty($h['Content-Length']) || empty($h['Content-Type']) ) {
- return false;
- }
-
- $len = is_array($h['Content-Length']) ? $h['Content-Length'][0] : $h['Content-Length'];
- $mime = is_array($h['Content-Type']) ? $h['Content-Type'][0] : $h['Content-Type'];
- }
+ if(!isset($h['content-length'], $h['content-type'])) {
+ return false;
+ }
+ $len = (int) $h['content-length'];
+ $mime = $h['content-type'];
if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) {
return false;
@@ -96,12 +86,11 @@ class MediaStorageService {
$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;
+ $url = ResilientMediaStorageService::store($storagePath, $path, $name);
+ if($thumb) {
+ $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
+ $media->thumbnail_url = $thumbUrl;
+ }
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->replicated_at = now();
diff --git a/app/Services/NetworkTimelineService.php b/app/Services/NetworkTimelineService.php
index 570899017..9aea47af4 100644
--- a/app/Services/NetworkTimelineService.php
+++ b/app/Services/NetworkTimelineService.php
@@ -49,9 +49,7 @@ class NetworkTimelineService
public static function add($val)
{
if(self::count() > config('instance.timeline.network.cache_dropoff')) {
- if(config('database.redis.client') === 'phpredis') {
- Redis::zpopmin(self::CACHE_KEY);
- }
+ Redis::zpopmin(self::CACHE_KEY);
}
return Redis::zadd(self::CACHE_KEY, $val, $val);
diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php
index 139b13a69..c068f8278 100644
--- a/app/Services/NotificationService.php
+++ b/app/Services/NotificationService.php
@@ -16,6 +16,8 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class NotificationService {
const CACHE_KEY = 'pf:services:notifications:ids:';
+ const EPOCH_CACHE_KEY = 'pf:services:notifications:epoch-id:by-months:';
+ const ITEM_CACHE_TTL = 86400;
const MASTODON_TYPES = [
'follow',
'follow_request',
@@ -44,11 +46,19 @@ class NotificationService {
return $res;
}
+ public static function getEpochId($months = 6)
+ {
+ return Cache::remember(self::EPOCH_CACHE_KEY . $months, 1209600, function() use($months) {
+ return Notification::where('created_at', '>', now()->subMonths($months))->first()->id;
+ });
+ }
+
public static function coldGet($id, $start = 0, $stop = 400)
{
$stop = $stop > 400 ? 400 : $stop;
- $ids = Notification::whereProfileId($id)
- ->latest()
+ $ids = Notification::where('id', '>', self::getEpochId())
+ ->where('profile_id', $id)
+ ->orderByDesc('id')
->skip($start)
->take($stop)
->pluck('id');
@@ -227,7 +237,7 @@ class NotificationService {
public static function getNotification($id)
{
- $notification = Cache::remember('service:notification:'.$id, 86400, function() use($id) {
+ $notification = Cache::remember('service:notification:'.$id, self::ITEM_CACHE_TTL, function() use($id) {
$n = Notification::with('item')->find($id);
if(!$n) {
@@ -259,19 +269,20 @@ class NotificationService {
public static function setNotification(Notification $notification)
{
- return Cache::remember('service:notification:'.$notification->id, now()->addDays(3), function() use($notification) {
+ return Cache::remember('service:notification:'.$notification->id, self::ITEM_CACHE_TTL, function() use($notification) {
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($notification, new NotificationTransformer());
return $fractal->createData($resource)->toArray();
});
- }
+ }
public static function warmCache($id, $stop = 400, $force = false)
{
if(self::count($id) == 0 || $force == true) {
- $ids = Notification::whereProfileId($id)
- ->latest()
+ $ids = Notification::where('profile_id', $id)
+ ->where('id', '>', self::getEpochId())
+ ->orderByDesc('id')
->limit($stop)
->pluck('id');
foreach($ids as $key) {
diff --git a/app/Services/PublicTimelineService.php b/app/Services/PublicTimelineService.php
index e1275065c..7cd6816b3 100644
--- a/app/Services/PublicTimelineService.php
+++ b/app/Services/PublicTimelineService.php
@@ -49,9 +49,7 @@ class PublicTimelineService {
public static function add($val)
{
if(self::count() > 400) {
- if(config('database.redis.client') === 'phpredis') {
- Redis::zpopmin(self::CACHE_KEY);
- }
+ Redis::zpopmin(self::CACHE_KEY);
}
return Redis::zadd(self::CACHE_KEY, $val, $val);
@@ -97,7 +95,7 @@ class PublicTimelineService {
if(self::count() == 0 || $force == true) {
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
Redis::del(self::CACHE_KEY);
- $minId = SnowflakeService::byDate(now()->subDays(14));
+ $minId = SnowflakeService::byDate(now()->subDays(90));
$ids = Status::where('id', '>', $minId)
->whereNull(['uri', 'in_reply_to_id', 'reblog_of_id'])
->when($hideNsfw, function($q, $hideNsfw) {
@@ -107,9 +105,11 @@ class PublicTimelineService {
->whereScope('public')
->orderByDesc('id')
->limit($limit)
- ->pluck('id');
- foreach($ids as $id) {
- self::add($id);
+ ->pluck('id', 'profile_id');
+ foreach($ids as $k => $id) {
+ if(AdminShadowFilterService::canAddToPublicFeedByProfileId($k)) {
+ self::add($id);
+ }
}
return 1;
}
diff --git a/app/Services/RelationshipService.php b/app/Services/RelationshipService.php
index 3c6d2818f..476c9c9ae 100644
--- a/app/Services/RelationshipService.php
+++ b/app/Services/RelationshipService.php
@@ -66,6 +66,14 @@ class RelationshipService
return self::get($aid, $tid);
}
+ public static function forget($aid, $tid)
+ {
+ Cache::forget('pf:services:follower:audience:' . $aid);
+ Cache::forget('pf:services:follower:audience:' . $tid);
+ self::delete($tid, $aid);
+ self::delete($aid, $tid);
+ }
+
public static function defaultRelation($tid)
{
return [
diff --git a/app/Services/ResilientMediaStorageService.php b/app/Services/ResilientMediaStorageService.php
new file mode 100644
index 000000000..ac1b089af
--- /dev/null
+++ b/app/Services/ResilientMediaStorageService.php
@@ -0,0 +1,66 @@
+putFileAs($storagePath, new File($path), $name, 'public');
+ return $disk->url($file);
+ }, random_int(100, 500));
+ }
+
+ public static function handleResilientStore($storagePath, $path, $name)
+ {
+ $attempts = 0;
+ return retry(4, function() use($storagePath, $path, $name, $attempts) {
+ self::$attempts++;
+ usleep(100000);
+ $baseDisk = self::$attempts > 1 ? self::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;
+ });
+ }
+
+ public static function getAltDriver()
+ {
+ $drivers = [];
+ if(config('filesystems.disks.alt-primary.enabled')) {
+ $drivers[] = 'alt-primary';
+ }
+ if(config('filesystems.disks.alt-secondary.enabled')) {
+ $drivers[] = 'alt-secondary';
+ }
+ if(empty($drivers)) {
+ return false;
+ }
+ $key = array_rand($drivers, 1);
+ return $drivers[$key];
+ }
+}
diff --git a/app/Services/SearchApiV2Service.php b/app/Services/SearchApiV2Service.php
index 465bd98a4..90691f0bd 100644
--- a/app/Services/SearchApiV2Service.php
+++ b/app/Services/SearchApiV2Service.php
@@ -18,317 +18,355 @@ use App\Services\StatusService;
class SearchApiV2Service
{
- private $query;
- static $mastodonMode = false;
+ private $query;
+ static $mastodonMode = false;
- public static function query($query, $mastodonMode = false)
- {
- self::$mastodonMode = $mastodonMode;
- return (new self)->run($query);
- }
+ public static function query($query, $mastodonMode = false)
+ {
+ self::$mastodonMode = $mastodonMode;
+ return (new self)->run($query);
+ }
- protected function run($query)
- {
- $this->query = $query;
- $q = urldecode($query->input('q'));
+ protected function run($query)
+ {
+ $this->query = $query;
+ $q = urldecode($query->input('q'));
- if($query->has('resolve') &&
- ( Str::startsWith($q, 'https://') ||
- Str::substrCount($q, '@') >= 1)
- ) {
- return $this->resolveQuery();
- }
+ if($query->has('resolve') &&
+ ( Str::startsWith($q, 'https://') ||
+ Str::substrCount($q, '@') >= 1)
+ ) {
+ return $this->resolveQuery();
+ }
- if($query->has('type')) {
- switch ($query->input('type')) {
- case 'accounts':
- return [
- 'accounts' => $this->accounts(),
- 'hashtags' => [],
- 'statuses' => []
- ];
- break;
- case 'hashtags':
- return [
- 'accounts' => [],
- 'hashtags' => $this->hashtags(),
- 'statuses' => []
- ];
- break;
- case 'statuses':
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => $this->statuses()
- ];
- break;
- }
- }
+ if($query->has('type')) {
+ switch ($query->input('type')) {
+ case 'accounts':
+ return [
+ 'accounts' => $this->accounts(),
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ break;
+ case 'hashtags':
+ return [
+ 'accounts' => [],
+ 'hashtags' => $this->hashtags(),
+ 'statuses' => []
+ ];
+ break;
+ case 'statuses':
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => $this->statuses()
+ ];
+ break;
+ }
+ }
- if($query->has('account_id')) {
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => $this->statusesById()
- ];
- }
+ if($query->has('account_id')) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => $this->statusesById()
+ ];
+ }
- return [
- 'accounts' => $this->accounts(),
- 'hashtags' => $this->hashtags(),
- 'statuses' => $this->statuses()
- ];
- }
+ return [
+ 'accounts' => $this->accounts(),
+ 'hashtags' => $this->hashtags(),
+ 'statuses' => $this->statuses()
+ ];
+ }
- protected function accounts($initalQuery = false)
- {
- $mastodonMode = self::$mastodonMode;
- $user = request()->user();
- $limit = $this->query->input('limit') ?? 20;
- $offset = $this->query->input('offset') ?? 0;
- $rawQuery = $initalQuery ? $initalQuery : $this->query->input('q');
- $query = $rawQuery . '%';
- $webfingerQuery = $query;
- if(Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') {
- $query = '@' . $query;
- }
- if(substr($webfingerQuery, 0, 1) !== '@') {
- $webfingerQuery = '@' . $webfingerQuery;
- }
- $banned = InstanceService::getBannedDomains();
- $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
- $results = Profile::select('username', 'id', 'followers_count', 'domain')
- ->where('username', $operator, $query)
- ->orWhere('webfinger', $operator, $webfingerQuery)
- ->orderByDesc('profiles.followers_count')
- ->offset($offset)
- ->limit($limit)
- ->get()
- ->filter(function($profile) use ($banned) {
- return in_array($profile->domain, $banned) == false;
- })
- ->map(function($res) use($mastodonMode) {
- return $mastodonMode ?
- AccountService::getMastodon($res['id']) :
- AccountService::get($res['id']);
- })
- ->filter(function($account) {
- return $account && isset($account['id']);
- })
- ->values();
+ protected function accounts($initalQuery = false)
+ {
+ $mastodonMode = self::$mastodonMode;
+ $user = request()->user();
+ $limit = $this->query->input('limit') ?? 20;
+ $offset = $this->query->input('offset') ?? 0;
+ $rawQuery = $initalQuery ? $initalQuery : $this->query->input('q');
+ $query = $rawQuery . '%';
+ $webfingerQuery = $query;
+ if(Str::substrCount($rawQuery, '@') == 1 && substr($rawQuery, 0, 1) !== '@') {
+ $query = '@' . $query;
+ }
+ if(substr($webfingerQuery, 0, 1) !== '@') {
+ $webfingerQuery = '@' . $webfingerQuery;
+ }
+ $banned = InstanceService::getBannedDomains();
+ $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
+ $results = Profile::select('username', 'id', 'followers_count', 'domain')
+ ->where('username', $operator, $query)
+ ->orWhere('webfinger', $operator, $webfingerQuery)
+ ->orderByDesc('profiles.followers_count')
+ ->offset($offset)
+ ->limit($limit)
+ ->get()
+ ->filter(function($profile) use ($banned) {
+ return in_array($profile->domain, $banned) == false;
+ })
+ ->map(function($res) use($mastodonMode) {
+ return $mastodonMode ?
+ AccountService::getMastodon($res['id']) :
+ AccountService::get($res['id']);
+ })
+ ->filter(function($account) {
+ return $account && isset($account['id']);
+ })
+ ->values();
- return $results;
- }
+ return $results;
+ }
- protected function hashtags()
- {
- $mastodonMode = self::$mastodonMode;
- $q = $this->query->input('q');
- $limit = $this->query->input('limit') ?? 20;
- $offset = $this->query->input('offset') ?? 0;
- $query = Str::startsWith($q, '#') ? '%' . substr($q, 1) . '%' : '%' . $q . '%';
- $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
- return Hashtag::where('name', $operator, $query)
- ->orWhere('slug', $operator, $query)
- ->where(function($q) {
- return $q->where('can_search', true)
- ->orWhereNull('can_search');
- })
- ->orderByDesc('cached_count')
- ->offset($offset)
- ->limit($limit)
- ->get()
- ->map(function($tag) use($mastodonMode) {
- $res = [
- 'name' => $tag->name,
- 'url' => $tag->url()
- ];
+ protected function hashtags()
+ {
+ $mastodonMode = self::$mastodonMode;
+ $q = $this->query->input('q');
+ $limit = $this->query->input('limit') ?? 20;
+ $offset = $this->query->input('offset') ?? 0;
+ $query = Str::startsWith($q, '#') ? '%' . substr($q, 1) . '%' : '%' . $q . '%';
+ $operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
+ return Hashtag::where('name', $operator, $query)
+ ->orWhere('slug', $operator, $query)
+ ->where(function($q) {
+ return $q->where('can_search', true)
+ ->orWhereNull('can_search');
+ })
+ ->orderByDesc('cached_count')
+ ->offset($offset)
+ ->limit($limit)
+ ->get()
+ ->map(function($tag) use($mastodonMode) {
+ $res = [
+ 'name' => $tag->name,
+ 'url' => $tag->url()
+ ];
- if(!$mastodonMode) {
- $res['history'] = [];
- $res['count'] = HashtagService::count($tag->id);
- }
+ if(!$mastodonMode) {
+ $res['history'] = [];
+ $res['count'] = HashtagService::count($tag->id);
+ }
- return $res;
- });
- }
+ return $res;
+ });
+ }
- protected function statuses()
- {
- // Removed until we provide more relevent sorting/results
- return [];
- }
+ protected function statuses()
+ {
+ // Removed until we provide more relevent sorting/results
+ return [];
+ }
- protected function statusesById()
- {
- // Removed until we provide more relevent sorting/results
- return [];
- }
+ protected function statusesById()
+ {
+ // Removed until we provide more relevent sorting/results
+ return [];
+ }
- protected function resolveQuery()
- {
- $default = [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => [],
- ];
- $mastodonMode = self::$mastodonMode;
- $query = urldecode($this->query->input('q'));
- if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) {
- $default['accounts'] = $this->accounts(substr($query, 1));
- return $default;
- }
- if(Helpers::validateLocalUrl($query)) {
- if(Str::contains($query, '/p/')) {
- return $this->resolveLocalStatus();
- } else {
- return $this->resolveLocalProfile();
- }
- } else {
- if(!Helpers::validateUrl($query) && strpos($query, '@') == -1) {
- return $default;
- }
+ protected function resolveQuery()
+ {
+ $default = [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => [],
+ ];
+ $mastodonMode = self::$mastodonMode;
+ $query = urldecode($this->query->input('q'));
+ if(substr($query, 0, 1) === '@' && !Str::contains($query, '.')) {
+ $default['accounts'] = $this->accounts(substr($query, 1));
+ return $default;
+ }
+ if(Helpers::validateLocalUrl($query)) {
+ if(Str::contains($query, '/p/') || Str::contains($query, 'i/web/post/')) {
+ return $this->resolveLocalStatus();
+ } else if(Str::contains($query, 'i/web/profile/')) {
+ return $this->resolveLocalProfileId();
+ } else {
+ return $this->resolveLocalProfile();
+ }
+ } else {
+ if(!Helpers::validateUrl($query) && strpos($query, '@') == -1) {
+ return $default;
+ }
- if(!Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) {
- try {
- $res = WebfingerService::lookup('@' . $query, $mastodonMode);
- } catch (\Exception $e) {
- return $default;
- }
- if($res && isset($res['id'])) {
- $default['accounts'][] = $res;
- return $default;
- } else {
- return $default;
- }
- }
+ if(!Str::startsWith($query, 'http') && Str::substrCount($query, '@') == 1 && strpos($query, '@') !== 0) {
+ try {
+ $res = WebfingerService::lookup('@' . $query, $mastodonMode);
+ } catch (\Exception $e) {
+ return $default;
+ }
+ if($res && isset($res['id'])) {
+ $default['accounts'][] = $res;
+ return $default;
+ } else {
+ return $default;
+ }
+ }
- if(Str::substrCount($query, '@') == 2) {
- try {
- $res = WebfingerService::lookup($query, $mastodonMode);
- } catch (\Exception $e) {
- return $default;
- }
- if($res && isset($res['id'])) {
- $default['accounts'][] = $res;
- return $default;
- } else {
- return $default;
- }
- }
+ if(Str::substrCount($query, '@') == 2) {
+ try {
+ $res = WebfingerService::lookup($query, $mastodonMode);
+ } catch (\Exception $e) {
+ return $default;
+ }
+ if($res && isset($res['id'])) {
+ $default['accounts'][] = $res;
+ return $default;
+ } else {
+ return $default;
+ }
+ }
- try {
- $res = ActivityPubFetchService::get($query);
- $banned = InstanceService::getBannedDomains();
- if($res) {
- $json = json_decode($res, true);
+ if($sid = Status::whereUri($query)->first()) {
+ $s = StatusService::get($sid->id, false);
+ if(in_array($s['visibility'], ['public', 'unlisted'])) {
+ $default['statuses'][] = $s;
+ return $default;
+ }
+ }
- if(!$json || !isset($json['@context']) || !isset($json['type']) || !in_array($json['type'], ['Note', 'Person'])) {
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => [],
- ];
- }
+ try {
+ $res = ActivityPubFetchService::get($query);
+ $banned = InstanceService::getBannedDomains();
+ if($res) {
+ $json = json_decode($res, true);
- switch($json['type']) {
- case 'Note':
- $obj = Helpers::statusFetch($query);
- if(!$obj || !isset($obj['id'])) {
- return $default;
- }
- $note = $mastodonMode ?
- StatusService::getMastodon($obj['id']) :
- StatusService::get($obj['id']);
- if(!$note) {
- return $default;
- }
- $default['statuses'][] = $note;
- return $default;
- break;
+ if(!$json || !isset($json['@context']) || !isset($json['type']) || !in_array($json['type'], ['Note', 'Person'])) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => [],
+ ];
+ }
- case 'Person':
- $obj = Helpers::profileFetch($query);
- if(!$obj) {
- return $default;
- }
- if(in_array($obj['domain'], $banned)) {
- return $default;
- }
- $default['accounts'][] = $mastodonMode ?
- AccountService::getMastodon($obj['id']) :
- AccountService::get($obj['id']);
- return $default;
- break;
+ switch($json['type']) {
+ case 'Note':
+ $obj = Helpers::statusFetch($query);
+ if(!$obj || !isset($obj['id'])) {
+ return $default;
+ }
+ $note = $mastodonMode ?
+ StatusService::getMastodon($obj['id'], false) :
+ StatusService::get($obj['id'], false);
+ if(!$note) {
+ return $default;
+ }
+ if(!isset($note['visibility']) || !in_array($note['visibility'], ['public', 'unlisted'])) {
+ return $default;
+ }
+ $default['statuses'][] = $note;
+ return $default;
+ break;
- default:
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => [],
- ];
- break;
- }
- }
- } catch (\Exception $e) {
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => [],
- ];
- }
+ case 'Person':
+ $obj = Helpers::profileFetch($query);
+ if(!$obj) {
+ return $default;
+ }
+ if(in_array($obj['domain'], $banned)) {
+ return $default;
+ }
+ $default['accounts'][] = $mastodonMode ?
+ AccountService::getMastodon($obj['id'], true) :
+ AccountService::get($obj['id'], true);
+ return $default;
+ break;
- return $default;
- }
- }
+ default:
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => [],
+ ];
+ break;
+ }
+ }
+ } catch (\Exception $e) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => [],
+ ];
+ }
- protected function resolveLocalStatus()
- {
- $query = urldecode($this->query->input('q'));
- $query = last(explode('/', $query));
- $status = StatusService::getMastodon($query);
- if(!$status) {
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => []
- ];
- }
+ return $default;
+ }
+ }
- $res = [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => [$status]
- ];
+ protected function resolveLocalStatus()
+ {
+ $query = urldecode($this->query->input('q'));
+ $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
+ $status = StatusService::getMastodon($query, false);
+ if(!$status || !in_array($status['visibility'], ['public', 'unlisted'])) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
- return $res;
- }
+ $res = [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => [$status]
+ ];
- protected function resolveLocalProfile()
- {
- $query = urldecode($this->query->input('q'));
- $query = last(explode('/', $query));
- $profile = Profile::whereNull('status')
- ->whereNull('domain')
- ->whereUsername($query)
- ->first();
+ return $res;
+ }
- if(!$profile) {
- return [
- 'accounts' => [],
- 'hashtags' => [],
- 'statuses' => []
- ];
- }
+ protected function resolveLocalProfile()
+ {
+ $query = urldecode($this->query->input('q'));
+ $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
+ $profile = Profile::whereNull('status')
+ ->whereNull('domain')
+ ->whereUsername($query)
+ ->first();
- $fractal = new Fractal\Manager();
- $fractal->setSerializer(new ArraySerializer());
- $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
- return [
- 'accounts' => $fractal->createData($resource)->toArray(),
- 'hashtags' => [],
- 'statuses' => []
- ];
- }
+ if(!$profile) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
+
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
+ return [
+ 'accounts' => [$fractal->createData($resource)->toArray()],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
+
+ protected function resolveLocalProfileId()
+ {
+ $query = urldecode($this->query->input('q'));
+ $query = last(explode('/', parse_url($query, PHP_URL_PATH)));
+ $profile = Profile::whereNull('status')
+ ->find($query);
+
+ if(!$profile) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
+
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
+ return [
+ 'accounts' => [$fractal->createData($resource)->toArray()],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
}
diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php
index 99bcee2b0..4051bede4 100644
--- a/app/Services/StatusService.php
+++ b/app/Services/StatusService.php
@@ -14,173 +14,181 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter;
class StatusService
{
- const CACHE_KEY = 'pf:services:status:';
+ const CACHE_KEY = 'pf:services:status:';
- public static function key($id, $publicOnly = true)
- {
- $p = $publicOnly ? 'pub:' : 'all:';
- return self::CACHE_KEY . $p . $id;
- }
+ public static function key($id, $publicOnly = true)
+ {
+ $p = $publicOnly ? 'pub:' : 'all:';
+ return self::CACHE_KEY . $p . $id;
+ }
- public static function get($id, $publicOnly = true)
- {
- return Cache::remember(self::key($id, $publicOnly), now()->addDays(7), function() use($id, $publicOnly) {
- if($publicOnly) {
- $status = Status::whereScope('public')->find($id);
- } else {
- $status = Status::whereIn('scope', ['public', 'private', 'unlisted', 'group'])->find($id);
- }
- if(!$status) {
- return null;
- }
- $fractal = new Fractal\Manager();
- $fractal->setSerializer(new ArraySerializer());
- $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
- return $fractal->createData($resource)->toArray();
- });
- }
+ public static function get($id, $publicOnly = true, $mastodonMode = false)
+ {
+ $res = Cache::remember(self::key($id, $publicOnly), 21600, function() use($id, $publicOnly) {
+ if($publicOnly) {
+ $status = Status::whereScope('public')->find($id);
+ } else {
+ $status = Status::whereIn('scope', ['public', 'private', 'unlisted', 'group'])->find($id);
+ }
+ if(!$status) {
+ return null;
+ }
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
+ $res = $fractal->createData($resource)->toArray();
+ $res['_pid'] = isset($res['account']) && isset($res['account']['id']) ? $res['account']['id'] : null;
+ if(isset($res['_pid'])) {
+ unset($res['account']);
+ }
+ return $res;
+ });
+ if($res && isset($res['_pid'])) {
+ $res['account'] = $mastodonMode === true ? AccountService::getMastodon($res['_pid'], true) : AccountService::get($res['_pid'], true);
+ unset($res['_pid']);
+ }
+ return $res;
+ }
- public static function getMastodon($id, $publicOnly = true)
- {
- $status = self::get($id, $publicOnly);
- if(!$status) {
- return null;
- }
+ public static function getMastodon($id, $publicOnly = true)
+ {
+ $status = self::get($id, $publicOnly, true);
+ if(!$status) {
+ return null;
+ }
- if(!isset($status['account'])) {
- return null;
- }
+ if(!isset($status['account'])) {
+ return null;
+ }
$status['replies_count'] = $status['reply_count'];
- if(config('exp.emc') == false) {
- return $status;
- }
+ if(config('exp.emc') == false) {
+ return $status;
+ }
- unset(
- $status['_v'],
- $status['comments_disabled'],
- $status['content_text'],
- $status['gid'],
- $status['label'],
- $status['liked_by'],
- $status['local'],
- $status['parent'],
- $status['pf_type'],
- $status['place'],
- $status['replies'],
- $status['reply_count'],
- $status['shortcode'],
- $status['taggedPeople'],
- $status['thread'],
- $status['pinned'],
- $status['account']['header_bg'],
- $status['account']['is_admin'],
- $status['account']['last_fetched_at'],
- $status['account']['local'],
- $status['account']['location'],
- $status['account']['note_text'],
- $status['account']['pronouns'],
- $status['account']['website'],
- $status['media_attachments'],
- );
- $status['account']['avatar_static'] = $status['account']['avatar'];
- $status['account']['bot'] = false;
- $status['account']['emojis'] = [];
- $status['account']['fields'] = [];
- $status['account']['header'] = url('/storage/headers/missing.png');
- $status['account']['header_static'] = url('/storage/headers/missing.png');
- $status['account']['last_status_at'] = null;
+ unset(
+ $status['_v'],
+ $status['comments_disabled'],
+ $status['content_text'],
+ $status['gid'],
+ $status['label'],
+ $status['liked_by'],
+ $status['local'],
+ $status['parent'],
+ $status['pf_type'],
+ $status['place'],
+ $status['replies'],
+ $status['reply_count'],
+ $status['shortcode'],
+ $status['taggedPeople'],
+ $status['thread'],
+ $status['pinned'],
+ $status['account']['header_bg'],
+ $status['account']['is_admin'],
+ $status['account']['last_fetched_at'],
+ $status['account']['local'],
+ $status['account']['location'],
+ $status['account']['note_text'],
+ $status['account']['pronouns'],
+ $status['account']['website'],
+ $status['media_attachments'],
+ );
+ $status['account']['avatar_static'] = $status['account']['avatar'];
+ $status['account']['bot'] = false;
+ $status['account']['emojis'] = [];
+ $status['account']['fields'] = [];
+ $status['account']['header'] = url('/storage/headers/missing.png');
+ $status['account']['header_static'] = url('/storage/headers/missing.png');
+ $status['account']['last_status_at'] = null;
- $status['media_attachments'] = array_values(MediaService::getMastodon($status['id']));
- $status['muted'] = false;
- $status['reblogged'] = false;
+ $status['media_attachments'] = array_values(MediaService::getMastodon($status['id']));
+ $status['muted'] = false;
+ $status['reblogged'] = false;
- return $status;
- }
+ return $status;
+ }
- public static function getState($id, $pid)
- {
- $status = self::get($id, false);
+ public static function getState($id, $pid)
+ {
+ $status = self::get($id, false);
- if(!$status) {
- return [
- 'liked' => false,
- 'shared' => false,
- 'bookmarked' => false
- ];
- }
+ if(!$status) {
+ return [
+ 'liked' => false,
+ 'shared' => false,
+ 'bookmarked' => false
+ ];
+ }
- return [
- 'liked' => LikeService::liked($pid, $id),
- 'shared' => self::isShared($id, $pid),
- 'bookmarked' => self::isBookmarked($id, $pid)
- ];
- }
+ return [
+ 'liked' => LikeService::liked($pid, $id),
+ 'shared' => self::isShared($id, $pid),
+ 'bookmarked' => self::isBookmarked($id, $pid)
+ ];
+ }
- public static function getFull($id, $pid, $publicOnly = true)
- {
- $res = self::get($id, $publicOnly);
- if(!$res || !isset($res['account']) || !isset($res['account']['id'])) {
- return $res;
- }
- $res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
- return $res;
- }
+ public static function getFull($id, $pid, $publicOnly = true)
+ {
+ $res = self::get($id, $publicOnly);
+ if(!$res || !isset($res['account']) || !isset($res['account']['id'])) {
+ return $res;
+ }
+ $res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
+ return $res;
+ }
- public static function getDirectMessage($id)
- {
- $status = Status::whereScope('direct')->find($id);
+ public static function getDirectMessage($id)
+ {
+ $status = Status::whereScope('direct')->find($id);
- if(!$status) {
- return null;
- }
+ if(!$status) {
+ return null;
+ }
- $fractal = new Fractal\Manager();
- $fractal->setSerializer(new ArraySerializer());
- $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
- return $fractal->createData($resource)->toArray();
- }
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
+ return $fractal->createData($resource)->toArray();
+ }
- public static function del($id, $purge = false)
- {
- if($purge) {
- $status = self::get($id);
- if($status && isset($status['account']) && isset($status['account']['id'])) {
- Cache::forget('profile:embed:' . $status['account']['id']);
- }
- Cache::forget('status:transformer:media:attachments:' . $id);
- MediaService::del($id);
- Cache::forget('status:thumb:nsfw0' . $id);
- Cache::forget('status:thumb:nsfw1' . $id);
- Cache::forget('pf:services:sh:id:' . $id);
- PublicTimelineService::rem($id);
- NetworkTimelineService::rem($id);
- }
+ public static function del($id, $purge = false)
+ {
+ if($purge) {
+ $status = self::get($id);
+ if($status && isset($status['account']) && isset($status['account']['id'])) {
+ Cache::forget('profile:embed:' . $status['account']['id']);
+ }
+ Cache::forget('status:transformer:media:attachments:' . $id);
+ MediaService::del($id);
+ Cache::forget('pf:services:sh:id:' . $id);
+ PublicTimelineService::rem($id);
+ NetworkTimelineService::rem($id);
+ }
- Cache::forget(self::key($id, false));
- return Cache::forget(self::key($id));
- }
+ Cache::forget(self::key($id, false));
+ return Cache::forget(self::key($id));
+ }
- public static function refresh($id)
- {
- Cache::forget(self::key($id, false));
- Cache::forget(self::key($id, true));
- self::get($id, false);
- self::get($id, true);
- }
+ public static function refresh($id)
+ {
+ Cache::forget(self::key($id, false));
+ Cache::forget(self::key($id, true));
+ self::get($id, false);
+ self::get($id, true);
+ }
- public static function isShared($id, $pid = null)
- {
- return $pid ?
- ReblogService::get($pid, $id) :
- false;
- }
+ public static function isShared($id, $pid = null)
+ {
+ return $pid ?
+ ReblogService::get($pid, $id) :
+ false;
+ }
- public static function isBookmarked($id, $pid = null)
- {
- return $pid ?
- BookmarkService::get($pid, $id) :
- false;
- }
+ public static function isBookmarked($id, $pid = null)
+ {
+ return $pid ?
+ BookmarkService::get($pid, $id) :
+ false;
+ }
}
diff --git a/app/Status.php b/app/Status.php
index 77262597e..d665464ae 100644
--- a/app/Status.php
+++ b/app/Status.php
@@ -9,7 +9,9 @@ use App\Http\Controllers\StatusController;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Poll;
use App\Services\AccountService;
+use App\Services\StatusService;
use App\Models\StatusEdit;
+use Illuminate\Support\Str;
class Status extends Model
{
@@ -95,16 +97,30 @@ class Status extends Model
public function thumb($showNsfw = false)
{
- $key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id;
- return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) {
- $type = $this->type ?? $this->setType();
- $is_nsfw = !$showNsfw ? $this->is_nsfw : false;
- if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {
- return url(Storage::url('public/no-preview.png'));
- }
+ $entity = StatusService::get($this->id, false);
- return url(Storage::url($this->firstMedia()->thumbnail_path));
- });
+ if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
+ return url(Storage::url('public/no-preview.png'));
+ }
+
+ if((!isset($entity['sensitive']) || $entity['sensitive']) && !$showNsfw) {
+ return url(Storage::url('public/no-preview.png'));
+ }
+
+ if(!isset($entity['visibility']) || !in_array($entity['visibility'], ['public', 'unlisted'])) {
+ return url(Storage::url('public/no-preview.png'));
+ }
+
+ return collect($entity['media_attachments'])
+ ->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png']))
+ ->map(function($media) {
+ if(!Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) {
+ return $media['preview_url'];
+ }
+
+ return $media['url'];
+ })
+ ->first() ?? url(Storage::url('public/no-preview.png'));
}
public function url($forceLocal = false)
diff --git a/app/Transformer/ActivityPub/ProfileTransformer.php b/app/Transformer/ActivityPub/ProfileTransformer.php
index 29f53425c..cdd4eb82d 100644
--- a/app/Transformer/ActivityPub/ProfileTransformer.php
+++ b/app/Transformer/ActivityPub/ProfileTransformer.php
@@ -4,17 +4,28 @@ namespace App\Transformer\ActivityPub;
use App\Profile;
use League\Fractal;
+use App\Services\AccountService;
class ProfileTransformer extends Fractal\TransformerAbstract
{
public function transform(Profile $profile)
{
- return [
+ $res = [
'@context' => [
'https://w3id.org/security/v1',
'https://www.w3.org/ns/activitystreams',
[
+ 'toot' => 'http://joinmastodon.org/ns#',
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
+ 'alsoKnownAs' => [
+ '@id' => 'as:alsoKnownAs',
+ '@type' => '@id'
+ ],
+ 'movedTo' => [
+ '@id' => 'as:movedTo',
+ '@type' => '@id'
+ ],
+ 'indexable' => 'toot:indexable',
],
],
'id' => $profile->permalink(),
@@ -28,6 +39,7 @@ class ProfileTransformer extends Fractal\TransformerAbstract
'summary' => $profile->bio,
'url' => $profile->url(),
'manuallyApprovesFollowers' => (bool) $profile->is_private,
+ 'indexable' => (bool) $profile->indexable,
'publicKey' => [
'id' => $profile->permalink().'#main-key',
'owner' => $profile->permalink(),
@@ -42,5 +54,15 @@ class ProfileTransformer extends Fractal\TransformerAbstract
'sharedInbox' => config('app.url') . '/f/inbox'
]
];
+
+ if($profile->aliases->count()) {
+ $res['alsoKnownAs'] = $profile->aliases->map(fn($alias) => $alias->uri);
+ }
+
+ if($profile->moved_to_profile_id) {
+ $res['movedTo'] = AccountService::get($profile->moved_to_profile_id)['url'];
+ }
+
+ return $res;
}
}
diff --git a/app/Transformer/ActivityPub/Verb/CreateNote.php b/app/Transformer/ActivityPub/Verb/CreateNote.php
index a9d40d9ed..55fdfa8f4 100644
--- a/app/Transformer/ActivityPub/Verb/CreateNote.php
+++ b/app/Transformer/ActivityPub/Verb/CreateNote.php
@@ -81,7 +81,8 @@ class CreateNote extends Fractal\TransformerAbstract
'@type' => '@id'
],
'toot' => 'http://joinmastodon.org/ns#',
- 'Emoji' => 'toot:Emoji'
+ 'Emoji' => 'toot:Emoji',
+ 'blurhash' => 'toot:blurhash',
]
],
'id' => $status->permalink(),
@@ -103,12 +104,22 @@ class CreateNote extends Fractal\TransformerAbstract
'cc' => $status->scopeToAudience('cc'),
'sensitive' => (bool) $status->is_nsfw,
'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
- return [
+ $res = [
'type' => $media->activityVerb(),
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => $media->caption,
];
+ if($media->blurhash) {
+ $res['blurhash'] = $media->blurhash;
+ }
+ if($media->width) {
+ $res['width'] = $media->width;
+ }
+ if($media->height) {
+ $res['height'] = $media->height;
+ }
+ return $res;
})->toArray(),
'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled,
diff --git a/app/Transformer/ActivityPub/Verb/Note.php b/app/Transformer/ActivityPub/Verb/Note.php
index 777bd22b0..1350641d4 100644
--- a/app/Transformer/ActivityPub/Verb/Note.php
+++ b/app/Transformer/ActivityPub/Verb/Note.php
@@ -82,7 +82,8 @@ class Note extends Fractal\TransformerAbstract
'@type' => '@id'
],
'toot' => 'http://joinmastodon.org/ns#',
- 'Emoji' => 'toot:Emoji'
+ 'Emoji' => 'toot:Emoji',
+ 'blurhash' => 'toot:blurhash',
]
],
'id' => $status->url(),
@@ -97,12 +98,22 @@ class Note extends Fractal\TransformerAbstract
'cc' => $status->scopeToAudience('cc'),
'sensitive' => (bool) $status->is_nsfw,
'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
- return [
+ $res = [
'type' => $media->activityVerb(),
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => $media->caption,
];
+ if($media->blurhash) {
+ $res['blurhash'] = $media->blurhash;
+ }
+ if($media->width) {
+ $res['width'] = $media->width;
+ }
+ if($media->height) {
+ $res['height'] = $media->height;
+ }
+ return $res;
})->toArray(),
'tag' => $tags,
'commentsEnabled' => (bool) !$status->comments_disabled,
diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php
index 2ed381c1a..c21720509 100644
--- a/app/Transformer/Api/StatusStatelessTransformer.php
+++ b/app/Transformer/Api/StatusStatelessTransformer.php
@@ -33,7 +33,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
- 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
+ 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id, false) : null,
'content' => $status->rendered ?? $status->caption,
'content_text' => $status->caption,
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
diff --git a/app/User.php b/app/User.php
index 3f826c143..a39f650be 100644
--- a/app/User.php
+++ b/app/User.php
@@ -19,9 +19,10 @@ class User extends Authenticatable
* @var array
*/
protected $casts = [
- 'deleted_at' => 'datetime',
- 'email_verified_at' => 'datetime',
- '2fa_setup_at' => 'datetime'
+ 'deleted_at' => 'datetime',
+ 'email_verified_at' => 'datetime',
+ '2fa_setup_at' => 'datetime',
+ 'last_active_at' => 'datetime',
];
/**
@@ -30,7 +31,14 @@ class User extends Authenticatable
* @var array
*/
protected $fillable = [
- 'name', 'username', 'email', 'password', 'app_register_ip'
+ 'name',
+ 'username',
+ 'email',
+ 'password',
+ 'app_register_ip',
+ 'email_verified_at',
+ 'last_active_at',
+ 'register_source'
];
/**
@@ -39,8 +47,8 @@ class User extends Authenticatable
* @var array
*/
protected $hidden = [
- 'email', 'password', 'is_admin', 'remember_token',
- 'email_verified_at', '2fa_enabled', '2fa_secret',
+ 'email', 'password', 'is_admin', 'remember_token',
+ 'email_verified_at', '2fa_enabled', '2fa_secret',
'2fa_backup_codes', '2fa_setup_at', 'deleted_at',
'updated_at'
];
@@ -100,11 +108,11 @@ 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);
+ return AvatarService::get($this->profile_id);
}
}
diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php
index a61e1d572..2fa98b8bd 100644
--- a/app/Util/ActivityPub/Helpers.php
+++ b/app/Util/ActivityPub/Helpers.php
@@ -4,14 +4,14 @@ namespace App\Util\ActivityPub;
use DB, Cache, Purify, Storage, Request, Validator;
use App\{
- Activity,
- Follower,
- Instance,
- Like,
- Media,
- Notification,
- Profile,
- Status
+ Activity,
+ Follower,
+ Instance,
+ Like,
+ Media,
+ Notification,
+ Profile,
+ Status
};
use Zttp\Zttp;
use Carbon\Carbon;
@@ -40,766 +40,787 @@ use App\Models\Poll;
use Illuminate\Contracts\Cache\LockTimeoutException;
use App\Jobs\ProfilePipeline\IncrementPostCount;
use App\Jobs\ProfilePipeline\DecrementPostCount;
+use App\Services\DomainService;
use App\Services\UserFilterService;
class Helpers {
- public static function validateObject($data)
- {
- $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
-
- $valid = Validator::make($data, [
- 'type' => [
- 'required',
- 'string',
- Rule::in($verbs)
- ],
- 'id' => 'required|string',
- 'actor' => 'required|string|url',
- 'object' => 'required',
- 'object.type' => 'required_if:type,Create',
- 'object.attributedTo' => 'required_if:type,Create|url',
- 'published' => 'required_if:type,Create|date'
- ])->passes();
-
- return $valid;
- }
-
- public static function verifyAttachments($data)
- {
- if(!isset($data['object']) || empty($data['object'])) {
- $data = ['object'=>$data];
- }
-
- $activity = $data['object'];
-
- $mimeTypes = explode(',', config_cache('pixelfed.media_types'));
- $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
-
- // Peertube
- // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image'];
-
- if(!isset($activity['attachment']) || empty($activity['attachment'])) {
- return false;
- }
-
- // peertube
- // $attachment = is_array($activity['url']) ?
- // collect($activity['url'])
- // ->filter(function($media) {
- // return $media['type'] == 'Link' && $media['mediaType'] == 'video/mp4';
- // })
- // ->take(1)
- // ->values()
- // ->toArray()[0] : $activity['attachment'];
-
- $attachment = $activity['attachment'];
-
- $valid = Validator::make($attachment, [
- '*.type' => [
- 'required',
- 'string',
- Rule::in($mediaTypes)
- ],
- '*.url' => 'required|url',
- '*.mediaType' => [
- 'required',
- 'string',
- Rule::in($mimeTypes)
- ],
- '*.name' => 'sometimes|nullable|string'
- ])->passes();
-
- return $valid;
- }
-
- public static function normalizeAudience($data, $localOnly = true)
- {
- if(!isset($data['to'])) {
- return;
- }
-
- $audience = [];
- $audience['to'] = [];
- $audience['cc'] = [];
- $scope = 'private';
-
- if(is_array($data['to']) && !empty($data['to'])) {
- foreach ($data['to'] as $to) {
- if($to == 'https://www.w3.org/ns/activitystreams#Public') {
- $scope = 'public';
- continue;
- }
- $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to);
- if($url != false) {
- array_push($audience['to'], $url);
- }
- }
- }
-
- if(is_array($data['cc']) && !empty($data['cc'])) {
- foreach ($data['cc'] as $cc) {
- if($cc == 'https://www.w3.org/ns/activitystreams#Public') {
- $scope = 'unlisted';
- continue;
- }
- $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc);
- if($url != false) {
- array_push($audience['cc'], $url);
- }
- }
- }
- $audience['scope'] = $scope;
- return $audience;
- }
-
- public static function userInAudience($profile, $data)
- {
- $audience = self::normalizeAudience($data);
- $url = $profile->permalink();
- return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
- }
-
- public static function validateUrl($url)
- {
- if(is_array($url)) {
- $url = $url[0];
- }
-
- $hash = hash('sha256', $url);
- $key = "helpers:url:valid:sha256-{$hash}";
- $ttl = now()->addMinutes(5);
-
- $valid = Cache::remember($key, $ttl, function() use($url) {
- $localhosts = [
- '127.0.0.1', 'localhost', '::1'
- ];
-
- if(mb_substr($url, 0, 8) !== 'https://') {
- return false;
- }
-
- $valid = filter_var($url, FILTER_VALIDATE_URL);
-
- if(!$valid) {
- return false;
- }
-
- $host = parse_url($valid, PHP_URL_HOST);
-
- // if(count(dns_get_record($host, DNS_A | DNS_AAAA)) == 0) {
- // return false;
- // }
-
- if(config('costar.enabled') == true) {
- if(
- (config('costar.domain.block') != null && Str::contains($host, config('costar.domain.block')) == true) ||
- (config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true)
- ) {
- return false;
- }
- }
-
- if(app()->environment() === 'production') {
- $bannedInstances = InstanceService::getBannedDomains();
- if(in_array($host, $bannedInstances)) {
- return false;
- }
- }
-
-
- if(in_array($host, $localhosts)) {
- return false;
- }
-
- return $url;
- });
-
- return $valid;
- }
-
- public static function validateLocalUrl($url)
- {
- $url = self::validateUrl($url);
- if($url == true) {
- $domain = config('pixelfed.domain.app');
- $host = parse_url($url, PHP_URL_HOST);
- $url = $domain === $host ? $url : false;
- return $url;
- }
- return false;
- }
-
- public static function zttpUserAgent()
- {
- $version = config('pixelfed.version');
- $url = config('app.url');
- return [
- 'Accept' => 'application/activity+json',
- 'User-Agent' => "(Pixelfed/{$version}; +{$url})",
- ];
- }
-
- public static function fetchFromUrl($url = false)
- {
- if(self::validateUrl($url) == false) {
- return;
- }
-
- $hash = hash('sha256', $url);
- $key = "helpers:url:fetcher:sha256-{$hash}";
- $ttl = now()->addMinutes(15);
-
- return Cache::remember($key, $ttl, function() use($url) {
- $res = ActivityPubFetchService::get($url);
- if(!$res || empty($res)) {
- return false;
- }
- $res = json_decode($res, true, 8);
- if(json_last_error() == JSON_ERROR_NONE) {
- return $res;
- } else {
- return false;
- }
- });
- }
-
- public static function fetchProfileFromUrl($url)
- {
- return self::fetchFromUrl($url);
- }
-
- public static function pluckval($val)
- {
- if(is_string($val)) {
- return $val;
- }
-
- if(is_array($val)) {
- return !empty($val) ? $val[0] : null;
- }
-
- return null;
- }
-
- public static function statusFirstOrFetch($url, $replyTo = false)
- {
- $url = self::validateUrl($url);
- if($url == false) {
- return;
- }
-
- $host = parse_url($url, PHP_URL_HOST);
- $local = config('pixelfed.domain.app') == $host ? true : false;
-
- if($local) {
- $id = (int) last(explode('/', $url));
- return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
- }
-
- $cached = Status::whereNotIn('scope', ['draft','archived'])
- ->whereUri($url)
- ->orWhere('object_url', $url)
- ->first();
-
- if($cached) {
- return $cached;
- }
-
- $res = self::fetchFromUrl($url);
-
- if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) || !isset($res['published']) ) {
- return;
- }
-
- if(isset($res['object'])) {
- $activity = $res;
- } else {
- $activity = ['object' => $res];
- }
-
- $scope = 'private';
-
- $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
-
- if(isset($res['to']) == true) {
- if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
- $scope = 'public';
- }
- if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
- $scope = 'public';
- }
- }
-
- if(isset($res['cc']) == true) {
- if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
- $scope = 'unlisted';
- }
- if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
- $scope = 'unlisted';
- }
- }
-
- if(config('costar.enabled') == true) {
- $blockedKeywords = config('costar.keyword.block');
- if($blockedKeywords !== null) {
- $keywords = config('costar.keyword.block');
- foreach($keywords as $kw) {
- if(Str::contains($res['content'], $kw) == true) {
- return;
- }
- }
- }
-
- $unlisted = config('costar.domain.unlisted');
- if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
- $unlisted = true;
- $scope = 'unlisted';
- } else {
- $unlisted = false;
- }
-
- $cwDomains = config('costar.domain.cw');
- if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
- $cw = true;
- }
- }
-
- $id = isset($res['id']) ? self::pluckval($res['id']) : self::pluckval($url);
- $idDomain = parse_url($id, PHP_URL_HOST);
- $urlDomain = parse_url($url, PHP_URL_HOST);
-
- if(!self::validateUrl($id)) {
- return;
- }
-
- if(!isset($activity['object']['attributedTo'])) {
- return;
- }
-
- $attributedTo = is_string($activity['object']['attributedTo']) ?
- $activity['object']['attributedTo'] :
- (is_array($activity['object']['attributedTo']) ?
- collect($activity['object']['attributedTo'])
- ->filter(function($o) {
- return $o && isset($o['type']) && $o['type'] == 'Person';
- })
- ->pluck('id')
- ->first() : null
- );
-
- if($attributedTo) {
- $actorDomain = parse_url($attributedTo, PHP_URL_HOST);
- if(!self::validateUrl($attributedTo) ||
- $idDomain !== $actorDomain ||
- $actorDomain !== $urlDomain
- )
- {
- return;
- }
- }
-
- if($idDomain !== $urlDomain) {
- return;
- }
-
- $profile = self::profileFirstOrNew($attributedTo);
-
- if(!$profile) {
- return;
- }
-
- if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) {
- $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false);
- if($reply_to) {
- $blocks = UserFilterService::blocks($reply_to->profile_id);
- if(in_array($profile->id, $blocks)) {
- return;
- }
- }
- $reply_to = optional($reply_to)->id;
- } else {
- $reply_to = null;
- }
- $ts = self::pluckval($res['published']);
-
- if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
- $scope = 'unlisted';
- }
-
- if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
- $cw = true;
- }
-
- if($res['type'] === 'Question') {
- $status = self::storePoll(
- $profile,
- $res,
- $url,
- $ts,
- $reply_to,
- $cw,
- $scope,
- $id
- );
- return $status;
- } else {
- $status = self::storeStatus($url, $profile, $res);
- }
-
- return $status;
- }
-
- public static function storeStatus($url, $profile, $activity)
- {
- $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']);
- $url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
- $idDomain = parse_url($id, PHP_URL_HOST);
- $urlDomain = parse_url($url, PHP_URL_HOST);
- if(!self::validateUrl($id) || !self::validateUrl($url)) {
- return;
- }
-
- $reply_to = self::getReplyTo($activity);
-
- $ts = self::pluckval($activity['published']);
- $scope = self::getScope($activity, $url);
- $cw = self::getSensitive($activity, $url);
- $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
- $commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false;
-
- if(!$pid) {
- return;
- }
-
- $status = Status::updateOrCreate(
- [
- 'uri' => $url
- ], [
- 'profile_id' => $pid,
- 'url' => $url,
- 'object_url' => $id,
- 'caption' => isset($activity['content']) ? Purify::clean(strip_tags($activity['content'])) : null,
- 'rendered' => isset($activity['content']) ? Purify::clean($activity['content']) : null,
- 'created_at' => Carbon::parse($ts)->tz('UTC'),
- 'in_reply_to_id' => $reply_to,
- 'local' => false,
- 'is_nsfw' => $cw,
- 'scope' => $scope,
- 'visibility' => $scope,
- 'cw_summary' => ($cw == true && isset($activity['summary']) ?
- Purify::clean(strip_tags($activity['summary'])) : null),
- 'comments_disabled' => $commentsDisabled
- ]
- );
-
- if($reply_to == null) {
- self::importNoteAttachment($activity, $status);
- } else {
- if(isset($activity['attachment']) && !empty($activity['attachment'])) {
- self::importNoteAttachment($activity, $status);
- }
- StatusReplyPipeline::dispatch($status);
- }
-
- if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) {
- StatusTagsPipeline::dispatch($activity, $status);
- }
-
- if( config('instance.timeline.network.cached') &&
- $status->in_reply_to_id === null &&
- $status->reblog_of_id === null &&
- in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
- $status->created_at->gt(now()->subHours(config('instance.timeline.network.max_hours_old'))) &&
- (config('instance.hide_nsfw_on_public_feeds') == true ? $status->is_nsfw == false : true)
- ) {
- $filteredDomains = collect(InstanceService::getBannedDomains())
+ public static function validateObject($data)
+ {
+ $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
+
+ $valid = Validator::make($data, [
+ 'type' => [
+ 'required',
+ 'string',
+ Rule::in($verbs)
+ ],
+ 'id' => 'required|string',
+ 'actor' => 'required|string|url',
+ 'object' => 'required',
+ 'object.type' => 'required_if:type,Create',
+ 'object.attributedTo' => 'required_if:type,Create|url',
+ 'published' => 'required_if:type,Create|date'
+ ])->passes();
+
+ return $valid;
+ }
+
+ public static function verifyAttachments($data)
+ {
+ if(!isset($data['object']) || empty($data['object'])) {
+ $data = ['object'=>$data];
+ }
+
+ $activity = $data['object'];
+
+ $mimeTypes = explode(',', config_cache('pixelfed.media_types'));
+ $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
+
+ // Peertube
+ // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image'];
+
+ if(!isset($activity['attachment']) || empty($activity['attachment'])) {
+ return false;
+ }
+
+ // peertube
+ // $attachment = is_array($activity['url']) ?
+ // collect($activity['url'])
+ // ->filter(function($media) {
+ // return $media['type'] == 'Link' && $media['mediaType'] == 'video/mp4';
+ // })
+ // ->take(1)
+ // ->values()
+ // ->toArray()[0] : $activity['attachment'];
+
+ $attachment = $activity['attachment'];
+
+ $valid = Validator::make($attachment, [
+ '*.type' => [
+ 'required',
+ 'string',
+ Rule::in($mediaTypes)
+ ],
+ '*.url' => 'required|url',
+ '*.mediaType' => [
+ 'required',
+ 'string',
+ Rule::in($mimeTypes)
+ ],
+ '*.name' => 'sometimes|nullable|string',
+ '*.blurhash' => 'sometimes|nullable|string|min:6|max:164',
+ '*.width' => 'sometimes|nullable|integer|min:1|max:5000',
+ '*.height' => 'sometimes|nullable|integer|min:1|max:5000',
+ ])->passes();
+
+ return $valid;
+ }
+
+ public static function normalizeAudience($data, $localOnly = true)
+ {
+ if(!isset($data['to'])) {
+ return;
+ }
+
+ $audience = [];
+ $audience['to'] = [];
+ $audience['cc'] = [];
+ $scope = 'private';
+
+ if(is_array($data['to']) && !empty($data['to'])) {
+ foreach ($data['to'] as $to) {
+ if($to == 'https://www.w3.org/ns/activitystreams#Public') {
+ $scope = 'public';
+ continue;
+ }
+ $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to);
+ if($url != false) {
+ array_push($audience['to'], $url);
+ }
+ }
+ }
+
+ if(is_array($data['cc']) && !empty($data['cc'])) {
+ foreach ($data['cc'] as $cc) {
+ if($cc == 'https://www.w3.org/ns/activitystreams#Public') {
+ $scope = 'unlisted';
+ continue;
+ }
+ $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc);
+ if($url != false) {
+ array_push($audience['cc'], $url);
+ }
+ }
+ }
+ $audience['scope'] = $scope;
+ return $audience;
+ }
+
+ public static function userInAudience($profile, $data)
+ {
+ $audience = self::normalizeAudience($data);
+ $url = $profile->permalink();
+ return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
+ }
+
+ public static function validateUrl($url)
+ {
+ if(is_array($url)) {
+ $url = $url[0];
+ }
+
+ $hash = hash('sha256', $url);
+ $key = "helpers:url:valid:sha256-{$hash}";
+
+ $valid = Cache::remember($key, 900, function() use($url) {
+ $localhosts = [
+ '127.0.0.1', 'localhost', '::1'
+ ];
+
+ 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) {
+ return false;
+ }
+ }
+
+ if(app()->environment() === 'production') {
+ $bannedInstances = InstanceService::getBannedDomains();
+ if(in_array($host, $bannedInstances)) {
+ return false;
+ }
+ }
+
+ return $url;
+ });
+
+ return $valid;
+ }
+
+ public static function validateLocalUrl($url)
+ {
+ $url = self::validateUrl($url);
+ if($url == true) {
+ $domain = config('pixelfed.domain.app');
+ $host = parse_url($url, PHP_URL_HOST);
+ $url = strtolower($domain) === strtolower($host) ? $url : false;
+ return $url;
+ }
+ return false;
+ }
+
+ public static function zttpUserAgent()
+ {
+ $version = config('pixelfed.version');
+ $url = config('app.url');
+ return [
+ 'Accept' => 'application/activity+json',
+ 'User-Agent' => "(Pixelfed/{$version}; +{$url})",
+ ];
+ }
+
+ public static function fetchFromUrl($url = false)
+ {
+ if(self::validateUrl($url) == false) {
+ return;
+ }
+
+ $hash = hash('sha256', $url);
+ $key = "helpers:url:fetcher:sha256-{$hash}";
+ $ttl = now()->addMinutes(15);
+
+ return Cache::remember($key, $ttl, function() use($url) {
+ $res = ActivityPubFetchService::get($url);
+ if(!$res || empty($res)) {
+ return false;
+ }
+ $res = json_decode($res, true, 8);
+ if(json_last_error() == JSON_ERROR_NONE) {
+ return $res;
+ } else {
+ return false;
+ }
+ });
+ }
+
+ public static function fetchProfileFromUrl($url)
+ {
+ return self::fetchFromUrl($url);
+ }
+
+ public static function pluckval($val)
+ {
+ if(is_string($val)) {
+ return $val;
+ }
+
+ if(is_array($val)) {
+ return !empty($val) ? head($val) : null;
+ }
+
+ return null;
+ }
+
+ public static function statusFirstOrFetch($url, $replyTo = false)
+ {
+ $url = self::validateUrl($url);
+ if($url == false) {
+ return;
+ }
+
+ $host = parse_url($url, PHP_URL_HOST);
+ $local = config('pixelfed.domain.app') == $host ? true : false;
+
+ if($local) {
+ $id = (int) last(explode('/', $url));
+ return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
+ }
+
+ $cached = Status::whereNotIn('scope', ['draft','archived'])
+ ->whereUri($url)
+ ->orWhere('object_url', $url)
+ ->first();
+
+ if($cached) {
+ return $cached;
+ }
+
+ $res = self::fetchFromUrl($url);
+
+ if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) || !isset($res['published']) ) {
+ return;
+ }
+
+ if(isset($res['object'])) {
+ $activity = $res;
+ } else {
+ $activity = ['object' => $res];
+ }
+
+ $scope = 'private';
+
+ $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
+
+ if(isset($res['to']) == true) {
+ if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
+ $scope = 'public';
+ }
+ if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) {
+ $scope = 'public';
+ }
+ }
+
+ if(isset($res['cc']) == true) {
+ if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
+ $scope = 'unlisted';
+ }
+ if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) {
+ $scope = 'unlisted';
+ }
+ }
+
+ if(config('costar.enabled') == true) {
+ $blockedKeywords = config('costar.keyword.block');
+ if($blockedKeywords !== null) {
+ $keywords = config('costar.keyword.block');
+ foreach($keywords as $kw) {
+ if(Str::contains($res['content'], $kw) == true) {
+ return;
+ }
+ }
+ }
+
+ $unlisted = config('costar.domain.unlisted');
+ if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
+ $unlisted = true;
+ $scope = 'unlisted';
+ } else {
+ $unlisted = false;
+ }
+
+ $cwDomains = config('costar.domain.cw');
+ if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
+ $cw = true;
+ }
+ }
+
+ $id = isset($res['id']) ? self::pluckval($res['id']) : self::pluckval($url);
+ $idDomain = parse_url($id, PHP_URL_HOST);
+ $urlDomain = parse_url($url, PHP_URL_HOST);
+
+ if(!self::validateUrl($id)) {
+ return;
+ }
+
+ if(!isset($activity['object']['attributedTo'])) {
+ return;
+ }
+
+ $attributedTo = is_string($activity['object']['attributedTo']) ?
+ $activity['object']['attributedTo'] :
+ (is_array($activity['object']['attributedTo']) ?
+ collect($activity['object']['attributedTo'])
+ ->filter(function($o) {
+ return $o && isset($o['type']) && $o['type'] == 'Person';
+ })
+ ->pluck('id')
+ ->first() : null
+ );
+
+ if($attributedTo) {
+ $actorDomain = parse_url($attributedTo, PHP_URL_HOST);
+ if(!self::validateUrl($attributedTo) ||
+ $idDomain !== $actorDomain ||
+ $actorDomain !== $urlDomain
+ )
+ {
+ return;
+ }
+ }
+
+ if($idDomain !== $urlDomain) {
+ return;
+ }
+
+ $profile = self::profileFirstOrNew($attributedTo);
+
+ if(!$profile) {
+ return;
+ }
+
+ if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) {
+ $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false);
+ if($reply_to) {
+ $blocks = UserFilterService::blocks($reply_to->profile_id);
+ if(in_array($profile->id, $blocks)) {
+ return;
+ }
+ }
+ $reply_to = optional($reply_to)->id;
+ } else {
+ $reply_to = null;
+ }
+ $ts = self::pluckval($res['published']);
+
+ if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
+ $scope = 'unlisted';
+ }
+
+ if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
+ $cw = true;
+ }
+
+ if($res['type'] === 'Question') {
+ $status = self::storePoll(
+ $profile,
+ $res,
+ $url,
+ $ts,
+ $reply_to,
+ $cw,
+ $scope,
+ $id
+ );
+ return $status;
+ } else {
+ $status = self::storeStatus($url, $profile, $res);
+ }
+
+ return $status;
+ }
+
+ public static function storeStatus($url, $profile, $activity)
+ {
+ $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($activity['url']);
+ $url = isset($activity['url']) && is_string($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
+ $idDomain = parse_url($id, PHP_URL_HOST);
+ $urlDomain = parse_url($url, PHP_URL_HOST);
+ if(!self::validateUrl($id) || !self::validateUrl($url)) {
+ return;
+ }
+
+ $reply_to = self::getReplyTo($activity);
+
+ $ts = self::pluckval($activity['published']);
+ $scope = self::getScope($activity, $url);
+ $cw = self::getSensitive($activity, $url);
+ $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
+ $isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false);
+ $commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false;
+
+ if(!$pid) {
+ return;
+ }
+
+ if($scope == 'public') {
+ if($isUnlisted == true) {
+ $scope = 'unlisted';
+ }
+ }
+
+ $status = Status::updateOrCreate(
+ [
+ 'uri' => $url
+ ], [
+ 'profile_id' => $pid,
+ 'url' => $url,
+ 'object_url' => $id,
+ 'caption' => isset($activity['content']) ? Purify::clean(strip_tags($activity['content'])) : null,
+ 'rendered' => isset($activity['content']) ? Purify::clean($activity['content']) : null,
+ 'created_at' => Carbon::parse($ts)->tz('UTC'),
+ 'in_reply_to_id' => $reply_to,
+ 'local' => false,
+ 'is_nsfw' => $cw,
+ 'scope' => $scope,
+ 'visibility' => $scope,
+ 'cw_summary' => ($cw == true && isset($activity['summary']) ?
+ Purify::clean(strip_tags($activity['summary'])) : null),
+ 'comments_disabled' => $commentsDisabled
+ ]
+ );
+
+ if($reply_to == null) {
+ self::importNoteAttachment($activity, $status);
+ } else {
+ if(isset($activity['attachment']) && !empty($activity['attachment'])) {
+ self::importNoteAttachment($activity, $status);
+ }
+ StatusReplyPipeline::dispatch($status);
+ }
+
+ if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) {
+ StatusTagsPipeline::dispatch($activity, $status);
+ }
+
+ if( config('instance.timeline.network.cached') &&
+ $status->in_reply_to_id === null &&
+ $status->reblog_of_id === null &&
+ in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
+ $status->created_at->gt(now()->subHours(config('instance.timeline.network.max_hours_old'))) &&
+ (config('instance.hide_nsfw_on_public_feeds') == true ? $status->is_nsfw == false : true)
+ ) {
+ $filteredDomains = collect(InstanceService::getBannedDomains())
->merge(InstanceService::getUnlistedDomains())
->unique()
->values()
->toArray();
if(!in_array($urlDomain, $filteredDomains)) {
- NetworkTimelineService::add($status->id);
+ if(!$isUnlisted) {
+ NetworkTimelineService::add($status->id);
+ }
}
- }
+ }
- IncrementPostCount::dispatch($pid)->onQueue('low');
+ IncrementPostCount::dispatch($pid)->onQueue('low');
- return $status;
- }
+ return $status;
+ }
- public static function getSensitive($activity, $url)
- {
- $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url);
- $url = isset($activity['url']) ? self::pluckval($activity['url']) : $id;
- $urlDomain = parse_url($url, PHP_URL_HOST);
+ public static function getSensitive($activity, $url)
+ {
+ $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url);
+ $url = isset($activity['url']) ? self::pluckval($activity['url']) : $id;
+ $urlDomain = parse_url($url, PHP_URL_HOST);
- $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
+ $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
- if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
- $cw = true;
- }
+ if(in_array($urlDomain, InstanceService::getNsfwDomains())) {
+ $cw = true;
+ }
- return $cw;
- }
+ return $cw;
+ }
- public static function getReplyTo($activity)
- {
- $reply_to = null;
- $inReplyTo = isset($activity['inReplyTo']) && !empty($activity['inReplyTo']) ?
- self::pluckval($activity['inReplyTo']) :
- false;
+ public static function getReplyTo($activity)
+ {
+ $reply_to = null;
+ $inReplyTo = isset($activity['inReplyTo']) && !empty($activity['inReplyTo']) ?
+ self::pluckval($activity['inReplyTo']) :
+ false;
- if($inReplyTo) {
- $reply_to = self::statusFirstOrFetch($inReplyTo);
- if($reply_to) {
- $reply_to = optional($reply_to)->id;
- }
- } else {
- $reply_to = null;
- }
+ if($inReplyTo) {
+ $reply_to = self::statusFirstOrFetch($inReplyTo);
+ if($reply_to) {
+ $reply_to = optional($reply_to)->id;
+ }
+ } else {
+ $reply_to = null;
+ }
- return $reply_to;
- }
+ return $reply_to;
+ }
- public static function getScope($activity, $url)
- {
- $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url);
- $url = isset($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
- $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST);
- $scope = 'private';
+ public static function getScope($activity, $url)
+ {
+ $id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url);
+ $url = isset($activity['url']) ? self::pluckval($activity['url']) : self::pluckval($id);
+ $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST);
+ $scope = 'private';
- if(isset($activity['to']) == true) {
- if(is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
- $scope = 'public';
- }
- if(is_string($activity['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['to']) {
- $scope = 'public';
- }
- }
+ if(isset($activity['to']) == true) {
+ if(is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
+ $scope = 'public';
+ }
+ if(is_string($activity['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['to']) {
+ $scope = 'public';
+ }
+ }
- if(isset($activity['cc']) == true) {
- if(is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
- $scope = 'unlisted';
- }
- if(is_string($activity['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['cc']) {
- $scope = 'unlisted';
- }
- }
+ if(isset($activity['cc']) == true) {
+ if(is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
+ $scope = 'unlisted';
+ }
+ if(is_string($activity['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['cc']) {
+ $scope = 'unlisted';
+ }
+ }
- if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
- $scope = 'unlisted';
- }
+ if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
+ $scope = 'unlisted';
+ }
- return $scope;
- }
+ return $scope;
+ }
- private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
- {
- if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) {
- return;
- }
+ private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
+ {
+ if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) {
+ return;
+ }
- $options = collect($res['oneOf'])->map(function($option) {
- return $option['name'];
- })->toArray();
+ $options = collect($res['oneOf'])->map(function($option) {
+ return $option['name'];
+ })->toArray();
- $cachedTallies = collect($res['oneOf'])->map(function($option) {
- return $option['replies']['totalItems'] ?? 0;
- })->toArray();
+ $cachedTallies = collect($res['oneOf'])->map(function($option) {
+ return $option['replies']['totalItems'] ?? 0;
+ })->toArray();
- $status = new Status;
- $status->profile_id = $profile->id;
- $status->url = isset($res['url']) ? $res['url'] : $url;
- $status->uri = isset($res['url']) ? $res['url'] : $url;
- $status->object_url = $id;
- $status->caption = strip_tags($res['content']);
- $status->rendered = Purify::clean($res['content']);
- $status->created_at = Carbon::parse($ts)->tz('UTC');
- $status->in_reply_to_id = null;
- $status->local = false;
- $status->is_nsfw = $cw;
- $status->scope = 'draft';
- $status->visibility = 'draft';
- $status->cw_summary = $cw == true && isset($res['summary']) ?
- Purify::clean(strip_tags($res['summary'])) : null;
- $status->save();
+ $status = new Status;
+ $status->profile_id = $profile->id;
+ $status->url = isset($res['url']) ? $res['url'] : $url;
+ $status->uri = isset($res['url']) ? $res['url'] : $url;
+ $status->object_url = $id;
+ $status->caption = strip_tags($res['content']);
+ $status->rendered = Purify::clean($res['content']);
+ $status->created_at = Carbon::parse($ts)->tz('UTC');
+ $status->in_reply_to_id = null;
+ $status->local = false;
+ $status->is_nsfw = $cw;
+ $status->scope = 'draft';
+ $status->visibility = 'draft';
+ $status->cw_summary = $cw == true && isset($res['summary']) ?
+ Purify::clean(strip_tags($res['summary'])) : null;
+ $status->save();
- $poll = new Poll;
- $poll->status_id = $status->id;
- $poll->profile_id = $status->profile_id;
- $poll->poll_options = $options;
- $poll->cached_tallies = $cachedTallies;
- $poll->votes_count = array_sum($cachedTallies);
- $poll->expires_at = now()->parse($res['endTime']);
- $poll->last_fetched_at = now();
- $poll->save();
+ $poll = new Poll;
+ $poll->status_id = $status->id;
+ $poll->profile_id = $status->profile_id;
+ $poll->poll_options = $options;
+ $poll->cached_tallies = $cachedTallies;
+ $poll->votes_count = array_sum($cachedTallies);
+ $poll->expires_at = now()->parse($res['endTime']);
+ $poll->last_fetched_at = now();
+ $poll->save();
- $status->type = 'poll';
- $status->scope = $scope;
- $status->visibility = $scope;
- $status->save();
+ $status->type = 'poll';
+ $status->scope = $scope;
+ $status->visibility = $scope;
+ $status->save();
- return $status;
- }
+ return $status;
+ }
- public static function statusFetch($url)
- {
- return self::statusFirstOrFetch($url);
- }
+ public static function statusFetch($url)
+ {
+ return self::statusFirstOrFetch($url);
+ }
- public static function importNoteAttachment($data, Status $status)
- {
- if(self::verifyAttachments($data) == false) {
- // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]);
- $status->viewType();
- return;
- }
- $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
- // peertube
- // if(!$attachments) {
- // $obj = isset($data['object']) ? $data['object'] : $data;
- // $attachments = is_array($obj['url']) ? $obj['url'] : null;
- // }
- $user = $status->profile;
- $storagePath = MediaPathService::get($user, 2);
- $allowed = explode(',', config_cache('pixelfed.media_types'));
+ public static function importNoteAttachment($data, Status $status)
+ {
+ if(self::verifyAttachments($data) == false) {
+ // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]);
+ $status->viewType();
+ return;
+ }
+ $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
+ // peertube
+ // if(!$attachments) {
+ // $obj = isset($data['object']) ? $data['object'] : $data;
+ // $attachments = is_array($obj['url']) ? $obj['url'] : null;
+ // }
+ $user = $status->profile;
+ $storagePath = MediaPathService::get($user, 2);
+ $allowed = explode(',', config_cache('pixelfed.media_types'));
- foreach($attachments as $key => $media) {
- $type = $media['mediaType'];
- $url = $media['url'];
- $valid = self::validateUrl($url);
- if(in_array($type, $allowed) == false || $valid == false) {
- continue;
- }
- $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
- $license = isset($media['license']) ? License::nameToId($media['license']) : null;
- $caption = isset($media['name']) ? Purify::clean($media['name']) : null;
+ foreach($attachments as $key => $media) {
+ $type = $media['mediaType'];
+ $url = $media['url'];
+ $valid = self::validateUrl($url);
+ if(in_array($type, $allowed) == false || $valid == false) {
+ continue;
+ }
+ $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
+ $license = isset($media['license']) ? License::nameToId($media['license']) : null;
+ $caption = isset($media['name']) ? Purify::clean($media['name']) : null;
+ $width = isset($media['width']) ? $media['width'] : false;
+ $height = isset($media['height']) ? $media['height'] : false;
- $media = new Media();
- $media->blurhash = $blurhash;
- $media->remote_media = true;
- $media->status_id = $status->id;
- $media->profile_id = $status->profile_id;
- $media->user_id = null;
- $media->media_path = $url;
- $media->remote_url = $url;
- $media->caption = $caption;
- $media->order = $key + 1;
- if($license) {
- $media->license = $license;
- }
- $media->mime = $type;
- $media->version = 3;
- $media->save();
+ $media = new Media();
+ $media->blurhash = $blurhash;
+ $media->remote_media = true;
+ $media->status_id = $status->id;
+ $media->profile_id = $status->profile_id;
+ $media->user_id = null;
+ $media->media_path = $url;
+ $media->remote_url = $url;
+ $media->caption = $caption;
+ $media->order = $key + 1;
+ if($width) {
+ $media->width = $width;
+ }
+ if($height) {
+ $media->height = $height;
+ }
+ if($license) {
+ $media->license = $license;
+ }
+ $media->mime = $type;
+ $media->version = 3;
+ $media->save();
- if(config_cache('pixelfed.cloud_storage') == true) {
- MediaStoragePipeline::dispatch($media);
- }
- }
+ if(config_cache('pixelfed.cloud_storage') == true) {
+ MediaStoragePipeline::dispatch($media);
+ }
+ }
- $status->viewType();
- return;
- }
+ $status->viewType();
+ return;
+ }
- public static function profileFirstOrNew($url)
- {
- $url = self::validateUrl($url);
- if($url == false) {
- return;
- }
+ public static function profileFirstOrNew($url)
+ {
+ $url = self::validateUrl($url);
+ if($url == false) {
+ return;
+ }
- $host = parse_url($url, PHP_URL_HOST);
- $local = config('pixelfed.domain.app') == $host ? true : false;
+ $host = parse_url($url, PHP_URL_HOST);
+ $local = config('pixelfed.domain.app') == $host ? true : false;
- if($local == true) {
- $id = last(explode('/', $url));
- return Profile::whereNull('status')
- ->whereNull('domain')
- ->whereUsername($id)
- ->firstOrFail();
- }
+ if($local == true) {
+ $id = last(explode('/', $url));
+ return Profile::whereNull('status')
+ ->whereNull('domain')
+ ->whereUsername($id)
+ ->firstOrFail();
+ }
- if($profile = Profile::whereRemoteUrl($url)->first()) {
- if($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
- return self::profileUpdateOrCreate($url);
- }
- return $profile;
- }
+ if($profile = Profile::whereRemoteUrl($url)->first()) {
+ if($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
+ return self::profileUpdateOrCreate($url);
+ }
+ return $profile;
+ }
- return self::profileUpdateOrCreate($url);
- }
+ return self::profileUpdateOrCreate($url);
+ }
- public static function profileUpdateOrCreate($url)
- {
- $res = self::fetchProfileFromUrl($url);
- if(!$res || isset($res['id']) == false) {
- return;
- }
- $domain = parse_url($res['id'], PHP_URL_HOST);
- if(!isset($res['preferredUsername']) && !isset($res['nickname'])) {
- return;
- }
- $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']);
- if(empty($username)) {
- return;
- }
- $remoteUsername = $username;
- $webfinger = "@{$username}@{$domain}";
+ public static function profileUpdateOrCreate($url)
+ {
+ $res = self::fetchProfileFromUrl($url);
+ if(!$res || isset($res['id']) == false) {
+ return;
+ }
+ $domain = parse_url($res['id'], PHP_URL_HOST);
+ if(!isset($res['preferredUsername']) && !isset($res['nickname'])) {
+ return;
+ }
+ $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']);
+ if(empty($username)) {
+ return;
+ }
+ $remoteUsername = $username;
+ $webfinger = "@{$username}@{$domain}";
- if(!self::validateUrl($res['inbox'])) {
- return;
- }
- if(!self::validateUrl($res['id'])) {
- return;
- }
+ if(!self::validateUrl($res['inbox'])) {
+ return;
+ }
+ if(!self::validateUrl($res['id'])) {
+ return;
+ }
- $instance = Instance::updateOrCreate([
- 'domain' => $domain
- ]);
- if($instance->wasRecentlyCreated == true) {
- \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
- }
+ $instance = Instance::updateOrCreate([
+ 'domain' => $domain
+ ]);
+ if($instance->wasRecentlyCreated == true) {
+ \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
+ }
- $profile = Profile::updateOrCreate(
- [
- 'domain' => strtolower($domain),
- 'username' => Purify::clean($webfinger),
- ],
- [
- 'webfinger' => Purify::clean($webfinger),
- 'key_id' => $res['publicKey']['id'],
- 'remote_url' => $res['id'],
- 'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user',
- 'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null,
- 'sharedInbox' => isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null,
- 'inbox_url' => $res['inbox'],
- 'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null,
- 'public_key' => $res['publicKey']['publicKeyPem'],
- ]
- );
+ $profile = Profile::updateOrCreate(
+ [
+ 'domain' => strtolower($domain),
+ 'username' => Purify::clean($webfinger),
+ ],
+ [
+ 'webfinger' => Purify::clean($webfinger),
+ 'key_id' => $res['publicKey']['id'],
+ 'remote_url' => $res['id'],
+ 'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user',
+ 'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null,
+ 'sharedInbox' => isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null,
+ 'inbox_url' => $res['inbox'],
+ 'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null,
+ 'public_key' => $res['publicKey']['publicKeyPem'],
+ 'indexable' => isset($res['indexable']) && is_bool($res['indexable']) ? $res['indexable'] : false,
+ ]
+ );
- if( $profile->last_fetched_at == null ||
- $profile->last_fetched_at->lt(now()->subHours(24))
- ) {
- RemoteAvatarFetch::dispatch($profile);
- }
- $profile->last_fetched_at = now();
- $profile->save();
- return $profile;
- }
+ if( $profile->last_fetched_at == null ||
+ $profile->last_fetched_at->lt(now()->subHours(24))
+ ) {
+ RemoteAvatarFetch::dispatch($profile);
+ }
+ $profile->last_fetched_at = now();
+ $profile->save();
+ return $profile;
+ }
- public static function profileFetch($url)
- {
- return self::profileFirstOrNew($url);
- }
+ public static function profileFetch($url)
+ {
+ return self::profileFirstOrNew($url);
+ }
- public static function sendSignedObject($profile, $url, $body)
- {
- ActivityPubDeliveryService::queue()
- ->from($profile)
- ->to($url)
- ->payload($body)
- ->send();
- }
+ public static function sendSignedObject($profile, $url, $body)
+ {
+ ActivityPubDeliveryService::queue()
+ ->from($profile)
+ ->to($url)
+ ->payload($body)
+ ->send();
+ }
}
diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php
index 0caf8f25d..4441becfb 100644
--- a/app/Util/ActivityPub/Inbox.php
+++ b/app/Util/ActivityPub/Inbox.php
@@ -25,7 +25,7 @@ use Illuminate\Support\Str;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
-use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
+use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StoryPipeline\StoryExpire;
use App\Jobs\StoryPipeline\StoryFetch;
use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline;
@@ -281,7 +281,8 @@ class Inbox
}
if($actor->followers_count == 0) {
- if(FollowerService::followerCount($actor->id, true) == 0) {
+ if(config('federation.activitypub.ingest.store_notes_without_followers')) {
+ } else if(FollowerService::followerCount($actor->id, true) == 0) {
return;
}
}
@@ -706,7 +707,7 @@ class Inbox
if(!$status) {
return;
}
- DeleteRemoteStatusPipeline::dispatch($status)->onQueue('high');
+ RemoteStatusDelete::dispatch($status)->onQueue('high');
return;
break;
diff --git a/app/Util/Media/Blurhash.php b/app/Util/Media/Blurhash.php
index c0cca59b9..8e232ea17 100644
--- a/app/Util/Media/Blurhash.php
+++ b/app/Util/Media/Blurhash.php
@@ -44,6 +44,9 @@ class Blurhash {
$pixels[] = $row;
}
+ // Free the allocated GdImage object from memory:
+ imagedestroy($image);
+
$components_x = 4;
$components_y = 4;
$blurhash = BlurhashEngine::encode($pixels, $components_x, $components_y);
@@ -53,4 +56,4 @@ class Blurhash {
return $blurhash;
}
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index e28376f20..74c453c67 100644
--- a/composer.lock
+++ b/composer.lock
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.275.5",
+ "version": "3.275.7",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "d46961b82e857f77059c0c78160719ecb26f6cc6"
+ "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d46961b82e857f77059c0c78160719ecb26f6cc6",
- "reference": "d46961b82e857f77059c0c78160719ecb26f6cc6",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/54dcef3349c81b46c0f5f6e54b5f9bfb5db19903",
+ "reference": "54dcef3349c81b46c0f5f6e54b5f9bfb5db19903",
"shasum": ""
},
"require": {
@@ -151,9 +151,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.275.5"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.275.7"
},
- "time": "2023-07-07T18:20:11+00:00"
+ "time": "2023-07-13T18:21:04+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -2357,16 +2357,16 @@
},
{
"name": "laravel/framework",
- "version": "v10.14.1",
+ "version": "v10.15.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "6f89a2b74b232d8bf2e1d9ed87e311841263dfcb"
+ "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/6f89a2b74b232d8bf2e1d9ed87e311841263dfcb",
- "reference": "6f89a2b74b232d8bf2e1d9ed87e311841263dfcb",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/c7599dc92e04532824bafbd226c2936ce6a905b8",
+ "reference": "c7599dc92e04532824bafbd226c2936ce6a905b8",
"shasum": ""
},
"require": {
@@ -2553,7 +2553,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2023-06-28T14:25:16+00:00"
+ "time": "2023-07-11T13:43:52+00:00"
},
{
"name": "laravel/helpers",
@@ -2613,16 +2613,16 @@
},
{
"name": "laravel/horizon",
- "version": "v5.17.0",
+ "version": "v5.18.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
- "reference": "569c7154033679a1ca05b43bfa640cc60aa3b37b"
+ "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/horizon/zipball/569c7154033679a1ca05b43bfa640cc60aa3b37b",
- "reference": "569c7154033679a1ca05b43bfa640cc60aa3b37b",
+ "url": "https://api.github.com/repos/laravel/horizon/zipball/b14498a09af826035e46ae8d6b013d0ec849bdb7",
+ "reference": "b14498a09af826035e46ae8d6b013d0ec849bdb7",
"shasum": ""
},
"require": {
@@ -2685,9 +2685,9 @@
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
- "source": "https://github.com/laravel/horizon/tree/v5.17.0"
+ "source": "https://github.com/laravel/horizon/tree/v5.18.0"
},
- "time": "2023-06-13T20:49:30+00:00"
+ "time": "2023-06-30T15:11:51+00:00"
},
{
"name": "laravel/passport",
@@ -6651,23 +6651,24 @@
},
{
"name": "react/promise",
- "version": "v2.10.0",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
- "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38"
+ "reference": "c86753c76fd3be465d93b308f18d189f01a22be4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38",
- "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4",
+ "reference": "c86753c76fd3be465d93b308f18d189f01a22be4",
"shasum": ""
},
"require": {
- "php": ">=5.4.0"
+ "php": ">=7.1.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36"
+ "phpstan/phpstan": "1.10.20 || 1.4.10",
+ "phpunit/phpunit": "^9.5 || ^7.5"
},
"type": "library",
"autoload": {
@@ -6711,7 +6712,7 @@
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
- "source": "https://github.com/reactphp/promise/tree/v2.10.0"
+ "source": "https://github.com/reactphp/promise/tree/v3.0.0"
},
"funding": [
{
@@ -6719,7 +6720,7 @@
"type": "open_collective"
}
],
- "time": "2023-05-02T15:15:43+00:00"
+ "time": "2023-07-11T16:12:49+00:00"
},
{
"name": "react/socket",
@@ -10920,16 +10921,16 @@
},
{
"name": "filp/whoops",
- "version": "2.15.2",
+ "version": "2.15.3",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73"
+ "reference": "c83e88a30524f9360b11f585f71e6b17313b7187"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
- "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/c83e88a30524f9360b11f585f71e6b17313b7187",
+ "reference": "c83e88a30524f9360b11f585f71e6b17313b7187",
"shasum": ""
},
"require": {
@@ -10979,7 +10980,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
- "source": "https://github.com/filp/whoops/tree/2.15.2"
+ "source": "https://github.com/filp/whoops/tree/2.15.3"
},
"funding": [
{
@@ -10987,7 +10988,7 @@
"type": "github"
}
],
- "time": "2023-04-12T12:00:00+00:00"
+ "time": "2023-07-13T12:00:00+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -11101,16 +11102,16 @@
},
{
"name": "laravel/telescope",
- "version": "v4.15.0",
+ "version": "v4.15.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/telescope.git",
- "reference": "572a19b4c9b09295848de9a2352737a756a0fb05"
+ "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/telescope/zipball/572a19b4c9b09295848de9a2352737a756a0fb05",
- "reference": "572a19b4c9b09295848de9a2352737a756a0fb05",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/5d74ae4c9f269b756d7877ad1527770c59846e14",
+ "reference": "5d74ae4c9f269b756d7877ad1527770c59846e14",
"shasum": ""
},
"require": {
@@ -11166,9 +11167,9 @@
],
"support": {
"issues": "https://github.com/laravel/telescope/issues",
- "source": "https://github.com/laravel/telescope/tree/v4.15.0"
+ "source": "https://github.com/laravel/telescope/tree/v4.15.2"
},
- "time": "2023-06-08T13:57:22+00:00"
+ "time": "2023-07-13T20:06:27+00:00"
},
{
"name": "mockery/mockery",
diff --git a/config/federation.php b/config/federation.php
index 4b6795687..773d3d16b 100644
--- a/config/federation.php
+++ b/config/federation.php
@@ -2,56 +2,59 @@
return [
- /*
- |--------------------------------------------------------------------------
- | ActivityPub
- |--------------------------------------------------------------------------
- |
- | ActivityPub configuration
- |
- */
- 'activitypub' => [
- 'enabled' => env('ACTIVITY_PUB', false),
- 'outbox' => env('AP_OUTBOX', true),
- 'inbox' => env('AP_INBOX', true),
- 'sharedInbox' => env('AP_SHAREDINBOX', true),
+ /*
+ |--------------------------------------------------------------------------
+ | ActivityPub
+ |--------------------------------------------------------------------------
+ |
+ | ActivityPub configuration
+ |
+ */
+ 'activitypub' => [
+ 'enabled' => env('ACTIVITY_PUB', false),
+ 'outbox' => env('AP_OUTBOX', true),
+ 'inbox' => env('AP_INBOX', true),
+ 'sharedInbox' => env('AP_SHAREDINBOX', true),
- 'remoteFollow' => env('AP_REMOTE_FOLLOW', true),
+ 'remoteFollow' => env('AP_REMOTE_FOLLOW', true),
- 'delivery' => [
- 'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0),
- 'concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10),
- 'logger' => [
- 'enabled' => env('AP_LOGGER_ENABLED', false),
- 'driver' => 'log'
- ]
- ]
- ],
+ 'delivery' => [
+ 'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0),
+ 'concurrency' => env('ACTIVITYPUB_DELIVERY_CONCURRENCY', 10),
+ 'logger' => [
+ 'enabled' => env('AP_LOGGER_ENABLED', false),
+ 'driver' => 'log'
+ ]
+ ],
- 'atom' => [
- 'enabled' => env('ATOM_FEEDS', true),
- ],
+ 'ingest' => [
+ 'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false),
+ ],
+ ],
- 'avatars' => [
- 'store_local' => env('REMOTE_AVATARS', true),
- ],
+ 'atom' => [
+ 'enabled' => env('ATOM_FEEDS', true),
+ ],
- 'nodeinfo' => [
- 'enabled' => env('NODEINFO', true),
- ],
+ 'avatars' => [
+ 'store_local' => env('REMOTE_AVATARS', true),
+ ],
- 'webfinger' => [
- 'enabled' => env('WEBFINGER', true)
- ],
+ 'nodeinfo' => [
+ 'enabled' => env('NODEINFO', true),
+ ],
- 'network_timeline' => env('PF_NETWORK_TIMELINE', true),
- 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2),
+ 'webfinger' => [
+ 'enabled' => env('WEBFINGER', true)
+ ],
- 'custom_emoji' => [
- 'enabled' => env('CUSTOM_EMOJI', false),
+ 'network_timeline' => env('PF_NETWORK_TIMELINE', true),
+ 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2),
- // max size in bytes, default is 2mb
- 'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000),
- ]
+ 'custom_emoji' => [
+ 'enabled' => env('CUSTOM_EMOJI', false),
+ // max size in bytes, default is 2mb
+ 'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000),
+ ],
];
diff --git a/config/filesystems.php b/config/filesystems.php
index 6817d5e34..80e63ed99 100644
--- a/config/filesystems.php
+++ b/config/filesystems.php
@@ -79,6 +79,34 @@ return [
'throw' => true,
],
+ 'alt-primary' => [
+ 'enabled' => env('ALT_PRI_ENABLED', false),
+ 'driver' => 's3',
+ 'key' => env('ALT_PRI_AWS_ACCESS_KEY_ID'),
+ 'secret' => env('ALT_PRI_AWS_SECRET_ACCESS_KEY'),
+ 'region' => env('ALT_PRI_AWS_DEFAULT_REGION'),
+ 'bucket' => env('ALT_PRI_AWS_BUCKET'),
+ 'visibility' => 'public',
+ 'url' => env('ALT_PRI_AWS_URL'),
+ 'endpoint' => env('ALT_PRI_AWS_ENDPOINT'),
+ 'use_path_style_endpoint' => env('ALT_PRI_AWS_USE_PATH_STYLE_ENDPOINT', false),
+ 'throw' => true,
+ ],
+
+ 'alt-secondary' => [
+ 'enabled' => env('ALT_SEC_ENABLED', false),
+ 'driver' => 's3',
+ 'key' => env('ALT_SEC_AWS_ACCESS_KEY_ID'),
+ 'secret' => env('ALT_SEC_AWS_SECRET_ACCESS_KEY'),
+ 'region' => env('ALT_SEC_AWS_DEFAULT_REGION'),
+ 'bucket' => env('ALT_SEC_AWS_BUCKET'),
+ 'visibility' => 'public',
+ 'url' => env('ALT_SEC_AWS_URL'),
+ 'endpoint' => env('ALT_SEC_AWS_ENDPOINT'),
+ 'use_path_style_endpoint' => env('ALT_SEC_AWS_USE_PATH_STYLE_ENDPOINT', false),
+ 'throw' => true,
+ ],
+
'spaces' => [
'driver' => 's3',
'key' => env('DO_SPACES_KEY'),
diff --git a/config/media.php b/config/media.php
index b7d6e95cc..f550ff291 100644
--- a/config/media.php
+++ b/config/media.php
@@ -1,24 +1,26 @@
env('MEDIA_DELETE_LOCAL_AFTER_CLOUD', true),
+ 'delete_local_after_cloud' => env('MEDIA_DELETE_LOCAL_AFTER_CLOUD', true),
- 'exif' => [
- 'database' => env('MEDIA_EXIF_DATABASE', false),
- ],
+ 'exif' => [
+ 'database' => env('MEDIA_EXIF_DATABASE', false),
+ ],
- 'storage' => [
- 'remote' => [
- /*
- |--------------------------------------------------------------------------
- | Store remote media on cloud/S3
- |--------------------------------------------------------------------------
- |
- | Set this to cache remote media on cloud/S3 filesystem drivers.
- | Disabled by default.
- |
- */
- 'cloud' => env('MEDIA_REMOTE_STORE_CLOUD', false)
- ],
- ]
+ 'storage' => [
+ 'remote' => [
+ /*
+ |--------------------------------------------------------------------------
+ | Store remote media on cloud/S3
+ |--------------------------------------------------------------------------
+ |
+ | Set this to cache remote media on cloud/S3 filesystem drivers.
+ | Disabled by default.
+ |
+ */
+ 'cloud' => env('MEDIA_REMOTE_STORE_CLOUD', false),
+
+ 'resilient_mode' => env('ALT_PRI_ENABLED', false) || env('ALT_SEC_ENABLED', false),
+ ],
+ ]
];
diff --git a/config/pixelfed.php b/config/pixelfed.php
index 18e78b21d..fcdb1a4b7 100644
--- a/config/pixelfed.php
+++ b/config/pixelfed.php
@@ -23,7 +23,7 @@ return [
| This value is the version of your Pixelfed instance.
|
*/
- 'version' => '0.11.8',
+ 'version' => '0.11.9',
/*
|--------------------------------------------------------------------------
diff --git a/config/remote-auth.php b/config/remote-auth.php
new file mode 100644
index 000000000..182bb99a7
--- /dev/null
+++ b/config/remote-auth.php
@@ -0,0 +1,57 @@
+ [
+ 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENABLED', false),
+ 'ignore_closed_state' => env('PF_LOGIN_WITH_MASTODON_ENABLED_SKIP_CLOSED', false),
+
+ 'contraints' => [
+ /*
+ * Skip email verification
+ *
+ * To improve the onboarding experience, you can opt to skip the email
+ * verification process and automatically verify their email
+ */
+ 'skip_email_verification' => env('PF_LOGIN_WITH_MASTODON_SKIP_EMAIL', true),
+ ],
+
+ 'domains' => [
+ 'default' => 'mastodon.social,mastodon.online,mstdn.social,mas.to',
+
+ /*
+ * Custom mastodon domains
+ *
+ * Define a comma separated list of custom domains to allow
+ */
+ 'custom' => env('PF_LOGIN_WITH_MASTODON_DOMAINS'),
+
+ /*
+ * Use only default domains
+ *
+ * Allow Sign-in with Mastodon using only the default domains
+ */
+ 'only_default' => env('PF_LOGIN_WITH_MASTODON_ONLY_DEFAULT', false),
+
+ /*
+ * Use only custom domains
+ *
+ * Allow Sign-in with Mastodon using only the custom domains
+ * you define, in comma separated format
+ */
+ 'only_custom' => env('PF_LOGIN_WITH_MASTODON_ONLY_CUSTOM', false),
+ ],
+
+ 'max_uses' => [
+ /*
+ * Max Uses
+ *
+ * Using a centralized service operated by pixelfed.org that tracks mastodon imports,
+ * you can set a limit of how many times a mastodon account can be imported across
+ * all known and reporting Pixelfed instances to prevent the same masto account from
+ * abusing this
+ */
+ 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENFORCE_MAX_USES', true),
+ 'limit' => env('PF_LOGIN_WITH_MASTODON_MAX_USES_LIMIT', 3)
+ ]
+ ],
+];
diff --git a/config/security.php b/config/security.php
new file mode 100644
index 000000000..a8f92360d
--- /dev/null
+++ b/config/security.php
@@ -0,0 +1,9 @@
+ [
+ 'verify_dns' => env('PF_SECURITY_URL_VERIFY_DNS', false),
+
+ 'trusted_domains' => env('PF_SECURITY_URL_TRUSTED_DOMAINS', 'pixelfed.social,pixelfed.art,mastodon.social'),
+ ]
+];
diff --git a/database/migrations/2021_08_04_095125_create_groups_table.php b/database/migrations/2021_08_04_095125_create_groups_table.php
new file mode 100644
index 000000000..29c63f73e
--- /dev/null
+++ b/database/migrations/2021_08_04_095125_create_groups_table.php
@@ -0,0 +1,42 @@
+bigInteger('id')->unsigned()->primary();
+ $table->bigInteger('profile_id')->unsigned()->nullable()->index();
+ $table->string('status')->nullable()->index();
+ $table->string('name')->nullable();
+ $table->text('description')->nullable();
+ $table->text('rules')->nullable();
+ $table->boolean('local')->default(true)->index();
+ $table->string('remote_url')->nullable();
+ $table->string('inbox_url')->nullable();
+ $table->boolean('is_private')->default(false);
+ $table->boolean('local_only')->default(false);
+ $table->json('metadata')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('groups');
+ }
+}
diff --git a/database/migrations/2021_08_04_095143_create_group_members_table.php b/database/migrations/2021_08_04_095143_create_group_members_table.php
new file mode 100644
index 000000000..33df26229
--- /dev/null
+++ b/database/migrations/2021_08_04_095143_create_group_members_table.php
@@ -0,0 +1,40 @@
+id();
+ $table->bigInteger('group_id')->unsigned()->index();
+ $table->bigInteger('profile_id')->unsigned()->index();
+ $table->string('role')->default('member')->index();
+ $table->boolean('local_group')->default(false)->index();
+ $table->boolean('local_profile')->default(false)->index();
+ $table->boolean('join_request')->default(false)->index();
+ $table->timestamp('approved_at')->nullable();
+ $table->timestamp('rejected_at')->nullable();
+ $table->unique(['group_id', 'profile_id']);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('group_members');
+ }
+}
diff --git a/database/migrations/2021_08_04_095238_create_group_posts_table.php b/database/migrations/2021_08_04_095238_create_group_posts_table.php
new file mode 100644
index 000000000..a5e637d8e
--- /dev/null
+++ b/database/migrations/2021_08_04_095238_create_group_posts_table.php
@@ -0,0 +1,42 @@
+bigInteger('id')->unsigned()->primary();
+ $table->bigInteger('group_id')->unsigned()->index();
+ $table->bigInteger('profile_id')->unsigned()->nullable()->index();
+ $table->string('type')->nullable()->index();
+ $table->bigInteger('status_id')->unsigned()->unique();
+ $table->string('remote_url')->unique()->nullable()->index();
+ $table->bigInteger('reply_child_id')->unsigned()->nullable();
+ $table->bigInteger('in_reply_to_id')->unsigned()->nullable();
+ $table->bigInteger('reblog_of_id')->unsigned()->nullable();
+ $table->unsignedInteger('reply_count')->nullable();
+ $table->string('status')->nullable()->index();
+ $table->json('metadata')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('group_posts');
+ }
+}
diff --git a/database/migrations/2021_08_16_072457_create_group_invitations_table.php b/database/migrations/2021_08_16_072457_create_group_invitations_table.php
new file mode 100644
index 000000000..aa13db23a
--- /dev/null
+++ b/database/migrations/2021_08_16_072457_create_group_invitations_table.php
@@ -0,0 +1,38 @@
+bigIncrements('id');
+ $table->bigInteger('group_id')->unsigned()->index();
+ $table->bigInteger('from_profile_id')->unsigned()->index();
+ $table->bigInteger('to_profile_id')->unsigned()->index();
+ $table->string('role')->nullable();
+ $table->boolean('to_local')->default(true)->index();
+ $table->boolean('from_local')->default(true)->index();
+ $table->unique(['group_id', 'to_profile_id']);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('group_invitations');
+ }
+}
diff --git a/database/migrations/2023_07_07_025757_create_remote_auths_table.php b/database/migrations/2023_07_07_025757_create_remote_auths_table.php
new file mode 100644
index 000000000..774965aa2
--- /dev/null
+++ b/database/migrations/2023_07_07_025757_create_remote_auths_table.php
@@ -0,0 +1,38 @@
+id();
+ $table->string('software')->nullable();
+ $table->string('domain')->nullable()->index();
+ $table->string('webfinger')->nullable()->unique()->index();
+ $table->unsignedInteger('instance_id')->nullable()->index();
+ $table->unsignedInteger('user_id')->nullable()->unique()->index();
+ $table->unsignedInteger('client_id')->nullable()->index();
+ $table->string('ip_address')->nullable();
+ $table->text('bearer_token')->nullable();
+ $table->json('verify_credentials')->nullable();
+ $table->timestamp('last_successful_login_at')->nullable();
+ $table->timestamp('last_verify_credentials_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('remote_auths');
+ }
+};
diff --git a/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php
new file mode 100644
index 000000000..690197b9b
--- /dev/null
+++ b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php
@@ -0,0 +1,37 @@
+id();
+ $table->string('domain')->nullable()->unique()->index();
+ $table->unsignedInteger('instance_id')->nullable()->index();
+ $table->string('client_id')->nullable();
+ $table->string('client_secret')->nullable();
+ $table->string('redirect_uri')->nullable();
+ $table->string('root_domain')->nullable()->index();
+ $table->boolean('allowed')->nullable()->index();
+ $table->boolean('banned')->default(false)->index();
+ $table->boolean('active')->default(true)->index();
+ $table->timestamp('last_refreshed_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('remote_auth_instances');
+ }
+};
diff --git a/database/migrations/2023_07_11_080040_add_show_reblogs_to_followers_table.php b/database/migrations/2023_07_11_080040_add_show_reblogs_to_followers_table.php
new file mode 100644
index 000000000..d84433747
--- /dev/null
+++ b/database/migrations/2023_07_11_080040_add_show_reblogs_to_followers_table.php
@@ -0,0 +1,30 @@
+boolean('show_reblogs')->default(true)->index()->after('local_following');
+ $table->boolean('notify')->default(false)->index()->after('show_reblogs');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('followers', function (Blueprint $table) {
+ $table->dropColumn('show_reblogs');
+ $table->dropColumn('notify');
+ });
+ }
+};
diff --git a/database/migrations/2023_08_07_021252_create_profile_aliases_table.php b/database/migrations/2023_08_07_021252_create_profile_aliases_table.php
new file mode 100644
index 000000000..ed9ab6ada
--- /dev/null
+++ b/database/migrations/2023_08_07_021252_create_profile_aliases_table.php
@@ -0,0 +1,32 @@
+id();
+ $table->unsignedBigInteger('profile_id')->nullable()->index();
+ $table->string('acct')->nullable();
+ $table->string('uri')->nullable();
+ $table->foreign('profile_id')->references('id')->on('profiles');
+ $table->unique(['profile_id', 'acct'], 'profile_id_acct_unique');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('profile_aliases');
+ }
+};
diff --git a/database/migrations/2023_08_08_045430_add_moved_to_profile_id_to_profiles_table.php b/database/migrations/2023_08_08_045430_add_moved_to_profile_id_to_profiles_table.php
new file mode 100644
index 000000000..a13bb1705
--- /dev/null
+++ b/database/migrations/2023_08_08_045430_add_moved_to_profile_id_to_profiles_table.php
@@ -0,0 +1,28 @@
+unsignedBigInteger('moved_to_profile_id')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('profiles', function (Blueprint $table) {
+ $table->dropColumn('moved_to_profile_id');
+ });
+ }
+};
diff --git a/database/migrations/2023_08_25_050021_add_indexable_column_to_profiles_table.php b/database/migrations/2023_08_25_050021_add_indexable_column_to_profiles_table.php
new file mode 100644
index 000000000..f735366bd
--- /dev/null
+++ b/database/migrations/2023_08_25_050021_add_indexable_column_to_profiles_table.php
@@ -0,0 +1,28 @@
+boolean('indexable')->default(false)->index();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('profiles', function (Blueprint $table) {
+ $table->dropColumn('indexable');
+ });
+ }
+};
diff --git a/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php b/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php
new file mode 100644
index 000000000..6b62f32c2
--- /dev/null
+++ b/database/migrations/2023_09_12_044900_create_admin_shadow_filters_table.php
@@ -0,0 +1,47 @@
+id();
+ $table->unsignedBigInteger('admin_id')->nullable();
+ $table->morphs('item');
+ $table->boolean('is_local')->default(true)->index();
+ $table->text('note')->nullable();
+ $table->boolean('active')->default(false)->index();
+ $table->json('history')->nullable();
+ $table->json('ruleset')->nullable();
+ $table->boolean('prevent_ap_fanout')->default(false)->index();
+ $table->boolean('prevent_new_dms')->default(false)->index();
+ $table->boolean('ignore_reports')->default(false)->index();
+ $table->boolean('ignore_mentions')->default(false)->index();
+ $table->boolean('ignore_links')->default(false)->index();
+ $table->boolean('ignore_hashtags')->default(false)->index();
+ $table->boolean('hide_from_public_feeds')->default(false)->index();
+ $table->boolean('hide_from_tag_feeds')->default(false)->index();
+ $table->boolean('hide_embeds')->default(false)->index();
+ $table->boolean('hide_from_story_carousel')->default(false)->index();
+ $table->boolean('hide_from_search_autocomplete')->default(false)->index();
+ $table->boolean('hide_from_search')->default(false)->index();
+ $table->boolean('requires_login')->default(false)->index();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('admin_shadow_filters');
+ }
+};
diff --git a/public/css/spa.css b/public/css/spa.css
index 37cce8287..fd4124d27 100644
Binary files a/public/css/spa.css and b/public/css/spa.css differ
diff --git a/public/js/daci.chunk.914d307d69fcfcd4.js b/public/js/daci.chunk.914d307d69fcfcd4.js
deleted file mode 100644
index 36ceb5372..000000000
Binary files a/public/js/daci.chunk.914d307d69fcfcd4.js and /dev/null differ
diff --git a/public/js/daci.chunk.bfa9e4f459fec835.js b/public/js/daci.chunk.bfa9e4f459fec835.js
new file mode 100644
index 000000000..672d36622
Binary files /dev/null and b/public/js/daci.chunk.bfa9e4f459fec835.js differ
diff --git a/public/js/discover~findfriends.chunk.006f0079e9f5a3eb.js b/public/js/discover~findfriends.chunk.006f0079e9f5a3eb.js
deleted file mode 100644
index 1817e2611..000000000
Binary files a/public/js/discover~findfriends.chunk.006f0079e9f5a3eb.js and /dev/null differ
diff --git a/public/js/discover~findfriends.chunk.6bd4ddbabd979778.js b/public/js/discover~findfriends.chunk.6bd4ddbabd979778.js
new file mode 100644
index 000000000..4c0abc8b2
Binary files /dev/null and b/public/js/discover~findfriends.chunk.6bd4ddbabd979778.js differ
diff --git a/public/js/discover~memories.chunk.400f9f019bdb9fdf.js b/public/js/discover~memories.chunk.400f9f019bdb9fdf.js
new file mode 100644
index 000000000..38fdde249
Binary files /dev/null and b/public/js/discover~memories.chunk.400f9f019bdb9fdf.js differ
diff --git a/public/js/discover~memories.chunk.4c0973f4400f25b4.js b/public/js/discover~memories.chunk.4c0973f4400f25b4.js
deleted file mode 100644
index ed1077251..000000000
Binary files a/public/js/discover~memories.chunk.4c0973f4400f25b4.js and /dev/null differ
diff --git a/public/js/discover~myhashtags.chunk.70e91906f0ce857a.js b/public/js/discover~myhashtags.chunk.70e91906f0ce857a.js
deleted file mode 100644
index bdc28118b..000000000
Binary files a/public/js/discover~myhashtags.chunk.70e91906f0ce857a.js and /dev/null differ
diff --git a/public/js/discover~myhashtags.chunk.ee5af357937cad2f.js b/public/js/discover~myhashtags.chunk.ee5af357937cad2f.js
new file mode 100644
index 000000000..f271b0d2c
Binary files /dev/null and b/public/js/discover~myhashtags.chunk.ee5af357937cad2f.js differ
diff --git a/public/js/discover~serverfeed.chunk.017fd16f00c55e60.js b/public/js/discover~serverfeed.chunk.017fd16f00c55e60.js
deleted file mode 100644
index 29f853c66..000000000
Binary files a/public/js/discover~serverfeed.chunk.017fd16f00c55e60.js and /dev/null differ
diff --git a/public/js/discover~serverfeed.chunk.fbe31eedcdafc87e.js b/public/js/discover~serverfeed.chunk.fbe31eedcdafc87e.js
new file mode 100644
index 000000000..841a15b90
Binary files /dev/null and b/public/js/discover~serverfeed.chunk.fbe31eedcdafc87e.js differ
diff --git a/public/js/discover~settings.chunk.72cc15c7b87b662d.js b/public/js/discover~settings.chunk.72cc15c7b87b662d.js
deleted file mode 100644
index a021de414..000000000
Binary files a/public/js/discover~settings.chunk.72cc15c7b87b662d.js and /dev/null differ
diff --git a/public/js/discover~settings.chunk.909aa0316f43235e.js b/public/js/discover~settings.chunk.909aa0316f43235e.js
new file mode 100644
index 000000000..b334ca360
Binary files /dev/null and b/public/js/discover~settings.chunk.909aa0316f43235e.js differ
diff --git a/public/js/home.chunk.2d93b527d492e6de.js b/public/js/home.chunk.2d93b527d492e6de.js
deleted file mode 100644
index a1f10eddc..000000000
Binary files a/public/js/home.chunk.2d93b527d492e6de.js and /dev/null differ
diff --git a/public/js/home.chunk.bd623a430a5584c2.js b/public/js/home.chunk.bd623a430a5584c2.js
new file mode 100644
index 000000000..5fa049906
Binary files /dev/null and b/public/js/home.chunk.bd623a430a5584c2.js differ
diff --git a/public/js/home.chunk.2d93b527d492e6de.js.LICENSE.txt b/public/js/home.chunk.bd623a430a5584c2.js.LICENSE.txt
similarity index 100%
rename from public/js/home.chunk.2d93b527d492e6de.js.LICENSE.txt
rename to public/js/home.chunk.bd623a430a5584c2.js.LICENSE.txt
diff --git a/public/js/landing.js b/public/js/landing.js
index 0feb25b13..2980b9e5a 100644
Binary files a/public/js/landing.js and b/public/js/landing.js differ
diff --git a/public/js/manifest.js b/public/js/manifest.js
index 7380d20eb..ff9202503 100644
Binary files a/public/js/manifest.js and b/public/js/manifest.js differ
diff --git a/public/js/post.chunk.729ca668f46545cb.js b/public/js/post.chunk.729ca668f46545cb.js
new file mode 100644
index 000000000..3ede2963b
Binary files /dev/null and b/public/js/post.chunk.729ca668f46545cb.js differ
diff --git a/public/js/post.chunk.cd535334efc77c34.js.LICENSE.txt b/public/js/post.chunk.729ca668f46545cb.js.LICENSE.txt
similarity index 100%
rename from public/js/post.chunk.cd535334efc77c34.js.LICENSE.txt
rename to public/js/post.chunk.729ca668f46545cb.js.LICENSE.txt
diff --git a/public/js/post.chunk.cd535334efc77c34.js b/public/js/post.chunk.cd535334efc77c34.js
deleted file mode 100644
index 6479707c6..000000000
Binary files a/public/js/post.chunk.cd535334efc77c34.js and /dev/null differ
diff --git a/public/js/profile.chunk.029572d9018fc65f.js b/public/js/profile.chunk.029572d9018fc65f.js
new file mode 100644
index 000000000..493a62661
Binary files /dev/null and b/public/js/profile.chunk.029572d9018fc65f.js differ
diff --git a/public/js/profile.chunk.4049e1eecea398ee.js b/public/js/profile.chunk.4049e1eecea398ee.js
deleted file mode 100644
index 2ef3482d3..000000000
Binary files a/public/js/profile.chunk.4049e1eecea398ee.js and /dev/null differ
diff --git a/public/js/remote_auth.js b/public/js/remote_auth.js
new file mode 100644
index 000000000..88c60f980
Binary files /dev/null and b/public/js/remote_auth.js differ
diff --git a/public/js/vendor.js b/public/js/vendor.js
index e95bc6e95..c4633d457 100644
Binary files a/public/js/vendor.js and b/public/js/vendor.js differ
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index 7874f175a..c580e4791 100644
Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ
diff --git a/resources/assets/components/partials/TimelineStatus.vue b/resources/assets/components/partials/TimelineStatus.vue
index 6351d3f51..06e225eb9 100644
--- a/resources/assets/components/partials/TimelineStatus.vue
+++ b/resources/assets/components/partials/TimelineStatus.vue
@@ -3,18 +3,20 @@
+ :status="shadowStatus" />
-
+
+
+
+
+
+
Sensitive Content
+
{{ status.spoiler_text && status.spoiler_text.length ? status.spoiler_text : 'This post may contain sensitive content' }}
+
+ See post
+
+
+
diff --git a/resources/assets/components/partials/post/PostHeader.vue b/resources/assets/components/partials/post/PostHeader.vue
index ddbbf740c..afa20b10a 100644
--- a/resources/assets/components/partials/post/PostHeader.vue
+++ b/resources/assets/components/partials/post/PostHeader.vue
@@ -1,348 +1,382 @@
-
diff --git a/resources/assets/components/remote-auth/GettingStartedComponent.vue b/resources/assets/components/remote-auth/GettingStartedComponent.vue
new file mode 100644
index 000000000..241730fe8
--- /dev/null
+++ b/resources/assets/components/remote-auth/GettingStartedComponent.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
+
+
+
Oops!
+
+
We cannot complete your request at this time
+
It appears that you've signed-in on other Pixelfed instances and reached the max limit that we accept.
+
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
+
+
+
+
Welcome back!
+
One moment please, we're logging you in...
+
+
+
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
+
+
+
Oops, something went wrong!
+
+
We cannot complete your request at this time, please try again later.
+
+
This can happen for a few different reasons:
+
+
+ The remote instance cannot be reached
+ The remote instance is not supported yet
+ The remote instance has been disabled by admins
+ The remote instance does not allow remote logins
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/remote-auth/StartComponent.vue b/resources/assets/components/remote-auth/StartComponent.vue
new file mode 100644
index 000000000..c93fc8f71
--- /dev/null
+++ b/resources/assets/components/remote-auth/StartComponent.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
+
+
+
+
+
+
+
+
Sign-in with Mastodon
+
+
Select your Mastodon server:
+
+ {{ domain }}
+
+
+
+ Sign-in with a different server
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/sections/Timeline.vue b/resources/assets/components/sections/Timeline.vue
index 2f222096a..dd2c7d2f0 100644
--- a/resources/assets/components/sections/Timeline.vue
+++ b/resources/assets/components/sections/Timeline.vue
@@ -8,6 +8,30 @@
+
+
+
+
+
Introducing Reblogs in feeds
+
+
+ See reblogs from accounts you follow in your home feed!
+
+
+ You can disable reblogs in feeds on the Timeline Settings page.
+
+
+
+
+ Show reblogs in home feed
+
+
+ Hide
+
+
+
+
+
{
+ this.settings = res.data;
+
+ if(!res.data) {
+ this.showReblogBanner = true;
+ } else {
+ if(res.data.hasOwnProperty('hide_reblog_banner')) {
+ } else if(res.data.hasOwnProperty('enable_reblogs')) {
+ if(!res.data.enable_reblogs) {
+ this.showReblogBanner = true;
+ }
+ } else {
+ this.showReblogBanner = true;
+ }
}
+ })
+ .finally(() => {
+ this.fetchTimeline();
+ })
+ },
+
+ fetchTimeline(scrollToTop = false) {
+ let url, params;
+ if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) {
+ url = `/api/v1/timelines/home`;
+ params = {
+ '_pe': 1,
+ max_id: this.max_id,
+ limit: 6,
+ include_reblogs: true,
+ }
+ } else {
+ url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
+ params = {
+ max_id: this.max_id,
+ limit: 6,
+ }
+ }
+ axios.get(url, {
+ params: params
}).then(res => {
let ids = res.data.map(p => {
if(p && p.hasOwnProperty('relationship')) {
@@ -242,12 +304,24 @@
this.isFetchingMore = true;
- let url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
- axios.get(url, {
- params: {
+ let url, params;
+ if(this.getScope() === 'home' && this.settings && this.settings.hasOwnProperty('enable_reblogs') && this.settings.enable_reblogs) {
+ url = `/api/v1/timelines/home`;
+ params = {
+ '_pe': 1,
max_id: this.max_id,
- limit: 6
+ limit: 6,
+ include_reblogs: true,
}
+ } else {
+ url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
+ params = {
+ max_id: this.max_id,
+ limit: 6,
+ }
+ }
+ axios.get(url, {
+ params: params
}).then(res => {
if(!res.data.length) {
this.endFeedReached = true;
@@ -287,17 +361,30 @@
likeStatus(index) {
let status = this.feed[index];
- let state = status.favourited;
- let count = status.favourites_count;
- this.feed[index].favourites_count = count + 1;
- this.feed[index].favourited = !status.favourited;
+ if(status.reblog) {
+ status = status.reblog;
+ let state = status.favourited;
+ let count = status.favourites_count;
+ this.feed[index].reblog.favourites_count = count + 1;
+ this.feed[index].reblog.favourited = !status.favourited;
+ } else {
+ let state = status.favourited;
+ let count = status.favourites_count;
+ this.feed[index].favourites_count = count + 1;
+ this.feed[index].favourited = !status.favourited;
+ }
axios.post('/api/v1/statuses/' + status.id + '/favourite')
.then(res => {
//
}).catch(err => {
- this.feed[index].favourites_count = count;
- this.feed[index].favourited = false;
+ if(status.reblog) {
+ this.feed[index].reblog.favourites_count = count;
+ this.feed[index].reblog.favourited = false;
+ } else {
+ this.feed[index].favourites_count = count;
+ this.feed[index].favourited = false;
+ }
let el = document.createElement('p');
el.classList.add('text-left');
@@ -339,17 +426,30 @@
unlikeStatus(index) {
let status = this.feed[index];
- let state = status.favourited;
- let count = status.favourites_count;
- this.feed[index].favourites_count = count - 1;
- this.feed[index].favourited = !status.favourited;
+ if(status.reblog) {
+ status = status.reblog;
+ let state = status.favourited;
+ let count = status.favourites_count;
+ this.feed[index].reblog.favourites_count = count - 1;
+ this.feed[index].reblog.favourited = !status.favourited;
+ } else {
+ let state = status.favourited;
+ let count = status.favourites_count;
+ this.feed[index].favourites_count = count - 1;
+ this.feed[index].favourited = !status.favourited;
+ }
axios.post('/api/v1/statuses/' + status.id + '/unfavourite')
.then(res => {
//
}).catch(err => {
- this.feed[index].favourites_count = count;
- this.feed[index].favourited = false;
+ if(status.reblog && status.pf_type == 'share') {
+ this.feed[index].reblog.favourites_count = count;
+ this.feed[index].reblog.favourited = false;
+ } else {
+ this.feed[index].favourites_count = count;
+ this.feed[index].favourited = false;
+ }
})
},
@@ -371,7 +471,8 @@
openLikesModal(idx) {
this.postIndex = idx;
- this.likesModalPost = this.feed[this.postIndex];
+ let post = this.feed[this.postIndex];
+ this.likesModalPost = post.reblog ? post.reblog : post;
this.showLikesModal = true;
this.$nextTick(() => {
this.$refs.likesModal.open();
@@ -380,7 +481,8 @@
openSharesModal(idx) {
this.postIndex = idx;
- this.sharesModalPost = this.feed[this.postIndex];
+ let post = this.feed[this.postIndex];
+ this.sharesModalPost = post.reblog ? post.reblog : post;
this.showSharesModal = true;
this.$nextTick(() => {
this.$refs.sharesModal.open();
@@ -418,19 +520,32 @@
},
counterChange(index, type) {
+ let post = this.feed[index];
switch(type) {
case 'comment-increment':
- this.feed[index].reply_count = this.feed[index].reply_count + 1;
+ if(post.reblog != null) {
+ this.feed[index].reblog.reply_count = this.feed[index].reblog.reply_count + 1;
+ } else {
+ this.feed[index].reply_count = this.feed[index].reply_count + 1;
+ }
break;
case 'comment-decrement':
- this.feed[index].reply_count = this.feed[index].reply_count - 1;
+ if(post.reblog != null) {
+ this.feed[index].reblog.reply_count = this.feed[index].reblog.reply_count - 1;
+ } else {
+ this.feed[index].reply_count = this.feed[index].reply_count - 1;
+ }
break;
}
},
openCommentLikesModal(post) {
- this.likesModalPost = post;
+ if(post.reblog != null) {
+ this.likesModalPost = post.reblog;
+ } else {
+ this.likesModalPost = post;
+ }
this.showLikesModal = true;
this.$nextTick(() => {
this.$refs.likesModal.open();
@@ -439,33 +554,59 @@
shareStatus(index) {
let status = this.feed[index];
- let state = status.reblogged;
- let count = status.reblogs_count;
- this.feed[index].reblogs_count = count + 1;
- this.feed[index].reblogged = !status.reblogged;
+ if(status.reblog) {
+ status = status.reblog;
+ let state = status.reblogged;
+ let count = status.reblogs_count;
+ this.feed[index].reblog.reblogs_count = count + 1;
+ this.feed[index].reblog.reblogged = !status.reblogged;
+ } else {
+ let state = status.reblogged;
+ let count = status.reblogs_count;
+ this.feed[index].reblogs_count = count + 1;
+ this.feed[index].reblogged = !status.reblogged;
+ }
axios.post('/api/v1/statuses/' + status.id + '/reblog')
.then(res => {
//
}).catch(err => {
- this.feed[index].reblogs_count = count;
- this.feed[index].reblogged = false;
+ if(status.reblog) {
+ this.feed[index].reblog.reblogs_count = count;
+ this.feed[index].reblog.reblogged = false;
+ } else {
+ this.feed[index].reblogs_count = count;
+ this.feed[index].reblogged = false;
+ }
})
},
unshareStatus(index) {
let status = this.feed[index];
- let state = status.reblogged;
- let count = status.reblogs_count;
- this.feed[index].reblogs_count = count - 1;
- this.feed[index].reblogged = !status.reblogged;
+ if(status.reblog) {
+ status = status.reblog;
+ let state = status.reblogged;
+ let count = status.reblogs_count;
+ this.feed[index].reblog.reblogs_count = count - 1;
+ this.feed[index].reblog.reblogged = !status.reblogged;
+ } else {
+ let state = status.reblogged;
+ let count = status.reblogs_count;
+ this.feed[index].reblogs_count = count - 1;
+ this.feed[index].reblogged = !status.reblogged;
+ }
axios.post('/api/v1/statuses/' + status.id + '/unreblog')
.then(res => {
//
}).catch(err => {
- this.feed[index].reblogs_count = count;
- this.feed[index].reblogged = false;
+ if(status.reblog) {
+ this.feed[index].reblog.reblogs_count = count;
+ this.feed[index].reblog.reblogged = false;
+ } else {
+ this.feed[index].reblogs_count = count;
+ this.feed[index].reblogged = false;
+ }
})
},
@@ -480,11 +621,19 @@
handleBookmark(index) {
let p = this.feed[index];
+ if(p.reblog) {
+ p = p.reblog;
+ }
+
axios.post('/i/bookmark', {
item: p.id
})
.then(res => {
- this.feed[index].bookmarked = !p.bookmarked;
+ if(this.feed[index].reblog) {
+ this.feed[index].reblog.bookmarked = !p.bookmarked;
+ } else {
+ this.feed[index].bookmarked = !p.bookmarked;
+ }
})
.catch(err => {
// this.feed[index].bookmarked = false;
@@ -497,31 +646,51 @@
},
follow(index) {
- // this.feed[index].relationship.following = true;
-
- axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/follow')
- .then(res => {
- this.$store.commit('updateRelationship', [res.data]);
- this.updateProfile({ following_count: this.profile.following_count + 1 });
- this.feed[index].account.followers_count = this.feed[index].account.followers_count + 1;
- }).catch(err => {
- swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
- this.feed[index].relationship.following = false;
- });
+ if(this.feed[index].reblog) {
+ axios.post('/api/v1/accounts/' + this.feed[index].reblog.account.id + '/follow')
+ .then(res => {
+ this.$store.commit('updateRelationship', [res.data]);
+ this.updateProfile({ following_count: this.profile.following_count + 1 });
+ this.feed[index].reblog.account.followers_count = this.feed[index].reblog.account.followers_count + 1;
+ }).catch(err => {
+ swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
+ this.feed[index].reblog.relationship.following = false;
+ });
+ } else {
+ axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/follow')
+ .then(res => {
+ this.$store.commit('updateRelationship', [res.data]);
+ this.updateProfile({ following_count: this.profile.following_count + 1 });
+ this.feed[index].account.followers_count = this.feed[index].account.followers_count + 1;
+ }).catch(err => {
+ swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
+ this.feed[index].relationship.following = false;
+ });
+ }
},
unfollow(index) {
- // this.feed[index].relationship.following = false;
-
- axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/unfollow')
- .then(res => {
- this.$store.commit('updateRelationship', [res.data]);
- this.updateProfile({ following_count: this.profile.following_count - 1 });
- this.feed[index].account.followers_count = this.feed[index].account.followers_count - 1;
- }).catch(err => {
- swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error');
- this.feed[index].relationship.following = true;
- });
+ if(this.feed[index].reblog) {
+ axios.post('/api/v1/accounts/' + this.feed[index].reblog.account.id + '/unfollow')
+ .then(res => {
+ this.$store.commit('updateRelationship', [res.data]);
+ this.updateProfile({ following_count: this.profile.following_count - 1 });
+ this.feed[index].reblog.account.followers_count = this.feed[index].reblog.account.followers_count - 1;
+ }).catch(err => {
+ swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error');
+ this.feed[index].reblog.relationship.following = true;
+ });
+ } else {
+ axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/unfollow')
+ .then(res => {
+ this.$store.commit('updateRelationship', [res.data]);
+ this.updateProfile({ following_count: this.profile.following_count - 1 });
+ this.feed[index].account.followers_count = this.feed[index].account.followers_count - 1;
+ }).catch(err => {
+ swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error');
+ this.feed[index].relationship.following = true;
+ });
+ }
},
updateProfile(delta) {
@@ -568,7 +737,31 @@
this.$nextTick(() => {
this.forceUpdateIdx++;
});
- }
+ },
+
+ enableReblogs() {
+ this.enablingReblogs = true;
+
+ axios.post('/api/pixelfed/v1/web/settings', {
+ field: 'enable_reblogs',
+ value: true
+ })
+ .then(res => {
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ })
+ },
+
+ hideReblogs() {
+ this.showReblogBanner = false;
+ axios.post('/api/pixelfed/v1/web/settings', {
+ field: 'hide_reblog_banner',
+ value: true
+ })
+ .then(res => {
+ })
+ },
},
watch: {
diff --git a/resources/assets/js/remote_auth.js b/resources/assets/js/remote_auth.js
new file mode 100644
index 000000000..a852e2d5f
--- /dev/null
+++ b/resources/assets/js/remote_auth.js
@@ -0,0 +1,9 @@
+Vue.component(
+ 'remote-auth-start-component',
+ require('./../components/remote-auth/StartComponent.vue').default
+);
+
+Vue.component(
+ 'remote-auth-getting-started-component',
+ require('./../components/remote-auth/GettingStartedComponent.vue').default
+);
diff --git a/resources/assets/sass/spa.scss b/resources/assets/sass/spa.scss
index e2ed0e054..72e08e16b 100644
--- a/resources/assets/sass/spa.scss
+++ b/resources/assets/sass/spa.scss
@@ -1,166 +1,172 @@
@import "lib/ibmplexsans";
:root {
- --light: #fff;
- --dark: #000;
- --body-bg: rgba(243,244,246,1);
- --body-color: #212529;
- --nav-bg: #fff;
- --bg-light: #f8f9fa;
+ --light: #fff;
+ --dark: #000;
+ --body-bg: rgba(243,244,246,1);
+ --body-color: #212529;
+ --nav-bg: #fff;
+ --bg-light: #f8f9fa;
- --primary: #3B82F6;
- --light-gray: #f8f9fa;
- --text-lighter: #94a3b8;
+ --primary: #3B82F6;
+ --light-gray: #f8f9fa;
+ --text-lighter: #94a3b8;
- --card-bg: #fff;
- --light-hover-bg: #f9fafb;
- --btn-light-border: #fff;
- --input-border: #e2e8f0;
- --comment-bg: #eff2f5;
- --border-color: #dee2e6;
- --card-header-accent: #f9fafb;
+ --card-bg: #fff;
+ --light-hover-bg: #f9fafb;
+ --btn-light-border: #fff;
+ --input-border: #e2e8f0;
+ --comment-bg: #eff2f5;
+ --border-color: #dee2e6;
+ --card-header-accent: #f9fafb;
- --dropdown-item-hover-bg: #e9ecef;
- --dropdown-item-hover-color: #16181b;
- --dropdown-item-color: #64748b;
- --dropdown-item-active-color: #334155;
+ --dropdown-item-hover-bg: #e9ecef;
+ --dropdown-item-hover-color: #16181b;
+ --dropdown-item-color: #64748b;
+ --dropdown-item-active-color: #334155;
}
@media (prefers-color-scheme: dark) {
- :root {
- --light: #000;
- --dark: #fff;
- --body-bg: #000;
- --body-color: #9ca3af;
- --nav-bg: #000;
- --bg-light: #212124;
+ :root {
+ --light: #000;
+ --dark: #fff;
+ --body-bg: #000;
+ --body-color: #9ca3af;
+ --nav-bg: #000;
+ --bg-light: #212124;
- --light-gray: #212124;
- --text-lighter: #818181;
+ --light-gray: #212124;
+ --text-lighter: #818181;
- --card-bg: #161618;
- --light-hover-bg: #212124;
- --btn-light-border: #161618;
- --input-border: #161618;
- --comment-bg: #212124;
- --border-color: #212124;
- --card-header-accent: #212124;
+ --card-bg: #161618;
+ --light-hover-bg: #212124;
+ --btn-light-border: #161618;
+ --input-border: #161618;
+ --comment-bg: #212124;
+ --border-color: #212124;
+ --card-header-accent: #212124;
- --dropdown-item-hover-bg: #000;
- --dropdown-item-hover-color: #818181;
- --dropdown-item-color: #64748b;
- --dropdown-item-active-color: #fff;
- }
+ --dropdown-item-hover-bg: #000;
+ --dropdown-item-hover-color: #818181;
+ --dropdown-item-color: #64748b;
+ --dropdown-item-active-color: #fff;
+ }
}
.force-light-mode {
- --light: #fff;
- --dark: #000;
- --body-bg: rgba(243,244,246,1);
- --body-color: #212529;
- --nav-bg: #fff;
- --bg-light: #f8f9fa;
+ --light: #fff;
+ --dark: #000;
+ --body-bg: rgba(243,244,246,1);
+ --body-color: #212529;
+ --nav-bg: #fff;
+ --bg-light: #f8f9fa;
- --primary: #3B82F6;
- --light-gray: #f8f9fa;
- --text-lighter: #94a3b8;
+ --primary: #3B82F6;
+ --light-gray: #f8f9fa;
+ --text-lighter: #94a3b8;
- --card-bg: #fff;
- --light-hover-bg: #f9fafb;
- --btn-light-border: #fff;
- --input-border: #e2e8f0;
- --comment-bg: #eff2f5;
- --border-color: #dee2e6;
- --card-header-accent: #f9fafb;
+ --card-bg: #fff;
+ --light-hover-bg: #f9fafb;
+ --btn-light-border: #fff;
+ --input-border: #e2e8f0;
+ --comment-bg: #eff2f5;
+ --border-color: #dee2e6;
+ --card-header-accent: #f9fafb;
- --dropdown-item-hover-bg: #e9ecef;
- --dropdown-item-hover-color: #16181b;
- --dropdown-item-color: #64748b;
- --dropdown-item-active-color: #334155;
+ --dropdown-item-hover-bg: #e9ecef;
+ --dropdown-item-hover-color: #16181b;
+ --dropdown-item-color: #64748b;
+ --dropdown-item-active-color: #334155;
}
.force-dark-mode {
- --light: #000;
- --dark: #fff;
- --body-bg: #000;
- --body-color: #9ca3af;
- --nav-bg: #000;
- --bg-light: #212124;
+ --light: #000;
+ --dark: #fff;
+ --body-bg: #000;
+ --body-color: #9ca3af;
+ --nav-bg: #000;
+ --bg-light: #212124;
- --light-gray: #212124;
- --text-lighter: #818181;
+ --light-gray: #212124;
+ --text-lighter: #818181;
- --card-bg: #161618;
- --light-hover-bg: #212124;
- --btn-light-border: #161618;
- --input-border: #161618;
- --comment-bg: #212124;
- --border-color: #212124;
- --card-header-accent: #212124;
+ --card-bg: #161618;
+ --light-hover-bg: #212124;
+ --btn-light-border: #161618;
+ --input-border: #161618;
+ --comment-bg: #212124;
+ --border-color: #212124;
+ --card-header-accent: #212124;
- --dropdown-item-hover-bg: #000;
- --dropdown-item-hover-color: #818181;
- --dropdown-item-color: #64748b;
- --dropdown-item-active-color: #b3b3b3;
+ --dropdown-item-hover-bg: #000;
+ --dropdown-item-hover-color: #818181;
+ --dropdown-item-color: #64748b;
+ --dropdown-item-active-color: #b3b3b3;
}
body {
- background: var(--body-bg);
- font-family: 'IBM Plex Sans', sans-serif;
- color: var(--body-color);
+ background: var(--body-bg);
+ font-family: 'IBM Plex Sans', sans-serif;
+ color: var(--body-color);
}
.web-wrapper {
- margin-bottom: 10rem;
+ margin-bottom: 10rem;
}
.container-fluid {
- max-width: 1440px !important;
+ max-width: 1440px !important;
}
.jumbotron {
- border-radius: 18px;
+ border-radius: 18px;
}
.rounded-px {
- border-radius: 18px;
+ border-radius: 18px;
}
.doc-body {
- p:last-child {
- margin-bottom: 0;
- }
+ p:last-child {
+ margin-bottom: 0;
+ }
}
.navbar-laravel {
- background-color: var(--nav-bg);
+ background-color: var(--nav-bg);
}
.sticky-top {
- z-index: 2;
+ z-index: 2;
}
.navbar-light .navbar-brand {
- color: var(--dark);
+ color: var(--dark);
- &:hover {
- color: var(--dark);
- }
+ &:hover {
+ color: var(--dark);
+ }
}
.primary {
- color: var(--primary);
+ color: var(--primary);
+}
+
+.bg-g-amin {
+ background: #8E2DE2;
+ background: -webkit-linear-gradient(to right, #4A00E0, #8E2DE2);
+ background: linear-gradient(to left, #4A00E0, #8E2DE2);
}
.text-lighter {
- color: var(--text-lighter) !important;
+ color: var(--text-lighter) !important;
}
.text-dark {
color: var(--body-color) !important;
&:hover {
- color: var(--dark) !important;
+ color: var(--dark) !important;
}
}
@@ -169,16 +175,16 @@ a.text-dark:hover {
}
.badge-primary {
- background-color: var(--primary);
+ background-color: var(--primary);
}
.btn-primary {
- background-color: var(--primary);
- color: #fff !important;
+ background-color: var(--primary);
+ color: #fff !important;
}
.btn-outline-light {
- border-color: var(--light-gray);
+ border-color: var(--light-gray);
}
.border {
@@ -187,51 +193,51 @@ a.text-dark:hover {
.bg-white,
.bg-light {
- background-color: var(--bg-light) !important;
- border-color: var(--bg-light) !important;
+ background-color: var(--bg-light) !important;
+ border-color: var(--bg-light) !important;
}
.btn-light {
- background-color: var(--light-gray);
- border-color: var(--btn-light-border);
- color: var(--body-color);
+ background-color: var(--light-gray);
+ border-color: var(--btn-light-border);
+ color: var(--body-color);
- &:hover {
- color: var(--body-color);
- background-color: var(--card-bg);
- border-color: var(--btn-light-border);
- }
+ &:hover {
+ color: var(--body-color);
+ background-color: var(--card-bg);
+ border-color: var(--btn-light-border);
+ }
}
.autocomplete-input {
- border: 1px solid var(--light-gray) !important;
- color: var(--body-color);
+ border: 1px solid var(--light-gray) !important;
+ color: var(--body-color);
}
.autocomplete-result-list {
- background: var(--light) !important;
- z-index: 2 !important;
+ background: var(--light) !important;
+ z-index: 2 !important;
}
.dropdown-menu,
span.twitter-typeahead .tt-menu,
.form-control {
- border: 1px solid var(--border-color) !important;
- color: var(--body-color);
- background-color: var(--card-bg);
+ border: 1px solid var(--border-color) !important;
+ color: var(--body-color);
+ background-color: var(--card-bg);
}
.tribute-container li,
.dropdown-item,
span.twitter-typeahead .tt-suggestion {
- color: var(--body-color);
+ color: var(--body-color);
}
.dropdown-item:hover,
span.twitter-typeahead .tt-suggestion:hover,
.dropdown-item:focus,
span.twitter-typeahead .tt-suggestion:focus {
- color: var(--dropdown-item-hover-color);
+ color: var(--dropdown-item-hover-color);
background-color: var(--dropdown-item-hover-bg);
text-decoration: none;
}
@@ -245,7 +251,7 @@ span.twitter-typeahead .tt-suggestion:focus {
.card-header,
.card-footer,
.ph-item {
- background-color: var(--card-bg);
+ background-color: var(--card-bg);
}
.badge-light,
@@ -253,143 +259,147 @@ span.twitter-typeahead .tt-suggestion:focus {
.ph-avatar,
.ph-picture,
.ph-row div {
- background-color: var(--light-gray);
+ background-color: var(--light-gray);
}
.card-header,
.border-top,
.border-bottom {
- border-color: var(--border-color) !important;
+ border-color: var(--border-color) !important;
}
.modal-header,
.modal-footer {
- border-color: var(--border-color);
+ border-color: var(--border-color);
}
.compose-action:hover {
- background-color: var(--light-gray) !important;
+ background-color: var(--light-gray) !important;
}
.dropdown-divider {
- border-color: var(--dropdown-item-hover-bg);
+ border-color: var(--dropdown-item-hover-bg);
}
.metro-nav {
- &.flex-column {
- background-color: var(--card-bg);
+ &.flex-column {
+ background-color: var(--card-bg);
- .nav-item {
- .nav-link:hover {
- background-color: var(--light-hover-bg);
- }
- }
- }
+ .nav-item {
+ .nav-link:hover {
+ background-color: var(--light-hover-bg);
+ }
+ }
+ }
}
.child-reply-form {
- .form-control {
- border-color: var(--input-border);
- color: var(--body-color);
- }
+ .form-control {
+ border-color: var(--input-border);
+ color: var(--body-color);
+ }
}
.ui-menu {
- .btn-group {
- .btn:first-child {
- border-top-left-radius: 50rem;
- border-bottom-left-radius: 50rem;
- }
+ .btn-group {
+ .btn:first-child {
+ border-top-left-radius: 50rem;
+ border-bottom-left-radius: 50rem;
+ }
- .btn:last-child {
- border-top-right-radius: 50rem;
- border-bottom-right-radius: 50rem;
- }
+ .btn:last-child {
+ border-top-right-radius: 50rem;
+ border-bottom-right-radius: 50rem;
+ }
- .btn-primary {
- font-weight: bold;
- }
- }
+ .btn-primary {
+ font-weight: bold;
+ }
+ }
- .b-custom-control-lg {
- padding-bottom: 8px;
- }
+ .b-custom-control-lg {
+ padding-bottom: 8px;
+ }
}
.content-label {
- &-wrapper {
- div:not(.content-label) {
- height: 100%;
- }
- }
+ &-wrapper {
+ div:not(.content-label) {
+ height: 100%;
+ }
+ }
- &-text {
- width: 80%;
- @media (min-width: 768px) {
- width: 50%;
- }
- }
+ &-text {
+ width: 80%;
+ @media (min-width: 768px) {
+ width: 50%;
+ }
+ }
}
.compose-modal-component {
- .form-control:focus {
- color: var(--body-color);
+ .form-control:focus {
+ color: var(--body-color);
}
}
.modal-body {
- .nav-tabs .nav-link.active,
- .nav-tabs .nav-item.show .nav-link {
- background-color: transparent;
- border-color: var(--border-color);
- }
+ .nav-tabs .nav-link.active,
+ .nav-tabs .nav-item.show .nav-link {
+ background-color: transparent;
+ border-color: var(--border-color);
+ }
- .nav-tabs .nav-link:hover,
- .nav-tabs .nav-link:focus {
- border-color: var(--border-color);
- }
+ .nav-tabs .nav-link:hover,
+ .nav-tabs .nav-link:focus {
+ border-color: var(--border-color);
+ }
- .form-control:focus {
- color: var(--body-color);
+ .form-control:focus {
+ color: var(--body-color);
}
}
.tribute-container {
- border: 0;
+ border: 0;
- ul {
- margin-top: 0;
- border-color: var(--border-color);
- }
+ ul {
+ margin-top: 0;
+ border-color: var(--border-color);
+ }
- li {
- padding: 0.5rem 1rem;
- border-top: 0;
- border-left: 0;
- border-right: 0;
- font-size: 13px;
+ li {
+ padding: 0.5rem 1rem;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ font-size: 13px;
- &:not(:last-child) {
- border-bottom: 1px solid var(--border-color);
- }
+ &:not(:last-child) {
+ border-bottom: 1px solid var(--border-color);
+ }
- &.highlight,
- &:hover {
- color: var(--body-color);
- font-weight: bold;
- background: rgba(44, 120, 191, 0.25);
- }
- }
+ &.highlight,
+ &:hover {
+ color: var(--body-color);
+ font-weight: bold;
+ background: rgba(44, 120, 191, 0.25);
+ }
+ }
+}
+
+.ft-std {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.timeline-status-component {
- .username {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- margin-bottom: -3px;
- word-break: break-word;
+ .username {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ margin-bottom: -3px;
+ word-break: break-word;
- @media (min-width: 768px) {
- font-size: 17px;
- }
- }
+ @media (min-width: 768px) {
+ font-size: 17px;
+ }
+ }
}
diff --git a/resources/views/admin/asf/create.blade.php b/resources/views/admin/asf/create.blade.php
new file mode 100644
index 000000000..8fc88e4bd
--- /dev/null
+++ b/resources/views/admin/asf/create.blade.php
@@ -0,0 +1,64 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+
+
+
+
+
+ @if ($errors->any())
+
+
+ @foreach ($errors->all() as $error)
+ {{ $error }}
+ @endforeach
+
+
+ @endif
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/asf/edit.blade.php b/resources/views/admin/asf/edit.blade.php
new file mode 100644
index 000000000..6d7a633f0
--- /dev/null
+++ b/resources/views/admin/asf/edit.blade.php
@@ -0,0 +1,64 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+
+
+
+
+
+ @if ($errors->any())
+
+
+ @foreach ($errors->all() as $error)
+ {{ $error }}
+ @endforeach
+
+
+ @endif
+
+
+ @csrf
+
+ Username
+
+
+
+ Filters
+
+
+
+ hide_from_public_feeds ? 'checked=""' : '' !!}>
+ Hide public posts from public feed
+
+
+ {{--
--}}
+
+
+
+ Note
+ {{ $filter->note }}
+
+
+ active ? 'checked=""' : ''}}>
+ Mark as Active
+
+
+ Save
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/asf/home.blade.php b/resources/views/admin/asf/home.blade.php
new file mode 100644
index 000000000..4fbb7730f
--- /dev/null
+++ b/resources/views/admin/asf/home.blade.php
@@ -0,0 +1,81 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+
+
+
+
+
+
+
+
+
+ ID
+ Username
+ Hide Feeds
+ Active
+ Created
+
+
+
+ @foreach($filters as $filter)
+
+ {{ $filter->id }}
+
+
+
+
+
+ @{{ $filter->account()['acct'] }}
+
+
+
+ {{ $filter->hide_from_public_feeds ? '✅' : ''}}
+ {{ $filter->active ? '✅' : ''}}
+ {{ $filter->created_at->diffForHumans() }}
+
+ @endforeach
+
+
+
+
+ {{ $filters->links() }}
+
+
+
+
+@endsection
diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php
index 2c784ccbb..b5ac22438 100644
--- a/resources/views/admin/users/edit.blade.php
+++ b/resources/views/admin/users/edit.blade.php
@@ -78,7 +78,7 @@
Website
-
+
Admin
@@ -97,4 +97,4 @@
-@endsection
\ No newline at end of file
+@endsection
diff --git a/resources/views/admin/users/show.blade.php b/resources/views/admin/users/show.blade.php
index 7652db6ea..f55c1dac3 100644
--- a/resources/views/admin/users/show.blade.php
+++ b/resources/views/admin/users/show.blade.php
@@ -58,52 +58,97 @@
ADMIN
@endif
-
- Joined {{$profile->created_at->diffForHumans()}}
-
+
+
+
+
+ {{$profile->created_at->diffForHumans()}}
+
+
+ Joined
+
+
+ @if($user->last_active_at)
+
+
+ {{$user->last_active_at->diffForHumans()}}
+
+
+ Last Active
+
+
+ @endif
+
-
-
-
- bookmarks
- {{$profile->bookmarks()->count()}}
-
-
- collections
- {{$profile->collections()->count()}}
-
-
- likes
- {{$profile->likes()->count()}}
-
-
- reports
- {{$profile->reports()->count()}}
-
-
- reported
- {{$profile->reported()->count()}}
-
-
- Active stories
- {{$profile->stories()->count()}}
-
-
- storage used
- {{PrettyNumber::size($profile->media()->sum('size'))}} / {{PrettyNumber::size(config_cache('pixelfed.max_account_size') * 1000)}}
-
-
-
+
+
+
+
email
+
{{$user->email}}
+
+
+ @if($profile->website)
+
+
website
+
{{$profile->website}}
+
+ @endif
+
+
+
bookmarks
+
{{$profile->bookmarks()->count()}}
+
+
+
+
collections
+
{{$profile->collections()->count()}}
+
+
+
+
likes
+
{{$profile->likes()->count()}}
+
+
+
+
reports
+
{{$profile->reports()->count()}}
+
+
+
+
reported
+
{{$profile->reported()->count()}}
+
+
+
+
active stories
+
{{$profile->stories()->count()}}
+
+
+
+
storage used
+
{{PrettyNumber::size($profile->media()->sum('size'))}} / {{PrettyNumber::size(config_cache('pixelfed.max_account_size') * 1000)}}
+
+
+
+
bio
+
{{ $profile->bio }}
+
+
Recent Posts
- @foreach($profile->statuses()->whereHas('media')->latest()->take(9)->get() as $item)
-
+ @foreach($profile->statuses()->whereHas('media')->latest()->take(16)->get() as $item)
+ @php($post = \App\Services\StatusService::get($item->id, false))
+
@endforeach
@@ -119,3 +164,44 @@
@endsection
+
+@push('styles')
+
+@endpush
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index 5654980cf..3eff0cf06 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -64,7 +64,7 @@
@endif
-