diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index a21a8f52..b18bc5b2 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -5,13 +5,11 @@ declare(strict_types=1);
use CodeIgniter\Router\RouteCollection;
/**
- * @var RouteCollection
- *
* --------------------------------------------------------------------
* Placeholder definitions
* --------------------------------------------------------------------
*/
-
+/** @var RouteCollection $routes */
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
diff --git a/app/Controllers/PostController.php b/app/Controllers/PostController.php
index 84b831fd..e19dcb18 100644
--- a/app/Controllers/PostController.php
+++ b/app/Controllers/PostController.php
@@ -69,6 +69,11 @@ class PostController extends FediversePostController
$this->post = $post;
+ // show 404 if post is private
+ if ($this->post->is_private && ! can_user_interact()) {
+ throw PageNotFoundException::forPageNotFound();
+ }
+
unset($params[0]);
unset($params[1]);
@@ -185,6 +190,7 @@ class PostController extends FediversePostController
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $validData['message'],
+ 'is_private' => $this->post->is_private,
'published_at' => Time::now(),
'created_by' => user_id(),
]);
diff --git a/app/Database/Migrations/2024-12-10-170000_drop_deprecated_podcasts_fields.php b/app/Database/Migrations/2025-08-25-170000_drop_deprecated_podcasts_fields.php
similarity index 100%
rename from app/Database/Migrations/2024-12-10-170000_drop_deprecated_podcasts_fields.php
rename to app/Database/Migrations/2025-08-25-170000_drop_deprecated_podcasts_fields.php
diff --git a/app/Database/Migrations/2024-12-10-180000_drop_deprecated_episodes_fields.php b/app/Database/Migrations/2025-08-25-180000_drop_deprecated_episodes_fields.php
similarity index 100%
rename from app/Database/Migrations/2024-12-10-180000_drop_deprecated_episodes_fields.php
rename to app/Database/Migrations/2025-08-25-180000_drop_deprecated_episodes_fields.php
diff --git a/app/Entities/Post.php b/app/Entities/Post.php
index 07793a51..fa0fa3c6 100644
--- a/app/Entities/Post.php
+++ b/app/Entities/Post.php
@@ -34,6 +34,7 @@ class Post extends FediversePost
'episode_id' => '?integer',
'message' => 'string',
'message_html' => 'string',
+ 'is_private' => 'boolean',
'favourites_count' => 'integer',
'reblogs_count' => 'integer',
'replies_count' => 'integer',
diff --git a/app/Language/en/Post.php b/app/Language/en/Post.php
index 58d1cf80..df54cef8 100644
--- a/app/Language/en/Post.php
+++ b/app/Language/en/Post.php
@@ -37,4 +37,7 @@ return [
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete post',
+ 'is_public' => 'Post is public',
+ 'is_private' => 'Post is private',
+ 'cannot_reblog' => 'This private post cannot be shared.',
];
diff --git a/app/Models/EpisodeCommentModel.php b/app/Models/EpisodeCommentModel.php
index 1929335c..5ba48d5f 100644
--- a/app/Models/EpisodeCommentModel.php
+++ b/app/Models/EpisodeCommentModel.php
@@ -204,7 +204,7 @@ class EpisodeCommentModel extends UuidModel
{
// TODO: merge with replies from posts linked to episode linked
$episodeCommentsBuilder = $this->builder();
- $episodeComments = $episodeCommentsBuilder->select('*, 0 as is_from_post')
+ $episodeComments = $episodeCommentsBuilder->select('*, 0 as is_private, 0 as is_from_post')
->where([
'episode_id' => $episodeId,
'in_reply_to_id' => null,
@@ -214,7 +214,7 @@ class EpisodeCommentModel extends UuidModel
$postModel = new PostModel();
$episodePostsRepliesBuilder = $postModel->builder();
$episodePostsReplies = $episodePostsRepliesBuilder->select(
- 'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post',
+ 'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, is_private, favourites_count as likes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post',
)
->whereIn('in_reply_to_id', static function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id')
@@ -224,8 +224,14 @@ class EpisodeCommentModel extends UuidModel
'in_reply_to_id' => null,
]);
})
- ->where('`created_at` <= UTC_TIMESTAMP()', null, false)
- ->getCompiledSelect();
+ ->where('`created_at` <= UTC_TIMESTAMP()', null, false);
+
+ // do not get private replies if public
+ if (! can_user_interact()) {
+ $episodePostsRepliesBuilder->where('is_private', false);
+ }
+
+ $episodePostsReplies = $episodePostsRepliesBuilder->getCompiledSelect();
/** @var BaseResult $allEpisodeComments */
$allEpisodeComments = $this->db->query(
diff --git a/app/Models/PostModel.php b/app/Models/PostModel.php
index 25a834a7..8e1a97b0 100644
--- a/app/Models/PostModel.php
+++ b/app/Models/PostModel.php
@@ -32,6 +32,7 @@ class PostModel extends FediversePostModel
'episode_id',
'message',
'message_html',
+ 'is_private',
'favourites_count',
'reblogs_count',
'replies_count',
diff --git a/app/Resources/icons/custom/_index.php b/app/Resources/icons/custom/_index.php
new file mode 100644
index 00000000..0ef2821e
--- /dev/null
+++ b/app/Resources/icons/custom/_index.php
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/docs/src/content/docs/en/plugins/create.mdx b/docs/src/content/docs/en/plugins/create.mdx
index 43566991..198425e4 100644
--- a/docs/src/content/docs/en/plugins/create.mdx
+++ b/docs/src/content/docs/en/plugins/create.mdx
@@ -39,22 +39,22 @@ project generated for you!
2. add a manifest.json file
-
- - hello-world
- - **manifest.json**
+
+ - hello-world
+ - **manifest.json**
-
+
- See the [manifest reference](./manifest).
+See the [manifest reference](./manifest).
3. add the Plugin.php class
-
- - hello-world
- - manifest.json
- - **Plugin.php**
+
+ - hello-world
+ - manifest.json
+ - **Plugin.php**
-
+
diff --git a/modules/Auth/Config/Routes.php b/modules/Auth/Config/Routes.php
index e9c46c34..0b386995 100644
--- a/modules/Auth/Config/Routes.php
+++ b/modules/Auth/Config/Routes.php
@@ -6,10 +6,7 @@ namespace Modules\Auth\Config;
use CodeIgniter\Router\RouteCollection;
-/**
- * @var RouteCollection
- */
-
+/** @var RouteCollection $routes */
service('auth')
->routes($routes);
diff --git a/modules/Fediverse/Controllers/ActorController.php b/modules/Fediverse/Controllers/ActorController.php
index 90031eaa..adc37cfd 100644
--- a/modules/Fediverse/Controllers/ActorController.php
+++ b/modules/Fediverse/Controllers/ActorController.php
@@ -111,6 +111,7 @@ class ActorController extends Controller
'actor_id' => $payloadActor->id,
'in_reply_to_id' => $replyToPost->id,
'message' => $message,
+ 'is_private' => ! is_note_public($payload->object),
'published_at' => Time::parse($payload->object->published),
]);
}
diff --git a/modules/Fediverse/Database/Migrations/2018-01-02-120000_update_activities_status.php b/modules/Fediverse/Database/Migrations/2018-01-02-120000_update_activities_status.php
index c8ccf0e9..2d7adac0 100644
--- a/modules/Fediverse/Database/Migrations/2018-01-02-120000_update_activities_status.php
+++ b/modules/Fediverse/Database/Migrations/2018-01-02-120000_update_activities_status.php
@@ -28,4 +28,17 @@ class UpdateActivitiesStatus extends BaseMigration
$this->forge->modifyColumn('fediverse_activities', $fields);
}
+
+ public function down(): void
+ {
+ $fields = [
+ 'status' => [
+ 'type' => 'ENUM',
+ 'constraint' => ['queued', 'delivered'],
+ 'null' => true,
+ ],
+ ];
+
+ $this->forge->modifyColumn('fediverse_activities', $fields);
+ }
}
diff --git a/modules/Fediverse/Database/Migrations/2025-07-31-120000_add_is_private_to_posts.php b/modules/Fediverse/Database/Migrations/2025-07-31-120000_add_is_private_to_posts.php
new file mode 100644
index 00000000..d1ac9327
--- /dev/null
+++ b/modules/Fediverse/Database/Migrations/2025-07-31-120000_add_is_private_to_posts.php
@@ -0,0 +1,35 @@
+ [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'default' => 0,
+ 'after' => 'message_html',
+ ],
+ ];
+
+ $this->forge->addColumn('fediverse_posts', $fields);
+ }
+
+ public function down(): void
+ {
+ $this->forge->dropColumn('fediverse_posts', 'is_private');
+ }
+}
diff --git a/modules/Fediverse/Entities/Post.php b/modules/Fediverse/Entities/Post.php
index f791d57f..c1b5e3ac 100644
--- a/modules/Fediverse/Entities/Post.php
+++ b/modules/Fediverse/Entities/Post.php
@@ -25,9 +25,12 @@ use RuntimeException;
* @property Post|null $reblog_of_post
* @property string $message
* @property string $message_html
+ * @property bool $is_private
+ *
* @property int $favourites_count
* @property int $reblogs_count
* @property int $replies_count
+ *
* @property Time $published_at
* @property Time $created_at
*
@@ -80,6 +83,7 @@ class Post extends UuidEntity
'reblog_of_id' => '?string',
'message' => 'string',
'message_html' => 'string',
+ 'is_private' => 'boolean',
'favourites_count' => 'integer',
'reblogs_count' => 'integer',
'replies_count' => 'integer',
diff --git a/modules/Fediverse/Filters/FediverseFilter.php b/modules/Fediverse/Filters/FediverseFilter.php
index 355546b2..2e9b32ba 100644
--- a/modules/Fediverse/Filters/FediverseFilter.php
+++ b/modules/Fediverse/Filters/FediverseFilter.php
@@ -59,6 +59,8 @@ class FediverseFilter implements FilterInterface
}
}
+ log_message('critical', 'ITS HEEEEEEEEEEEERE');
+
if (in_array('verify-signature', $params, true)) {
try {
// securityCheck: check activity signature before handling it
diff --git a/modules/Fediverse/Helpers/fediverse_helper.php b/modules/Fediverse/Helpers/fediverse_helper.php
index d73bbe14..c842d8e5 100644
--- a/modules/Fediverse/Helpers/fediverse_helper.php
+++ b/modules/Fediverse/Helpers/fediverse_helper.php
@@ -345,7 +345,7 @@ if (! function_exists('get_message_from_object')) {
*/
function get_message_from_object(stdClass $object): string | false
{
- if (property_exists($object, 'content')) {
+ if (property_exists($object, 'content') && is_string($object->content)) {
extract_text_from_html($object->content);
return $object->content;
}
@@ -365,6 +365,29 @@ if (! function_exists('get_message_from_object')) {
}
}
+if (! function_exists('is_note_public')) {
+ /**
+ * Check whether note is public or not
+ */
+ function is_note_public(stdClass $object): bool
+ {
+ $isPublic = false;
+ if (property_exists($object, 'to') && is_array($object->to)) {
+ $isPublic = in_array('https://www.w3.org/ns/activitystreams#Public', $object->to, true);
+ }
+
+ if ($isPublic) {
+ return true;
+ }
+
+ if (property_exists($object, 'cc') && is_array($object->cc)) {
+ return in_array('https://www.w3.org/ns/activitystreams#Public', $object->cc, true);
+ }
+
+ return $isPublic;
+ }
+}
+
if (! function_exists('linkify')) {
/**
* Turn all link elements in clickable links. Transforms urls and handles
diff --git a/modules/Fediverse/Models/PostModel.php b/modules/Fediverse/Models/PostModel.php
index a1b4a54d..80169b76 100644
--- a/modules/Fediverse/Models/PostModel.php
+++ b/modules/Fediverse/Models/PostModel.php
@@ -52,6 +52,7 @@ class PostModel extends UuidModel
'reblog_of_id',
'message',
'message_html',
+ 'is_private',
'favourites_count',
'reblogs_count',
'replies_count',
@@ -183,6 +184,12 @@ class PostModel extends UuidModel
$this->where('in_reply_to_id', $this->uuid->fromString($postId) ->getBytes())
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'ASC');
+
+ // do not get private replies if public
+ if (! can_user_interact()) {
+ $this->where('is_private', false);
+ }
+
$found = $this->findAll();
cache()
@@ -284,6 +291,10 @@ class PostModel extends UuidModel
->set('actor', $post->actor->uri)
->set('object', new $noteObjectClass($post));
+ if ($post->in_reply_to_id !== null && $post->is_private) {
+ $createActivity->set('to', [$post->reply_to_post->actor->uri]);
+ }
+
$activityId = model('ActivityModel', false)
->newActivity(
'Create',
@@ -410,11 +421,13 @@ class PostModel extends UuidModel
Events::trigger('on_post_remove', $post);
} elseif ($post->in_reply_to_id !== null) {
- // Post to remove is a reply
- model('PostModel', false)
- ->builder()
- ->where('id', $this->uuid->fromString($post->in_reply_to_id) ->getBytes())
- ->decrement('replies_count');
+ if (! $post->is_private) {
+ // Post to remove is a reply
+ model('PostModel', false)
+ ->builder()
+ ->where('id', $this->uuid->fromString($post->in_reply_to_id) ->getBytes())
+ ->decrement('replies_count');
+ }
Events::trigger('on_reply_remove', $post);
}
@@ -442,10 +455,12 @@ class PostModel extends UuidModel
$postId = $this->addPost($reply, $createPreviewCard, $registerActivity);
- model('PostModel', false)
- ->builder()
- ->where('id', $this->uuid->fromString($reply->in_reply_to_id) ->getBytes())
- ->increment('replies_count');
+ if (! $reply->is_private) {
+ model('PostModel', false)
+ ->builder()
+ ->where('id', $this->uuid->fromString($reply->in_reply_to_id) ->getBytes())
+ ->increment('replies_count');
+ }
Events::trigger('on_post_reply', $reply);
@@ -458,6 +473,11 @@ class PostModel extends UuidModel
public function reblog(Actor $actor, Post $post, bool $registerActivity = true): string | false
{
+ // cannot reblog a private post
+ if ($post->is_private) {
+ return false;
+ }
+
$this->db->transStart();
$userId = null;
diff --git a/modules/Fediverse/Objects/NoteObject.php b/modules/Fediverse/Objects/NoteObject.php
index 561ce89b..73af27a3 100644
--- a/modules/Fediverse/Objects/NoteObject.php
+++ b/modules/Fediverse/Objects/NoteObject.php
@@ -39,13 +39,19 @@ class NoteObject extends ObjectType
$this->attributedTo = $post->actor->uri;
if ($post->in_reply_to_id !== null) {
- $this->to[] = $post->reply_to_post->actor->uri;
+ if ($post->is_private) {
+ $this->to = [$post->reply_to_post->actor->uri];
+ } else {
+ $this->to[] = $post->reply_to_post->actor->uri;
+ }
$this->inReplyTo = $post->reply_to_post->uri;
}
$this->replies = url_to('post-replies', esc($post->actor->username), $post->id);
- $this->cc = [$post->actor->followers_url];
+ if (! $post->is_private) {
+ $this->cc = [$post->actor->followers_url];
+ }
}
}
diff --git a/php-icons.php b/php-icons.php
index f3f59198..1c6b6915 100644
--- a/php-icons.php
+++ b/php-icons.php
@@ -7,9 +7,10 @@ use PHPIcons\Config\PHPIconsConfig;
return PHPIconsConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/themes', __DIR__ . '/resources'])
->withLocalIconSets([
- 'funding' => __DIR__ . '/resources/icons/funding',
- 'podcasting' => __DIR__ . '/resources/icons/podcasting',
- 'social' => __DIR__ . '/resources/icons/social',
+ 'funding' => __DIR__ . '/app/Resources/icons/funding',
+ 'podcasting' => __DIR__ . '/app/Resources/icons/podcasting',
+ 'social' => __DIR__ . '/app/Resources/icons/social',
+ 'custom' => __DIR__ . '/app/Resources/icons/custom',
])
->withDefaultIconPerSet([
'funding' => 'funding:default',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 85ca7049..5ee4bc85 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4400,10 +4400,10 @@ packages:
engines: { node: ">=0.10.0" }
hasBin: true
- electron-to-chromium@1.5.208:
+ electron-to-chromium@1.5.209:
resolution:
{
- integrity: sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==,
+ integrity: sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==,
}
emoji-regex@10.4.0:
@@ -12024,7 +12024,7 @@ snapshots:
browserslist@4.25.3:
dependencies:
caniuse-lite: 1.0.30001737
- electron-to-chromium: 1.5.208
+ electron-to-chromium: 1.5.209
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.3)
@@ -12631,7 +12631,7 @@ snapshots:
dependencies:
jake: 10.9.4
- electron-to-chromium@1.5.208: {}
+ electron-to-chromium@1.5.209: {}
emoji-regex@10.4.0: {}
diff --git a/themes/cp_app/episode/comment.php b/themes/cp_app/episode/comment.php
index ea038878..9031d033 100644
--- a/themes/cp_app/episode/comment.php
+++ b/themes/cp_app/episode/comment.php
@@ -6,7 +6,9 @@
= icon(
'arrow-left-line',
- 'mr-2 text-lg',
+ [
+ 'class' => 'mr-2 text-lg',
+ ],
) . lang('Comment.back_to_comments') ?>
diff --git a/themes/cp_app/post/_partials/actions.php b/themes/cp_app/post/_partials/actions.php
index 55a3774d..456cbae8 100644
--- a/themes/cp_app/post/_partials/actions.php
+++ b/themes/cp_app/post/_partials/actions.php
@@ -14,14 +14,22 @@
]),
],
) ?>
-
+ is_private): ?>
+
+
+
+
diff --git a/themes/cp_app/post/_partials/reply.php b/themes/cp_app/post/_partials/reply.php
index fecee07a..6c3db6d5 100644
--- a/themes/cp_app/post/_partials/reply.php
+++ b/themes/cp_app/post/_partials/reply.php
@@ -11,7 +11,9 @@
->display_name) ?>@= esc($reply
->actor->username) .
($reply->actor->is_local ? '' : '@' . esc($reply->actor->domain)) ?>
- = relative_time($reply->published_at, 'flex-shrink-0 ml-auto text-xs text-skin-muted') ?>
+
+ = relative_time($reply->published_at) ?>= $reply->is_private ? icon('lock-fill') : icon('earth-fill') ?>
+
= $reply->message_html ?>
preview_card_id): ?>
diff --git a/themes/cp_app/post/_partials/reply_actions.php b/themes/cp_app/post/_partials/reply_actions.php
index 2078efe9..0fe3560a 100644
--- a/themes/cp_app/post/_partials/reply_actions.php
+++ b/themes/cp_app/post/_partials/reply_actions.php
@@ -16,14 +16,22 @@ if (can_user_interact()): ?>
]),
],
) ?>
-
+ is_private): ?>
+
+
+
+