[PLUGIN][Pinboard] Implement tag handling
This commit is contained in:
parent
ca9945a4be
commit
74ffd261b8
|
@ -29,12 +29,14 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||||
|
use Component\Tag\Tag;
|
||||||
|
use Component\Link\Link;
|
||||||
|
|
||||||
class APIv1 extends Controller
|
class APIv1 extends Controller
|
||||||
{
|
{
|
||||||
public const SOURCE = 'Pinboard API v1';
|
public const SOURCE = 'Pinboard API v1';
|
||||||
|
|
||||||
private function before()
|
private function preCheck()
|
||||||
{
|
{
|
||||||
$format = $this->string('format');
|
$format = $this->string('format');
|
||||||
$auth_token = $this->string('auth_token');
|
$auth_token = $this->string('auth_token');
|
||||||
|
@ -80,13 +82,17 @@ class APIv1 extends Controller
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
[$id, $token] = explode(':', $input);
|
[$id, $token] = explode(':', $input);
|
||||||
return Token::get($id, $token)?->getUser();
|
if (filter_var($id, FILTER_VALIDATE_INT) !== false) {
|
||||||
|
return Token::get((int) $id, $token)?->getUser();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function deleteNoteAndMaybePin(LocalUser $user, Note $note, ?Pin $pin): void
|
private function deleteNoteAndMaybePin(LocalUser $user, Note $note, ?Pin $pin): void
|
||||||
{
|
{
|
||||||
$note->delete($user->getActor(), self::SOURCE);
|
$note->delete($user->getActor(), self::SOURCE);
|
||||||
if (!\is_null($note)) {
|
if (!\is_null($pin)) {
|
||||||
DB::remove($pin);
|
DB::remove($pin);
|
||||||
}
|
}
|
||||||
DB::flush();
|
DB::flush();
|
||||||
|
@ -101,6 +107,15 @@ class APIv1 extends Controller
|
||||||
return $tags;
|
return $tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Pin[] $pins
|
||||||
|
* @param string[] $tags
|
||||||
|
*/
|
||||||
|
private function filterByTags(array $pins, array $tags): array
|
||||||
|
{
|
||||||
|
return F\select($pins, fn (Pin $pin) => array_intersect(F\map($pin->getTags(), fn (NoteTag $tag) => $tag->getTag()), $tags) !== []);
|
||||||
|
}
|
||||||
|
|
||||||
private function getLatestModified(LocalUser $user): string
|
private function getLatestModified(LocalUser $user): string
|
||||||
{
|
{
|
||||||
return Cache::get(
|
return Cache::get(
|
||||||
|
@ -147,12 +162,17 @@ class APIv1 extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the most recent time a bookmark was added, updated or
|
* Returns the most recent time a bookmark was added, updated or
|
||||||
* deleted. Use this before calling posts/all to see if the data
|
* deleted. Use this preCheck calling posts/all to see if the data
|
||||||
* has changed since the last fetch
|
* has changed since the last fetch
|
||||||
*/
|
*/
|
||||||
public function posts_update(Request $request)
|
public function posts_update(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before();
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
return self::respond(['update_time' => self::getLatestModified($user)]);
|
return self::respond(['update_time' => self::getLatestModified($user)]);
|
||||||
}
|
}
|
||||||
|
@ -162,7 +182,12 @@ class APIv1 extends Controller
|
||||||
*/
|
*/
|
||||||
public function posts_add(Request $request)
|
public function posts_add(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before();
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
if (\is_null($url = $this->string('url'))) {
|
if (\is_null($url = $this->string('url'))) {
|
||||||
throw new ClientException('URL must be provided');
|
throw new ClientException('URL must be provided');
|
||||||
|
@ -199,28 +224,33 @@ class APIv1 extends Controller
|
||||||
if (!$replace) {
|
if (!$replace) {
|
||||||
$result_code = 'item already exists';
|
$result_code = 'item already exists';
|
||||||
} else {
|
} else {
|
||||||
throw new ServerException('Updating is unimplemented'); // TODO delete old note, create new one
|
|
||||||
$this->deleteNoteAndMaybePin($user, $pin->getNote(), pin: null);
|
$this->deleteNoteAndMaybePin($user, $pin->getNote(), pin: null);
|
||||||
|
// Continue below
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
DB::persist($note = Note::create([
|
DB::persist($note = Note::create([
|
||||||
'actor_id' => $user->getId(),
|
'actor_id' => $user->getId(),
|
||||||
'content' => $url,
|
'content' => "Bookmark: {$url}\nTitle: {$title}\nDescription: {$description}",
|
||||||
'content_type' => 'text/uri-list',
|
'content_type' => 'text/plain',
|
||||||
'rendered' => Formatting::twigRenderFile('pinboard/render.html.twig', ['url' => $url, 'title' => $title, 'description' => $description]),
|
'rendered' => Formatting::twigRenderFile('pinboard/render.html.twig', ['url' => $url, 'title' => $title, 'description' => $description]),
|
||||||
'reply_to' => null,
|
'reply_to' => null,
|
||||||
'is_local' => true,
|
'is_local' => true,
|
||||||
'source' => self::SOURCE,
|
'source' => self::SOURCE,
|
||||||
'scope' => $public ? VisibilityScope::EVERYWHERE->value : VisibilityScope::ADDRESSEE->value,
|
'scope' => $public ? VisibilityScope::EVERYWHERE->value : VisibilityScope::ADDRESSEE->value,
|
||||||
'language_id' => $user->getActor()->getTopLanguage()->getId(),
|
'language_id' => $user->getActor()->getTopLanguage()->getId(),
|
||||||
'type' => 'page',
|
'type' => Pin::note_type,
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
]));
|
]));
|
||||||
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
|
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
|
||||||
$pin->setNoteId($note->getId());
|
$pin->setNoteId($note->getId());
|
||||||
Conversation::assignLocalConversation($note, null);
|
Conversation::assignLocalConversation($note, null);
|
||||||
DB::persist($pin);
|
DB::persist($pin);
|
||||||
// TODO handle tags
|
foreach ($tags as $tag) {
|
||||||
|
if (!\is_null($nt = Tag::maybeCreateTag(tag: $tag, note_id: $note->getId(), lang_id: $note->getLanguageId()))) {
|
||||||
|
DB::persist($nt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_values(Link::maybeCreateLink($url, $note->getId())) !== [null, null]) {
|
||||||
DB::flush();
|
DB::flush();
|
||||||
$result_code = 'done';
|
$result_code = 'done';
|
||||||
}
|
}
|
||||||
|
@ -233,7 +263,13 @@ class APIv1 extends Controller
|
||||||
*/
|
*/
|
||||||
public function posts_delete(Request $request)
|
public function posts_delete(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before($request);
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
$url = $this->string('url');
|
$url = $this->string('url');
|
||||||
if (\is_null($url)) {
|
if (\is_null($url)) {
|
||||||
throw new ClientException('URL must be provided');
|
throw new ClientException('URL must be provided');
|
||||||
|
@ -255,16 +291,18 @@ class APIv1 extends Controller
|
||||||
*/
|
*/
|
||||||
public function posts_get(Request $request)
|
public function posts_get(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before($request);
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
$tags = self::parseTags();
|
$tags = self::parseTags();
|
||||||
$day = $this->string('dt');
|
$day = $this->string('dt');
|
||||||
$url = $this->string('url');
|
$url = $this->string('url');
|
||||||
$meta = $this->bool('meta');
|
$meta = $this->bool('meta');
|
||||||
|
|
||||||
if (!\is_null($tags) && $tags !== []) {
|
|
||||||
throw new ClientException(_m('tags attribute not implemented'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_null($url) && \is_null($day)) {
|
if (\is_null($url) && \is_null($day)) {
|
||||||
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId(), 'gte' => ['modified' => new DateTimeImmutable('today')]]);
|
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId(), 'gte' => ['modified' => new DateTimeImmutable('today')]]);
|
||||||
} elseif (!\is_null($day)) {
|
} elseif (!\is_null($day)) {
|
||||||
|
@ -276,6 +314,8 @@ class APIv1 extends Controller
|
||||||
throw new BugFoundException('Wonky logic in pinboard/posts/get');
|
throw new BugFoundException('Wonky logic in pinboard/posts/get');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pins = self::filterByTags($pins, $tags);
|
||||||
|
|
||||||
return self::respond(self::formatPins($user, $pins));
|
return self::respond(self::formatPins($user, $pins));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,15 +324,18 @@ class APIv1 extends Controller
|
||||||
*/
|
*/
|
||||||
public function posts_recent(Request $request)
|
public function posts_recent(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before($request);
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
$tags = self::parseTags();
|
$tags = self::parseTags();
|
||||||
$limit = min($this->int('count') ?? 15, 100);
|
$limit = min($this->int('count') ?? 15, 100);
|
||||||
|
|
||||||
if (!\is_null($tags) && $tags !== []) {
|
|
||||||
throw new ClientException('tags attribute not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId()], order_by: ['modified' => 'asc'], limit: $limit);
|
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId()], order_by: ['modified' => 'asc'], limit: $limit);
|
||||||
|
$pins = self::filterByTags($pins, $tags);
|
||||||
|
|
||||||
return self::respond(self::formatPins($user, $pins));
|
return self::respond(self::formatPins($user, $pins));
|
||||||
}
|
}
|
||||||
|
@ -310,7 +353,13 @@ class APIv1 extends Controller
|
||||||
*/
|
*/
|
||||||
public function posts_all(Request $request)
|
public function posts_all(Request $request)
|
||||||
{
|
{
|
||||||
$user = self::before($request);
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
$tags = self::parseTags();
|
$tags = self::parseTags();
|
||||||
$offset = $this->int('start');
|
$offset = $this->int('start');
|
||||||
$limit = $this->int('results');
|
$limit = $this->int('results');
|
||||||
|
@ -318,10 +367,6 @@ class APIv1 extends Controller
|
||||||
$end_time = $this->string('todt');
|
$end_time = $this->string('todt');
|
||||||
$meta = $this->bool('meta');
|
$meta = $this->bool('meta');
|
||||||
|
|
||||||
if (!\is_null($tags) && $tags !== []) {
|
|
||||||
throw new ClientException('tags attribute not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
$criteria = ['actor_id' => $user->getId()];
|
$criteria = ['actor_id' => $user->getId()];
|
||||||
if (!\is_null($start_time)) {
|
if (!\is_null($start_time)) {
|
||||||
$criteria['gte'] = ['modified' => new DateTimeImmutable($start_time)];
|
$criteria['gte'] = ['modified' => new DateTimeImmutable($start_time)];
|
||||||
|
@ -331,6 +376,37 @@ class APIv1 extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
$pins = DB::findBy(Pin::class, $criteria, order_by: ['modified' => 'asc'], offset: $offset, limit: $limit);
|
$pins = DB::findBy(Pin::class, $criteria, order_by: ['modified' => 'asc'], offset: $offset, limit: $limit);
|
||||||
|
$pins = self::filterByTags($pins, $tags);
|
||||||
|
|
||||||
return self::respond(self::formatPins($user, $pins)['posts']);
|
return self::respond(self::formatPins($user, $pins)['posts']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a full list of the user's tags along with the number of times they were used
|
||||||
|
*/
|
||||||
|
public function tags_get(Request $request)
|
||||||
|
{
|
||||||
|
$check = self::preCheck();
|
||||||
|
if (!$check instanceof LocalUser) {
|
||||||
|
return $check;
|
||||||
|
} else {
|
||||||
|
$user = $check;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tags_freq = [];
|
||||||
|
foreach ($user->getActor()->getNoteTags(Pin::note_type) as $tag) {
|
||||||
|
if (!isset($tags_freq[$tag->getCanonical()])) {
|
||||||
|
$tags_freq[$tag->getCanonical()] = [];
|
||||||
|
}
|
||||||
|
$tags_freq[$tag->getCanonical()][] = $tag->getTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($tags_freq as $canon => $variations) {
|
||||||
|
$freqs = array_count_values($variations);
|
||||||
|
arsort($freqs);
|
||||||
|
$tags_freq[$canon] = $freqs[array_key_first($freqs)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::respond($tags_freq);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,8 @@ class Pin extends Entity
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
// }}} Autocode
|
// }}} Autocode
|
||||||
|
|
||||||
|
public const note_type = 'page';
|
||||||
|
|
||||||
public static function cacheKeys(int|LocalUser|Actor $user): array
|
public static function cacheKeys(int|LocalUser|Actor $user): array
|
||||||
{
|
{
|
||||||
$id = \is_int($user) ? $user : $user->getId();
|
$id = \is_int($user) ? $user : $user->getId();
|
||||||
|
@ -157,7 +159,7 @@ class Pin extends Entity
|
||||||
*/
|
*/
|
||||||
public function getTags(): array
|
public function getTags(): array
|
||||||
{
|
{
|
||||||
return [];
|
return Note::getById($this->getNoteId())->getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function schemaDef(): array
|
public static function schemaDef(): array
|
||||||
|
|
|
@ -95,7 +95,7 @@ class Token extends Entity
|
||||||
options: ['limit' => 1],
|
options: ['limit' => 1],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} elseif (!is_id($id) && !\is_null($token)) {
|
} elseif (!\is_null($id) && !\is_null($token)) {
|
||||||
return Cache::get(
|
return Cache::get(
|
||||||
self::cacheKeys($id)['user-token'],
|
self::cacheKeys($id)['user-token'],
|
||||||
fn () => DB::dql(
|
fn () => DB::dql(
|
||||||
|
|
|
@ -87,6 +87,12 @@ class Pinboard extends Plugin
|
||||||
[C\APIv1::class, 'posts_all'],
|
[C\APIv1::class, 'posts_all'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$r->connect(
|
||||||
|
'pinboard_tags_get',
|
||||||
|
'/pinboard/v1/tags/get',
|
||||||
|
[C\APIv1::class, 'tags_get'],
|
||||||
|
);
|
||||||
|
|
||||||
return Event::next;
|
return Event::next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user