Merge branch 'pixelfed:dev' into main

This commit is contained in:
Felipe Mateus 2023-10-08 11:46:01 -03:00 committed by GitHub
commit f34e3eac1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
130 changed files with 6202 additions and 2196 deletions

View file

@ -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)

View file

@ -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);
}

View file

@ -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)

View 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');
}
}

View file

@ -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));

View file

@ -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];
}
}

View file

@ -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);

View file

@ -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);

View 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'
]);
}
}

View 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!');
}
}

View file

@ -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 &&

View 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();
}
}

View file

@ -39,9 +39,11 @@ trait PrivacySettings
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
'indexable',
'show_atom',
];
$profile->indexable = $request->input('indexable') == 'on';
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
$profile->save();
@ -70,6 +72,8 @@ trait PrivacySettings
} else {
$settings->{$field} = false;
}
} elseif ($field == 'indexable') {
} else {
if ($form == 'on') {
$settings->{$field} = true;

View file

@ -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 {
$uid = $request->user()->id;
$this->validate($request, [
'enable_reblogs' => 'sometimes',
'photo_reblogs_only' => 'sometimes'
]);
Redis::zrem('pf:tl:top', $pid);
}
if($replies) {
Redis::zadd('pf:tl:replies', $pid, $pid);
} else {
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!');
}

View file

@ -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()) {

View file

@ -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);
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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');
}
});

View file

@ -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);

View file

@ -53,44 +53,24 @@ class FollowPipeline implements ShouldQueue
return;
}
if($target->domain || !$target->private_key) {
return;
}
Cache::forget('profile:following:' . $actor->id);
Cache::forget('profile:following:' . $target->id);
FollowerService::add($actor->id, $target->id);
$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);
}
$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);
}
if($target->domain || !$target->private_key) {
return;
}
try {
$notification = new Notification();

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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;
}

View 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;
}
}

View file

@ -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)
) {
if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
PublicTimelineService::add($status->id);
}
}
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {

View file

@ -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();
$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
]);
}

View file

@ -78,6 +78,9 @@ class Media extends Model
public function mimeType()
{
if(!$this->mime) {
return;
}
return explode('/', $this->mime)[0];
}

View 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;
}
}

View 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
View 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'
];
}

View 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 = [];
}

View file

@ -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);
}
}

View 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;
}
}

View file

@ -11,11 +11,13 @@ use Illuminate\Http\Client\RequestException;
class ActivityPubFetchService
{
public static function get($url)
public static function get($url, $validateUrl = true)
{
if($validateUrl === true) {
if(!Helpers::validateUrl($url)) {
return 0;
}
}
$baseHeaders = [
'Accept' => 'application/activity+json, application/ld+json',

View 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);
}
}
}

View 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;
});
}
}

View file

@ -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);
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);

View file

@ -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);

View file

@ -24,10 +24,8 @@ 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);
}
}
return Redis::zadd(self::CACHE_SET_KEY . $profileId, $statusId, $statusId);
}

View file

@ -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) {
if(!isset($h['content-length'], $h['content-type'])) {
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'];
}
$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);
$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();

View file

@ -49,10 +49,8 @@ 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);
}
}
return Redis::zadd(self::CACHE_KEY, $val, $val);
}

View file

@ -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,7 +269,7 @@ 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());
@ -270,8 +280,9 @@ class NotificationService {
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) {

View file

@ -49,10 +49,8 @@ class PublicTimelineService {
public static function add($val)
{
if(self::count() > 400) {
if(config('database.redis.client') === 'phpredis') {
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) {
->pluck('id', 'profile_id');
foreach($ids as $k => $id) {
if(AdminShadowFilterService::canAddToPublicFeedByProfileId($k)) {
self::add($id);
}
}
return 1;
}

View file

@ -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 [

View 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];
}
}

View file

@ -179,8 +179,10 @@ class SearchApiV2Service
return $default;
}
if(Helpers::validateLocalUrl($query)) {
if(Str::contains($query, '/p/')) {
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();
}
@ -217,6 +219,14 @@ class SearchApiV2Service
}
}
if($sid = Status::whereUri($query)->first()) {
$s = StatusService::get($sid->id, false);
if(in_array($s['visibility'], ['public', 'unlisted'])) {
$default['statuses'][] = $s;
return $default;
}
}
try {
$res = ActivityPubFetchService::get($query);
$banned = InstanceService::getBannedDomains();
@ -238,11 +248,14 @@ class SearchApiV2Service
return $default;
}
$note = $mastodonMode ?
StatusService::getMastodon($obj['id']) :
StatusService::get($obj['id']);
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;
@ -256,8 +269,8 @@ class SearchApiV2Service
return $default;
}
$default['accounts'][] = $mastodonMode ?
AccountService::getMastodon($obj['id']) :
AccountService::get($obj['id']);
AccountService::getMastodon($obj['id'], true) :
AccountService::get($obj['id'], true);
return $default;
break;
@ -285,9 +298,9 @@ class SearchApiV2Service
protected function resolveLocalStatus()
{
$query = urldecode($this->query->input('q'));
$query = last(explode('/', $query));
$status = StatusService::getMastodon($query);
if(!$status) {
$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' => [],
@ -307,7 +320,7 @@ class SearchApiV2Service
protected function resolveLocalProfile()
{
$query = urldecode($this->query->input('q'));
$query = last(explode('/', $query));
$query = last(explode('/', parse_url($query, PHP_URL_PATH)));
$profile = Profile::whereNull('status')
->whereNull('domain')
->whereUsername($query)
@ -325,7 +338,32 @@ class SearchApiV2Service
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return [
'accounts' => $fractal->createData($resource)->toArray(),
'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' => []
];

View file

@ -22,9 +22,9 @@ class StatusService
return self::CACHE_KEY . $p . $id;
}
public static function get($id, $publicOnly = true)
public static function get($id, $publicOnly = true, $mastodonMode = false)
{
return Cache::remember(self::key($id, $publicOnly), now()->addDays(7), function() use($id, $publicOnly) {
$res = Cache::remember(self::key($id, $publicOnly), 21600, function() use($id, $publicOnly) {
if($publicOnly) {
$status = Status::whereScope('public')->find($id);
} else {
@ -36,13 +36,23 @@ class StatusService
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
return $fractal->createData($resource)->toArray();
$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);
$status = self::get($id, $publicOnly, true);
if(!$status) {
return null;
}
@ -151,8 +161,6 @@ class StatusService
}
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);

View file

@ -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'])) {
$entity = StatusService::get($this->id, false);
if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
return url(Storage::url('public/no-preview.png'));
}
return url(Storage::url($this->firstMedia()->thumbnail_path));
});
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)

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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)),

View file

@ -21,7 +21,8 @@ class User extends Authenticatable
protected $casts = [
'deleted_at' => 'datetime',
'email_verified_at' => 'datetime',
'2fa_setup_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'
];
/**

View file

@ -40,6 +40,7 @@ use App\Models\Poll;
use Illuminate\Contracts\Cache\LockTimeoutException;
use App\Jobs\ProfilePipeline\IncrementPostCount;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Services\DomainService;
use App\Services\UserFilterService;
class Helpers {
@ -107,7 +108,10 @@ class Helpers {
'string',
Rule::in($mimeTypes)
],
'*.name' => 'sometimes|nullable|string'
'*.name' => 'sometimes|nullable|string',
'*.blurhash' => 'sometimes|nullable|string|min:6|max:164',
'*.width' => 'sometimes|nullable|integer|min:1|max:5000',
'*.height' => 'sometimes|nullable|integer|min:1|max:5000',
])->passes();
return $valid;
@ -168,17 +172,24 @@ class Helpers {
$hash = hash('sha256', $url);
$key = "helpers:url:valid:sha256-{$hash}";
$ttl = now()->addMinutes(5);
$valid = Cache::remember($key, $ttl, function() use($url) {
$valid = Cache::remember($key, 900, function() use($url) {
$localhosts = [
'127.0.0.1', 'localhost', '::1'
];
if(mb_substr($url, 0, 8) !== 'https://') {
if(strtolower(mb_substr($url, 0, 8)) !== 'https://') {
return false;
}
if(substr_count($url, '://') !== 1) {
return false;
}
if(mb_substr($url, 0, 8) !== 'https://') {
$url = 'https://' . substr($url, 8);
}
$valid = filter_var($url, FILTER_VALIDATE_URL);
if(!$valid) {
@ -187,15 +198,12 @@ class Helpers {
$host = parse_url($valid, PHP_URL_HOST);
// if(count(dns_get_record($host, DNS_A | DNS_AAAA)) == 0) {
// return false;
// }
if(in_array($host, $localhosts)) {
return false;
}
if(config('costar.enabled') == true) {
if(
(config('costar.domain.block') != null && Str::contains($host, config('costar.domain.block')) == true) ||
(config('costar.actor.block') != null && in_array($url, config('costar.actor.block')) == true)
) {
if(config('security.url.verify_dns')) {
if(DomainService::hasValidDns($host) === false) {
return false;
}
}
@ -207,11 +215,6 @@ class Helpers {
}
}
if(in_array($host, $localhosts)) {
return false;
}
return $url;
});
@ -224,7 +227,7 @@ class Helpers {
if($url == true) {
$domain = config('pixelfed.domain.app');
$host = parse_url($url, PHP_URL_HOST);
$url = $domain === $host ? $url : false;
$url = strtolower($domain) === strtolower($host) ? $url : false;
return $url;
}
return false;
@ -276,7 +279,7 @@ class Helpers {
}
if(is_array($val)) {
return !empty($val) ? $val[0] : null;
return !empty($val) ? head($val) : null;
}
return null;
@ -466,12 +469,19 @@ class Helpers {
$scope = self::getScope($activity, $url);
$cw = self::getSensitive($activity, $url);
$pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
$isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false);
$commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false;
if(!$pid) {
return;
}
if($scope == 'public') {
if($isUnlisted == true) {
$scope = 'unlisted';
}
}
$status = Status::updateOrCreate(
[
'uri' => $url
@ -519,9 +529,11 @@ class Helpers {
->values()
->toArray();
if(!in_array($urlDomain, $filteredDomains)) {
if(!$isUnlisted) {
NetworkTimelineService::add($status->id);
}
}
}
IncrementPostCount::dispatch($pid)->onQueue('low');
@ -675,6 +687,8 @@ class Helpers {
$blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
$license = isset($media['license']) ? License::nameToId($media['license']) : null;
$caption = isset($media['name']) ? Purify::clean($media['name']) : null;
$width = isset($media['width']) ? $media['width'] : false;
$height = isset($media['height']) ? $media['height'] : false;
$media = new Media();
$media->blurhash = $blurhash;
@ -686,6 +700,12 @@ class Helpers {
$media->remote_url = $url;
$media->caption = $caption;
$media->order = $key + 1;
if($width) {
$media->width = $width;
}
if($height) {
$media->height = $height;
}
if($license) {
$media->license = $license;
}
@ -776,6 +796,7 @@ class Helpers {
'inbox_url' => $res['inbox'],
'outbox_url' => isset($res['outbox']) ? $res['outbox'] : null,
'public_key' => $res['publicKey']['publicKeyPem'],
'indexable' => isset($res['indexable']) && is_bool($res['indexable']) ? $res['indexable'] : false,
]
);

View file

@ -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;

View file

@ -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);

75
composer.lock generated
View file

@ -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",

View file

@ -25,7 +25,11 @@ return [
'enabled' => env('AP_LOGGER_ENABLED', false),
'driver' => 'log'
]
]
],
'ingest' => [
'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false),
],
],
'atom' => [
@ -52,6 +56,5 @@ return [
// max size in bytes, default is 2mb
'max_size' => env('CUSTOM_EMOJI_MAX_SIZE', 2000000),
]
],
];

View file

@ -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'),

View file

@ -18,7 +18,9 @@ return [
| Disabled by default.
|
*/
'cloud' => env('MEDIA_REMOTE_STORE_CLOUD', false)
'cloud' => env('MEDIA_REMOTE_STORE_CLOUD', false),
'resilient_mode' => env('ALT_PRI_ENABLED', false) || env('ALT_SEC_ENABLED', false),
],
]
];

View file

@ -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
View 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
View 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'),
]
];

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}
};

View file

@ -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');
}
};

View file

@ -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');
});
}
};

View file

@ -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');
}
};

View file

@ -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');
});
}
};

View file

@ -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');
});
}
};

View file

@ -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

Binary file not shown.

Binary file not shown.

BIN
public/js/daci.chunk.bfa9e4f459fec835.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/home.chunk.bd623a430a5584c2.js vendored Normal file

Binary file not shown.

BIN
public/js/landing.js vendored

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

BIN
public/js/post.chunk.729ca668f46545cb.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more