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:
parent
4cc297cc10
commit
34867e2154
@ -26,6 +26,9 @@ export const makeOrderedCollection = async <T extends AnyAPObject>(opts: {
|
||||
|
||||
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 {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
id: `${id}?page=true`,
|
||||
|
@ -15,15 +15,20 @@ import {
|
||||
ActivityIsFollow,
|
||||
AnyAPObject,
|
||||
ObjectIsNote,
|
||||
ObjectIsOrganization,
|
||||
} from "activitypub-types";
|
||||
import { Request } from "express";
|
||||
import { HttpSig } from "../HttpSig";
|
||||
import { federationQueue } from "../queue";
|
||||
import { transformNoteToMessage } from "../transforms";
|
||||
import {
|
||||
transformNoteToMessage,
|
||||
transformOrganisationToGuild,
|
||||
} from "../transforms";
|
||||
import { APFollowWithInvite } from "../types";
|
||||
import {
|
||||
ACTIVITYSTREAMS_CONTEXT,
|
||||
APError,
|
||||
createChannelsFromGuildFollows,
|
||||
fetchFederatedUser,
|
||||
hasAPContext,
|
||||
resolveAPObject,
|
||||
@ -106,13 +111,29 @@ const handlers = {
|
||||
if (typeof inner.object != "string")
|
||||
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")
|
||||
throw new APError("not implemented");
|
||||
|
||||
const { user } = splitQualifiedMention(inner.actor);
|
||||
Member.addToGuild(user, guild.entity.id);
|
||||
Member.addToGuild(user, guild.id);
|
||||
},
|
||||
} as Record<string, (activity: AnyAPObject) => Promise<unknown>>;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
ActorType,
|
||||
Channel,
|
||||
ChannelType,
|
||||
Config,
|
||||
DmChannelDTO,
|
||||
FederationKey,
|
||||
@ -8,6 +9,7 @@ import {
|
||||
Invite,
|
||||
Member,
|
||||
Message,
|
||||
Role,
|
||||
Snowflake,
|
||||
User,
|
||||
UserSettings,
|
||||
@ -343,7 +345,25 @@ export const transformOrganisationToGuild = async (org: APOrganization) => {
|
||||
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 role.save();
|
||||
|
||||
return guild;
|
||||
};
|
||||
|
||||
@ -372,6 +392,7 @@ export const transformGuildToOrganisation = async (
|
||||
inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
|
||||
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
|
||||
followers: `https://${host}/federation/guilds/${guild.id}/followers`,
|
||||
following: `https://${host}/federation/guilds/${guild.id}/following`,
|
||||
publicKey: {
|
||||
id: `https://${host}/federation/guilds/${guild.id}#main-key`,
|
||||
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;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
|
||||
import {
|
||||
ActorType,
|
||||
BaseClass,
|
||||
ChannelCreateEvent,
|
||||
Config,
|
||||
Debug,
|
||||
FederationActivity,
|
||||
@ -13,9 +14,11 @@ import {
|
||||
User,
|
||||
UserSettings,
|
||||
WebfingerResponse,
|
||||
emitEvent,
|
||||
} from "@spacebar/util";
|
||||
import {
|
||||
APObject,
|
||||
APOrderedCollection,
|
||||
APPerson,
|
||||
AnyAPObject,
|
||||
ObjectIsGroup,
|
||||
@ -27,6 +30,7 @@ import fetch from "node-fetch";
|
||||
import { ProxyAgent } from "proxy-agent";
|
||||
import TurndownService from "turndown";
|
||||
import { federationQueue } from "./queue";
|
||||
import { transformGroupToChannel } from "./transforms";
|
||||
import { APFollowWithInvite } from "./types";
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
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 = (
|
||||
object: AnyAPObject,
|
||||
): object is APPerson => {
|
||||
|
35
src/activitypub/routes/guilds/#guild_id/following.ts
Normal file
35
src/activitypub/routes/guilds/#guild_id/following.ts
Normal 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;
|
@ -19,6 +19,7 @@
|
||||
import { HTTPError } from "lambert-server";
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
@ -73,7 +74,7 @@ export enum ChannelType {
|
||||
|
||||
@Entity("channels")
|
||||
export class Channel extends BaseClass {
|
||||
@Column()
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@ -301,7 +302,6 @@ export class Channel extends BaseClass {
|
||||
channel = {
|
||||
...channel,
|
||||
...(!opts?.keepId && { id: Snowflake.generate() }),
|
||||
created_at: new Date(),
|
||||
position:
|
||||
(channel.type === ChannelType.UNHANDLED
|
||||
? 0
|
||||
@ -381,7 +381,6 @@ export class Channel extends BaseClass {
|
||||
name,
|
||||
type,
|
||||
owner_id: undefined,
|
||||
created_at: new Date(),
|
||||
last_message_id: undefined,
|
||||
recipients: channelRecipients.map((x) =>
|
||||
Recipient.create({
|
||||
|
Loading…
Reference in New Issue
Block a user