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);
|
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`,
|
||||||
|
@ -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>>;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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 => {
|
||||||
|
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 { 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({
|
||||||
|
Loading…
Reference in New Issue
Block a user