[PLUGIN][Pinboard] Implement remaining API endpoints, restructure, fix template
This commit is contained in:
parent
a9665177ea
commit
e2501ee927
@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace Plugin\Pinboard\Controller;
|
|
||||||
|
|
||||||
use App\Core\Controller;
|
|
||||||
use App\Core\DB;
|
|
||||||
use App\Core\Router;
|
|
||||||
use App\Core\VisibilityScope;
|
|
||||||
use App\Entity\Note;
|
|
||||||
use App\Util\Exception\ClientException;
|
|
||||||
use App\Util\Exception\ServerException;
|
|
||||||
use App\Util\Formatting;
|
|
||||||
use Component\Conversation\Conversation;
|
|
||||||
use Datetime;
|
|
||||||
use Plugin\Pinboard\Entity\Pin;
|
|
||||||
use Plugin\Pinboard\Util;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
class API extends Controller
|
|
||||||
{
|
|
||||||
private function before()
|
|
||||||
{
|
|
||||||
$format = $this->string('format');
|
|
||||||
$auth_token = $this->string('auth_token');
|
|
||||||
if ($format !== 'json') {
|
|
||||||
throw new ServerException(_m('Only JSON is supported'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_null($auth_token)) {
|
|
||||||
return new Response('API requires authentication', status: 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_null($user = Util::validateToken($auth_token))) {
|
|
||||||
return new Response('401 Forbidden', status: 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last post update
|
|
||||||
*/
|
|
||||||
public function posts_update(Request $request)
|
|
||||||
{
|
|
||||||
self::before();
|
|
||||||
|
|
||||||
return new JsonResponse(
|
|
||||||
['update_time' => date(\DATE_ISO8601)], // TODO fetch latest and reply accordingly
|
|
||||||
status: 200,
|
|
||||||
headers: ['content-type' => 'application/json'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a pin
|
|
||||||
*/
|
|
||||||
public function posts_add(Request $request)
|
|
||||||
{
|
|
||||||
$user = self::before();
|
|
||||||
|
|
||||||
if (\is_null($url = $this->string('url'))) {
|
|
||||||
throw new ClientException('URL must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_null($title = $this->string('description'))) { // Logically.
|
|
||||||
throw new ClientException('Desciption must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
$description = $this->string('extended') ?? '';
|
|
||||||
|
|
||||||
$tags = [];
|
|
||||||
if (!\is_null($tags_text = $this->string('tags'))) {
|
|
||||||
Formatting::toArray($tags_text, $tags, Formatting::SPLIT_BY_BOTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
$modified = $this->string('dt') ?? new Datetime;
|
|
||||||
$replace = $this->bool('replace') ?? true;
|
|
||||||
$public = $this->bool('shared') ?? true;
|
|
||||||
$unread = $this->bool('toread') ?? false;
|
|
||||||
|
|
||||||
$result_code = 'something went wrong';
|
|
||||||
[$pin, $existed] = Pin::checkExistingAndCreateOrUpdate(
|
|
||||||
args: [
|
|
||||||
'actor_id' => $user->getId(),
|
|
||||||
'url_hash' => hash('sha256', $url),
|
|
||||||
'url' => $url,
|
|
||||||
'replace' => $replace,
|
|
||||||
'public' => $public,
|
|
||||||
'unread' => $unread,
|
|
||||||
'modified' => $modified,
|
|
||||||
],
|
|
||||||
find_by_keys: ['actor_id', 'url_hash'],
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($existed) {
|
|
||||||
if (!$replace) {
|
|
||||||
$result_code = 'item already exists';
|
|
||||||
} else {
|
|
||||||
throw new ServerException('Updating is unimplemented'); // TODO delete old note, create new one
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DB::persist($note = Note::create([
|
|
||||||
'actor_id' => $user->getId(),
|
|
||||||
'content' => $url,
|
|
||||||
'content_type' => 'text/uri-list',
|
|
||||||
'rendered' => Formatting::twigRenderFile('pinboard/render.html.twig', ['url' => $url, 'title' => $title, 'description' => $description]),
|
|
||||||
'reply_to' => null,
|
|
||||||
'is_local' => true,
|
|
||||||
'source' => 'Pinboard API',
|
|
||||||
'scope' => $public ? VisibilityScope::EVERYWHERE->value : VisibilityScope::ADDRESSEE->value,
|
|
||||||
'language_id' => $user->getActor()->getTopLanguage()->getId(),
|
|
||||||
'type' => 'pin',
|
|
||||||
'title' => $title,
|
|
||||||
]));
|
|
||||||
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
|
|
||||||
$pin->setNoteId($note->getId());
|
|
||||||
Conversation::assignLocalConversation($note, null);
|
|
||||||
// TODO handle tags
|
|
||||||
DB::flush();
|
|
||||||
$result_code = 'done';
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse(
|
|
||||||
['result_code' => $result_code],
|
|
||||||
status: 200,
|
|
||||||
headers: ['content-type' => 'application/json'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
327
plugins/Pinboard/Controller/APIv1.php
Normal file
327
plugins/Pinboard/Controller/APIv1.php
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Plugin\Pinboard\Controller;
|
||||||
|
|
||||||
|
use App\Core\Cache;
|
||||||
|
use App\Core\Controller;
|
||||||
|
use App\Core\DB;
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
|
use App\Core\Router;
|
||||||
|
use App\Core\VisibilityScope;
|
||||||
|
use App\Entity\LocalUser;
|
||||||
|
use App\Entity\Note;
|
||||||
|
use App\Util\Exception\BugFoundException;
|
||||||
|
use App\Util\Exception\ClientException;
|
||||||
|
use App\Util\Exception\ServerException;
|
||||||
|
use App\Util\Formatting;
|
||||||
|
use Component\Conversation\Conversation;
|
||||||
|
use Component\Tag\Entity\NoteTag;
|
||||||
|
use Datetime;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Functional as F;
|
||||||
|
use Plugin\Pinboard\Entity\Pin;
|
||||||
|
use Plugin\Pinboard\Util;
|
||||||
|
use SimpleXMLElement;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||||
|
|
||||||
|
class APIv1 extends Controller
|
||||||
|
{
|
||||||
|
public const SOURCE = 'Pinboard API v1';
|
||||||
|
|
||||||
|
private function before()
|
||||||
|
{
|
||||||
|
$format = $this->string('format');
|
||||||
|
$auth_token = $this->string('auth_token');
|
||||||
|
if (\is_null($format)) {
|
||||||
|
return new Response(_m('Format must be specified'), status: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($format !== 'json') {
|
||||||
|
throw new ServerException(_m('Only JSON is supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_null($auth_token)) {
|
||||||
|
return new Response('API requires authentication', status: 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_null($user = Util::validateToken($auth_token))) {
|
||||||
|
return new Response('401 Forbidden', status: 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function respond(array $result, int $status = 200): Response
|
||||||
|
{
|
||||||
|
$format = $this->string('format');
|
||||||
|
if ($format === 'json') {
|
||||||
|
return new JsonResponse($result, status: $status);
|
||||||
|
} elseif ($format === 'xml') {
|
||||||
|
// $encoder = new XmlEncoder();
|
||||||
|
// $xml = $encoder->encode($result, 'xml');
|
||||||
|
[$tag_names, $keys] = F\zip(...F\map(array_keys($result), fn (string $k) => explode('_', $k)));
|
||||||
|
$xml = new SimpleXMLElement('<' . $tag_names[0] . '/>');
|
||||||
|
dd($tag_names, $keys, $result, $xml, (string) $xml);
|
||||||
|
$xml->addChild();
|
||||||
|
dd($xml);
|
||||||
|
return new Response(content: $xml, status: $statis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteNoteAndMaybePin(LocalUser $user, Note $note, ?Pin $pin): void
|
||||||
|
{
|
||||||
|
$note->delete($user->getActor(), self::SOURCE);
|
||||||
|
if (!\is_null($note)) {
|
||||||
|
DB::remove($pin);
|
||||||
|
}
|
||||||
|
DB::flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseTags(): array
|
||||||
|
{
|
||||||
|
$tags = [];
|
||||||
|
if (!\is_null($tags_text = $this->string('tags'))) {
|
||||||
|
Formatting::toArray($tags_text, $tags, Formatting::SPLIT_BY_BOTH);
|
||||||
|
}
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLatestModified(LocalUser $user): string
|
||||||
|
{
|
||||||
|
return Cache::get(
|
||||||
|
Pin::cacheKeys($user)['last-modified'],
|
||||||
|
fn () => DB::dql(
|
||||||
|
<<<'EOF'
|
||||||
|
select MAX(p.modified) as max from \Plugin\Pinboard\Entity\Pin p
|
||||||
|
where p.actor_id = :actor_id
|
||||||
|
group by p.modified
|
||||||
|
EOF,
|
||||||
|
['actor_id' => $user->getId()],
|
||||||
|
)[0]['max'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Pin[] $pins
|
||||||
|
*
|
||||||
|
* @return array json for response
|
||||||
|
*/
|
||||||
|
private function formatPins(LocalUser $user, array $pins): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => self::getLatestModified($user),
|
||||||
|
'user' => $user->getNickname(),
|
||||||
|
'posts' => F\map(
|
||||||
|
$pins,
|
||||||
|
fn (Pin $pin) => [
|
||||||
|
'href' => $pin->getUrl(),
|
||||||
|
'description' => $pin->getTitle(),
|
||||||
|
'extended' => $pin->getDescription(),
|
||||||
|
'meta' => hash('md5', $pin->getModified()->format(DateTimeInterface::ISO8601)),
|
||||||
|
'hash' => hash('md5', $pin->getUrl() . $pin->getTitle() . $pin->getDescription()), // idk...
|
||||||
|
'time' => $pin->getModified()->format(DateTimeInterface::ISO8601),
|
||||||
|
'shared' => $pin->getPublic(),
|
||||||
|
'toread' => $pin->getUnread(),
|
||||||
|
'tags' => implode(' ', F\map($pin->getTags(), fn (NoteTag $tag) => $tag->getTag())),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recent time a bookmark was added, updated or
|
||||||
|
* deleted. Use this before calling posts/all to see if the data
|
||||||
|
* has changed since the last fetch
|
||||||
|
*/
|
||||||
|
public function posts_update(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before();
|
||||||
|
|
||||||
|
return self::respond(['update_time' => self::getLatestModified($user)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a bookmark
|
||||||
|
*/
|
||||||
|
public function posts_add(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before();
|
||||||
|
|
||||||
|
if (\is_null($url = $this->string('url'))) {
|
||||||
|
throw new ClientException('URL must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_null($title = $this->string('description'))) { // Logically.
|
||||||
|
throw new ClientException('Desciption must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
$description = $this->string('extended') ?? '';
|
||||||
|
$tags = self::parseTags();
|
||||||
|
$modified = $this->string('dt') ?? new Datetime;
|
||||||
|
$replace = $this->bool('replace') ?? true;
|
||||||
|
$public = $this->bool('shared') ?? true;
|
||||||
|
$unread = $this->bool('toread') ?? false;
|
||||||
|
|
||||||
|
$result_code = 'something went wrong';
|
||||||
|
[$pin, $existed] = Pin::checkExistingAndCreateOrUpdate(
|
||||||
|
args: [
|
||||||
|
'actor_id' => $user->getId(),
|
||||||
|
'url_hash' => hash('sha256', $url),
|
||||||
|
'url' => $url,
|
||||||
|
'title' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'replace' => $replace,
|
||||||
|
'public' => $public,
|
||||||
|
'unread' => $unread,
|
||||||
|
'modified' => $modified,
|
||||||
|
],
|
||||||
|
find_by_keys: ['actor_id', 'url_hash'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existed) {
|
||||||
|
if (!$replace) {
|
||||||
|
$result_code = 'item already exists';
|
||||||
|
} else {
|
||||||
|
throw new ServerException('Updating is unimplemented'); // TODO delete old note, create new one
|
||||||
|
$this->deleteNoteAndMaybePin($user, $pin->getNote(), pin: null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DB::persist($note = Note::create([
|
||||||
|
'actor_id' => $user->getId(),
|
||||||
|
'content' => $url,
|
||||||
|
'content_type' => 'text/uri-list',
|
||||||
|
'rendered' => Formatting::twigRenderFile('pinboard/render.html.twig', ['url' => $url, 'title' => $title, 'description' => $description]),
|
||||||
|
'reply_to' => null,
|
||||||
|
'is_local' => true,
|
||||||
|
'source' => self::SOURCE,
|
||||||
|
'scope' => $public ? VisibilityScope::EVERYWHERE->value : VisibilityScope::ADDRESSEE->value,
|
||||||
|
'language_id' => $user->getActor()->getTopLanguage()->getId(),
|
||||||
|
'type' => 'page',
|
||||||
|
'title' => $title,
|
||||||
|
]));
|
||||||
|
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
|
||||||
|
$pin->setNoteId($note->getId());
|
||||||
|
Conversation::assignLocalConversation($note, null);
|
||||||
|
DB::persist($pin);
|
||||||
|
// TODO handle tags
|
||||||
|
DB::flush();
|
||||||
|
$result_code = 'done';
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::respond(['result_code' => $result_code]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a bookmark
|
||||||
|
*/
|
||||||
|
public function posts_delete(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before($request);
|
||||||
|
$url = $this->string('url');
|
||||||
|
if (\is_null($url)) {
|
||||||
|
throw new ClientException('URL must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pin = DB::findOneBy(Pin::class, ['actor_id' => $user->getId(), 'url_hash' => hash('sha256', $url)], return_null: true);
|
||||||
|
if (\is_null($pin)) {
|
||||||
|
return self::respond(['result_code' => 'item not found']);
|
||||||
|
} else {
|
||||||
|
$this->deleteNoteAndMaybePin($user, $pin->getNote(), $pin);
|
||||||
|
return self::respond(['result_code' => 'done']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns one or more posts on a single day matching the
|
||||||
|
* arguments. If no date or url is given, date of most recent
|
||||||
|
* bookmark will be used
|
||||||
|
*/
|
||||||
|
public function posts_get(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before($request);
|
||||||
|
$tags = self::parseTags();
|
||||||
|
$day = $this->string('dt');
|
||||||
|
$url = $this->string('url');
|
||||||
|
$meta = $this->bool('meta');
|
||||||
|
|
||||||
|
if (!\is_null($tags) && $tags !== []) {
|
||||||
|
throw new ClientException(_m('tags attribute not implemented'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_null($url) && \is_null($day)) {
|
||||||
|
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId(), 'gte' => ['modified' => new DateTimeImmutable('today')]]);
|
||||||
|
} elseif (!\is_null($day)) {
|
||||||
|
$day = new DateTimeImmutable($day);
|
||||||
|
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId(), 'gte' => ['modified' => $day], 'lt' => ['modified' => $day->modify('+1 day')]]);
|
||||||
|
} elseif (!\is_null($url)) {
|
||||||
|
$pins = DB::findBy(Pin::class, ['actor_id' => $user->getId(), 'url_hash' => hash('sha256', $url)]);
|
||||||
|
} else {
|
||||||
|
throw new BugFoundException('Wonky logic in pinboard/posts/get');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::respond(self::formatPins($user, $pins));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of the user's most recent posts, filtered by tag
|
||||||
|
*/
|
||||||
|
public function posts_recent(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before($request);
|
||||||
|
$tags = self::parseTags();
|
||||||
|
$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);
|
||||||
|
|
||||||
|
return self::respond(self::formatPins($user, $pins));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of dates with the number of posts at each date
|
||||||
|
*/
|
||||||
|
public function posts_dates(Request $request)
|
||||||
|
{
|
||||||
|
return self::respond(['result_code' => 'unimplemented']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all bookmarks in the user's account
|
||||||
|
*/
|
||||||
|
public function posts_all(Request $request)
|
||||||
|
{
|
||||||
|
$user = self::before($request);
|
||||||
|
$tags = self::parseTags();
|
||||||
|
$offset = $this->int('start');
|
||||||
|
$limit = $this->int('results');
|
||||||
|
$start_time = $this->string('fromdt');
|
||||||
|
$end_time = $this->string('todt');
|
||||||
|
$meta = $this->bool('meta');
|
||||||
|
|
||||||
|
if (!\is_null($tags) && $tags !== []) {
|
||||||
|
throw new ClientException('tags attribute not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
$criteria = ['actor_id' => $user->getId()];
|
||||||
|
if (!\is_null($start_time)) {
|
||||||
|
$criteria['gte'] = ['modified' => new DateTimeImmutable($start_time)];
|
||||||
|
}
|
||||||
|
if (!\is_null($end_time)) {
|
||||||
|
$criteria['lt'] = ['modified' => new DateTimeImmutable($end_time)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pins = DB::findBy(Pin::class, $criteria, order_by: ['modified' => 'asc'], offset: $offset, limit: $limit);
|
||||||
|
return self::respond(self::formatPins($user, $pins)['posts']);
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,10 @@ declare(strict_types = 1);
|
|||||||
namespace Plugin\Pinboard\Entity;
|
namespace Plugin\Pinboard\Entity;
|
||||||
|
|
||||||
use App\Core\Entity;
|
use App\Core\Entity;
|
||||||
|
use App\Entity\Actor;
|
||||||
|
use App\Entity\LocalUser;
|
||||||
|
use App\Entity\Note;
|
||||||
|
use Component\Tag\Entity\NoteTag;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
|
||||||
class Pin extends Entity
|
class Pin extends Entity
|
||||||
@ -15,6 +19,8 @@ class Pin extends Entity
|
|||||||
private int $actor_id;
|
private int $actor_id;
|
||||||
private string $url_hash;
|
private string $url_hash;
|
||||||
private string $url;
|
private string $url;
|
||||||
|
private string $title;
|
||||||
|
private ?string $description = null;
|
||||||
private bool $replace;
|
private bool $replace;
|
||||||
private bool $public;
|
private bool $public;
|
||||||
private bool $unread;
|
private bool $unread;
|
||||||
@ -64,6 +70,28 @@ class Pin extends Entity
|
|||||||
return $this->url;
|
return $this->url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): self
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): self
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
public function setReplace(bool $replace): self
|
public function setReplace(bool $replace): self
|
||||||
{
|
{
|
||||||
$this->replace = $replace;
|
$this->replace = $replace;
|
||||||
@ -111,21 +139,47 @@ class Pin extends Entity
|
|||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
// }}} Autocode
|
// }}} Autocode
|
||||||
|
|
||||||
|
public static function cacheKeys(int|LocalUser|Actor $user): array
|
||||||
|
{
|
||||||
|
$id = \is_int($user) ? $user : $user->getId();
|
||||||
|
return [
|
||||||
|
'last-modified' => "pinboard-pin-{$id}-last-modified",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNote(): Note
|
||||||
|
{
|
||||||
|
return Note::getById($this->getNoteId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return NoteTag[]
|
||||||
|
*/
|
||||||
|
public function getTags(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
public static function schemaDef(): array
|
public static function schemaDef(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'pinboard_pin',
|
'name' => 'pinboard_pin',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'note_id' => ['type' => 'int', 'not null' => true, 'description' => 'Id of the note this pin created'],
|
'note_id' => ['type' => 'int', 'not null' => true, 'description' => 'Id of the note this pin created'],
|
||||||
'actor_id' => ['type' => 'int', 'not null' => true, 'description' => 'Actor who created this note'],
|
'actor_id' => ['type' => 'int', 'not null' => true, 'description' => 'Actor who created this note'],
|
||||||
'url_hash' => ['type' => 'char', 'length' => 64, 'not null' => true, 'description' => 'Hash of the url, for indexing'],
|
'url_hash' => ['type' => 'char', 'length' => 64, 'not null' => true, 'description' => 'Hash of the url, for indexing'],
|
||||||
'url' => ['type' => 'text', 'not null' => true, 'description' => 'Plain URL this pin refers to (gets rendered in the corresponding note, useful for replace)'],
|
'url' => ['type' => 'text', 'not null' => true, 'description' => 'Plain URL this pin refers to (gets rendered in the corresponding note, useful for replace)'],
|
||||||
'replace' => ['type' => 'bool', 'not null' => true, 'description' => 'Replace any existing bookmark with this URL. Default is yes. If set to no, will throw an error if bookmark exists'],
|
'title' => ['type' => 'text', 'not null' => true, 'description' => 'Title given to this bookmark'],
|
||||||
'public' => ['type' => 'bool', 'not null' => true, 'description' => 'Whether private or public'],
|
'description' => ['type' => 'text', 'description' => 'Description given to this bookmark'],
|
||||||
'unread' => ['type' => 'bool', 'not null' => true, 'description' => 'Has this been read'],
|
'replace' => ['type' => 'bool', 'not null' => true, 'description' => 'Replace any existing bookmark with this URL. Default is yes. If set to no, will throw an error if bookmark exists'],
|
||||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'],
|
'public' => ['type' => 'bool', 'not null' => true, 'description' => 'Whether private or public'],
|
||||||
|
'unread' => ['type' => 'bool', 'not null' => true, 'description' => 'Has this been read'],
|
||||||
|
'modified' => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'],
|
||||||
],
|
],
|
||||||
'primary key' => ['actor_id', 'url_hash'],
|
'primary key' => ['actor_id', 'url_hash'],
|
||||||
|
'indexes' => [
|
||||||
|
'actor_modified_idx' => ['actor_id', 'modified'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,13 +44,37 @@ class Pinboard extends Plugin
|
|||||||
$r->connect(
|
$r->connect(
|
||||||
'pinboard_posts_update',
|
'pinboard_posts_update',
|
||||||
'/pinboard/v1/posts/update',
|
'/pinboard/v1/posts/update',
|
||||||
[C\API::class, 'posts_update'],
|
[C\APIv1::class, 'posts_update'],
|
||||||
);
|
);
|
||||||
|
|
||||||
$r->connect(
|
$r->connect(
|
||||||
'pinboard_posts_add',
|
'pinboard_posts_add',
|
||||||
'/pinboard/v1/posts/add',
|
'/pinboard/v1/posts/add',
|
||||||
[C\API::class, 'posts_add'],
|
[C\APIv1::class, 'posts_add'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$r->connect(
|
||||||
|
'pinboard_posts_delete',
|
||||||
|
'/pinboard/v1/posts/delete',
|
||||||
|
[C\APIv1::class, 'posts_delete'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$r->connect(
|
||||||
|
'pinboard_posts_get',
|
||||||
|
'/pinboard/v1/posts/get',
|
||||||
|
[C\APIv1::class, 'posts_get'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$r->connect(
|
||||||
|
'pinboard_posts_recent',
|
||||||
|
'/pinboard/v1/posts/recent',
|
||||||
|
[C\APIv1::class, 'posts_recent'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$r->connect(
|
||||||
|
'pinboard_posts_all',
|
||||||
|
'/pinboard/v1/posts/all',
|
||||||
|
[C\APIv1::class, 'posts_all'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Event::next;
|
return Event::next;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<details>
|
<details class="details-summary-title">
|
||||||
<summary><a href="{{ url }}" rel="noopener noreferrer" title="{{ title }}"></a></summary> {# User provided title, do not translate #}
|
<summary>{{ title }}</summary> {# User provided title, do not translate #}
|
||||||
{{ description }} {# Likewise for the description, which may be empty#}
|
<a href="{{ url }}" rel="noopener noreferrer">{{ url }}</a>
|
||||||
|
<p>{{ description }}</p> {# Likewise for the description, which may be empty #}
|
||||||
</details>
|
</details>
|
||||||
|
Loading…
Reference in New Issue
Block a user