[COMPONENT][Notification] Consider attention properly in notes
This commit is contained in:
parent
f5e92de62d
commit
d5731e6351
@ -357,26 +357,29 @@ class Posting extends Component
|
|||||||
]);
|
]);
|
||||||
DB::persist($activity);
|
DB::persist($activity);
|
||||||
|
|
||||||
|
$attention_ids = [];
|
||||||
foreach ($targets as $target) {
|
foreach ($targets as $target) {
|
||||||
$target = \is_int($target) ? Actor::getById($target) : $target;
|
$target_id = \is_int($target) ? $target : $target->getId();
|
||||||
DB::persist(Attention::create(['note_id' => $note->getId(), 'target_id' => $target->getId()]));
|
DB::persist(Attention::create(['note_id' => $note->getId(), 'target_id' => $target_id]));
|
||||||
$mentions[] = [
|
$attention_ids[$target_id] = true;
|
||||||
'mentioned' => [$target],
|
|
||||||
'type' => match ($target->getType()) {
|
|
||||||
Actor::PERSON => 'mention',
|
|
||||||
Actor::GROUP => 'group',
|
|
||||||
default => throw new ClientException(_m('Unknown target type give in \'In\' field: {target}', ['{target}' => $target?->getNickname() ?? '<null>'])),
|
|
||||||
},
|
|
||||||
'text' => $target->getNickname(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
$attention_ids = array_keys($attention_ids);
|
||||||
$attention_ids = F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId())));
|
|
||||||
|
|
||||||
if ($flush_and_notify) {
|
if ($flush_and_notify) {
|
||||||
// Flush before notification
|
// Flush before notification
|
||||||
DB::flush();
|
DB::flush();
|
||||||
Event::handle('NewNotification', [$actor, $activity, ['object' => $attention_ids], _m('{nickname} created a note {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]);
|
Event::handle('NewNotification', [
|
||||||
|
$actor,
|
||||||
|
$activity,
|
||||||
|
[
|
||||||
|
'note-attention' => $attention_ids,
|
||||||
|
'object' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
|
||||||
|
],
|
||||||
|
_m('{nickname} created a note {note_id}.', [
|
||||||
|
'{nickname}' => $actor->getNickname(),
|
||||||
|
'{note_id}' => $activity->getObjectId(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$activity, $note, $attention_ids];
|
return [$activity, $note, $attention_ids];
|
||||||
|
@ -180,8 +180,8 @@ class Inbox extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
DB::flush();
|
DB::flush();
|
||||||
if (Event::handle('ActivityPubNewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} mentioned you.', ['{nickname}' => $actor->getNickname()])]) === Event::next) {
|
if (Event::handle('ActivityPubNewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]) === Event::next) {
|
||||||
Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} mentioned you.', ['{nickname}' => $actor->getNickname()])]);
|
Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject());
|
dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject());
|
||||||
|
@ -155,7 +155,8 @@ class Note extends Model
|
|||||||
'reply_to' => $reply_to = $handleInReplyTo($type_note),
|
'reply_to' => $reply_to = $handleInReplyTo($type_note),
|
||||||
'modified' => new DateTime(),
|
'modified' => new DateTime(),
|
||||||
'type' => match ($type_note->get('type')) {
|
'type' => match ($type_note->get('type')) {
|
||||||
'Page' => NoteType::PAGE, default => NoteType::NOTE
|
'Page' => NoteType::PAGE,
|
||||||
|
default => NoteType::NOTE
|
||||||
},
|
},
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
];
|
];
|
||||||
@ -177,14 +178,14 @@ class Note extends Model
|
|||||||
} else {
|
} else {
|
||||||
// Either Followers-only or Direct
|
// Either Followers-only or Direct
|
||||||
if ($type_note->get('type') === 'ChatMessage' // Is DM explicitly?
|
if ($type_note->get('type') === 'ChatMessage' // Is DM explicitly?
|
||||||
|| (empty($type_note->get('cc')))) { // Only has TO targets
|
|| (empty($type_note->get('cc')))) { // Only has TO targets
|
||||||
$map['scope'] = VisibilityScope::MESSAGE;
|
$map['scope'] = VisibilityScope::MESSAGE;
|
||||||
} else { // Then is collection
|
} else { // Then is collection
|
||||||
$map['scope'] = VisibilityScope::COLLECTION;
|
$map['scope'] = VisibilityScope::COLLECTION;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$object_mentions_ids = [];
|
$attention_ids = [];
|
||||||
foreach ($to as $target) {
|
foreach ($to as $target) {
|
||||||
if ($target === 'https://www.w3.org/ns/activitystreams#Public') {
|
if ($target === 'https://www.w3.org/ns/activitystreams#Public') {
|
||||||
continue;
|
continue;
|
||||||
@ -219,6 +220,21 @@ class Note extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
$obj = GSNote::create($map);
|
$obj = GSNote::create($map);
|
||||||
|
DB::persist($obj);
|
||||||
|
|
||||||
|
foreach ($attention_ids as $attention_uri) {
|
||||||
|
$explorer = new Explorer();
|
||||||
|
try {
|
||||||
|
$actors = $explorer->lookup($attention_uri);
|
||||||
|
foreach ($actors as $actor) {
|
||||||
|
$object_mention_ids[$target_id = $actor->getId()] = $attention_uri;
|
||||||
|
DB::persist(Attention::create(['note_id' => $obj->getId(), 'target_id' => $target_id]));
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::debug('ActivityPub->Model->Note->fromJson->Attention->Explorer', [$e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$attention_ids = array_keys($attention_ids);
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
$processed_attachments = [];
|
$processed_attachments = [];
|
||||||
@ -246,11 +262,10 @@ class Note extends Model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::persist($obj);
|
|
||||||
|
|
||||||
// Assign conversation to this note
|
// Assign conversation to this note
|
||||||
Conversation::assignLocalConversation($obj, $reply_to);
|
Conversation::assignLocalConversation($obj, $reply_to);
|
||||||
|
|
||||||
|
$object_mention_ids = [];
|
||||||
foreach ($type_note->get('tag') ?? [] as $ap_tag) {
|
foreach ($type_note->get('tag') ?? [] as $ap_tag) {
|
||||||
switch ($ap_tag->get('type')) {
|
switch ($ap_tag->get('type')) {
|
||||||
case 'Mention':
|
case 'Mention':
|
||||||
@ -258,7 +273,7 @@ class Note extends Model
|
|||||||
try {
|
try {
|
||||||
$actors = $explorer->lookup($ap_tag->get('href'));
|
$actors = $explorer->lookup($ap_tag->get('href'));
|
||||||
foreach ($actors as $actor) {
|
foreach ($actors as $actor) {
|
||||||
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
|
$object_mention_ids[$actor->getId()] = $ap_tag->get('href');
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::debug('ActivityPub->Model->Note->fromJson->Mention->Explorer', [$e]);
|
Log::debug('ActivityPub->Model->Note->fromJson->Mention->Explorer', [$e]);
|
||||||
@ -284,10 +299,10 @@ class Note extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The content would be non-sanitized text/html
|
// The content would be non-sanitized text/html
|
||||||
Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true, 'ignoreLinks' => $object_mentions_ids]]);
|
Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true, 'ignoreLinks' => $object_mention_ids]]);
|
||||||
|
|
||||||
$object_mentions_ids = array_keys($object_mentions_ids);
|
$object_mention_ids = array_keys($object_mention_ids);
|
||||||
$obj->setObjectMentionsIds($object_mentions_ids);
|
$obj->setObjectMentionsIds($object_mention_ids);
|
||||||
|
|
||||||
if ($processed_attachments !== []) {
|
if ($processed_attachments !== []) {
|
||||||
foreach ($processed_attachments as [$a, $fname]) {
|
foreach ($processed_attachments as [$a, $fname]) {
|
||||||
@ -318,7 +333,9 @@ class Note extends Model
|
|||||||
/**
|
/**
|
||||||
* Get a JSON
|
* Get a JSON
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws ClientException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
* @throws ServerException
|
||||||
*/
|
*/
|
||||||
public static function toJson(mixed $object, ?int $options = null): string
|
public static function toJson(mixed $object, ?int $options = null): string
|
||||||
{
|
{
|
||||||
@ -329,7 +346,8 @@ class Note extends Model
|
|||||||
$attr = [
|
$attr = [
|
||||||
'@context' => ActivityPub::$activity_streams_two_context,
|
'@context' => ActivityPub::$activity_streams_two_context,
|
||||||
'type' => $object->getScope() === VisibilityScope::MESSAGE ? 'ChatMessage' : (match ($object->getType()) {
|
'type' => $object->getScope() === VisibilityScope::MESSAGE ? 'ChatMessage' : (match ($object->getType()) {
|
||||||
NoteType::NOTE => 'Note', NoteType::PAGE => 'Page'
|
NoteType::NOTE => 'Note',
|
||||||
|
NoteType::PAGE => 'Page'
|
||||||
}),
|
}),
|
||||||
'id' => $object->getUrl(),
|
'id' => $object->getUrl(),
|
||||||
'published' => $object->getCreated()->format(DateTimeInterface::RFC3339),
|
'published' => $object->getCreated()->format(DateTimeInterface::RFC3339),
|
||||||
@ -349,7 +367,7 @@ class Note extends Model
|
|||||||
case VisibilityScope::EVERYWHERE:
|
case VisibilityScope::EVERYWHERE:
|
||||||
$attr['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
$attr['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
$attr['cc'] = [Router::url('actor_subscribers_id', ['id' => $object->getActor()->getId()], Router::ABSOLUTE_URL)];
|
$attr['cc'] = [Router::url('actor_subscribers_id', ['id' => $object->getActor()->getId()], Router::ABSOLUTE_URL)];
|
||||||
break;
|
break;
|
||||||
case VisibilityScope::LOCAL:
|
case VisibilityScope::LOCAL:
|
||||||
throw new ClientException('This note was not federated.', 403);
|
throw new ClientException('This note was not federated.', 403);
|
||||||
case VisibilityScope::ADDRESSEE:
|
case VisibilityScope::ADDRESSEE:
|
||||||
@ -371,9 +389,9 @@ class Note extends Model
|
|||||||
throw new ServerException('Found an unknown visibility scope which cannot federate.');
|
throw new ServerException('Found an unknown visibility scope which cannot federate.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$attention_cc = DB::findBy(Attention::class, ['note_id' => $object->getId()]);
|
// Notification Targets without Mentions
|
||||||
foreach ($attention_cc as $cc_id) {
|
$attentions = $object->getNotificationTargets(ids_already_known: ['object' => []]);
|
||||||
$target = \App\Entity\Actor::getById($cc_id->getTargetId());
|
foreach ($attentions as $target) {
|
||||||
if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) {
|
if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) {
|
||||||
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||||
} else {
|
} else {
|
||||||
@ -382,7 +400,7 @@ class Note extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mentions
|
// Mentions
|
||||||
foreach ($object->getNotificationTargets() as $mention) {
|
foreach ($object->getMentionTargets() as $mention) {
|
||||||
$attr['tag'][] = [
|
$attr['tag'][] = [
|
||||||
'type' => 'Mention',
|
'type' => 'Mention',
|
||||||
'href' => ($href = $mention->getUri()),
|
'href' => ($href = $mention->getUri()),
|
||||||
|
@ -28,6 +28,7 @@ use App\Core\Cache;
|
|||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Entity;
|
use App\Core\Entity;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Core\Router\Router;
|
use App\Core\Router\Router;
|
||||||
use App\Core\VisibilityScope;
|
use App\Core\VisibilityScope;
|
||||||
@ -37,15 +38,17 @@ use App\Util\Formatting;
|
|||||||
use Component\Avatar\Avatar;
|
use Component\Avatar\Avatar;
|
||||||
use Component\Conversation\Entity\Conversation;
|
use Component\Conversation\Entity\Conversation;
|
||||||
use Component\Language\Entity\Language;
|
use Component\Language\Entity\Language;
|
||||||
use function App\Core\I18n\_m;
|
use Component\Notification\Entity\Attention;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use function mb_substr;
|
||||||
|
use const PREG_SPLIT_NO_EMPTY;
|
||||||
|
|
||||||
// The domain of this enum are Notes
|
// The domain of this enum are Notes
|
||||||
enum NoteType: int // having an int is just convenient
|
enum NoteType : int // having an int is just convenient
|
||||||
{
|
{
|
||||||
case NOTE = 1; // Is an element of microblogging, a direct message, or a reply to another note or page
|
case NOTE = 1; // Is an element of microblogging, a direct message, or a reply to another note or page
|
||||||
case PAGE = 2; // Larger content note, beginning of a thread, or an email message
|
case PAGE = 2; // Larger content note, beginning of a thread, or an email message
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity for notices
|
* Entity for notices
|
||||||
@ -63,20 +66,20 @@ class Note extends Entity
|
|||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
private int $id;
|
private int $id;
|
||||||
private int $actor_id;
|
private int $actor_id;
|
||||||
private ?string $content = null;
|
private ?string $content = null;
|
||||||
private string $content_type = 'text/plain';
|
private string $content_type = 'text/plain';
|
||||||
private ?string $rendered = null;
|
private ?string $rendered = null;
|
||||||
private int $conversation_id;
|
private int $conversation_id;
|
||||||
private ?int $reply_to = null;
|
private ?int $reply_to = null;
|
||||||
private bool $is_local;
|
private bool $is_local;
|
||||||
private ?string $source = null;
|
private ?string $source = null;
|
||||||
private int $scope = 1; //VisibilityScope::EVERYWHERE->value;
|
private int $scope = 1; //VisibilityScope::EVERYWHERE->value;
|
||||||
private ?string $url = null;
|
private ?string $url = null;
|
||||||
private ?int $language_id = null;
|
private ?int $language_id = null;
|
||||||
private int $type = 1; //NoteType::NOTE->value;
|
private int $type = 1; //NoteType::NOTE->value;
|
||||||
private ?string $title = null;
|
private ?string $title = null;
|
||||||
private \DateTimeInterface $created;
|
private DateTimeInterface $created;
|
||||||
private \DateTimeInterface $modified;
|
private DateTimeInterface $modified;
|
||||||
|
|
||||||
public function setId(int $id): self
|
public function setId(int $id): self
|
||||||
{
|
{
|
||||||
@ -113,7 +116,7 @@ class Note extends Entity
|
|||||||
|
|
||||||
public function setContentType(string $content_type): self
|
public function setContentType(string $content_type): self
|
||||||
{
|
{
|
||||||
$this->content_type = \mb_substr($content_type, 0, 129);
|
$this->content_type = mb_substr($content_type, 0, 129);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +171,7 @@ class Note extends Entity
|
|||||||
|
|
||||||
public function setSource(?string $source): self
|
public function setSource(?string $source): self
|
||||||
{
|
{
|
||||||
$this->source = \is_null($source) ? null : \mb_substr($source, 0, 32);
|
$this->source = \is_null($source) ? null : mb_substr($source, 0, 32);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +182,7 @@ class Note extends Entity
|
|||||||
|
|
||||||
public function setScope(VisibilityScope|int $scope): self
|
public function setScope(VisibilityScope|int $scope): self
|
||||||
{
|
{
|
||||||
$this->scope = is_int($scope) ? $scope : $scope->value;
|
$this->scope = \is_int($scope) ? $scope : $scope->value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +215,7 @@ class Note extends Entity
|
|||||||
|
|
||||||
public function setType(NoteType|int $type): self
|
public function setType(NoteType|int $type): self
|
||||||
{
|
{
|
||||||
$this->type = is_int($type) ? $type : $type->value;
|
$this->type = \is_int($type) ? $type : $type->value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +226,7 @@ class Note extends Entity
|
|||||||
|
|
||||||
public function setTitle(?string $title): self
|
public function setTitle(?string $title): self
|
||||||
{
|
{
|
||||||
$this->title = \is_null($title) ? null : \mb_substr($title, 0, 129);
|
$this->title = \is_null($title) ? null : mb_substr($title, 0, 129);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,24 +235,24 @@ class Note extends Entity
|
|||||||
return $this->title;
|
return $this->title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreated(\DateTimeInterface $created): self
|
public function setCreated(DateTimeInterface $created): self
|
||||||
{
|
{
|
||||||
$this->created = $created;
|
$this->created = $created;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreated(): \DateTimeInterface
|
public function getCreated(): DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setModified(\DateTimeInterface $modified): self
|
public function setModified(DateTimeInterface $modified): self
|
||||||
{
|
{
|
||||||
$this->modified = $modified;
|
$this->modified = $modified;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModified(): \DateTimeInterface
|
public function getModified(): DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->modified;
|
return $this->modified;
|
||||||
}
|
}
|
||||||
@ -260,12 +263,12 @@ class Note extends Entity
|
|||||||
public static function cacheKeys(int $note_id)
|
public static function cacheKeys(int $note_id)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'note' => "note-{$note_id}",
|
'note' => "note-{$note_id}",
|
||||||
'attachments' => "note-attachments-{$note_id}",
|
'attachments' => "note-attachments-{$note_id}",
|
||||||
'attachments-title' => "note-attachments-with-title-{$note_id}",
|
'attachments-title' => "note-attachments-with-title-{$note_id}",
|
||||||
'links' => "note-links-{$note_id}",
|
'links' => "note-links-{$note_id}",
|
||||||
'tags' => "note-tags-{$note_id}",
|
'tags' => "note-tags-{$note_id}",
|
||||||
'replies' => "note-replies-{$note_id}",
|
'replies' => "note-replies-{$note_id}",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +338,7 @@ class Note extends Entity
|
|||||||
return DB::dql(
|
return DB::dql(
|
||||||
<<<'EOF'
|
<<<'EOF'
|
||||||
select att from attachment att
|
select att from attachment att
|
||||||
join attachment_to_note atn with atn.attachment_id = att.id
|
join attachment_to_note atn with atn.attachment_id = att.id
|
||||||
where atn.note_id = :note_id
|
where atn.note_id = :note_id
|
||||||
EOF,
|
EOF,
|
||||||
['note_id' => $this->id],
|
['note_id' => $this->id],
|
||||||
@ -350,7 +353,7 @@ class Note extends Entity
|
|||||||
<<<'EOF'
|
<<<'EOF'
|
||||||
select att, atn.title
|
select att, atn.title
|
||||||
from attachment att
|
from attachment att
|
||||||
join attachment_to_note atn with atn.attachment_id = att.id
|
join attachment_to_note atn with atn.attachment_id = att.id
|
||||||
where atn.note_id = :note_id
|
where atn.note_id = :note_id
|
||||||
EOF,
|
EOF,
|
||||||
['note_id' => $this->id],
|
['note_id' => $this->id],
|
||||||
@ -369,7 +372,7 @@ class Note extends Entity
|
|||||||
return DB::dql(
|
return DB::dql(
|
||||||
<<<'EOF'
|
<<<'EOF'
|
||||||
select l from link l
|
select l from link l
|
||||||
join note_to_link ntl with ntl.link_id = l.id
|
join note_to_link ntl with ntl.link_id = l.id
|
||||||
where ntl.note_id = :note_id
|
where ntl.note_id = :note_id
|
||||||
EOF,
|
EOF,
|
||||||
['note_id' => $this->id],
|
['note_id' => $this->id],
|
||||||
@ -396,7 +399,7 @@ class Note extends Entity
|
|||||||
*/
|
*/
|
||||||
public function getReplyToNote(): ?self
|
public function getReplyToNote(): ?self
|
||||||
{
|
{
|
||||||
return is_null($this->getReplyTo()) ? null : self::getById($this->getReplyTo());
|
return \is_null($this->getReplyTo()) ? null : self::getById($this->getReplyTo());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -419,36 +422,34 @@ class Note extends Entity
|
|||||||
return true;
|
return true;
|
||||||
case VisibilityScope::ADDRESSEE:
|
case VisibilityScope::ADDRESSEE:
|
||||||
// If the actor is logged in and
|
// If the actor is logged in and
|
||||||
if (!\is_null($actor)
|
return (bool) (!\is_null($actor)
|
||||||
&& (
|
&& (
|
||||||
// Is either the author Or
|
// Is either the author Or
|
||||||
$this->getActorId() == $actor->getId()
|
$this->getActorId() == $actor->getId()
|
||||||
// one of the targets
|
// one of the targets
|
||||||
|| \in_array($actor->getId(), $this->getNotificationTargetIds())
|
|| \in_array($actor->getId(), $this->getNotificationTargetIds())
|
||||||
)) {
|
));
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case VisibilityScope::GROUP:
|
case VisibilityScope::GROUP:
|
||||||
if (is_null($in)) {
|
if (\is_null($in)) {
|
||||||
return false; // If we don't have a context, don't risk leaking this note.
|
return false; // If we don't have a context, don't risk leaking this note.
|
||||||
}
|
}
|
||||||
// Only for the group to see
|
// Only for the group to see
|
||||||
return !\is_null($actor) && (
|
return !\is_null($actor) && (
|
||||||
!($in->getRoles() & ActorLocalRoles::PRIVATE_GROUP) // Public Group
|
!($in->getRoles() & ActorLocalRoles::PRIVATE_GROUP) // Public Group
|
||||||
|| DB::dql( // It's a member of the private group
|
|| DB::dql( // It's a member of the private group
|
||||||
<<<'EOF'
|
<<<'EOF'
|
||||||
SELECT m FROM \Component\Group\Entity\GroupMember m
|
SELECT m FROM \Component\Group\Entity\GroupMember m
|
||||||
JOIN \Component\Notification\Entity\Notification att WITH m.group_id = att.target_id
|
JOIN \Component\Notification\Entity\Notification att WITH m.group_id = att.target_id
|
||||||
JOIN \App\Entity\Activity a WITH att.activity_id = a.id
|
JOIN \App\Entity\Activity a WITH att.activity_id = a.id
|
||||||
WHERE a.object_id = :note_id AND m.actor_id = :actor_id
|
WHERE a.object_id = :note_id AND m.actor_id = :actor_id
|
||||||
EOF,
|
EOF,
|
||||||
['note_id' => $this->id, 'actor_id' => $in->getId()],
|
['note_id' => $this->id, 'actor_id' => $in->getId()],
|
||||||
) !== []);
|
) !== []
|
||||||
|
);
|
||||||
case VisibilityScope::COLLECTION:
|
case VisibilityScope::COLLECTION:
|
||||||
case VisibilityScope::MESSAGE:
|
case VisibilityScope::MESSAGE:
|
||||||
// Only for the collection to see
|
// Only for the collection to see
|
||||||
return !\is_null($actor) && in_array($actor->getId(), $this->getNotificationTargetIds());
|
return !\is_null($actor) && \in_array($actor->getId(), $this->getNotificationTargetIds());
|
||||||
default:
|
default:
|
||||||
Log::error("Unknown scope found: {$this->getScope()->value}.");
|
Log::error("Unknown scope found: {$this->getScope()->value}.");
|
||||||
}
|
}
|
||||||
@ -457,36 +458,52 @@ class Note extends Entity
|
|||||||
|
|
||||||
// @return array of ids of Actors
|
// @return array of ids of Actors
|
||||||
public array $_object_mentions_ids = [];
|
public array $_object_mentions_ids = [];
|
||||||
|
|
||||||
public function setObjectMentionsIds(array $mentions): self
|
public function setObjectMentionsIds(array $mentions): self
|
||||||
{
|
{
|
||||||
$this->_object_mentions_ids = $mentions;
|
$this->_object_mentions_ids = $mentions;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAttentionTargetIds(?int $sender_id = null): array
|
||||||
|
{
|
||||||
|
$attentioned = [];
|
||||||
|
$attention_cc = DB::findBy(Attention::class, ['note_id' => $this->getId()]);
|
||||||
|
foreach ($attention_cc as $cc) {
|
||||||
|
$cc_id = $cc->getTargetId();
|
||||||
|
if ($cc_id === $sender_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$attentioned[] = $cc_id;
|
||||||
|
}
|
||||||
|
return $attentioned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMentionTargetIds(): array
|
||||||
|
{
|
||||||
|
$target_ids = [];
|
||||||
|
$content = $this->getContent();
|
||||||
|
if (!\is_null($content)) {
|
||||||
|
$mentions = Formatting::findMentions($content, $this->getActor());
|
||||||
|
foreach ($mentions as $mention) {
|
||||||
|
foreach ($mention['mentioned'] as $m) {
|
||||||
|
$target_ids[] = $m->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $target_ids;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see Entity->getNotificationTargetIds
|
* @see Entity->getNotificationTargetIds
|
||||||
*/
|
*/
|
||||||
public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
|
public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
|
||||||
{
|
{
|
||||||
$target_ids = $this->_object_mentions_ids ?? [];
|
$target_ids = $this->_object_mentions_ids ?? [];
|
||||||
if ($target_ids === []) {
|
|
||||||
$content = $this->getContent();
|
|
||||||
if (!\array_key_exists('object', $ids_already_known)) {
|
|
||||||
if (!\is_null($content)) {
|
|
||||||
$mentions = Formatting::findMentions($content, $this->getActor());
|
|
||||||
foreach ($mentions as $mention) {
|
|
||||||
foreach ($mention['mentioned'] as $m) {
|
|
||||||
$target_ids[] = $m->getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$target_ids = $ids_already_known['object'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Parent
|
||||||
if (!\array_key_exists('object-related', $ids_already_known)) {
|
if (!\array_key_exists('object-related', $ids_already_known)) {
|
||||||
if (!is_null($parent = $this->getReplyToNote())) {
|
if (!\is_null($parent = $this->getReplyToNote())) {
|
||||||
$target_ids[] = $parent->getActorId();
|
$target_ids[] = $parent->getActorId();
|
||||||
array_push($target_ids, ...$parent->getNotificationTargetIds());
|
array_push($target_ids, ...$parent->getNotificationTargetIds());
|
||||||
}
|
}
|
||||||
@ -494,6 +511,20 @@ class Note extends Entity
|
|||||||
array_push($target_ids, ...$ids_already_known['object-related']);
|
array_push($target_ids, ...$ids_already_known['object-related']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mentions
|
||||||
|
if (!\array_key_exists('object', $ids_already_known)) {
|
||||||
|
array_push($target_ids, ...$this->getMentionTargetIds());
|
||||||
|
} else {
|
||||||
|
array_push($target_ids, ...$ids_already_known['object']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attentions
|
||||||
|
if (!\array_key_exists('note-attention', $ids_already_known)) {
|
||||||
|
array_push($target_ids, ...$this->getAttentionTargetIds($sender_id));
|
||||||
|
} else {
|
||||||
|
array_push($target_ids, ...$ids_already_known['note-attention']);
|
||||||
|
}
|
||||||
|
|
||||||
// Additional actors that should know about this
|
// Additional actors that should know about this
|
||||||
if ($include_additional && \array_key_exists('additional', $ids_already_known)) {
|
if ($include_additional && \array_key_exists('additional', $ids_already_known)) {
|
||||||
array_push($target_ids, ...$ids_already_known['additional']);
|
array_push($target_ids, ...$ids_already_known['additional']);
|
||||||
@ -502,38 +533,65 @@ class Note extends Entity
|
|||||||
return array_unique($target_ids);
|
return array_unique($target_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAttentionTargets(?int $sender_id = null): array
|
||||||
|
{
|
||||||
|
$attentioned = $this->getAttentionTargetIds();
|
||||||
|
return DB::findBy('actor', ['id' => $attentioned]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMentionTargets(): array
|
||||||
|
{
|
||||||
|
$mentioned = [];
|
||||||
|
$mentions = Formatting::findMentions($this->getContent(), $this->getActor());
|
||||||
|
foreach ($mentions as $mention) {
|
||||||
|
foreach ($mention['mentioned'] as $m) {
|
||||||
|
$mentioned[] = $m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $mentioned;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array of Actors
|
* @return array of Actors
|
||||||
*/
|
*/
|
||||||
public function getNotificationTargets(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
|
public function getNotificationTargets(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
|
||||||
{
|
{
|
||||||
|
// Additional (if we have additional, we will just return all the actors from ids)
|
||||||
if ($include_additional && \array_key_exists('additional', $ids_already_known)) {
|
if ($include_additional && \array_key_exists('additional', $ids_already_known)) {
|
||||||
$target_ids = $this->getNotificationTargetIds($ids_already_known, $sender_id);
|
$target_ids = $this->getNotificationTargetIds($ids_already_known, $sender_id);
|
||||||
return $target_ids === [] ? [] : DB::findBy('actor', ['id' => $target_ids]);
|
return $target_ids === [] ? [] : DB::findBy(Actor::class, ['id' => $target_ids]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mentioned = [];
|
$targets = $this->_object_mentions_ids === [] ? [] : DB::findBy(Actor::class, ['id' => $this->_object_mentions_ids]);
|
||||||
if (!\array_key_exists('object', $ids_already_known)) {
|
|
||||||
$mentions = Formatting::findMentions($this->getContent(), $this->getActor());
|
|
||||||
foreach ($mentions as $mention) {
|
|
||||||
foreach ($mention['mentioned'] as $m) {
|
|
||||||
$mentioned[] = $m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$mentioned = $ids_already_known['object'] === [] ? [] : DB::findBy('actor', ['id' => $ids_already_known['object']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Parent
|
||||||
if (!\array_key_exists('object-related', $ids_already_known)) {
|
if (!\array_key_exists('object-related', $ids_already_known)) {
|
||||||
if (!is_null($parent = $this->getReplyToNote())) {
|
if (!\is_null($parent = $this->getReplyToNote())) {
|
||||||
$mentioned[] = $parent->getActor();
|
$targets[] = $parent->getActor();
|
||||||
array_push($mentioned, ...$parent->getNotificationTargets());
|
array_push($targets, ...$parent->getNotificationTargets());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
array_push($mentioned, ...$ids_already_known['object-related']);
|
array_push($targets, ...$ids_already_known['object-related']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $mentioned;
|
// Mentions
|
||||||
|
if (!\array_key_exists('object', $ids_already_known)) {
|
||||||
|
array_push($targets, ...$this->getMentionTargets());
|
||||||
|
} elseif ($ids_already_known['object'] !== []) {
|
||||||
|
array_push($targets, ...DB::findBy('actor', ['id' => $ids_already_known['object']]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attentions
|
||||||
|
if (!\array_key_exists('note-attention', $ids_already_known)) {
|
||||||
|
array_push($targets, ...$this->getAttentionTargets($sender_id));
|
||||||
|
} else {
|
||||||
|
$attentioned = $ids_already_known['note-attention'] ?? [];
|
||||||
|
if ($attentioned !== []) {
|
||||||
|
array_push($targets, ...DB::findBy('actor', ['id' => $attentioned]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(?Actor $actor = null, string $source = 'web'): Activity
|
public function delete(?Actor $actor = null, string $source = 'web'): Activity
|
||||||
@ -550,7 +608,7 @@ class Note extends Entity
|
|||||||
return $activity;
|
return $activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function ensureCanInteract(?Note $note, LocalUser|Actor $actor): Note
|
public static function ensureCanInteract(?self $note, LocalUser|Actor $actor): self
|
||||||
{
|
{
|
||||||
if (\is_null($note)) {
|
if (\is_null($note)) {
|
||||||
throw new NoSuchNoteException();
|
throw new NoSuchNoteException();
|
||||||
@ -566,21 +624,21 @@ class Note extends Entity
|
|||||||
return [
|
return [
|
||||||
'name' => 'note',
|
'name' => 'note',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'id' => ['type' => 'serial', 'not null' => true],
|
'id' => ['type' => 'serial', 'not null' => true],
|
||||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'],
|
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'],
|
||||||
'content' => ['type' => 'text', 'description' => 'note content'],
|
'content' => ['type' => 'text', 'description' => 'note content'],
|
||||||
'content_type' => ['type' => 'varchar', 'not null' => true, 'default' => 'text/plain', 'length' => 129, 'description' => 'A note can be written in a multitude of formats such as text/plain, text/markdown, application/x-latex, and text/html'],
|
'content_type' => ['type' => 'varchar', 'not null' => true, 'default' => 'text/plain', 'length' => 129, 'description' => 'A note can be written in a multitude of formats such as text/plain, text/markdown, application/x-latex, and text/html'],
|
||||||
'rendered' => ['type' => 'text', 'description' => 'rendered note content, so we can keep the microtags (if not local)'],
|
'rendered' => ['type' => 'text', 'description' => 'rendered note content, so we can keep the microtags (if not local)'],
|
||||||
'conversation_id' => ['type' => 'serial', 'not null' => true, 'foreign key' => true, 'target' => 'Conversation.id', 'multiplicity' => 'one to one', 'description' => 'the conversation identifier'],
|
'conversation_id' => ['type' => 'serial', 'not null' => true, 'foreign key' => true, 'target' => 'Conversation.id', 'multiplicity' => 'one to one', 'description' => 'the conversation identifier'],
|
||||||
'reply_to' => ['type' => 'int', 'not null' => false, 'default' => null, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'],
|
'reply_to' => ['type' => 'int', 'not null' => false, 'default' => null, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'],
|
||||||
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'was this note generated by a local actor'],
|
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'was this note generated by a local actor'],
|
||||||
'source' => ['type' => 'varchar', 'foreign key' => true, 'length' => 32, 'target' => 'NoteSource.code', 'multiplicity' => 'many to one', 'description' => 'fkey to source of note, like "web", "im", or "clientname"'],
|
'source' => ['type' => 'varchar', 'foreign key' => true, 'length' => 32, 'target' => 'NoteSource.code', 'multiplicity' => 'many to one', 'description' => 'fkey to source of note, like "web", "im", or "clientname"'],
|
||||||
'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::EVERYWHERE->value, 'description' => 'bit map for distribution scope; 1 = everywhere; 2 = this server only; 4 = addressees; 8 = groups; 16 = collection; 32 = messages'],
|
'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::EVERYWHERE->value, 'description' => 'bit map for distribution scope; 1 = everywhere; 2 = this server only; 4 = addressees; 8 = groups; 16 = collection; 32 = messages'],
|
||||||
'url' => ['type' => 'text', 'description' => 'Permalink to Note'],
|
'url' => ['type' => 'text', 'description' => 'Permalink to Note'],
|
||||||
'language_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'],
|
'language_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'],
|
||||||
'type' => ['type' => 'int', 'not null' => true, 'default' => NoteType::NOTE->value, 'description' => 'bit map for note type; 1 = Note; 2 = Page'],
|
'type' => ['type' => 'int', 'not null' => true, 'default' => NoteType::NOTE->value, 'description' => 'bit map for note type; 1 = Note; 2 = Page'],
|
||||||
'title' => ['type' => 'varchar', 'not null' => false, 'default' => null, 'length' => 129, 'description' => 'Title of a page or a note'],
|
'title' => ['type' => 'varchar', 'not null' => false, 'default' => null, 'length' => 129, 'description' => 'Title of a page or a note'],
|
||||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||||
],
|
],
|
||||||
'primary key' => ['id'],
|
'primary key' => ['id'],
|
||||||
|
Loading…
Reference in New Issue
Block a user