diff --git a/README.md b/README.md index f86069b..0cb4353 100644 --- a/README.md +++ b/README.md @@ -195,11 +195,12 @@ Restart nginx: sudo systemctl restart nginx ``` -Now link the storage to the public folder and install the dependencies for reverb: +Now link the storage to the public folder, install the dependencies for reverb and seed the database: ```bash php artisan storage:link php artisan install:broadcasting +php artisan db:seed ``` Now, we need to create three services to handle the jobs that OurSpace needs to run, another to handle the notifications' queue and another one to run Laravel Reverb. So run something `emacs /lib/systemd/system/ourspace-queue.service`, `emacs /lib/systemd/system/ourspace-notifications.service`, `emacs /lib/systemd/system/ourspace-ws.service` and add the following content: diff --git a/app/Actions/ActionsPost.php b/app/Actions/ActionsPost.php index de61426..90cad87 100644 --- a/app/Actions/ActionsPost.php +++ b/app/Actions/ActionsPost.php @@ -115,7 +115,11 @@ class ActionsPost $processed = ActionsPost::process_content_and_attachments ($request); - $actor = auth ()->user ()->actor ()->first (); + $actor = null; + if ($request ["blog_id"]) + $actor = Actor::where ("blog_id", $request ["blog_id"])->first (); + else + $actor = auth ()->user ()->actor ()->first (); try { $client = new Client (); diff --git a/app/Events/BlogCreatedEvent.php b/app/Events/BlogCreatedEvent.php new file mode 100644 index 0000000..1280ada --- /dev/null +++ b/app/Events/BlogCreatedEvent.php @@ -0,0 +1,31 @@ +blog = $blog; + $this->user = $user; + } +} diff --git a/app/Http/Controllers/AP/APActorController.php b/app/Http/Controllers/AP/APActorController.php index 57a1ff8..00a2e38 100644 --- a/app/Http/Controllers/AP/APActorController.php +++ b/app/Http/Controllers/AP/APActorController.php @@ -17,25 +17,36 @@ use App\Http\Controllers\Controller; class APActorController extends Controller { - public function user (User $user) + public function user ($name) { + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + if (!$actor) + return response ()->json (["error" => "Actor not found"], 404)->header ("Content-Type", "application/activity+json"); + if (str_contains (request ()->header ("Accept"), "text/html")) { - return redirect (route ("users.show", ["user_name" => $user->name])); + if ($actor->blog_id) { + return redirect (route ("blogs.show", ["blog" => $actor->preferredUsername])); + } + + return redirect (route ("users.show", ["user_name" => $actor->preferredUsername])); } - $actor = $user->actor ()->get (); - $response = Actor::build_response ($actor->first ()); + $response = Actor::build_response ($actor); return response ()->json ($response)->header ("Content-Type", "application/activity+json"); } - public function followers (User $user) + public function followers ($name) { - $follower_ids = Follow::where ("object", $user->actor->id)->get (); + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + if (!$actor) + return response ()->json (["error" => "Actor not found"], 404)->header ("Content-Type", "application/activity+json"); + + $follower_ids = Follow::where ("object", $actor->id)->get (); $followers = Actor::whereIn ("id", $follower_ids->pluck ("actor")->toArray ()); $ordered_collection = new TypeOrderedCollection (); $ordered_collection->collection = $followers->get ()->pluck ("actor_id")->toArray (); - $ordered_collection->url = route ("ap.followers", $user->name); + $ordered_collection->url = route ("ap.followers", $actor->name); $ordered_collection->page_size = 10; if (request ()->has ("page")) { @@ -46,14 +57,18 @@ class APActorController extends Controller return response ()->json ($ordered_collection->build_response_main ())->header ("Content-Type", "application/activity+json"); } - public function following (User $user) + public function following ($name) { - $following_ids = Follow::where ("actor", $user->actor->id)->get (); + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + if (!$actor) + return response ()->json (["error" => "Actor not found"], 404)->header ("Content-Type", "application/activity+json"); + + $following_ids = Follow::where ("actor", $actor->id)->get (); $following = Actor::whereIn ("id", $following_ids->pluck ("object")->toArray ()); $ordered_collection = new TypeOrderedCollection (); $ordered_collection->collection = $following->get ()->pluck ("actor_id")->toArray (); - $ordered_collection->url = route ("ap.following", $user->name); + $ordered_collection->url = route ("ap.following", $actor->name); $ordered_collection->page_size = 10; if (request ()->has ("page")) { @@ -64,9 +79,13 @@ class APActorController extends Controller return response ()->json ($ordered_collection->build_response_main ())->header ("Content-Type", "application/activity+json"); } - public function featured (User $user) + public function featured ($name) { - $featured_ids = ProfilePin::where ("actor_id", $user->actor->id)->pluck ("note_id")->toArray (); + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + if (!$actor) + return response ()->json (["error" => "Actor not found"], 404)->header ("Content-Type", "application/activity+json"); + + $featured_ids = ProfilePin::where ("actor_id", $actor->id)->pluck ("note_id")->toArray (); $notes = Note::whereIn ("id", $featured_ids)->get (); $collection = []; @@ -77,7 +96,7 @@ class APActorController extends Controller $ordered_collection = new TypeOrderedCollection (); $ordered_collection->collection = $collection; - $ordered_collection->url = route ("ap.featured", $user->name); + $ordered_collection->url = route ("ap.featured", $actor->preferredUsername); $ordered_collection->page_size = 10; if (request ()->has ("page")) { diff --git a/app/Http/Controllers/AP/APInboxController.php b/app/Http/Controllers/AP/APInboxController.php index 0cdc084..2c7eae2 100644 --- a/app/Http/Controllers/AP/APInboxController.php +++ b/app/Http/Controllers/AP/APInboxController.php @@ -25,8 +25,12 @@ use App\Http\Controllers\Controller; class APInboxController extends Controller { - public function inbox (User $user) + public function inbox ($name) { + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + if (!$actor) + return response ()->json ([ "error" => "Actor not found" ], 404); + $request = request (); $type = $request->get ("type"); @@ -35,15 +39,15 @@ class APInboxController extends Controller switch ($type) { case "Follow": - $this->handle_follow ($user, $request->all ()); + $this->handle_follow ($actor, $request->all ()); break; case "Undo": - $this->handle_undo ($user, $request->all ()); + $this->handle_undo ($actor, $request->all ()); break; case "Like": - $this->handle_like ($user, $request->all ()); + $this->handle_like ($actor, $request->all ()); break; default: @@ -53,17 +57,17 @@ class APInboxController extends Controller } } - private function handle_follow (User $user, $activity) + private function handle_follow (Actor $actor, $activity) { ActivityFollowEvent::dispatch ($activity); } - public function handle_undo (User $user, $activity) + public function handle_undo (Actor $actor, $activity) { ActivityUndoEvent::dispatch ($activity); } - public function handle_like (User $user, $activity) + public function handle_like (Actor $actor, $activity) { ActivityLikeEvent::dispatch ($activity); } diff --git a/app/Http/Controllers/AP/APOutboxController.php b/app/Http/Controllers/AP/APOutboxController.php index 997a3c6..c7e5551 100644 --- a/app/Http/Controllers/AP/APOutboxController.php +++ b/app/Http/Controllers/AP/APOutboxController.php @@ -29,45 +29,47 @@ use Illuminate\Support\Facades\Storage; class APOutboxController extends Controller { - public function outbox (User $user, Request $request) + public function outbox ($name, Request $request) { + $actor = Actor::where ("preferredUsername", $name)->where ("user_id", "!=", null)->first (); + // TODO: check we are logged in and we are the logged in user switch ($request->get ("type")) { case "UpdateProfile": - return $this->handle_update_profile ($user); + return $this->handle_update_profile ($actor); break; case "UpdateNote": - return $this->handle_update_note ($user, $request); + return $this->handle_update_note ($actor, $request); break; case "DeleteNote": - return $this->handle_delete_note ($user, $request); + return $this->handle_delete_note ($actor, $request); break; case "Follow": - return $this->handle_follow ($user, $request->get ("object")); + return $this->handle_follow ($actor, $request->get ("object")); break; case "Unfollow": - return $this->handle_unfollow ($user, $request->get ("object")); + return $this->handle_unfollow ($actor, $request->get ("object")); break; case "Like": - return $this->handle_like ($user, $request->get ("object")); + return $this->handle_like ($actor, $request->get ("object")); break; case "Boost": - return $this->handle_boost ($user, $request->get ("object")); + return $this->handle_boost ($actor, $request->get ("object")); break; case "Pin": - return $this->handle_pin ($user, $request->get ("object")); + return $this->handle_pin ($actor, $request->get ("object")); break; case "Post": - return $this->handle_post ($user, $request); + return $this->handle_post ($actor, $request); break; default: @@ -77,9 +79,8 @@ class APOutboxController extends Controller } } - public function handle_update_profile (User $user) + public function handle_update_profile (Actor $actor) { - $actor = $user->actor ()->first (); $actor_response = TypeActor::build_response ($actor); $update_activity = TypeActivity::craft_update ($actor, $actor_response); @@ -87,10 +88,8 @@ class APOutboxController extends Controller return response ()->json ("success", 200); } - public function handle_update_note (User $user, $request) + public function handle_update_note (Actor $actor, $request) { - $actor = $user->actor ()->first (); - // first check if there are new attachments if ($request ["attachments"]) { @@ -135,9 +134,8 @@ class APOutboxController extends Controller return response ()->json ("success", 200); } - public function handle_delete_note (User $user, $request) + public function handle_delete_note (Actor $actor, $request) { - $actor = $user->actor ()->first (); $note = Note::where ("id", $request ["note"])->first (); if (!$note) return response ()->json ([ "error" => "note not found" ], 404); @@ -154,29 +152,29 @@ class APOutboxController extends Controller return response ()->json ("success", 200); } - public function handle_follow (User $user, string $object) + public function handle_follow (Actor $actor, string $object) { $object_actor = Actor::where ("actor_id", $object)->first (); if (!$object_actor) return response ()->json ([ "error" => "object not found" ], 404); - if ($user->actor ()->first ()->actor_id == $object_actor->actor_id) + if ($actor->actor_id == $object_actor->actor_id) return response ()->json ([ "error" => "cannot follow self" ], 400); // check we are not following already - $following_activity = Activity::where ("actor", $user->actor ()->first ()->actor_id) + $following_activity = Activity::where ("actor", $actor->actor_id) ->where ("object", '"' . str_replace ("/", "\/", $object_actor->actor_id) . '"') ->where ("type", "Follow") ->first (); if ($following_activity) return response ()->json ([ "error" => "already following" ], 400); - $follow_activity = TypeActivity::craft_follow ($user->actor ()->first (), $object_actor); - $response = TypeActivity::post_activity ($follow_activity, $user->actor ()->first (), $object_actor); + $follow_activity = TypeActivity::craft_follow ($actor, $object_actor); + $response = TypeActivity::post_activity ($follow_activity, $actor, $object_actor); $follow = Follow::create ([ "activity_id" => $follow_activity->id, - "actor" => $user->actor ()->first ()->id, + "actor" => $actor->id, "object" => $object_actor->id, ]); @@ -189,21 +187,21 @@ class APOutboxController extends Controller ]; } - public function handle_unfollow (User $user, string $object) + public function handle_unfollow (Actor $actor, string $object) { $object_actor = Actor::where ("actor_id", $object)->first (); if (!$object_actor) return response ()->json ([ "error" => "object not found" ], 404); - $follow_activity = Activity::where ("actor", $user->actor ()->first ()->actor_id) + $follow_activity = Activity::where ("actor", $actor->actor_id) ->where ("object", json_encode ($object_actor->actor_id, JSON_UNESCAPED_SLASHES)) ->where ("type", "Follow") ->first (); if (!$follow_activity) - return response ()->json ([ "error" => "no follow activity found. " . $user->actor ()->first ()->actor_id . " unfollowing " . $object_actor->actor_id ], 404); + return response ()->json ([ "error" => "no follow activity found. " . $actor->actor_id . " unfollowing " . $object_actor->actor_id ], 404); - $unfollow_activity = TypeActivity::craft_undo ($follow_activity, $user->actor ()->first ()); - $response = TypeActivity::post_activity ($unfollow_activity, $user->actor ()->first (), $object_actor); + $unfollow_activity = TypeActivity::craft_undo ($follow_activity, $actor); + $response = TypeActivity::post_activity ($unfollow_activity, $actor, $object_actor); // TODO: Check if it was successfully sent /* if (!$response || $response->getStatusCode () < 200 || $response->getStatusCode () >= 300) @@ -216,13 +214,12 @@ class APOutboxController extends Controller ]; } - public function handle_like (User $user, $request) + public function handle_like (Actor $actor, $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) { @@ -262,13 +259,12 @@ class APOutboxController extends Controller ]; } - public function handle_boost (User $user, $object) + public function handle_boost (Actor $actor, $object) { $object = Note::where ("note_id", $object)->first (); if (!$object) return response ()->json ([ "error" => "object not found" ], 404); - $actor = $user->actor ()->first (); $already_boosted = $actor->boosted_note ($object); if ($already_boosted) { @@ -302,13 +298,12 @@ class APOutboxController extends Controller ]; } - public function handle_pin (User $user, $object) + public function handle_pin (Actor $actor, $object) { $object = Note::where ("note_id", $object)->first (); if (!$object) return response ()->json ([ "error" => "object not found" ], 404); - $actor = $user->actor ()->first (); $already_pinned = $object->is_pinned ($actor); if ($already_pinned) { @@ -342,9 +337,8 @@ class APOutboxController extends Controller ]; } - public function handle_post (User $user, $request) + public function handle_post (Actor $actor, $request) { - $actor = $user->actor ()->first (); $note = TypeNote::craft_from_outbox ($actor, $request); if (isset ($request ["attachments"])) @@ -417,6 +411,18 @@ class APOutboxController extends Controller } $note->visibility = $request ["visibility"]; + + // if the parent note is not public, responses shouldn't be either + if ($request ["inReplyTo"]) + { + $parent_note = TypeNote::note_exists($request ["inReplyTo"]); + if ($parent_note) + { + $note->to = $parent_note->to; + $note->cc = $parent_note->cc; + $note->visibility = $parent_note->visibility; + } + } $note->save (); $create_activity = TypeActivity::craft_create ($actor, $note); diff --git a/app/Http/Controllers/AP/APWebfingerController.php b/app/Http/Controllers/AP/APWebfingerController.php index a6a261c..38d885d 100644 --- a/app/Http/Controllers/AP/APWebfingerController.php +++ b/app/Http/Controllers/AP/APWebfingerController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\AP; use App\Models\User; +use App\Models\Blog; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -32,7 +33,9 @@ class APWebfingerController extends Controller $user = $user[0]; $actual_user = User::where ("name", $user)->first (); if (!isset ($actual_user)) { - return response ()->json ([ "error" => "user not found" ], 404); + $actual_user = Blog::where ("slug", $user)->first (); + if (!$actual_user) + return response ()->json ([ "error" => "user not found" ], 404); } $webfinger = [ diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php new file mode 100644 index 0000000..e438da0 --- /dev/null +++ b/app/Http/Controllers/BlogController.php @@ -0,0 +1,94 @@ +check ()) + $user = auth ()->user (); + + $blogs = Blog::orderBy ("created_at", "desc")->paginate (10); + + return view ("blogs", compact ("user", "blogs", "categories")); + } + + public function create () + { + $categories = BlogCategory::all (); + + return view ("blogs.create", compact ("categories")); + } + + public function store (Request $request) + { + if (!auth ()->check ()) + return redirect ()->route ("login")->with ("error", "You must be logged in to create a blog."); + + $request->validate ([ + "name" => "required|unique:users|unique:blogs", + "description" => "required", + "icon" => "required|image|max:4096", + "category" => "required" + ]); + + $user = auth ()->user (); + + $category = BlogCategory::find ($request->category); + if (!$category) + return redirect ()->route ("blogs.create")->with ("error", "Invalid category selected."); + + $icon = null; + $fname = $user->id . "-" . uniqid(); + if ($request->icon) + { + $manager = new ImageManager (new Driver ()); + $image = $manager->read ($request->file ("icon")); + $image_data = $image->cover (256, 256)->toJpeg (); + Storage::disk ("public")->put ("blog_icons/" . $fname . ".jpg", $image_data); + } + + $blog = Blog::create ([ + "name" => $request ["name"], + "slug" => Str::slug ($request ["name"]), + "description" => Str::markdown($request ["description"]), + "icon" => $fname . ".jpg", + "user_id" => $user->id, + "blog_category_id" => $category->id + ]); + + BlogCreatedEvent::dispatch ($blog, $user); + + return redirect ()->route ("blogs.show", [ 'blog' => $blog->slug ])->with ("success", "Blog created successfully!"); + } + + public function show (Blog $blog) + { + $notes = PaginationHelper::paginate ($blog->notes ()->orderBy ("created_at", "desc")->get (), 10); + + return view ("blogs.show", compact ("blog", "notes")); + } + + public function new_entry (Blog $blog) + { + return view ("blogs.new_entry", compact ("blog")); + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index fb9988b..5b32aeb 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -9,6 +9,7 @@ use App\Models\User; use App\Models\Actor; use App\Models\Note; use App\Models\Hashtag; +use App\Models\BlogCategory; use App\Helpers\PaginationHelper; diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index fe2a013..425e667 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -106,8 +106,7 @@ class PostController extends Controller if (!auth ()->check ()) return back ()->with ("error", "You need to be logged in to pin a post."); - $user = auth ()->user (); - $actor = $user->actor ()->first (); + $actor = $note->get_actor ()->first (); $response = ActionsPost::pin_post ($actor, $note); diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index da5f4b6..224924a 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -11,6 +11,7 @@ use Intervention\Image\Drivers\Gd\Driver; use App\Models\User; use App\Models\Actor; use App\Models\Note; +use App\Models\Blog; use App\Actions\ActionsUser; use App\Helpers\PaginationHelper; @@ -168,8 +169,9 @@ class ProfileController extends Controller $ids = $user->mutual_friends (); if (request ()->get ("query")) { + $query = request ()->get ("query"); $friends = Actor::whereIn ("actor_id", $ids) - ->where ("preferredUsername", "like", "%" . request ()->get ("query") . "%") + ->where ("preferredUsername", "like", "%" . $query . "%") ->get (); } else @@ -231,4 +233,20 @@ class ProfileController extends Controller return view ("users.notifications", compact ("user", "notifications", "processed_notifications", "unread_notifications")); } + + public function blogs ($user_name) + { + if (str_starts_with ($user_name, "@")) + { + return redirect ()->route ("users.show", [ "user_name" => $user_name ]); + } + + $user = User::where ("name", $user_name)->first (); + if (!$user) + return redirect ()->route ("home"); + + $blogs = Blog::where ("user_id", $user->id)->orderBy ("created_at", "desc")->get (); + + return view ("users.blogs", compact ("user", "blogs")); + } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 225f6de..a9c234d 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -26,7 +26,7 @@ class UserController extends Controller public function do_signup (Request $request) { $incoming_fields = $request->validate ([ - "name" => "required|alpha_dash", + "name" => "required|alpha_dash|unique:users|unique:blogs", "email" => "required|email|unique:users", "password" => "required|confirmed" ]); diff --git a/app/Listeners/BlogCreatedListener.php b/app/Listeners/BlogCreatedListener.php new file mode 100644 index 0000000..9faaa73 --- /dev/null +++ b/app/Listeners/BlogCreatedListener.php @@ -0,0 +1,36 @@ +create_from_blog ($event->blog); + $actor->blog_id = $event->blog->id; + $actor->user_id = $event->user->id; + $actor->save (); + + $event->blog->actor_id = $actor->id; + $event->blog->save (); + } +} diff --git a/app/Models/Actor.php b/app/Models/Actor.php index 37fcd31..391fa15 100644 --- a/app/Models/Actor.php +++ b/app/Models/Actor.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Models\User; use App\Models\Announcement; use App\Models\Note; +use App\Models\Blog; use App\Helpers\PaginationHelper; @@ -19,6 +20,7 @@ class Actor extends Model "type", "actor_id", + "blog_id", "local_actor_id", "following", @@ -57,6 +59,11 @@ class Actor extends Model return $this->belongsTo (User::class); } + public function blog () + { + return $this->belongsTo (Blog::class); + } + public function profile_attachment () { return $this->hasMany (ProfileAttachment::class); @@ -84,6 +91,12 @@ class Actor extends Model return $this->create ($data); } + public function create_from_blog (Blog $blog) + { + $data = TypeActor::create_from_blog ($blog); + return $this->create ($data); + } + public static function build_response (Actor $actor) { return TypeActor::build_response ($actor); diff --git a/app/Models/Blog.php b/app/Models/Blog.php new file mode 100644 index 0000000..b57c64a --- /dev/null +++ b/app/Models/Blog.php @@ -0,0 +1,38 @@ +belongsTo (User::class); + } + + public function actor () + { + return $this->belongsTo (Actor::class); + } + + public function notes () + { + return $this->hasMany (Note::class, "actor_id", "actor_id"); + } + + public function pinned_notes () + { + return $this->hasMany (ProfilePin::class, "actor_id", "actor_id"); + } +} diff --git a/app/Models/BlogCategory.php b/app/Models/BlogCategory.php new file mode 100644 index 0000000..edf5640 --- /dev/null +++ b/app/Models/BlogCategory.php @@ -0,0 +1,13 @@ +belongsTo (Activity::class); + } + + public function note () + { + return $this->belongsTo (Note::class); + } + + public function actor () + { + return $this->belongsTo (Actor::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 7b9947d..0c1e85a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -99,6 +99,11 @@ class User extends Authenticatable return Cache::has ("user-online-" . $this->id); } + public function blogs () + { + return $this->hasMany (Blog::class); + } + public function mutual_friends () { $followers = Follow::where ("actor", $this->actor->id)->pluck ("object")->toArray (); diff --git a/app/Types/TypeActor.php b/app/Types/TypeActor.php index 999e562..fd95332 100644 --- a/app/Types/TypeActor.php +++ b/app/Types/TypeActor.php @@ -3,6 +3,7 @@ namespace App\Types; use App\Models\User; +use App\Models\Blog; use App\Models\Actor; use App\Models\ProfileAttachment; use App\Models\Instance; @@ -62,6 +63,40 @@ class TypeActor { ]; } + public static function create_from_blog (Blog $blog) + { + $keys = TypeActor::gen_keys (); + $app_url = env ("APP_URL"); + + return [ + "blog_id" => $blog->id, + + "type" => "Person", + "actor_id" => $app_url . "/ap/v1/user/" . $blog->slug, + + "inbox" => $app_url . "/ap/v1/user/" . $blog->slug . "/inbox", + "outbox" => $app_url . "/ap/v1/user/" . $blog->slug . "/outbox", + + "following" => $app_url . "/ap/v1/user/" . $blog->slug . "/following", + "followers" => $app_url . "/ap/v1/user/" . $blog->slug . "/followers", + + "liked" => $app_url . "/ap/v1/user/" . $blog->slug . "/liked", + "featured" => $app_url . "/ap/v1/user/" . $blog->slug . "/collections/featured", + "featured_tags" => $app_url . "/ap/v1/user/" . $blog->slug . "/collections/featured/tags", + + "sharedInbox" => $app_url . "/ap/v1/inbox", + + "preferredUsername" => $blog->slug, + "name" => $blog->name, + "summary" => $blog->description, + + "icon" => $app_url . "/storage/blog_icons/" . $blog->icon, + + "public_key" => $keys["public_key"]["key"], + "private_key" => $keys["private_key"] + ]; + } + public static function build_response (Actor $actor) { $response = [ @@ -122,7 +157,7 @@ class TypeActor { ] ]; - if ($actor->user) + if ($actor->user && !$actor->blog_id) { // appent to @context $response ["@context"][] = [ diff --git a/database/migrations/2025_01_12_225005_create_blogs_table.php b/database/migrations/2025_01_12_225005_create_blogs_table.php new file mode 100644 index 0000000..32055fe --- /dev/null +++ b/database/migrations/2025_01_12_225005_create_blogs_table.php @@ -0,0 +1,39 @@ +id(); + + $table->string ("name")->unique (); + $table->string ("slug")->unique (); + + $table->text ("description")->nullable (); + + $table->string ("icon")->nullable (); + + $table->foreignId ("user_id")->nullable ()->constrained ()->onDelete ("cascade"); + $table->foreignId ("actor_id")->nullable ()->constrained ()->onDelete ("cascade"); + $table->foreignId ("blog_category_id")->nullable ()->constrained (); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blogs'); + } +}; diff --git a/database/migrations/2025_01_12_225655_create_blog_categories_table.php b/database/migrations/2025_01_12_225655_create_blog_categories_table.php new file mode 100644 index 0000000..b7d9e7f --- /dev/null +++ b/database/migrations/2025_01_12_225655_create_blog_categories_table.php @@ -0,0 +1,31 @@ +id(); + + $table->string ("name")->unique (); + $table->string ("slug")->unique (); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_categories'); + } +}; diff --git a/database/migrations/2025_01_12_232530_add_fields_to_actors_table.php b/database/migrations/2025_01_12_232530_add_fields_to_actors_table.php new file mode 100644 index 0000000..6758828 --- /dev/null +++ b/database/migrations/2025_01_12_232530_add_fields_to_actors_table.php @@ -0,0 +1,29 @@ +foreignId ("blog_id")->nullable ()->after ("user_id")->constrained ()->onDelete ("cascade"); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('actors', function (Blueprint $table) { + $table->dropForeign (["blog_id"]); + $table->dropColumn ("blog_id"); + }); + } +}; diff --git a/database/seeders/BlogCategorySeeder.php b/database/seeders/BlogCategorySeeder.php new file mode 100644 index 0000000..19ab2aa --- /dev/null +++ b/database/seeders/BlogCategorySeeder.php @@ -0,0 +1,95 @@ + "Art", + "slug" => "art" + ], + [ + "name" => "Automotive", + "slug" => "automotive" + ], + [ + "name" => "Fashion", + "slug" => "fashion" + ], + [ + "name" => "Financial", + "slug" => "financial" + ], + [ + "name" => "Food", + "slug" => "food" + ], + [ + "name" => "Games", + "slug" => "games" + ], + [ + "name" => "Life", + "slug" => "life" + ], + [ + "name" => "Literature", + "slug" => "literature" + ], + [ + "name" => "Math & Science", + "slug" => "math-science" + ], + [ + "name" => "Movies & TV", + "slug" => "movies-tv" + ], + [ + "name" => "Music", + "slug" => "music" + ], + [ + "name" => "Paranormal", + "slug" => "paranormal" + ], + [ + "name" => "Politics", + "slug" => "politics" + ], + [ + "name" => "Humanity", + "slug" => "humanity" + ], + [ + "name" => "Romance", + "slug" => "romance" + ], + [ + "name" => "Sports", + "slug" => "sports" + ], + [ + "name" => "Technology", + "slug" => "technology" + ], + [ + "name" => "Travel", + "slug" => "travel" + ] + ]; + + foreach ($categories as $category) { + DB::table("blog_categories")->insert($category); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..43eed67 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,9 +15,8 @@ class DatabaseSeeder extends Seeder { // User::factory(10)->create(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call ([ + BlogCategorySeeder::class ]); } } diff --git a/resources/views/blogs.blade.php b/resources/views/blogs.blade.php new file mode 100644 index 0000000..4fe7d88 --- /dev/null +++ b/resources/views/blogs.blade.php @@ -0,0 +1,85 @@ +@extends ("partials.layout") + +@section ("title", "Blogs") + +@section ("content") +
+
+
+ View: + + + + Categories: + +
+
+ +
+

Blogs

+ + @auth +
+

+ [ + Create a blog + ] +

+

+ [ + View your blogs + ] +

+
+ @endauth + +
+ +

Latest Blogs

+
+ @foreach ($blogs as $blog) +
+

+ + — by {{ $blog->user->name }} + — {{ count ($blog->notes) }} Posts +

+ +
+

+ + {{ $blog->name }} + +

+ +

+ {!! $blog->description !!} +

+ + + » Read more + +
+
+ @endforeach + + {{ $blogs->links ("pagination::default") }} +
+
+
+@endsection diff --git a/resources/views/blogs/create.blade.php b/resources/views/blogs/create.blade.php new file mode 100644 index 0000000..45509ef --- /dev/null +++ b/resources/views/blogs/create.blade.php @@ -0,0 +1,52 @@ +@extends ("partials.layout") + +@section ("title", "Create a new blog") + +@section ("content") +
+
+
+

You can use markdown in the description of your blog!

+
+
+ +
+

Create Blog

+
+ +
+ @csrf + + + + @error("name") +

{{ $message }}

+ @enderror + + + + @error("description") +

{{ $message }}

+ @enderror + + + + @error("icon") +

{{ $message }}

+ @enderror + +
+ + + +
+ +
+
+
+
+@endsection diff --git a/resources/views/blogs/new_entry.blade.php b/resources/views/blogs/new_entry.blade.php new file mode 100644 index 0000000..515e31c --- /dev/null +++ b/resources/views/blogs/new_entry.blade.php @@ -0,0 +1,56 @@ +@extends ("partials.layout") + +@section ("title", "Create New Entry") + +@section ("content") +
+
+
+

You can use Markdown in the content of your entry!

+
+
+ +
+

New Entry

+
+ +
+ @csrf + + + + + + @error("summary") +

{{ $message }}

+ @enderror + +
+ + + Markdown is supported + @error("content") +

{{ $message }}

+ @enderror +
+ +
+
+ @error("files.*") +

{{ $message }}

+ @enderror +
+ + + +

+ + +
+
+
+@endsection diff --git a/resources/views/blogs/show.blade.php b/resources/views/blogs/show.blade.php new file mode 100644 index 0000000..2ddcf9c --- /dev/null +++ b/resources/views/blogs/show.blade.php @@ -0,0 +1,88 @@ +@extends ("partials.layout") + +@section ("title", $blog->name) + +@section ("content") +
+
+

+ {{ $blog->name }} +

+ +
+
+ {{ $blog->name }} +
+ +
+ @if ($blog->user->is_online ()) +

+ ONLINE! +

+ @endif +
+
+ +
+

Mood: {{ $blog->user->mood }}

+
+

+ View my: Profile +

+
+ +
+

+ + Federation Handle: + +

+

@php echo "@" . $blog->slug . "@" . explode ("/", env ("APP_URL"))[2] @endphp

+
+ + +
+ +
+
+

+ {{ $blog->name }}'s Blog Entries +

+ +
+

+ [ + + New Entry + + ] +

+
+ +
+ +
+

Pinned

+ + @foreach ($blog->pinned_notes as $note) + + @endforeach + +
+ + @foreach ($notes as $note) + + @endforeach + + {{ $notes->links () }} +
+
+
+
+@endsection diff --git a/resources/views/components/blog_entry_block.blade.php b/resources/views/components/blog_entry_block.blade.php new file mode 100644 index 0000000..7839690 --- /dev/null +++ b/resources/views/components/blog_entry_block.blade.php @@ -0,0 +1,20 @@ +
+

+ +

+ +
+

+ + {{ $note->summary }} + +

+

+ + » View Blog Entry + +

+
+
diff --git a/resources/views/components/comment_block.blade.php b/resources/views/components/comment_block.blade.php index 85c6ff2..1cc4399 100644 --- a/resources/views/components/comment_block.blade.php +++ b/resources/views/components/comment_block.blade.php @@ -17,7 +17,9 @@ if (!$actor) return; } -if ($actor->user_id) +if ($actor->blog_id) + $actor_url = route ('blogs.show', [ 'blog' => $actor->blog->slug ]); +else if ($actor->user_id) $actor_url = route ('users.show', [ 'user_name' => $actor->user->name ]); else $actor_url = route ('users.show', [ 'user_name' => $actor->local_actor_id ]); @@ -35,11 +37,7 @@ if (!$display_post->can_view ())

- @if ($actor->user) - - @else - - @endif +

diff --git a/resources/views/components/create_note.blade.php b/resources/views/components/create_note.blade.php index 9b6468a..73a31f0 100644 --- a/resources/views/components/create_note.blade.php +++ b/resources/views/components/create_note.blade.php @@ -13,14 +13,14 @@ Markdown is supported

-

+

Visibility: -

+
@error ("content") diff --git a/resources/views/home_loggedin.blade.php b/resources/views/home_loggedin.blade.php index cc2b7b2..1d50047 100644 --- a/resources/views/home_loggedin.blade.php +++ b/resources/views/home_loggedin.blade.php @@ -25,7 +25,7 @@ View My Profile | - Blog + Blog | Bulletins | diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php index 94acfdb..fe166c0 100644 --- a/resources/views/partials/header.blade.php +++ b/resources/views/partials/header.blade.php @@ -47,7 +47,11 @@
  • -  Blog + @auth +  Blog + @else +   Blog + @endauth
  • diff --git a/resources/views/posts/show.blade.php b/resources/views/posts/show.blade.php index e708bfc..f688ca2 100644 --- a/resources/views/posts/show.blade.php +++ b/resources/views/posts/show.blade.php @@ -1,3 +1,20 @@ +@php + $user_url = null; + + if ($actor->blog_id) + { + $user_url = route ('blogs.show', [ 'blog' => $actor->blog->slug ]); + } + else if ($actor->user_id) + { + $user_url = route ('users.show', [ 'user_name' => $actor->user->name ]); + } + else + { + $user_url = route ('users.show', [ 'user_name' => $actor->local_actor_id ]); + } +@endphp + @extends("partials.layout") @section ("title", "View Post") @@ -8,14 +25,14 @@
    - +
    diff --git a/resources/views/users/blogs.blade.php b/resources/views/users/blogs.blade.php new file mode 100644 index 0000000..3aa1226 --- /dev/null +++ b/resources/views/users/blogs.blade.php @@ -0,0 +1,42 @@ +@extends ("partials.layout") + +@section ("title", $user->name . "'s Blogs") + +@section ("content") +
    +

    {{ $user->name }}'s Blogs

    +

    + « Back to profile +

    + +
    + +
    + @foreach ($blogs as $blog) +
    +

    + + — by {{ $blog->user->name }} + — {{ count ($blog->notes) }} Posts +

    + +
    +

    + + {{ $blog->name }} + +

    + +

    + {!! $blog->description !!} +

    + + + » Read more + +
    +
    + @endforeach +
    +
    +@endsection diff --git a/resources/views/users/notifications.blade.php b/resources/views/users/notifications.blade.php index 56afd32..92157d6 100644 --- a/resources/views/users/notifications.blade.php +++ b/resources/views/users/notifications.blade.php @@ -39,7 +39,11 @@ @elseif ($notification ['type'] == 'Follow')

    Followed you

    @elseif ($notification ['type'] == 'Unfollow') -

    Unfollowed you

    + @if ($notification ["object"]->id == auth ()->user ()->id) +

    You unfollowed you

    + @else +

    Unfollowed {{ $notification ['object']->name }}

    + @endif @elseif ($notification ['type'] == 'Boost')

    Boosted this post

    @elseif ($notification ['type'] == 'Like') diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php index 84e7ff6..6338e4b 100644 --- a/resources/views/users/profile.blade.php +++ b/resources/views/users/profile.blade.php @@ -50,7 +50,7 @@
    @if ($user != null)

    Mood: {{ $user->mood }}

    -

    View my: Blog | Bulletins

    +

    View my: Blog | Bulletins

    @endif
    @@ -280,10 +280,9 @@ @if ($user != null)

    - {{ $user->name }}'s Latest Blog Entries [View Blog] + {{ $user->name }}'s Latest Blog Entries [View Blog]

    - There are no Blog Entries yet.

    @endif diff --git a/routes/api.php b/routes/api.php index c0ed3f8..648ce85 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,15 +19,16 @@ Route::get ("/.well-known/nodeinfo/2.1", [ APNodeInfoController::class, "nodeinf Route::prefix ("/ap/v1")->group (function () { // users - Route::post ("/user/{user:name}/inbox", [ APInboxController::class, "inbox" ])->name ("ap.inbox"); - Route::post ("/user/{user:name}/outbox", [ APOutboxController::class, "outbox" ])->name ("ap.outbox"); - Route::get ("/user/{user:name}/followers", [ APActorController::class, "followers" ])->name ("ap.followers"); - Route::get ("/user/{user:name}/following", [ APActorController::class, "following" ])->name ("ap.following"); - Route::get ("/user/{user:name}/collections/featured", [ APActorController::class, "featured" ])->name ("ap.featured"); - Route::get ("/user/{user:name}", [ APActorController::class, "user" ])->name ("ap.user"); + Route::post ("/user/{name}/inbox", [ APInboxController::class, "inbox" ])->name ("ap.inbox"); + Route::post ("/user/{name}/outbox", [ APOutboxController::class, "outbox" ])->name ("ap.outbox"); + Route::get ("/user/{name}/followers", [ APActorController::class, "followers" ])->name ("ap.followers"); + Route::get ("/user/{name}/following", [ APActorController::class, "following" ])->name ("ap.following"); + Route::get ("/user/{name}/collections/featured", [ APActorController::class, "featured" ])->name ("ap.featured"); + Route::get ("/user/{name}", [ APActorController::class, "user" ])->name ("ap.user"); // notes Route::get ("/note/{note:private_id}", [ APGeneralController::class, "note" ])->name ("ap.note"); + // instance Route::post ("/inbox", [ APInstanceInboxController::class, "inbox" ])->name ("ap.inbox"); }); diff --git a/routes/web.php b/routes/web.php index 2b2c1b4..457e74b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\PostController; use App\Http\Controllers\UserController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\UserActionController; +use App\Http\Controllers\BlogController; // auth related Route::get ("/auth/login", [ UserController::class, "login" ])->name ("login")->middleware ("guest"); @@ -28,6 +29,7 @@ Route::middleware ("update_online")->group (function () { Route::post ("/user/edit", [ ProfileController::class, "update" ])->middleware ("auth"); Route::get ("/user/notifications", [ ProfileController::class, "notifications" ])->name ("users.notifications")->middleware ("auth"); Route::get ("/user/{user_name}/friends", [ ProfileController::class, "friends" ])->name ("users.friends"); + Route::get ("/user/{user_name}/blogs", [ ProfileController::class, "blogs" ])->name ("users.blogs"); Route::get ("/user/{user_name}", [ ProfileController::class, "show" ])->name ("users.show"); // posts routes @@ -42,10 +44,17 @@ Route::middleware ("update_online")->group (function () { // other routes Route::get ("/browse", [ HomeController::class, "browse" ])->name ("browse"); Route::get ("/search", [ HomeController::class, "search" ])->name ("search"); - Route::get ("/tags/{tag}", [ HomeController::class, "tag" ])->name ("tags"); // TODO: This + Route::get ("/tags/{tag}", [ HomeController::class, "tag" ])->name ("tags"); 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"); + + // blog routes + Route::get ("/blogs/create", [ BlogController::class, "create" ])->name ("blogs.create")->middleware ("auth"); + Route::post ("/blogs/create", [ BlogController::class, "store" ])->middleware ("auth"); + Route::get ("/blogs/{blog:slug}/entry/new", [ BlogController::class, "new_entry" ])->name ("blogs.new_entry")->middleware("auth"); + Route::get ("/blogs/{blog:slug}", [ BlogController::class, "show" ])->name ("blogs.show"); + Route::get ("/blogs", [ BlogController::class, "index" ])->name ("blogs"); }); require __DIR__ . "/api.php";