mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-22 02:12:40 +01:00
ap guild endpoint
fetching invites from remote instances
This commit is contained in:
parent
29f1c36527
commit
904618e0a7
@ -118,6 +118,7 @@ Is a [Collection](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collecti
|
||||
Base url: `/federation`
|
||||
|
||||
- `/.well-known/webfinger?resource=acct@domain` - Returns webfinger response i.e. https://docs.joinmastodon.org/spec/webfinger/
|
||||
- - Webfinger resources include users, channels, guilds, as well as invite codes which returns a the corresponding guild
|
||||
- `/.well-known/host-meta` - Returns location of webfinger? Why is this neccessary?
|
||||
|
||||
- `/channels/:channel_id` - Returns specified Channel as AP object ( Group )
|
||||
|
125
src/activitypub/federation/HttpSig.ts
Normal file
125
src/activitypub/federation/HttpSig.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { Config, FederationKey, OrmUtils } from "@spacebar/util";
|
||||
import { APActivity } from "activitypub-types";
|
||||
import crypto from "crypto";
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { APError, fetchFederatedUser, fetchOpts } from "./utils";
|
||||
|
||||
export class HttpSig {
|
||||
private static getSignString<T extends IncomingHttpHeaders>(
|
||||
target: string,
|
||||
method: string,
|
||||
headers: T,
|
||||
names: string[],
|
||||
) {
|
||||
const requestTarget = `${method.toLowerCase()} ${target}`;
|
||||
headers = {
|
||||
...headers,
|
||||
"(request-target)": requestTarget,
|
||||
};
|
||||
|
||||
return names
|
||||
.map((header) => `${header.toLowerCase()}: ${headers[header]}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
public static async validate(
|
||||
target: string,
|
||||
activity: APActivity,
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
) {
|
||||
const sigheader = requestHeaders["signature"] as string;
|
||||
const sigopts: { [key: string]: string } = Object.assign(
|
||||
{},
|
||||
...sigheader.split(",").flatMap((keyval) => {
|
||||
const split = keyval.split("=");
|
||||
return {
|
||||
[split[0]]: split[1].replaceAll('"', ""),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const { signature, headers, keyId, algorithm } = sigopts;
|
||||
|
||||
if (!signature || !headers || !keyId)
|
||||
throw new APError("Invalid signature");
|
||||
|
||||
// If it's provided, check it. otherwise just assume it's sha256
|
||||
if (algorithm && algorithm != "rsa-sha256")
|
||||
throw new APError(`Unsupported encryption algorithm ${algorithm}`);
|
||||
|
||||
const url = new URL(keyId);
|
||||
const actorId = `${url.origin}${url.pathname}`; // likely wrong
|
||||
|
||||
const remoteUser = await fetchFederatedUser(actorId);
|
||||
|
||||
const expected = this.getSignString(
|
||||
target,
|
||||
"post",
|
||||
requestHeaders,
|
||||
headers.split(/\s+/),
|
||||
);
|
||||
|
||||
const verifier = crypto.createVerify(algorithm.toUpperCase());
|
||||
verifier.write(expected);
|
||||
verifier.end();
|
||||
|
||||
return verifier.verify(
|
||||
remoteUser.keys.publicKey,
|
||||
Buffer.from(signature, "base64"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a signed request that can be passed to fetch
|
||||
* ```
|
||||
* const signed = await signActivity(receiver.inbox, sender, activity);
|
||||
* await fetch(receiver.inbox, signed);
|
||||
* ```
|
||||
*/
|
||||
public static sign(
|
||||
inbox: string,
|
||||
sender: FederationKey,
|
||||
message: APActivity,
|
||||
) {
|
||||
if (!sender.privateKey)
|
||||
throw new APError("cannot sign without private key");
|
||||
|
||||
const digest = crypto
|
||||
.createHash("sha256")
|
||||
.update(JSON.stringify(message))
|
||||
.digest("base64");
|
||||
const signer = crypto.createSign("sha256");
|
||||
const now = new Date();
|
||||
|
||||
const url = new URL(inbox);
|
||||
const inboxFrag = url.pathname;
|
||||
const toSign =
|
||||
`(request-target): post ${inboxFrag}\n` +
|
||||
`host: ${url.hostname}\n` +
|
||||
`date: ${now.toUTCString()}\n` +
|
||||
`digest: SHA-256=${digest}`;
|
||||
|
||||
signer.update(toSign);
|
||||
signer.end();
|
||||
|
||||
const signature = signer.sign(sender.privateKey);
|
||||
const sig_b64 = signature.toString("base64");
|
||||
|
||||
const { host } = Config.get().federation;
|
||||
const header =
|
||||
`keyId="https://${host}/federation/${sender.type}/${sender.actorId}",` +
|
||||
`headers="(request-target) host date digest",` +
|
||||
`signature=${sig_b64}`;
|
||||
|
||||
return OrmUtils.mergeDeep(fetchOpts, {
|
||||
method: "POST",
|
||||
body: message,
|
||||
headers: {
|
||||
Host: url.hostname,
|
||||
Date: now.toUTCString(),
|
||||
Digest: `SHA-256=${digest}`,
|
||||
Signature: header,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
/**
|
||||
* To be injected into API
|
||||
* Responsible for dispatching activitypub events to external instances
|
||||
*/
|
||||
|
||||
import { MessageCreateEvent, emitEvent } from "@spacebar/util";
|
||||
import { APActivity } from "activitypub-types";
|
||||
import { Request } from "express";
|
||||
import { HttpSig } from "./HttpSig";
|
||||
import { federationQueue } from "./queue";
|
||||
import { transformNoteToMessage } from "./transforms";
|
||||
import {
|
||||
APActivityIsCreate,
|
||||
APError,
|
||||
APObjectIsNote,
|
||||
hasAPContext,
|
||||
} from "./utils";
|
||||
|
||||
export * from "./OrderedCollection";
|
||||
export * from "./transforms";
|
||||
@ -14,4 +19,37 @@ export class Federation {
|
||||
static async distribute(activity: APActivity) {
|
||||
await federationQueue.distribute(activity);
|
||||
}
|
||||
|
||||
static async genericInboxHandler(req: Request) {
|
||||
const activity = req.body;
|
||||
|
||||
if (!hasAPContext(activity))
|
||||
throw new APError("Activity does not have @context");
|
||||
|
||||
if (!(await HttpSig.validate(req.originalUrl, activity, req.headers))) {
|
||||
throw new APError("Invalid signature");
|
||||
}
|
||||
|
||||
if (!APActivityIsCreate(activity)) throw new APError("not implemented");
|
||||
|
||||
const object = Array.isArray(activity.object)
|
||||
? activity.object[0]
|
||||
: activity.object;
|
||||
|
||||
if (!object || typeof object == "string" || !APObjectIsNote(object))
|
||||
throw new APError("not implemented");
|
||||
|
||||
const message = await transformNoteToMessage(object);
|
||||
|
||||
await Promise.all([
|
||||
emitEvent({
|
||||
event: "MESSAGE_CREATE",
|
||||
channel_id: message.channel_id,
|
||||
data: message,
|
||||
} as MessageCreateEvent),
|
||||
message.save(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Config, FederationKey } from "@spacebar/util";
|
||||
import fetch from "node-fetch";
|
||||
import { APError, signActivity, splitQualifiedMention } from "./utils";
|
||||
import { APActivity } from "activitypub-types";
|
||||
import fetch from "node-fetch";
|
||||
import { HttpSig } from "./HttpSig";
|
||||
import { APError, splitQualifiedMention } from "./utils";
|
||||
|
||||
//
|
||||
type Instance = string;
|
||||
@ -38,12 +39,12 @@ class FederationQueue {
|
||||
}
|
||||
|
||||
for (const receiver of to) {
|
||||
if (!(receiver instanceof URL)) {
|
||||
if (typeof receiver != "string") {
|
||||
console.error(receiver);
|
||||
continue;
|
||||
}
|
||||
|
||||
const signedActivity = await signActivity(
|
||||
const signedActivity = await HttpSig.sign(
|
||||
receiver.toString(),
|
||||
sender,
|
||||
activity,
|
||||
|
@ -4,12 +4,21 @@ import {
|
||||
Config,
|
||||
DmChannelDTO,
|
||||
FederationKey,
|
||||
Guild,
|
||||
Invite,
|
||||
Member,
|
||||
Message,
|
||||
Snowflake,
|
||||
User,
|
||||
UserSettings,
|
||||
} from "@spacebar/util";
|
||||
import {
|
||||
APAnnounce,
|
||||
APGroup,
|
||||
APNote,
|
||||
APOrganization,
|
||||
APPerson,
|
||||
} from "activitypub-types";
|
||||
import TurndownService from "turndown";
|
||||
import { In } from "typeorm";
|
||||
import {
|
||||
@ -18,7 +27,6 @@ import {
|
||||
APObjectIsPerson,
|
||||
resolveAPObject,
|
||||
} from "./utils";
|
||||
import { APAnnounce, APGroup, APNote, APPerson } from "activitypub-types";
|
||||
|
||||
export const transformMessageToAnnounceNoce = async (
|
||||
message: Message,
|
||||
@ -32,9 +40,8 @@ export const transformMessageToAnnounceNoce = async (
|
||||
},
|
||||
});
|
||||
|
||||
let to = [
|
||||
`https://${host}/federation/channels/${message.channel_id}/followers`,
|
||||
];
|
||||
// let to = `https://${host}/federation/channels/${message.channel_id}/followers`;
|
||||
let to = ["https://www.w3.org/ns/activitystreams#Public"]; // TODO
|
||||
|
||||
if (channel.isDm()) {
|
||||
const otherUsers = channel.recipients?.filter(
|
||||
@ -83,7 +90,7 @@ export const transformMessageToNote = async (
|
||||
published: message.timestamp,
|
||||
attributedTo: `https://${host}/federation/users/${message.author_id}`,
|
||||
|
||||
to: [`https://${host}/federation/channels/${message.channel_id}`],
|
||||
to: `https://${host}/federation/channels/${message.channel_id}/followers`,
|
||||
tag: message.mentions?.map(
|
||||
(x) => `https://${host}/federation/users/${x.id}`,
|
||||
),
|
||||
@ -250,6 +257,7 @@ export const transformPersonToUser = async (person: APPerson) => {
|
||||
const keys = await FederationKey.create({
|
||||
actorId: Snowflake.generate(),
|
||||
federatedId: url.toString(),
|
||||
username: person.preferredUsername,
|
||||
domain: url.hostname,
|
||||
publicKey: person.publicKey?.publicKeyPem,
|
||||
type: ActorType.USER,
|
||||
@ -280,3 +288,44 @@ export const transformPersonToUser = async (person: APPerson) => {
|
||||
created_at: new Date(),
|
||||
}).save();
|
||||
};
|
||||
|
||||
export const transformOrganisationToInvite = (guild: APOrganization) => {
|
||||
return Invite.create({
|
||||
code: guild.id,
|
||||
temporary: false,
|
||||
uses: -1,
|
||||
max_uses: 0,
|
||||
max_age: 0,
|
||||
created_at: new Date(0),
|
||||
flags: 0,
|
||||
});
|
||||
};
|
||||
|
||||
export const transformGuildToOrganisation = async (
|
||||
guild: Guild,
|
||||
): Promise<APOrganization> => {
|
||||
const { host, accountDomain } = Config.get().federation;
|
||||
|
||||
const keys = await FederationKey.findOneOrFail({
|
||||
where: { actorId: guild.id, domain: accountDomain },
|
||||
});
|
||||
|
||||
return {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
type: "Organization",
|
||||
id: `https://${host}/federation/guilds/${guild.id}`,
|
||||
|
||||
name: guild.name,
|
||||
preferredUsername: guild.id,
|
||||
icon: undefined,
|
||||
|
||||
inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
|
||||
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
|
||||
followers: `https://${host}/federation/guilds/${guild.id}/followers`,
|
||||
publicKey: {
|
||||
id: `https://${host}/federation/guilds/${guild.id}#main-key`,
|
||||
owner: `https://${host}/federation/guilds/${guild.id}`,
|
||||
publicKeyPem: keys.publicKey,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,21 +1,26 @@
|
||||
import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
|
||||
import {
|
||||
ActorType,
|
||||
Config,
|
||||
FederationKey,
|
||||
OrmUtils,
|
||||
Snowflake,
|
||||
User,
|
||||
UserSettings,
|
||||
WebfingerResponse,
|
||||
} from "@spacebar/util";
|
||||
import {
|
||||
APActivity,
|
||||
APActor,
|
||||
APObject,
|
||||
APAnnounce,
|
||||
APCreate,
|
||||
APNote,
|
||||
APPerson,
|
||||
AnyAPObject,
|
||||
} from "activitypub-types";
|
||||
import crypto from "crypto";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
import { ProxyAgent } from "proxy-agent";
|
||||
import TurndownService from "turndown";
|
||||
|
||||
export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
||||
|
||||
@ -30,10 +35,9 @@ export class APError extends HTTPError {}
|
||||
export const hasAPContext = (data: object) => {
|
||||
if (!("@context" in data)) return false;
|
||||
const context = data["@context"];
|
||||
const activitystreams = "https://www.w3.org/ns/activitystreams";
|
||||
if (Array.isArray(context))
|
||||
return context.find((x) => x == activitystreams);
|
||||
return context == activitystreams;
|
||||
return !!context.find((x) => x == ACTIVITYSTREAMS_CONTEXT);
|
||||
return context == ACTIVITYSTREAMS_CONTEXT;
|
||||
};
|
||||
|
||||
export const resolveAPObject = async <T extends AnyAPObject>(
|
||||
@ -97,64 +101,91 @@ export const resolveWebfinger = async (
|
||||
},
|
||||
).then((x) => x.json())) as WebfingerResponse;
|
||||
|
||||
if (!("links" in wellknown))
|
||||
throw new APError(
|
||||
`webfinger did not return any links for actor ${lookup}`,
|
||||
);
|
||||
|
||||
const link = wellknown.links.find((x) => x.rel == "self");
|
||||
if (!link) throw new APError(".well-known did not contain rel=self link");
|
||||
|
||||
return await resolveAPObject<AnyAPObject>(link.href);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a signed request that can be passed to fetch
|
||||
* ```
|
||||
* const signed = await signActivity(receiver.inbox, sender, activity);
|
||||
* await fetch(receiver.inbox, signed);
|
||||
* ```
|
||||
*/
|
||||
export const signActivity = async (
|
||||
inbox: string,
|
||||
sender: FederationKey,
|
||||
message: APActivity,
|
||||
) => {
|
||||
if (!sender.privateKey)
|
||||
throw new APError("cannot sign without private key");
|
||||
|
||||
const digest = crypto
|
||||
.createHash("sha256")
|
||||
.update(JSON.stringify(message))
|
||||
.digest("base64");
|
||||
const signer = crypto.createSign("sha256");
|
||||
const now = new Date();
|
||||
|
||||
const url = new URL(inbox);
|
||||
const inboxFrag = url.pathname;
|
||||
const toSign =
|
||||
`(request-target): post ${inboxFrag}\n` +
|
||||
`host: ${url.hostname}\n` +
|
||||
`date: ${now.toUTCString()}\n` +
|
||||
`digest: SHA-256=${digest}`;
|
||||
|
||||
signer.update(toSign);
|
||||
signer.end();
|
||||
|
||||
const signature = signer.sign(sender.privateKey);
|
||||
const sig_b64 = signature.toString("base64");
|
||||
|
||||
const { host } = Config.get().federation;
|
||||
const header =
|
||||
`keyId="${host}/${sender.type}/${sender.actorId}#main-key",` +
|
||||
`headers="(request-target) host date digest",` +
|
||||
`signature=${sig_b64}`;
|
||||
|
||||
return OrmUtils.mergeDeep(fetchOpts, {
|
||||
method: "POST",
|
||||
body: message,
|
||||
headers: {
|
||||
Host: url.hostname,
|
||||
Date: now.toUTCString(),
|
||||
Digest: `SHA-256=${digest}`,
|
||||
Signature: header,
|
||||
},
|
||||
/** Fetch from local db, if not found fetch from remote instance and save */
|
||||
export const fetchFederatedUser = async (actorId: string) => {
|
||||
// if we were given webfinger, resolve that first
|
||||
const mention = splitQualifiedMention(actorId);
|
||||
const cache = await FederationKey.findOne({
|
||||
where: { username: mention.user, domain: mention.domain },
|
||||
});
|
||||
if (cache) {
|
||||
return {
|
||||
keys: cache,
|
||||
user: await User.findOneOrFail({ where: { id: cache.actorId } }),
|
||||
};
|
||||
}
|
||||
|
||||
// if we don't already have it, resolve webfinger
|
||||
const remoteActor = await resolveWebfinger(actorId);
|
||||
|
||||
let type: ActorType;
|
||||
if (APObjectIsPerson(remoteActor)) type = ActorType.USER;
|
||||
else if (APObjectIsGroup(remoteActor)) type = ActorType.CHANNEL;
|
||||
else if (APObjectIsOrganisation(remoteActor)) type = ActorType.GUILD;
|
||||
else
|
||||
throw new APError(
|
||||
`The remote actor '${actorId}' is not a Person, Group, or Organisation`,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof remoteActor.inbox != "string" ||
|
||||
typeof remoteActor.outbox != "string"
|
||||
)
|
||||
throw new APError("Actor inbox/outbox must be string");
|
||||
|
||||
const keys = FederationKey.create({
|
||||
actorId: Snowflake.generate(),
|
||||
federatedId: actorId,
|
||||
username: remoteActor.preferredUsername,
|
||||
// this is technically not correct
|
||||
// but it's slightly more difficult to go from actor url -> handle
|
||||
// so thats a problem for future me
|
||||
domain: mention.domain,
|
||||
publicKey: remoteActor.publicKey?.publicKeyPem,
|
||||
type,
|
||||
inbox: remoteActor.inbox,
|
||||
outbox: remoteActor.outbox,
|
||||
});
|
||||
|
||||
const user = User.create({
|
||||
id: keys.actorId,
|
||||
username: remoteActor.preferredUsername,
|
||||
discriminator: "0",
|
||||
bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown
|
||||
email: `${remoteActor.preferredUsername}@${keys.domain}`,
|
||||
data: {
|
||||
hash: "#",
|
||||
valid_tokens_since: new Date(),
|
||||
},
|
||||
extended_settings: "{}",
|
||||
settings: UserSettings.create(),
|
||||
premium: false,
|
||||
|
||||
premium_since: Config.get().defaults.user.premium
|
||||
? new Date()
|
||||
: undefined,
|
||||
rights: Config.get().register.defaultRights,
|
||||
premium_type: Config.get().defaults.user.premiumType ?? 0,
|
||||
verified: Config.get().defaults.user.verified ?? true,
|
||||
created_at: new Date(),
|
||||
});
|
||||
|
||||
await Promise.all([keys.save(), user.save()]);
|
||||
return {
|
||||
keys,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
// fetch from remote server?
|
||||
@ -181,3 +212,15 @@ export const APObjectIsSpacebarActor = (
|
||||
APObjectIsPerson(object)
|
||||
);
|
||||
};
|
||||
|
||||
export const APActivityIsCreate = (act: APActivity): act is APCreate => {
|
||||
return act.type == "Create";
|
||||
};
|
||||
|
||||
export const APActivityIsAnnounce = (act: APActivity): act is APAnnounce => {
|
||||
return act.type == "Announce";
|
||||
};
|
||||
|
||||
export const APObjectIsNote = (obj: AnyAPObject): obj is APNote => {
|
||||
return obj.type == "Note";
|
||||
};
|
||||
|
16
src/activitypub/routes/guilds/#guild_id/index.ts
Normal file
16
src/activitypub/routes/guilds/#guild_id/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { transformGuildToOrganisation } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Guild } from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
// TODO: auth
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { id: req.params.guild_id },
|
||||
});
|
||||
|
||||
return res.json(await transformGuildToOrganisation(guild));
|
||||
});
|
||||
|
||||
export default router;
|
@ -1,3 +1,4 @@
|
||||
import { Federation } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Request, Response, Router } from "express";
|
||||
const router = Router();
|
||||
@ -5,6 +6,8 @@ const router = Router();
|
||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||
// TODO: support lemmy ChatMessage type?
|
||||
// TODO: check if the activity exists on the remote server
|
||||
|
||||
res.json(await Federation.genericInboxHandler(req));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -6,12 +6,13 @@ import {
|
||||
FederationKey,
|
||||
FieldErrors,
|
||||
Guild,
|
||||
Invite,
|
||||
User,
|
||||
WebfingerResponse,
|
||||
} from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { splitQualifiedMention } from "./federation";
|
||||
import { APError, splitQualifiedMention } from "./federation";
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@ -41,11 +42,13 @@ router.get(
|
||||
|
||||
const { accountDomain, host } = Config.get().federation;
|
||||
|
||||
const { user, domain } = splitQualifiedMention(resource);
|
||||
const mention = splitQualifiedMention(resource);
|
||||
const domain = mention.domain;
|
||||
let user = mention.user;
|
||||
if (domain != accountDomain)
|
||||
throw new HTTPError("Resource could not be found", 404);
|
||||
|
||||
const keys = await FederationKey.findOneOrFail({
|
||||
let keys = await FederationKey.findOne({
|
||||
where: {
|
||||
actorId: user,
|
||||
domain,
|
||||
@ -53,6 +56,21 @@ router.get(
|
||||
select: ["type"],
|
||||
});
|
||||
|
||||
if (!keys) {
|
||||
// maybe it's an invite?
|
||||
|
||||
const invite = await Invite.findOne({ where: { code: user } });
|
||||
if (!invite) throw new APError("Resource count not be found");
|
||||
|
||||
// yippe, it is.
|
||||
|
||||
keys = await FederationKey.findOneOrFail({
|
||||
where: { domain, actorId: invite.guild_id },
|
||||
});
|
||||
|
||||
user = invite.guild_id;
|
||||
}
|
||||
|
||||
let entity: User | Channel | Guild;
|
||||
switch (keys.type) {
|
||||
case ActorType.USER:
|
||||
|
@ -16,9 +16,10 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { APError } from "@spacebar/ap";
|
||||
import { ApiError, FieldError } from "@spacebar/util";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { ApiError, FieldError } from "@spacebar/util";
|
||||
const EntityNotFoundErrorRegex = /"(\w+)"/;
|
||||
|
||||
export function ErrorHandler(
|
||||
@ -35,7 +36,10 @@ export function ErrorHandler(
|
||||
let message = error?.toString();
|
||||
let errors = undefined;
|
||||
|
||||
if (error instanceof HTTPError && error.code)
|
||||
if (
|
||||
(error instanceof HTTPError || error instanceof APError) &&
|
||||
error.code
|
||||
)
|
||||
code = httpcode = error.code;
|
||||
else if (error instanceof ApiError) {
|
||||
code = error.code;
|
||||
|
@ -16,16 +16,24 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
APError,
|
||||
APObjectIsOrganisation,
|
||||
resolveWebfinger,
|
||||
splitQualifiedMention,
|
||||
transformOrganisationToInvite,
|
||||
} from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import {
|
||||
Config,
|
||||
DiscordApiErrors,
|
||||
emitEvent,
|
||||
getPermission,
|
||||
Guild,
|
||||
Invite,
|
||||
InviteDeleteEvent,
|
||||
PublicInviteRelation,
|
||||
User,
|
||||
emitEvent,
|
||||
getPermission,
|
||||
} from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
@ -46,6 +54,31 @@ router.get(
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
const { inputValue } = req.query;
|
||||
|
||||
if (inputValue && typeof inputValue == "string") {
|
||||
const mention = splitQualifiedMention(inputValue);
|
||||
if (mention.user.length && Config.get().federation.enabled) {
|
||||
// This invite is in the form `invitecode@domain.com` OR `https://domain.com/whatever/invitecode`
|
||||
// If the domain provided isn't ours, it's a federated invite
|
||||
// and we should try and fetch that
|
||||
|
||||
const { domain } = mention;
|
||||
const { accountDomain, host } = Config.get().federation;
|
||||
if (domain != accountDomain && domain != host) {
|
||||
// The domain isn't ours
|
||||
|
||||
const remoteGuild = await resolveWebfinger(inputValue);
|
||||
|
||||
if (APObjectIsOrganisation(remoteGuild))
|
||||
return res.json(
|
||||
transformOrganisationToInvite(remoteGuild),
|
||||
);
|
||||
|
||||
throw new APError("Remote resource is not a guild");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await Invite.findOneOrFail({
|
||||
where: { code },
|
||||
|
@ -25,9 +25,9 @@ export class FederationKey extends BaseClassWithoutId {
|
||||
@Column()
|
||||
domain: string;
|
||||
|
||||
/** The federated preferred username */
|
||||
@Column()
|
||||
username: string;
|
||||
/** The federated preferred username of remote users. Local usernames are null. */
|
||||
@Column({ nullable: true, type: String })
|
||||
username: string | null;
|
||||
|
||||
/** The remote ID ( actor URL ) of this user */
|
||||
@Column()
|
||||
|
@ -24,7 +24,14 @@ import {
|
||||
OneToMany,
|
||||
RelationId,
|
||||
} from "typeorm";
|
||||
import { Config, GuildWelcomeScreen, Snowflake, handleFile } from "..";
|
||||
import {
|
||||
ActorType,
|
||||
Config,
|
||||
FederationKey,
|
||||
GuildWelcomeScreen,
|
||||
Snowflake,
|
||||
handleFile,
|
||||
} from "..";
|
||||
import { Ban } from "./Ban";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
@ -388,6 +395,16 @@ export class Guild extends BaseClass {
|
||||
);
|
||||
}
|
||||
|
||||
// If federation is enabled, generate signing keys for this actor.
|
||||
setImmediate(
|
||||
async () =>
|
||||
Config.get().federation.enabled &&
|
||||
(await FederationKey.generateSigningKeys(
|
||||
guild.id,
|
||||
ActorType.GUILD,
|
||||
)),
|
||||
);
|
||||
|
||||
return guild;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user