mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-02-03 10:20:46 +00:00
Merge branch 'pixelfed:dev' into main
This commit is contained in:
commit
f34e3eac1a
130 changed files with 6202 additions and 2196 deletions
58
CHANGELOG.md
58
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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
122
app/Http/Controllers/AdminShadowFilterController.php
Normal file
122
app/Http/Controllers/AdminShadowFilterController.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AdminShadowFilter;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminShadowFilterService;
|
||||
|
||||
class AdminShadowFilterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
16
app/Http/Controllers/HealthCheckController.php
Normal file
16
app/Http/Controllers/HealthCheckController.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HealthCheckController extends Controller
|
||||
{
|
||||
public function get(Request $request)
|
||||
{
|
||||
return response('OK')->withHeaders([
|
||||
'Content-Type' => 'text/plain',
|
||||
'Cache-Control' => 'max-age=0, must-revalidate, no-cache, no-store'
|
||||
]);
|
||||
}
|
||||
}
|
64
app/Http/Controllers/ProfileAliasController.php
Normal file
64
app/Http/Controllers/ProfileAliasController.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\WebfingerUrl;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Services\WebfingerService;
|
||||
|
||||
class ProfileAliasController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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!');
|
||||
}
|
||||
}
|
|
@ -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 &&
|
||||
|
|
718
app/Http/Controllers/RemoteAuthController.php
Normal file
718
app/Http/Controllers/RemoteAuthController.php
Normal file
|
@ -0,0 +1,718 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Account\RemoteAuthService;
|
||||
use App\Models\RemoteAuth;
|
||||
use App\Profile;
|
||||
use App\Instance;
|
||||
use App\User;
|
||||
use Purify;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class RemoteAuthController extends Controller
|
||||
{
|
||||
public function start(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.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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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!');
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
30
app/Http/Resources/AdminProfile.php
Normal file
30
app/Http/Resources/AdminProfile.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class AdminProfile extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
20
app/Http/Resources/StoryView.php
Normal file
20
app/Http/Resources/StoryView.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class StoryView extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request)
|
||||
{
|
||||
return AccountService::get($this->profile_id, true);
|
||||
}
|
||||
}
|
139
app/Jobs/AdminPipeline/AdminProfileActionPipeline.php
Normal file
139
app/Jobs/AdminPipeline/AdminProfileActionPipeline.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\AdminPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Avatar;
|
||||
use App\Follower;
|
||||
use App\Instance;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use Purify;
|
||||
use App\Services\ActivityPubFetchService;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\StatusService;
|
||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||
|
||||
class AdminProfileActionPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $action;
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profile, $action)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<int, object>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Storage;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
|
||||
class FollowServiceWarmCacheLargeIngestPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $profileId;
|
||||
public $followType;
|
||||
public $tries = 5;
|
||||
public $timeout = 5000;
|
||||
public $failOnTimeout = false;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profileId, $followType = 'following')
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
142
app/Jobs/StatusPipeline/RemoteStatusDelete.php
Normal file
142
app/Jobs/StatusPipeline/RemoteStatusDelete.php
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use DB, Cache, Storage;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
Bookmark,
|
||||
CollectionItem,
|
||||
DirectMessage,
|
||||
Like,
|
||||
Media,
|
||||
MediaTag,
|
||||
Mention,
|
||||
Notification,
|
||||
Report,
|
||||
Status,
|
||||
StatusArchived,
|
||||
StatusHashtag,
|
||||
StatusView
|
||||
};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteNote;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\StatusService;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
|
||||
class RemoteStatusDelete implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $timeout = 90;
|
||||
public $tries = 2;
|
||||
public $maxExceptions = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -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') {
|
||||
|
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
27
app/Models/AdminShadowFilter.php
Normal file
27
app/Models/AdminShadowFilter.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class AdminShadowFilter extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime'
|
||||
];
|
||||
|
||||
public function account()
|
||||
{
|
||||
if($this->item_type === 'App\Profile') {
|
||||
return AccountService::get($this->item_id, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
17
app/Models/ProfileAlias.php
Normal file
17
app/Models/ProfileAlias.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class ProfileAlias extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
}
|
19
app/Models/RemoteAuth.php
Normal file
19
app/Models/RemoteAuth.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RemoteAuth extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'verify_credentials' => 'array',
|
||||
'last_successful_login_at' => 'datetime',
|
||||
'last_verify_credentials_at' => 'datetime'
|
||||
];
|
||||
}
|
13
app/Models/RemoteAuthInstance.php
Normal file
13
app/Models/RemoteAuthInstance.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RemoteAuthInstance extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
|
@ -7,6 +7,7 @@ use App\Util\Lexer\PrettyNumber;
|
|||
use App\HasSnowflakePrimary;
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
use App\Services\FollowerService;
|
||||
use App\Models\ProfileAlias;
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
|
@ -369,9 +370,13 @@ class Profile extends Model
|
|||
return $this->hasMany(Story::class);
|
||||
}
|
||||
|
||||
|
||||
public function reported()
|
||||
{
|
||||
return $this->hasMany(Report::class, 'object_id');
|
||||
}
|
||||
|
||||
public function aliases()
|
||||
{
|
||||
return $this->hasMany(ProfileAlias::class);
|
||||
}
|
||||
}
|
||||
|
|
191
app/Services/Account/RemoteAuthService.php
Normal file
191
app/Services/Account/RemoteAuthService.php
Normal file
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Account;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Models\RemoteAuthInstance;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
class RemoteAuthService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:remoteauth:';
|
||||
|
||||
public static function getConfig()
|
||||
{
|
||||
return json_encode([
|
||||
'default_only' => 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;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
51
app/Services/AdminShadowFilterService.php
Normal file
51
app/Services/AdminShadowFilterService.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AdminShadowFilter;
|
||||
use Cache;
|
||||
|
||||
class AdminShadowFilterService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:asfs:';
|
||||
|
||||
public static function queryFilter($name = 'hide_from_public_feeds')
|
||||
{
|
||||
return AdminShadowFilter::whereItemType('App\Profile')
|
||||
->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);
|
||||
}
|
||||
}
|
||||
}
|
28
app/Services/DomainService.php
Normal file
28
app/Services/DomainService.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class DomainService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:domains:';
|
||||
|
||||
public static function hasValidDns($domain)
|
||||
{
|
||||
if(!$domain || !strlen($domain) || strpos($domain, '.') == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config('security.url.trusted_domains')) {
|
||||
if(in_array($domain, explode(',', config('security.url.trusted_domains')))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Cache::remember(self::CACHE_KEY . 'valid-dns:' . $domain, 14400, function() use($domain) {
|
||||
return count(dns_get_record($domain, DNS_A | DNS_AAAA)) > 0;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 [
|
||||
|
|
66
app/Services/ResilientMediaStorageService.php
Normal file
66
app/Services/ResilientMediaStorageService.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Storage;
|
||||
use Illuminate\Http\File;
|
||||
use Exception;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Aws\S3\Exception\S3Exception;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class ResilientMediaStorageService
|
||||
{
|
||||
static $attempts = 0;
|
||||
|
||||
public static function store($storagePath, $path, $name)
|
||||
{
|
||||
return (bool) config_cache('pixelfed.cloud_storage') && (bool) config('media.storage.remote.resilient_mode') ?
|
||||
self::handleResilientStore($storagePath, $path, $name) :
|
||||
self::handleStore($storagePath, $path, $name);
|
||||
}
|
||||
|
||||
public static function handleStore($storagePath, $path, $name)
|
||||
{
|
||||
return retry(3, function() use($storagePath, $path, $name) {
|
||||
$baseDisk = (bool) config_cache('pixelfed.cloud_storage') ? config('filesystems.cloud') : 'local';
|
||||
$disk = Storage::disk($baseDisk);
|
||||
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
|
||||
return $disk->url($file);
|
||||
}, random_int(100, 500));
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
|
@ -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' => []
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)),
|
||||
|
|
28
app/User.php
28
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
75
composer.lock
generated
75
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
];
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'delete_local_after_cloud' => 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),
|
||||
],
|
||||
]
|
||||
];
|
||||
|
|
|
@ -23,7 +23,7 @@ return [
|
|||
| This value is the version of your Pixelfed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.11.8',
|
||||
'version' => '0.11.9',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
57
config/remote-auth.php
Normal file
57
config/remote-auth.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'mastodon' => [
|
||||
'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)
|
||||
]
|
||||
],
|
||||
];
|
9
config/security.php
Normal file
9
config/security.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'url' => [
|
||||
'verify_dns' => env('PF_SECURITY_URL_VERIFY_DNS', false),
|
||||
|
||||
'trusted_domains' => env('PF_SECURITY_URL_TRUSTED_DOMAINS', 'pixelfed.social,pixelfed.art,mastodon.social'),
|
||||
]
|
||||
];
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('groups', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupMembersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_members', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupPostsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_posts', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGroupInvitationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('group_invitations', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('remote_auths', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('remote_auth_instances', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('followers', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('profile_aliases', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('profiles', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('profiles', function (Blueprint $table) {
|
||||
$table->boolean('indexable')->default(false)->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('profiles', function (Blueprint $table) {
|
||||
$table->dropColumn('indexable');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_shadow_filters', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
BIN
public/css/spa.css
vendored
BIN
public/css/spa.css
vendored
Binary file not shown.
BIN
public/js/daci.chunk.914d307d69fcfcd4.js
vendored
BIN
public/js/daci.chunk.914d307d69fcfcd4.js
vendored
Binary file not shown.
BIN
public/js/daci.chunk.bfa9e4f459fec835.js
vendored
Normal file
BIN
public/js/daci.chunk.bfa9e4f459fec835.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~findfriends.chunk.6bd4ddbabd979778.js
vendored
Normal file
BIN
public/js/discover~findfriends.chunk.6bd4ddbabd979778.js
vendored
Normal file
Binary file not shown.
BIN
public/js/discover~memories.chunk.400f9f019bdb9fdf.js
vendored
Normal file
BIN
public/js/discover~memories.chunk.400f9f019bdb9fdf.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~myhashtags.chunk.ee5af357937cad2f.js
vendored
Normal file
BIN
public/js/discover~myhashtags.chunk.ee5af357937cad2f.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~serverfeed.chunk.fbe31eedcdafc87e.js
vendored
Normal file
BIN
public/js/discover~serverfeed.chunk.fbe31eedcdafc87e.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/discover~settings.chunk.909aa0316f43235e.js
vendored
Normal file
BIN
public/js/discover~settings.chunk.909aa0316f43235e.js
vendored
Normal file
Binary file not shown.
BIN
public/js/home.chunk.2d93b527d492e6de.js
vendored
BIN
public/js/home.chunk.2d93b527d492e6de.js
vendored
Binary file not shown.
BIN
public/js/home.chunk.bd623a430a5584c2.js
vendored
Normal file
BIN
public/js/home.chunk.bd623a430a5584c2.js
vendored
Normal file
Binary file not shown.
BIN
public/js/landing.js
vendored
BIN
public/js/landing.js
vendored
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/post.chunk.729ca668f46545cb.js
vendored
Normal file
BIN
public/js/post.chunk.729ca668f46545cb.js
vendored
Normal file
Binary file not shown.
BIN
public/js/post.chunk.cd535334efc77c34.js
vendored
BIN
public/js/post.chunk.cd535334efc77c34.js
vendored
Binary file not shown.
BIN
public/js/profile.chunk.029572d9018fc65f.js
vendored
Normal file
BIN
public/js/profile.chunk.029572d9018fc65f.js
vendored
Normal file
Binary file not shown.
BIN
public/js/profile.chunk.4049e1eecea398ee.js
vendored
BIN
public/js/profile.chunk.4049e1eecea398ee.js
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue