1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-25 11:43:07 +01:00

basic channel federation

This commit is contained in:
Madeline 2023-09-30 01:33:52 +00:00
parent 4cc297cc10
commit 34867e2154
6 changed files with 150 additions and 6 deletions

View File

@ -26,6 +26,9 @@ export const makeOrderedCollection = async <T extends AnyAPObject>(opts: {
const elems = await getElements(before, after); const elems = await getElements(before, after);
// TODO: we need to specify next/prev props
// and should probably let the caller of this function specify what they are
// along with first/last
return { return {
"@context": ACTIVITYSTREAMS_CONTEXT, "@context": ACTIVITYSTREAMS_CONTEXT,
id: `${id}?page=true`, id: `${id}?page=true`,

View File

@ -15,15 +15,20 @@ import {
ActivityIsFollow, ActivityIsFollow,
AnyAPObject, AnyAPObject,
ObjectIsNote, ObjectIsNote,
ObjectIsOrganization,
} from "activitypub-types"; } from "activitypub-types";
import { Request } from "express"; import { Request } from "express";
import { HttpSig } from "../HttpSig"; import { HttpSig } from "../HttpSig";
import { federationQueue } from "../queue"; import { federationQueue } from "../queue";
import { transformNoteToMessage } from "../transforms"; import {
transformNoteToMessage,
transformOrganisationToGuild,
} from "../transforms";
import { APFollowWithInvite } from "../types"; import { APFollowWithInvite } from "../types";
import { import {
ACTIVITYSTREAMS_CONTEXT, ACTIVITYSTREAMS_CONTEXT,
APError, APError,
createChannelsFromGuildFollows,
fetchFederatedUser, fetchFederatedUser,
hasAPContext, hasAPContext,
resolveAPObject, resolveAPObject,
@ -106,13 +111,29 @@ const handlers = {
if (typeof inner.object != "string") if (typeof inner.object != "string")
throw new APError("not implemented"); throw new APError("not implemented");
const guild = await fetchFederatedUser(inner.object); const apGuild = await resolveAPObject(inner.object);
if (!ObjectIsOrganization(apGuild))
throw new APError(
"Accept Follow received for object other than Organisation ( Guild ), Ignoring",
);
if (!apGuild.following || typeof apGuild.following != "string")
throw new APError("Guild must be following channels");
const guild = await transformOrganisationToGuild(apGuild);
// create the channels
await createChannelsFromGuildFollows(
apGuild.following + "?page=true", // TODO: wrong
guild.id,
);
if (typeof inner.actor != "string") if (typeof inner.actor != "string")
throw new APError("not implemented"); throw new APError("not implemented");
const { user } = splitQualifiedMention(inner.actor); const { user } = splitQualifiedMention(inner.actor);
Member.addToGuild(user, guild.entity.id); Member.addToGuild(user, guild.id);
}, },
} as Record<string, (activity: AnyAPObject) => Promise<unknown>>; } as Record<string, (activity: AnyAPObject) => Promise<unknown>>;

View File

@ -1,6 +1,7 @@
import { import {
ActorType, ActorType,
Channel, Channel,
ChannelType,
Config, Config,
DmChannelDTO, DmChannelDTO,
FederationKey, FederationKey,
@ -8,6 +9,7 @@ import {
Invite, Invite,
Member, Member,
Message, Message,
Role,
Snowflake, Snowflake,
User, User,
UserSettings, UserSettings,
@ -343,7 +345,25 @@ export const transformOrganisationToGuild = async (org: APOrganization) => {
owner_id: owner.entity.id, owner_id: owner.entity.id,
}); });
const role = Role.create({
id: guild.id,
guild_id: guild.id,
color: 0,
hoist: false,
managed: false,
// NB: in Spacebar, every role will be non-managed, as we use user-groups instead of roles for managed groups
mentionable: false,
name: "@everyone",
permissions: String("2251804225"),
position: 0,
icon: undefined,
unicode_emoji: undefined,
flags: 0, // TODO?
});
await Promise.all([guild.save(), keys.save()]); await Promise.all([guild.save(), keys.save()]);
await role.save();
return guild; return guild;
}; };
@ -372,6 +392,7 @@ export const transformGuildToOrganisation = async (
inbox: `https://${host}/federation/guilds/${guild.id}/inbox`, inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`, outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
followers: `https://${host}/federation/guilds/${guild.id}/followers`, followers: `https://${host}/federation/guilds/${guild.id}/followers`,
following: `https://${host}/federation/guilds/${guild.id}/following`,
publicKey: { publicKey: {
id: `https://${host}/federation/guilds/${guild.id}#main-key`, id: `https://${host}/federation/guilds/${guild.id}#main-key`,
owner: `https://${host}/federation/guilds/${guild.id}`, owner: `https://${host}/federation/guilds/${guild.id}`,
@ -379,3 +400,42 @@ export const transformGuildToOrganisation = async (
}, },
}; };
}; };
export const transformGroupToChannel = async (
group: APGroup,
guild_id: string,
) => {
if (!group.id) throw new APError("Channel ( group ) must have ID");
if (!group.publicKey || !group.publicKey.publicKeyPem)
throw new APError("Federated guild must have public key.");
const cache = await FederationKey.findOne({
where: { federatedId: group.id },
});
if (cache) return Channel.findOneOrFail({ where: { id: cache.actorId } });
const keys = FederationKey.create({
actorId: Snowflake.generate(),
federatedId: group.id,
username: group.name,
domain: new URL(group.id).hostname,
publicKey: group.publicKey.publicKeyPem,
type: ActorType.CHANNEL,
inbox: group.inbox.toString(),
outbox: group.outbox.toString(),
});
const channel = Channel.create({
id: keys.actorId,
name: group.name,
type: ChannelType.GUILD_TEXT, // TODO
owner_id: undefined,
last_message_id: undefined,
position: 0, // TODO
guild_id,
});
await Promise.all([keys.save(), channel.save()]);
return channel;
};

View File

@ -2,6 +2,7 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
import { import {
ActorType, ActorType,
BaseClass, BaseClass,
ChannelCreateEvent,
Config, Config,
Debug, Debug,
FederationActivity, FederationActivity,
@ -13,9 +14,11 @@ import {
User, User,
UserSettings, UserSettings,
WebfingerResponse, WebfingerResponse,
emitEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { import {
APObject, APObject,
APOrderedCollection,
APPerson, APPerson,
AnyAPObject, AnyAPObject,
ObjectIsGroup, ObjectIsGroup,
@ -27,6 +30,7 @@ import fetch from "node-fetch";
import { ProxyAgent } from "proxy-agent"; import { ProxyAgent } from "proxy-agent";
import TurndownService from "turndown"; import TurndownService from "turndown";
import { federationQueue } from "./queue"; import { federationQueue } from "./queue";
import { transformGroupToChannel } from "./transforms";
import { APFollowWithInvite } from "./types"; import { APFollowWithInvite } from "./types";
export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams"; export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
@ -256,6 +260,28 @@ export const tryFederatedGuildJoin = async (code: string, user_id: string) => {
await federationQueue.distribute(follow.toJSON()); await federationQueue.distribute(follow.toJSON());
}; };
export const createChannelsFromGuildFollows = async (
endpoint: string,
guild_id: string,
) => {
const collection = (await resolveAPObject(endpoint)) as APOrderedCollection; // TODO: validation
if (!collection.orderedItems)
throw new APError("Guild followers did not contain orderedItems");
// resolve every channel
for (const channel of collection.orderedItems) {
if (typeof channel == "string" || !ObjectIsGroup(channel)) continue;
const guildchannel = await transformGroupToChannel(channel, guild_id);
await emitEvent({
event: "CHANNEL_CREATE",
data: guildchannel,
guild_id: guildchannel.guild_id,
} as ChannelCreateEvent);
}
};
export const APObjectIsSpacebarActor = ( export const APObjectIsSpacebarActor = (
object: AnyAPObject, object: AnyAPObject,
): object is APPerson => { ): object is APPerson => {

View File

@ -0,0 +1,35 @@
import { makeOrderedCollection, transformChannelToGroup } from "@spacebar/ap";
import { route } from "@spacebar/api";
import { Channel, Config } from "@spacebar/util";
import { APGroup } from "activitypub-types";
import { Request, Response, Router } from "express";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const { page, min_id, max_id } = req.query;
const { host } = Config.get().federation;
const ret = await makeOrderedCollection({
page: page != undefined,
min_id: min_id?.toString(),
max_id: max_id?.toString(),
id: `https://${host}/federation/guilds/${guild_id}/followers`,
getTotalElements: () => Channel.count({ where: { guild_id } }),
getElements: async (before, after): Promise<APGroup[]> => {
const channels = await Channel.find({
where: { guild_id },
order: { position: "ASC" },
});
// TODO: actual pagination
return Promise.all(channels.map((x) => transformChannelToGroup(x)));
},
});
return res.json(ret);
});
export default router;

View File

@ -19,6 +19,7 @@
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { import {
Column, Column,
CreateDateColumn,
Entity, Entity,
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
@ -73,7 +74,7 @@ export enum ChannelType {
@Entity("channels") @Entity("channels")
export class Channel extends BaseClass { export class Channel extends BaseClass {
@Column() @CreateDateColumn()
created_at: Date; created_at: Date;
@Column({ nullable: true }) @Column({ nullable: true })
@ -301,7 +302,6 @@ export class Channel extends BaseClass {
channel = { channel = {
...channel, ...channel,
...(!opts?.keepId && { id: Snowflake.generate() }), ...(!opts?.keepId && { id: Snowflake.generate() }),
created_at: new Date(),
position: position:
(channel.type === ChannelType.UNHANDLED (channel.type === ChannelType.UNHANDLED
? 0 ? 0
@ -381,7 +381,6 @@ export class Channel extends BaseClass {
name, name,
type, type,
owner_id: undefined, owner_id: undefined,
created_at: new Date(),
last_message_id: undefined, last_message_id: undefined,
recipients: channelRecipients.map((x) => recipients: channelRecipients.map((x) =>
Recipient.create({ Recipient.create({