fedipod 1.0.0 → 1.3.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 +4 -4
- package/bin/fedipod.mjs +45 -2253
- package/gateway.md +1 -1
- package/lib/{c2s.mjs → client/c2s.mjs} +10 -3
- package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
- package/lib/client/masto/accounts.mjs +264 -0
- package/lib/client/masto/body.mjs +69 -0
- package/lib/client/masto/index.mjs +183 -0
- package/lib/client/masto/instance.mjs +104 -0
- package/lib/client/masto/media.mjs +133 -0
- package/lib/client/masto/oauth.mjs +599 -0
- package/lib/client/masto/render.mjs +459 -0
- package/lib/client/masto/statuses.mjs +331 -0
- package/lib/client/masto/timelines.mjs +316 -0
- package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
- package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
- package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
- package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
- package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
- package/lib/{import.mjs → connections/import.mjs} +1 -1
- package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
- package/lib/connections/vault.mjs +114 -0
- package/lib/core/as2.mjs +170 -0
- package/lib/core/contexts/activitystreams.json +379 -0
- package/lib/core/contexts/did-v1.json +57 -0
- package/lib/core/contexts/fep-5711.json +36 -0
- package/lib/core/contexts/gotosocial.json +86 -0
- package/lib/core/contexts/identity-v1.json +152 -0
- package/lib/core/contexts/index.mjs +45 -0
- package/lib/core/contexts/join-lemmy.json +33 -0
- package/lib/core/contexts/joinmastodon.json +28 -0
- package/lib/core/contexts/map.json +16 -0
- package/lib/core/contexts/miscellany.json +19 -0
- package/lib/core/contexts/schemaorg.json +8845 -0
- package/lib/core/contexts/security-data-integrity-v1.json +78 -0
- package/lib/core/contexts/security-data-integrity-v2.json +81 -0
- package/lib/core/contexts/security-multikey-v1.json +35 -0
- package/lib/core/contexts/security-v1.json +74 -0
- package/lib/core/contexts/webfinger.json +10 -0
- package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
- package/lib/core/graphview.mjs +269 -0
- package/lib/core/intake/activities.mjs +437 -0
- package/lib/core/intake/activity.mjs +240 -0
- package/lib/core/intake/channel.mjs +144 -0
- package/lib/core/intake/group.mjs +222 -0
- package/lib/core/intake/index.mjs +629 -0
- package/lib/core/intake/notes.mjs +288 -0
- package/lib/core/intake/verify.mjs +142 -0
- package/lib/{keys.mjs → core/keys.mjs} +1 -1
- package/lib/core/publisher/collections.mjs +229 -0
- package/lib/core/publisher/index.mjs +421 -0
- package/lib/core/publisher/notes.mjs +188 -0
- package/lib/core/publisher/questions.mjs +233 -0
- package/lib/core/publisher/restore.mjs +199 -0
- package/lib/core/shapes/activitystreams.ttl +129 -0
- package/lib/core/shapes/index.mjs +107 -0
- package/lib/core/shapes/shapes-text.mjs +13 -0
- package/lib/{social.mjs → core/social.mjs} +2 -2
- package/lib/{store.mjs → core/store.mjs} +4 -0
- package/lib/{wire.mjs → core/wire.mjs} +2 -2
- package/lib/device/admin/index.mjs +13 -0
- package/lib/device/admin/origins.mjs +35 -0
- package/lib/device/admin/routes/connections.mjs +144 -0
- package/lib/device/admin/routes/gateway.mjs +199 -0
- package/lib/device/admin/routes/lifecycle.mjs +191 -0
- package/lib/device/admin/routes/owner.mjs +322 -0
- package/lib/device/admin/routes/setup.mjs +393 -0
- package/lib/device/admin/routes/social.mjs +188 -0
- package/lib/device/admin/server.mjs +95 -0
- package/lib/device/admin/static.mjs +244 -0
- package/lib/device/admin/surface.mjs +274 -0
- package/lib/device/cli/commands/account.mjs +586 -0
- package/lib/device/cli/commands/run.mjs +278 -0
- package/lib/device/cli/commands/service.mjs +221 -0
- package/lib/device/cli/commands/setup.mjs +410 -0
- package/lib/device/cli/commands/state.mjs +559 -0
- package/lib/device/cli/context.mjs +288 -0
- package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
- package/lib/{remote.mjs → device/remote.mjs} +3 -3
- package/lib/{setup.mjs → device/setup.mjs} +3 -3
- package/lib/{update.mjs → device/update.mjs} +1 -1
- package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
- package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
- package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
- package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
- package/lib/server/embed.mjs +405 -0
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +11 -1
- package/run-agent.mjs +33 -25
- package/scripts/build-app.mjs +4 -0
- package/scripts/check-pod-calls.mjs +8 -2
- package/scripts/refresh-contexts.mjs +39 -0
- package/web/admin/actors.js +145 -0
- package/web/admin/common.js +23 -0
- package/web/admin/connections.js +112 -0
- package/web/admin/gateway.js +111 -0
- package/web/admin/group.js +258 -0
- package/web/admin/index.html +7 -1
- package/web/admin/record.js +378 -0
- package/web/admin/setup/index.html +1 -0
- package/web/admin/setup/setup.js +2 -13
- package/web/admin/upkeep.js +170 -0
- package/web/app/README.md +6 -6
- package/web/app/admin-facade.mjs +3 -3
- package/web/app/agent.mjs +14 -16
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/boot.mjs +2 -3
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/boot.js +22 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +21913 -5446
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/keys-browser.mjs +27 -4
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/signup.mjs +2 -3
- package/web/app/site/admin/actors.js +145 -0
- package/web/app/site/admin/common.js +23 -0
- package/web/app/site/admin/connections.js +112 -0
- package/web/app/site/admin/gateway.js +111 -0
- package/web/app/site/admin/group.js +258 -0
- package/web/app/site/admin/index.html +7 -1
- package/web/app/site/admin/record.js +378 -0
- package/web/app/site/admin/setup/index.html +1 -0
- package/web/app/site/admin/setup/setup.js +2 -13
- package/web/app/site/admin/upkeep.js +170 -0
- package/web/app/site/boot.js +22 -3
- package/web/app/site/sw.js +21913 -5446
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/embed.mjs +0 -220
- package/lib/intake.mjs +0 -1981
- package/lib/mastoapi.mjs +0 -2284
- package/lib/publisher.mjs +0 -1192
- package/web/admin/admin.js +0 -1181
- package/web/app/site/admin/admin.js +0 -1181
- /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
- /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
- /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
- /package/lib/{lease.mjs → core/lease.mjs} +0 -0
- /package/lib/{polls.mjs → core/polls.mjs} +0 -0
- /package/lib/{proof.mjs → core/proof.mjs} +0 -0
- /package/lib/{storage.mjs → core/storage.mjs} +0 -0
- /package/lib/{account.mjs → device/account.mjs} +0 -0
- /package/lib/{certs.mjs → device/certs.mjs} +0 -0
- /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
- /package/lib/{home.mjs → device/home.mjs} +0 -0
- /package/lib/{ports.mjs → device/ports.mjs} +0 -0
- /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
- /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// instance.mjs — what the instance says about itself: the v1 and v2 instance
|
|
2
|
+
// documents, the limits a client reads from them, the title and blurb, the
|
|
3
|
+
// tag object, and the empty-collection stubs for endpoints a single-actor
|
|
4
|
+
// instance has nothing to say to.
|
|
5
|
+
|
|
6
|
+
import { TRANSPARENT_PNG } from './render.mjs';
|
|
7
|
+
import {
|
|
8
|
+
MAX_OPTIONS as POLL_MAX_OPTIONS, MAX_OPTION_CHARS as POLL_MAX_OPTION_CHARS,
|
|
9
|
+
MIN_SECONDS as POLL_MIN_SECONDS, MAX_SECONDS as POLL_MAX_SECONDS,
|
|
10
|
+
} from '../../core/polls.mjs';
|
|
11
|
+
|
|
12
|
+
const STUBS = new Map(Object.entries({
|
|
13
|
+
'/api/v1/filters': [],
|
|
14
|
+
'/api/v1/custom_emojis': [],
|
|
15
|
+
'/api/v1/announcements': [],
|
|
16
|
+
'/api/v1/instance/peers': [],
|
|
17
|
+
'/api/v1/trends/tags': [], '/api/v1/trends/links': [],
|
|
18
|
+
'/api/v2/suggestions': [],
|
|
19
|
+
'/api/v1/preferences': {},
|
|
20
|
+
})); // followed_tags is served live from the tag feed, not stubbed
|
|
21
|
+
|
|
22
|
+
export function instanceConfig() {
|
|
23
|
+
return {
|
|
24
|
+
statuses: { max_characters: 5000, max_media_attachments: 4, characters_reserved_per_url: 23 },
|
|
25
|
+
media_attachments: {
|
|
26
|
+
supported_mime_types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
27
|
+
'video/mp4', 'video/webm', 'audio/mpeg', 'audio/ogg'],
|
|
28
|
+
image_size_limit: 10 * 1024 * 1024, video_size_limit: 40 * 1024 * 1024,
|
|
29
|
+
image_matrix_limit: 16777216, video_matrix_limit: 2304000,
|
|
30
|
+
},
|
|
31
|
+
polls: {
|
|
32
|
+
max_options: POLL_MAX_OPTIONS,
|
|
33
|
+
max_characters_per_option: POLL_MAX_OPTION_CHARS,
|
|
34
|
+
min_expiration: POLL_MIN_SECONDS,
|
|
35
|
+
max_expiration: POLL_MAX_SECONDS,
|
|
36
|
+
},
|
|
37
|
+
accounts: { max_featured_tags: 0 },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Every agent used to report title 'solid-activitypub', so a client holding two
|
|
42
|
+
// of them showed two identical instances and you had to read the acct to tell
|
|
43
|
+
// them apart. The title is free text no client parses — make it say who.
|
|
44
|
+
export function instanceTitle(api) {
|
|
45
|
+
const cfg = api.store.getConfig();
|
|
46
|
+
return cfg?.handle ? `@${cfg.handle}@${api.host}` : 'FediPod';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A Mastodon Tag object. The client reads `following` in its Followed
|
|
50
|
+
// Hashtags view and toggles it with the follow/unfollow endpoints. No usage
|
|
51
|
+
// history — a single-actor instance has no firehose stats to report.
|
|
52
|
+
export function tagObject(api, name, following, req) {
|
|
53
|
+
const host = req?.headers?.host || api.host;
|
|
54
|
+
return { name, url: `https://${host}/tags/${name}`, history: [], following: !!following };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function instanceBlurb(api) {
|
|
58
|
+
const kind = api.store.getConfig()?.kind === 'group' ? 'group' : 'actor';
|
|
59
|
+
return `Solid pod ActivityPub ${kind}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function handle(api, ctx) {
|
|
63
|
+
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
64
|
+
|
|
65
|
+
// --- instance (public) ---
|
|
66
|
+
if (pathname === '/api/v1/instance') {
|
|
67
|
+
const su = api.streamingUrl(req);
|
|
68
|
+
return send(200, {
|
|
69
|
+
uri: api.host, title: api.instanceTitle(), short_description: api.instanceBlurb(),
|
|
70
|
+
description: api.instanceBlurb(), email: '', version: '4.2.0 (compatible; fedipod)',
|
|
71
|
+
urls: su ? { streaming_api: su } : {},
|
|
72
|
+
stats: { user_count: 1, status_count: api.store.countStatuses(), domain_count: 1 },
|
|
73
|
+
languages: ['en'], registrations: false, approval_required: false, invites_enabled: false,
|
|
74
|
+
configuration: instanceConfig(),
|
|
75
|
+
contact_account: null, rules: [],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (pathname === '/api/v2/instance') {
|
|
79
|
+
const su = api.streamingUrl(req);
|
|
80
|
+
return send(200, {
|
|
81
|
+
domain: api.host, title: api.instanceTitle(), version: '4.2.0 (compatible; fedipod)',
|
|
82
|
+
source_url: 'https://github.com/jeff-zucker/FediPod', description: api.instanceBlurb(),
|
|
83
|
+
usage: { users: { active_month: 1 } },
|
|
84
|
+
thumbnail: { url: TRANSPARENT_PNG },
|
|
85
|
+
languages: ['en'],
|
|
86
|
+
// Both spellings: v2 clients read configuration.urls.streaming, older
|
|
87
|
+
// ones the top-level urls.streaming_api, and some fall back blindly.
|
|
88
|
+
// Omitted entirely when there is no streaming, so the client polls.
|
|
89
|
+
urls: su ? { streaming_api: su } : {},
|
|
90
|
+
configuration: {
|
|
91
|
+
...instanceConfig(),
|
|
92
|
+
...(su ? { urls: { streaming: su } } : {}),
|
|
93
|
+
...(api.webPush ? { vapid: { public_key: api.push.publicKey() } } : {}),
|
|
94
|
+
},
|
|
95
|
+
registrations: { enabled: false, approval_required: false, message: null },
|
|
96
|
+
contact: { email: '', account: null }, rules: [],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stub = STUBS.get(pathname);
|
|
101
|
+
if (stub !== undefined && req.method === 'GET') return send(200, stub);
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// media.mjs — an upload on its way to the pod's media container: what an
|
|
2
|
+
// attachment is allowed to be, the multipart reader, and the two media
|
|
3
|
+
// endpoints.
|
|
4
|
+
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import * as podMedia from '../../pod/media.mjs';
|
|
7
|
+
import { readBody } from './body.mjs';
|
|
8
|
+
|
|
9
|
+
// What an attachment is allowed to BE. Anything else is stored as bytes, which
|
|
10
|
+
// a browser downloads rather than runs.
|
|
11
|
+
const ATTACHMENT_KINDS = new Set(['image', 'video', 'audio']);
|
|
12
|
+
// image/* with an exception: an SVG is a document, it carries script, and a
|
|
13
|
+
// browser renders it rather than showing it. Mastodon does not take them as
|
|
14
|
+
// media either.
|
|
15
|
+
const NEVER = new Set(['image/svg+xml', 'image/svg']);
|
|
16
|
+
const OPAQUE = 'application/octet-stream';
|
|
17
|
+
|
|
18
|
+
export function attachmentType(claimed) {
|
|
19
|
+
const t = String(claimed || '').split(';')[0].trim().toLowerCase();
|
|
20
|
+
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(t) || NEVER.has(t)) return OPAQUE;
|
|
21
|
+
return ATTACHMENT_KINDS.has(t.split('/')[0]) ? t : OPAQUE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// From the TYPE we accepted, not from the name the client sent — an `.html`
|
|
25
|
+
// suffix on a file stored as octet-stream is what a pod would serve from, and
|
|
26
|
+
// a filename is the client's to choose.
|
|
27
|
+
export function extensionFor(mediaType, filename = '') {
|
|
28
|
+
if (mediaType === OPAQUE) return 'bin';
|
|
29
|
+
const sub = mediaType.split('/')[1].replace(/[^a-z0-9]/g, '');
|
|
30
|
+
const given = String(filename || '').includes('.')
|
|
31
|
+
? filename.split('.').pop().toLowerCase().replace(/[^a-z0-9]/g, '') : '';
|
|
32
|
+
// Keep the client's suffix only when it plainly belongs to the accepted type,
|
|
33
|
+
// so jpg/jpeg and mp4/m4v survive without a table of every media type.
|
|
34
|
+
return given && (given === sub || sub.startsWith(given) || given.startsWith(sub)) ? given : (sub || 'bin');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Minimal multipart/form-data reader for media uploads: string fields plus
|
|
38
|
+
// at most one file part (Mastodon's media endpoints send exactly one).
|
|
39
|
+
export function readMultipart(req, limit = 12e6) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let n = 0;
|
|
43
|
+
req.on('data', c => {
|
|
44
|
+
n += c.length;
|
|
45
|
+
if (n > limit) { reject(new Error('upload too large')); req.destroy(); return; }
|
|
46
|
+
chunks.push(c);
|
|
47
|
+
});
|
|
48
|
+
req.on('error', reject);
|
|
49
|
+
req.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers['content-type'] || ''));
|
|
52
|
+
if (!m) return resolve({ fields: {}, file: null });
|
|
53
|
+
const buf = Buffer.concat(chunks);
|
|
54
|
+
const boundary = Buffer.from('--' + (m[1] || m[2]).trim());
|
|
55
|
+
const fields = {};
|
|
56
|
+
// `file` is the last one seen, which is what the single-file media
|
|
57
|
+
// upload wants. `files` keys them by field name, because the profile
|
|
58
|
+
// editor submits an avatar and a header in one request.
|
|
59
|
+
const files = {};
|
|
60
|
+
let file = null;
|
|
61
|
+
let i = buf.indexOf(boundary);
|
|
62
|
+
while (i >= 0) {
|
|
63
|
+
const start = i + boundary.length;
|
|
64
|
+
if (buf.slice(start, start + 2).toString() === '--') break;
|
|
65
|
+
const next = buf.indexOf(boundary, start);
|
|
66
|
+
if (next < 0) break;
|
|
67
|
+
const part = buf.slice(start + 2, next - 2); // strip the CRLFs framing the part
|
|
68
|
+
const sep = part.indexOf('\r\n\r\n');
|
|
69
|
+
if (sep >= 0) {
|
|
70
|
+
const head = part.slice(0, sep).toString();
|
|
71
|
+
const body = part.slice(sep + 4);
|
|
72
|
+
const name = /name="([^"]*)"/.exec(head)?.[1];
|
|
73
|
+
const filename = /filename="([^"]*)"/.exec(head)?.[1];
|
|
74
|
+
if (filename !== undefined) {
|
|
75
|
+
file = {
|
|
76
|
+
filename,
|
|
77
|
+
contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || 'application/octet-stream',
|
|
78
|
+
data: body,
|
|
79
|
+
};
|
|
80
|
+
if (name) files[name] = file;
|
|
81
|
+
} else if (name) fields[name] = body.toString();
|
|
82
|
+
}
|
|
83
|
+
i = next;
|
|
84
|
+
}
|
|
85
|
+
resolve({ fields, file, files });
|
|
86
|
+
} catch (e) { reject(e); }
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function handle(api, ctx) {
|
|
92
|
+
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
93
|
+
|
|
94
|
+
// Media upload: file → remote pod /ap/media/ (public-Read), entry in the
|
|
95
|
+
// media registry so a later POST /statuses can attach it.
|
|
96
|
+
if ((pathname === '/api/v2/media' || pathname === '/api/v1/media') && req.method === 'POST') {
|
|
97
|
+
const { fields, file } = await readMultipart(req);
|
|
98
|
+
if (!file?.data?.length) return send(422, { error: 'file required' });
|
|
99
|
+
// The client said what this is, and we used to believe it. The media
|
|
100
|
+
// container is world-readable and sits on the pod's own origin — the same
|
|
101
|
+
// origin as the WebID and the ACLs — so a file stored as text/html is a
|
|
102
|
+
// page served from your identity, and script in it runs as you in any
|
|
103
|
+
// browser already logged into that pod. A bearer is not "only you": the
|
|
104
|
+
// facade exists so third-party clients can connect, tokens last 90 days,
|
|
105
|
+
// and no scope is enforced. So any client you authorize could leave that
|
|
106
|
+
// page behind.
|
|
107
|
+
const mediaType = attachmentType(file.contentType);
|
|
108
|
+
const ext = extensionFor(mediaType, file.filename);
|
|
109
|
+
const slug = new Date().toISOString().slice(0, 10) + '-' + crypto.randomBytes(4).toString('hex') + '.' + ext;
|
|
110
|
+
const mediaUrl = api.urls.media + slug;
|
|
111
|
+
await api.agent.publisher.ensureMediaContainer();
|
|
112
|
+
await podMedia.write(api.agent.remote, mediaUrl, file.data, mediaType);
|
|
113
|
+
const entry = { url: mediaUrl, mediaType, description: fields.description || '' };
|
|
114
|
+
const id = api.store.idFor(mediaUrl);
|
|
115
|
+
api.store.setMedia(id, entry);
|
|
116
|
+
return send(200, api.mediaJson({ id, ...entry }));
|
|
117
|
+
}
|
|
118
|
+
const mMedia = /^\/api\/v1\/media\/([a-f0-9]+)$/.exec(pathname);
|
|
119
|
+
if (mMedia) {
|
|
120
|
+
const entry = api.store.getMedia()[mMedia[1]];
|
|
121
|
+
if (!entry) return send(404, { error: 'Record not found' });
|
|
122
|
+
if (req.method === 'PUT') {
|
|
123
|
+
const body = await readBody(req);
|
|
124
|
+
if (typeof body.description === 'string') {
|
|
125
|
+
entry.description = body.description;
|
|
126
|
+
api.store.setMedia(mMedia[1], entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return send(200, api.mediaJson({ id: mMedia[1], ...entry }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|