From 0d14d0824615ab5e978b0ace82529affd17f4516 Mon Sep 17 00:00:00 2001 From: Ghostie Date: Sat, 28 Dec 2024 20:07:56 -0500 Subject: [PATCH] now users can be followed through the profile page --- README.md | 4 +- app/Actions/ActionsFriends.php | 60 +++++++++++++++++ .../Controllers/AP/APOutboxController.php | 65 ++++++++++++++++++- app/Http/Controllers/HomeController.php | 23 ++++++- app/Http/Controllers/UserActionController.php | 28 ++++++++ app/Models/Actor.php | 13 ++++ app/Models/User.php | 12 ++-- app/Types/TypeActivity.php | 24 +++++++ app/Types/TypeActor.php | 2 +- resources/views/users/profile.blade.php | 18 ++++- resources/views/users/requests.blade.php | 5 +- routes/web.php | 6 ++ 12 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 app/Actions/ActionsFriends.php create mode 100644 app/Http/Controllers/UserActionController.php diff --git a/README.md b/README.md index e6c54c0..fb949be 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Notice that the styles were taken from [AnySpace](https://anyspace.3to.moe/about - [ ] Local posts should be federated - [ ] Remote posts should be fetched - [x] Follows + - [ ] I cannot follow myself + - [ ] Check when waiting for approval - [ ] Likes - [ ] Comments @@ -25,7 +27,7 @@ Notice that the styles were taken from [AnySpace](https://anyspace.3to.moe/about - [ ] Show when the user is online - [ ] Set mood - [ ] Set interests - - [ ] Update profile picture + - [x] Update profile picture - [ ] Mark account as private (in federation manual approval is needed) - [ ] Allow custom CSS - [ ] Profile audio diff --git a/app/Actions/ActionsFriends.php b/app/Actions/ActionsFriends.php new file mode 100644 index 0000000..8c9a31e --- /dev/null +++ b/app/Actions/ActionsFriends.php @@ -0,0 +1,60 @@ +check ()) + return ["error" => "You must be logged in to add friends."]; + + $target_actor = TypeActor::actor_exists_or_obtain ($target); + + try { + $client = new Client (); + $response = $client->post (auth ()->user ()->actor->outbox, [ + "json" => [ + "type" => "Follow", + "object" => $target + ] + ]); + } + catch (\Exception $e) + { + Log::error ("Error adding friend: " . $e->getMessage ()); + return ["error" => "Error adding friend"]; + } + + return ["success" => "Friend added"]; + } + + public static function remove_friend ($target) + { + if (!auth ()->check ()) + return ["error" => "You must be logged in to remove friends."]; + + $target_actor = TypeActor::actor_exists_or_obtain ($target); + + try { + $client = new Client (); + $response = $client->post (auth ()->user ()->actor->outbox, [ + "json" => [ + "type" => "Unfollow", + "object" => $target + ] + ]); + } + catch (\Exception $e) + { + Log::error ("Error removing friend: " . $e->getMessage ()); + return ["error" => "Error removing friend"]; + } + + return ["success" => "Friend removed"]; + } +} diff --git a/app/Http/Controllers/AP/APOutboxController.php b/app/Http/Controllers/AP/APOutboxController.php index 339f980..4396e06 100644 --- a/app/Http/Controllers/AP/APOutboxController.php +++ b/app/Http/Controllers/AP/APOutboxController.php @@ -4,6 +4,9 @@ namespace App\Http\Controllers\AP; use App\Models\User; use App\Models\Actor; +use App\Models\Activity; + +use App\Types\TypeActivity; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -11,9 +14,65 @@ use App\Http\Controllers\Controller; class APOutboxController extends Controller { - public function outbox (User $user) + public function outbox (User $user, Request $request) { - Log::info ("APOutboxController@index"); - Log::info (json_encode (request ()->all ())); + switch ($request->get ("type")) + { + case "Follow": + return $this->handle_follow ($user, $request->get ("object")); + break; + + case "Unfollow": + return $this->handle_unfollow ($user, $request->get ("object")); + break; + + default: + Log::info ("APOutboxController@index"); + Log::info (json_encode (request ()->all ())); + break; + } + } + + 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); + + $follow_activity = TypeActivity::craft_follow ($user->actor ()->first (), $object_actor); + $response = TypeActivity::post_activity ($follow_activity, $user->actor ()->first (), $object_actor); + + if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300) + return response ()->json ([ "error" => "failed to post activity" ], 500); + + return [ + "success" => "followed" + ]; + } + + public function handle_unfollow (User $user, string $object) + { + $object_actor = Actor::where ("actor_id", $object)->first (); + if (!$object_actor) + return response ()->json ([ "error" => "object not found" ], 404); + $object_id = '"' . str_replace ("/", "\/", $object_actor->actor_id) . '"'; + + $follow_activity = Activity::where ("actor", $user->actor ()->first ()->actor_id) + ->where ("object", $object_id) + ->where ("type", "Follow") + ->first (); + if (!$follow_activity) + return response ()->json ([ "error" => "no follow activity found" ], 404); + + $unfollow_activity = TypeActivity::craft_undo ($follow_activity, $user->actor ()->first ()); + $response = TypeActivity::post_activity ($unfollow_activity, $user->actor ()->first (), $object_actor); + + if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300) + return response ()->json ([ "error" => "failed to post activity" ], 500); + + $follow_activity->delete (); + return [ + "success" => "unfollowed" + ]; } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index a8f91e9..b91dfe1 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -3,10 +3,13 @@ namespace App\Http\Controllers; use App\Types\TypeActor; +use App\Actions\ActionsFriends; use App\Models\User; use App\Models\Actor; +use GuzzleHttp\Client; + use Illuminate\Http\Request; class HomeController extends Controller @@ -48,7 +51,7 @@ class HomeController extends Controller foreach ($user->friend_requests () as $request) { - $actor = Actor::where ("actor_id", $request->actor)->first (); + $actor = Actor::where ("actor_id", $request)->first (); if (!$actor) continue; @@ -57,4 +60,22 @@ class HomeController extends Controller return view ("users.requests", compact ("user", "requests")); } + + public function requests_accept (Request $request) + { + $user = auth ()->user (); + + if (isset ($request->accept)) + { + // accept a single request + $target = $request->accept; + $action = ActionsFriends::add_friend ($target); + if (isset ($action ["error"])) + { + return back ()->with ("error", $action ["error"]); + } + + return back ()->with ("success", $action ["success"]); + } + } } diff --git a/app/Http/Controllers/UserActionController.php b/app/Http/Controllers/UserActionController.php new file mode 100644 index 0000000..e9a50a5 --- /dev/null +++ b/app/Http/Controllers/UserActionController.php @@ -0,0 +1,28 @@ +get ("object")); + if (isset ($response ["error"])) + return back ()->with ("error", $response ["error"]); + + return back ()->with ("success", $response ["success"]); + } + + public function unfriend (Request $request) + { + $response = ActionsFriends::remove_friend ($request->get ("object")); + if (isset ($response ["error"])) + return back ()->with ("error", $response ["error"]); + + return back ()->with ("success", $response ["success"]); + } +} diff --git a/app/Models/Actor.php b/app/Models/Actor.php index 753d999..d19840b 100644 --- a/app/Models/Actor.php +++ b/app/Models/Actor.php @@ -3,6 +3,8 @@ namespace App\Models; use App\Models\User; +use App\Models\Activity; + use App\Types\TypeActor; use Illuminate\Database\Eloquent\Model; @@ -52,4 +54,15 @@ class Actor extends Model { return TypeActor::build_response ($actor); } + + public function friends_with (Actor $actor) + { + $self_id = '"' . str_replace ("/", "\/", $this->actor_id) . '"'; + $other_id = '"' . str_replace ("/", "\/", $actor->actor_id) . '"'; + + $following = Activity::where ("actor", $this->actor_id)->where ("type", "Follow")->where ("object", $other_id)->first (); + $followers = Activity::where ("actor", $actor->actor_id)->where ("type", "Follow")->where ("object", $self_id)->first (); + + return $following && $followers; + } } diff --git a/app/Models/User.php b/app/Models/User.php index 43f4026..7179a3c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -77,19 +77,19 @@ class User extends Authenticatable { $actor_id = '"' . str_replace ("/", "\/", $this->actor->actor_id) . '"'; - $followers = Activity::where ("type", "Follow")->where ("object", $actor_id)->get (); - $following = Activity::where ("type", "Follow")->where ("actor", $actor_id)->get (); + $followers = Activity::where ("type", "Follow")->where ("object", $actor_id)->pluck ("actor")->toArray (); + $following = Activity::where ("type", "Follow")->where ("actor", $this->actor->actor_id)->pluck ("object")->toArray (); - return $followers->intersect ($following); + return array_intersect ($followers, $following); } public function friend_requests () { $actor_id = '"' . str_replace ("/", "\/", $this->actor->actor_id) . '"'; - $followers = Activity::where ("type", "Follow")->where ("object", $actor_id)->get (); - $following = Activity::where ("type", "Follow")->where ("actor", $actor_id)->get (); + $followers = Activity::where ("type", "Follow")->where ("object", $actor_id)->pluck ("actor")->toArray (); + $following = Activity::where ("type", "Follow")->where ("actor", $this->actor->actor_id)->pluck ("object")->toArray (); - return $followers->diff ($following); + return array_diff ($followers, $following); } } diff --git a/app/Types/TypeActivity.php b/app/Types/TypeActivity.php index cff9579..76cdc19 100644 --- a/app/Types/TypeActivity.php +++ b/app/Types/TypeActivity.php @@ -45,6 +45,30 @@ class TypeActivity { return $accept_activity; } + public static function craft_undo (Activity $activity, Actor $self) + { + $undo_activity = new Activity (); + $undo_activity->activity_id = env ("APP_URL") . "/activity/" . uniqid (); + $undo_activity->type = "Undo"; + $undo_activity->actor = $self->actor_id; + $undo_activity->object = $activity; + $undo_activity->save (); + + return $undo_activity; + } + + public static function craft_follow (Actor $actor, Actor $object) + { + $follow_activity = new Activity (); + $follow_activity->activity_id = env ("APP_URL") . "/activity/" . uniqid (); + $follow_activity->type = "Follow"; + $follow_activity->actor = $actor->actor_id; + $follow_activity->object = $object->actor_id; + $follow_activity->save (); + + return $follow_activity; + } + public static function craft_signed_headers ($activity, Actor $source, Actor $target) { if (!$source->user) diff --git a/app/Types/TypeActor.php b/app/Types/TypeActor.php index f420980..8726466 100644 --- a/app/Types/TypeActor.php +++ b/app/Types/TypeActor.php @@ -151,7 +151,7 @@ class TypeActor { foreach ($well_known->links as $link) { - if ($link->rel == "self") + if ($link->rel == "self") { $client = new Client (); $res = $client->request ("GET", $link->href, [ diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php index ed33a6a..d65b6bc 100644 --- a/resources/views/users/profile.blade.php +++ b/resources/views/users/profile.blade.php @@ -46,12 +46,23 @@

Contacting {{ $actor->preferredUsername }}

+ @auth
- - Add to Friends - + @if (auth ()->user ()->actor->friends_with ($actor)) +
+ @csrf + + Remove Friend +
+ @else +
+ @csrf + + Add to Friends +
+ @endif
@@ -103,6 +114,7 @@
+ @endauth
diff --git a/resources/views/users/requests.blade.php b/resources/views/users/requests.blade.php index c0b9352..2f94650 100644 --- a/resources/views/users/requests.blade.php +++ b/resources/views/users/requests.blade.php @@ -43,9 +43,10 @@

Friend Request

-
+ @csrf - + +
diff --git a/routes/web.php b/routes/web.php index 0c8f38a..3b7b125 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\HomeController; use App\Http\Controllers\UserController; use App\Http\Controllers\ProfileController; +use App\Http\Controllers\UserActionController; Route::get('/', [ HomeController::class, "home" ])->name ("home"); @@ -15,6 +16,10 @@ Route::get ("/auth/logout", [ UserController::class, "logout" ])->name ("logout" Route::post ("/auth/signup", [ UserController::class, "do_signup" ])->middleware ("guest"); Route::post ("/auth/login", [ UserController::class, "do_login" ])->middleware ("guest"); +// user actions +Route::post ("/user/action/friend", [ UserActionController::class, "friend" ])->name ("user.friend")->middleware ("auth"); +Route::post ("/user/action/unfriend", [ UserActionController::class, "unfriend" ])->name ("user.unfriend")->middleware ("auth"); + // user routes Route::get ("/user/edit", [ ProfileController::class, "edit" ])->name ("users.edit")->middleware ("auth"); Route::post ("/user/edit", [ ProfileController::class, "update" ])->middleware ("auth"); @@ -23,5 +28,6 @@ Route::get ("/user/{user_name}", [ ProfileController::class, "show" ])->name ("u // other routes Route::get ("/search", [ HomeController::class, "search" ])->name ("search"); Route::get ("/requests", [ HomeController::class, "requests" ])->name ("requests")->middleware ("auth"); +Route::post ("/requests", [ HomeController::class, "requests_accept" ])->middleware ("auth"); require __DIR__ . "/api.php";