diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts
index 90b98b7a..0b3a4152 100644
--- a/src/util/config/Config.ts
+++ b/src/util/config/Config.ts
@@ -38,6 +38,7 @@ import {
SentryConfiguration,
TemplateConfiguration,
} from "../config";
+import { FederationConfiguration } from "./types/FederationConfiguration";
export class ConfigValue {
gateway: EndpointConfiguration = new EndpointConfiguration();
@@ -61,4 +62,5 @@ export class ConfigValue {
email: EmailConfiguration = new EmailConfiguration();
passwordReset: PasswordResetConfiguration =
new PasswordResetConfiguration();
+ federation = new FederationConfiguration();
}
diff --git a/src/util/config/types/FederationConfiguration.ts b/src/util/config/types/FederationConfiguration.ts
new file mode 100644
index 00000000..b04388fd
--- /dev/null
+++ b/src/util/config/types/FederationConfiguration.ts
@@ -0,0 +1,5 @@
+export class FederationConfiguration {
+ enabled: boolean = false;
+ localDomain: string | null = null;
+ webDomain: string | null = null;
+}
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 9f7041d4..0ccabd62 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -28,6 +28,7 @@ import {
import { DmChannelDTO } from "../dtos";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import {
+ Config,
InvisibleCharacters,
Snowflake,
containsAll,
@@ -41,10 +42,14 @@ import { Invite } from "./Invite";
import { Message } from "./Message";
import { ReadState } from "./ReadState";
import { Recipient } from "./Recipient";
-import { PublicUserProjection, User } from "./User";
+import { APPersonButMore, PublicUserProjection, User } from "./User";
import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook";
+import crypto from "crypto";
+import { promisify } from "util";
+const generateKeyPair = promisify(crypto.generateKeyPair);
+
export enum ChannelType {
GUILD_TEXT = 0, // a text channel within a guild
DM = 1, // a direct message between users
@@ -193,6 +198,12 @@ export class Channel extends BaseClass {
@Column()
default_thread_rate_limit_per_user: number = 0;
+ @Column()
+ publicKey: string;
+
+ @Column()
+ privateKey: string;
+
// TODO: DM channel
static async createChannel(
channel: Partial<Channel>,
@@ -303,6 +314,21 @@ export class Channel extends BaseClass {
: channel.position) || 0,
};
+ const { publicKey, privateKey } = await generateKeyPair("rsa", {
+ modulusLength: 4096,
+ publicKeyEncoding: {
+ type: "spki",
+ format: "pem",
+ },
+ privateKeyEncoding: {
+ type: "pkcs8",
+ format: "pem",
+ },
+ });
+
+ channel.publicKey = publicKey;
+ channel.privateKey = privateKey;
+
const ret = Channel.create(channel);
await Promise.all([
@@ -362,6 +388,18 @@ export class Channel extends BaseClass {
if (channel == null) {
name = trimSpecial(name);
+ const { publicKey, privateKey } = await generateKeyPair("rsa", {
+ modulusLength: 4096,
+ publicKeyEncoding: {
+ type: "spki",
+ format: "pem",
+ },
+ privateKeyEncoding: {
+ type: "pkcs8",
+ format: "pem",
+ },
+ });
+
channel = await Channel.create({
name,
type,
@@ -378,6 +416,8 @@ export class Channel extends BaseClass {
}),
),
nsfw: false,
+ publicKey,
+ privateKey,
}).save();
}
@@ -483,6 +523,31 @@ export class Channel extends BaseClass {
owner_id: this.owner_id || undefined,
};
}
+
+ toAP(): APPersonButMore {
+ const { webDomain } = Config.get().federation;
+
+ return {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ type: "Group",
+ id: `https://${webDomain}/fed/channel/${this.id}`,
+ name: this.name,
+ preferredUsername: this.id,
+ summary: this.topic,
+ icon: undefined,
+ discoverable: true,
+
+ publicKey: {
+ id: `https://${webDomain}/fed/user/${this.id}#main-key`,
+ owner: `https://${webDomain}/fed/user/${this.id}`,
+ publicKeyPem: this.publicKey,
+ },
+
+ inbox: `https://${webDomain}/fed/channel/${this.id}/inbox`,
+ outbox: `https://${webDomain}/fed/channel/${this.id}/outbox`,
+ followers: `https://${webDomain}/fed/channel/${this.id}/followers`,
+ };
+ }
}
export interface ChannelPermissionOverwrite {
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 0535313e..16b18ab1 100644
--- a/src/util/entities/Member.ts
+++ b/src/util/entities/Member.ts
@@ -366,28 +366,30 @@ export class Member extends BaseClassWithoutId {
bio: "",
};
+ const ret = Member.create({
+ ...member,
+ roles: [Role.create({ id: guild_id })],
+ // read_state: {},
+ settings: {
+ guild_id: null,
+ mute_config: null,
+ mute_scheduled_events: false,
+ flags: 0,
+ hide_muted_channels: false,
+ notify_highlights: 0,
+ channel_overrides: {},
+ message_notifications: 0,
+ mobile_push: true,
+ muted: false,
+ suppress_everyone: false,
+ suppress_roles: false,
+ version: 0,
+ },
+ // Member.save is needed because else the roles relations wouldn't be updated
+ });
+
await Promise.all([
- Member.create({
- ...member,
- roles: [Role.create({ id: guild_id })],
- // read_state: {},
- settings: {
- guild_id: null,
- mute_config: null,
- mute_scheduled_events: false,
- flags: 0,
- hide_muted_channels: false,
- notify_highlights: 0,
- channel_overrides: {},
- message_notifications: 0,
- mobile_push: true,
- muted: false,
- suppress_everyone: false,
- suppress_roles: false,
- version: 0,
- },
- // Member.save is needed because else the roles relations wouldn't be updated
- }).save(),
+ ret.save(),
Guild.increment({ id: guild_id }, "member_count", 1),
emitEvent({
event: "GUILD_MEMBER_ADD",
@@ -444,6 +446,8 @@ export class Member extends BaseClassWithoutId {
} as MessageCreateEvent),
]);
}
+
+ return ret;
}
toPublicMember() {
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 3598d29f..3bf3b9d0 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -16,12 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { User } from "./User";
-import { Member } from "./Member";
-import { Role } from "./Role";
-import { Channel } from "./Channel";
-import { InteractionType } from "../interfaces/Interaction";
-import { Application } from "./Application";
+import type { APAnnounce, APCreate, APNote } from "activitypub-types";
import {
Column,
CreateDateColumn,
@@ -34,11 +29,18 @@ import {
OneToMany,
RelationId,
} from "typeorm";
+import { Config } from "..";
+import { InteractionType } from "../interfaces/Interaction";
+import { Application } from "./Application";
+import { Attachment } from "./Attachment";
import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
import { Guild } from "./Guild";
-import { Webhook } from "./Webhook";
+import { Member } from "./Member";
+import { Role } from "./Role";
import { Sticker } from "./Sticker";
-import { Attachment } from "./Attachment";
+import { User } from "./User";
+import { Webhook } from "./Webhook";
export enum MessageType {
DEFAULT = 0,
@@ -218,6 +220,9 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true })
components?: MessageComponent[];
+ @Column({ nullable: true })
+ federatedId: string;
+
toJSON(): Message {
return {
...this,
@@ -225,6 +230,7 @@ export class Message extends BaseClass {
member_id: undefined,
webhook_id: undefined,
application_id: undefined,
+ federatedId: undefined,
nonce: this.nonce ?? undefined,
tts: this.tts ?? false,
@@ -240,6 +246,47 @@ export class Message extends BaseClass {
components: this.components ?? undefined,
};
}
+
+ toAnnounceAP(): APAnnounce {
+ const { webDomain } = Config.get().federation;
+
+ return {
+ id: `https://${webDomain}/fed/channel/${this.channel_id}/messages/${this.id}`,
+ type: "Announce",
+ actor: `https://${webDomain}/fed/user/${this.author_id}`,
+ published: this.timestamp,
+ to: ["https://www.w3.org/ns/activitystreams#Public"],
+ object: this.toAP(),
+ };
+ }
+
+ toCreateAP(): APCreate {
+ const { webDomain } = Config.get().federation;
+
+ return {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ type: "Create",
+ id: `https://${webDomain}/fed/channel/${this.channel_id}/messages/${this.id}`,
+ to: [],
+ actor: `https://${webDomain}/fed/user/${this.author_id}`,
+ object: this.toAP(),
+ };
+ }
+
+ // TODO: move to AP module
+ toAP(): APNote {
+ const { webDomain } = Config.get().federation;
+
+ return {
+ id: `https://${webDomain}/fed/messages/${this.id}`,
+ type: "Note",
+ published: this.timestamp,
+ url: `https://${webDomain}/fed/messages/${this.id}`,
+ attributedTo: `https://${webDomain}/fed/user/${this.author_id}`,
+ to: ["https://www.w3.org/ns/activitystreams#Public"],
+ content: this.content,
+ };
+ }
}
export interface MessageComponent {
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index c6582b00..1594093f 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import { APPerson } from "activitypub-types";
import { Request } from "express";
import {
Column,
@@ -35,6 +36,10 @@ import { SecurityKey } from "./SecurityKey";
import { Session } from "./Session";
import { UserSettings } from "./UserSettings";
+import crypto from "crypto";
+import { promisify } from "util";
+const generateKeyPair = promisify(crypto.generateKeyPair);
+
export enum PublicUserEnum {
username,
discriminator,
@@ -85,6 +90,16 @@ export interface UserPrivate extends Pick<User, PrivateUserKeys> {
locale: string;
}
+export interface APPersonButMore extends APPerson {
+ publicKey: {
+ id: string;
+ owner: string;
+ publicKeyPem: string;
+ };
+
+ discoverable: boolean;
+}
+
@Entity("users")
export class User extends BaseClass {
@Column()
@@ -231,6 +246,15 @@ export class User extends BaseClass {
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
security_keys: SecurityKey[];
+ @Column()
+ publicKey: string;
+
+ @Column({ select: false })
+ privateKey: string;
+
+ @Column({ nullable: true })
+ federatedId: string;
+
// TODO: I don't like this method?
validate() {
if (this.discriminator) {
@@ -271,6 +295,37 @@ export class User extends BaseClass {
return user as UserPrivate;
}
+ // TODO: move to AP module
+ toAP(): APPersonButMore {
+ const { webDomain } = Config.get().federation;
+
+ return {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ type: "Person",
+ id: `https://${webDomain}/fed/user/${this.id}`,
+ name: this.username,
+ preferredUsername: this.id,
+ summary: this.bio,
+ icon: this.avatar
+ ? [
+ `${Config.get().cdn.endpointPublic}/avatars/${
+ this.id
+ }/${this.avatar}`,
+ ]
+ : undefined,
+ discoverable: true,
+
+ inbox: `https://${webDomain}/fed/user/${this.id}/inbox`,
+ outbox: `https://${webDomain}/fed/user/${this.id}/outbox`,
+ followers: `https://${webDomain}/fed/user/${this.id}/followers`,
+ publicKey: {
+ id: `https://${webDomain}/fed/user/${this.id}#main-key`,
+ owner: `https://${webDomain}/fed/user/${this.id}`,
+ publicKeyPem: this.publicKey,
+ },
+ };
+ }
+
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
return await User.findOneOrFail({
where: { id: user_id },
@@ -362,6 +417,18 @@ export class User extends BaseClass {
locale: language,
});
+ const { publicKey, privateKey } = await generateKeyPair("rsa", {
+ modulusLength: 4096,
+ publicKeyEncoding: {
+ type: "spki",
+ format: "pem",
+ },
+ privateKeyEncoding: {
+ type: "pkcs8",
+ format: "pem",
+ },
+ });
+
const user = User.create({
username: username,
discriminator,
@@ -372,7 +439,9 @@ export class User extends BaseClass {
valid_tokens_since: new Date(),
},
extended_settings: "{}",
- settings: settings,
+ settings,
+ publicKey,
+ privateKey,
premium_since: Config.get().defaults.user.premium
? new Date()
diff --git a/src/util/schemas/MessageAcknowledgeSchema.ts b/src/util/schemas/MessageAcknowledgeSchema.ts
index 28cd9c79..726dc21b 100644
--- a/src/util/schemas/MessageAcknowledgeSchema.ts
+++ b/src/util/schemas/MessageAcknowledgeSchema.ts
@@ -19,4 +19,7 @@
export interface MessageAcknowledgeSchema {
manual?: boolean;
mention_count?: number;
+ flags?: number;
+ last_viewed?: number;
+ token?: unknown; // was null
}
diff --git a/src/util/schemas/responses/WebfingerResponse.ts b/src/util/schemas/responses/WebfingerResponse.ts
new file mode 100644
index 00000000..6b0ab0f9
--- /dev/null
+++ b/src/util/schemas/responses/WebfingerResponse.ts
@@ -0,0 +1,12 @@
+interface WebfingerLink {
+ rel: string;
+ type?: string;
+ href: string;
+ template?: string;
+}
+
+export interface WebfingerResponse {
+ subject: string;
+ aliases: string[];
+ links: WebfingerLink[];
+}
diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts
index d8b7fd57..66b9986b 100644
--- a/src/util/schemas/responses/index.ts
+++ b/src/util/schemas/responses/index.ts
@@ -28,7 +28,8 @@ export * from "./TypedResponses";
export * from "./UpdatesResponse";
export * from "./UserNoteResponse";
export * from "./UserProfileResponse";
-export * from "./UserRelationshipsResponse";
export * from "./UserRelationsResponse";
+export * from "./UserRelationshipsResponse";
export * from "./WebAuthnCreateResponse";
+export * from "./WebfingerResponse";
export * from "./WebhookCreateResponse";
diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts
index 01f4911a..76a529ed 100644
--- a/src/util/util/Event.ts
+++ b/src/util/util/Event.ts
@@ -17,9 +17,9 @@
*/
import { Channel } from "amqplib";
-import { RabbitMQ } from "./RabbitMQ";
-import EventEmitter from "events";
+import EventEmitter from "eventemitter2";
import { EVENT, Event } from "../interfaces";
+import { RabbitMQ } from "./RabbitMQ";
export const events = new EventEmitter();
export async function emitEvent(payload: Omit<Event, "created_at">) {
|