diff --git a/app/Actions/ActionsPost.php b/app/Actions/ActionsPost.php index e188cdd..0c32a14 100644 --- a/app/Actions/ActionsPost.php +++ b/app/Actions/ActionsPost.php @@ -33,6 +33,36 @@ class ActionsPost ]; } + preg_match_all ("/@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?/", $request->get ("content"), $mention_matches); + $mentions = $mention_matches [0]; + $processed_mentions = []; + + foreach ($mentions as $mention) + { + $ats = explode ("@", $mention); + $actor = null; + + if (count ($ats) == 2) + { + // it's a local user + $actor = Actor::where ("preferredUsername", $ats [1])->first (); + if (!$actor) + continue; + } + else + { + $actor = Actor::where ("local_actor_id", $mention)->first (); + if (!$actor) + continue; + } + + $processed_mentions[] = [ + "type" => "Mention", + "href" => $actor->actor_id, + "name" => $mention + ]; + } + $processed_content = Str::markdown ($request->get ("content")); $attachments = []; @@ -57,7 +87,8 @@ class ActionsPost "content" => $processed_content, "attachments" => $attachments, "inReplyTo" => $request->inReplyTo ?? null, - "tags" => $processed_tags + "tags" => $processed_tags, + "mentions" => $processed_mentions ]; } @@ -96,6 +127,7 @@ class ActionsPost "attachments" => $processed ["attachments"], "inReplyTo" => $processed ["inReplyTo"] ?? null, "tags" => $processed ["tags"] ?? null, + "mentions" => $processed ["mentions"] ?? null ] ]); } diff --git a/app/Http/Controllers/AP/APOutboxController.php b/app/Http/Controllers/AP/APOutboxController.php index 1873d6e..6342ce0 100644 --- a/app/Http/Controllers/AP/APOutboxController.php +++ b/app/Http/Controllers/AP/APOutboxController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\AP; use App\Models\Note; use App\Models\NoteAttachment; +use App\Models\NoteMention; use App\Models\Announcement; use App\Models\User; use App\Models\Actor; @@ -208,6 +209,7 @@ class APOutboxController extends Controller /* if (!$response || $response->getStatusCode () < 200 || $response->getStatusCode () >= 300) return response ()->json ([ "error" => "failed to post activity" ], 500); */ + Log::info ($follow_activity); $follow_activity->delete (); return [ "success" => "unfollowed" @@ -368,8 +370,27 @@ class APOutboxController extends Controller } } - $create_activity = TypeActivity::craft_create ($actor, $note); + if (isset ($request ["mentions"])) + { + foreach ($request ["mentions"] as $mention) + { + $mention_exists = NoteMention::where ("note_id", $note->id)->where ("object", $mention ["href"])->first (); + if ($mention_exists) + continue; + $object = TypeActor::actor_exists ($mention ["href"]); + if (!$object) + // we don't obtain actors when we are just mentioning them + continue; + + $mention = NoteMention::create ([ + "note_id" => $note->id, + "actor_id" => $object->id + ]); + } + } + + $create_activity = TypeActivity::craft_create ($actor, $note); $note->activity_id = $create_activity->id; $note->save (); diff --git a/app/Jobs/PostActivityJob.php b/app/Jobs/PostActivityJob.php index d4ce1bb..c5d2adf 100644 --- a/app/Jobs/PostActivityJob.php +++ b/app/Jobs/PostActivityJob.php @@ -44,10 +44,6 @@ class PostActivityJob implements ShouldQueue if ($this->should_sign) { - $crafted_activity ["to"] = [ - "https://www.w3.org/ns/activitystreams#Public", - ]; - $crafted_activity ["cc"] = [ $this->actor->following ]; diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 29c575b..f4298ab 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -12,12 +12,16 @@ class Activity extends Model "type", "object", "target", - "summary" + "summary", + "to", + "cc" ]; protected $casts = [ "object" => "array", - "target" => "array" + "target" => "array", + "to" => "array", + "cc" => "array" ]; public function setObjectAttribute ($value) @@ -25,6 +29,16 @@ class Activity extends Model $this->attributes["object"] = json_encode ($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); } + public function setToAttribute ($value) + { + $this->attributes["to"] = json_encode ($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + } + + public function setCcAttribute ($value) + { + $this->attributes["cc"] = json_encode ($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + } + public function actor () { return $this->belongsTo (Actor::class); diff --git a/app/Models/Note.php b/app/Models/Note.php index d8634f8..e3ea891 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -19,8 +19,25 @@ class Note extends Model "attributedTo", "content", "tag", + "to", + "cc" ]; + protected $casts = [ + "to" => "array", + "cc" => "array" + ]; + + public function setToAttribute ($value) + { + $this->attributes["to"] = json_encode ($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + } + + public function setCcAttribute ($value) + { + $this->attributes["cc"] = json_encode ($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION); + } + public function get_activity () { return $this->hasOne (Activity::class, "id", "activity_id"); @@ -57,6 +74,11 @@ class Note extends Model return $this->belongsToMany (Hashtag::class, "note_hashtag"); } + public function get_mentions () + { + return $this->hasMany (NoteMention::class); + } + public function attachments () { return $this->hasMany (NoteAttachment::class); diff --git a/app/Models/NoteMention.php b/app/Models/NoteMention.php new file mode 100644 index 0000000..76b56ad --- /dev/null +++ b/app/Models/NoteMention.php @@ -0,0 +1,23 @@ +belongsTo (Note::class); + } + + public function actor () + { + return $this->belongsTo (Actor::class); + } +} diff --git a/app/Types/TypeActivity.php b/app/Types/TypeActivity.php index dbaccf3..7c08d6a 100644 --- a/app/Types/TypeActivity.php +++ b/app/Types/TypeActivity.php @@ -21,6 +21,8 @@ class TypeActivity { "type" => $activity->type, "actor" => $activity->actor, "object" => $activity->object, + "to" => $activity->to, + "cc" => $activity->cc, "published" => $activity->created_at, ]; @@ -96,6 +98,7 @@ class TypeActivity { { case "Note": $create_activity->object = TypeNote::build_response ($fields); + $create_activity->cc = $fields["cc"]; break; } diff --git a/app/Types/TypeActor.php b/app/Types/TypeActor.php index db2bdf2..1ee9edd 100644 --- a/app/Types/TypeActor.php +++ b/app/Types/TypeActor.php @@ -366,6 +366,7 @@ class TypeActor { return TypeActor::actor_process_ordered_collection ($actor->featured); } + // TODO: Move this to TypeOrderedCollection public static function actor_process_ordered_collection ($collection_link) { $items = []; diff --git a/app/Types/TypeNote.php b/app/Types/TypeNote.php index f9aa2e7..05cc9ab 100644 --- a/app/Types/TypeNote.php +++ b/app/Types/TypeNote.php @@ -7,7 +7,7 @@ use App\Models\Hashtag; use App\Models\Actor; use App\Models\Activity; use App\Models\NoteAttachment; - +use App\Models\NoteMention; use GuzzleHttp\Client; use Illuminate\Support\Facades\Log; @@ -27,12 +27,8 @@ class TypeNote "updated" => $note->updated_at, "url" => $note->url, "attributedTo" => $note->attributedTo, - "to" => [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc" => [ - $author->following - ], + "to" => $note->to, + "cc" => $note->cc, "content" => $note->content ]; @@ -56,6 +52,18 @@ class TypeNote ]; } + $mentions = $note->get_mentions ()->get (); + foreach ($mentions as $mention) + { + $response ["tag"] [] = [ + "type" => "Mention", + "href" => $mention->actor->actor_id, + "name" => $mention->actor->local_actor_id ?? "@" . $mention->actor->preferredUsername + ]; + } + + Log::info (json_encode ($response)); + return $response; } @@ -74,7 +82,12 @@ class TypeNote "summary" => $request ["summary"] ?? null, "attributedTo" => $actor->actor_id, "content" => $request ["content"] ?? null, - "tag" => $request ["tag"] ?? null + "tag" => $request ["tag"] ?? null, + + // TODO: This should change when I implement visibilities and private notes + "cc" => [ + $actor->followers + ] ]); $note->url = route ('posts.show', $note->id); @@ -135,37 +148,61 @@ class TypeNote } } - if (isset ($request ["tag"]) && $request ["tag"]) - { - foreach ($request ["tag"] as $tag) - { - if ($tag ["type"] != "Hashtag") - continue; - - $tag_name = $tag ["name"]; - - $hashtag_exists = Hashtag::where ("name", $tag_name)->first (); - if ($hashtag_exists) - { - $note->get_hashtags ()->attach ($hashtag_exists->id); - continue; - } - - $hashtag = Hashtag::create ([ - "name" => $tag_name - ]); - $note->get_hashtags ()->attach ($hashtag->id); - } - } - if ($request ["inReplyTo"]) { $parent_exists = Note::where ("note_id", $request ["inReplyTo"])->first (); if (!$parent_exists) + $parent_exists = TypeNote::obtain_external ($request ["inReplyTo"]); + + $note->in_reply_to = $parent_exists ? $parent_exists->note_id : null; + } + + if (isset ($request ["tag"]) && $request ["tag"]) + { + foreach ($request ["tag"] as $tag) { - $parent = TypeNote::obtain_external ($request ["inReplyTo"]); - if ($parent) - $note->in_reply_to = $parent->note_id; + // TODO: refactor this, this code is shit but I want to get first working first + switch ($tag ["type"]) + { + case "Hashtag": + $tag_name = $tag ["name"]; + + $hashtag_exists = Hashtag::where ("name", $tag_name)->first (); + if ($hashtag_exists) + { + $note->get_hashtags ()->attach ($hashtag_exists->id); + continue; + } + + $hashtag = Hashtag::create ([ + "name" => $tag_name + ]); + $note->get_hashtags ()->attach ($hashtag->id); + break; + + case "Mention": + $mention_name = $tag["name"]; + $mention_actor = null; + + $actor_exists = Actor::where ("local_actor_id", $mention_name)->first (); + if (!$actor_exists) + { + // let's check if maybe it's local + $processed_name = explode ("@", $mention_name); + if (count ($processed_name) < 2) + continue; + + $actor_exists = Actor::where ("preferredUsername", $processed_name [1])->first (); + if (!$actor_exists) + continue; + } + + $mention = NoteMention::create ([ + "note_id" => $note->id, + "actor_id" => $actor_exists->id + ]); + break; + } } } diff --git a/database/migrations/2025_01_09_001938_add_fields_to_activities_table.php b/database/migrations/2025_01_09_001938_add_fields_to_activities_table.php new file mode 100644 index 0000000..644e8ae --- /dev/null +++ b/database/migrations/2025_01_09_001938_add_fields_to_activities_table.php @@ -0,0 +1,40 @@ +json ("to")->default (json_encode (["https://www.w3.org/ns/activitystreams#Public"], JSON_UNESCAPED_SLASHES))->nullable (); + $table->json ("cc")->default (json_encode ([], JSON_UNESCAPED_SLASHES))->nullable (); + }); + + Schema::table ("notes", function (Blueprint $table) { + $table->json ("to")->default (json_encode (["https://www.w3.org/ns/activitystreams#Public"], JSON_UNESCAPED_SLASHES))->nullable (); + $table->json ("cc")->default (json_encode ([], JSON_UNESCAPED_SLASHES))->nullable (); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn ("to"); + $table->dropColumn ("cc"); + }); + + Schema::table ("notes", function (Blueprint $table) { + $table->dropColumn ("to"); + $table->dropColumn ("cc"); + }); + } +}; diff --git a/database/migrations/2025_01_09_003700_create_note_mentions_table.php b/database/migrations/2025_01_09_003700_create_note_mentions_table.php new file mode 100644 index 0000000..623be1f --- /dev/null +++ b/database/migrations/2025_01_09_003700_create_note_mentions_table.php @@ -0,0 +1,31 @@ +id(); + + $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('note_mentions'); + } +}; diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php index 786c1b4..c58c012 100644 --- a/resources/views/users/profile.blade.php +++ b/resources/views/users/profile.blade.php @@ -317,7 +317,7 @@

- {{ $actor->name }} has {{ count ($actor->get_posts ()) }} posts. + {{ $actor->name }} has {{ $actor->get_posts ()->total () }} posts.

@if (auth ()->user () && auth ()->user ()->is ($user))