[COMPONENT][Conversation] added onProcessNoteContent event

If the source lacks capability of sending the reply_to metadata, it might be on the note content itself as a reference.

[ENTITY][Note] Documenting conversation/reply related functions. The entity Note reply_to shouldn't be trusted toknow whether or not the Note is a Conversation root. This will happen if a known remote user replies to an unknown remote user - within a known conversation.
This commit is contained in:
Eliseu Amaro 2021-12-10 23:09:39 +00:00 committed by Diogo Peralta Cordeiro
parent c83ae76a68
commit 3ca7a35158
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
4 changed files with 63 additions and 34 deletions

View File

@ -74,7 +74,15 @@ class Reply extends FeedController
throw new NoSuchNoteException(); throw new NoSuchNoteException();
} }
// TODO shouldn't this be the posting form? /*
* TODO shouldn't this be the posting form?
* Posting needs to be improved to do that. Currently, if it was used here,
* there are only slow ways to retrieve the resulting note.
* Not only is it part of a right panel event, there's an immediate redirect exception
* after submitting it.
* That event needs to be extended to allow this component to automagically fill the To: field and get the
* resulting note
*/
$form = Form::create([ $form = Form::create([
['content', TextareaType::class, ['label' => _m('Reply'), 'label_attr' => ['class' => 'section-form-label'], 'help' => _m('Please input your reply.')]], ['content', TextareaType::class, ['label' => _m('Reply'), 'label_attr' => ['class' => 'section-form-label'], 'help' => _m('Please input your reply.')]],
FormFields::language( FormFields::language(

View File

@ -43,7 +43,7 @@ use Symfony\Component\HttpFoundation\Request;
class Conversation extends Component class Conversation extends Component
{ {
/** /**
* HTML rendering event that adds the repeat form as a note * HTML rendering event that adds a reply link as a note
* action, if a user is logged in * action, if a user is logged in
*/ */
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool public function onAddNoteActions(Request $request, Note $note, array &$actions): bool
@ -125,6 +125,14 @@ class Conversation extends Component
return Event::next; return Event::next;
} }
public function onProcessNoteContent(Note $note, string $content): bool
{
// If the source lacks capability of sending the "reply_to"
// metadata, let's try to find an inline reply_to-reference.
// TODO: preg match any reply_to reference and handle reply to funky business (see Link component)
return Event::next;
}
/** /**
* @return bool * @return bool
*/ */

View File

@ -47,8 +47,8 @@ class Conversation extends Entity
// {{{ Autocode // {{{ Autocode
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
private int $id; private int $id;
private string $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space private string $uri;
private int $note_id; private int $initial_note_id;
public function setId(int $id): self public function setId(int $id): self
{ {
@ -72,15 +72,15 @@ class Conversation extends Entity
return $this->uri; return $this->uri;
} }
public function setNoteId(int $note_id): self public function setInitialNoteId(int $initial_note_id): self
{ {
$this->note_id = $note_id; $this->initial_note_id = $initial_note_id;
return $this; return $this;
} }
public function getNoteId(): int public function getInitialNoteId(): int
{ {
return $this->note_id; return $this->initial_note_id;
} }
@ -94,14 +94,14 @@ class Conversation extends Entity
'fields' => [ 'fields' => [
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier, since any additional meaning would require updating its value for every reply upon receiving a new aparent root'], 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier, since any additional meaning would require updating its value for every reply upon receiving a new aparent root'],
'uri' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the conversation'], 'uri' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the conversation'],
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Root of note for this conversation'], 'initial_note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Initial note seen on this host for this conversation'],
], ],
'primary key' => ['id'], 'primary key' => ['id'],
'unique keys' => [ 'unique keys' => [
'conversation_uri_uniq' => ['uri'], 'conversation_uri_uniq' => ['uri'],
], ],
'foreign keys' => [ 'foreign keys' => [
'note_id_to_id_fkey' => ['note', ['note_id' => 'id']], 'initial_note_id_to_id_fkey' => ['note', ['initial_note_id' => 'id']],
], ],
]; ];
} }

View File

@ -58,8 +58,8 @@ class Note extends Entity
private int $scope = 1; private int $scope = 1;
private ?string $url; private ?string $url;
private ?int $language_id; private ?int $language_id;
private \DateTimeInterface $created; private DateTimeInterface $created;
private \DateTimeInterface $modified; private DateTimeInterface $modified;
public function setId(int $id): self public function setId(int $id): self
{ {
@ -193,29 +193,28 @@ class Note extends Entity
return $this->language_id; return $this->language_id;
} }
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;
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
// }}} Autocode // }}} Autocode
@ -326,15 +325,29 @@ class Note extends Entity
}); });
} }
/**
* Returns this Note's reply_to/parent.
*
* If we don't know the reply, we might know the **Conversation**!
* This will happen if a known remote user replies to an
* unknown remote user - within a known Conversation.
*
* As such, **do not take for granted** that this is a root
* Note of a Conversation, in case this returns null!
* Whenever a note is created, checks should be made
* to guarantee that the latest info is correct.
*/
public function getReplyToNote(): ?self public function getReplyToNote(): ?self
{ {
return self::getByPK($this->getReplyTo()); return self::getByPK($this->getReplyTo());
} }
public function getReplies() /**
* Returns all **known** replies made to this entity
*/
public function getReplies(): array
{ {
$id = $this->getId(); return Cache::get('note-replies-' . $this->getId(), fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $this->getId()]));
return Cache::get('note-replies-' . $id, fn () => DB::dql('select n from note n where n.reply_to = :id', ['id' => $id]));
} }
/** /**
@ -433,20 +446,20 @@ 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', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'], 'reply_to' => ['type' => 'int', '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::PUBLIC, 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = groups; 8 = subscribers; 16 = messages; null = default'], 'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::PUBLIC, 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = groups; 8 = subscribers; 16 = messages; null = default'],
'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'],
'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'],
'indexes' => [ 'indexes' => [