notefeed 0.7.0 → 0.9.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 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, markdown)` replaces its text 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.
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
- An image can be uploaded to an existing feed and then used in a note: `const { file, url, markdown } = await client.uploadImage(bytes)` takes a `Uint8Array` or a `Blob` (PNG, JPEG, GIF or WebP; the server recognizes the format by the bytes), takes the same `{ feed, feedPassword }` options as `post`, and returns the image's `url` and `markdown` (`![](url)`), which you put in a note. The feed must already have a note, and an image that is too large is a `NoteTooLargeError`. `await client.updateFeed({ title, description, image: file })` makes an uploaded image the feed's title image (`image: ""` removes it; leaving `image` out keeps it). `notefeed image <PATH>` uploads a file and prints the markdown. Images are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
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 `![](file)` 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 with the text in one request, and the server swaps `![](chart.png)` for each file name (`null` content posts only the pictures) (`notefeed post "…" --attach chart.png` does the same); nothing is posted when one is refused. Pictures are public to anyone with the feed's read link, and metadata such as EXIF is not removed.
35
35
 
36
- Full documentation: https://docs.notefeed.me/clients/
36
+ Full documentation: https://docs.notefeed.me/integrations/clients/
package/dist/cli.js CHANGED
@@ -1,17 +1,20 @@
1
- /** `notefeed post`, `edit`, `delete`, `notes` and `image`: post, change, remove and read notes, and upload images, from the command line. */
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,30 @@ 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 markdown = await read(text, values.file, io);
45
- const note = await client(values).post(markdown, { tags: values.tag });
46
- io.stdout.write(`${note.url}\n`);
51
+ const attachments = await Promise.all((values.attach ?? []).map(attachment));
52
+ // Pictures alone need no text; the result is then the first picture, whose URL is also in `attachments`.
53
+ const noText = attachments.length > 0 && text === undefined && !values.file;
54
+ const input = noText ? { content: null, type: undefined, name: undefined } : await read(text, values.file, values.type, io);
55
+ // The server refuses a file name next to attachments: the text has none then.
56
+ const note = await client(values).post(input.content, { type: input.type, title: values.title, tags: values.tag, name: attachments.length ? undefined : input.name, attachments });
57
+ io.stdout.write(`${[...(noText ? [] : [note.url]), ...note.attachments.map((a) => a.url)].join("\n")}\n`);
47
58
  return 0;
48
59
  }
49
60
  if (command === "edit" && text !== undefined && rest.length <= 1) {
50
- const note = await client(values).edit(text, await read(rest[0], values.file, io));
61
+ const input = await read(rest[0], values.file, values.type, io);
62
+ const note = await client(values).edit(text, input.content, { type: input.type });
51
63
  io.stdout.write(`${note.url}\n`);
52
64
  return 0;
53
65
  }
54
- if (command === "delete" && text !== undefined && !rest.length) {
55
- await client(values).delete(text);
66
+ if (command === "update" && text !== undefined && !rest.length) {
67
+ if (values.title === undefined && values.alt === undefined)
68
+ throw new UsageError("give --title and/or --alt");
69
+ const note = await client(values).update(text, { title: values.title, alt: values.alt });
70
+ io.stdout.write(`${note.url}\n`);
56
71
  return 0;
57
72
  }
58
- if (command === "image" && text !== undefined && !rest.length) {
59
- let bytes;
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`);
73
+ if (command === "delete" && text !== undefined && !rest.length) {
74
+ await client(values).delete(text);
67
75
  return 0;
68
76
  }
69
77
  if (command === "notes" && text === undefined) {
@@ -75,7 +83,7 @@ export async function main(argv, io = process) {
75
83
  // To the second, UTC: the same form as the Python CLI, so scripts read either the same way.
76
84
  const when = new Date(n.created_at).toISOString().replace(/\.\d+Z$/, "Z");
77
85
  // --json: exactly the documented fields, like the Python CLI, even if the server adds more.
78
- const fields = { id: n.id, title: n.title, markdown: n.markdown, created_at: when, url: n.url };
86
+ 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
87
  io.stdout.write(values.json ? `${JSON.stringify(fields)}\n` : `${when} ${n.title || n.id} ${n.url}\n`);
80
88
  if (--left === 0)
81
89
  break;
@@ -108,26 +116,42 @@ function client(values) {
108
116
  feedPassword: process.env.NOTEFEED_FEED_PASSWORD,
109
117
  });
110
118
  }
111
- async function read(text, file, io) {
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
- return utf8(await readFile(file), file);
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 with the note: its name is the file's (the client checks it), 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
+ try {
150
+ return { name: basename(path), content: await readFile(path), type };
151
+ }
152
+ catch (e) {
153
+ throw new UsageError(`cannot read ${path}: ${e.code ?? e.message}`);
154
+ }
131
155
  }
132
156
  function utf8(bytes, source) {
133
157
  try {
package/dist/client.d.ts CHANGED
@@ -1,7 +1,14 @@
1
- import type { Created, Error as ApiError, Feed, FeedSettings, ImageUploaded, Note } from "./generated/types.gen.js";
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, ImageUploaded, Note };
4
+ export type { Created, Feed, FeedSettings, Note };
5
+ /** A picture posted with a note: `![](name)` 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;
@@ -49,19 +56,49 @@ export declare class Client {
49
56
  constructor(options: ClientOptions);
50
57
  /** NOTEFEED_URL, NOTEFEED_FEED, NOTEFEED_PASSWORD and NOTEFEED_FEED_PASSWORD. */
51
58
  static fromEnv(env?: Record<string, string | undefined>): Client;
52
- /** `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 letters, digits, `-`, `_`, `.`, `:`; not verified). */
53
- post(markdown: string, options?: {
59
+ /**
60
+ * 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`,
61
+ * `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
62
+ * The feed is created by its first note. `Created.file` is the note's file name: write `![](file)` in a markdown note to show a picture, or
63
+ * pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
64
+ *
65
+ * `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
66
+ * letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
67
+ * 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
68
+ * (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).
69
+ *
70
+ * `attachments` are pictures posted with the note in one request: `![](name)` in the markdown shows one (a picture the text never refers to is
71
+ * added at the end), and `Created.attachments` lists them in order. `tags` label the pictures too. `content` may be `null` when there are
72
+ * attachments: only the pictures are posted, `title` and `tags` go on each, and the result is the first picture plus `attachments`. Checked
73
+ * here, before anything is sent (ConfigError): a name given twice, an attachment that is not a picture, content that is not markdown, `alt` or
74
+ * `name` with attachments. What a name may be is the server's rule (docs/using/pictures.md, "Posting a note with its pictures"): a name it refuses is an
75
+ * InvalidRequestError, `attachment "<name>": ...`, and whenever the server refuses any part (its message names it) nothing is posted.
76
+ */
77
+ post(content: string | Uint8Array | Blob | null, options?: {
54
78
  feed?: string;
55
79
  feedPassword?: string;
80
+ type?: string;
81
+ title?: string;
56
82
  tags?: string[];
57
- }): Promise<Created>;
58
- /** Upload a PNG, JPEG, GIF or WebP image to an existing feed. The server decides the format by the bytes, so no content type is needed. `markdown` in the answer is `![](url)`, to put in a note. Same options as post(). */
59
- uploadImage(data: Uint8Array | Blob, options?: {
83
+ alt?: string;
84
+ name?: string;
85
+ readId?: string;
86
+ attachments?: Attachment[];
87
+ }): Promise<Created & {
88
+ attachments: Created[];
89
+ }>;
90
+ private postOne;
91
+ /** 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(). */
92
+ edit(id: string, content: string | Uint8Array | Blob, options?: {
60
93
  feed?: string;
61
94
  feedPassword?: string;
62
- }): Promise<ImageUploaded>;
63
- /** Replace a note's markdown; its id and URLs stay. Same options as post(). */
64
- edit(id: string, markdown: string, options?: {
95
+ type?: string;
96
+ }): Promise<Note>;
97
+ /** 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(). */
98
+ update(id: string, meta: {
99
+ title?: string;
100
+ alt?: string;
101
+ }, options?: {
65
102
  feed?: string;
66
103
  feedPassword?: string;
67
104
  }): Promise<Note>;
@@ -75,7 +112,7 @@ export declare class Client {
75
112
  feed?: string;
76
113
  feedPassword?: string;
77
114
  }): 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(). */
115
+ /** 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
116
  updateFeed(settings: FeedSettings, options?: {
80
117
  feed?: string;
81
118
  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, uploadImage } from "./generated/sdk.gen.js";
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;
@@ -54,9 +54,23 @@ const BY_CODE = {
54
54
  invalid_body: InvalidRequestError,
55
55
  invalid_request: InvalidRequestError,
56
56
  feed_exists: InvalidRequestError,
57
+ taken: InvalidRequestError,
57
58
  unsupported_type: InvalidRequestError,
58
59
  };
60
+ const IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
59
61
  const FEED_RE = /^[a-z0-9_-]{1,64}$/; // same rule as the server; reserved names still come back as a 400
62
+ // 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
63
+ // latin1 characters, which is what the server reads back.
64
+ const headerValue = (text) => Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join("");
65
+ // What a post sends: a string is markdown; bytes and Blobs need a media type (a Blob's own, if it has one).
66
+ function asFile(content, type) {
67
+ if (typeof content === "string")
68
+ return { body: new Blob([content]), type: type ?? "text/markdown" };
69
+ const media = type ?? (content instanceof Blob ? content.type : "");
70
+ if (!media)
71
+ throw new ConfigError("give the file's media type as `type`, e.g. image/png");
72
+ return { body: content instanceof Blob ? content : new Blob([content]), type: media };
73
+ }
60
74
  export class Client {
61
75
  url;
62
76
  feed;
@@ -87,23 +101,83 @@ export class Client {
87
101
  feedPassword: env.NOTEFEED_FEED_PASSWORD,
88
102
  });
89
103
  }
90
- /** `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 letters, digits, `-`, `_`, `.`, `:`; not verified). */
91
- async post(markdown, options = {}) {
104
+ /**
105
+ * 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`,
106
+ * `image/png`, `image/jpeg`, `image/gif` or `image/webp`. The server accepts nothing else and checks that the body is what the type says.
107
+ * The feed is created by its first note. `Created.file` is the note's file name: write `![](file)` in a markdown note to show a picture, or
108
+ * pass it as `image` to updateFeed; `file_url` is for a link outside notefeed.
109
+ *
110
+ * `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
111
+ * letters, digits, `-`, `_`, `.`, `:`; not verified). `title` is its title (at most 100 characters, one line); left out, it is taken from the
112
+ * 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
113
+ * (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).
114
+ *
115
+ * `attachments` are pictures posted with the note in one request: `![](name)` in the markdown shows one (a picture the text never refers to is
116
+ * added at the end), and `Created.attachments` lists them in order. `tags` label the pictures too. `content` may be `null` when there are
117
+ * attachments: only the pictures are posted, `title` and `tags` go on each, and the result is the first picture plus `attachments`. Checked
118
+ * here, before anything is sent (ConfigError): a name given twice, an attachment that is not a picture, content that is not markdown, `alt` or
119
+ * `name` with attachments. What a name may be is the server's rule (docs/using/pictures.md, "Posting a note with its pictures"): a name it refuses is an
120
+ * InvalidRequestError, `attachment "<name>": ...`, and whenever the server refuses any part (its message names it) nothing is posted.
121
+ */
122
+ async post(content, options = {}) {
123
+ const { attachments = [], ...own } = options;
124
+ if (content === null && attachments.length === 0)
125
+ throw new ConfigError("no content and no attachments");
126
+ if (attachments.length === 0)
127
+ return { ...(await this.postOne(content, own)), attachments: [] };
128
+ if (content !== null && (typeof content !== "string" || (own.type ?? "text/markdown") !== "text/markdown"))
129
+ throw new ConfigError("attachments go with a markdown string");
130
+ if (own.alt || own.name)
131
+ throw new ConfigError("alt and name do not go with attachments: give each attachment its alt");
132
+ const form = new FormData();
133
+ if (content !== null)
134
+ form.append("text", new Blob([content], { type: "text/markdown" }));
135
+ for (const a of attachments) {
136
+ if (attachments.findIndex((b) => b.name === a.name) !== attachments.indexOf(a))
137
+ throw new ConfigError(`attachment "${a.name}" is given twice`);
138
+ const { body, type } = asFile(a.content, a.type);
139
+ if (!IMAGE_TYPES.includes(type))
140
+ throw new ConfigError(`attachment "${a.name}" must be a picture (${IMAGE_TYPES.join(", ")})`);
141
+ form.append("file", new Blob([body], { type }), a.name);
142
+ if (a.alt)
143
+ form.append(`alt.${a.name}`, a.alt);
144
+ }
145
+ const feed = this.feedFor(own.feed);
146
+ const headers = {
147
+ ...this.opts(own.feedPassword).headers,
148
+ "Content-Type": null, // fetch sets multipart/form-data with its boundary
149
+ ...(own.title && { "X-Note-Title": headerValue(own.title) }),
150
+ ...(own.tags?.length && { "X-Note-Tags": own.tags.join(",") }),
151
+ ...(own.readId && { "X-Read-Id": own.readId }),
152
+ };
153
+ const created = await this.call(postNote({ client: this.api, path: { feed }, body: form, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
154
+ return { ...created, attachments: created.attachments ?? [] };
155
+ }
156
+ async postOne(content, options) {
92
157
  const feed = this.feedFor(options.feed);
93
- const body = { markdown, ...(options.tags?.length && { tags: options.tags }) };
94
- return this.call(postNote({ client: this.api, path: { feed }, body, ...this.opts(options.feedPassword) }));
158
+ const { body, type } = asFile(content, options.type);
159
+ const headers = {
160
+ ...this.opts(options.feedPassword).headers,
161
+ "Content-Type": type,
162
+ ...(options.title && { "X-Note-Title": headerValue(options.title) }),
163
+ ...(options.tags?.length && { "X-Note-Tags": options.tags.join(",") }),
164
+ ...(options.alt && { "X-Note-Alt": headerValue(options.alt) }),
165
+ ...(options.name && { "X-Note-Name": headerValue(options.name) }),
166
+ ...(options.readId && { "X-Read-Id": options.readId }),
167
+ };
168
+ return this.call(postNote({ client: this.api, path: { feed }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
95
169
  }
96
- /** Upload a PNG, JPEG, GIF or WebP image to an existing feed. The server decides the format by the bytes, so no content type is needed. `markdown` in the answer is `![](url)`, to put in a note. Same options as post(). */
97
- async uploadImage(data, options = {}) {
170
+ /** 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(). */
171
+ async edit(id, content, options = {}) {
98
172
  const feed = this.feedFor(options.feed);
99
- const body = data instanceof Blob ? data : new Blob([data]);
100
- const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": "application/octet-stream" };
101
- return this.call(uploadImage({ client: this.api, path: { feed }, body, signal: AbortSignal.timeout(this.timeoutMs), headers }));
173
+ const { body, type } = asFile(content, options.type);
174
+ const headers = { ...this.opts(options.feedPassword).headers, "Content-Type": type };
175
+ return this.call(editNote({ client: this.api, path: { feed, id }, body, headers, signal: AbortSignal.timeout(this.timeoutMs) }));
102
176
  }
103
- /** Replace a note's markdown; its id and URLs stay. Same options as post(). */
104
- async edit(id, markdown, options = {}) {
177
+ /** 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(). */
178
+ async update(id, meta, options = {}) {
105
179
  const feed = this.feedFor(options.feed);
106
- return this.call(editNote({ client: this.api, path: { feed, id }, body: { markdown }, ...this.opts(options.feedPassword) }));
180
+ return this.call(patchNote({ client: this.api, path: { feed, id }, body: meta, ...this.opts(options.feedPassword) }));
107
181
  }
108
182
  /** Remove a note for good. The feed stays, even with no notes left. Same options as post(). */
109
183
  async delete(id, options = {}) {
@@ -115,10 +189,10 @@ export class Client {
115
189
  const feed = this.feedFor(options.feed);
116
190
  return this.call(getFeed({ client: this.api, path: { feed }, ...this.opts(options.feedPassword) }));
117
191
  }
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(). */
192
+ /** 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
193
  async updateFeed(settings, options = {}) {
120
194
  const feed = this.feedFor(options.feed);
121
- const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }) };
195
+ const body = { title: settings.title, description: settings.description, ...(settings.image === undefined ? {} : { image: settings.image }), ...(settings.read_id === undefined ? {} : { read_id: settings.read_id }) };
122
196
  return this.call(updateFeed({ client: this.api, path: { feed }, body, ...this.opts(options.feedPassword) }));
123
197
  }
124
198
  /** 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, uploadImage } 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, ImageUploaded, ListNotesData, ListNotesError, ListNotesErrors, ListNotesResponse, ListNotesResponses, ListReadNotesData, ListReadNotesError, ListReadNotesErrors, ListReadNotesResponse, ListReadNotesResponses, Note, NoteList, PasswordJson, PostForm, PostJson, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, ReadFeed, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses, UploadImageData, UploadImageError, UploadImageErrors, UploadImageResponse, UploadImageResponses } from './types.gen.js';
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, MultipartNote, Note, NoteEdited, NoteList, NoteMeta, PasswordJson, PatchNoteData, PatchNoteError, PatchNoteErrors, PatchNoteResponse, PatchNoteResponses, Posted, PostNoteData, PostNoteError, PostNoteErrors, PostNoteResponse, PostNoteResponses, ReadFeed, RemoveFeedPasswordData, RemoveFeedPasswordError, RemoveFeedPasswordErrors, RemoveFeedPasswordResponse, RemoveFeedPasswordResponses, UpdateFeedData, UpdateFeedError, UpdateFeedErrors, UpdateFeedResponse, UpdateFeedResponses } from './types.gen.js';
@@ -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, uploadImage } from './sdk.gen.js';
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, UploadImageData, UploadImageErrors, UploadImageResponses } from './types.gen.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, 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` header or a `password` field in the JSON or form body; 1 to 256 printable ASCII characters, with no space at the start or end). Posting to a protected feed needs that password. Also served at `POST /{feed}`, the short form the client packages and curl one-liners use. The body is at most 102400 bytes and must be UTF-8. `application/x-www-form-urlencoded` (what `curl -d` sends) is read as raw markdown, not as form fields. Tags (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed) go in the JSON `tags` array, a repeated `tags` form field, or, for a raw body, the `X-Note-Tags` header.
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). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note and `X-Note-Tags` on every note; with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
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
- * Edit a note
39
+ * Change a note's title or alt text
40
40
  *
41
- * Replaces the note's markdown; its id and creation time stay, the title follows the new text. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. The body is as for posting: at most 102400 bytes, UTF-8, a `password` field is ignored.
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 editNote: <ThrowOnError extends boolean = false>(options: Options<EditNoteData, ThrowOnError>) => RequestResult<EditNoteResponses, EditNoteErrors, ThrowOnError>;
43
+ export declare const patchNote: <ThrowOnError extends boolean = false>(options: Options<PatchNoteData, ThrowOnError>) => RequestResult<PatchNoteResponses, PatchNoteErrors, ThrowOnError>;
44
44
  /**
45
- * Upload an image
45
+ * Replace a note's content
46
46
  *
47
- * The body is the image itself: PNG, JPEG, GIF or WebP, recognized by its first bytes, whatever `Content-Type` is sent (SVG is refused). Stored byte-for-byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in the file), as the first 32 hex characters of its SHA-256 plus an extension: the same bytes always give the same URL. The URL is under the feed's read id, so it works in the feed page, the read-only view and RSS readers without any password. Needs the same credentials as posting and counts against the post rate limit. The feed must exist: it is created by its first note. The size limit is NOTEFEED_MAX_IMAGE_BYTES (default 5 MiB); images deleted only with the feed.
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. A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
48
48
  */
49
- export declare const uploadImage: <ThrowOnError extends boolean = false>(options: Options<UploadImageData, ThrowOnError>) => RequestResult<UploadImageResponses, UploadImageErrors, ThrowOnError>;
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 and text-direction override 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` header or a `password` field in the JSON or form body; 1 to 256 printable ASCII characters, with no space at the start or end). Posting to a protected feed needs that password. Also served at `POST /{feed}`, the short form the client packages and curl one-liners use. The body is at most 102400 bytes and must be UTF-8. `application/x-www-form-urlencoded` (what `curl -d` sends) is read as raw markdown, not as form fields. Tags (at most 10 tags, each 1 to 32 characters of letters, digits, `-`, `_`, `.` and `:`; case is folded to lowercase, duplicates are removed) go in the JSON `tags` array, a repeated `tags` form field, or, for a raw body, the `X-Note-Tags` header.
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). A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. The `text` part is optional: `X-Note-Title` goes on the text note and `X-Note-Tags` on every note; with no `text` part only the pictures are stored, each with the title, and the answer's top level is the first picture. A refused multipart post that would have created a protected feed creates nothing.
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': 'application/json',
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
- * Edit a note
47
+ * Change a note's title or alt text
47
48
  *
48
- * Replaces the note's markdown; its id and creation time stay, the title follows the new text. Needs the feed's password if it has one, and counts against the post rate limit. Read links can't edit. The body is as for posting: at most 102400 bytes, UTF-8, a `password` field is ignored.
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 editNote = (options) => (options.client ?? client).put({
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
- * Upload an image
61
+ * Replace a note's content
61
62
  *
62
- * The body is the image itself: PNG, JPEG, GIF or WebP, recognized by its first bytes, whatever `Content-Type` is sent (SVG is refused). Stored byte-for-byte, with no resizing and no metadata stripped (EXIF such as GPS position stays in the file), as the first 32 hex characters of its SHA-256 plus an extension: the same bytes always give the same URL. The URL is under the feed's read id, so it works in the feed page, the read-only view and RSS readers without any password. Needs the same credentials as posting and counts against the post rate limit. The feed must exist: it is created by its first note. The size limit is NOTEFEED_MAX_IMAGE_BYTES (default 5 MiB); images deleted only with the feed.
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. A markdown note with its pictures is one `multipart/form-data` request: a `text` part (the markdown, a field or a file part; a file part keeps its line breaks, a field's are sent as CRLF), `file` parts (up to 10 pictures, each with its file name and image `Content-Type`) and `alt.<file name>` fields. The pictures are stored first, each as its own note named by its file name, then the text with its references to them (`![](chart.png)`, `[x]: chart.png`, as written or percent-decoded) swapped for the stored files; a picture it never refers to is appended as `![](file)`. Everything is checked before the first write, and a failed write removes what the request stored: all or nothing. `X-Note-Alt` and `X-Note-Name` are 400. The answer has `attachments`, the stored pictures in the order of the `file` parts. On a `PUT` the `text` part is required and the note must be a markdown note; the note keeps its own title and tags, `X-Note-Tags` goes on the new pictures. A raw body ignores `X-Note-Tags`. The note is looked up first: a missing note is `404` and an image note `415` whatever the body holds.
63
64
  */
64
- export const uploadImage = (options) => (options.client ?? client).post({
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}/images',
68
+ url: '/api/v1/feeds/{feed}/notes/{id}',
68
69
  ...options,
69
70
  headers: {
70
- 'Content-Type': 'image/png',
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 and text-direction override 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 slug of the title
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 first heading, or the first non-empty line; may be empty
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
31
+ */
32
+ content?: string;
33
+ /**
34
+ * Alternative text of an image note, when it has one
15
35
  */
16
- markdown: string;
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,52 +79,132 @@ 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;
82
+ /**
83
+ * The note's file name, `<id>.<extension>`. For an image note, pass it as a feed's `image` setting
84
+ */
85
+ file: string;
86
+ /**
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
88
+ */
89
+ file_url: string | null;
90
+ };
91
+ /**
92
+ * The note the post is about: its text note, or with no `text` part its first picture
93
+ */
94
+ export type Posted = {
95
+ id: string;
96
+ /**
97
+ * The note's page in the web UI
98
+ */
99
+ url: string;
100
+ /**
101
+ * The feed's page in the web UI
102
+ */
103
+ feed_url: string;
104
+ /**
105
+ * 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
106
+ */
107
+ read_url: string | null;
108
+ /**
109
+ * The note's file name, `<id>.<extension>`. For an image note, pass it as a feed's `image` setting
110
+ */
111
+ file: string;
112
+ /**
113
+ * 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
114
+ */
115
+ file_url: string | null;
116
+ /**
117
+ * A multipart request's pictures, one per `file` part, in their order; absent for a raw body
118
+ */
119
+ attachments?: Array<Created>;
58
120
  };
59
- export type ImageUploaded = {
121
+ export type NoteEdited = {
122
+ /**
123
+ * 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
124
+ */
125
+ id: string;
60
126
  /**
61
- * The stored file's name: 32 hex characters of the SHA-256 plus the extension. Pass it as a feed's `image` setting
127
+ * The media type of the note's file: `text/markdown`, `image/png`, `image/jpeg`, `image/gif` or `image/webp`
128
+ */
129
+ type: string;
130
+ /**
131
+ * The note's file name, `<id>.<extension>`
62
132
  */
63
133
  file: string;
64
134
  /**
65
- * Where the image is served, absolute, under the feed's read id; public like the read link
135
+ * 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
136
+ */
137
+ file_url: string | null;
138
+ /**
139
+ * The size of the note's file in bytes
140
+ */
141
+ size: number;
142
+ /**
143
+ * 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
144
+ */
145
+ title: string;
146
+ /**
147
+ * The note's text, byte-for-byte as posted: for a text type (markdown) only; absent for a picture
148
+ */
149
+ content?: string;
150
+ /**
151
+ * Alternative text of an image note, when it has one
152
+ */
153
+ alt?: string;
154
+ /**
155
+ * The file name an image was posted with, when it came with one
156
+ */
157
+ name?: string;
158
+ /**
159
+ * When the note was posted (UTC)
160
+ */
161
+ created_at: string;
162
+ /**
163
+ * The note's page in the web UI
66
164
  */
67
165
  url: string;
68
166
  /**
69
- * `![](url)`, to paste into a note
167
+ * Verified sign-in name of the poster; absent when the note was posted without a sign-in
168
+ */
169
+ sender?: string | null;
170
+ /**
171
+ * Labels the poster gave the note (not verified, and shown to readers like the note itself); empty when none
70
172
  */
71
- markdown: string;
173
+ tags: Array<string>;
174
+ /**
175
+ * A multipart request's pictures, one per `file` part, in their order; absent for a raw body
176
+ */
177
+ attachments?: Array<Created>;
72
178
  };
73
- export type Error = {
179
+ export type MultipartNote = {
74
180
  /**
75
- * A short reason, for people
181
+ * The markdown note (UTF-8): a field, or a file part (its name is ignored; its type, if any, `text/markdown` or `application/octet-stream`) to keep its line breaks as they are, since a form field's are sent as CRLF. At most one; required on a PUT. Its references to the files (`![](chart.png)`, `[x]: chart.png`) are swapped for the stored files; a file it never refers to is appended as `![](file)`
76
182
  */
77
- error: string;
183
+ text?: string;
78
184
  /**
79
- * Stable machine-readable code; absent only on a 500
185
+ * A picture: up to 10 parts, each with a file name (one path segment, 1 to 200 characters, unique in the request) and its image `Content-Type`. An `alt.<file name>` field gives one its alternative text
80
186
  */
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';
187
+ file?: Array<Blob | File>;
82
188
  };
83
- export type PostJson = {
84
- markdown: string;
189
+ export type Error = {
85
190
  /**
86
- * 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.
191
+ * A short reason, for people
87
192
  */
88
- password?: string;
193
+ error: string;
89
194
  /**
90
- * 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.
195
+ * Stable machine-readable code; absent only on a 500
91
196
  */
92
- tags?: Array<string>;
197
+ 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';
93
198
  };
94
- export type PostForm = {
95
- markdown: string;
199
+ export type NoteMeta = {
96
200
  /**
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.
201
+ * The note's title, at most 100 characters, one line (control and text-direction override characters are refused); empty or left out means the title is taken from the text (a markdown note) or there is none (an image)
98
202
  */
99
- password?: string;
203
+ title?: string;
100
204
  /**
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.
205
+ * Alternative text of an image note, at most 500 characters, one line (control and text-direction override characters are refused)
102
206
  */
103
- tags?: Array<string>;
207
+ alt?: string;
104
208
  };
105
209
  export type PasswordJson = {
106
210
  /**
@@ -110,11 +214,11 @@ export type PasswordJson = {
110
214
  };
111
215
  export type FeedSettings = {
112
216
  /**
113
- * Display title, at most 100 characters, one line; empty means none (the feed's name is shown)
217
+ * Display title, at most 100 characters, one line (control and text-direction override characters are refused); empty means none (the feed's name is shown)
114
218
  */
115
219
  title: string;
116
220
  /**
117
- * Description, at most 500 characters, one line; may be empty
221
+ * Description, at most 500 characters, one line (control and text-direction override characters are refused); may be empty
118
222
  */
119
223
  description: string;
120
224
  /**
@@ -125,6 +229,10 @@ export type FeedSettings = {
125
229
  * 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
230
  */
127
231
  show_sender?: boolean;
232
+ /**
233
+ * 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 (`![](file)`) 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
234
+ */
235
+ read_id?: string;
128
236
  };
129
237
  export type Feed = {
130
238
  /**
@@ -132,11 +240,11 @@ export type Feed = {
132
240
  */
133
241
  name: string;
134
242
  /**
135
- * Display title, at most 100 characters, one line; empty means none (the feed's name is shown)
243
+ * Display title, at most 100 characters, one line (control and text-direction override characters are refused); empty means none (the feed's name is shown)
136
244
  */
137
245
  title: string;
138
246
  /**
139
- * Description, at most 500 characters, one line; may be empty
247
+ * Description, at most 500 characters, one line (control and text-direction override characters are refused); may be empty
140
248
  */
141
249
  description: string;
142
250
  /**
@@ -158,11 +266,11 @@ export type Feed = {
158
266
  };
159
267
  export type ReadFeed = {
160
268
  /**
161
- * Display title, at most 100 characters, one line; empty means none (the feed's name is shown)
269
+ * Display title, at most 100 characters, one line (control and text-direction override characters are refused); empty means none (the feed's name is shown)
162
270
  */
163
271
  title: string;
164
272
  /**
165
- * Description, at most 500 characters, one line; may be empty
273
+ * Description, at most 500 characters, one line (control and text-direction override characters are refused); may be empty
166
274
  */
167
275
  description: string;
168
276
  /**
@@ -223,16 +331,35 @@ export type ListNotesResponses = {
223
331
  };
224
332
  export type ListNotesResponse = ListNotesResponses[keyof ListNotesResponses];
225
333
  export type PostNoteData = {
226
- body: PostJson;
334
+ /**
335
+ * The note, a file: its bytes, of the type `Content-Type` declares
336
+ */
337
+ body: Blob | File;
227
338
  headers?: {
228
339
  /**
229
340
  * 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
341
  */
231
342
  'X-Feed-Password'?: string;
343
+ /**
344
+ * 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
345
+ */
346
+ 'X-Read-Id'?: string;
347
+ /**
348
+ * The note's title, for a raw body; a JSON or form body's own `title` wins. At most 100 characters, one line, with no control or text-direction override characters.
349
+ */
350
+ 'X-Note-Title'?: string;
232
351
  /**
233
352
  * 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
353
  */
235
354
  'X-Note-Tags'?: string;
355
+ /**
356
+ * For an image body: its alternative text; a form's own `alt` wins.
357
+ */
358
+ 'X-Note-Alt'?: string;
359
+ /**
360
+ * For an image body: the picture's original file name, kept with the note (at most 200 characters)
361
+ */
362
+ 'X-Note-Name'?: string;
236
363
  };
237
364
  path: {
238
365
  /**
@@ -245,7 +372,7 @@ export type PostNoteData = {
245
372
  };
246
373
  export type PostNoteErrors = {
247
374
  /**
248
- * Invalid or reserved feed name; empty note; bad JSON, form or UTF-8; a new password that is not printable ASCII; invalid tags
375
+ * Invalid or reserved feed name; a blank note; a bad title, alt text or tags; a new password that is not printable ASCII; a multipart body with an unexpected part, no `text` and no `file`, more than 10 files, a bad or repeated file name, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
249
376
  */
250
377
  400: Error;
251
378
  /**
@@ -253,15 +380,15 @@ export type PostNoteErrors = {
253
380
  */
254
381
  401: Error;
255
382
  /**
256
- * A password was sent for a feed that already exists without one: it can't be claimed
383
+ * 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
384
  */
258
385
  409: Error;
259
386
  /**
260
- * Body over 102400 bytes
387
+ * Markdown over 102400 bytes, or an image over NOTEFEED_MAX_IMAGE_BYTES (each picture of a multipart body too), or a multipart body over 102400 bytes plus 10 images plus 64 KiB
261
388
  */
262
389
  413: Error;
263
390
  /**
264
- * Unsupported content type
391
+ * Content-Type missing or not one of the accepted types, or the body (or a multipart body's picture) is not what it declares
265
392
  */
266
393
  415: Error;
267
394
  /**
@@ -269,16 +396,16 @@ export type PostNoteErrors = {
269
396
  */
270
397
  429: Error;
271
398
  /**
272
- * NOTEFEED_MAX_FEEDS or NOTEFEED_MAX_NOTES_PER_FEED reached
399
+ * NOTEFEED_MAX_FEEDS, NOTEFEED_MAX_NOTES_PER_FEED or NOTEFEED_MAX_IMAGES_PER_FEED reached
273
400
  */
274
401
  507: Error;
275
402
  };
276
403
  export type PostNoteError = PostNoteErrors[keyof PostNoteErrors];
277
404
  export type PostNoteResponses = {
278
405
  /**
279
- * Stored
406
+ * Stored: the note, and with a multipart body its pictures
280
407
  */
281
- 201: Created;
408
+ 201: Posted;
282
409
  };
283
410
  export type PostNoteResponse = PostNoteResponses[keyof PostNoteResponses];
284
411
  export type DeleteNoteData = {
@@ -375,8 +502,8 @@ export type GetNoteResponses = {
375
502
  200: Note;
376
503
  };
377
504
  export type GetNoteResponse = GetNoteResponses[keyof GetNoteResponses];
378
- export type EditNoteData = {
379
- body: PostJson;
505
+ export type PatchNoteData = {
506
+ body: NoteMeta;
380
507
  headers?: {
381
508
  /**
382
509
  * 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 +523,9 @@ export type EditNoteData = {
396
523
  query?: never;
397
524
  url: '/api/v1/feeds/{feed}/notes/{id}';
398
525
  };
399
- export type EditNoteErrors = {
526
+ export type PatchNoteErrors = {
400
527
  /**
401
- * Invalid or reserved feed name; empty note; bad JSON, form or UTF-8
528
+ * Invalid or reserved feed name; nothing to change; a bad title or alt text; alt for a note that has none; bad JSON
402
529
  */
403
530
  400: Error;
404
531
  /**
@@ -410,11 +537,7 @@ export type EditNoteErrors = {
410
537
  */
411
538
  404: Error;
412
539
  /**
413
- * Body over 102400 bytes
414
- */
415
- 413: Error;
416
- /**
417
- * Unsupported content type
540
+ * Content-Type is not application/json
418
541
  */
419
542
  415: Error;
420
543
  /**
@@ -422,17 +545,17 @@ export type EditNoteErrors = {
422
545
  */
423
546
  429: Error;
424
547
  };
425
- export type EditNoteError = EditNoteErrors[keyof EditNoteErrors];
426
- export type EditNoteResponses = {
548
+ export type PatchNoteError = PatchNoteErrors[keyof PatchNoteErrors];
549
+ export type PatchNoteResponses = {
427
550
  /**
428
551
  * The note as it is now
429
552
  */
430
553
  200: Note;
431
554
  };
432
- export type EditNoteResponse = EditNoteResponses[keyof EditNoteResponses];
433
- export type UploadImageData = {
555
+ export type PatchNoteResponse = PatchNoteResponses[keyof PatchNoteResponses];
556
+ export type EditNoteData = {
434
557
  /**
435
- * The image's bytes: PNG, JPEG, GIF or WebP, recognized by their first bytes, not by the Content-Type
558
+ * The note, a file: its bytes, of the type `Content-Type` declares
436
559
  */
437
560
  body: Blob | File;
438
561
  headers?: {
@@ -440,19 +563,27 @@ export type UploadImageData = {
440
563
  * 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.
441
564
  */
442
565
  'X-Feed-Password'?: string;
566
+ /**
567
+ * Only for a multipart body: the tags go on the new pictures (the note keeps its own). A raw body ignores it. Comma-separated, empty is none.
568
+ */
569
+ 'X-Note-Tags'?: string;
443
570
  };
444
571
  path: {
445
572
  /**
446
573
  * The feed's name. It is the write key: anyone who knows it can post.
447
574
  */
448
575
  feed: string;
576
+ /**
577
+ * The note's id
578
+ */
579
+ id: string;
449
580
  };
450
581
  query?: never;
451
- url: '/api/v1/feeds/{feed}/images';
582
+ url: '/api/v1/feeds/{feed}/notes/{id}';
452
583
  };
453
- export type UploadImageErrors = {
584
+ export type EditNoteErrors = {
454
585
  /**
455
- * Invalid or reserved feed name
586
+ * Invalid or reserved feed name; a blank note; a multipart body with an unexpected part, no `text`, more than 10 files, a bad or repeated file name, a text sent with pictures that takes too long to read (`NOTEFEED_PARSE_TIMEOUT_MS`), or `X-Note-Alt` / `X-Note-Name`
456
587
  */
457
588
  400: Error;
458
589
  /**
@@ -460,19 +591,19 @@ export type UploadImageErrors = {
460
591
  */
461
592
  401: Error;
462
593
  /**
463
- * No such feed
594
+ * No such note
464
595
  */
465
596
  404: Error;
466
597
  /**
467
- * Body over NOTEFEED_MAX_IMAGE_BYTES
598
+ * Markdown over 102400 bytes, or an image over NOTEFEED_MAX_IMAGE_BYTES (each picture of a multipart body too), or a multipart body over 102400 bytes plus 10 images plus 64 KiB
468
599
  */
469
600
  413: Error;
470
601
  /**
471
- * Not a PNG, JPEG, GIF or WebP image
602
+ * Content-Type missing, not accepted or not the note's own type (a multipart body is for a markdown note), or the body (or a picture) is not what it declares
472
603
  */
473
604
  415: Error;
474
605
  /**
475
- * Too many posts, uploads, edits and deletes, or wrong passwords, from this client
606
+ * Too many posts, edits and deletes, or wrong passwords, from this client
476
607
  */
477
608
  429: Error;
478
609
  /**
@@ -480,14 +611,14 @@ export type UploadImageErrors = {
480
611
  */
481
612
  507: Error;
482
613
  };
483
- export type UploadImageError = UploadImageErrors[keyof UploadImageErrors];
484
- export type UploadImageResponses = {
614
+ export type EditNoteError = EditNoteErrors[keyof EditNoteErrors];
615
+ export type EditNoteResponses = {
485
616
  /**
486
- * Stored (or already there)
617
+ * The note as it is now, and with a multipart body the new pictures
487
618
  */
488
- 201: ImageUploaded;
619
+ 200: NoteEdited;
489
620
  };
490
- export type UploadImageResponse = UploadImageResponses[keyof UploadImageResponses];
621
+ export type EditNoteResponse = EditNoteResponses[keyof EditNoteResponses];
491
622
  export type DeleteFeedData = {
492
623
  body?: never;
493
624
  headers?: {
@@ -593,7 +724,7 @@ export type UpdateFeedData = {
593
724
  };
594
725
  export type UpdateFeedErrors = {
595
726
  /**
596
- * Invalid or reserved feed name, bad JSON, or a title or description that is too long or has control characters
727
+ * Invalid or reserved feed name, bad JSON, a title or description that is too long or has control or text-direction override characters, a malformed read id, a reserved feed's read id, or chosen read ids turned off
597
728
  */
598
729
  400: Error;
599
730
  /**
@@ -604,6 +735,10 @@ export type UpdateFeedErrors = {
604
735
  * No such feed
605
736
  */
606
737
  404: Error;
738
+ /**
739
+ * The read id belongs to another feed or is held back for a reserved one
740
+ */
741
+ 409: Error;
607
742
  /**
608
743
  * Too many posts, edits and deletes, or wrong passwords, from this client
609
744
  */
@@ -707,7 +842,7 @@ export type GetReadFeedData = {
707
842
  body?: never;
708
843
  path: {
709
844
  /**
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.
845
+ * 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
846
  */
712
847
  readId: string;
713
848
  };
@@ -732,7 +867,7 @@ export type ListReadNotesData = {
732
867
  body?: never;
733
868
  path: {
734
869
  /**
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.
870
+ * 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
871
  */
737
872
  readId: string;
738
873
  };
@@ -774,7 +909,7 @@ export type GetReadNoteData = {
774
909
  body?: never;
775
910
  path: {
776
911
  /**
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.
912
+ * 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
913
  */
779
914
  readId: string;
780
915
  /**
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, ImageUploaded, Note } from "./client.js";
2
+ export type { Attachment, ClientOptions, Created, ErrorCode, Feed, FeedSettings, Note } from "./client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notefeed",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Client and CLI for notefeed, a self-hosted inbox for markdown notes that you read back as RSS.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Marcel Bruckner",
@@ -39,7 +39,7 @@
39
39
  "url": "git+https://github.com/Marceltov/notefeed.git",
40
40
  "directory": "packages/js"
41
41
  },
42
- "homepage": "https://docs.notefeed.me/clients/",
42
+ "homepage": "https://docs.notefeed.me/integrations/clients/",
43
43
  "bugs": {
44
44
  "url": "https://github.com/Marceltov/notefeed/issues"
45
45
  },