Compare commits

...

10 Commits

Author SHA1 Message Date
ded4ca5a1b derp 2025-01-12 21:38:04 -05:00
81f0d8818f added the blogs feature 2025-01-12 21:35:10 -05:00
25f00b1dc7 Added support for custom notification sounds 2025-01-12 16:51:59 -05:00
518586b6ab added note visibility 2025-01-12 15:10:44 -05:00
daa8b3eaeb Refactored the Actor Inbox 2025-01-11 19:49:54 -05:00
d2a2e466ef added a very simple tag page 2025-01-11 19:16:26 -05:00
7e3b868a90 added a queue for posting activities 2025-01-11 15:29:26 -05:00
c0ad0c46d6 added the signup notification 2025-01-11 14:51:17 -05:00
dca3116d2a Created a notifications page 2025-01-11 14:20:19 -05:00
e31e7ff847 bug fix 2025-01-09 22:36:02 -05:00
65 changed files with 1685 additions and 248 deletions

View File

@ -164,7 +164,7 @@ server {
server_name ws.ourspace.lat;
root /var/www/html/ourspace/public;
location / {
location /app {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
@ -173,16 +173,11 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_pass http://0.0.0.0:8080;
}
location ~ ^/apps/(?<reverbid>[^/]+)/events$ { # variable reverbid
proxy_pass http://0.0.0.0:8080/apps/$reverbid/events;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8080;
}
}
```
@ -200,20 +195,36 @@ 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 two services to handle the jobs that OurSpace needs to run and another one to run Laravel Reverb. So run something `emacs /lib/systemd/system/ourspace-queue.service` and `emacs /lib/systemd/system/ourspace-ws.service` and add the following content:
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:
```ini
# /lib/systemd/system/ourspace-queue.service
[Unit]
Description=OurSpace queue worker
[Service]
User=www-data
Group=www-data
Restart=on-failure
ExecStart=/usr/bin/php /var/www/html/ourspace/artisan queue:work --daemon --env=production --queue=ap
[Install]
WantedBy=multi-user.target
```
```ini
# /lib/systemd/system/ourspace-notifications.service
[Unit]
Description=OurSpace notifications worker
[Service]
User=www-data
Group=www-data
@ -225,6 +236,7 @@ WantedBy=multi-user.target
```
```ini
# /lib/systemd/system/ourspace-ws.service
[Unit]
Description=OurSpace WebSockets Service
@ -238,8 +250,21 @@ ExecStart=/usr/bin/php /var/www/html/ourspace/artisan reverb:start
WantedBy=multi-user.target
```
Now reload the systemd daemon:
```bash
sudo systemctl daemon-reload
```
Finally, enable and start both services, and your OurSpace instance will be ready to be used!
Aditionally, if you want to start multiple instances of the queue workers (which is ideal) you can name the service like `/lib/systemd/system/ourspace-queue@.service` and you can enable and manage them like this:
```bash
sudo systemctl enable ourspace-queue\{1..6} # enables 6 services
sudo systemctl start ourspace-queue\{1..6} # starts 6 services
```
## TODO:
For a list of planned features and improvements, please refer to the [TODO](TODO.md) file.

View File

@ -2,7 +2,7 @@
- [-] Activitypub
- [x] Accounts
- [-] Posts
- [x] Posts
- [x] Local posts should be federated
- [x] Local posts should be deleted
- [x] Remote posts should be fetched
@ -18,10 +18,10 @@
- [x] Tags
- [x] Mentions
- [x] Local mentions
- [ ] Private post
- [x] Private post
- [x] Pinned Posts
- [x] Nodeinfo
- [ ] Notifications
- [x] Notifications
- [-] Social features
- [x] Profile

View File

@ -115,6 +115,10 @@ class ActionsPost
$processed = ActionsPost::process_content_and_attachments ($request);
$actor = null;
if ($request ["blog_id"])
$actor = Actor::where ("blog_id", $request ["blog_id"])->first ();
else
$actor = auth ()->user ()->actor ()->first ();
try {
@ -125,6 +129,7 @@ class ActionsPost
"summary" => $processed ["summary"],
"content" => $processed ["content"],
"attachments" => $processed ["attachments"],
"visibility" => $request ["visibility"],
"inReplyTo" => $processed ["inReplyTo"] ?? null,
"tags" => $processed ["tags"] ?? null,
"mentions" => $processed ["mentions"] ?? null

View File

@ -0,0 +1,26 @@
<?php
namespace App\Events\AP;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ActivityFollowEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $activity;
/**
* Create a new event instance.
*/
public function __construct($activity)
{
$this->activity = $activity;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Events\AP;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ActivityLikeEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $activity;
/**
* Create a new event instance.
*/
public function __construct($activity)
{
$this->activity = $activity;
}
}

View File

@ -23,8 +23,6 @@ class ActivityUndoEvent
use Dispatchable, InteractsWithSockets, SerializesModels;
public $activity;
public $actor;
public $object;
/**
* Create a new event instance.
@ -32,31 +30,5 @@ class ActivityUndoEvent
public function __construct($activity)
{
$this->activity = $activity;
$this->actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]);
$child_activity = $activity ["object"];
$child_activity_id = "";
if (is_array ($child_activity))
$child_activity_id = $child_activity ["id"];
else
$child_activity_id = $child_activity;
if (!TypeActivity::activity_exists ($child_activity_id))
return ["error" => "Activity not found",];
$child_activity = Activity::where ("activity_id", $child_activity_id)->first ();
$this->object = $child_activity;
switch ($this->object->type)
{
case "Follow":
$unfollowed_actor = Actor::where ("actor_id", $this->object->object)->first ();
UserUnfollowedEvent::dispatch ($this->object, $this->actor, $unfollowed_actor);
break;
}
$child_activity->delete ();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Events;
use App\Models\Blog;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class BlogCreatedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Blog $blog;
public User $user;
/**
* Create a new event instance.
*/
public function __construct(Blog $blog, User $user)
{
$this->blog = $blog;
$this->user = $user;
}
}

View File

@ -18,14 +18,14 @@ class NoteLikedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Activity $activity;
public Actor $actor;
public Note $note;
public $activity;
public $actor;
public $note;
/**
* Create a new event instance.
*/
public function __construct(Activity $activity, Actor $actor, Note $note)
public function __construct($activity, $actor, $note)
{
$this->activity = $activity;
$this->actor = $actor;

View File

@ -19,13 +19,13 @@ class NoteRepliedEvent
use Dispatchable, InteractsWithSockets, SerializesModels;
public $activity;
public Actor $actor;
public Note $object;
public $actor;
public $object;
/**
* Create a new event instance.
*/
public function __construct($activity, Actor $actor, Note $object)
public function __construct($activity, $actor, $object)
{
$this->activity = $activity;
$this->actor = $actor;

View File

@ -18,9 +18,9 @@ class UserFollowedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Activity $activity;
public Actor $actor;
public Actor $object;
public $activity;
public $actor;
public $object;
/**
* Create a new event instance.

View File

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserSignedUp
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
/**
* Create a new event instance.
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@ -17,14 +17,14 @@ class UserUnfollowedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public Activity $activity;
public Actor $actor;
public Actor $object;
public $activity;
public $actor;
public $object;
/**
* Create a new event instance.
*/
public function __construct(Activity $activity, Actor $actor, Actor $object)
public function __construct($activity, $actor, $object)
{
$this->activity = $activity;
$this->actor = $actor;

View File

@ -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]));
}
$actor = $user->actor ()->get ();
$response = Actor::build_response ($actor->first ());
return redirect (route ("users.show", ["user_name" => $actor->preferredUsername]));
}
$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")) {

View File

@ -13,10 +13,11 @@ use App\Models\Like;
use App\Types\TypeActor;
use App\Types\TypeActivity;
use App\Events\UserFollowedEvent;
use App\Events\NoteLikedEvent;
use App\Events\AP\ActivityUndoEvent;
use App\Events\AP\ActivityFollowEvent;
use App\Events\AP\ActivityLikeEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@ -24,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");
@ -34,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:
@ -52,71 +57,18 @@ class APInboxController extends Controller
}
}
private function handle_follow (User $user, $activity)
private function handle_follow (Actor $actor, $activity)
{
if (TypeActivity::activity_exists ($activity["id"]))
return response ()->json (["error" => "Activity already exists",], 409);
ActivityFollowEvent::dispatch ($activity);
}
$actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]);
$target = TypeActor::actor_get_local ($activity ["object"]);
if (!$target || !$target->user)
return response ()->json (["error" => "Target not found",], 404);
// check follow doesn't exist
$follow_exists = Follow::where ("actor", $actor->id)
->where ("object", $target->id)
->first ();
if ($follow_exists)
return response ()->json (["error" => "Follow already exists",], 409);
$activity ["activity_id"] = $activity ["id"];
$act = Activity::create ($activity);
UserFollowedEvent::dispatch ($act, $actor, $target);
// TODO: Users should be able to manually check this
$accept_activity = TypeActivity::craft_accept ($act);
$response = TypeActivity::post_activity ($accept_activity, $target, $actor);
if (!$response)
public function handle_undo (Actor $actor, $activity)
{
return response ()->json ([
"error" => "Error posting activity",
], 500);
}
ActivityUndoEvent::dispatch ($activity);
}
public function handle_undo (User $user, $activity)
public function handle_like (Actor $actor, $activity)
{
ActivityUndoEvent::dispatch ($activity, $activity);
return response ()->json (ActionsActivity::activity_undo ($activity));
}
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 ();
NoteLikedEvent::dispatch ($act, $actor, $note);
return response ()->json (["success" => "Like created",], 200);
ActivityLikeEvent::dispatch ($activity);
}
}

View File

@ -99,7 +99,7 @@ class APInstanceInboxController extends Controller
$note_actor = $note_exists->get_actor ()->first ();
if ($note_actor->user)
{
$note_actor->user->notify (new UserNotification("Boost", $announcement_actor, $note_exists));
$note_actor->user->notify (new UserNotification("Boost", $announcement_actor->id, $note_exists->id));
}
return response ()->json (["status" => "ok"]);

View File

@ -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"]))
@ -370,6 +364,7 @@ class APOutboxController extends Controller
}
}
$mentions = [];
if (isset ($request ["mentions"]))
{
foreach ($request ["mentions"] as $mention)
@ -387,10 +382,54 @@ class APOutboxController extends Controller
"note_id" => $note->id,
"actor_id" => $object->id
]);
$mentions[] = $object->actor_id;
}
}
if ($request ["visibility"] == "public")
{
$note->to = [
"https://www.w3.org/ns/activitystreams#Public"
];
$note->cc = [
$actor->followers
];
}
else if ($request ["visibility"] == "followers")
{
// TODO: Boosting should be disabled
$note->to = [
$actor->followers
];
$note->cc = [];
}
else if ($request ["visibility"] == "private")
{
// TODO: Boosting should be disabled
$note->to = $mentions;
}
$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);
$create_activity->to = $note->to;
$create_activity->cc = $note->cc;
$note->activity_id = $create_activity->id;
$note->save ();

View File

@ -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,6 +33,8 @@ class APWebfingerController extends Controller
$user = $user[0];
$actual_user = User::where ("name", $user)->first ();
if (!isset ($actual_user)) {
$actual_user = Blog::where ("slug", $user)->first ();
if (!$actual_user)
return response ()->json ([ "error" => "user not found" ], 404);
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers;
use App\Models\Blog;
use App\Events\BlogCreatedEvent;
use App\Models\BlogCategory;
use App\Helpers\PaginationHelper;
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class BlogController extends Controller
{
public function index ()
{
$categories = BlogCategory::all ();
$user = null;
if (auth ()->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"));
}
}

View File

@ -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;
@ -42,21 +43,30 @@ class HomeController extends Controller
if (request ()->get ("posts") == "latest")
{
$notes = Note::latest ()->where ("in_reply_to", null)->take (10)->get ();
$notes = Note::latest ();
}
else
{
$notes = Note::withCount ([ "get_likes" => function ($query) {
$query->where ("created_at", ">=", now ()->subDay ());
}])->where ("in_reply_to", null)->orderBy ("get_likes_count", "desc")->take (8)->get ();
}])->where ("in_reply_to", null)->orderBy ("get_likes_count", "desc");
}
$notes = $notes->paginate (10);
return view ("browse", compact ("users", "popular_hashtags", "notes"));
}
public function tag ($tag)
{
dd ($tag);
$tag_name = "#" . $tag;
$hashtag = Hashtag::where ("name", $tag_name)->first ();
if (!$hashtag)
return redirect ()->route ("browse");
$posts = $hashtag->get_notes ()->paginate (20);
return view ("posts.tag", compact ("hashtag", "posts"));
}
public function search ()

View File

@ -18,6 +18,11 @@ class PostController extends Controller
{
$actor = $note->get_actor ()->first ();
if (!$actor)
return redirect ()->back ();
if (!$note->can_view ())
return redirect ()->back ()->with ("error", "You are not allowed to view this post.");
return view ("posts.show", compact ("note", "actor"));
}
@ -101,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);

View File

@ -10,8 +10,11 @@ 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;
class ProfileController extends Controller
{
@ -55,7 +58,8 @@ class ProfileController extends Controller
$incoming_fields = $request->validate ([
"avatar" => "image|max:4096",
"song" => "file|mimes:audio/mpeg,mp3|max:1024",
"song" => "file|mimes:audio/mpeg,mp3|max:4096",
"notification_sound" => "file|mimes:audio/mpeg,mp3|max:1024",
"bio" => "sometimes|nullable|string",
"about_you" => "sometimes|nullable|string",
"status" => "sometimes|nullable|string",
@ -74,9 +78,11 @@ class ProfileController extends Controller
$changing_avatar = false;
$changing_song = false;
$changing_notification_sound = false;
$old_avatar = null;
$old_song = null;
$old_notification_sound = null;
if (isset ($incoming_fields["avatar"]) && !empty ($incoming_fields["avatar"]))
{
$manager = new ImageManager (new Driver ());
@ -102,6 +108,16 @@ class ProfileController extends Controller
$changing_song = true;
}
if (isset ($incoming_fields ["notification_sound"]) && !empty ($incoming_fields["notification_sound"]))
{
$file = $request->file ("notification_sound");
Storage::disk ("public")->put ("notification_sounds/" . $fname . ".mp3", file_get_contents ($file));
$old_notification_sound = "notification_sounds/" . $user->notification_sound;
$user->notification_sound = $fname . ".mp3";
$changing_notification_sound = true;
}
$user->bio = $incoming_fields["bio"];
$user->about_you = $incoming_fields["about_you"];
$user->status = $incoming_fields["status"];
@ -126,6 +142,9 @@ class ProfileController extends Controller
if ($changing_song)
Storage::disk ("public")->delete (str_replace ("/storage/", "", $old_song));
if ($changing_notification_sound)
Storage::disk ("public")->delete (str_replace ("/storage/", "", $old_notification_sound));
$response = ActionsUser::update_profile ();
if (isset ($response["error"]))
return back ()->with ("error", "Error updating profile: " . $response["error"]);
@ -150,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
@ -161,4 +181,72 @@ class ProfileController extends Controller
return view ("users.friends", compact ("actor", "user", "friends"));
}
public function notifications ()
{
if (!auth ()->check ())
return redirect ()->route ("login");
$user = auth ()->user ();
$user_notifications = $user->notifications;
$notifications = PaginationHelper::paginate (collect ($user_notifications), 20);
$unread_notifications = $user->unreadNotifications->count ();
$processed_notifications = [];
foreach ($notifications as $notification)
{
$data = $notification->data;
$type = $data ['type'];
$actor = Actor::find ($data["actor"]);
$object = null;
switch ($type)
{
case "Follow":
case "Unfollow":
$object = Actor::find ($data["object"]);
break;
case "Signup":
$object = User::find ($data ["object"]);
break;
case "Like":
case "Reply":
case "Boost":
case "Mention":
$object = Note::find ($data["object"]);
break;
}
$processed_notifications[] = [
"id"=> $notification->id,
"type" => $type,
"actor" => $actor,
"object" => $object,
"created_at" => $notification->created_at,
"read_at" => $notification->read_at
];
$notification->markAsRead ();
}
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"));
}
}

View File

@ -32,7 +32,8 @@ class UserActionController extends Controller
$request->validate ([
"summary" => "nullable|string",
"content" => "required",
"files.*" => "max:4096"
"files.*" => "max:4096",
"visibility" => "required|in:public,private,followers",
]);
$response = ActionsPost::post_new ($request);

View File

@ -7,6 +7,8 @@ use App\Models\Actor;
use App\Actions\ActionsFriends;
use App\Events\UserSignedUp;
use Illuminate\Http\Request;
class UserController extends Controller
@ -24,20 +26,15 @@ 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"
]);
$user = User::create ($incoming_fields);
$actor = new Actor ();
$actor->create_from_user ($user);
auth ()->login ($user);
// create a friendship between the new user and the admin
$admin = User::where ("is_admin", 1)->first ();
if ($admin)
ActionsFriends::force_friendship ($user, $admin);
UserSignedUp::dispatch ($user);
auth ()->login ($user);
return redirect ()->route ("home")->with ("success", "You have successfuly signed up!");
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Listeners\AP;
use App\Models\Activity;
use App\Models\Follow;
use App\Types\TypeActivity;
use App\Types\TypeActor;
use App\Events\UserFollowedEvent;
use App\Events\AP\ActivityFollowEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ActivityFollowListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(ActivityFollowEvent $event): void
{
if (TypeActivity::activity_exists ($event->activity["id"]))
return;
$actor = TypeActor::actor_exists_or_obtain ($event->activity ["actor"]);
$target = TypeActor::actor_get_local ($event->activity ["object"]);
if (!$target || !$target->user)
return;
// check follow doesn't exist
$follow_exists = Follow::where ("actor", $actor->id)
->where ("object", $target->id)
->first ();
if ($follow_exists)
return;
$event->activity ["activity_id"] = $event->activity ["id"];
$act = Activity::create ($event->activity);
UserFollowedEvent::dispatch ($act, $actor, $target);
// TODO: Users should be able to manually check this
$accept_activity = TypeActivity::craft_accept ($act);
TypeActivity::post_activity ($accept_activity, $target, $actor);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Listeners\AP;
use App\Models\Note;
use App\Models\Activity;
use App\Types\TypeActivity;
use App\Types\TypeActor;
use App\Events\AP\ActivityLikeEvent;
use App\Events\NoteLikedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ActivityLikeListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(ActivityLikeEvent $event): void
{
$actor = TypeActor::actor_exists_or_obtain ($event->activity ["actor"]);
$note_id = $event->activity ["object"];
$note = Note::where ("note_id", $note_id)->first ();
if (!$note)
{
return;
}
// check like doesn't already exist
$like_exists = $actor->liked_note ($note);
if ($like_exists)
return;
$event->activity ["activity_id"] = $event->activity ["id"];
$activity_exists = TypeActivity::activity_exists ($event->activity ["id"]);
if (!$activity_exists)
$act = Activity::create ($event->activity);
else
$act = Activity::where ("activity_id", $event->activity ["id"])->first ();
NoteLikedEvent::dispatch ($act, $actor, $note);
return;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Listeners\AP;
use App\Models\Activity;
use App\Models\Actor;
use App\Types\TypeActor;
use App\Types\TypeActivity;
use App\Events\AP\ActivityUndoEvent;
use App\Events\UserUnfollowedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ActivityUndoListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(ActivityUndoEvent $event): void
{
$actor = TypeActor::actor_exists_or_obtain ($event->activity ["actor"]);
$child_activity = $event->activity ["object"];
$child_activity_id = "";
if (is_array ($child_activity))
$child_activity_id = $child_activity ["id"];
else
$child_activity_id = $child_activity;
if (!TypeActivity::activity_exists ($child_activity_id))
return;
$child_activity = Activity::where ("activity_id", $child_activity_id)->first ();
$object = $child_activity;
switch ($object->type)
{
case "Follow":
$unfollowed_actor = Actor::where ("actor_id", $object->object)->first ();
UserUnfollowedEvent::dispatch ($object, $actor, $unfollowed_actor);
break;
}
$child_activity->delete ();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Listeners;
use App\Models\Actor;
use App\Types\TypeActor;
use App\Events\BlogCreatedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class BlogCreatedListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(BlogCreatedEvent $event): void
{
$actor = new Actor ();
$actor = $actor->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 ();
}
}

View File

@ -29,12 +29,9 @@ class NoteRepliedListener
{
$note_actor = $event->object->get_actor ()->first ();
Log::info ("hi");
if (!$note_actor || !$note_actor->user)
return;
Log::info ("bai");
$note_actor->user->notify (new UserNotification(
"Reply",
$event->actor->id,

View File

@ -0,0 +1,48 @@
<?php
namespace App\Listeners;
use App\Models\User;
use App\Models\Actor;
use App\Actions\ActionsFriends;
use App\Events\UserSignedUp;
use App\Notifications\UserNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UserSignedUpListener
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(UserSignedUp $event): void
{
$actor = new Actor ();
$actor->create_from_user ($event->user);
// create a friendship between the new user and the admin
$admin = User::where ("is_admin", 1)->first ();
if ($admin)
{
ActionsFriends::force_friendship ($event->user, $admin);
$admin->notify (new UserNotification(
"Signup",
$actor->id,
$event->user->id
));
}
}
}

View File

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

38
app/Models/Blog.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Blog extends Model
{
protected $fillable = [
"name",
"slug",
"description",
"icon",
"user_id",
"actor_id",
"blog_category_id"
];
public function user ()
{
return $this->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");
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BlogCategory extends Model
{
protected $fillable = [
"name",
"slug"
];
}

View File

@ -11,6 +11,6 @@ class Hashtag extends Model
];
public function get_notes () {
return $this->belongsToMany(Note::class, 'note_hashtag');
return $this->belongsToMany(Note::class, 'note_hashtag')->orderBy('created_at', 'desc');
}
}

View File

@ -20,7 +20,8 @@ class Note extends Model
"content",
"tag",
"to",
"cc"
"cc",
"visibility"
];
protected $casts = [
@ -88,4 +89,34 @@ class Note extends Model
{
return ProfilePin::where ("actor_id", $actor->id)->where ("note_id", $this->id)->first ();
}
public function can_view (Actor $actor = null)
{
$final_actor = $actor;
$note_actor = $this->get_actor ()->first ();
if (!$final_actor && auth ()->check ())
{
$final_actor = auth ()->user ()->actor;
}
if ($this->visibility == "public")
{
return true;
}
else if ($this->visibility == "followers" && $final_actor)
{
return $final_actor->friends_with ($note_actor);
}
else if ($this->visibility == "private" && $final_actor)
{
if ($final_actor == $note_actor)
return true;
$mention_exists = NoteMention::where ("note_id", $this->id)->where ("actor_id", $final_actor->id)->first ();
if ($mention_exists)
return true;
}
return false;
}
}

View File

@ -11,4 +11,19 @@ class ProfilePin extends Model
"note_id",
"actor_id"
];
public function activity ()
{
return $this->belongsTo (Activity::class);
}
public function note ()
{
return $this->belongsTo (Note::class);
}
public function actor ()
{
return $this->belongsTo (Actor::class);
}
}

View File

@ -82,6 +82,13 @@ class User extends Authenticatable
});
}
protected function notificationSound () : Attribute
{
return Attribute::make (get: function ($value) {
return $value ? "/storage/notification_sounds/" . $value : "/resources/sounds/notification.mp3";
});
}
public function actor ()
{
return $this->hasOne (Actor::class);
@ -92,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 ();

View File

@ -4,10 +4,11 @@ namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class UserNotification extends Notification
class UserNotification extends Notification implements ShouldQueue
{
use Queueable;

View File

@ -16,7 +16,9 @@ class TypeActivity {
public static function craft_response (Activity $activity)
{
$crafted_activity = [
"@context" => "https://www.w3.org/ns/activitystreams",
"@context" => [
"https://www.w3.org/ns/activitystreams",
],
"id" => $activity->activity_id,
"type" => $activity->type,
"actor" => $activity->actor,
@ -237,7 +239,7 @@ class TypeActivity {
public static function post_activity (Activity $activity, Actor $source, $target, $should_sign = false)
{
PostActivityJob::dispatch ($activity, $source, $target, $should_sign);
PostActivityJob::dispatch ($activity, $source, $target, $should_sign)->onQueue ("ap");
return [
"success" => "activity posted"

View File

@ -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"][] = [
@ -209,6 +244,8 @@ class TypeActor {
// we need to save the model first
$actor->save ();
if (isset ($request ["attachment"]))
{
ProfileAttachment::where ("actor_id", $actor->id)->delete ();
foreach ($request ["attachment"] as $attachment)
{
@ -218,6 +255,7 @@ class TypeActor {
"content" => $attachment ["value"]
]);
}
}
$featured_items = TypeActor::actor_process_featured ($actor);
ProfilePin::where ("actor_id", $actor->id)->delete ();

View File

@ -86,11 +86,6 @@ class TypeNote
"attributedTo" => $actor->actor_id,
"content" => $request ["content"] ?? 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);
@ -104,6 +99,7 @@ class TypeNote
if ($activity)
$note->activity_id = $activity->id;
$note_actor = $actor;
if ($actor)
$note->actor_id = $actor->id;
else
@ -119,6 +115,7 @@ class TypeNote
}
$note->actor_id = $actor->id;
$note_actor = $actor;
}
$note->note_id = $request["id"] ?? null;
@ -129,6 +126,8 @@ class TypeNote
$note->content = $request["content"] ?? null;
$note->tag = $request["tag"] ?? null;
$note->created_at = $request["published"] ?? null;
$note->to = $request["to"] ?? null;
$note->cc = $request["cc"] ?? null;
$attachments = $note->attachments ()->get ();
foreach ($attachments as $attachment)
@ -222,8 +221,8 @@ class TypeNote
{
$actor_exists->user->notify (new UserNotification(
"Mention",
$actor,
$actor_exists
$actor->id,
$note->id
));
}
break;
@ -235,6 +234,20 @@ class TypeNote
{
// TODO: Handle replies
}
$note_to = $note->to;
if (in_array ("https://www.w3.org/ns/activitystreams#Public", $note_to))
{
$note->visibility = "public";
}
else if (in_array ($note_actor->followers, $note_to))
{
$note->visibility = "followers";
}
else
{
$note->visibility = "private";
}
}
public static function create_from_request ($request, Activity $activity, Actor $actor)

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->string ("visibility")->default ("public");
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropColumn ("visibility");
});
}
};

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blog_categories', function (Blueprint $table) {
$table->id();
$table->string ("name")->unique ();
$table->string ("slug")->unique ();
$table->timestamps();
});
Schema::create('blogs', function (Blueprint $table) {
$table->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');
Schema::dropIfExists('blog_categories');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('actors', function (Blueprint $table) {
$table->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");
});
}
};

View File

@ -0,0 +1,95 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
class BlogCategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$categories = [
[
"name" => "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);
}
}
}

View File

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

View File

@ -0,0 +1,85 @@
@extends ("partials.layout")
@section ("title", "Blogs")
@section ("content")
<div class="row blog-category">
<div class="col w-20 left">
<div class="category-list">
<b>View:</b>
<ul>
<li>
<a href="{{ route ('blogs') }}">
<img loading="lazy" src="/resources/icons/clock.png" class="icon">
<b>Recent Blogs</b>
</a>
</li>
{{-- TODO: Top entries and blogs I'm following --}}
</ul>
<b>Categories:</b>
<ul>
@foreach ($categories as $category)
<li>
<a href="#">{{ $category->name }}</a>
</li>
@endforeach
</ul>
</div>
</div>
<div class="col right">
<h1>Blogs</h1>
@auth
<div class="blog-preview">
<h3>
[
<a href="{{ route ('blogs.create') }}">Create a blog</a>
]
</h3>
<h3>
[
<a href="#">View your blogs</a>
]
</h3>
</div>
@endauth
<hr>
<h3>Latest Blogs</h3>
<div class="blog-entries">
@foreach ($blogs as $blog)
<div class="entry">
<p class="publish-date">
<time class="ago">{{ $blog->created_at->diffForHumans () }}</time>
&mdash; by <a href="{{ route ('users.show', [ 'user_name' => $blog->user->name ]) }}">{{ $blog->user->name }}</a>
&mdash; <a>{{ count ($blog->notes) }} Posts</a>
</p>
<div class="inner">
<h3 class="title">
<a href="{{ route ('blogs.show', [ 'blog' => $blog ]) }}">
{{ $blog->name }}
</a>
</h3>
<p>
{!! $blog->description !!}
</p>
<a href="{{ route ('blogs.show', [ 'blog' => $blog ]) }}">
&raquo; Read more
</a>
</div>
</div>
@endforeach
{{ $blogs->links ("pagination::default") }}
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,52 @@
@extends ("partials.layout")
@section ("title", "Create a new blog")
@section ("content")
<div class="row edit-blog-entry">
<div class="col w-20 left">
<div class="edit-info">
<p>You can use markdown in the description of your blog!</p>
</div>
</div>
<div class="col right">
<h1>Create Blog</h1>
<br>
<form method="POST" class="ctrl-enter-submit" enctype="multipart/form-data">
@csrf
<label for="name">Name:</label>
<input type="text" name="name" id="name" value="{{ old ('name') }}" required>
@error("name")
<p class="error">{{ $message }}</p>
@enderror
<label for="description">Description:</label>
<textarea name="description" id="description">{{ old ("description") }}</textarea>
@error("description")
<p class="error">{{ $message }}</p>
@enderror
<label for="icon">Logo:</label>
<input type="file" name="icon" id="icon" accept="image/*" required>
@error("icon")
<p class="error">{{ $message }}</p>
@enderror
<br>
<label for="category">Category:</label>
<select name="category" id="category">
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
<div class="publish">
<button type="submit" name="submit">Create Blog!</button>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,56 @@
@extends ("partials.layout")
@section ("title", "Create New Entry")
@section ("content")
<div class="row edit-blog-entry">
<div class="col w-20 left">
<div class="edit-info">
<p>You can use Markdown in the content of your entry!</p>
</div>
</div>
<div class="col right">
<h1>New Entry</h1>
<br>
<form method="POST" class="ctrl-enter-submit" enctype="multipart/form-data" action="{{ route ('user.post.new') }}">
@csrf
<input type="hidden" name="blog_id" value="{{ $blog->id }}">
<label for="summary">Title:</label>
<input type="text" name="summary" id="summary" value="{{ old ('summary') }}" placeholder="A really cool post!" required>
@error("summary")
<p class="error">{{ $message }}</p>
@enderror
<br>
<textarea name="content" placeholder="What's on your mind?">{{ old ('content') }}</textarea>
<small>Markdown is supported</small>
@error("content")
<p class="error">{{ $message }}</p>
@enderror
<br>
<label for="files">Attachments:</label><br>
<input type="file" name="files[]" accept="image/*" id="files" multiple><br>
@error("files.*")
<p class="error">{{ $message }}</p>
@enderror
<br>
<label for="visibility">Visibility</label>
<select name="visibility">
<option value="public">Public</option>
<option value="followers">Friends only</option>
<option value="private">Private</option>
</select>
<br><br>
<button type="submit">Post!</button>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,88 @@
@extends ("partials.layout")
@section ("title", $blog->name)
@section ("content")
<div class="row profile">
<div class="col w-30 left">
<h1>
{{ $blog->name }}
</h1>
<div class="general-about">
<div class="profile-pic">
<img loading="lazy" src="{{ $blog->actor->icon }}" alt="{{ $blog->name }}" class="pfp-fallback">
</div>
<div class="details below">
@if ($blog->user->is_online ())
<p class="online">
<img loading="lazy" src="/resources/img/green_person.png"> ONLINE!
</p>
@endif
</div>
</div>
<div class="mood">
<p><b>Mood:</b> {{ $blog->user->mood }}</p>
<br>
<p>
<b>View my: <a href="{{ route ('users.show', [ 'user_name' => $blog->user->name ]) }}">Profile</a></b>
</p>
</div>
<div class="url-info">
<p>
<b>
Federation Handle:
</b>
</p>
<p>@php echo "@" . $blog->slug . "@" . explode ("/", env ("APP_URL"))[2] @endphp</p>
</div>
<div class="url-info view-full-profile">
<p>
<a href="{{ route ('users.show', [ 'user_name' => $blog->user->name ]) }}">
<b>View Full Profile</b>
</a>
</p>
</div>
</div>
<div class="col right">
<div class="blog-preview">
<h1>
{{ $blog->name }}'s Blog Entries
</h1>
<div class="blog-preview">
<h3>
[
<a href="{{ route ('blogs.new_entry', [ 'blog' => $blog->slug ]) }}">
New Entry
</a>
]
</h3>
</div>
<br>
<div class="blog-entries">
<h3>Pinned</h3>
@foreach ($blog->pinned_notes as $note)
<x-blog_entry_block :note="$note->note" />
@endforeach
<hr>
@foreach ($notes as $note)
<x-blog_entry_block :note="$note" />
@endforeach
{{ $notes->links () }}
</div>
</div>
</div>
</div>
@endsection

View File

@ -53,5 +53,7 @@
@endforeach
</tbody>
</table>
{{ $notes->links("pagination::default") }}
</div>
@endsection

View File

@ -0,0 +1,20 @@
<div class="entry">
<p class="publish-date">
<time class="ago">
{{ $note->created_at->diffForHumans () }}
</time>
</p>
<div class="inner">
<h3 class="title">
<a href="{{ route ('posts.show', [ 'note' => $note ]) }}">
{{ $note->summary }}
</a>
</h3>
<p>
<a href="{{ route ('posts.show', [ 'note' => $note ]) }}">
&raquo; View Blog Entry
</a>
</p>
</div>
</div>

View File

@ -17,10 +17,15 @@ 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 ]);
if (!$display_post->can_view ())
return;
@endphp
<tr>
@ -32,11 +37,7 @@ else
</a>
<a href="{{ $actor_url }}">
<p>
@if ($actor->user)
<img loading="lazy" src="{{ $actor->user->avatar }}" class="pfp-fallback" width="50">
@else
<img loading="lazy" src="{{ $actor->icon }}" class="pfp-fallback" width="50">
@endif
</p>
</a>
</td>

View File

@ -10,9 +10,18 @@
<br>
<textarea name="content" placeholder="What's on your mind?" style="width: 100%"></textarea>
<input type="file" name="files[]" accept="image/*" multiple><br>
<button type="submit">Post</button>
<small>Markdown is supported</small>
<br>
<input type="file" name="files[]" accept="image/*" multiple><br>
<div>
<b>Visibility:</b>
<select name="visibility">
<option value="public">Public</option>
<option value="followers">Friends only</option>
<option value="private">Mentioned Only</option>
</select>
</div>
<button type="submit">Post</button>
@error ("content")
<div class="error">{{ $message }}</div>

View File

@ -25,11 +25,11 @@
View My
<a href="{{ route('users.show', ['user_name' => auth()->user()->name]) }}">Profile</a>
|
<a href="#">Blog</a>
<a href="{{ route ('users.blogs', [ 'user_name' => auth ()->user ()->name ]) }}">Blog</a>
|
<a href="#">Bulletins</a>
|
<a href="#">Friends</a>
<a href="{{ route ('users.friends', [ 'user_name' => auth ()->user ()->name ]) }}">Friends</a>
</p>
<p>
My URL:

View File

@ -47,7 +47,11 @@
</li>
<li>
<a href="#">&nbsp;Blog </a>
@auth
<a href="{{ route ('blogs') }}">&nbsp;Blog </a>
@else
<a href="{{ route ('login') }}">&nbsp; Blog</a>
@endauth
</li>
<li>
@ -66,6 +70,12 @@
<a href="#">&nbsp;Favs </a>
</li>
@auth
<li class="active">
<a href="{{ route ('users.notifications') }}">&nbsp;Notifications ({{ count (auth ()->user ()->unreadNotifications) }})</a>
</li>
@endauth
<li>
<a href="https://github.com/0xd011f4ce/OurSpace">&nbsp;Source </a>
</li>

View File

@ -44,10 +44,7 @@
@if (auth ()->check ())
<script>
const notification_sound = new Audio ("/resources/sounds/notification.mp3")
notification_sound.muted = true
notification_sound.play ()
notification_sound.muted = false
const notification_sound = new Audio ("{{ auth ()->user ()->notification_sound }}")
function register_echo ()
{

View File

@ -1,20 +1,38 @@
@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")
@section ("content")
<div class="row article blog-entry">
<div class="col w-20 left">
<div class="edit-info">
<div class="profile-pic">
<img loading="lazy" src="{{ $actor->user ? $actor->user->avatar : $actor->icon }}" class="pfp-fallback">
<img loading="lazy" src="{{ $actor->icon }}" class="pfp-fallback">
</div>
<div class="author-details">
<h4>
Published by
<span>
<a href="{{ route ('users.show', [ 'user_name' => $actor->user ? $actor->user->name : $actor->local_actor_id ]) }}">
<a href="{{ $user_url }}">
{{ $actor->name }}
</a>
</span>
@ -28,9 +46,9 @@
</p>
<p class="links">
<a href="{{ route ('users.show', [ 'user_name' => $actor->user ? $actor->user->name : $actor->local_actor_id ]) }}">
<a href="{{ $user_url }}">
<img loading="lazy" src="/resources/icons/user.png" class="icon">
<span class="m-hide">View</span> Profile
<span class="m-hide">View</span> {{ $actor->blog_id ? "Blog" : "Profile" }}
</a>
</p>
</div>

View File

@ -0,0 +1,20 @@
@extends ("partials.layout")
@section ("title", "Posts Tagged with " . $hashtag->name)
@section ("content")
<div class="simple-container">
<h1>Showing posts with tag: {{ $hashtag->name }}</h1>
<br>
<table class="comments-table" cellspacing="0" cellpadding="3" bordercollor="#ffffff" border="1">
<tbody>
@foreach ($posts as $post)
<x-comment_block :post="$post" />
@endforeach
</tbody>
</table>
{{ $posts->links("pagination::default") }}
</div>
@endsection

View File

@ -0,0 +1,42 @@
@extends ("partials.layout")
@section ("title", $user->name . "'s Blogs")
@section ("content")
<div class="simple-container">
<h1>{{ $user->name }}'s Blogs</h1>
<p>
<a href="{{ route ('users.show', [ 'user_name' => $user->name ]) }}">&laquo; Back to profile</a>
</p>
<br>
<div class="blog-entries">
@foreach ($blogs as $blog)
<div class="entry">
<p class="publish-date">
<time class="ago">{{ $blog->created_at->diffForHumans () }}</time>
&mdash; by <a href="{{ route ('users.show', [ 'user_name' => $blog->user->name ]) }}">{{ $blog->user->name }}</a>
&mdash; <a>{{ count ($blog->notes) }} Posts</a>
</p>
<div class="inner">
<h3 class="title">
<a href="{{ route ('blogs.show', [ 'blog' => $blog ]) }}">
{{ $blog->name }}
</a>
</h3>
<p>
{!! $blog->description !!}
</p>
<a href="{{ route ('blogs.show', [ 'blog' => $blog ]) }}">
&raquo; Read more
</a>
</div>
</div>
@endforeach
</div>
</div>
@endsection

View File

@ -36,8 +36,14 @@
@error("song")
<p class="error">{{ $message }}</p>
@enderror
<small>Max file size: 4MB</small>
<br><br>
<small>Select custom notification sound:</small>
<input type="file" name="notification_sound" accept="audio/*"><br>
@error("notification_sound")
<p class="error">{{ $message }}</p>
@enderror
<small>Max file size: 1MB</small>
<br>
<h1>Bio:</h1>
<br>
<textarea name="bio" id="bio" cols="58" placeholder="Bio">{{ $user->bio }}</textarea>

View File

@ -0,0 +1,71 @@
@extends ("partials.layout")
@section ("title", "Notifications")
@section ("content")
<div class="simple-container">
<h1>Notifications</h1>
<p>You have <b>{{ $unread_notifications }}</b> unread notifications</p>
<table border="1" width="100%">
<tr>
<th style="width: 100px">Actor</th>
<th>Content</th>
<th>Time</th>
<th>Read</th>
</tr>
@foreach ($processed_notifications as $notification)
@if (!$notification ['actor'] || !$notification ['object'])
@continue
@endif
<tr @if ($notification ['read_at'] == null) style="font-weight: bold" @endif>
<td>
@if ($notification ['type'] == 'Signup')
<a href="{{ route ('users.show', [ 'user_name' => $notification ['object']->actor->preferredUsername ]) }}">
<p>{{ $notification ['object']->actor->preferredUsername }}</p>
</a>
@else
<a href="{{ route ('users.show', [ 'user_name' => $notification ['actor']->local_actor_id ? $notification ['actor']->local_actor_id : $notification ['actor']->name ]) }}">
<p>{{ $notification ['actor']->name }}</p>
</a>
@endif
</td>
<td>
@if ($notification ['type'] == 'Signup')
<p>Joined Ourspace! Say something!!</p>
@elseif ($notification ['type'] == 'Follow')
<p>Followed you</p>
@elseif ($notification ['type'] == 'Unfollow')
@if ($notification ["object"]->id == auth ()->user ()->id)
<p>You unfollowed you</p>
@else
<p>Unfollowed <b>{{ $notification ['object']->name }}</b></p>
@endif
@elseif ($notification ['type'] == 'Boost')
<p>Boosted this <b><a href="{{ route ('posts.show', ['note' => $notification['object']->id]) }}">post</a></b></p>
@elseif ($notification ['type'] == 'Like')
<p>Liked this <b><a href="{{ route ('posts.show', ['note' => $notification['object']->id]) }}">post</a></b></p>
@elseif ($notification ['type'] == 'Reply')
<p>Replied to this <b><a href="{{ route ('posts.show', ['note' => $notification['object']->id]) }}">post</a></b></p>
@elseif ($notification ['type'] == 'Mention')
<p>Mentioned you in this <b><a href="{{ route ('posts.show', ['note' => $notification['object']->id])}}">post</a></b></p>
@endif
</td>
<td>
<p>{{ $notification ['created_at']->diffForHumans () }}</p>
</td>
<td>
<input type="checkbox" @if ($notification ['read_at'] != null) checked @endif disabled>
</td>
</tr>
@endforeach
</table>
{{ $notifications->links ("pagination::default") }}
</div>
@endsection

View File

@ -40,7 +40,7 @@
<script>
let music = document.getElementById ('music');
music.volume = 0.1;
music.volume = 0.075;
music.play ()
</script>
@endif
@ -50,7 +50,7 @@
<div class="mood">
@if ($user != null)
<p><b>Mood:</b> {{ $user->mood }}</p>
<p><b>View my: <a href="#">Blog</a> | <a href="#">Bulletins</a></b></p>
<p><b>View my: <a href="{{ route ('users.blogs', [ 'user_name' => $user->name ]) }}">Blog</a> | <a href="#">Bulletins</a></b></p>
@endif
</div>
@ -280,10 +280,9 @@
@if ($user != null)
<div class="blog-preview">
<h4>
{{ $user->name }}'s Latest Blog Entries [<a href="#">View Blog</a>]
{{ $user->name }}'s Latest Blog Entries [<a href="{{ route ('users.blogs', [ 'user_name' => $user->name ]) }}">View Blog</a>]
</h4>
<p>
<i>There are no Blog Entries yet.</i>
</p>
</div>
@endif

View File

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

View File

@ -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");
@ -26,7 +27,9 @@ Route::middleware ("update_online")->group (function () {
// user routes
Route::get ("/user/edit", [ ProfileController::class, "edit" ])->name ("users.edit")->middleware ("auth");
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
@ -41,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";