[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;
|
||||
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Entity\Note;
|
||||
use Component\Tag\Entity\NoteTag;
|
||||
use DateTimeInterface;
|
||||
|
||||
class Pin extends Entity
|
||||
@ -15,6 +19,8 @@ class Pin extends Entity
|
||||
private int $actor_id;
|
||||
private string $url_hash;
|
||||
private string $url;
|
||||
private string $title;
|
||||
private ?string $description = null;
|
||||
private bool $replace;
|
||||
private bool $public;
|
||||
private bool $unread;
|
||||
@ -64,6 +70,28 @@ class Pin extends Entity
|
||||
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
|
||||
{
|
||||
$this->replace = $replace;
|
||||
@ -111,21 +139,47 @@ class Pin extends Entity
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} 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
|
||||
{
|
||||
return [
|
||||
'name' => 'pinboard_pin',
|
||||
'fields' => [
|
||||
'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'],
|
||||
'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)'],
|
||||
'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'],
|
||||
'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'],
|
||||
'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'],
|
||||
'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)'],
|
||||
'title' => ['type' => 'text', 'not null' => true, 'description' => 'Title given to this bookmark'],
|
||||
'description' => ['type' => 'text', 'description' => 'Description given to this bookmark'],
|
||||
'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'],
|
||||
'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'],
|
||||
'indexes' => [
|
||||
'actor_modified_idx' => ['actor_id', 'modified'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -44,13 +44,37 @@ class Pinboard extends Plugin
|
||||
$r->connect(
|
||||
'pinboard_posts_update',
|
||||
'/pinboard/v1/posts/update',
|
||||
[C\API::class, 'posts_update'],
|
||||
[C\APIv1::class, 'posts_update'],
|
||||
);
|
||||
|
||||
$r->connect(
|
||||
'pinboard_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;
|
||||
|
@ -1,4 +1,5 @@
|
||||
<details>
|
||||
<summary><a href="{{ url }}" rel="noopener noreferrer" title="{{ title }}"></a></summary> {# User provided title, do not translate #}
|
||||
{{ description }} {# Likewise for the description, which may be empty#}
|
||||
<details class="details-summary-title">
|
||||
<summary>{{ title }}</summary> {# User provided title, do not translate #}
|
||||
<a href="{{ url }}" rel="noopener noreferrer">{{ url }}</a>
|
||||
<p>{{ description }}</p> {# Likewise for the description, which may be empty #}
|
||||
</details>
|
||||
|
Loading…
Reference in New Issue
Block a user