notefeed 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +53 -26
- package/dist/client.d.ts +49 -11
- package/dist/client.js +114 -15
- package/dist/generated/index.d.ts +2 -2
- package/dist/generated/index.js +1 -1
- package/dist/generated/sdk.gen.d.ts +9 -9
- package/dist/generated/sdk.gen.js +12 -11
- package/dist/generated/types.gen.d.ts +100 -71
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,10 +27,10 @@ for await (const note of client.notes()) console.log(note.created_at, note.title
|
|
|
27
27
|
|
|
28
28
|
A feed can have its own password, which gates reading, listing and posting to it. Pass `feedPassword: "..."` to `new Client(...)` (or per call, e.g. `client.post(text, { feedPassword })`), or set `NOTEFEED_FEED_PASSWORD`, which `fromEnv()` and the `notefeed` command both read (there is deliberately no flag, so it stays out of shell history). It is sent as `X-Feed-Password`; `Authorization` stays the instance password. A feed password is 1 to 256 printable ASCII characters with no space at the start or end; an empty one is the same as none.
|
|
29
29
|
|
|
30
|
-
A posted note can be changed or removed: `await client.edit(id,
|
|
30
|
+
A posted note can be changed or removed: `await client.edit(id, content)` replaces its content and returns the note (its id and URLs stay), and `await client.delete(id)` removes it. Both take the same `{ feed, feedPassword }` options as `post`, and a missing note is a `NotFoundError`. `notefeed edit <id> <text | - | --file PATH>` prints the note URL; `notefeed delete <id>` prints nothing. Anyone who can post to a feed can edit and delete its notes, and deleting is permanent.
|
|
31
31
|
|
|
32
32
|
A feed can have a title and a description, and can be deleted: `await client.feedInfo()` returns `{ name, title, description, protected, read_url }`, `await client.updateFeed({ title, description })` replaces both (an empty string clears one) and returns the feed, and `await client.deleteFeed()` removes the feed with all its notes for good. All three take the same `{ feed, feedPassword }` options as `post`, and a feed that doesn't exist is a `NotFoundError`. Anyone who can post to a feed can do this, and deleting is permanent; there are no `notefeed` commands for it.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
A picture is a note of its own: `await client.post(bytes, { type: "image/png", alt: "…" })` takes a `Uint8Array` or a `Blob` (PNG, JPEG, GIF or WebP; a Blob's own type is used if you leave `type` out) and returns what a markdown post returns, with the picture's `file` and `file_url` added. Write `` in a markdown note to show it; the feed is created by its first note, and a picture that is too large is a `NoteTooLargeError`. The server checks the bytes against the declared type. `await client.update(id, { title, alt })` changes a note's title or a picture's alt text, and `await client.edit(id, content)` replaces its content. `await client.updateFeed({ title, description, image: file })` makes a picture the feed's title image (`image: ""` removes it; leaving `image` out keeps it). `notefeed post --file photo.png` posts a file (the type comes from the extension, or `--type`). To post a note with its pictures in one call, `await client.post(text, { attachments: [{ name: "chart.png", content: bytes, type: "image/png" }] })` posts them first and swaps `` for each file name (`notefeed post "…" --attach chart.png` does the same); a failure carries the pictures already posted as `posted`. Pictures are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
|
|
35
35
|
|
|
36
36
|
Full documentation: https://docs.notefeed.me/clients/
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/** `notefeed post`, `edit`, `
|
|
1
|
+
/** `notefeed post`, `edit`, `update`, `delete` and `notes`: post, change, remove and read notes, markdown or any accepted file, from the command line. */
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename, extname } from "node:path";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { Client, ConfigError, NotefeedError } from "./client.js";
|
|
6
7
|
class UsageError extends Error {
|
|
7
8
|
}
|
|
8
9
|
const USAGE = [
|
|
9
|
-
"usage: notefeed post <text | - | --file PATH> [--tag TAG]... [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
10
|
-
" notefeed edit <id> <text | - | --file PATH> [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
10
|
+
"usage: notefeed post <text | - | --file PATH> [--type MEDIA_TYPE] [--title TITLE] [--tag TAG]... [--attach PATH]... [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
11
|
+
" notefeed edit <id> <text | - | --file PATH> [--type MEDIA_TYPE] [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
12
|
+
" notefeed update <id> [--title TITLE] [--alt ALT] [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
11
13
|
" notefeed delete <id> [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
12
|
-
" notefeed image <PATH> [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
13
14
|
" notefeed notes [--limit N] [--tag TAG] [--json] [--url URL] [--feed FEED] [--password PASSWORD]",
|
|
14
15
|
].join("\n");
|
|
16
|
+
// The media type of a file by its extension: the types the server accepts.
|
|
17
|
+
const TYPES = { ".md": "text/markdown", ".markdown": "text/markdown", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
15
18
|
/** Returns the exit code: 0 ok, 1 server/network error, 2 usage/config error. */
|
|
16
19
|
export async function main(argv, io = process) {
|
|
17
20
|
try {
|
|
@@ -23,7 +26,11 @@ export async function main(argv, io = process) {
|
|
|
23
26
|
feed: { type: "string" },
|
|
24
27
|
password: { type: "string" },
|
|
25
28
|
file: { type: "string" },
|
|
29
|
+
type: { type: "string" },
|
|
30
|
+
title: { type: "string" },
|
|
31
|
+
alt: { type: "string" },
|
|
26
32
|
tag: { type: "string", multiple: true },
|
|
33
|
+
attach: { type: "string", multiple: true },
|
|
27
34
|
limit: { type: "string" },
|
|
28
35
|
json: { type: "boolean" },
|
|
29
36
|
version: { type: "boolean" },
|
|
@@ -41,29 +48,27 @@ export async function main(argv, io = process) {
|
|
|
41
48
|
}
|
|
42
49
|
const [command, text, ...rest] = positionals;
|
|
43
50
|
if (command === "post" && !rest.length) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
51
|
+
const input = await read(text, values.file, values.type, io);
|
|
52
|
+
const attachments = await Promise.all((values.attach ?? []).map(attachment));
|
|
53
|
+
const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, name: input.name, attachments });
|
|
54
|
+
io.stdout.write(`${[note.url, ...note.attachments.map((a) => a.url)].join("\n")}\n`);
|
|
47
55
|
return 0;
|
|
48
56
|
}
|
|
49
57
|
if (command === "edit" && text !== undefined && rest.length <= 1) {
|
|
50
|
-
const
|
|
58
|
+
const input = await read(rest[0], values.file, values.type, io);
|
|
59
|
+
const note = await client(values).edit(text, input.content, { type: input.type });
|
|
51
60
|
io.stdout.write(`${note.url}\n`);
|
|
52
61
|
return 0;
|
|
53
62
|
}
|
|
54
|
-
if (command === "
|
|
55
|
-
|
|
63
|
+
if (command === "update" && text !== undefined && !rest.length) {
|
|
64
|
+
if (values.title === undefined && values.alt === undefined)
|
|
65
|
+
throw new UsageError("give --title and/or --alt");
|
|
66
|
+
const note = await client(values).update(text, { title: values.title, alt: values.alt });
|
|
67
|
+
io.stdout.write(`${note.url}\n`);
|
|
56
68
|
return 0;
|
|
57
69
|
}
|
|
58
|
-
if (command === "
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
bytes = await readFile(text);
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
throw new UsageError(`cannot read ${text}: ${e.code ?? e.message}`);
|
|
65
|
-
}
|
|
66
|
-
io.stdout.write(`${(await client(values).uploadImage(bytes)).markdown}\n`);
|
|
70
|
+
if (command === "delete" && text !== undefined && !rest.length) {
|
|
71
|
+
await client(values).delete(text);
|
|
67
72
|
return 0;
|
|
68
73
|
}
|
|
69
74
|
if (command === "notes" && text === undefined) {
|
|
@@ -75,7 +80,7 @@ export async function main(argv, io = process) {
|
|
|
75
80
|
// To the second, UTC: the same form as the Python CLI, so scripts read either the same way.
|
|
76
81
|
const when = new Date(n.created_at).toISOString().replace(/\.\d+Z$/, "Z");
|
|
77
82
|
// --json: exactly the documented fields, like the Python CLI, even if the server adds more.
|
|
78
|
-
const fields = { id: n.id, title: n.title,
|
|
83
|
+
const fields = { id: n.id, type: n.type, title: n.title, ...(n.content !== undefined && { content: n.content }), file: n.file, file_url: n.file_url, size: n.size, tags: n.tags, created_at: when, url: n.url };
|
|
79
84
|
io.stdout.write(values.json ? `${JSON.stringify(fields)}\n` : `${when} ${n.title || n.id} ${n.url}\n`);
|
|
80
85
|
if (--left === 0)
|
|
81
86
|
break;
|
|
@@ -86,6 +91,9 @@ export async function main(argv, io = process) {
|
|
|
86
91
|
}
|
|
87
92
|
catch (e) {
|
|
88
93
|
io.stderr.write(`notefeed: ${e.message.split("\n")[0]}\n`);
|
|
94
|
+
if (e instanceof NotefeedError)
|
|
95
|
+
for (const p of e.posted)
|
|
96
|
+
io.stderr.write(`posted: ${p.url}\n`);
|
|
89
97
|
if (e instanceof ConfigError || e instanceof UsageError)
|
|
90
98
|
return 2;
|
|
91
99
|
if (e instanceof NotefeedError)
|
|
@@ -108,26 +116,45 @@ function client(values) {
|
|
|
108
116
|
feedPassword: process.env.NOTEFEED_FEED_PASSWORD,
|
|
109
117
|
});
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
// What a post or an edit sends: text on the command line or stdin is markdown; a file is sent as it is, as `--type` or its extension says.
|
|
120
|
+
async function read(text, file, type, io) {
|
|
112
121
|
if (file) {
|
|
122
|
+
const media = type ?? TYPES[extname(file).toLowerCase()];
|
|
123
|
+
if (!media)
|
|
124
|
+
throw new UsageError(`cannot tell the type of ${file}: pass --type (${Object.values(TYPES).filter((t, i, a) => a.indexOf(t) === i).join(", ")})`);
|
|
125
|
+
let bytes;
|
|
113
126
|
try {
|
|
114
|
-
|
|
127
|
+
bytes = await readFile(file);
|
|
115
128
|
}
|
|
116
129
|
catch (e) {
|
|
117
|
-
if (e instanceof UsageError)
|
|
118
|
-
throw e;
|
|
119
130
|
throw new UsageError(`cannot read ${file}: ${e.code ?? e.message}`);
|
|
120
131
|
}
|
|
132
|
+
return { content: media === "text/markdown" ? utf8(bytes, file) : bytes, type: media, name: basename(file) };
|
|
121
133
|
}
|
|
122
134
|
if (text === "-") {
|
|
123
135
|
const chunks = [];
|
|
124
136
|
for await (const c of io.stdin)
|
|
125
137
|
chunks.push(typeof c === "string" ? Buffer.from(c) : c);
|
|
126
|
-
return utf8(Buffer.concat(chunks), "stdin");
|
|
138
|
+
return { content: utf8(Buffer.concat(chunks), "stdin"), type: type ?? "text/markdown" };
|
|
127
139
|
}
|
|
128
140
|
if (text === undefined)
|
|
129
141
|
throw new UsageError('give the note text, "-" for stdin, or --file PATH');
|
|
130
|
-
return text;
|
|
142
|
+
return { content: text, type: type ?? "text/markdown" };
|
|
143
|
+
}
|
|
144
|
+
// A picture to post first: its name is the file's, its type comes from the extension.
|
|
145
|
+
async function attachment(path) {
|
|
146
|
+
const type = TYPES[extname(path).toLowerCase()];
|
|
147
|
+
if (!type || type === "text/markdown")
|
|
148
|
+
throw new UsageError(`cannot attach ${path}: a picture (${Object.keys(TYPES).filter((e) => TYPES[e] !== "text/markdown").join(", ")}) is expected`);
|
|
149
|
+
const name = basename(path);
|
|
150
|
+
if (!/^(?!\.+$)[A-Za-z0-9._-]+$/.test(name))
|
|
151
|
+
throw new UsageError(`cannot attach ${path}: its file name "${name}" is what the text refers to it by, so it may only have letters, digits, ., _ and -: rename the file`);
|
|
152
|
+
try {
|
|
153
|
+
return { name, content: await readFile(path), type };
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
throw new UsageError(`cannot read ${path}: ${e.code ?? e.message}`);
|
|
157
|
+
}
|
|
131
158
|
}
|
|
132
159
|
function utf8(bytes, source) {
|
|
133
160
|
try {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import type { Created, Error as ApiError, Feed, FeedSettings,
|
|
1
|
+
import type { Created, Error as ApiError, Feed, FeedSettings, Note } from "./generated/types.gen.js";
|
|
2
2
|
/** The stable error codes the API answers with. */
|
|
3
3
|
export type ErrorCode = NonNullable<ApiError["code"]>;
|
|
4
|
-
export type { Created, Feed, FeedSettings,
|
|
4
|
+
export type { Created, Feed, FeedSettings, Note };
|
|
5
|
+
/** A picture posted with a note: `` in the markdown refers to it. `type` as for post(). */
|
|
6
|
+
export type Attachment = {
|
|
7
|
+
name: string;
|
|
8
|
+
content: Uint8Array | Blob;
|
|
9
|
+
type?: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
};
|
|
5
12
|
/** `timeoutMs` (default 10000) limits each whole request, including reading the answer. */
|
|
6
13
|
export type ClientOptions = {
|
|
7
14
|
url: string;
|
|
@@ -15,6 +22,9 @@ export declare class NotefeedError extends Error {
|
|
|
15
22
|
readonly status: number | null;
|
|
16
23
|
readonly code: ErrorCode | null;
|
|
17
24
|
constructor(message: string, status?: number | null, code?: ErrorCode | null);
|
|
25
|
+
/** Set when a post with `attachments` failed on one of them: its name, and the images already posted (the text note is not). */
|
|
26
|
+
attachment: string | null;
|
|
27
|
+
posted: Created[];
|
|
18
28
|
}
|
|
19
29
|
/** Raised before sending: no URL, no feed, an invalid feed name or password. */
|
|
20
30
|
export declare class ConfigError extends NotefeedError {
|
|
@@ -49,19 +59,47 @@ export declare class Client {
|
|
|
49
59
|
constructor(options: ClientOptions);
|
|
50
60
|
/** NOTEFEED_URL, NOTEFEED_FEED, NOTEFEED_PASSWORD and NOTEFEED_FEED_PASSWORD. */
|
|
51
61
|
static fromEnv(env?: Record<string, string | undefined>): Client;
|
|
52
|
-
/**
|
|
53
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Post a note: a string is markdown, bytes or a Blob are a file whose media type is `type` (a Blob's own `type` if left out): `text/markdown`,
|
|
64
|
+
* `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
|
|
65
|
+
* The feed is created by its first note. `Created.file` is the note's file name: write `` in a markdown note to show a picture, or
|
|
66
|
+
* pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
|
|
67
|
+
*
|
|
68
|
+
* `feedPassword` overrides the client's, for a feed that has its own password. `tags` label the note (at most 10, each 1 to 32 characters of
|
|
69
|
+
* letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
|
|
70
|
+
* text. `alt` is a picture's alternative text, `name` the file's original name. `readId` is the read id the feed gets when this post creates it
|
|
71
|
+
* (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; random when left out; ignored for a feed that exists; a `taken` error when another feed has it).
|
|
72
|
+
*
|
|
73
|
+
* `attachments` (only with a markdown string) are pictures posted first, one note each, in order: `` in the markdown shows one (an
|
|
74
|
+
* attachment the text never refers to is added at the end), and `Created.attachments` lists them. `tags` label the images too. The options
|
|
75
|
+
* are checked before anything is sent. If an upload fails, its error is thrown with `attachment` (the name) and `posted` (the images already
|
|
76
|
+
* posted, which stay) set; the text note is not posted.
|
|
77
|
+
*/
|
|
78
|
+
post(content: string | Uint8Array | Blob, options?: {
|
|
54
79
|
feed?: string;
|
|
55
80
|
feedPassword?: string;
|
|
81
|
+
type?: string;
|
|
82
|
+
title?: string;
|
|
56
83
|
tags?: string[];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
alt?: string;
|
|
85
|
+
name?: string;
|
|
86
|
+
readId?: string;
|
|
87
|
+
attachments?: Attachment[];
|
|
88
|
+
}): Promise<Created & {
|
|
89
|
+
attachments: Created[];
|
|
90
|
+
}>;
|
|
91
|
+
private postOne;
|
|
92
|
+
/** Replace a note's content (a string is markdown; bytes and Blobs need the note's own `type`, as for post()). Its id, URLs and metadata stay. Same options as post(). */
|
|
93
|
+
edit(id: string, content: string | Uint8Array | Blob, options?: {
|
|
60
94
|
feed?: string;
|
|
61
95
|
feedPassword?: string;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
type?: string;
|
|
97
|
+
}): Promise<Note>;
|
|
98
|
+
/** Set a note's title and/or alt text (alt for pictures); `""` removes one, so a markdown note's title follows its text again. Same options as post(). */
|
|
99
|
+
update(id: string, meta: {
|
|
100
|
+
title?: string;
|
|
101
|
+
alt?: string;
|
|
102
|
+
}, options?: {
|
|
65
103
|
feed?: string;
|
|
66
104
|
feedPassword?: string;
|
|
67
105
|
}): Promise<Note>;
|
|
@@ -75,7 +113,7 @@ export declare class Client {
|
|
|
75
113
|
feed?: string;
|
|
76
114
|
feedPassword?: string;
|
|
77
115
|
}): Promise<Feed>;
|
|
78
|
-
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. The feed must already exist. Same options as post(). */
|
|
116
|
+
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. `read_id` gives the feed another read id (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; `""` for a random one; left out keeps it); the old one is freed, and relative image links in notes follow while full URLs do not. The feed must already exist. Same options as post(). */
|
|
79
117
|
updateFeed(settings: FeedSettings, options?: {
|
|
80
118
|
feed?: string;
|
|
81
119
|
feedPassword?: string;
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* OpenAPI description (./generated, `npm run generate` in the repo). No runtime dependencies.
|
|
4
4
|
*/
|
|
5
5
|
import { createClient, createConfig } from "./generated/client/index.js";
|
|
6
|
-
import { deleteFeed, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, postNote, updateFeed
|
|
6
|
+
import { deleteFeed, deleteNote, editNote, getFeed, getNote, getReadNote, listNotes, listReadNotes, patchNote, postNote, updateFeed } from "./generated/sdk.gen.js";
|
|
7
7
|
/** Any failure talking to notefeed. `status` and `code` are null when there was no API answer. */
|
|
8
8
|
export class NotefeedError extends Error {
|
|
9
9
|
status;
|
|
@@ -14,6 +14,9 @@ export class NotefeedError extends Error {
|
|
|
14
14
|
this.code = code;
|
|
15
15
|
this.name = new.target.name;
|
|
16
16
|
}
|
|
17
|
+
/** Set when a post with `attachments` failed on one of them: its name, and the images already posted (the text note is not). */
|
|
18
|
+
attachment = null;
|
|
19
|
+
posted = [];
|
|
17
20
|
}
|
|
18
21
|
/** Raised before sending: no URL, no feed, an invalid feed name or password. */
|
|
19
22
|
export class ConfigError extends NotefeedError {
|
|
@@ -54,9 +57,36 @@ const BY_CODE = {
|
|
|
54
57
|
invalid_body: InvalidRequestError,
|
|
55
58
|
invalid_request: InvalidRequestError,
|
|
56
59
|
feed_exists: InvalidRequestError,
|
|
60
|
+
taken: InvalidRequestError,
|
|
57
61
|
unsupported_type: InvalidRequestError,
|
|
58
62
|
};
|
|
63
|
+
const ATTACHMENT_NAME = /^(?!\.+$)[A-Za-z0-9._-]+$/; // same rule as the server's MCP tool: safe to write in 
|
|
64
|
+
const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
59
65
|
const FEED_RE = /^[a-z0-9_-]{1,64}$/; // same rule as the server; reserved names still come back as a 400
|
|
66
|
+
// A header holds bytes, and fetch refuses a character above U+00FF: text that may not be ASCII goes as its UTF-8 bytes written as
|
|
67
|
+
// latin1 characters, which is what the server reads back.
|
|
68
|
+
const headerValue = (text) => Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join("");
|
|
69
|
+
// What a post sends: a string is markdown; bytes and Blobs need a media type (a Blob's own, if it has one).
|
|
70
|
+
function asFile(content, type) {
|
|
71
|
+
if (typeof content === "string")
|
|
72
|
+
return { body: new Blob([content]), type: type ?? "text/markdown" };
|
|
73
|
+
const media = type ?? (content instanceof Blob ? content.type : "");
|
|
74
|
+
if (!media)
|
|
75
|
+
throw new ConfigError("give the file's media type as `type`, e.g. image/png");
|
|
76
|
+
return { body: content instanceof Blob ? content : new Blob([content]), type: media };
|
|
77
|
+
}
|
|
78
|
+
// The markdown with each `` of `sent` (name → file) pointing at its file; a name the text never refers to is appended as ``.
|
|
79
|
+
function placeImages(markdown, sent) {
|
|
80
|
+
const used = new Set();
|
|
81
|
+
const text = markdown.replace(/(!\[[^\]]*\]\()([^)\s]+)(\))/g, (m, head, dest, tail) => {
|
|
82
|
+
if (!sent.has(dest))
|
|
83
|
+
return m;
|
|
84
|
+
used.add(dest);
|
|
85
|
+
return head + sent.get(dest) + tail;
|
|
86
|
+
});
|
|
87
|
+
const rest = [...sent].filter(([name]) => !used.has(name)).map(([, file]) => ``);
|
|
88
|
+
return rest.length === 0 ? text : [text.trimEnd(), ...rest].filter((p) => p !== "").join("\n\n");
|
|
89
|
+
}
|
|
60
90
|
export class Client {
|
|
61
91
|
url;
|
|
62
92
|
feed;
|
|
@@ -87,23 +117,92 @@ export class Client {
|
|
|
87
117
|
feedPassword: env.NOTEFEED_FEED_PASSWORD,
|
|
88
118
|
});
|
|
89
119
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Post a note: a string is markdown, bytes or a Blob are a file whose media type is `type` (a Blob's own `type` if left out): `text/markdown`,
|
|
122
|
+
* `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
|
|
123
|
+
* The feed is created by its first note. `Created.file` is the note's file name: write `` in a markdown note to show a picture, or
|
|
124
|
+
* pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
|
|
125
|
+
*
|
|
126
|
+
* `feedPassword` overrides the client's, for a feed that has its own password. `tags` label the note (at most 10, each 1 to 32 characters of
|
|
127
|
+
* letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
|
|
128
|
+
* text. `alt` is a picture's alternative text, `name` the file's original name. `readId` is the read id the feed gets when this post creates it
|
|
129
|
+
* (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; random when left out; ignored for a feed that exists; a `taken` error when another feed has it).
|
|
130
|
+
*
|
|
131
|
+
* `attachments` (only with a markdown string) are pictures posted first, one note each, in order: `` in the markdown shows one (an
|
|
132
|
+
* attachment the text never refers to is added at the end), and `Created.attachments` lists them. `tags` label the images too. The options
|
|
133
|
+
* are checked before anything is sent. If an upload fails, its error is thrown with `attachment` (the name) and `posted` (the images already
|
|
134
|
+
* posted, which stay) set; the text note is not posted.
|
|
135
|
+
*/
|
|
136
|
+
async post(content, options = {}) {
|
|
137
|
+
const { attachments = [], ...own } = options;
|
|
138
|
+
if (attachments.length === 0)
|
|
139
|
+
return { ...(await this.postOne(content, own)), attachments: [] };
|
|
140
|
+
if (typeof content !== "string")
|
|
141
|
+
throw new ConfigError("attachments go with a markdown string");
|
|
142
|
+
if ((own.type ?? "text/markdown") !== "text/markdown")
|
|
143
|
+
throw new ConfigError("attachments go with a markdown string");
|
|
144
|
+
const types = attachments.map((a) => {
|
|
145
|
+
if (!ATTACHMENT_NAME.test(a.name))
|
|
146
|
+
throw new ConfigError(`attachment name "${a.name}": use letters, digits, ., _ and - only`);
|
|
147
|
+
const type = asFile(a.content, a.type).type;
|
|
148
|
+
if (!IMAGE_TYPES.includes(type))
|
|
149
|
+
throw new ConfigError(`attachment "${a.name}" must be a picture (${IMAGE_TYPES.join(", ")})`);
|
|
150
|
+
return type;
|
|
151
|
+
});
|
|
152
|
+
const dup = attachments.find((a, i) => attachments.findIndex((b) => b.name === a.name) !== i);
|
|
153
|
+
if (dup)
|
|
154
|
+
throw new ConfigError(`attachment "${dup.name}" is given twice`);
|
|
155
|
+
const posted = [];
|
|
156
|
+
const sent = new Map();
|
|
157
|
+
for (const [i, a] of attachments.entries()) {
|
|
158
|
+
try {
|
|
159
|
+
const done = await this.postOne(a.content, { feed: own.feed, feedPassword: own.feedPassword, type: types[i], alt: a.alt, name: a.name, tags: own.tags, readId: own.readId });
|
|
160
|
+
posted.push(done);
|
|
161
|
+
sent.set(a.name, done.file);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof NotefeedError) {
|
|
165
|
+
e.message = `attachment "${a.name}": ${e.message}`;
|
|
166
|
+
e.attachment = a.name;
|
|
167
|
+
e.posted = posted;
|
|
168
|
+
}
|
|
169
|
+
throw e;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
return { ...(await this.postOne(placeImages(content, sent), own)), attachments: posted };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (e instanceof NotefeedError)
|
|
177
|
+
e.posted = posted; // the text failed: the images stay, `attachment` stays null
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async postOne(content, options) {
|
|
92
182
|
const feed = this.feedFor(options.feed);
|
|
93
|
-
const
|
|
94
|
-
|
|
183
|
+
const { body, type } = asFile(content, options.type);
|
|
184
|
+
const headers = {
|
|
185
|
+
...this.opts(options.feedPassword).headers,
|
|
186
|
+
"Content-Type": type,
|
|
187
|
+
...(options.title && { "X-Note-Title": headerValue(options.title) }),
|
|
188
|
+
...(options.tags?.length && { "X-Note-Tags": options.tags.join(",") }),
|
|
189
|
+
...(options.alt && { "X-Note-Alt": headerValue(options.alt) }),
|
|
190
|
+
...(options.name && { "X-Note-Name": headerValue(options.name) }),
|
|
191
|
+
...(options.readId && { "X-Read-Id": options.readId }),
|
|
192
|
+
};
|
|
193
|
+
return this.call(postNote({ client: this.api, path: { feed }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
|
|
95
194
|
}
|
|
96
|
-
/**
|
|
97
|
-
async
|
|
195
|
+
/** Replace a note's content (a string is markdown; bytes and Blobs need the note's own `type`, as for post()). Its id, URLs and metadata stay. Same options as post(). */
|
|
196
|
+
async edit(id, content, options = {}) {
|
|
98
197
|
const feed = this.feedFor(options.feed);
|
|
99
|
-
const body
|
|
100
|
-
const headers = { ...this.opts(options.feedPassword).headers, "Content-Type":
|
|
101
|
-
return this.call(
|
|
198
|
+
const { body, type } = asFile(content, options.type);
|
|
199
|
+
const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": type };
|
|
200
|
+
return this.call(editNote({ client: this.api, path: { feed, id }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
|
|
102
201
|
}
|
|
103
|
-
/**
|
|
104
|
-
async
|
|
202
|
+
/** Set a note's title and/or alt text (alt for pictures); `""` removes one, so a markdown note's title follows its text again. Same options as post(). */
|
|
203
|
+
async update(id, meta, options = {}) {
|
|
105
204
|
const feed = this.feedFor(options.feed);
|
|
106
|
-
return this.call(
|
|
205
|
+
return this.call(patchNote({ client: this.api, path: { feed, id }, body: meta, ...this.opts(options.feedPassword) }));
|
|
107
206
|
}
|
|
108
207
|
/** Remove a note for good. The feed stays, even with no notes left. Same options as post(). */
|
|
109
208
|
async delete(id, options = {}) {
|
|
@@ -115,10 +214,10 @@ export class Client {
|
|
|
115
214
|
const feed = this.feedFor(options.feed);
|
|
116
215
|
return this.call(getFeed({ client: this.api, path: { feed }, ...this.opts(options.feedPassword) }));
|
|
117
216
|
}
|
|
118
|
-
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. The feed must already exist. Same options as post(). */
|
|
217
|
+
/** Replace the feed's title and description (both; an empty string clears one), and set (`image`: a file name from uploadImage) or clear (`""`) the title image; leave `image` out to keep it. `read_id` gives the feed another read id (3 to 64 characters of `a-z`, `0-9`, `-`, `_`; `""` for a random one; left out keeps it); the old one is freed, and relative image links in notes follow while full URLs do not. The feed must already exist. Same options as post(). */
|
|
119
218
|
async updateFeed(settings, options = {}) {
|
|
120
219
|
const feed = this.feedFor(options.feed);
|
|
121
|
-
const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }) };
|
|
220
|
+
const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }), ...(settings.read_id === undefined ? {} : { read_id: settings.read_id }) };
|
|
122
221
|
return this.call(updateFeed({ client: this.api, path: { feed }, body, ...this.opts(options.feedPassword) }));
|
|
123
222
|
}
|
|
124
223
|
/** Delete the feed with all its notes, settings and password, for good; its name is free again. Same options as post(). */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, postNote, removeFeedPassword, updateFeed
|
|
2
|
-
export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, DeleteFeedData, DeleteFeedError, DeleteFeedErrors, DeleteFeedResponse, DeleteFeedResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, EditNoteData, EditNoteError, EditNoteErrors, EditNoteResponse, EditNoteResponses, Error, Feed, FeedSettings, GetFeedData, GetFeedError, GetFeedErrors, GetFeedResponse, GetFeedResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetOpenApiData, GetOpenApiResponse, GetOpenApiResponses, GetReadFeedData, GetReadFeedError, GetReadFeedErrors, GetReadFeedResponse, GetReadFeedResponses, GetReadNoteData, GetReadNoteError, GetReadNoteErrors, GetReadNoteResponse, GetReadNoteResponses,
|
|
1
|
+
export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, type Options, patchNote, postNote, removeFeedPassword, updateFeed } from './sdk.gen.js';
|
|
2
|
+
export type { ChangeFeedPasswordData, ChangeFeedPasswordError, ChangeFeedPasswordErrors, ChangeFeedPasswordResponse, ChangeFeedPasswordResponses, ClientOptions, Created, DeleteFeedData, DeleteFeedError, DeleteFeedErrors, DeleteFeedResponse, DeleteFeedResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, EditNoteData, EditNoteError, EditNoteErrors, EditNoteResponse, EditNoteResponses, Error, Feed, FeedSettings, GetFeedData, GetFeedError, GetFeedErrors, GetFeedResponse, GetFeedResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetOpenApiData, GetOpenApiResponse, GetOpenApiResponses, GetReadFeedData, GetReadFeedError, GetReadFeedErrors, GetReadFeedResponse, GetReadFeedResponses, GetReadNoteData, GetReadNoteError, GetReadNoteErrors, GetReadNoteResponse, GetReadNoteResponses, ListNotesData, ListNotesError, ListNotesErrors, ListNotesResponse, ListNotesResponses, ListReadNotesData, ListReadNotesError, ListReadNotesErrors, ListReadNotesResponse, ListReadNotesResponses, Note, NoteList, NoteMeta, PasswordJson, PatchNoteData, PatchNoteError, PatchNoteErrors, PatchNoteResponse, PatchNoteResponses, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, ReadFeed, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
|
package/dist/generated/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated by @hey-api/openapi-ts
|
|
2
|
-
export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, postNote, removeFeedPassword, updateFeed
|
|
2
|
+
export { changeFeedPassword, deleteFeed, deleteNote, editNote, getFeed, getNote, getOpenApi, getReadFeed, getReadNote, listNotes, listReadNotes, patchNote, postNote, removeFeedPassword, updateFeed } from './sdk.gen.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Client, ClientMeta, Options as Options2, RequestResult, TDataShape } from './client/index.js';
|
|
2
|
-
import type { ChangeFeedPasswordData, ChangeFeedPasswordErrors, ChangeFeedPasswordResponses, DeleteFeedData, DeleteFeedErrors, DeleteFeedResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, EditNoteData, EditNoteErrors, EditNoteResponses, GetFeedData, GetFeedErrors, GetFeedResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetOpenApiData, GetOpenApiResponses, GetReadFeedData, GetReadFeedErrors, GetReadFeedResponses, GetReadNoteData, GetReadNoteErrors, GetReadNoteResponses, ListNotesData, ListNotesErrors, ListNotesResponses, ListReadNotesData, ListReadNotesErrors, ListReadNotesResponses, PostNoteData, PostNoteErrors, PostNoteResponses, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedErrors, UpdateFeedResponses
|
|
2
|
+
import type { ChangeFeedPasswordData, ChangeFeedPasswordErrors, ChangeFeedPasswordResponses, DeleteFeedData, DeleteFeedErrors, DeleteFeedResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, EditNoteData, EditNoteErrors, EditNoteResponses, GetFeedData, GetFeedErrors, GetFeedResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetOpenApiData, GetOpenApiResponses, GetReadFeedData, GetReadFeedErrors, GetReadFeedResponses, GetReadNoteData, GetReadNoteErrors, GetReadNoteResponses, ListNotesData, ListNotesErrors, ListNotesResponses, ListReadNotesData, ListReadNotesErrors, ListReadNotesResponses, PatchNoteData, PatchNoteErrors, PatchNoteResponses, PostNoteData, PostNoteErrors, PostNoteResponses, RemoveFeedPasswordData, RemoveFeedPasswordErrors, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedErrors, UpdateFeedResponses } from './types.gen.js';
|
|
3
3
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
|
4
4
|
/**
|
|
5
5
|
* You can provide a client instance returned by `createClient()` instead of
|
|
@@ -22,7 +22,7 @@ export declare const listNotes: <ThrowOnError extends boolean = false>(options:
|
|
|
22
22
|
/**
|
|
23
23
|
* Post a note
|
|
24
24
|
*
|
|
25
|
-
* Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password
|
|
25
|
+
* The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES, default 5 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name).
|
|
26
26
|
*/
|
|
27
27
|
export declare const postNote: <ThrowOnError extends boolean = false>(options: Options<PostNoteData, ThrowOnError>) => RequestResult<PostNoteResponses, PostNoteErrors, ThrowOnError>;
|
|
28
28
|
/**
|
|
@@ -36,17 +36,17 @@ export declare const deleteNote: <ThrowOnError extends boolean = false>(options:
|
|
|
36
36
|
*/
|
|
37
37
|
export declare const getNote: <ThrowOnError extends boolean = false>(options: Options<GetNoteData, ThrowOnError>) => RequestResult<GetNoteResponses, GetNoteErrors, ThrowOnError>;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* Change a note's title or alt text
|
|
40
40
|
*
|
|
41
|
-
*
|
|
41
|
+
* Sets the note's title and/or alt text (alt only for images). An empty string removes one: a markdown note's title follows its text again. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
|
|
42
42
|
*/
|
|
43
|
-
export declare const
|
|
43
|
+
export declare const patchNote: <ThrowOnError extends boolean = false>(options: Options<PatchNoteData, ThrowOnError>) => RequestResult<PatchNoteResponses, PatchNoteErrors, ThrowOnError>;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Replace a note's content
|
|
46
46
|
*
|
|
47
|
-
* The body is the
|
|
47
|
+
* The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title or alt text with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit.
|
|
48
48
|
*/
|
|
49
|
-
export declare const
|
|
49
|
+
export declare const editNote: <ThrowOnError extends boolean = false>(options: Options<EditNoteData, ThrowOnError>) => RequestResult<EditNoteResponses, EditNoteErrors, ThrowOnError>;
|
|
50
50
|
/**
|
|
51
51
|
* Delete a feed
|
|
52
52
|
*
|
|
@@ -62,7 +62,7 @@ export declare const getFeed: <ThrowOnError extends boolean = false>(options: Op
|
|
|
62
62
|
/**
|
|
63
63
|
* Change a feed's settings
|
|
64
64
|
*
|
|
65
|
-
* Replaces both the title (at most 100 characters) and the description (at most 500); surrounding whitespace is trimmed and control characters are refused. `show_sender` (default true) shows who posted each note to readers; omitted leaves it as it is. `image` is the file name `uploadImage` returned for this feed (title image), empty to remove it, or omitted to leave it as it is; any other value is a 400. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note. Read links can't change settings.
|
|
65
|
+
* Replaces both the title (at most 100 characters) and the description (at most 500); surrounding whitespace is trimmed and control characters are refused. `show_sender` (default true) shows who posted each note to readers; omitted leaves it as it is. `read_id` gives the feed another read link (3 to 64 characters: a-z, 0-9, - and _; empty for a random one): the old id is freed and answers like an unknown read id until another feed takes it, and the notes are not edited (a relative image link follows the new id, a full URL does not). A short readable read id is guessable, so protect the feed with a password if that matters. An instance can turn chosen read ids off (NOTEFEED_ALLOW_CUSTOM_IDS=0): then only an empty `read_id` is accepted. `image` is the file name `uploadImage` returned for this feed (title image), empty to remove it, or omitted to leave it as it is; any other value is a 400. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note. Read links can't change settings.
|
|
66
66
|
*/
|
|
67
67
|
export declare const updateFeed: <ThrowOnError extends boolean = false>(options: Options<UpdateFeedData, ThrowOnError>) => RequestResult<UpdateFeedResponses, UpdateFeedErrors, ThrowOnError>;
|
|
68
68
|
/**
|
|
@@ -13,14 +13,15 @@ export const listNotes = (options) => (options.client ?? client).get({
|
|
|
13
13
|
/**
|
|
14
14
|
* Post a note
|
|
15
15
|
*
|
|
16
|
-
* Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password
|
|
16
|
+
* The body is the note, a file, and `Content-Type` says which kind: `text/markdown` (UTF-8, at most 102400 bytes), or an image, `image/png`, `image/jpeg`, `image/gif` or `image/webp` (at most NOTEFEED_MAX_IMAGE_BYTES, default 5 MiB). Nothing is guessed: any other type, or none, is `415`, and so is a body that is not what the type says (an image is recognized by its first bytes; SVG is refused). It is stored byte for byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in an image). Creates the feed with its first note, optionally protected by its own password (`X-Feed-Password`; 1 to 256 printable ASCII characters, with no space at the start or end) and with the read id in `X-Read-Id`; both are only used by the post that creates the feed. Posting to a protected feed needs its password. Also served at `POST /{feed}`, the short form curl one-liners use. The response names the note's `file` and where it is served, `file_url`, under the feed's read id (public like the read link). Metadata goes in headers: `X-Note-Title`, `X-Note-Tags` (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed), `X-Note-Alt` (images), `X-Note-Name` (the original file name).
|
|
17
17
|
*/
|
|
18
18
|
export const postNote = (options) => (options.client ?? client).post({
|
|
19
|
+
bodySerializer: null,
|
|
19
20
|
security: [{ scheme: 'bearer', type: 'http' }],
|
|
20
21
|
url: '/api/v1/feeds/{feed}/notes',
|
|
21
22
|
...options,
|
|
22
23
|
headers: {
|
|
23
|
-
'Content-Type': '
|
|
24
|
+
'Content-Type': 'text/markdown',
|
|
24
25
|
...options.headers
|
|
25
26
|
}
|
|
26
27
|
});
|
|
@@ -43,11 +44,11 @@ export const getNote = (options) => (options.client ?? client).get({
|
|
|
43
44
|
...options
|
|
44
45
|
});
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
+
* Change a note's title or alt text
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
+
* Sets the note's title and/or alt text (alt only for images). An empty string removes one: a markdown note's title follows its text again. At least one is needed. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't change notes.
|
|
49
50
|
*/
|
|
50
|
-
export const
|
|
51
|
+
export const patchNote = (options) => (options.client ?? client).patch({
|
|
51
52
|
security: [{ scheme: 'bearer', type: 'http' }],
|
|
52
53
|
url: '/api/v1/feeds/{feed}/notes/{id}',
|
|
53
54
|
...options,
|
|
@@ -57,17 +58,17 @@ export const editNote = (options) => (options.client ?? client).put({
|
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
+
* Replace a note's content
|
|
61
62
|
*
|
|
62
|
-
* The body is the
|
|
63
|
+
* The body is the new file, with the same rules as posting: a `Content-Type` that is one of the accepted types, a body that is what it declares. A note keeps its type, so the type must be the note's own (`415` otherwise). Id, creation time and metadata stay: the title of a markdown note without one set follows the new text. Change the title or alt text with `PATCH`. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit.
|
|
63
64
|
*/
|
|
64
|
-
export const
|
|
65
|
+
export const editNote = (options) => (options.client ?? client).put({
|
|
65
66
|
bodySerializer: null,
|
|
66
67
|
security: [{ scheme: 'bearer', type: 'http' }],
|
|
67
|
-
url: '/api/v1/feeds/{feed}/
|
|
68
|
+
url: '/api/v1/feeds/{feed}/notes/{id}',
|
|
68
69
|
...options,
|
|
69
70
|
headers: {
|
|
70
|
-
'Content-Type': '
|
|
71
|
+
'Content-Type': 'text/markdown',
|
|
71
72
|
...options.headers
|
|
72
73
|
}
|
|
73
74
|
});
|
|
@@ -94,7 +95,7 @@ export const getFeed = (options) => (options.client ?? client).get({
|
|
|
94
95
|
/**
|
|
95
96
|
* Change a feed's settings
|
|
96
97
|
*
|
|
97
|
-
* Replaces both the title (at most 100 characters) and the description (at most 500); surrounding whitespace is trimmed and control characters are refused. `show_sender` (default true) shows who posted each note to readers; omitted leaves it as it is. `image` is the file name `uploadImage` returned for this feed (title image), empty to remove it, or omitted to leave it as it is; any other value is a 400. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note. Read links can't change settings.
|
|
98
|
+
* Replaces both the title (at most 100 characters) and the description (at most 500); surrounding whitespace is trimmed and control characters are refused. `show_sender` (default true) shows who posted each note to readers; omitted leaves it as it is. `read_id` gives the feed another read link (3 to 64 characters: a-z, 0-9, - and _; empty for a random one): the old id is freed and answers like an unknown read id until another feed takes it, and the notes are not edited (a relative image link follows the new id, a full URL does not). A short readable read id is guessable, so protect the feed with a password if that matters. An instance can turn chosen read ids off (NOTEFEED_ALLOW_CUSTOM_IDS=0): then only an empty `read_id` is accepted. `image` is the file name `uploadImage` returned for this feed (title image), empty to remove it, or omitted to leave it as it is; any other value is a 400. Needs the feed's password if it has one, and counts against the post rate limit. Only on a feed that exists: it is created by its first note. Read links can't change settings.
|
|
98
99
|
*/
|
|
99
100
|
export const updateFeed = (options) => (options.client ?? client).put({
|
|
100
101
|
security: [{ scheme: 'bearer', type: 'http' }],
|
|
@@ -3,17 +3,41 @@ export type ClientOptions = {
|
|
|
3
3
|
};
|
|
4
4
|
export type Note = {
|
|
5
5
|
/**
|
|
6
|
-
* UTC time to the second plus a
|
|
6
|
+
* The note's id: a UTC time to the second plus a random UUID for notes made here; any name without a dot for a file placed by hand
|
|
7
7
|
*/
|
|
8
8
|
id: string;
|
|
9
9
|
/**
|
|
10
|
-
* The
|
|
10
|
+
* The media type of the note's file: `text/markdown`, `image/png`, `image/jpeg`, `image/gif` or `image/webp`
|
|
11
|
+
*/
|
|
12
|
+
type: string;
|
|
13
|
+
/**
|
|
14
|
+
* The note's file name, `<id>.<extension>`
|
|
15
|
+
*/
|
|
16
|
+
file: string;
|
|
17
|
+
/**
|
|
18
|
+
* Where the note's file is served, absolute, under the feed's read id (public like the read link); null while the feed has no read link
|
|
19
|
+
*/
|
|
20
|
+
file_url: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The size of the note's file in bytes
|
|
23
|
+
*/
|
|
24
|
+
size: number;
|
|
25
|
+
/**
|
|
26
|
+
* The title set for the note, else the first heading or the first non-empty line of a markdown note, else the picture's alt text; may be empty
|
|
11
27
|
*/
|
|
12
28
|
title: string;
|
|
13
29
|
/**
|
|
14
|
-
* The note, byte-for-byte as posted
|
|
30
|
+
* The note's text, byte-for-byte as posted: for a text type (markdown) only; absent for a picture
|
|
15
31
|
*/
|
|
16
|
-
|
|
32
|
+
content?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Alternative text of an image note, when it has one
|
|
35
|
+
*/
|
|
36
|
+
alt?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The file name an image was posted with, when it came with one
|
|
39
|
+
*/
|
|
40
|
+
name?: string;
|
|
17
41
|
/**
|
|
18
42
|
* When the note was posted (UTC)
|
|
19
43
|
*/
|
|
@@ -55,20 +79,14 @@ export type Created = {
|
|
|
55
79
|
* The feed's read-only RSS link, safe to share; null only if the server can't read the feed's stored read id, or if that id and the derived one both belong to other feeds
|
|
56
80
|
*/
|
|
57
81
|
read_url: string | null;
|
|
58
|
-
};
|
|
59
|
-
export type ImageUploaded = {
|
|
60
82
|
/**
|
|
61
|
-
* The
|
|
83
|
+
* The note's file name, `<id>.<extension>`. For an image note, pass it as a feed's `image` setting
|
|
62
84
|
*/
|
|
63
85
|
file: string;
|
|
64
86
|
/**
|
|
65
|
-
* Where the
|
|
66
|
-
*/
|
|
67
|
-
url: string;
|
|
68
|
-
/**
|
|
69
|
-
* ``, to paste into a note
|
|
87
|
+
* Where the note's file is served, absolute, under the feed's read id (public like the read link); null while the feed has no read link
|
|
70
88
|
*/
|
|
71
|
-
|
|
89
|
+
file_url: string | null;
|
|
72
90
|
};
|
|
73
91
|
export type Error = {
|
|
74
92
|
/**
|
|
@@ -78,29 +96,17 @@ export type Error = {
|
|
|
78
96
|
/**
|
|
79
97
|
* Stable machine-readable code; absent only on a 500
|
|
80
98
|
*/
|
|
81
|
-
code?: 'invalid_feed' | 'reserved_feed' | 'auth' | 'rate_limited' | 'too_many_attempts' | 'feed_limit' | 'note_limit' | 'image_limit' | 'empty_note' | 'too_large' | 'unsupported_type' | 'invalid_body' | 'invalid_request' | 'not_found' | 'feed_exists';
|
|
99
|
+
code?: 'invalid_feed' | 'reserved_feed' | 'auth' | 'rate_limited' | 'too_many_attempts' | 'feed_limit' | 'note_limit' | 'image_limit' | 'empty_note' | 'too_large' | 'unsupported_type' | 'invalid_body' | 'invalid_request' | 'not_found' | 'feed_exists' | 'taken';
|
|
82
100
|
};
|
|
83
|
-
export type
|
|
84
|
-
markdown: string;
|
|
101
|
+
export type NoteMeta = {
|
|
85
102
|
/**
|
|
86
|
-
*
|
|
103
|
+
* The note's title, at most 100 characters, one line; empty or left out means the title is taken from the text (a markdown note) or there is none (an image)
|
|
87
104
|
*/
|
|
88
|
-
|
|
105
|
+
title?: string;
|
|
89
106
|
/**
|
|
90
|
-
*
|
|
107
|
+
* Alternative text of an image note, at most 500 characters, one line
|
|
91
108
|
*/
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
export type PostForm = {
|
|
95
|
-
markdown: string;
|
|
96
|
-
/**
|
|
97
|
-
* Protects the feed: 1 to 256 printable ASCII characters, with no space at the start or end. Only honored on the post that creates the feed; an existing open feed answers 409. Empty is the same as leaving it out.
|
|
98
|
-
*/
|
|
99
|
-
password?: string;
|
|
100
|
-
/**
|
|
101
|
-
* Labels for the note: at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed. Free labels, not verified, shown with the note (also to readers of the read link and RSS). Ignored when editing a note: an edit keeps its tags.
|
|
102
|
-
*/
|
|
103
|
-
tags?: Array<string>;
|
|
109
|
+
alt?: string;
|
|
104
110
|
};
|
|
105
111
|
export type PasswordJson = {
|
|
106
112
|
/**
|
|
@@ -125,6 +131,10 @@ export type FeedSettings = {
|
|
|
125
131
|
* Whether readers (RSS, the read API and pages) see who posted each note; omitted leaves it as it is, a new feed starts with true
|
|
126
132
|
*/
|
|
127
133
|
show_sender?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* A new read id (3 to 64 characters: a-z, 0-9, - and _), empty for a random one; omitted leaves it as it is. The old read link stops showing this feed, and may later show another one. Notes are not edited: a relative image link (``) follows the new id, a full URL keeps the old one and is yours to change. Reserved feeds keep theirs. 409 when it is taken
|
|
136
|
+
*/
|
|
137
|
+
read_id?: string;
|
|
128
138
|
};
|
|
129
139
|
export type Feed = {
|
|
130
140
|
/**
|
|
@@ -223,16 +233,35 @@ export type ListNotesResponses = {
|
|
|
223
233
|
};
|
|
224
234
|
export type ListNotesResponse = ListNotesResponses[keyof ListNotesResponses];
|
|
225
235
|
export type PostNoteData = {
|
|
226
|
-
|
|
236
|
+
/**
|
|
237
|
+
* The note, a file: its bytes, of the type `Content-Type` declares
|
|
238
|
+
*/
|
|
239
|
+
body: Blob | File;
|
|
227
240
|
headers?: {
|
|
228
241
|
/**
|
|
229
242
|
* The feed's own password, when it has one: to post, list or get, and (as the current password) to change or remove it. On the `POST` that creates a feed it sets the feed's password; on a `POST` to an existing feed that has none it answers 409. An empty value is the same as no header, so a `POST` with an empty one creates an open feed.
|
|
230
243
|
*/
|
|
231
244
|
'X-Feed-Password'?: string;
|
|
245
|
+
/**
|
|
246
|
+
* The read id the feed gets when this post creates it: 3 to 64 characters (a-z, 0-9, - and _), random when left out or empty, ignored for a feed that exists. A short readable one is guessable: protect the feed with a password if that matters. 409 when it is taken
|
|
247
|
+
*/
|
|
248
|
+
'X-Read-Id'?: string;
|
|
249
|
+
/**
|
|
250
|
+
* The note's title, for a raw body; a JSON or form body's own `title` wins. At most 100 characters, one line.
|
|
251
|
+
*/
|
|
252
|
+
'X-Note-Title'?: string;
|
|
232
253
|
/**
|
|
233
254
|
* Tags for the note, comma-separated (`ci,deploy`): at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed. For a raw markdown body; a JSON or form body's own `tags` wins. Empty is none.
|
|
234
255
|
*/
|
|
235
256
|
'X-Note-Tags'?: string;
|
|
257
|
+
/**
|
|
258
|
+
* For an image body: its alternative text; a form's own `alt` wins.
|
|
259
|
+
*/
|
|
260
|
+
'X-Note-Alt'?: string;
|
|
261
|
+
/**
|
|
262
|
+
* For an image body: the picture's original file name, kept with the note (at most 200 characters)
|
|
263
|
+
*/
|
|
264
|
+
'X-Note-Name'?: string;
|
|
236
265
|
};
|
|
237
266
|
path: {
|
|
238
267
|
/**
|
|
@@ -245,7 +274,7 @@ export type PostNoteData = {
|
|
|
245
274
|
};
|
|
246
275
|
export type PostNoteErrors = {
|
|
247
276
|
/**
|
|
248
|
-
* Invalid or reserved feed name;
|
|
277
|
+
* Invalid or reserved feed name; a blank note; a bad title, alt text or tags; a new password that is not printable ASCII
|
|
249
278
|
*/
|
|
250
279
|
400: Error;
|
|
251
280
|
/**
|
|
@@ -253,15 +282,15 @@ export type PostNoteErrors = {
|
|
|
253
282
|
*/
|
|
254
283
|
401: Error;
|
|
255
284
|
/**
|
|
256
|
-
* A password was sent for a feed that already exists without one: it can't be claimed
|
|
285
|
+
* A password was sent for a feed that already exists without one: it can't be claimed; or the chosen read id is taken
|
|
257
286
|
*/
|
|
258
287
|
409: Error;
|
|
259
288
|
/**
|
|
260
|
-
*
|
|
289
|
+
* Markdown over 102400 bytes, or an image over NOTEFEED_MAX_IMAGE_BYTES
|
|
261
290
|
*/
|
|
262
291
|
413: Error;
|
|
263
292
|
/**
|
|
264
|
-
*
|
|
293
|
+
* Content-Type missing or not one of the accepted types, or the body is not what it declares
|
|
265
294
|
*/
|
|
266
295
|
415: Error;
|
|
267
296
|
/**
|
|
@@ -269,7 +298,7 @@ export type PostNoteErrors = {
|
|
|
269
298
|
*/
|
|
270
299
|
429: Error;
|
|
271
300
|
/**
|
|
272
|
-
* NOTEFEED_MAX_FEEDS or
|
|
301
|
+
* NOTEFEED_MAX_FEEDS, NOTEFEED_MAX_NOTES_PER_FEED or NOTEFEED_MAX_IMAGES_PER_FEED reached
|
|
273
302
|
*/
|
|
274
303
|
507: Error;
|
|
275
304
|
};
|
|
@@ -375,8 +404,8 @@ export type GetNoteResponses = {
|
|
|
375
404
|
200: Note;
|
|
376
405
|
};
|
|
377
406
|
export type GetNoteResponse = GetNoteResponses[keyof GetNoteResponses];
|
|
378
|
-
export type
|
|
379
|
-
body:
|
|
407
|
+
export type PatchNoteData = {
|
|
408
|
+
body: NoteMeta;
|
|
380
409
|
headers?: {
|
|
381
410
|
/**
|
|
382
411
|
* The feed's own password, when it has one: to post, list or get, and (as the current password) to change or remove it. On the `POST` that creates a feed it sets the feed's password; on a `POST` to an existing feed that has none it answers 409. An empty value is the same as no header, so a `POST` with an empty one creates an open feed.
|
|
@@ -396,9 +425,9 @@ export type EditNoteData = {
|
|
|
396
425
|
query?: never;
|
|
397
426
|
url: '/api/v1/feeds/{feed}/notes/{id}';
|
|
398
427
|
};
|
|
399
|
-
export type
|
|
428
|
+
export type PatchNoteErrors = {
|
|
400
429
|
/**
|
|
401
|
-
* Invalid or reserved feed name;
|
|
430
|
+
* Invalid or reserved feed name; nothing to change; a bad title or alt text; alt for a note that has none; bad JSON
|
|
402
431
|
*/
|
|
403
432
|
400: Error;
|
|
404
433
|
/**
|
|
@@ -410,11 +439,7 @@ export type EditNoteErrors = {
|
|
|
410
439
|
*/
|
|
411
440
|
404: Error;
|
|
412
441
|
/**
|
|
413
|
-
*
|
|
414
|
-
*/
|
|
415
|
-
413: Error;
|
|
416
|
-
/**
|
|
417
|
-
* Unsupported content type
|
|
442
|
+
* Content-Type is not application/json
|
|
418
443
|
*/
|
|
419
444
|
415: Error;
|
|
420
445
|
/**
|
|
@@ -422,17 +447,17 @@ export type EditNoteErrors = {
|
|
|
422
447
|
*/
|
|
423
448
|
429: Error;
|
|
424
449
|
};
|
|
425
|
-
export type
|
|
426
|
-
export type
|
|
450
|
+
export type PatchNoteError = PatchNoteErrors[keyof PatchNoteErrors];
|
|
451
|
+
export type PatchNoteResponses = {
|
|
427
452
|
/**
|
|
428
453
|
* The note as it is now
|
|
429
454
|
*/
|
|
430
455
|
200: Note;
|
|
431
456
|
};
|
|
432
|
-
export type
|
|
433
|
-
export type
|
|
457
|
+
export type PatchNoteResponse = PatchNoteResponses[keyof PatchNoteResponses];
|
|
458
|
+
export type EditNoteData = {
|
|
434
459
|
/**
|
|
435
|
-
* The
|
|
460
|
+
* The note, a file: its bytes, of the type `Content-Type` declares
|
|
436
461
|
*/
|
|
437
462
|
body: Blob | File;
|
|
438
463
|
headers?: {
|
|
@@ -446,13 +471,17 @@ export type UploadImageData = {
|
|
|
446
471
|
* The feed's name. It is the write key: anyone who knows it can post.
|
|
447
472
|
*/
|
|
448
473
|
feed: string;
|
|
474
|
+
/**
|
|
475
|
+
* The note's id
|
|
476
|
+
*/
|
|
477
|
+
id: string;
|
|
449
478
|
};
|
|
450
479
|
query?: never;
|
|
451
|
-
url: '/api/v1/feeds/{feed}/
|
|
480
|
+
url: '/api/v1/feeds/{feed}/notes/{id}';
|
|
452
481
|
};
|
|
453
|
-
export type
|
|
482
|
+
export type EditNoteErrors = {
|
|
454
483
|
/**
|
|
455
|
-
* Invalid or reserved feed name
|
|
484
|
+
* Invalid or reserved feed name; a blank note
|
|
456
485
|
*/
|
|
457
486
|
400: Error;
|
|
458
487
|
/**
|
|
@@ -460,34 +489,30 @@ export type UploadImageErrors = {
|
|
|
460
489
|
*/
|
|
461
490
|
401: Error;
|
|
462
491
|
/**
|
|
463
|
-
* No such
|
|
492
|
+
* No such note
|
|
464
493
|
*/
|
|
465
494
|
404: Error;
|
|
466
495
|
/**
|
|
467
|
-
*
|
|
496
|
+
* Markdown over 102400 bytes, or an image over NOTEFEED_MAX_IMAGE_BYTES
|
|
468
497
|
*/
|
|
469
498
|
413: Error;
|
|
470
499
|
/**
|
|
471
|
-
*
|
|
500
|
+
* Content-Type missing, not accepted or not the note's own type, or the body is not what it declares
|
|
472
501
|
*/
|
|
473
502
|
415: Error;
|
|
474
503
|
/**
|
|
475
|
-
* Too many posts,
|
|
504
|
+
* Too many posts, edits and deletes, or wrong passwords, from this client
|
|
476
505
|
*/
|
|
477
506
|
429: Error;
|
|
478
|
-
/**
|
|
479
|
-
* NOTEFEED_MAX_IMAGES_PER_FEED reached
|
|
480
|
-
*/
|
|
481
|
-
507: Error;
|
|
482
507
|
};
|
|
483
|
-
export type
|
|
484
|
-
export type
|
|
508
|
+
export type EditNoteError = EditNoteErrors[keyof EditNoteErrors];
|
|
509
|
+
export type EditNoteResponses = {
|
|
485
510
|
/**
|
|
486
|
-
*
|
|
511
|
+
* The note as it is now
|
|
487
512
|
*/
|
|
488
|
-
|
|
513
|
+
200: Note;
|
|
489
514
|
};
|
|
490
|
-
export type
|
|
515
|
+
export type EditNoteResponse = EditNoteResponses[keyof EditNoteResponses];
|
|
491
516
|
export type DeleteFeedData = {
|
|
492
517
|
body?: never;
|
|
493
518
|
headers?: {
|
|
@@ -593,7 +618,7 @@ export type UpdateFeedData = {
|
|
|
593
618
|
};
|
|
594
619
|
export type UpdateFeedErrors = {
|
|
595
620
|
/**
|
|
596
|
-
* Invalid or reserved feed name, bad JSON,
|
|
621
|
+
* Invalid or reserved feed name, bad JSON, a title or description that is too long or has control characters, a malformed read id, a reserved feed's read id, or chosen read ids turned off
|
|
597
622
|
*/
|
|
598
623
|
400: Error;
|
|
599
624
|
/**
|
|
@@ -604,6 +629,10 @@ export type UpdateFeedErrors = {
|
|
|
604
629
|
* No such feed
|
|
605
630
|
*/
|
|
606
631
|
404: Error;
|
|
632
|
+
/**
|
|
633
|
+
* The read id belongs to another feed or is held back for a reserved one
|
|
634
|
+
*/
|
|
635
|
+
409: Error;
|
|
607
636
|
/**
|
|
608
637
|
* Too many posts, edits and deletes, or wrong passwords, from this client
|
|
609
638
|
*/
|
|
@@ -707,7 +736,7 @@ export type GetReadFeedData = {
|
|
|
707
736
|
body?: never;
|
|
708
737
|
path: {
|
|
709
738
|
/**
|
|
710
|
-
* The feed's read id, from its read link: 22 random characters, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
739
|
+
* The feed's read id, from its read link: 22 random characters, one the feed's owner chose, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
711
740
|
*/
|
|
712
741
|
readId: string;
|
|
713
742
|
};
|
|
@@ -732,7 +761,7 @@ export type ListReadNotesData = {
|
|
|
732
761
|
body?: never;
|
|
733
762
|
path: {
|
|
734
763
|
/**
|
|
735
|
-
* The feed's read id, from its read link: 22 random characters, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
764
|
+
* The feed's read id, from its read link: 22 random characters, one the feed's owner chose, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
736
765
|
*/
|
|
737
766
|
readId: string;
|
|
738
767
|
};
|
|
@@ -774,7 +803,7 @@ export type GetReadNoteData = {
|
|
|
774
803
|
body?: never;
|
|
775
804
|
path: {
|
|
776
805
|
/**
|
|
777
|
-
* The feed's read id, from its read link: 22 random characters, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
806
|
+
* The feed's read id, from its read link: 22 random characters, one the feed's owner chose, or the feed's own name for a reserved feed. Read-only; never reveals the name of any other feed.
|
|
778
807
|
*/
|
|
779
808
|
readId: string;
|
|
780
809
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { AuthError, Client, ConfigError, InvalidRequestError, LimitReachedError, NotefeedError, NotFoundError, NoteTooLargeError, RateLimitedError, } from "./client.js";
|
|
2
|
-
export type { ClientOptions, Created, ErrorCode, Feed, FeedSettings,
|
|
2
|
+
export type { Attachment, ClientOptions, Created, ErrorCode, Feed, FeedSettings, Note } from "./client.js";
|
package/package.json
CHANGED