fix(fediverse): add is_private field to posts to flag private posts and hide them from public views

This commit is contained in:
Yassine Doghri 2025-08-22 09:56:45 +00:00
parent 08a3779ee3
commit d5ef2ab86f
25 changed files with 4455 additions and 4412 deletions

View File

@ -66,6 +66,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]);
@ -183,6 +188,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(),
]);

View File

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

View File

@ -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.',
];

View File

@ -201,7 +201,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,
@ -211,7 +211,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')
@ -221,8 +221,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(

View File

@ -32,6 +32,7 @@ class PostModel extends FediversePostModel
'episode_id',
'message',
'message_html',
'is_private',
'favourites_count',
'reblogs_count',
'replies_count',

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
/**
* @icon("custom:repeat-off")
*/

View File

@ -0,0 +1 @@
<svg fill="currentColor" viewBox="0 0 24 24" width="1em" height="1em"><path d="m23 19-5 4v-3H8.02l2-2H18v-3l5 4ZM6 4h12.586l2.606-2.606 1.414 1.414L2.414 23 1 21.586l1.65-1.65A1 1 0 0 1 2 19v-7h2v6h.586l12-12H6v3L1 5l5-4v3Zm16 8h-2V8.02l2-2V12Z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -744,7 +744,8 @@ export class AudioClipper extends LitElement {
var(--tw-ring-offset-shadow),
var(--tw-ring-shadow),
0 0 rgba(0, 0, 0, 0);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
box-shadow:
var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow, 0 0 rgba(0, 0, 0, 0));
--tw-ring-offset-width: 2px;
--tw-ring-opacity: 1;

View File

@ -11,8 +11,9 @@
body {
height: 100%;
background: var(--main-bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
color: var(--main-text-color);
font-weight: 300;
margin: 0;

View File

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

View File

@ -26,4 +26,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);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Fediverse\Migrations;
use App\Database\Migrations\BaseMigration;
class AddIsPrivateToPosts extends BaseMigration
{
public function up(): void
{
$fields = [
'is_private' => [
'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');
}
}

View File

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

View File

@ -60,6 +60,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

View File

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

View File

@ -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',
@ -409,11 +420,13 @@ class PostModel extends UuidModel
Events::trigger('on_post_remove', $post);
} elseif ($post->in_reply_to_id !== null) {
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);
}
@ -441,10 +454,12 @@ class PostModel extends UuidModel
$postId = $this->addPost($reply, $createPreviewCard, $registerActivity);
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);
@ -457,6 +472,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;

View File

@ -39,13 +39,19 @@ class NoteObject extends ObjectType
$this->attributedTo = $post->actor->uri;
if ($post->in_reply_to_id !== null) {
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);
if (! $post->is_private) {
$this->cc = [$post->actor->followers_url];
}
}
}

View File

@ -26,18 +26,18 @@
"prepare": "is-ci || husky"
},
"dependencies": {
"@amcharts/amcharts4": "^4.10.39",
"@amcharts/amcharts4": "^4.10.40",
"@amcharts/amcharts4-geodata": "^4.1.30",
"@codemirror/commands": "^6.7.1",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.10.8",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.36.1",
"@floating-ui/dom": "^1.6.13",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.8",
"@floating-ui/dom": "^1.7.0",
"@github/clipboard-copy-element": "^1.3.0",
"@github/hotkey": "^3.1.1",
"@github/markdown-toolbar-element": "^2.2.3",
"@github/relative-time-element": "^4.4.4",
"@github/relative-time-element": "^4.4.8",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@vime/core": "^5.4.1",
"choices.js": "^10.2.0",
@ -45,52 +45,52 @@
"flatpickr": "^4.6.13",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"lit": "^3.2.1",
"marked": "^15.0.6",
"wavesurfer.js": "^7.8.15",
"xml-formatter": "^3.6.3"
"lit": "^3.3.0",
"marked": "^15.0.11",
"wavesurfer.js": "^7.9.5",
"xml-formatter": "^3.6.6"
},
"devDependencies": {
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@csstools/css-tokenizer": "^3.0.3",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"@semantic-release/gitlab": "^13.2.3",
"@semantic-release/gitlab": "^13.2.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/leaflet": "^1.9.16",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"@types/leaflet": "^1.9.17",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"all-contributors-cli": "^6.26.1",
"commitizen": "^4.3.1",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"cssnano": "^7.0.7",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.14.0",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"globals": "^16.1.0",
"husky": "^9.1.7",
"is-ci": "^4.1.0",
"lint-staged": "^15.3.0",
"postcss": "^8.4.49",
"lint-staged": "^16.0.0",
"postcss": "^8.5.3",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1",
"postcss-preset-env": "^10.1.3",
"postcss-preset-env": "^10.1.6",
"postcss-reporter": "^7.1.0",
"prettier": "3.4.2",
"prettier": "3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"semantic-release": "^24.2.1",
"stylelint": "^16.12.0",
"semantic-release": "^24.2.3",
"stylelint": "^16.19.1",
"stylelint-config-standard": "^36.0.1",
"svgo": "^3.3.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^6.0.7",
"vite-plugin-pwa": "^0.21.1",
"typescript": "~5.7.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.0",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",
"workbox-routing": "^7.3.0",
@ -105,7 +105,7 @@
"stylelint --fix",
"prettier --write"
],
"!(*.css|*.js|*.ts|*.php|*.neon|*.sh)": [
"!(*.css|*.js|*.ts|*.php|*.neon|*.sh|*.svg)": [
"prettier --write"
]
},

View File

@ -10,6 +10,7 @@ return PHPIconsConfig::configure()
'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',

8565
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,9 @@
<a href="<?= route_to('episode', esc($podcast->handle), esc($episode->slug)) ?>"
class="inline-flex items-center px-4 py-2 text-sm focus:ring-accent"><?= icon(
'arrow-left-line',
'mr-2 text-lg',
[
'class' => 'mr-2 text-lg',
],
) . lang('Comment.back_to_comments') ?></a>
</nav>
<div class="pb-12">

View File

@ -14,6 +14,13 @@
]),
],
) ?>
<?php if ($post->is_private): ?>
<button type="button" class="inline-flex items-center cursor-not-allowed" title="<?= lang(
'Post.cannot_reblog'
) ?>"><?= icon('custom:repeat-off', [
'class' => 'text-2xl mr-1 opacity-40',
]) ?></button>
<?php else: ?>
<button type="submit" name="action" value="reblog" class="inline-flex items-center hover:underline" title="<?= lang(
'Post.reblogs',
[
@ -22,6 +29,7 @@
) ?>"><?= icon('repeat-fill', [
'class' => 'text-2xl mr-1 opacity-40',
]) . $post->reblogs_count ?></button>
<?php endif; ?>
<button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline" title="<?= lang(
'Post.favourites',
[

View File

@ -17,8 +17,8 @@
: '@' . esc($post->actor->domain)) ?></span>
</a>
<a href="<?= route_to('post', esc($podcast->handle), $post->id) ?>"
class="text-xs text-skin-muted">
<?= relative_time($post->published_at) ?>
class="text-xs text-skin-muted inline-flex items-center">
<?= relative_time($post->published_at) ?><span class="ml-1" data-tooltip="bottom" title="<?= $post->is_private ? lang('Post.is_private') : lang('Post.is_public') ?>"><?= $post->is_private ? icon('lock-fill') : icon('earth-fill') ?></span>
</a>
</div>
</header>

View File

@ -11,7 +11,9 @@
->display_name) ?><span class="ml-1 text-sm font-normal text-skin-muted">@<?= esc($reply
->actor->username) .
($reply->actor->is_local ? '' : '@' . esc($reply->actor->domain)) ?></span></a>
<?= relative_time($reply->published_at, 'flex-shrink-0 ml-auto text-xs text-skin-muted') ?>
<a href="<?= route_to('post', esc($podcast->handle), $reply->id) ?>" class="flex-shrink-0 ml-auto text-xs text-skin-muted inline-flex items-center gap-x-1">
<?= relative_time($reply->published_at) ?><span data-tooltip="bottom" title="<?= $reply->is_private ? lang('Post.is_private') : lang('Post.is_public') ?>"><?= $reply->is_private ? icon('lock-fill') : icon('earth-fill') ?></span>
</a>
</header>
<p class="mb-2 post-content"><?= $reply->message_html ?></p>
<?php if ($reply->preview_card_id): ?>

View File

@ -16,6 +16,13 @@ if (can_user_interact()): ?>
]),
],
) ?>
<?php if ($reply->is_private): ?>
<button type="button" class="inline-flex items-center text-sm cursor-not-allowed" title="<?= lang(
'Post.cannot_reblog'
) ?>"><?= icon('custom:repeat-off', [
'class' => 'text-lg mr-1 opacity-40',
]) ?></button>
<?php else: ?>
<button type="submit" name="action" value="reblog" class="inline-flex items-center text-sm hover:underline" title="<?= lang(
'Post.reblogs',
[
@ -24,6 +31,7 @@ if (can_user_interact()): ?>
) ?>"><?= icon('repeat-fill', [
'class' => 'text-lg mr-1 opacity-40',
]) . $reply->reblogs_count ?></button>
<?php endif; ?>
<button type="submit" name="action" value="favourite" class="inline-flex items-center text-sm hover:underline" title="<?= lang(
'Post.favourites',
[