@sofia-post/node 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +64 -2
- package/package.json +29 -3
- package/src/index.d.ts +7 -0
- package/src/index.js +8 -0
- package/src/lib/client/sofia.d.ts +57 -0
- package/src/lib/client/sofia.js +135 -0
- package/src/lib/discovery/discovery.types.d.ts +50 -0
- package/src/lib/discovery/discovery.types.js +2 -0
- package/src/lib/establishments/establishment.types.d.ts +71 -0
- package/src/lib/establishments/establishment.types.js +2 -0
- package/src/lib/http/api-request.d.ts +23 -0
- package/src/lib/http/api-request.js +2 -0
- package/src/lib/http/content-type.d.ts +2 -0
- package/src/lib/http/content-type.js +20 -0
- package/src/lib/http/error-from-response.d.ts +9 -0
- package/src/lib/http/error-from-response.js +43 -0
- package/src/lib/http/path-of.d.ts +2 -0
- package/src/lib/http/path-of.js +6 -0
- package/src/lib/http/sdk-version.d.ts +2 -0
- package/src/lib/http/sdk-version.js +5 -0
- package/src/lib/http/sofia-api-error.d.ts +33 -0
- package/src/lib/http/sofia-api-error.js +20 -0
- package/src/lib/http/sofia-http-client.d.ts +16 -0
- package/src/lib/http/sofia-http-client.js +78 -0
- package/src/lib/media/media.types.d.ts +34 -0
- package/src/lib/media/media.types.js +2 -0
- package/src/lib/posts/post.types.d.ts +109 -0
- package/src/lib/posts/post.types.js +2 -0
- package/src/lib/webhooks/webhook.types.d.ts +19 -0
- package/src/lib/webhooks/webhook.types.js +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sofia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sofia Node.js SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official Node.js SDK for [Sofia API](https://www.sofia-post.com/developers/reference): publish, schedule and read your social posts from your own code.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @sofia-post/node
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Node.js 20 or later. No dependencies: the SDK uses the `fetch`, `FormData` and `Blob` built into Node.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Sofia } from '@sofia-post/node';
|
|
15
|
+
|
|
16
|
+
const sofia = new Sofia(process.env.SOFIA_API_KEY);
|
|
17
|
+
|
|
18
|
+
const [establishment] = await sofia.establishments();
|
|
19
|
+
|
|
20
|
+
const post = await sofia.post({
|
|
21
|
+
establishmentId: establishment.id,
|
|
22
|
+
targets: [{ platform: 'instagram', text: 'Tarte tatin du jour 🍎' }],
|
|
23
|
+
scheduledAt: new Date('2026-12-01T09:00:00Z'),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The key is a personal key (`sofia_pk_live_…`, or `sofia_pk_test_…` for the sandbox) created in **Settings › API**, **Personal access** tab, or an OAuth access token. A second argument changes the base URL: `new Sofia(key, { baseUrl: 'http://localhost:3008' })`.
|
|
28
|
+
|
|
29
|
+
## Methods
|
|
30
|
+
|
|
31
|
+
| Method | What it does |
|
|
32
|
+
| ------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
33
|
+
| `me()` | Who the key acts for, its scopes, test or live |
|
|
34
|
+
| `platforms()` | Each network's rules: caption length, media, formats |
|
|
35
|
+
| `establishments()` | The establishments the key reaches |
|
|
36
|
+
| `accounts(establishmentId)` | The connected social accounts of an establishment |
|
|
37
|
+
| `connectionLink(establishmentId, input)` | A hosted page where the person connects a network |
|
|
38
|
+
| `insights(establishmentId, period?)` | Reach, engagement and followers per network |
|
|
39
|
+
| `post(input, { dryRun?, idempotencyKey? })` | Publish now, schedule (`scheduledAt`) or save a draft |
|
|
40
|
+
| `postList(filters?)` | A page of posts, filtered by establishment, status or dates |
|
|
41
|
+
| `getPost(id)` | One post and the state of each network |
|
|
42
|
+
| `updatePost(id, input)` | Change the text, media or date of a post not yet published |
|
|
43
|
+
| `deletePost(id)` | Withdraw a post |
|
|
44
|
+
| `upload({ establishmentId, file, fileName })` | Add a file to the library (a `Buffer`, `Uint8Array` or `Blob`) |
|
|
45
|
+
| `uploadFromUrl({ establishmentId, url })` | Let Sofia download a file into the library |
|
|
46
|
+
| `mediaList(filters)` · `getMedia(id)` · `deleteMedia(id)` | Read and clean the library |
|
|
47
|
+
| `createWebhook(input)` · `webhooks()` · `deleteWebhook(id)` · `testWebhook(id)` | Be told when a post goes out or fails |
|
|
48
|
+
|
|
49
|
+
## Errors
|
|
50
|
+
|
|
51
|
+
Every method throws a `SofiaApiError` on failure, with the API's `status`, a stable `code`, `details` (the network rules a post breaks, for instance), `requestId` and, on 429, `retryAfterSeconds`. When the API cannot be reached, `code` is `network_error` and `status` is 0.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { SofiaApiError } from '@sofia-post/node';
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await sofia.post(input);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof SofiaApiError && error.code === 'post_refused') {
|
|
60
|
+
console.log(error.details);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Full documentation: [sofia-post.com/developers/node-sdk](https://www.sofia-post.com/developers/node-sdk).
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sofia-post/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Node.js SDK for the Sofia API: publish, schedule and read your social posts from your own code.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://www.sofia-post.com/developers/node-sdk",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"sofia",
|
|
22
|
+
"social-media",
|
|
23
|
+
"publishing",
|
|
24
|
+
"scheduling",
|
|
25
|
+
"sdk",
|
|
26
|
+
"api"
|
|
27
|
+
],
|
|
28
|
+
"author": "Sofia <contact@sofia-post.com>",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"email": "contact@sofia-post.com"
|
|
31
|
+
}
|
|
6
32
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_BASE_URL, Sofia, type SofiaOptions } from './lib/client/sofia';
|
|
2
|
+
export { SofiaApiError, type SofiaApiErrorDetail } from './lib/http/sofia-api-error';
|
|
3
|
+
export type { Platform, Instant, PostMedia, PostTarget, PublishPostInput, PublishPostOptions, EditPostInput, TargetState, LivePostTarget, LivePost, SandboxPost, CreatedLivePost, CheckedPost, Post, PublishedPost, ListPostsFilters, Page, } from './lib/posts/post.types';
|
|
4
|
+
export type { MediaType, Media, MediaListItem, UploadableFile, UploadInput, UploadFromUrlInput, ListMediaFilters, } from './lib/media/media.types';
|
|
5
|
+
export type { Establishment, SocialAccount, ConnectionLinkInput, ConnectionLink, InsightsPeriod, MetricReading, NetworkInsights, EstablishmentInsights, } from './lib/establishments/establishment.types';
|
|
6
|
+
export type { WebhookEvent, RegisterWebhookInput, Webhook, CreatedWebhook, WebhookTest, } from './lib/webhooks/webhook.types';
|
|
7
|
+
export type { Scope, Me, PlatformRules } from './lib/discovery/discovery.types';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SofiaApiError = exports.Sofia = exports.DEFAULT_BASE_URL = void 0;
|
|
4
|
+
var sofia_1 = require("./lib/client/sofia");
|
|
5
|
+
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return sofia_1.DEFAULT_BASE_URL; } });
|
|
6
|
+
Object.defineProperty(exports, "Sofia", { enumerable: true, get: function () { return sofia_1.Sofia; } });
|
|
7
|
+
var sofia_api_error_1 = require("./lib/http/sofia-api-error");
|
|
8
|
+
Object.defineProperty(exports, "SofiaApiError", { enumerable: true, get: function () { return sofia_api_error_1.SofiaApiError; } });
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Me, PlatformRules } from '../discovery/discovery.types';
|
|
2
|
+
import type { ConnectionLink, ConnectionLinkInput, Establishment, EstablishmentInsights, InsightsPeriod, SocialAccount } from '../establishments/establishment.types';
|
|
3
|
+
import type { ListMediaFilters, Media, MediaListItem, UploadFromUrlInput, UploadInput } from '../media/media.types';
|
|
4
|
+
import type { CheckedPost, CreatedLivePost, EditPostInput, ListPostsFilters, LivePost, Page, Post, PublishedPost, PublishPostInput, PublishPostOptions, SandboxPost } from '../posts/post.types';
|
|
5
|
+
import type { CreatedWebhook, RegisterWebhookInput, Webhook, WebhookTest } from '../webhooks/webhook.types';
|
|
6
|
+
export declare const DEFAULT_BASE_URL = "https://api.sofia-post.com";
|
|
7
|
+
export type SofiaOptions = {
|
|
8
|
+
/** `https://api.sofia-post.com` by default. */
|
|
9
|
+
readonly baseUrl?: string;
|
|
10
|
+
/** The `fetch` to send requests with: the global one by default (Node 20+). */
|
|
11
|
+
readonly fetch?: typeof fetch;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The Sofia API v1 client. One instance per key: a personal key (`sofia_pk_live_…`,
|
|
15
|
+
* `sofia_pk_test_…`) or an OAuth access token. Every method throws a `SofiaApiError` on failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Sofia {
|
|
18
|
+
private readonly http;
|
|
19
|
+
/** `apiKey` may come straight from `process.env`: an absent or empty key throws here, before any request. */
|
|
20
|
+
constructor(apiKey: string | undefined, options?: SofiaOptions);
|
|
21
|
+
/** Who the key acts for, its scopes, and whether it is a test key. */
|
|
22
|
+
me(): Promise<Me>;
|
|
23
|
+
/** Each network's rules: caption length, media, formats. */
|
|
24
|
+
platforms(): Promise<readonly PlatformRules[]>;
|
|
25
|
+
establishments(): Promise<readonly Establishment[]>;
|
|
26
|
+
/** The connected social accounts of an establishment: Postiz's `integrations()`. */
|
|
27
|
+
accounts(establishmentId: string): Promise<readonly SocialAccount[]>;
|
|
28
|
+
/** A page hosted by Sofia where the person connects a network to the establishment. */
|
|
29
|
+
connectionLink(establishmentId: string, input: ConnectionLinkInput): Promise<ConnectionLink>;
|
|
30
|
+
insights(establishmentId: string, period?: InsightsPeriod): Promise<EstablishmentInsights>;
|
|
31
|
+
/** A dry run writes nothing: it answers the post Sofia would accept, or throws its refusal. */
|
|
32
|
+
post(input: PublishPostInput, options: PublishPostOptions & {
|
|
33
|
+
dryRun: true;
|
|
34
|
+
}): Promise<CheckedPost>;
|
|
35
|
+
/** Publishes now, schedules (`scheduledAt`) or saves a draft (`status: 'draft'`). */
|
|
36
|
+
post(input: PublishPostInput, options?: PublishPostOptions & {
|
|
37
|
+
dryRun?: false;
|
|
38
|
+
}): Promise<CreatedLivePost | SandboxPost>;
|
|
39
|
+
post(input: PublishPostInput, options?: PublishPostOptions): Promise<PublishedPost>;
|
|
40
|
+
postList(filters?: ListPostsFilters): Promise<Page<Post>>;
|
|
41
|
+
getPost(id: string): Promise<Post>;
|
|
42
|
+
updatePost(id: string, input: EditPostInput): Promise<LivePost>;
|
|
43
|
+
/** Withdraws the post from every network it was scheduled on. */
|
|
44
|
+
deletePost(id: string): Promise<void>;
|
|
45
|
+
/** Adds a file to the establishment's library; its `id` then goes in a post's `media`. */
|
|
46
|
+
upload(input: UploadInput): Promise<Media>;
|
|
47
|
+
/** Sofia downloads the file at `url` itself. */
|
|
48
|
+
uploadFromUrl({ establishmentId, url, idempotencyKey }: UploadFromUrlInput): Promise<Media>;
|
|
49
|
+
mediaList(filters: ListMediaFilters): Promise<Page<MediaListItem>>;
|
|
50
|
+
getMedia(id: string): Promise<Media>;
|
|
51
|
+
deleteMedia(id: string): Promise<void>;
|
|
52
|
+
createWebhook(input: RegisterWebhookInput): Promise<CreatedWebhook>;
|
|
53
|
+
webhooks(): Promise<readonly Webhook[]>;
|
|
54
|
+
deleteWebhook(id: string): Promise<void>;
|
|
55
|
+
/** Sends a test delivery, signed like a real one. */
|
|
56
|
+
testWebhook(id: string): Promise<WebhookTest>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Sofia = exports.DEFAULT_BASE_URL = void 0;
|
|
4
|
+
const content_type_1 = require("../http/content-type");
|
|
5
|
+
const path_of_1 = require("../http/path-of");
|
|
6
|
+
const sdk_version_1 = require("../http/sdk-version");
|
|
7
|
+
const sofia_http_client_1 = require("../http/sofia-http-client");
|
|
8
|
+
exports.DEFAULT_BASE_URL = 'https://api.sofia-post.com';
|
|
9
|
+
const blobOf = ({ file, fileName, contentType }) => {
|
|
10
|
+
const type = contentType ?? (0, content_type_1.contentTypeOf)(fileName);
|
|
11
|
+
return new Blob([file instanceof Blob ? file : new Uint8Array(file)], { type });
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The Sofia API v1 client. One instance per key: a personal key (`sofia_pk_live_…`,
|
|
15
|
+
* `sofia_pk_test_…`) or an OAuth access token. Every method throws a `SofiaApiError` on failure.
|
|
16
|
+
*/
|
|
17
|
+
class Sofia {
|
|
18
|
+
/** `apiKey` may come straight from `process.env`: an absent or empty key throws here, before any request. */
|
|
19
|
+
constructor(apiKey, options = {}) {
|
|
20
|
+
if (apiKey === undefined || apiKey.trim() === '') {
|
|
21
|
+
throw new TypeError('Sofia needs an API key: new Sofia(apiKey)');
|
|
22
|
+
}
|
|
23
|
+
this.http = new sofia_http_client_1.SofiaHttpClient({
|
|
24
|
+
apiKey,
|
|
25
|
+
baseUrl: (options.baseUrl ?? exports.DEFAULT_BASE_URL).replace(/\/+$/, ''),
|
|
26
|
+
userAgent: `sofia-node/${sdk_version_1.SDK_VERSION}`,
|
|
27
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Who the key acts for, its scopes, and whether it is a test key. */
|
|
31
|
+
me() {
|
|
32
|
+
return this.http.send({ method: 'GET', path: '/me' });
|
|
33
|
+
}
|
|
34
|
+
/** Each network's rules: caption length, media, formats. */
|
|
35
|
+
async platforms() {
|
|
36
|
+
return (await this.http.send({ method: 'GET', path: '/platforms' })).data;
|
|
37
|
+
}
|
|
38
|
+
async establishments() {
|
|
39
|
+
const list = await this.http.send({
|
|
40
|
+
method: 'GET',
|
|
41
|
+
path: '/establishments',
|
|
42
|
+
});
|
|
43
|
+
return list.data;
|
|
44
|
+
}
|
|
45
|
+
/** The connected social accounts of an establishment: Postiz's `integrations()`. */
|
|
46
|
+
async accounts(establishmentId) {
|
|
47
|
+
const path = (0, path_of_1.pathOf)('establishments', establishmentId, 'accounts');
|
|
48
|
+
return (await this.http.send({ method: 'GET', path })).data;
|
|
49
|
+
}
|
|
50
|
+
/** A page hosted by Sofia where the person connects a network to the establishment. */
|
|
51
|
+
connectionLink(establishmentId, input) {
|
|
52
|
+
const path = (0, path_of_1.pathOf)('establishments', establishmentId, 'connection-links');
|
|
53
|
+
return this.http.send({ method: 'POST', path, body: { kind: 'json', value: input } });
|
|
54
|
+
}
|
|
55
|
+
insights(establishmentId, period) {
|
|
56
|
+
const path = (0, path_of_1.pathOf)('establishments', establishmentId, 'insights');
|
|
57
|
+
return this.http.send({ method: 'GET', path, query: { period } });
|
|
58
|
+
}
|
|
59
|
+
post(input, options = {}) {
|
|
60
|
+
return this.http.send({
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: '/posts',
|
|
63
|
+
query: { dry_run: options.dryRun ? 'true' : undefined },
|
|
64
|
+
body: { kind: 'json', value: input },
|
|
65
|
+
idempotencyKey: options.idempotencyKey,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
postList(filters = {}) {
|
|
69
|
+
return this.http.send({ method: 'GET', path: '/posts', query: filters });
|
|
70
|
+
}
|
|
71
|
+
getPost(id) {
|
|
72
|
+
return this.http.send({ method: 'GET', path: (0, path_of_1.pathOf)('posts', id) });
|
|
73
|
+
}
|
|
74
|
+
updatePost(id, input) {
|
|
75
|
+
return this.http.send({
|
|
76
|
+
method: 'PATCH',
|
|
77
|
+
path: (0, path_of_1.pathOf)('posts', id),
|
|
78
|
+
body: { kind: 'json', value: input },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Withdraws the post from every network it was scheduled on. */
|
|
82
|
+
async deletePost(id) {
|
|
83
|
+
await this.http.send({ method: 'DELETE', path: (0, path_of_1.pathOf)('posts', id) });
|
|
84
|
+
}
|
|
85
|
+
/** Adds a file to the establishment's library; its `id` then goes in a post's `media`. */
|
|
86
|
+
upload(input) {
|
|
87
|
+
return this.http.send({
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/media',
|
|
90
|
+
body: {
|
|
91
|
+
kind: 'multipart',
|
|
92
|
+
fields: { establishmentId: input.establishmentId },
|
|
93
|
+
file: { field: 'file', name: input.fileName, blob: blobOf(input) },
|
|
94
|
+
},
|
|
95
|
+
idempotencyKey: input.idempotencyKey,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Sofia downloads the file at `url` itself. */
|
|
99
|
+
uploadFromUrl({ establishmentId, url, idempotencyKey }) {
|
|
100
|
+
const value = { establishmentId, url };
|
|
101
|
+
return this.http.send({
|
|
102
|
+
method: 'POST',
|
|
103
|
+
path: '/media',
|
|
104
|
+
body: { kind: 'json', value },
|
|
105
|
+
idempotencyKey,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
mediaList(filters) {
|
|
109
|
+
return this.http.send({ method: 'GET', path: '/media', query: filters });
|
|
110
|
+
}
|
|
111
|
+
getMedia(id) {
|
|
112
|
+
return this.http.send({ method: 'GET', path: (0, path_of_1.pathOf)('media', id) });
|
|
113
|
+
}
|
|
114
|
+
async deleteMedia(id) {
|
|
115
|
+
await this.http.send({ method: 'DELETE', path: (0, path_of_1.pathOf)('media', id) });
|
|
116
|
+
}
|
|
117
|
+
createWebhook(input) {
|
|
118
|
+
return this.http.send({
|
|
119
|
+
method: 'POST',
|
|
120
|
+
path: '/webhooks',
|
|
121
|
+
body: { kind: 'json', value: input },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async webhooks() {
|
|
125
|
+
return (await this.http.send({ method: 'GET', path: '/webhooks' })).data;
|
|
126
|
+
}
|
|
127
|
+
async deleteWebhook(id) {
|
|
128
|
+
await this.http.send({ method: 'DELETE', path: (0, path_of_1.pathOf)('webhooks', id) });
|
|
129
|
+
}
|
|
130
|
+
/** Sends a test delivery, signed like a real one. */
|
|
131
|
+
testWebhook(id) {
|
|
132
|
+
return this.http.send({ method: 'POST', path: (0, path_of_1.pathOf)('webhooks', id, 'test') });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.Sofia = Sofia;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type Scope = 'establishments:read' | 'accounts:read' | 'accounts:write' | 'media:read' | 'media:write' | 'posts:read' | 'posts:write' | 'insights:read' | 'webhooks:read' | 'webhooks:write' | 'offline_access';
|
|
2
|
+
/** Who the key acts for, and what it may do. */
|
|
3
|
+
export type Me = {
|
|
4
|
+
readonly tokenType: 'personal_key' | 'authorization_code' | 'client_credentials';
|
|
5
|
+
readonly application: {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly clientId: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
};
|
|
10
|
+
readonly user: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
};
|
|
13
|
+
readonly coverage: 'workspace' | 'account';
|
|
14
|
+
readonly workspaces: readonly {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
}[];
|
|
18
|
+
readonly scopes: readonly Scope[];
|
|
19
|
+
/** `test`: nothing reaches a network. */
|
|
20
|
+
readonly mode: 'test' | 'live';
|
|
21
|
+
readonly expiresAt?: string;
|
|
22
|
+
};
|
|
23
|
+
/** A network's rules, the same ones Sofia applies before publishing. */
|
|
24
|
+
export type PlatformRules = {
|
|
25
|
+
readonly platform: 'instagram' | 'facebook' | 'x' | 'linkedin' | 'tiktok';
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly available: boolean;
|
|
28
|
+
readonly caption: {
|
|
29
|
+
readonly maxLength: number;
|
|
30
|
+
readonly maxHashtags?: number;
|
|
31
|
+
readonly maxMentions?: number;
|
|
32
|
+
readonly enforced: boolean;
|
|
33
|
+
readonly thread?: {
|
|
34
|
+
readonly maxPosts: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
readonly media: {
|
|
38
|
+
readonly maxCount: number;
|
|
39
|
+
readonly enforced: boolean;
|
|
40
|
+
readonly mixImagesAndVideo: boolean;
|
|
41
|
+
readonly imageTypes: readonly string[];
|
|
42
|
+
readonly videoTypes: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
readonly formats: readonly {
|
|
45
|
+
readonly format: string;
|
|
46
|
+
readonly default: boolean;
|
|
47
|
+
readonly requiredMedia: 'none' | 'image' | 'video' | 'any';
|
|
48
|
+
readonly captionPublished: boolean;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Platform } from '../posts/post.types';
|
|
2
|
+
export type Establishment = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
};
|
|
6
|
+
/** A connected social account: Postiz calls it an integration, the Sofia app a network. */
|
|
7
|
+
export type SocialAccount = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly platform: string;
|
|
10
|
+
readonly handle?: string;
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
readonly avatarUrl?: string;
|
|
13
|
+
readonly status: string;
|
|
14
|
+
};
|
|
15
|
+
export type ConnectionLinkInput = {
|
|
16
|
+
readonly platform: Platform;
|
|
17
|
+
/** Where Sofia sends the person back once the account is connected. */
|
|
18
|
+
readonly redirectUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
/** A page hosted by Sofia where the person connects the account. */
|
|
21
|
+
export type ConnectionLink = {
|
|
22
|
+
readonly platform: string;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly expiresAt: string;
|
|
25
|
+
};
|
|
26
|
+
export type InsightsPeriod = 'last24Hours' | 'last7Days' | 'last28Days';
|
|
27
|
+
export type MetricReading = {
|
|
28
|
+
readonly status: 'available' | 'unavailable';
|
|
29
|
+
readonly value?: number;
|
|
30
|
+
readonly reason?: string;
|
|
31
|
+
readonly mayBeDelayed?: boolean;
|
|
32
|
+
readonly sourceMetric?: string;
|
|
33
|
+
readonly granularity?: 'lifetime' | 'hour' | 'day' | 'period' | 'video-second';
|
|
34
|
+
};
|
|
35
|
+
export type NetworkInsights = {
|
|
36
|
+
readonly platform: string;
|
|
37
|
+
readonly socialAccountId: string;
|
|
38
|
+
readonly views: MetricReading;
|
|
39
|
+
readonly reach: MetricReading;
|
|
40
|
+
readonly reachDeltaPercent: MetricReading;
|
|
41
|
+
readonly accountsEngaged: MetricReading;
|
|
42
|
+
readonly totalInteractions: MetricReading;
|
|
43
|
+
readonly likes: MetricReading;
|
|
44
|
+
readonly comments: MetricReading;
|
|
45
|
+
readonly shares: MetricReading;
|
|
46
|
+
readonly saves: MetricReading;
|
|
47
|
+
readonly engagementRatePercent: MetricReading;
|
|
48
|
+
readonly engagementDeltaPoints: MetricReading;
|
|
49
|
+
readonly followersDelta: MetricReading;
|
|
50
|
+
readonly postsPublishedDuringPeriod: MetricReading;
|
|
51
|
+
readonly followersTotal: MetricReading;
|
|
52
|
+
readonly postsTotal: MetricReading;
|
|
53
|
+
readonly reachDailySeries?: readonly number[];
|
|
54
|
+
readonly engagementDailySeries?: readonly number[];
|
|
55
|
+
readonly followersDailySeries?: readonly number[];
|
|
56
|
+
readonly hasInsightsData: boolean;
|
|
57
|
+
readonly mayBeDelayed: boolean;
|
|
58
|
+
readonly fetchedAt?: number;
|
|
59
|
+
readonly collection: {
|
|
60
|
+
readonly state: 'collected' | 'awaiting-first-collection';
|
|
61
|
+
readonly collectedAt?: number;
|
|
62
|
+
readonly isOutdated: boolean;
|
|
63
|
+
readonly isCollecting: boolean;
|
|
64
|
+
readonly nextCollectionAt?: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export type EstablishmentInsights = {
|
|
68
|
+
readonly establishmentId: string;
|
|
69
|
+
readonly period: InsightsPeriod;
|
|
70
|
+
readonly data: readonly NetworkInsights[];
|
|
71
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
2
|
+
export type QueryValue = string | number | boolean | Date | undefined;
|
|
3
|
+
export type MultipartBody = {
|
|
4
|
+
readonly kind: 'multipart';
|
|
5
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
6
|
+
readonly file: {
|
|
7
|
+
readonly field: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly blob: Blob;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type JsonBody = {
|
|
13
|
+
readonly kind: 'json';
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
};
|
|
16
|
+
/** A call to Sofia API v1: `path` is relative to `/api/v1`. */
|
|
17
|
+
export type ApiRequest = {
|
|
18
|
+
readonly method: HttpMethod;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly query?: Readonly<Record<string, QueryValue>>;
|
|
21
|
+
readonly body?: JsonBody | MultipartBody;
|
|
22
|
+
readonly idempotencyKey?: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.contentTypeOf = void 0;
|
|
4
|
+
const CONTENT_TYPE_OF_EXTENSION = {
|
|
5
|
+
jpg: 'image/jpeg',
|
|
6
|
+
jpeg: 'image/jpeg',
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
gif: 'image/gif',
|
|
9
|
+
webp: 'image/webp',
|
|
10
|
+
heic: 'image/heic',
|
|
11
|
+
mp4: 'video/mp4',
|
|
12
|
+
mov: 'video/quicktime',
|
|
13
|
+
webm: 'video/webm',
|
|
14
|
+
};
|
|
15
|
+
/** The content type a file name implies; the API reads the bytes and refuses what it does not take. */
|
|
16
|
+
const contentTypeOf = (fileName) => {
|
|
17
|
+
const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
|
|
18
|
+
return CONTENT_TYPE_OF_EXTENSION[extension] ?? 'application/octet-stream';
|
|
19
|
+
};
|
|
20
|
+
exports.contentTypeOf = contentTypeOf;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SofiaApiError } from './sofia-api-error';
|
|
2
|
+
/** `Retry-After` in seconds; an HTTP date is turned into the seconds left until it. */
|
|
3
|
+
export declare const retryAfterSecondsOf: (header: string | undefined, now: Date) => number | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Reads both error shapes of Sofia API: the `{ error: { code, message, details? } }` envelope
|
|
6
|
+
* and the RFC 6749 `{ error, error_description }` of the token endpoint. Anything else is
|
|
7
|
+
* `http_<status>`: a proxy page, an empty body.
|
|
8
|
+
*/
|
|
9
|
+
export declare const errorFromResponse: (status: number, body: unknown, retryAfterSeconds: number | undefined) => SofiaApiError;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.errorFromResponse = exports.retryAfterSecondsOf = void 0;
|
|
4
|
+
const sofia_api_error_1 = require("./sofia-api-error");
|
|
5
|
+
const asFields = (value) => typeof value === 'object' && value ? value : {};
|
|
6
|
+
const textOf = (value) => typeof value === 'string' ? value : undefined;
|
|
7
|
+
const detailsOf = (value) => Array.isArray(value)
|
|
8
|
+
? value.filter((detail) => textOf(asFields(detail)['code']) !== undefined)
|
|
9
|
+
: [];
|
|
10
|
+
/** `Retry-After` in seconds; an HTTP date is turned into the seconds left until it. */
|
|
11
|
+
const retryAfterSecondsOf = (header, now) => {
|
|
12
|
+
if (header === undefined || header.trim() === '')
|
|
13
|
+
return undefined;
|
|
14
|
+
const seconds = Number(header);
|
|
15
|
+
if (Number.isFinite(seconds))
|
|
16
|
+
return Math.max(0, Math.ceil(seconds));
|
|
17
|
+
const at = Date.parse(header);
|
|
18
|
+
return Number.isNaN(at) ? undefined : Math.max(0, Math.ceil((at - now.getTime()) / 1000));
|
|
19
|
+
};
|
|
20
|
+
exports.retryAfterSecondsOf = retryAfterSecondsOf;
|
|
21
|
+
/**
|
|
22
|
+
* Reads both error shapes of Sofia API: the `{ error: { code, message, details? } }` envelope
|
|
23
|
+
* and the RFC 6749 `{ error, error_description }` of the token endpoint. Anything else is
|
|
24
|
+
* `http_<status>`: a proxy page, an empty body.
|
|
25
|
+
*/
|
|
26
|
+
const errorFromResponse = (status, body, retryAfterSeconds) => {
|
|
27
|
+
const fields = asFields(body);
|
|
28
|
+
const envelope = asFields(fields['error']);
|
|
29
|
+
const code = textOf(envelope['code']) ?? textOf(fields['error']) ?? `http_${status}`;
|
|
30
|
+
const message = textOf(envelope['message']) ??
|
|
31
|
+
textOf(fields['error_description']) ??
|
|
32
|
+
`Sofia API answered ${status}.`;
|
|
33
|
+
return new sofia_api_error_1.SofiaApiError({
|
|
34
|
+
status,
|
|
35
|
+
code,
|
|
36
|
+
message,
|
|
37
|
+
details: detailsOf(envelope['details']),
|
|
38
|
+
docUrl: textOf(envelope['docUrl']),
|
|
39
|
+
requestId: textOf(envelope['requestId']),
|
|
40
|
+
retryAfterSeconds,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
exports.errorFromResponse = errorFromResponse;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pathOf = void 0;
|
|
4
|
+
/** `/posts/<id>`: every identifier is encoded, so an id never reaches another route. */
|
|
5
|
+
const pathOf = (...segments) => segments.map((segment) => `/${encodeURIComponent(segment)}`).join('');
|
|
6
|
+
exports.pathOf = pathOf;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One reason of a refusal: a field of a malformed request, or a network rule a post breaks. */
|
|
2
|
+
export type SofiaApiErrorDetail = {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly field?: string;
|
|
5
|
+
readonly platform?: string;
|
|
6
|
+
readonly message?: string;
|
|
7
|
+
readonly max?: number;
|
|
8
|
+
readonly actual?: number;
|
|
9
|
+
};
|
|
10
|
+
export type SofiaApiErrorInit = {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly details?: readonly SofiaApiErrorDetail[];
|
|
15
|
+
readonly docUrl?: string;
|
|
16
|
+
readonly requestId?: string;
|
|
17
|
+
readonly retryAfterSeconds?: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Every failure of a call: the error envelope of Sofia API (ADR 0612), or `network_error` with
|
|
21
|
+
* status 0 when the API could not be reached. `code` is stable, `message` is for people.
|
|
22
|
+
*/
|
|
23
|
+
export declare class SofiaApiError extends Error {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly details: readonly SofiaApiErrorDetail[];
|
|
27
|
+
/** Absent on the RFC 6749 errors of the token endpoint, and on `network_error`. */
|
|
28
|
+
readonly docUrl?: string;
|
|
29
|
+
readonly requestId?: string;
|
|
30
|
+
/** Set on 429 and 503: how long to wait before trying again. */
|
|
31
|
+
readonly retryAfterSeconds?: number;
|
|
32
|
+
constructor(init: SofiaApiErrorInit);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SofiaApiError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Every failure of a call: the error envelope of Sofia API (ADR 0612), or `network_error` with
|
|
6
|
+
* status 0 when the API could not be reached. `code` is stable, `message` is for people.
|
|
7
|
+
*/
|
|
8
|
+
class SofiaApiError extends Error {
|
|
9
|
+
constructor(init) {
|
|
10
|
+
super(init.message);
|
|
11
|
+
this.name = 'SofiaApiError';
|
|
12
|
+
this.status = init.status;
|
|
13
|
+
this.code = init.code;
|
|
14
|
+
this.details = init.details ?? [];
|
|
15
|
+
this.docUrl = init.docUrl;
|
|
16
|
+
this.requestId = init.requestId;
|
|
17
|
+
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.SofiaApiError = SofiaApiError;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ApiRequest } from './api-request';
|
|
2
|
+
export type SofiaHttpClientOptions = {
|
|
3
|
+
readonly apiKey: string;
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly userAgent: string;
|
|
6
|
+
readonly fetch: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
/** Sends one request to `<baseUrl>/api/v1`, and turns any non-2xx answer into a `SofiaApiError`. */
|
|
9
|
+
export declare class SofiaHttpClient {
|
|
10
|
+
private readonly options;
|
|
11
|
+
constructor(options: SofiaHttpClientOptions);
|
|
12
|
+
send<T>(request: ApiRequest): Promise<T>;
|
|
13
|
+
private dispatch;
|
|
14
|
+
private urlOf;
|
|
15
|
+
private headersOf;
|
|
16
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SofiaHttpClient = void 0;
|
|
4
|
+
const error_from_response_1 = require("./error-from-response");
|
|
5
|
+
const sofia_api_error_1 = require("./sofia-api-error");
|
|
6
|
+
const queryText = (value) => value instanceof Date ? value.toISOString() : String(value);
|
|
7
|
+
const bodyOf = (body) => {
|
|
8
|
+
if (body === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (body.kind === 'json')
|
|
11
|
+
return JSON.stringify(body.value);
|
|
12
|
+
const form = new FormData();
|
|
13
|
+
Object.entries(body.fields).forEach(([name, value]) => form.append(name, value));
|
|
14
|
+
form.append(body.file.field, body.file.blob, body.file.name);
|
|
15
|
+
return form;
|
|
16
|
+
};
|
|
17
|
+
const readBody = async (response) => {
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
if (text === '')
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Sends one request to `<baseUrl>/api/v1`, and turns any non-2xx answer into a `SofiaApiError`. */
|
|
29
|
+
class SofiaHttpClient {
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
async send(request) {
|
|
34
|
+
const response = await this.dispatch(request);
|
|
35
|
+
const body = await readBody(response);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const retryAfter = (0, error_from_response_1.retryAfterSecondsOf)(response.headers.get('retry-after') ?? undefined, new Date());
|
|
38
|
+
throw (0, error_from_response_1.errorFromResponse)(response.status, body, retryAfter);
|
|
39
|
+
}
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
async dispatch(request) {
|
|
43
|
+
try {
|
|
44
|
+
return await this.options.fetch(this.urlOf(request), {
|
|
45
|
+
method: request.method,
|
|
46
|
+
headers: this.headersOf(request),
|
|
47
|
+
body: bodyOf(request.body),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new sofia_api_error_1.SofiaApiError({
|
|
52
|
+
status: 0,
|
|
53
|
+
code: 'network_error',
|
|
54
|
+
message: `Could not reach ${this.options.baseUrl}: ${error instanceof Error ? error.message : String(error)}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
urlOf(request) {
|
|
59
|
+
const url = new URL(`${this.options.baseUrl}/api/v1${request.path}`);
|
|
60
|
+
Object.entries(request.query ?? {}).forEach(([name, value]) => {
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
url.searchParams.set(name, queryText(value));
|
|
63
|
+
});
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
headersOf(request) {
|
|
67
|
+
return {
|
|
68
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
'User-Agent': this.options.userAgent,
|
|
71
|
+
...(request.body?.kind === 'json' ? { 'Content-Type': 'application/json' } : {}),
|
|
72
|
+
...(request.idempotencyKey === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { 'Idempotency-Key': request.idempotencyKey }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.SofiaHttpClient = SofiaHttpClient;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type MediaType = 'image' | 'video';
|
|
2
|
+
export type Media = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly type: MediaType;
|
|
5
|
+
readonly url: string;
|
|
6
|
+
};
|
|
7
|
+
export type MediaListItem = Media & {
|
|
8
|
+
readonly fileName: string;
|
|
9
|
+
readonly contentType: string;
|
|
10
|
+
readonly size: number;
|
|
11
|
+
readonly createdAt: string;
|
|
12
|
+
readonly thumbnailUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
/** A file's bytes: a `Buffer` read from disk, any `Uint8Array`, or a `Blob`. */
|
|
15
|
+
export type UploadableFile = Uint8Array | Blob;
|
|
16
|
+
export type UploadInput = {
|
|
17
|
+
readonly establishmentId: string;
|
|
18
|
+
readonly file: UploadableFile;
|
|
19
|
+
/** Its extension gives the content type when `contentType` is absent: `photo.jpg`, `clip.mp4`. */
|
|
20
|
+
readonly fileName: string;
|
|
21
|
+
readonly contentType?: string;
|
|
22
|
+
readonly idempotencyKey?: string;
|
|
23
|
+
};
|
|
24
|
+
export type UploadFromUrlInput = {
|
|
25
|
+
readonly establishmentId: string;
|
|
26
|
+
/** A public URL Sofia downloads itself. */
|
|
27
|
+
readonly url: string;
|
|
28
|
+
readonly idempotencyKey?: string;
|
|
29
|
+
};
|
|
30
|
+
export type ListMediaFilters = {
|
|
31
|
+
readonly establishmentId: string;
|
|
32
|
+
readonly cursor?: string;
|
|
33
|
+
readonly limit?: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** The networks Sofia API v1 publishes to. */
|
|
2
|
+
export type Platform = 'instagram' | 'facebook' | 'x' | 'linkedin' | 'tiktok';
|
|
3
|
+
/** An instant, as an ISO 8601 string or a `Date` (sent as its ISO string). */
|
|
4
|
+
export type Instant = string | Date;
|
|
5
|
+
export type PostMedia = {
|
|
6
|
+
/** A media of the library, returned by `upload` or `uploadFromUrl`. */
|
|
7
|
+
readonly id?: string;
|
|
8
|
+
/** A public URL Sofia fetches itself. */
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly type?: 'image' | 'video';
|
|
11
|
+
};
|
|
12
|
+
/** One network of a post: each network gets its own text, media and format. */
|
|
13
|
+
export type PostTarget = {
|
|
14
|
+
readonly platform: Platform;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly media?: readonly PostMedia[];
|
|
17
|
+
/** A format listed by `platforms()`, such as `reel` or `story`; the network's default otherwise. */
|
|
18
|
+
readonly format?: string;
|
|
19
|
+
/** Needed only when the establishment has several accounts on this network. */
|
|
20
|
+
readonly accountId?: string;
|
|
21
|
+
};
|
|
22
|
+
export type PublishPostInput = {
|
|
23
|
+
readonly establishmentId: string;
|
|
24
|
+
readonly targets: readonly PostTarget[];
|
|
25
|
+
/** Absent: published now. In the future: scheduled. */
|
|
26
|
+
readonly scheduledAt?: Instant;
|
|
27
|
+
/** `draft`: saved, neither scheduled nor published. */
|
|
28
|
+
readonly status?: 'draft';
|
|
29
|
+
};
|
|
30
|
+
export type PublishPostOptions = {
|
|
31
|
+
/** Checks everything sending would check, writes nothing. */
|
|
32
|
+
readonly dryRun?: boolean;
|
|
33
|
+
/** Replayed with the same body, the request answers the same post instead of a second one. */
|
|
34
|
+
readonly idempotencyKey?: string;
|
|
35
|
+
};
|
|
36
|
+
export type EditPostInput = {
|
|
37
|
+
readonly targets?: readonly PostTarget[];
|
|
38
|
+
readonly scheduledAt?: Instant;
|
|
39
|
+
readonly status?: 'draft' | 'scheduled';
|
|
40
|
+
};
|
|
41
|
+
export type TargetState = 'draft' | 'scheduled' | 'publishing' | 'published' | 'sent_to_inbox' | 'failed';
|
|
42
|
+
export type LivePostTarget = {
|
|
43
|
+
readonly platform: string;
|
|
44
|
+
readonly accountId: string;
|
|
45
|
+
readonly state: TargetState;
|
|
46
|
+
readonly publishedAt?: string;
|
|
47
|
+
readonly permalink?: string;
|
|
48
|
+
readonly error?: string;
|
|
49
|
+
};
|
|
50
|
+
export type LivePost = {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
/** The synthesis of the networks' states: `partial` when one failed and another went out. */
|
|
53
|
+
readonly status: TargetState | 'partial';
|
|
54
|
+
readonly establishmentId: string;
|
|
55
|
+
readonly scheduledAt?: string;
|
|
56
|
+
readonly createdAt: string;
|
|
57
|
+
readonly targets: readonly LivePostTarget[];
|
|
58
|
+
};
|
|
59
|
+
/** What a test key answers: nothing reaches a network. */
|
|
60
|
+
export type SandboxPost = {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly status: 'simulated';
|
|
63
|
+
readonly scheduledAt?: string;
|
|
64
|
+
readonly targets: readonly {
|
|
65
|
+
readonly platform: string;
|
|
66
|
+
readonly accountId?: string;
|
|
67
|
+
readonly state: 'simulated';
|
|
68
|
+
}[];
|
|
69
|
+
};
|
|
70
|
+
export type CreatedLivePost = {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly status: 'draft' | 'publishing' | 'scheduled';
|
|
73
|
+
readonly establishmentId: string;
|
|
74
|
+
readonly scheduledAt?: string;
|
|
75
|
+
readonly targets: readonly {
|
|
76
|
+
readonly platform: string;
|
|
77
|
+
readonly accountId: string;
|
|
78
|
+
readonly state: 'draft' | 'publishing' | 'scheduled' | 'failed';
|
|
79
|
+
}[];
|
|
80
|
+
};
|
|
81
|
+
/** What a dry run answers when the post would be accepted. */
|
|
82
|
+
export type CheckedPost = {
|
|
83
|
+
readonly status: 'ready';
|
|
84
|
+
readonly dryRun: true;
|
|
85
|
+
readonly targets: readonly {
|
|
86
|
+
readonly platform: string;
|
|
87
|
+
readonly accountId?: string;
|
|
88
|
+
readonly handle?: string;
|
|
89
|
+
readonly state: 'ready';
|
|
90
|
+
}[];
|
|
91
|
+
};
|
|
92
|
+
export type Post = LivePost | SandboxPost;
|
|
93
|
+
export type PublishedPost = CreatedLivePost | SandboxPost | CheckedPost;
|
|
94
|
+
export type ListPostsFilters = {
|
|
95
|
+
readonly establishmentId?: string;
|
|
96
|
+
readonly cursor?: string;
|
|
97
|
+
readonly status?: TargetState;
|
|
98
|
+
readonly from?: Instant;
|
|
99
|
+
readonly to?: Instant;
|
|
100
|
+
/** `me`: the person the key acts for; `app`: the posts this application created. */
|
|
101
|
+
readonly createdBy?: 'me' | 'app';
|
|
102
|
+
/** 1 to 100, 20 by default. */
|
|
103
|
+
readonly limit?: number;
|
|
104
|
+
};
|
|
105
|
+
export type Page<Item> = {
|
|
106
|
+
readonly data: readonly Item[];
|
|
107
|
+
/** Absent on the last page. */
|
|
108
|
+
readonly nextCursor?: string;
|
|
109
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type WebhookEvent = 'post.published' | 'post.failed';
|
|
2
|
+
export type RegisterWebhookInput = {
|
|
3
|
+
/** An HTTPS URL that answers 2xx. */
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly events: readonly WebhookEvent[];
|
|
6
|
+
};
|
|
7
|
+
export type Webhook = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly events: readonly WebhookEvent[];
|
|
11
|
+
readonly createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
/** The secret signs every delivery; it is shown only once, here. */
|
|
14
|
+
export type CreatedWebhook = Webhook & {
|
|
15
|
+
readonly secret: string;
|
|
16
|
+
};
|
|
17
|
+
export type WebhookTest = {
|
|
18
|
+
readonly deliveryId: string;
|
|
19
|
+
};
|