now posts can be updated

This commit is contained in:
Ghostie 2025-01-02 20:05:54 -05:00
parent fdb9d09f0d
commit 031b805e5b
17 changed files with 391 additions and 34 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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,10 +63,72 @@ 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);
}
@ -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);

View File

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

View File

@ -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"
]);

View File

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

View File

@ -232,6 +232,7 @@ class TypeActor {
]
]);
} catch (\Exception $e) {
// TODO: check if we got a 404
return json_encode (["error" => "Actor not found"]);
}

View File

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

View File

@ -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",

91
composer.lock generated
View File

@ -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",

View File

@ -45,7 +45,7 @@ else
<a href="{{ route ('posts.show', [ 'note' => $post ]) }}">
<button type="button">View</button>
</a>
@if ($actor->user && auth ()->check () && auth ()->user ()->is ($actor->user))
{{-- @if ($actor->user && auth ()->check () && auth ()->user ()->is ($actor->user))
<form action="#" method="POST" style="display: inline">
@csrf
<a href="#">
@ -55,6 +55,6 @@ else
</a>
<input type="submit" value="Delete">
</form>
@endif
@endif --}}
</td>
</tr>

View File

@ -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")
<div class="row edit-blog-entry">
<div class="col w-20 left">
<div class="edit-info">
<p>Edit your post</p>
</div>
</div>
<div class="col right">
<h1>Edit Post</h1>
<p>
<a href="{{ route ('posts.show', ['note' => $note ]) }}">&larr; View post</a>
</p>
<br>
<form method="POST" enctype="multipart/form-data">
@csrf
<input type="text" name="summary" placeholder="Summary" value="{{ old ('summary', $note->summary) }}">
<br>
<textarea name="content" id="content">{{ old ('content', $markdown) }}</textarea>
<br>
<input type="file" name="files[]" accept="image/*" multiple>
<div class="publish">
<button type="submit" name="submit">
Update
</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -42,14 +42,21 @@
@if (auth ()->check () && auth ()->user ()->is ($actor->user))
<form action="#" method="POST">
@csrf
<a href="#">
<a href="{{ route ('posts.edit', [ 'note' => $note ]) }}">
<button type="button">Edit</button>
</a>
<button type="submit">Delete</button>
</form>
@endif
<div class="content">
<div class="heading">
<h4>{{ $note->summary }}</h4>
</div>
{!! $note->content !!}
@foreach ($note->attachments as $attachment)
<img loading="lazy" src="{{ $attachment->url }}" width="250">
@endforeach
</div>
<br>

View File

@ -277,6 +277,8 @@
@if (auth ()->user () && auth ()->user ()->is ($user))
<form action="{{ route ('user.post.new') }}" method="post" enctype="multipart/form-data">
@csrf
<input type="text" name="summary" placeholder="Title" size="60">
<br>
<textarea name="content" placeholder="What's on your mind?" cols="60" rows="5"></textarea>
<input type="file" name="files[]" accept="image/*" multiple>
<button type="submit">Post</button>

View File

@ -28,6 +28,8 @@ Route::post ("/user/edit", [ ProfileController::class, "update" ])->middleware (
Route::get ("/user/{user_name}", [ ProfileController::class, "show" ])->name ("users.show");
// 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::get ("/post/{note}", [ PostController::class, "show" ])->name ("posts.show");
// other routes