Merge pull request #4578 from pixelfed/staging

Add Account Migrations
This commit is contained in:
daniel 2023-08-08 00:03:18 -06:00 committed by GitHub
commit b6c3ac4b13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 352 additions and 5 deletions

View file

@ -1,12 +1,15 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.8...dev)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.9...dev)
## [v0.11.9 (2023-08-06)](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))
@ -50,6 +53,11 @@
- 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))
- ([](https://github.com/pixelfed/pixelfed/commit/))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)

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

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

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

@ -4,17 +4,26 @@ 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',
[
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'alsoKnownAs' => [
'@id' => 'as:alsoKnownAs',
'@type' => '@id'
],
'movedTo' => [
'@id' => 'as:movedTo',
'@type' => '@id'
]
],
],
'id' => $profile->permalink(),
@ -42,5 +51,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

@ -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,121 @@
@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">Manage Aliases</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 col-md-7">
<p class="lead">If you want to move from another account to this one, you can create an alias here first.</p>
<p>This alias is needed before you can move your followers from the old account to this one. Don't worry, making this change is safe and can be undone. The process of moving the account starts from the old one.</p>
<p class="mb-0">Your followers will be migrated to your new account, and in some instances your posts too! For more information on Aliases and Account Migration, visit the <a href="/site/kb/your-profile">Help Center</a>.</p>
</div>
<div class="col-12 col-md-5">
<div class="d-flex h-100 justify-content-center align-items-center flex-column">
<div class="border rounded-pill px-4 py-2">
<p class="small mb-n1 text-lighter font-weight-bold">Old Account</p>
<p class="lead mb-0">oldUsername@example.org</p>
</div>
<div class="border rounded-pill px-4 py-2 mt-3">
<p class="small mb-n1 text-lighter font-weight-bold">Old Account</p>
<p class="lead mb-0">oldUsername2@example.net</p>
</div>
<hr>
<p class="mb-0 small">We support migration to and from Pixelfed, Mastodon and most other platforms that use the Mastodon Account Migration <a href="https://docs.joinmastodon.org/spec/activitypub/#Move">extension</a>.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<hr>
</div>
<div class="col-12 col-md-7 p-3 p-md-5">
<form method="post">
@csrf
<div class="form-group">
<label class="font-weight-bold mb-0">Old Account</label>
<p class="small text-muted">Enter the username@domain of your old account</p>
<input type="email" class="form-control" name="acct" placeholder="username@domain.tld"/>
</div>
<button class="btn btn-primary btn-block font-weight-bold btn-lg">Create Alias</button>
</form>
</div>
<div class="col-12 col-md-5 p-3 p-md-5 bg-white">
<p class="text-center font-weight-bold">Aliases</p>
<div class="list-group">
@if(count($aliases))
@foreach($aliases as $alias)
<div class="list-group-item d-flex justify-content-between">
<div class="mb-0 font-weight-bold small text-break">
{{ $alias->acct }}
</div>
<div class="ml-2 mb-0 font-weight-bold small">
<form action="/settings/account/aliases/manage/delete" method="post">
@csrf
<input type="hidden" name="id" value="{{ $alias->id }}">
<input type="hidden" name="acct" value="{{ $alias->acct }}">
<button class="btn btn-link btn-sm p-0">
<i class="far fa-trash-alt text-danger"></i>
</button>
</form>
</div>
</div>
@endforeach
@else
<div class="border rounded p-5">
<p class="text-center mb-0">No aliases found!</p>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -87,6 +87,14 @@
<p class="help-text text-muted small">Select up to 4 pronouns that will appear on your profile.</p>
</div>
</div>
<div class="form-group row">
<label for="aliases" class="col-sm-3 col-form-label font-weight-bold">Account Aliases</label>
<div class="col-sm-9" id="aliases">
<a class="font-weight-bold" href="/settings/account/aliases/manage">Manage account alias</a>
<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>
@if(config_cache('pixelfed.enforce_account_limit'))
<div class="pt-3">
<p class="font-weight-bold text-muted text-center">Storage Usage</p>

View file

@ -141,6 +141,45 @@
</div>
</p>
<hr>
<p class="h5 text-muted " id="migration">Migration</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#migrate-collapse1" role="button" aria-expanded="false" aria-controls="migrate-collapse1">
<i class="fas fa-chevron-down mr-2"></i>
How can I migrate my account?
</a>
<div class="collapse" id="migrate-collapse1">
<div>
To migrate your account successfully, your old account must be on a Pixelfed or Mastodon server, or one that supports the Mastodon Account Migration <a href="https://docs.joinmastodon.org/spec/activitypub/#Move">extension</a>.
<hr>
<p>Navigate to the <a href="/settings/account/aliases/manage">Account Aliases</a> page in the Settings to begin.</p>
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#migrate-collapse2" role="button" aria-expanded="false" aria-controls="migrate-collapse2">
<i class="fas fa-chevron-down mr-2"></i>
How long does the migration take?
</a>
<div class="collapse" id="migrate-collapse2">
<div>
It can take a few hours to process post migration imports, please contact admins if it takes longer than 24 hours.
</div>
</div>
</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#migrate-collapse3" role="button" aria-expanded="false" aria-controls="migrate-collapse3">
<i class="fas fa-chevron-down mr-2"></i>
Why are my posts not migrated?
</a>
<div class="collapse" id="migrate-collapse3">
<div>
Post migrations are officially supported on Pixelfed servers running v0.11.9+ and higher, and when enabled by server admins.
<hr>
It can take a few hours to process post migration imports, please contact admins if it takes longer than 24 hours.
</div>
</div>
</p>
<hr>
<p class="h5 text-muted " id="delete-your-account">Delete Your Account</p>
<p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#del-collapse1" role="button" aria-expanded="false" aria-controls="del-collapse1">

View file

@ -549,7 +549,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('invites', 'UserInviteController@show')->name('settings.invites');
// Route::get('sponsor', 'SettingsController@sponsor')->name('settings.sponsor');
// Route::post('sponsor', 'SettingsController@sponsorStore');
Route::prefix('import')->group(function() {
Route::group(['prefix' => 'import', 'middleware' => 'dangerzone'], function() {
Route::get('/', 'SettingsController@dataImport')->name('settings.import');
Route::prefix('instagram')->group(function() {
Route::get('/', 'ImportController@instagram')->name('settings.import.ig');
@ -564,6 +564,12 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('timeline', 'SettingsController@updateTimelineSettings');
Route::get('media', 'SettingsController@mediaSettings')->name('settings.media');
Route::post('media', 'SettingsController@updateMediaSettings');
Route::group(['prefix' => 'account/aliases', 'middleware' => 'dangerzone'], function() {
Route::get('manage', 'ProfileAliasController@index');
Route::post('manage', 'ProfileAliasController@store');
Route::post('manage/delete', 'ProfileAliasController@delete');
});
});
Route::group(['prefix' => 'site'], function () {