summary refs log tree commit diff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/config/Config.ts2
-rw-r--r--src/util/config/types/FederationConfiguration.ts5
-rw-r--r--src/util/entities/Channel.ts67
-rw-r--r--src/util/entities/Member.ts46
-rw-r--r--src/util/entities/Message.ts63
-rw-r--r--src/util/entities/User.ts71
-rw-r--r--src/util/schemas/MessageAcknowledgeSchema.ts3
-rw-r--r--src/util/schemas/responses/WebfingerResponse.ts12
-rw-r--r--src/util/schemas/responses/index.ts3
-rw-r--r--src/util/util/Event.ts4
10 files changed, 242 insertions, 34 deletions
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">) {