mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-21 14:01:27 +00:00
Add Profile Migrations
This commit is contained in:
parent
d5a6d9cc8d
commit
f8145a78cf
16 changed files with 703 additions and 132 deletions
|
@ -2,11 +2,13 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\WebfingerUrl;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileAliasController extends Controller
|
||||
{
|
||||
|
@ -18,31 +20,47 @@ class ProfileAliasController extends Controller
|
|||
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' => 'required',
|
||||
]);
|
||||
|
||||
$acct = $request->input('acct');
|
||||
|
||||
if($request->user()->profile->aliases->count() >= 3) {
|
||||
$nn = Nickname::normalizeProfileUrl($acct);
|
||||
if (! $nn) {
|
||||
return back()->with('error', 'Invalid account alias.');
|
||||
}
|
||||
|
||||
if ($nn['domain'] === config('pixelfed.domain.app')) {
|
||||
if (strtolower($nn['username']) == ($request->user()->username)) {
|
||||
return back()->with('error', 'You cannot add an alias to your own account.');
|
||||
}
|
||||
}
|
||||
|
||||
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'])) {
|
||||
$webfingerUrl = WebfingerService::rawGet($acct);
|
||||
|
||||
if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) {
|
||||
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->uri = $webfingerUrl;
|
||||
$alias->save();
|
||||
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
|
||||
|
||||
return back()->with('status', 'Successfully added alias!');
|
||||
}
|
||||
|
||||
|
@ -50,14 +68,25 @@ class ProfileAliasController extends Controller
|
|||
{
|
||||
$this->validate($request, [
|
||||
'acct' => 'required',
|
||||
'id' => 'required|exists:profile_aliases'
|
||||
'id' => 'required|exists:profile_aliases',
|
||||
]);
|
||||
|
||||
$alias = ProfileAlias::where('profile_id', $request->user()->profile_id)
|
||||
->where('acct', $request->input('acct'))
|
||||
$pid = $request->user()->profile_id;
|
||||
$acct = $request->input('acct');
|
||||
$alias = ProfileAlias::where('profile_id', $pid)
|
||||
->where('acct', $acct)
|
||||
->findOrFail($request->input('id'));
|
||||
$migration = ProfileMigration::whereProfileId($pid)
|
||||
->whereAcct($acct)
|
||||
->first();
|
||||
if ($migration) {
|
||||
$request->user()->profile->update([
|
||||
'moved_to_profile_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$alias->delete();
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$pid);
|
||||
AccountService::del($pid);
|
||||
|
||||
return back()->with('status', 'Successfully deleted alias!');
|
||||
}
|
||||
|
|
62
app/Http/Controllers/ProfileMigrationController.php
Normal file
62
app/Http/Controllers/ProfileMigrationController.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileMigrationStoreRequest;
|
||||
use App\Jobs\ProfilePipeline\ProfileMigrationMoveFollowersPipeline;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileMigrationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id)
|
||||
->where('created_at', '>', now()->subDays(30))
|
||||
->exists();
|
||||
|
||||
return view('settings.migration.index', compact('hasExistingMigration'));
|
||||
}
|
||||
|
||||
public function store(ProfileMigrationStoreRequest $request)
|
||||
{
|
||||
$acct = WebfingerService::rawGet($request->safe()->acct);
|
||||
if (! $acct) {
|
||||
return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']);
|
||||
}
|
||||
$newAccount = Helpers::profileFetch($acct);
|
||||
if (! $newAccount) {
|
||||
return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']);
|
||||
}
|
||||
$user = $request->user();
|
||||
ProfileAlias::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'uri' => $acct,
|
||||
]);
|
||||
ProfileMigration::create([
|
||||
'profile_id' => $request->user()->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'followers_count' => $request->user()->profile->followers_count,
|
||||
'target_profile_id' => $newAccount['id'],
|
||||
]);
|
||||
$user->profile->update([
|
||||
'moved_to_profile_id' => $newAccount->id,
|
||||
'indexable' => false,
|
||||
]);
|
||||
AccountService::del($user->profile_id);
|
||||
|
||||
ProfileMigrationMoveFollowersPipeline::dispatch($user->profile_id, $newAccount->id);
|
||||
|
||||
return redirect()->back()->with(['status' => 'Succesfully migrated account!']);
|
||||
}
|
||||
}
|
76
app/Http/Requests/ProfileMigrationStoreRequest.php
Normal file
76
app/Http/Requests/ProfileMigrationStoreRequest.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\FetchCacheService;
|
||||
use App\Services\WebfingerService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ProfileMigrationStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user() || $this->user()->status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'acct' => 'required|email',
|
||||
'password' => 'required|current_password',
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator) {
|
||||
$err = $this->validateNewAccount();
|
||||
if ($err !== 'noerr') {
|
||||
$validator->errors()->add(
|
||||
'acct',
|
||||
$err
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
protected function validateNewAccount()
|
||||
{
|
||||
if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) {
|
||||
return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.';
|
||||
}
|
||||
$acct = WebfingerService::rawGet($this->acct);
|
||||
if (! $acct) {
|
||||
return 'The new account you provided is not responding to our requests.';
|
||||
}
|
||||
$pr = FetchCacheService::getJson($acct);
|
||||
if (! $pr || ! isset($pr['alsoKnownAs'])) {
|
||||
return 'Invalid account lookup response.';
|
||||
}
|
||||
if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) {
|
||||
return 'The new account does not contain an alias to your current account.';
|
||||
}
|
||||
$curAcctUrl = $this->user()->profile->permalink();
|
||||
if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) {
|
||||
return 'The new account does not contain an alias to your current account.';
|
||||
}
|
||||
|
||||
return 'noerr';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\ProfilePipeline;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Follower;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class ProfileMigrationMoveFollowersPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $oldPid;
|
||||
public $newPid;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($oldPid, $newPid)
|
||||
{
|
||||
$this->oldPid = $oldPid;
|
||||
$this->newPid = $newPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$og = Profile::find($this->oldPid);
|
||||
$ne = Profile::find($this->newPid);
|
||||
if(!$og || !$ne || $og == $ne) {
|
||||
return;
|
||||
}
|
||||
$ne->followers_count = $og->followers_count;
|
||||
$ne->save();
|
||||
$og->followers_count = 0;
|
||||
$og->save();
|
||||
foreach (Follower::whereFollowingId($this->oldPid)->lazyById(200, 'id') as $follower) {
|
||||
try {
|
||||
$follower->following_id = $this->newPid;
|
||||
$follower->save();
|
||||
} catch (Exception $e) {
|
||||
$follower->delete();
|
||||
}
|
||||
}
|
||||
AccountService::del($this->oldPid);
|
||||
AccountService::del($this->newPid);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ class ProfileAlias extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
|
19
app/Models/ProfileMigration.php
Normal file
19
app/Models/ProfileMigration.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Profile;
|
||||
|
||||
class ProfileMigration extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id');
|
||||
}
|
||||
}
|
79
app/Services/FetchCacheService.php
Normal file
79
app/Services/FetchCacheService.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FetchCacheService
|
||||
{
|
||||
const CACHE_KEY = 'pf:fetch_cache_service:getjson:';
|
||||
|
||||
public static function getJson($url, $verifyCheck = true, $ttl = 3600, $allowRedirects = true)
|
||||
{
|
||||
$vc = $verifyCheck ? 'vc1:' : 'vc0:';
|
||||
$ar = $allowRedirects ? 'ar1:' : 'ar0';
|
||||
$key = self::CACHE_KEY.sha1($url).':'.$vc.$ar.$ttl;
|
||||
if (Cache::has($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($verifyCheck) {
|
||||
if (! Helpers::validateUrl($url)) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
|
||||
];
|
||||
|
||||
if ($allowRedirects) {
|
||||
$options = [
|
||||
'allow_redirects' => [
|
||||
'max' => 2,
|
||||
'strict' => true,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$options = [
|
||||
'allow_redirects' => false,
|
||||
];
|
||||
}
|
||||
try {
|
||||
$res = Http::withOptions($options)
|
||||
->retry(3, function (int $attempt, $exception) {
|
||||
return $attempt * 500;
|
||||
})
|
||||
->acceptJson()
|
||||
->withHeaders($headers)
|
||||
->timeout(40)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $res->ok()) {
|
||||
Cache::put($key, 1, $ttl);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $res->json();
|
||||
}
|
||||
}
|
|
@ -2,69 +2,95 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Util\Webfinger\WebfingerUrl;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class WebfingerService
|
||||
{
|
||||
public static function lookup($query, $mastodonMode = false)
|
||||
{
|
||||
return (new self)->run($query, $mastodonMode);
|
||||
}
|
||||
public static function rawGet($url)
|
||||
{
|
||||
$n = WebfingerUrl::get($url);
|
||||
if (! $n) {
|
||||
return false;
|
||||
}
|
||||
$webfinger = FetchCacheService::getJson($n);
|
||||
if (! $webfinger) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function run($query, $mastodonMode)
|
||||
{
|
||||
if($profile = Profile::whereUsername($query)->first()) {
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
$url = WebfingerUrl::generateWebfingerUrl($query);
|
||||
if(!Helpers::validateUrl($url)) {
|
||||
return [];
|
||||
}
|
||||
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return false;
|
||||
}
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function ($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
|
||||
try {
|
||||
$res = Http::retry(3, 100)
|
||||
->acceptJson()
|
||||
->withHeaders([
|
||||
'User-Agent' => '(Pixelfed/' . config('pixelfed.version') . '; +' . config('app.url') . ')'
|
||||
])
|
||||
->timeout(20)
|
||||
->get($url);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
return [];
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
if(!$res->successful()) {
|
||||
return [];
|
||||
}
|
||||
public static function lookup($query, $mastodonMode = false)
|
||||
{
|
||||
return (new self)->run($query, $mastodonMode);
|
||||
}
|
||||
|
||||
$webfinger = $res->json();
|
||||
if(!isset($webfinger['links']) || !is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return [];
|
||||
}
|
||||
protected function run($query, $mastodonMode)
|
||||
{
|
||||
if ($profile = Profile::whereUsername($query)->first()) {
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
$url = WebfingerUrl::generateWebfingerUrl($query);
|
||||
if (! Helpers::validateUrl($url)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json','application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
try {
|
||||
$res = Http::retry(3, 100)
|
||||
->acceptJson()
|
||||
->withHeaders([
|
||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
|
||||
])
|
||||
->timeout(20)
|
||||
->get($url);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$profile = Helpers::profileFetch($link);
|
||||
if(!$profile) {
|
||||
return;
|
||||
}
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
if (! $res->successful()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$webfinger = $res->json();
|
||||
if (! isset($webfinger['links']) || ! is_array($webfinger['links']) || empty($webfinger['links'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$link = collect($webfinger['links'])
|
||||
->filter(function ($link) {
|
||||
return $link &&
|
||||
isset($link['rel'], $link['type'], $link['href']) &&
|
||||
$link['rel'] === 'self' &&
|
||||
in_array($link['type'], ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']);
|
||||
})
|
||||
->pluck('href')
|
||||
->first();
|
||||
|
||||
$profile = Helpers::profileFetch($link);
|
||||
if (! $profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $mastodonMode ?
|
||||
AccountService::getMastodon($profile->id, true) :
|
||||
AccountService::get($profile->id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,80 +2,91 @@
|
|||
|
||||
namespace App\Transformer\Api;
|
||||
|
||||
use Auth;
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\PronounService;
|
||||
use App\User;
|
||||
use App\UserSetting;
|
||||
use Cache;
|
||||
use League\Fractal;
|
||||
use App\Services\PronounService;
|
||||
|
||||
class AccountTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
// 'relationship',
|
||||
];
|
||||
protected $defaultIncludes = [
|
||||
// 'relationship',
|
||||
];
|
||||
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
if(!$profile) {
|
||||
return [];
|
||||
}
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
if (! $profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$adminIds = Cache::remember('pf:admin-ids', 604800, function() {
|
||||
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
|
||||
});
|
||||
$adminIds = Cache::remember('pf:admin-ids', 604800, function () {
|
||||
return User::whereIsAdmin(true)->pluck('profile_id')->toArray();
|
||||
});
|
||||
|
||||
$local = $profile->private_key != null;
|
||||
$local = $profile->user_id && $profile->private_key != null;
|
||||
$hideFollowing = false;
|
||||
$hideFollowers = false;
|
||||
if($local) {
|
||||
$hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:' . $profile->id, 2592000, function() use($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if(!$settings) {
|
||||
return false;
|
||||
}
|
||||
return $settings->show_profile_following_count == false;
|
||||
});
|
||||
$hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:' . $profile->id, 2592000, function() use($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if(!$settings) {
|
||||
return false;
|
||||
}
|
||||
return $settings->show_profile_follower_count == false;
|
||||
});
|
||||
}
|
||||
$is_admin = !$local ? false : in_array($profile->id, $adminIds);
|
||||
$acct = $local ? $profile->username : substr($profile->username, 1);
|
||||
$username = $local ? $profile->username : explode('@', $acct)[0];
|
||||
return [
|
||||
'id' => (string) $profile->id,
|
||||
'username' => $username,
|
||||
'acct' => $acct,
|
||||
'display_name' => $profile->name,
|
||||
'discoverable' => true,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count,
|
||||
'following_count' => $hideFollowing ? 0 : (int) $profile->following_count,
|
||||
'statuses_count' => (int) $profile->status_count,
|
||||
'note' => $profile->bio ?? '',
|
||||
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'website' => $profile->website,
|
||||
'local' => (bool) $local,
|
||||
'is_admin' => (bool) $is_admin,
|
||||
'created_at' => $profile->created_at->toJSON(),
|
||||
'header_bg' => $profile->header_bg,
|
||||
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
|
||||
'pronouns' => PronounService::get($profile->id),
|
||||
'location' => $profile->location
|
||||
];
|
||||
}
|
||||
$local = $profile->private_key != null;
|
||||
$local = $profile->user_id && $profile->private_key != null;
|
||||
$hideFollowing = false;
|
||||
$hideFollowers = false;
|
||||
if ($local) {
|
||||
$hideFollowing = Cache::remember('pf:acct-trans:hideFollowing:'.$profile->id, 2592000, function () use ($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if (! $settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function includeRelationship(Profile $profile)
|
||||
{
|
||||
return $this->item($profile, new RelationshipTransformer());
|
||||
}
|
||||
return $settings->show_profile_following_count == false;
|
||||
});
|
||||
$hideFollowers = Cache::remember('pf:acct-trans:hideFollowers:'.$profile->id, 2592000, function () use ($profile) {
|
||||
$settings = UserSetting::whereUserId($profile->user_id)->first();
|
||||
if (! $settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $settings->show_profile_follower_count == false;
|
||||
});
|
||||
}
|
||||
$is_admin = ! $local ? false : in_array($profile->id, $adminIds);
|
||||
$acct = $local ? $profile->username : substr($profile->username, 1);
|
||||
$username = $local ? $profile->username : explode('@', $acct)[0];
|
||||
$res = [
|
||||
'id' => (string) $profile->id,
|
||||
'username' => $username,
|
||||
'acct' => $acct,
|
||||
'display_name' => $profile->name,
|
||||
'discoverable' => true,
|
||||
'locked' => (bool) $profile->is_private,
|
||||
'followers_count' => $hideFollowers ? 0 : (int) $profile->followers_count,
|
||||
'following_count' => $hideFollowing ? 0 : (int) $profile->following_count,
|
||||
'statuses_count' => (int) $profile->status_count,
|
||||
'note' => $profile->bio ?? '',
|
||||
'note_text' => $profile->bio ? strip_tags($profile->bio) : null,
|
||||
'url' => $profile->url(),
|
||||
'avatar' => $profile->avatarUrl(),
|
||||
'website' => $profile->website,
|
||||
'local' => (bool) $local,
|
||||
'is_admin' => (bool) $is_admin,
|
||||
'created_at' => $profile->created_at->toJSON(),
|
||||
'header_bg' => $profile->header_bg,
|
||||
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
|
||||
'pronouns' => PronounService::get($profile->id),
|
||||
'location' => $profile->location,
|
||||
];
|
||||
|
||||
if ($profile->moved_to_profile_id) {
|
||||
$mt = AccountService::getMastodon($profile->moved_to_profile_id, true);
|
||||
if ($mt) {
|
||||
$res['moved'] = $mt;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function includeRelationship(Profile $profile)
|
||||
{
|
||||
return $this->item($profile, new RelationshipTransformer());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,28 @@
|
|||
namespace App\Util\Webfinger;
|
||||
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Services\InstanceService;
|
||||
|
||||
class WebfingerUrl
|
||||
{
|
||||
public static function get($url)
|
||||
{
|
||||
$n = Nickname::normalizeProfileUrl($url);
|
||||
if(!$n || !isset($n['domain'], $n['username'])) {
|
||||
return false;
|
||||
}
|
||||
if(in_array($n['domain'], InstanceService::getBannedDomains())) {
|
||||
return false;
|
||||
}
|
||||
return 'https://' . $n['domain'] . '/.well-known/webfinger?resource=acct:' . $n['username'] . '@' . $n['domain'];
|
||||
}
|
||||
|
||||
public static function generateWebfingerUrl($url)
|
||||
{
|
||||
$url = Nickname::normalizeProfileUrl($url);
|
||||
$domain = $url['domain'];
|
||||
$username = $url['username'];
|
||||
$path = "https://{$domain}/.well-known/webfinger?resource=acct:{$username}@{$domain}";
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?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_migrations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('profile_id');
|
||||
$table->string('acct')->nullable();
|
||||
$table->unsignedBigInteger('followers_count')->default(0);
|
||||
$table->unsignedBigInteger('target_profile_id')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('profile_migrations');
|
||||
}
|
||||
};
|
|
@ -1,7 +1,40 @@
|
|||
<template>
|
||||
<div class="profile-timeline-component">
|
||||
<div v-if="isLoaded" class="container-fluid mt-3">
|
||||
<div class="row">
|
||||
<div v-if="profile && profile.hasOwnProperty('moved') && profile.moved.hasOwnProperty('id') && !showMoved">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card shadow-none border card-body mt-5 mb-3 ft-std" style="border-radius: 20px;">
|
||||
<p class="lead font-weight-bold text-center mb-0"><i class="far fa-exclamation-triangle mr-2"></i>This account has indicated their new account is:</p>
|
||||
</div>
|
||||
<div class="card shadow-none border" style="border-radius: 20px;">
|
||||
<div class="card-body ft-std">
|
||||
<div class="d-flex justify-content-between align-items-center" style="gap: 1rem;">
|
||||
<div class="d-flex align-items-center flex-shrink-1" style="gap: 1rem;">
|
||||
<img :src="profile.moved.avatar" width="50" height="50" class="rounded-circle">
|
||||
<p class="h3 font-weight-light mb-0 text-break">@{{ profile.moved.acct }}</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-grow-1 justify-content-end" style="min-width: 200px;">
|
||||
<router-link
|
||||
:to="`/i/web/profile/${profile.moved.id}`"
|
||||
class="btn btn-outline-primary rounded-pill font-weight-bold"
|
||||
>View New Account</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="lead text-center ft-std">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary btn-lg rounded-pill font-weight-bold px-5"
|
||||
@click.prevent="showMoved = true">Proceed</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="row">
|
||||
<div class="col-md-3 d-md-block px-md-3 px-xl-5">
|
||||
<profile-sidebar
|
||||
:profile="profile"
|
||||
|
@ -72,7 +105,8 @@
|
|||
curUser: undefined,
|
||||
tab: "index",
|
||||
profile: undefined,
|
||||
relationship: undefined
|
||||
relationship: undefined,
|
||||
showMoved: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -150,6 +150,19 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else-if="profile.hasOwnProperty('moved') && profile.moved.id" style="flex-grow: 1;">
|
||||
<div class="card shadow-none rounded-lg mb-3 bg-danger">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center ft-std text-white mb-2">
|
||||
<i class="far fa-exclamation-triangle mr-2 text-white"></i>
|
||||
Account has moved to:
|
||||
</div>
|
||||
<p class="mb-0 lead ft-std text-white text-break">
|
||||
<router-link :to="`/i/web/profile/${profile.moved.id}`" class="btn btn-outline-light btn-block rounded-pill font-weight-bold">@{{truncate(profile.moved.acct)}}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="profile.locked" style="flex-grow: 1;">
|
||||
<template v-if="!relationship.following && !relationship.requested">
|
||||
<button
|
||||
|
@ -375,6 +388,16 @@
|
|||
}
|
||||
},
|
||||
|
||||
truncate(str) {
|
||||
if(!str) {
|
||||
return;
|
||||
}
|
||||
if(str.length > 15) {
|
||||
return str.slice(0, 15) + '...';
|
||||
}
|
||||
return str;
|
||||
},
|
||||
|
||||
formatCount(val) {
|
||||
return App.util.format.count(val);
|
||||
},
|
||||
|
|
|
@ -95,6 +95,14 @@
|
|||
<p class="help-text text-muted small">To move from another account to this one, first you need to create an alias.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="aliases" class="col-sm-3 col-form-label font-weight-bold">Account Migrate</label>
|
||||
<div class="col-sm-9" id="aliases">
|
||||
<a class="font-weight-bold" href="/settings/account/migration/manage">Migrate to another account</a>
|
||||
<p class="help-text text-muted small">To redirect this account to a different one (where supported).</p>
|
||||
</div>
|
||||
</div>
|
||||
@if(config_cache('pixelfed.enforce_account_limit'))
|
||||
<div class="pt-3">
|
||||
<p class="font-weight-bold text-muted text-center">Storage Usage</p>
|
||||
|
|
99
resources/views/settings/migration/index.blade.php
Normal file
99
resources/views/settings/migration/index.blade.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
@if (session('status'))
|
||||
<div class="alert alert-primary px-3 h6 font-weight-bold text-center">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger px-3 h6 text-center">
|
||||
@foreach($errors->all() as $error)
|
||||
<p class="font-weight-bold mb-1">{{ $error }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger px-3 h6 text-center">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-none border mt-5">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12 p-3 p-md-5">
|
||||
<div class="title">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3 class="font-weight-bold">Account Migration</h3>
|
||||
|
||||
<a class="font-weight-bold" href="/settings/home">
|
||||
<i class="far fa-long-arrow-left"></i>
|
||||
Back to Settings
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p class="lead">If you want to move this account to another account, please read the following carefully.</p>
|
||||
<ul class="text-danger lead">
|
||||
<li class="font-weight-bold">Only followers will be transferred; no other information will be moved automatically.</li>
|
||||
<li>This process will transfer all followers from your existing account to your new account.</li>
|
||||
<li>A redirect notice will be added to your current account's profile, and it will be removed from search results.</li>
|
||||
<li>You must set up the new account to link back to your current account before proceeding.</li>
|
||||
<li>Once the transfer is initiated, there will be a waiting period during which you cannot initiate another transfer.</li>
|
||||
<li>After the transfer, your current account will be limited in functionality, but you will retain the ability to export data and possibly reactivate the account.</li>
|
||||
</ul>
|
||||
<p class="mb-0">For more information on Aliases and Account Migration, visit the <a href="/site/kb/your-profile">Help Center</a>.</p>
|
||||
<hr>
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold mb-0">New Account Handle</label>
|
||||
<p class="small text-muted">Enter the username@domain of the account you want to move to</p>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
name="acct"
|
||||
placeholder="username@domain.tld"
|
||||
role="presentation"
|
||||
autocomplete="new-user-email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold mb-0">Account Password</label>
|
||||
<p class="small text-muted">For security purposes please enter the password of the current account</p>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="password"
|
||||
role="presentation"
|
||||
placeholder="Your account password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block font-weight-bold btn-lg rounded-pill">Move Followers</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
|
@ -266,6 +266,11 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::post('manage', 'ProfileAliasController@store');
|
||||
Route::post('manage/delete', 'ProfileAliasController@delete');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'account/migration', 'middleware' => 'dangerzone'], function() {
|
||||
Route::get('manage', 'ProfileMigrationController@index');
|
||||
Route::post('manage', 'ProfileMigrationController@store');
|
||||
});
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'site'], function () {
|
||||
|
|
Loading…
Reference in a new issue