Merge branch 'actor' into 'develop'
Expose local users as ActivityPub actors See merge request soapbox-pub/ditto!1
This commit is contained in:
commit
8019a8d958
|
@ -2,6 +2,7 @@ import { type Context, cors, type Handler, Hono, type HonoEnv, logger, type Midd
|
||||||
import { type Event } from '@/event.ts';
|
import { type Event } from '@/event.ts';
|
||||||
import '@/loopback.ts';
|
import '@/loopback.ts';
|
||||||
|
|
||||||
|
import { actorController } from './controllers/activitypub/actor.ts';
|
||||||
import {
|
import {
|
||||||
accountController,
|
accountController,
|
||||||
accountLookupController,
|
accountLookupController,
|
||||||
|
@ -67,6 +68,8 @@ app.get('/.well-known/host-meta', hostMetaController);
|
||||||
app.get('/.well-known/nodeinfo', nodeInfoController);
|
app.get('/.well-known/nodeinfo', nodeInfoController);
|
||||||
app.get('/.well-known/nostr.json', nostrController);
|
app.get('/.well-known/nostr.json', nostrController);
|
||||||
|
|
||||||
|
app.get('/users/:username', actorController);
|
||||||
|
|
||||||
app.get('/nodeinfo/:version', nodeInfoSchemaController);
|
app.get('/nodeinfo/:version', nodeInfoSchemaController);
|
||||||
|
|
||||||
app.get('/api/v1/instance', instanceController);
|
app.get('/api/v1/instance', instanceController);
|
||||||
|
|
|
@ -1,7 +1,32 @@
|
||||||
|
import { nip19, secp } from '@/deps.ts';
|
||||||
|
|
||||||
/** Application-wide configuration. */
|
/** Application-wide configuration. */
|
||||||
const Conf = {
|
const Conf = {
|
||||||
get nsec() {
|
get nsec() {
|
||||||
return Deno.env.get('DITTO_NSEC');
|
const value = Deno.env.get('DITTO_NSEC');
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('Missing DITTO_NSEC');
|
||||||
|
}
|
||||||
|
if (!value.startsWith('nsec1')) {
|
||||||
|
throw new Error('Invalid DITTO_NSEC');
|
||||||
|
}
|
||||||
|
return value as `nsec1${string}`;
|
||||||
|
},
|
||||||
|
get seckey() {
|
||||||
|
const result = nip19.decode(Conf.nsec);
|
||||||
|
if (result.type !== 'nsec') {
|
||||||
|
throw new Error('Invalid DITTO_NSEC');
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
get cryptoKey() {
|
||||||
|
return crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
secp.utils.hexToBytes(Conf.seckey),
|
||||||
|
{ name: 'HMAC', hash: 'SHA-256' },
|
||||||
|
false,
|
||||||
|
['sign', 'verify'],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
get relay() {
|
get relay() {
|
||||||
const value = Deno.env.get('DITTO_RELAY');
|
const value = Deno.env.get('DITTO_RELAY');
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { getAuthor } from '@/client.ts';
|
||||||
|
import { db } from '@/db.ts';
|
||||||
|
import { toActor } from '@/transformers/nostr-to-activitypub.ts';
|
||||||
|
import { activityJson } from '@/utils.ts';
|
||||||
|
|
||||||
|
import type { AppContext, AppController } from '@/app.ts';
|
||||||
|
|
||||||
|
const actorController: AppController = async (c) => {
|
||||||
|
const username = c.req.param('username');
|
||||||
|
const user = await db.users.findFirst({ where: { username } });
|
||||||
|
|
||||||
|
const event = await getAuthor(user.pubkey);
|
||||||
|
if (!event) return notFound(c);
|
||||||
|
|
||||||
|
const actor = await toActor(event);
|
||||||
|
if (!actor) return notFound(c);
|
||||||
|
|
||||||
|
return activityJson(c, actor);
|
||||||
|
};
|
||||||
|
|
||||||
|
function notFound(c: AppContext) {
|
||||||
|
return c.json({ error: 'Not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { actorController };
|
|
@ -3,7 +3,7 @@ import { type Filter, findReplyTag, z } from '@/deps.ts';
|
||||||
import { getAuthor, getFilter, getFollows, publish } from '@/client.ts';
|
import { getAuthor, getFilter, getFollows, publish } from '@/client.ts';
|
||||||
import { parseMetaContent } from '@/schema.ts';
|
import { parseMetaContent } from '@/schema.ts';
|
||||||
import { signEvent } from '@/sign.ts';
|
import { signEvent } from '@/sign.ts';
|
||||||
import { toAccount, toStatus } from '@/transmute.ts';
|
import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
import { buildLinkHeader, eventDateComparator, lookupAccount, nostrNow, paginationSchema, parseBody } from '@/utils.ts';
|
import { buildLinkHeader, eventDateComparator, lookupAccount, nostrNow, paginationSchema, parseBody } from '@/utils.ts';
|
||||||
|
|
||||||
const createAccountController: AppController = (c) => {
|
const createAccountController: AppController = (c) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { lookupAccount } from '../../utils.ts';
|
import { lookupAccount } from '@/utils.ts';
|
||||||
import { toAccount } from '../../transmute.ts';
|
import { toAccount } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
|
|
||||||
const searchController: AppController = async (c) => {
|
const searchController: AppController = async (c) => {
|
||||||
const q = c.req.query('q');
|
const q = c.req.query('q');
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { getAncestors, getDescendants, getEvent, publish } from '@/client.ts';
|
||||||
import { ISO6391, Kind, z } from '@/deps.ts';
|
import { ISO6391, Kind, z } from '@/deps.ts';
|
||||||
import { type Event } from '@/event.ts';
|
import { type Event } from '@/event.ts';
|
||||||
import { signEvent } from '@/sign.ts';
|
import { signEvent } from '@/sign.ts';
|
||||||
import { toStatus } from '@/transmute.ts';
|
import { toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
import { nostrNow, parseBody } from '@/utils.ts';
|
import { nostrNow, parseBody } from '@/utils.ts';
|
||||||
|
|
||||||
const createStatusSchema = z.object({
|
const createStatusSchema = z.object({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getFeed, getFollows, getPublicFeed } from '@/client.ts';
|
import { getFeed, getFollows, getPublicFeed } from '@/client.ts';
|
||||||
import { toStatus } from '@/transmute.ts';
|
import { toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
import { buildLinkHeader, paginationSchema } from '@/utils.ts';
|
import { buildLinkHeader, paginationSchema } from '@/utils.ts';
|
||||||
|
|
||||||
import type { AppController } from '@/app.ts';
|
import type { AppController } from '@/app.ts';
|
||||||
|
|
12
src/deps.ts
12
src/deps.ts
|
@ -21,7 +21,7 @@ export {
|
||||||
nip19,
|
nip19,
|
||||||
nip21,
|
nip21,
|
||||||
verifySignature,
|
verifySignature,
|
||||||
} from 'npm:nostr-tools@^1.11.2';
|
} from 'npm:nostr-tools@^1.12.1';
|
||||||
export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
||||||
export { parseFormData } from 'npm:formdata-helper@^0.3.0';
|
export { parseFormData } from 'npm:formdata-helper@^0.3.0';
|
||||||
// @deno-types="npm:@types/lodash@4.14.194"
|
// @deno-types="npm:@types/lodash@4.14.194"
|
||||||
|
@ -39,5 +39,15 @@ export { default as sanitizeHtml } from 'npm:sanitize-html@^2.10.0';
|
||||||
export { default as ISO6391 } from 'npm:iso-639-1@2.1.15';
|
export { default as ISO6391 } from 'npm:iso-639-1@2.1.15';
|
||||||
export { Dongoose } from 'https://raw.githubusercontent.com/alexgleason/dongoose/68b7ad9dd7b6ec0615e246a9f1603123c1709793/mod.ts';
|
export { Dongoose } from 'https://raw.githubusercontent.com/alexgleason/dongoose/68b7ad9dd7b6ec0615e246a9f1603123c1709793/mod.ts';
|
||||||
export { createPentagon } from 'https://deno.land/x/pentagon@v0.1.1/mod.ts';
|
export { createPentagon } from 'https://deno.land/x/pentagon@v0.1.1/mod.ts';
|
||||||
|
export {
|
||||||
|
type ParsedSignature,
|
||||||
|
pemToPublicKey,
|
||||||
|
publicKeyToPem,
|
||||||
|
signRequest,
|
||||||
|
verifyRequest,
|
||||||
|
} from 'https://gitlab.com/soapbox-pub/fedisign/-/raw/v0.2.1/mod.ts';
|
||||||
|
export { generateSeededRsa } from 'https://gitlab.com/soapbox-pub/seeded-rsa/-/raw/v1.0.0/mod.ts';
|
||||||
|
export * as secp from 'npm:@noble/secp256k1@^1.7.1';
|
||||||
|
export { LRUCache } from 'npm:lru-cache@^10.0.0';
|
||||||
export { DB as Sqlite } from 'https://deno.land/x/sqlite@v3.7.0/mod.ts';
|
export { DB as Sqlite } from 'https://deno.land/x/sqlite@v3.7.0/mod.ts';
|
||||||
export { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
export { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
||||||
|
|
|
@ -0,0 +1,323 @@
|
||||||
|
import { z } from '@/deps.ts';
|
||||||
|
|
||||||
|
const apId = z.string().url();
|
||||||
|
const recipients = z.array(z.string()).catch([]);
|
||||||
|
const published = () => z.string().datetime().catch(new Date().toISOString());
|
||||||
|
|
||||||
|
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||||
|
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
||||||
|
return z.any().array()
|
||||||
|
.transform((arr) => (
|
||||||
|
arr.map((item) => {
|
||||||
|
const parsed = schema.safeParse(item);
|
||||||
|
return parsed.success ? parsed.data : undefined;
|
||||||
|
}).filter((item): item is z.infer<T> => Boolean(item))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSchema = z.object({
|
||||||
|
type: z.literal('Image').catch('Image'),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachmentSchema = z.object({
|
||||||
|
type: z.literal('Document').catch('Document'),
|
||||||
|
mediaType: z.string().optional().catch(undefined),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mentionSchema = z.object({
|
||||||
|
type: z.literal('Mention'),
|
||||||
|
href: z.string().url(),
|
||||||
|
name: z.string().optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const hashtagSchema = z.object({
|
||||||
|
type: z.literal('Hashtag'),
|
||||||
|
href: z.string().url(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiSchema = z.object({
|
||||||
|
type: z.literal('Emoji'),
|
||||||
|
icon: imageSchema,
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagSchema = z.discriminatedUnion('type', [
|
||||||
|
mentionSchema,
|
||||||
|
hashtagSchema,
|
||||||
|
emojiSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const propertyValueSchema = z.object({
|
||||||
|
type: z.literal('PropertyValue'),
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
verified_at: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-fffd.md */
|
||||||
|
const proxySchema = z.object({
|
||||||
|
protocol: z.string().url(),
|
||||||
|
proxied: z.string(),
|
||||||
|
authoritative: z.boolean().optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const personSchema = z.object({
|
||||||
|
type: z.literal('Person'),
|
||||||
|
id: apId,
|
||||||
|
icon: imageSchema.optional().catch(undefined),
|
||||||
|
image: imageSchema.optional().catch(undefined),
|
||||||
|
name: z.string().catch(''),
|
||||||
|
preferredUsername: z.string(),
|
||||||
|
inbox: apId,
|
||||||
|
followers: apId.optional().catch(undefined),
|
||||||
|
following: apId.optional().catch(undefined),
|
||||||
|
outbox: apId.optional().catch(undefined),
|
||||||
|
summary: z.string().catch(''),
|
||||||
|
attachment: filteredArray(propertyValueSchema).catch([]),
|
||||||
|
tag: filteredArray(emojiSchema).catch([]),
|
||||||
|
endpoints: z.object({
|
||||||
|
sharedInbox: apId.optional(),
|
||||||
|
}).optional().catch({}),
|
||||||
|
publicKey: z.object({
|
||||||
|
id: apId,
|
||||||
|
owner: apId,
|
||||||
|
publicKeyPem: z.string(),
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const applicationSchema = personSchema.merge(z.object({ type: z.literal('Application') }));
|
||||||
|
const groupSchema = personSchema.merge(z.object({ type: z.literal('Group') }));
|
||||||
|
const organizationSchema = personSchema.merge(z.object({ type: z.literal('Organization') }));
|
||||||
|
const serviceSchema = personSchema.merge(z.object({ type: z.literal('Service') }));
|
||||||
|
|
||||||
|
const actorSchema = z.discriminatedUnion('type', [
|
||||||
|
personSchema,
|
||||||
|
applicationSchema,
|
||||||
|
groupSchema,
|
||||||
|
organizationSchema,
|
||||||
|
serviceSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const noteSchema = z.object({
|
||||||
|
type: z.literal('Note'),
|
||||||
|
id: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
content: z.string(),
|
||||||
|
attachment: z.array(attachmentSchema).optional().catch(undefined),
|
||||||
|
tag: filteredArray(tagSchema).catch([]),
|
||||||
|
inReplyTo: apId.optional().catch(undefined),
|
||||||
|
attributedTo: apId,
|
||||||
|
published: published(),
|
||||||
|
sensitive: z.boolean().optional().catch(undefined),
|
||||||
|
summary: z.string().nullish().catch(undefined),
|
||||||
|
quoteUrl: apId.optional().catch(undefined),
|
||||||
|
source: z.object({
|
||||||
|
content: z.string(),
|
||||||
|
mediaType: z.literal('text/markdown'),
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const flexibleNoteSchema = noteSchema.extend({
|
||||||
|
quoteURL: apId.optional().catch(undefined),
|
||||||
|
quoteUri: apId.optional().catch(undefined),
|
||||||
|
_misskey_quote: apId.optional().catch(undefined),
|
||||||
|
}).transform((note) => {
|
||||||
|
const { quoteUrl, quoteUri, quoteURL, _misskey_quote, ...rest } = note;
|
||||||
|
return {
|
||||||
|
quoteUrl: quoteUrl || quoteUri || quoteURL || _misskey_quote,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/colinhacks/zod/discussions/2100#discussioncomment-5109781
|
||||||
|
const objectSchema = z.union([
|
||||||
|
flexibleNoteSchema,
|
||||||
|
personSchema,
|
||||||
|
applicationSchema,
|
||||||
|
groupSchema,
|
||||||
|
organizationSchema,
|
||||||
|
serviceSchema,
|
||||||
|
]).pipe(
|
||||||
|
z.discriminatedUnion('type', [
|
||||||
|
noteSchema,
|
||||||
|
personSchema,
|
||||||
|
applicationSchema,
|
||||||
|
groupSchema,
|
||||||
|
organizationSchema,
|
||||||
|
serviceSchema,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const createNoteSchema = z.object({
|
||||||
|
type: z.literal('Create'),
|
||||||
|
id: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
actor: apId,
|
||||||
|
object: noteSchema,
|
||||||
|
published: published(),
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const announceNoteSchema = z.object({
|
||||||
|
type: z.literal('Announce'),
|
||||||
|
id: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
actor: apId,
|
||||||
|
object: apId.or(noteSchema),
|
||||||
|
published: published(),
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const followSchema = z.object({
|
||||||
|
type: z.literal('Follow'),
|
||||||
|
id: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
actor: apId,
|
||||||
|
object: apId,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptSchema = z.object({
|
||||||
|
type: z.literal('Accept'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
object: apId.or(followSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const likeSchema = z.object({
|
||||||
|
type: z.literal('Like'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
object: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiReactSchema = z.object({
|
||||||
|
type: z.literal('EmojiReact'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
object: apId,
|
||||||
|
content: z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)),
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteSchema = z.object({
|
||||||
|
type: z.literal('Delete'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
object: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateActorSchema = z.object({
|
||||||
|
type: z.literal('Update'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
object: actorSchema,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom Zap activity type we made up, based on:
|
||||||
|
* https://github.com/nostr-protocol/nips/blob/master/57.md
|
||||||
|
*/
|
||||||
|
const zapSchema = z.object({
|
||||||
|
type: z.literal('Zap'),
|
||||||
|
id: apId,
|
||||||
|
actor: apId,
|
||||||
|
object: apId,
|
||||||
|
to: recipients,
|
||||||
|
cc: recipients,
|
||||||
|
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const activitySchema = z.discriminatedUnion('type', [
|
||||||
|
followSchema,
|
||||||
|
acceptSchema,
|
||||||
|
createNoteSchema,
|
||||||
|
announceNoteSchema,
|
||||||
|
updateActorSchema,
|
||||||
|
likeSchema,
|
||||||
|
emojiReactSchema,
|
||||||
|
deleteSchema,
|
||||||
|
zapSchema,
|
||||||
|
]).refine((activity) => {
|
||||||
|
const ids: string[] = [activity.id];
|
||||||
|
|
||||||
|
if (activity.type === 'Create') {
|
||||||
|
ids.push(
|
||||||
|
activity.object.id,
|
||||||
|
activity.object.attributedTo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity.type === 'Update') {
|
||||||
|
ids.push(activity.object.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { origin: actorOrigin } = new URL(activity.actor);
|
||||||
|
|
||||||
|
// Object containment
|
||||||
|
return ids.every((id) => {
|
||||||
|
const { origin: idOrigin } = new URL(id);
|
||||||
|
return idOrigin === actorOrigin;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type Activity = z.infer<typeof activitySchema>;
|
||||||
|
type CreateNote = z.infer<typeof createNoteSchema>;
|
||||||
|
type Announce = z.infer<typeof announceNoteSchema>;
|
||||||
|
type Update = z.infer<typeof updateActorSchema>;
|
||||||
|
type Object = z.infer<typeof objectSchema>;
|
||||||
|
type Follow = z.infer<typeof followSchema>;
|
||||||
|
type Accept = z.infer<typeof acceptSchema>;
|
||||||
|
type Actor = z.infer<typeof actorSchema>;
|
||||||
|
type Note = z.infer<typeof noteSchema>;
|
||||||
|
type Mention = z.infer<typeof mentionSchema>;
|
||||||
|
type Hashtag = z.infer<typeof hashtagSchema>;
|
||||||
|
type Emoji = z.infer<typeof emojiSchema>;
|
||||||
|
type Like = z.infer<typeof likeSchema>;
|
||||||
|
type EmojiReact = z.infer<typeof emojiReactSchema>;
|
||||||
|
type Delete = z.infer<typeof deleteSchema>;
|
||||||
|
type Zap = z.infer<typeof zapSchema>;
|
||||||
|
type Proxy = z.infer<typeof proxySchema>;
|
||||||
|
|
||||||
|
export { acceptSchema, activitySchema, actorSchema, emojiSchema, followSchema, imageSchema, noteSchema, objectSchema };
|
||||||
|
export type {
|
||||||
|
Accept,
|
||||||
|
Activity,
|
||||||
|
Actor,
|
||||||
|
Announce,
|
||||||
|
CreateNote,
|
||||||
|
Delete,
|
||||||
|
Emoji,
|
||||||
|
EmojiReact,
|
||||||
|
Follow,
|
||||||
|
Hashtag,
|
||||||
|
Like,
|
||||||
|
Mention,
|
||||||
|
Note,
|
||||||
|
Object,
|
||||||
|
Proxy,
|
||||||
|
Update,
|
||||||
|
Zap,
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { parseMetaContent } from '@/schema.ts';
|
||||||
|
import { getPublicKeyPem } from '@/utils/rsa.ts';
|
||||||
|
|
||||||
|
import type { Event } from '@/event.ts';
|
||||||
|
import type { Actor } from '@/schemas/activitypub.ts';
|
||||||
|
|
||||||
|
/** Nostr metadata event to ActivityPub actor. */
|
||||||
|
async function toActor(event: Event<0>): Promise<Actor | undefined> {
|
||||||
|
const content = parseMetaContent(event);
|
||||||
|
|
||||||
|
if (!content.nip05) return;
|
||||||
|
const [username, hostname] = content.nip05.split('@');
|
||||||
|
if (hostname !== Conf.url.hostname) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Person',
|
||||||
|
id: Conf.local(`/users/${username}`),
|
||||||
|
name: content?.name || '',
|
||||||
|
preferredUsername: username,
|
||||||
|
inbox: Conf.local(`/users/${username}/inbox`),
|
||||||
|
followers: Conf.local(`/users/${username}/followers`),
|
||||||
|
following: Conf.local(`/users/${username}/following`),
|
||||||
|
outbox: Conf.local(`/users/${username}/outbox`),
|
||||||
|
icon: content.picture
|
||||||
|
? {
|
||||||
|
type: 'Image',
|
||||||
|
url: content.picture,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
image: content.banner
|
||||||
|
? {
|
||||||
|
type: 'Image',
|
||||||
|
url: content.banner,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
summary: content.about ?? '',
|
||||||
|
attachment: [],
|
||||||
|
tag: [],
|
||||||
|
publicKey: {
|
||||||
|
id: Conf.local(`/users/${username}#main-key`),
|
||||||
|
owner: Conf.local(`/users/${username}`),
|
||||||
|
publicKeyPem: await getPublicKeyPem(event.pubkey),
|
||||||
|
},
|
||||||
|
endpoints: {
|
||||||
|
sharedInbox: Conf.local('/inbox'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toActor };
|
|
@ -1,13 +1,13 @@
|
||||||
|
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
||||||
|
|
||||||
|
import { getAuthor } from '@/client.ts';
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
import { findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, z } from '@/deps.ts';
|
import { findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, z } from '@/deps.ts';
|
||||||
import { type Event } from '@/event.ts';
|
import { type Event } from '@/event.ts';
|
||||||
|
import { verifyNip05Cached } from '@/nip05.ts';
|
||||||
|
import { getMediaLinks, type MediaLink, parseNoteContent } from '@/note.ts';
|
||||||
import { emojiTagSchema, filteredArray, type MetaContent, parseMetaContent } from '@/schema.ts';
|
import { emojiTagSchema, filteredArray, type MetaContent, parseMetaContent } from '@/schema.ts';
|
||||||
|
import { type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts';
|
||||||
import { Conf } from './config.ts';
|
|
||||||
import { getAuthor } from './client.ts';
|
|
||||||
import { verifyNip05Cached } from './nip05.ts';
|
|
||||||
import { getMediaLinks, type MediaLink, parseNoteContent } from './note.ts';
|
|
||||||
import { type Nip05, nostrDate, parseNip05, Time } from './utils.ts';
|
|
||||||
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
|
||||||
|
|
||||||
const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png';
|
const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png';
|
||||||
const DEFAULT_BANNER = 'https://gleasonator.com/images/banner.png';
|
const DEFAULT_BANNER = 'https://gleasonator.com/images/banner.png';
|
21
src/utils.ts
21
src/utils.ts
|
@ -1,6 +1,6 @@
|
||||||
import { getAuthor } from '@/client.ts';
|
import { getAuthor } from '@/client.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { nip19, parseFormData, z } from '@/deps.ts';
|
import { type Context, nip19, parseFormData, z } from '@/deps.ts';
|
||||||
import { type Event } from '@/event.ts';
|
import { type Event } from '@/event.ts';
|
||||||
import { lookupNip05Cached } from '@/nip05.ts';
|
import { lookupNip05Cached } from '@/nip05.ts';
|
||||||
|
|
||||||
|
@ -124,7 +124,26 @@ async function sha256(message: string): Promise<string> {
|
||||||
return hashHex;
|
return hashHex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** JSON-LD context. */
|
||||||
|
type LDContext = (string | Record<string, string | Record<string, string>>)[];
|
||||||
|
|
||||||
|
/** Add a basic JSON-LD context to ActivityStreams object, if it doesn't already exist. */
|
||||||
|
function maybeAddContext<T>(object: T): T & { '@context': LDContext } {
|
||||||
|
return {
|
||||||
|
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||||
|
...object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like hono's `c.json()` except returns JSON-LD. */
|
||||||
|
function activityJson<T, P extends string>(c: Context<any, P>, object: T) {
|
||||||
|
const response = c.json(maybeAddContext(object));
|
||||||
|
response.headers.set('content-type', 'application/activity+json; charset=UTF-8');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
activityJson,
|
||||||
bech32ToPubkey,
|
bech32ToPubkey,
|
||||||
buildLinkHeader,
|
buildLinkHeader,
|
||||||
eventAge,
|
eventAge,
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { generateSeededRsa, LRUCache, publicKeyToPem, secp } from '@/deps.ts';
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
bits: 2048,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rsaCache = new LRUCache<string, Promise<string>>({ max: 1000 });
|
||||||
|
|
||||||
|
async function buildSeed(pubkey: string): Promise<string> {
|
||||||
|
const key = await Conf.cryptoKey;
|
||||||
|
const data = new TextEncoder().encode(pubkey);
|
||||||
|
const signature = await window.crypto.subtle.sign('HMAC', key, data);
|
||||||
|
return secp.utils.bytesToHex(new Uint8Array(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicKeyPem(pubkey: string): Promise<string> {
|
||||||
|
const cached = await rsaCache.get(pubkey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const seed = await buildSeed(pubkey);
|
||||||
|
const { publicKey } = await generateSeededRsa(seed, opts);
|
||||||
|
const promise = publicKeyToPem(publicKey);
|
||||||
|
|
||||||
|
rsaCache.set(pubkey, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getPublicKeyPem };
|
Loading…
Reference in New Issue