diff --git a/README.md b/README.md index 1858cc5..fb4e47d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Notice that the styles were taken from [AnySpace](https://anyspace.3to.moe/about - [x] I cannot follow myself - [ ] Check when waiting for approval - [ ] Handle Rejection - - [ ] Likes + - [x] Likes - [ ] Comments - [-] Social features @@ -39,8 +39,8 @@ Notice that the styles were taken from [AnySpace](https://anyspace.3to.moe/about - [x] Remove friends - [x] Posts (everything should be federated) - [x] Create posts - - [ ] Delete posts - - [ ] Like posts + - [x] Delete posts + - [x] Like posts - [ ] Comment posts - [ ] Boost posts - [ ] Post tags diff --git a/app/Actions/ActionsPost.php b/app/Actions/ActionsPost.php index 8378b21..0be048b 100644 --- a/app/Actions/ActionsPost.php +++ b/app/Actions/ActionsPost.php @@ -89,4 +89,25 @@ class ActionsPost return ["success" => "Post created"]; } + + public static function like_post (Actor $actor, Note $note) + { + $client = new Client (); + + try + { + $response = $client->post ($actor->outbox, [ + "json" => [ + "type" => "Like", + "object" => $note->note_id, + ] + ]); + } + catch (\Exception $e) + { + return ["error" => "Could not connect to server."]; + } + + return $response; + } } diff --git a/app/Http/Controllers/AP/APInboxController.php b/app/Http/Controllers/AP/APInboxController.php index 04c0fa7..5750dee 100644 --- a/app/Http/Controllers/AP/APInboxController.php +++ b/app/Http/Controllers/AP/APInboxController.php @@ -6,6 +6,8 @@ use App\Models\User; use App\Models\Actor; use App\Models\Activity; use App\Models\Follow; +use App\Models\Note; +use App\Models\Like; use App\Types\TypeActor; use App\Types\TypeActivity; @@ -32,14 +34,20 @@ class APInboxController extends Controller case "Undo": $this->handle_undo ($user, $request->all ()); break; + + case "Like": + $this->handle_like ($user, $request->all ()); + break; + + default: + Log::info ("APInboxController@index"); + Log::info ("Unknown type: " . $type); + break; } } private function handle_follow (User $user, $activity) { - if (TypeActivity::activity_exists ($activity ["id"])) - return response ()->json (["error" => "Activity already exists",], 409); - $actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]); $target = TypeActor::actor_get_local ($activity ["object"]); @@ -77,9 +85,6 @@ class APInboxController extends Controller public function handle_undo (User $user, $activity) { - if (TypeActivity::activity_exists ($activity ["id"])) - return response ()->json (["error" => "Activity already exists",], 409); - $actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]); $child_activity = $activity ["object"]; @@ -99,4 +104,36 @@ class APInboxController extends Controller // TODO: Should Undo create a new activity in database? return response ()->json (["success" => "Activity undone",], 200); } + + public function handle_like (User $user, $activity) + { + $actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]); + $note_id = $activity ["object"]; + $note = Note::where ("note_id", $note_id)->first (); + if (!$note) + { + Log::info ("Note not found: " . $note_id); + return response ()->json (["error" => "Note not found",], 404); + } + + // check like doesn't already exist + $like_exists = $actor->liked_note ($note); + if ($like_exists) + return response ()->json (["error" => "Like already exists",], 409); + + $activity ["activity_id"] = $activity ["id"]; + $activity_exists = TypeActivity::activity_exists ($activity ["id"]); + if (!$activity_exists) + $act = Activity::create ($activity); + else + $act = Activity::where ("activity_id", $activity ["id"])->first (); + + $like = Like::create ([ + "activity_id" => $act->id, + "actor_id" => $actor->id, + "note_id" => $note->id, + ]); + + return response ()->json (["success" => "Like created",], 200); + } } diff --git a/app/Http/Controllers/AP/APOutboxController.php b/app/Http/Controllers/AP/APOutboxController.php index 629f9ea..4fb5a7a 100644 --- a/app/Http/Controllers/AP/APOutboxController.php +++ b/app/Http/Controllers/AP/APOutboxController.php @@ -49,6 +49,10 @@ class APOutboxController extends Controller return $this->handle_unfollow ($user, $request->get ("object")); break; + case "Like": + return $this->handle_like ($user, $request->get ("object")); + break; + case "Post": return $this->handle_post ($user, $request); break; @@ -200,6 +204,37 @@ class APOutboxController extends Controller ]; } + public function handle_like (User $user, $request) + { + $object = Note::where ("note_id", $request)->first (); + if (!$object) + return response ()->json ([ "error" => "object not found" ], 404); + + $actor = $user->actor ()->first (); + $already_liked = $actor->liked_note ($object); + if ($already_liked) + { + // undo the like + $like_activity = $already_liked->get_activity ()->first (); + $undo_activity = TypeActivity::craft_undo ($like_activity, $actor); + + $response = TypeActivity::post_activity ($undo_activity, $actor, $object->get_actor ()->first ()); + return [ + "success" => "unliked" + ]; + } + + $like_activity = TypeActivity::craft_like ($actor, $object->note_id); + $response = TypeActivity::post_activity ($like_activity, $actor, $object->get_actor ()->first ()); + + if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300) + return response ()->json ([ "error" => "failed to post activity" ], 500); + + return [ + "success" => "liked" + ]; + } + public function handle_post (User $user, $request) { $actor = $user->actor ()->first (); diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index fc126f4..cef27d3 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -70,6 +70,19 @@ class PostController extends Controller } } + public function like (Note $note) + { + if (!auth ()->check ()) + return back ()->with ("error", "You need to be logged in to like a post."); + + $user = auth ()->user (); + $actor = $user->actor ()->first (); + + $response = ActionsPost::like_post ($actor, $note); + + return back ()->with ("success", "Post liked successfully."); + } + public function delete (Note $note) { $actor = auth ()->user ()->actor ()->first (); diff --git a/app/Models/Actor.php b/app/Models/Actor.php index df544d4..775c2b6 100644 --- a/app/Models/Actor.php +++ b/app/Models/Actor.php @@ -76,4 +76,9 @@ class Actor extends Model return $following && $followers; } + + public function liked_note (Note $note) + { + return Like::where ("actor_id", $this->id)->where ("note_id", $note->id)->first (); + } } diff --git a/app/Models/Like.php b/app/Models/Like.php new file mode 100644 index 0000000..fb48021 --- /dev/null +++ b/app/Models/Like.php @@ -0,0 +1,29 @@ +belongsTo (Activity::class, "activity_id"); + } + + public function get_note () + { + return $this->belongsTo (Note::class, "note_id"); + } + + public function get_actor () + { + return $this->belongsTo (Actor::class, "actor_id"); + } +} diff --git a/app/Models/Note.php b/app/Models/Note.php index a828c44..0079ceb 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -30,6 +30,11 @@ class Note extends Model return $this->hasOne (Actor::class, "id", "actor_id"); } + public function get_likes () + { + return $this->hasMany (Like::class); + } + public function attachments () { return $this->hasMany (NoteAttachment::class); diff --git a/app/Types/TypeActivity.php b/app/Types/TypeActivity.php index f56d089..af3a397 100644 --- a/app/Types/TypeActivity.php +++ b/app/Types/TypeActivity.php @@ -53,7 +53,7 @@ class TypeActivity { $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->object = TypeActivity::craft_response ($activity); $undo_activity->save (); return $undo_activity; @@ -119,6 +119,18 @@ class TypeActivity { return $delete_activity; } + public static function craft_like (Actor $actor, $id) + { + $like_activity = new Activity (); + $like_activity->activity_id = env ("APP_URL") . "/activity/" . uniqid (); + $like_activity->type = "Like"; + $like_activity->actor = $actor->actor_id; + $like_activity->object = $id; + $like_activity->save (); + + return $like_activity; + } + public static function get_private_key (Actor $actor) { return openssl_get_privatekey ($actor->private_key); diff --git a/database/migrations/2025_01_04_184440_create_likes_table.php b/database/migrations/2025_01_04_184440_create_likes_table.php new file mode 100644 index 0000000..a4ed35a --- /dev/null +++ b/database/migrations/2025_01_04_184440_create_likes_table.php @@ -0,0 +1,32 @@ +id(); + + $table->foreignId ("activity_id")->constrained ()->onDelete ("cascade"); + $table->foreignId ("note_id")->constrained ()->onDelete ("cascade"); + $table->foreignId ("actor_id")->constrained ()->onDelete ("cascade"); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('likes'); + } +}; diff --git a/resources/views/components/comment_block.blade.php b/resources/views/components/comment_block.blade.php index 363fe44..f2ac51b 100644 --- a/resources/views/components/comment_block.blade.php +++ b/resources/views/components/comment_block.blade.php @@ -31,6 +31,8 @@ else

+

{{ $post->summary }}

+ {!! $post->content !!}

@@ -42,6 +44,10 @@ else


+

+ Likes: {{ $post->get_likes ()->count () }} +

+ diff --git a/resources/views/posts/show.blade.php b/resources/views/posts/show.blade.php index 0f38d62..42f8ac2 100644 --- a/resources/views/posts/show.blade.php +++ b/resources/views/posts/show.blade.php @@ -62,6 +62,17 @@
+
+
+ @csrf + +
+
+ +

+ Likes: {{ $note->get_likes ()->count () }} +

+

Comments

diff --git a/routes/web.php b/routes/web.php index fc575d6..6ef296a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,6 +31,7 @@ Route::get ("/user/{user_name}", [ ProfileController::class, "show" ])->name ("u // posts routes Route::get ("/post/{note}/edit", [ PostController::class, "edit" ])->name ("posts.edit")->middleware ("auth"); Route::post ("/post/{note}/edit", [ PostController::class, "update" ])->middleware ("auth"); +Route::post ("/post/{note}/like", [ PostController::class, "like" ])->name ("posts.like")->middleware ("auth"); Route::get ("/post/{note}", [ PostController::class, "show" ])->name ("posts.show"); Route::delete ("/post/{note}", [ PostController::class, "delete" ])->name ("posts.delete")->middleware ("auth");