now profile edits are federalized

This commit is contained in:
Ghostie 2024-12-29 14:10:32 -05:00
parent 0d14d08246
commit 27079892f2
10 changed files with 189 additions and 21 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions;
use GuzzleHttp\Client;
use App\Types\TypeActor;
use Illuminate\Support\Facades\Log;
class ActionsUser
{
public static function update_profile ()
{
if (!auth ()->check ())
return ["error" => "You must be logged in to update your profile."];
$user = auth ()->user ();
try {
$client = new Client ();
$response = $client->post ($user->actor->outbox, [
"json" => [
"type" => "UpdateProfile"
]
]);
} catch (\Exception $e)
{
Log::error ("Error updating profile: " . $e->getMessage ());
return ["error" => "Error updating profile"];
}
return ["success" => "Profile updated"];
}
}

View File

@ -39,8 +39,7 @@ class APActorController extends Controller
public function following (User $user)
{
$actor_id = '"' . str_replace ("/", "\/", $user->actor->actor_id) . '"';
$following = Activity::where ("type", "Follow")->where ("actor", $actor_id);
$following = Activity::where ("type", "Follow")->where ("actor", $user->actor->actor_id);
$ordered_collection = new TypeOrderedCollection ();
$ordered_collection->collection = $following->get ()->pluck ("object")->toArray ();
$ordered_collection->url = route ("ap.following", $user->name);

View File

@ -5,8 +5,10 @@ namespace App\Http\Controllers\AP;
use App\Models\User;
use App\Models\Actor;
use App\Models\Activity;
use App\Models\Instance;
use App\Types\TypeActivity;
use App\Types\TypeActor;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@ -16,8 +18,13 @@ class APOutboxController extends Controller
{
public function outbox (User $user, Request $request)
{
// TODO: check we are logged in and we are the logged in user
switch ($request->get ("type"))
{
case "UpdateProfile":
return $this->handle_update_profile ($user);
break;
case "Follow":
return $this->handle_follow ($user, $request->get ("object"));
break;
@ -33,12 +40,39 @@ class APOutboxController extends Controller
}
}
public function handle_update_profile (User $user)
{
$actor = $user->actor ()->first ();
$actor_response = TypeActor::build_response ($actor);
$update_activity = TypeActivity::craft_update ($actor, $actor_response);
$instances = Instance::all ();
foreach ($instances as $instance)
{
$response = TypeActivity::post_activity ($update_activity, $actor, $instance->inbox);
if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300)
continue;
}
return response ()->json ("success", 200);
}
public function handle_follow (User $user, string $object)
{
$object_actor = Actor::where ("actor_id", $object)->first ();
if (!$object_actor)
return response ()->json ([ "error" => "object not found" ], 404);
if ($user->actor ()->first ()->actor_id == $object_actor->actor_id)
return response ()->json ([ "error" => "cannot follow self" ], 400);
// check we are not following already
$following_activity = Activity::where ("actor", $user->actor ()->first ()->actor_id)
->where ("object", '"' . str_replace ("/", "\/", $object_actor->actor_id) . '"')
->where ("type", "Follow")
->first ();
if ($following_activity)
return response ()->json ([ "error" => "already following" ], 400);
$follow_activity = TypeActivity::craft_follow ($user->actor ()->first (), $object_actor);
$response = TypeActivity::post_activity ($follow_activity, $user->actor ()->first (), $object_actor);

View File

@ -11,6 +11,8 @@ use Intervention\Image\Drivers\Gd\Driver;
use App\Models\User;
use App\Models\Actor;
use App\Actions\ActionsUser;
class ProfileController extends Controller
{
public function show ($user_name)
@ -96,6 +98,10 @@ class ProfileController extends Controller
Storage::disk ("public")->delete (str_replace ("/storage/", "", $old_avatar));
}
$response = ActionsUser::update_profile ();
if (isset ($response["error"]))
return back ()->with ("error", "Error updating profile: " . $response["error"]);
return back ()->with ("success", "Profile updated successfully!");
}
}

View File

@ -39,6 +39,14 @@ class Actor extends Model
"private_key"
];
protected $hidden = [
"id",
"user_id",
"created_at",
"updated_at",
"private_key"
];
public function user ()
{
return $this->belongsTo (User::class);

12
app/Models/Instance.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Instance extends Model
{
protected $fillable = [
"inbox"
];
}

View File

@ -69,7 +69,19 @@ class TypeActivity {
return $follow_activity;
}
public static function craft_signed_headers ($activity, Actor $source, Actor $target)
public static function craft_update (Actor $actor, $fields)
{
$update_activity = new Activity ();
$update_activity->activity_id = env ("APP_URL") . "/activity/" . uniqid ();
$update_activity->type = "Update";
$update_activity->actor = $actor->actor_id;
$update_activity->object = $fields;
$update_activity->save ();
return $update_activity;
}
public static function craft_signed_headers ($activity, Actor $source, $target)
{
if (!$source->user)
{
@ -87,7 +99,13 @@ class TypeActivity {
$hash = hash ("sha256", $activity, true);
$digest = base64_encode ($hash);
$url = parse_url ($target->inbox);
$url = null;
if ($target instanceof Actor)
$url = parse_url ($target->inbox);
else
$url = parse_url ($target);
$string_to_sign = "(request-target): post ". $url["path"] . "\nhost: " . $url["host"] . "\ndate: " . $date . "\ndigest: SHA-256=" . $digest;
openssl_sign ($string_to_sign, $signature, $signer, OPENSSL_ALGO_SHA256);
@ -105,7 +123,7 @@ class TypeActivity {
];
}
public static function post_activity (Activity $activity, Actor $source, Actor $target)
public static function post_activity (Activity $activity, Actor $source, $target)
{
$crafted_activity = TypeActivity::craft_response ($activity);
$activity_json = json_encode ($crafted_activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION);
@ -114,8 +132,19 @@ class TypeActivity {
$headers = TypeActivity::craft_signed_headers ($activity_json, $source, $target);
try {
$target_inbox = null;
if ($target instanceof Actor)
{
$target_inbox = $target->inbox;
}
else
{
$target_inbox = $target;
}
$client = new Client ();
$response = $client->post ($target->inbox, [
$response = $client->post ($target_inbox, [
"headers" => $headers,
"body" => $activity_json,
"debug" => true

View File

@ -4,6 +4,7 @@ namespace App\Types;
use App\Models\User;
use App\Models\Actor;
use App\Models\Instance;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
@ -137,6 +138,14 @@ class TypeActor {
$actor->save ();
$instances = Instance::where ("inbox", $actor->sharedInbox);
if (!$instances->first ())
{
$instance = new Instance ();
$instance->inbox = $actor->sharedInbox;
$instance->save ();
}
return $actor;
}

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::create('instances', function (Blueprint $table) {
$table->id();
$table->string ("inbox")->unique ();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('instances');
}
};

View File

@ -49,21 +49,29 @@
@auth
<div class="inner">
<div class="f-row">
<div class="f-col">
@if (auth ()->user ()->actor->friends_with ($actor))
<form action="{{ route ('user.unfriend') }}" onclick="this.submit ()" method="post">
@csrf
<input type="hidden" name="object" value="{{ $actor->actor_id }}">
<img loading="lazy" src="/resources/icons/delete.png" alt=""> Remove Friend
</form>
@else
<form action="{{ route ('user.friend') }}" onclick="this.submit ()" method="post">
@csrf
<input type="hidden" name="object" value="{{ $actor->actor_id }}">
<img loading="lazy" src="/resources/icons/add.png" alt=""> Add to Friends
</form>
@endif
</div>
@if (!auth ()->user ()->is ($user))
<div class="f-col">
@if (auth ()->user ()->actor->friends_with ($actor))
<form action="{{ route ('user.unfriend') }}" onclick="this.submit ()" method="post" style="cursor: pointer">
@csrf
<input type="hidden" name="object" value="{{ $actor->actor_id }}">
<img loading="lazy" src="/resources/icons/delete.png" alt=""> Remove Friend
</form>
@else
<form action="{{ route ('user.friend') }}" onclick="this.submit ()" method="post" style="cursor: pointer">
@csrf
<input type="hidden" name="object" value="{{ $actor->actor_id }}">
<img loading="lazy" src="/resources/icons/add.png" alt=""> Add to Friends
</form>
@endif
</div>
@else
<div class="f-col">
<a href="{{ route ('users.edit') }}">
<img loading="lazy" src="/resources/icons/asterisk_yellow.png" alt=""> Edit Profile
</a>
</div>
@endif
<div class="f-col">
<a href="#">