diff --git a/README.md b/README.md index 1e1c55f..ff368e6 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,6 @@ Notice that the styles were taken from [AnySpace](https://anyspace.3to.moe/about - [ ] Others - [ ] Music - [ ] Announcements + +- [ ] Fixes + - [ ] Fix that weird json encoding in the object field of an activity diff --git a/app/Actions/ActionsPost.php b/app/Actions/ActionsPost.php index 388d7ff..8378b21 100644 --- a/app/Actions/ActionsPost.php +++ b/app/Actions/ActionsPost.php @@ -18,11 +18,8 @@ use Intervention\Image\Drivers\Gd\Driver; class ActionsPost { - public static function post_new ($request) + public static function process_content_and_attachments ($request) { - if (!auth ()->check ()) - return ["error" => "You must be logged in to post."]; - $processed_content = Str::markdown ($request->get ("content")); $attachments = []; @@ -42,6 +39,36 @@ class ActionsPost } } + return [ + "summary" => $request->summary, + "content" => $processed_content, + "attachments" => $attachments, + ]; + } + + public static function create_attachment (Note $note, $url) + { + $attachment = new NoteAttachment (); + $attachment->note_id = $note->id; + $attachment->url = $url; + $attachment->save (); + } + + public static function create_attachments (Note $note, $attachments) + { + foreach ($attachments as $attachment) + { + ActionsPost::create_attachment ($note, $attachment); + } + } + + public static function post_new ($request) + { + if (!auth ()->check ()) + return ["error" => "You must be logged in to post."]; + + $processed = ActionsPost::process_content_and_attachments ($request); + $actor = auth ()->user ()->actor ()->first (); try { @@ -49,8 +76,9 @@ class ActionsPost $response = $client->post ($actor->outbox, [ "json" => [ "type" => "Post", - "content" => $processed_content, - "attachments" => $attachments, + "summary" => $processed ["summary"], + "content" => $processed ["content"], + "attachments" => $processed ["attachments"], ] ]); } diff --git a/app/Http/Controllers/AP/APInboxController.php b/app/Http/Controllers/AP/APInboxController.php index 5ee63bf..0261b88 100644 --- a/app/Http/Controllers/AP/APInboxController.php +++ b/app/Http/Controllers/AP/APInboxController.php @@ -20,6 +20,9 @@ class APInboxController extends Controller $request = request (); $type = $request->get ("type"); + Log::info ("APInboxController@index"); + Log::info (json_encode ($request->all ())); + switch ($type) { case "Follow": $this->handle_follow ($user, $request->all ()); @@ -29,9 +32,6 @@ class APInboxController extends Controller $this->handle_undo ($user, $request->all ()); break; } - - Log::info ("APInboxController@index"); - Log::info (json_encode ($request->all ())); } private function handle_follow (User $user, $activity) diff --git a/app/Http/Controllers/AP/APInstanceInboxController.php b/app/Http/Controllers/AP/APInstanceInboxController.php index 09c85ef..d9acb57 100644 --- a/app/Http/Controllers/AP/APInstanceInboxController.php +++ b/app/Http/Controllers/AP/APInstanceInboxController.php @@ -21,6 +21,9 @@ class APInstanceInboxController extends Controller $activity = request ()->all (); $activity_type = $activity['type']; + Log::info ("APInstanceInboxController:inbox"); + Log::info ($activity); + switch ($activity_type) { case "Create": @@ -41,9 +44,6 @@ class APInstanceInboxController extends Controller break; } - Log::info ("APInstanceInboxController:inbox"); - Log::info ($activity); - return response ()->json (["status" => "ok"]); } @@ -69,6 +69,9 @@ class APInstanceInboxController extends Controller public function handle_delete ($activity) { + if (!is_array ($activity ["object"])) + return response ()->json (["error" => "not implemented"]); + // we suppose that we are deleting a note $note = TypeNote::note_exists ($activity ["object"]["id"]); if (!$note) diff --git a/app/Http/Controllers/AP/APOutboxController.php b/app/Http/Controllers/AP/APOutboxController.php index 396665b..59a8b89 100644 --- a/app/Http/Controllers/AP/APOutboxController.php +++ b/app/Http/Controllers/AP/APOutboxController.php @@ -2,20 +2,23 @@ namespace App\Http\Controllers\AP; +use App\Models\Note; use App\Models\User; use App\Models\Actor; +use App\Types\TypeNote; use App\Models\Activity; use App\Models\Instance; -use App\Models\Note; -use App\Models\NoteAttachment; -use App\Types\TypeActivity; use App\Types\TypeActor; -use App\Types\TypeNote; +use App\Types\TypeActivity; +use App\Actions\ActionsPost; use Illuminate\Http\Request; + +use App\Models\NoteAttachment; use Illuminate\Support\Facades\Log; use App\Http\Controllers\Controller; +use Illuminate\Support\Facades\Storage; class APOutboxController extends Controller { @@ -28,6 +31,10 @@ class APOutboxController extends Controller return $this->handle_update_profile ($user); break; + case "UpdateNote": + return $this->handle_update_note ($user, $request); + break; + case "Follow": return $this->handle_follow ($user, $request->get ("object")); break; @@ -56,13 +63,75 @@ class APOutboxController extends Controller $instances = Instance::all (); foreach ($instances as $instance) { - $response = TypeActivity::post_activity ($update_activity, $actor, $instance->inbox); + $response = TypeActivity::post_activity ($update_activity, $actor, $instance->inbox, true); if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300) - continue; + { + Log::info ("failed to post activity to " . $instance->inbox); + } } return response ()->json ("success", 200); } + public function handle_update_note (User $user, $request) + { + $actor = $user->actor ()->first (); + + // first check if there are new attachments + if ($request ["attachments"]) + { + // TODO: Keep old attachments + $attachments = NoteAttachment::where ("note_id", $request ["note"])->get (); + foreach ($attachments as $attachment) + { + $processed_url = parse_url ($attachment->url); + $processed_path = $processed_url["path"]; + + $processed_path = str_replace ("/storage", "", $processed_path); + if (Storage::disk ("public")->exists ($processed_path)) + { + Storage::disk ("public")->delete ($processed_path); + } + else + { + Log::error ("Attachment not found: " . $attachment->url . " " . $processed_path); + } + + $attachment->delete (); + } + } + + $note = Note::where ("id", $request ["note"])->first (); + if (!$note) + return response ()->json ([ "error" => "note not found" ], 404); + + $note_actor = $note->get_actor ()->first (); + if ($actor != $note_actor) + return response ()->json ([ "error" => "not allowed" ], 403); + + $note->summary = $request ["summary"]; + $note->content = $request ["content"]; + $note->save (); + + if ($request ["attachments"]) + { + ActionsPost::create_attachments ($note, $request ["attachments"]); + } + + $note_response = TypeNote::build_response ($note); + $update_activity = TypeActivity::craft_update ($actor, $note_response); + $instances = Instance::all (); + foreach ($instances as $instance) + { + $response = TypeActivity::post_activity ($update_activity, $actor, $instance->inbox, true); + if ($response->getStatusCode () < 200 || $response->getStatusCode () >= 300) + { + Log::info ("failed to post activity to " . $instance->inbox); + } + } + + return response ()->json ("success", 200); + } + public function handle_follow (User $user, string $object) { $object_actor = Actor::where ("actor_id", $object)->first (); @@ -124,13 +193,7 @@ class APOutboxController extends Controller if (isset ($request ["attachments"])) { - foreach ($request ["attachments"] as $attachment) - { - $attachment_note = NoteAttachment::create ([ - "note_id" => $note->id, - "url" => $attachment - ]); - } + ActionsPost::create_attachments ($note, $request ["attachments"]); } $create_activity = TypeActivity::craft_create ($actor, $note); diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index 61c44e6..719243f 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -4,8 +4,14 @@ namespace App\Http\Controllers; use App\Models\Note; +use GuzzleHttp\Client; +use App\Actions\ActionsPost; + use Illuminate\Http\Request; +use App\Models\NoteAttachment; +use Illuminate\Support\Facades\Log; + class PostController extends Controller { public function show (Note $note) @@ -14,4 +20,53 @@ class PostController extends Controller return view ("posts.show", compact ("note", "actor")); } + + public function edit (Note $note) + { + $actor = $note->get_actor ()->first (); + $note_user = $actor->user ()->first (); + if (!auth()->user ()->is ($note_user)) { + return back ()->with ("error", "You are not allowed to edit this post."); + } + + return view ("posts.edit", compact ("note", "actor")); + } + + public function update (Note $note, Request $request) + { + $actor = auth ()->user ()->actor ()->first (); + $note_user = $actor->user ()->first (); + if (!auth ()->user ()->is ($note_user)) { + return back ()->with ("error", "You are not allowed to edit this post."); + } + + $incoming_fields = $request->validate ([ + "summary" => "nullable|string", + "content" => "required|string", + "files" => "nullable|array", + "files.*" => "image" + ]); + + $processed = ActionsPost::process_content_and_attachments ($request); + + try { + $client = new Client (); + $client->request ("POST", $note->get_actor ()->first ()->outbox, [ + "json" => [ + "type" => "UpdateNote", + "note" => $note->id, + "summary" => $processed["summary"], + "content" => $processed["content"], + "attachments" => $processed["attachments"] + ] + ]); + + return redirect ()->route ("posts.show", $note)->with ("success", "Post updated successfully."); + } catch (\Exception $e) { + return back ()->with ("error", "An error occurred while updating the post."); + + Log::error ("An error occurred while updating the post."); + Log::error ($e->getMessage ()); + } + } } diff --git a/app/Http/Controllers/UserActionController.php b/app/Http/Controllers/UserActionController.php index 635f292..aa5b586 100644 --- a/app/Http/Controllers/UserActionController.php +++ b/app/Http/Controllers/UserActionController.php @@ -30,6 +30,7 @@ class UserActionController extends Controller public function post_new (Request $request) { $request->validate ([ + "summary" => "nullable|string", "content" => "required", "files.*" => "mimes:jpeg,png,jpg,gif,webm|max:4096" ]); diff --git a/app/Types/TypeActivity.php b/app/Types/TypeActivity.php index 3fffb4f..4f67e16 100644 --- a/app/Types/TypeActivity.php +++ b/app/Types/TypeActivity.php @@ -18,6 +18,7 @@ class TypeActivity { "type" => $activity->type, "actor" => $activity->actor, "object" => $activity->object, + "published" => $activity->created_at, ]; if ($activity->target) @@ -100,6 +101,18 @@ class TypeActivity { return $create_activity; } + public static function get_private_key (Actor $actor) + { + return openssl_get_privatekey ($actor->private_key); + } + + public static function sign ($data, $key) + { + openssl_sign ($data, $signature, $key, OPENSSL_ALGO_SHA256); + + return $signature; + } + public static function craft_signed_headers ($activity, Actor $source, $target) { if (!$source->user) @@ -110,7 +123,7 @@ class TypeActivity { $key_id = $source->actor_id . "#main-key"; - $signer = openssl_get_privatekey ($source->private_key); + $signer = TypeActivity::get_private_key ($source); $date = gmdate ("D, d M Y H:i:s \G\M\T"); @@ -125,6 +138,12 @@ class TypeActivity { else $url = parse_url ($target); + if (!$url ["path"] || !$url ["host"]) + { + Log::error ("Target not found"); + return null; + } + $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); @@ -139,16 +158,47 @@ class TypeActivity { "Signature" => $signature_header, "Content-Type" => "application/activity+json", "Accept" => "application/activity+json", + "B64" => $signature_b64 ]; } - public static function post_activity (Activity $activity, Actor $source, $target) + public static function post_activity (Activity $activity, Actor $source, $target, $should_sign = false) { $crafted_activity = TypeActivity::craft_response ($activity); + + if ($should_sign) + { + $crafted_activity["to"] = [ + "https://www.w3.org/ns/activitystreams#Public" + ]; + + $crafted_activity["cc"] = [ + $source->following + ]; + + $key = TypeActivity::get_private_key ($source); + $activity_json = json_encode ($crafted_activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + $signature = TypeActivity::sign ($activity_json, $key); + + $crafted_activity ["signature"] = [ + "type" => "RsaSignature2017", + "creator" => $source->actor_id . "#main-key", + "created" => gmdate ("Y-m-d\TH:i:s\Z"), + "signatureValue" => base64_encode ($signature) + ]; + + $activity_json = json_encode ($crafted_activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + } + $activity_json = json_encode ($crafted_activity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); $activity_json = mb_convert_encoding ($activity_json, "UTF-8"); $headers = TypeActivity::craft_signed_headers ($activity_json, $source, $target); + if (!$headers) + { + Log::error ("Failed to craft headers"); + return null; + } try { $target_inbox = null; @@ -171,7 +221,13 @@ class TypeActivity { } catch (RequestException $e) { - Log::error ($e->getMessage ()); + $response = $e->getResponse (); + if ($response) + { + Log::error ("Failed to post activity: " . $response->getBody ()); + } + + Log::error ("Failed to post activity: " . $e->getMessage ()); return null; } diff --git a/app/Types/TypeActor.php b/app/Types/TypeActor.php index a3490be..ef14fe2 100644 --- a/app/Types/TypeActor.php +++ b/app/Types/TypeActor.php @@ -232,6 +232,7 @@ class TypeActor { ] ]); } catch (\Exception $e) { + // TODO: check if we got a 404 return json_encode (["error" => "Actor not found"]); } diff --git a/app/Types/TypeNote.php b/app/Types/TypeNote.php index 306079b..3a7fa25 100644 --- a/app/Types/TypeNote.php +++ b/app/Types/TypeNote.php @@ -21,6 +21,7 @@ class TypeNote "summary" => $note->summary, "inReplyTo" => $note->in_reply_to, "published" => $note->created_at, + "updated" => $note->updated_at, "url" => $note->url, "attributedTo" => $note->attributedTo, "to" => [ @@ -50,16 +51,19 @@ class TypeNote // TODO: url should be route ('posts.show', $note->id) $note = Note::create ([ "actor_id" => $actor->id, + "summary" => $request ["summary"], "note_id" => env ("APP_URL") . "/ap/v1/note/" . uniqid (), "in_reply_to" => $request ["inReplyTo"] ?? null, "type" => "Note", "summary" => $request ["summary"] ?? null, - "url" => "TODO", "attributedTo" => $actor->actor_id, "content" => $request ["content"] ?? null, "tag" => $request ["tag"] ?? null ]); + $note->url = route ('posts.show', $note->id); + $note->save (); + return $note; } diff --git a/composer.json b/composer.json index b0c1727..c69544b 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "guzzlehttp/guzzle": "^7.9", "intervention/image": "^3.10", "laravel/framework": "^11.31", - "laravel/tinker": "^2.9" + "laravel/tinker": "^2.9", + "league/html-to-markdown": "^5.1" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 984aaac..9fa84cc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "067a260457c754c554a61fce0e741b0f", + "content-hash": "628d31471da81db14ba13b073eaa09e0", "packages": [ { "name": "brick/math", @@ -1920,6 +1920,95 @@ }, "time": "2024-08-09T21:24:39+00:00" }, + { + "name": "league/html-to-markdown", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.1.0", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^8.5 || ^9.2", + "scrutinizer/ocular": "^1.6", + "unleashedtech/php-coding-standard": "^2.7 || ^3.0", + "vimeo/psalm": "^4.22 || ^5.0" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "support": { + "issues": "https://github.com/thephpleague/html-to-markdown/issues", + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" + } + ], + "time": "2023-07-12T21:21:09+00:00" + }, { "name": "league/mime-type-detection", "version": "1.16.0", diff --git a/resources/views/components/comment_block.blade.php b/resources/views/components/comment_block.blade.php index ac4ee0e..363fe44 100644 --- a/resources/views/components/comment_block.blade.php +++ b/resources/views/components/comment_block.blade.php @@ -45,7 +45,7 @@ else - @if ($actor->user && auth ()->check () && auth ()->user ()->is ($actor->user)) + {{-- @if ($actor->user && auth ()->check () && auth ()->user ()->is ($actor->user))
- @endif + @endif --}} diff --git a/resources/views/posts/edit.blade.php b/resources/views/posts/edit.blade.php new file mode 100644 index 0000000..1396f5a --- /dev/null +++ b/resources/views/posts/edit.blade.php @@ -0,0 +1,42 @@ +@extends ("partials.layout") + +@section ("title", "Edit Post") + +@php + use League\HTMLToMarkdown\HtmlConverter; + $converter = new HtmlConverter (); + $markdown = $converter->convert ($note->content); +@endphp + +@section ("content") +Edit your post
+