Added src/util files
This commit is contained in:
parent
13c00a9bbe
commit
99e27306d0
308 changed files with 12291 additions and 0 deletions
46
src/util/config/Config.ts
Normal file
46
src/util/config/Config.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import {
|
||||||
|
ApiConfiguration,
|
||||||
|
CdnConfiguration,
|
||||||
|
DefaultsConfiguration,
|
||||||
|
EmailConfiguration,
|
||||||
|
EndpointConfiguration,
|
||||||
|
ExternalTokensConfiguration,
|
||||||
|
GeneralConfiguration,
|
||||||
|
GifConfiguration,
|
||||||
|
GuildConfiguration,
|
||||||
|
KafkaConfiguration,
|
||||||
|
LimitsConfiguration,
|
||||||
|
LoginConfiguration,
|
||||||
|
MetricsConfiguration,
|
||||||
|
PasswordResetConfiguration,
|
||||||
|
RabbitMQConfiguration,
|
||||||
|
RegionConfiguration,
|
||||||
|
RegisterConfiguration,
|
||||||
|
SecurityConfiguration,
|
||||||
|
SentryConfiguration,
|
||||||
|
TemplateConfiguration,
|
||||||
|
} from "../config";
|
||||||
|
|
||||||
|
export class ConfigValue {
|
||||||
|
gateway: EndpointConfiguration = new EndpointConfiguration();
|
||||||
|
cdn: CdnConfiguration = new CdnConfiguration();
|
||||||
|
api: ApiConfiguration = new ApiConfiguration();
|
||||||
|
general: GeneralConfiguration = new GeneralConfiguration();
|
||||||
|
limits: LimitsConfiguration = new LimitsConfiguration();
|
||||||
|
security: SecurityConfiguration = new SecurityConfiguration();
|
||||||
|
login: LoginConfiguration = new LoginConfiguration();
|
||||||
|
register: RegisterConfiguration = new RegisterConfiguration();
|
||||||
|
regions: RegionConfiguration = new RegionConfiguration();
|
||||||
|
guild: GuildConfiguration = new GuildConfiguration();
|
||||||
|
gif: GifConfiguration = new GifConfiguration();
|
||||||
|
rabbitmq: RabbitMQConfiguration = new RabbitMQConfiguration();
|
||||||
|
kafka: KafkaConfiguration = new KafkaConfiguration();
|
||||||
|
templates: TemplateConfiguration = new TemplateConfiguration();
|
||||||
|
metrics: MetricsConfiguration = new MetricsConfiguration();
|
||||||
|
sentry: SentryConfiguration = new SentryConfiguration();
|
||||||
|
defaults: DefaultsConfiguration = new DefaultsConfiguration();
|
||||||
|
external: ExternalTokensConfiguration = new ExternalTokensConfiguration();
|
||||||
|
email: EmailConfiguration = new EmailConfiguration();
|
||||||
|
passwordReset: PasswordResetConfiguration =
|
||||||
|
new PasswordResetConfiguration();
|
||||||
|
}
|
2
src/util/config/index.ts
Normal file
2
src/util/config/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./Config";
|
||||||
|
export * from "./types";
|
5
src/util/config/types/ApiConfiguration.ts
Normal file
5
src/util/config/types/ApiConfiguration.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export class ApiConfiguration {
|
||||||
|
defaultVersion: string = "9";
|
||||||
|
activeVersions: string[] = ["6", "7", "8", "9"];
|
||||||
|
endpointPublic: string | null = null;
|
||||||
|
}
|
12
src/util/config/types/CdnConfiguration.ts
Normal file
12
src/util/config/types/CdnConfiguration.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { EndpointConfiguration } from "./EndpointConfiguration";
|
||||||
|
|
||||||
|
export class CdnConfiguration extends EndpointConfiguration {
|
||||||
|
resizeHeightMax: number = 1000;
|
||||||
|
resizeWidthMax: number = 1000;
|
||||||
|
imagorServerUrl: string | null = null;
|
||||||
|
|
||||||
|
endpointPublic: string | null = null;
|
||||||
|
endpointPrivate: string | null = null;
|
||||||
|
|
||||||
|
proxyCacheHeaderSeconds: number = 60 * 60 * 24;
|
||||||
|
}
|
6
src/util/config/types/DefaultsConfiguration.ts
Normal file
6
src/util/config/types/DefaultsConfiguration.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { GuildDefaults, UserDefaults } from ".";
|
||||||
|
|
||||||
|
export class DefaultsConfiguration {
|
||||||
|
guild: GuildDefaults = new GuildDefaults();
|
||||||
|
user: UserDefaults = new UserDefaults();
|
||||||
|
}
|
15
src/util/config/types/EmailConfiguration.ts
Normal file
15
src/util/config/types/EmailConfiguration.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {
|
||||||
|
MailGunConfiguration,
|
||||||
|
MailJetConfiguration,
|
||||||
|
SMTPConfiguration,
|
||||||
|
} from "./subconfigurations/email";
|
||||||
|
import { SendGridConfiguration } from "./subconfigurations/email/SendGrid";
|
||||||
|
|
||||||
|
export class EmailConfiguration {
|
||||||
|
provider: string | null = null;
|
||||||
|
senderAddress: string | null = null;
|
||||||
|
smtp: SMTPConfiguration = new SMTPConfiguration();
|
||||||
|
mailgun: MailGunConfiguration = new MailGunConfiguration();
|
||||||
|
mailjet: MailJetConfiguration = new MailJetConfiguration();
|
||||||
|
sendgrid: SendGridConfiguration = new SendGridConfiguration();
|
||||||
|
}
|
5
src/util/config/types/EndpointConfiguration.ts
Normal file
5
src/util/config/types/EndpointConfiguration.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export class EndpointConfiguration {
|
||||||
|
endpointClient: string | null = null;
|
||||||
|
endpointPrivate: string | null = null;
|
||||||
|
endpointPublic: string | null = null;
|
||||||
|
}
|
3
src/util/config/types/ExternalTokensConfiguration.ts
Normal file
3
src/util/config/types/ExternalTokensConfiguration.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export class ExternalTokensConfiguration {
|
||||||
|
twitter: string | null = null;
|
||||||
|
}
|
14
src/util/config/types/GeneralConfiguration.ts
Normal file
14
src/util/config/types/GeneralConfiguration.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Snowflake } from "@valkyrie/util";
|
||||||
|
|
||||||
|
export class GeneralConfiguration {
|
||||||
|
instanceName: string = "Valkyrie Instance";
|
||||||
|
instanceDescription: string | null =
|
||||||
|
"This is a ValkyrieChat instance made in the pre-release days";
|
||||||
|
frontPage: string | null = null;
|
||||||
|
tosPage: string | null = null;
|
||||||
|
correspondenceEmail: string | null = null;
|
||||||
|
correspondenceUserID: string | null = null;
|
||||||
|
image: string | null = null;
|
||||||
|
instanceId: string = Snowflake.generate();
|
||||||
|
autoCreateBotUsers: boolean = false;
|
||||||
|
}
|
5
src/util/config/types/GifConfiguration.ts
Normal file
5
src/util/config/types/GifConfiguration.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export class GifConfiguration {
|
||||||
|
enabled: boolean = true;
|
||||||
|
provider = "tenor" as const; // more coming soon
|
||||||
|
apiKey?: string = "LIVDSRZULELA";
|
||||||
|
}
|
7
src/util/config/types/GuildConfiguration.ts
Normal file
7
src/util/config/types/GuildConfiguration.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { DiscoveryConfiguration, AutoJoinConfiguration } from ".";
|
||||||
|
|
||||||
|
export class GuildConfiguration {
|
||||||
|
discovery: DiscoveryConfiguration = new DiscoveryConfiguration();
|
||||||
|
autoJoin: AutoJoinConfiguration = new AutoJoinConfiguration();
|
||||||
|
defaultFeatures: string[] = [];
|
||||||
|
}
|
5
src/util/config/types/KafkaConfiguration.ts
Normal file
5
src/util/config/types/KafkaConfiguration.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { KafkaBroker } from ".";
|
||||||
|
|
||||||
|
export class KafkaConfiguration {
|
||||||
|
brokers: KafkaBroker[] | null = null;
|
||||||
|
}
|
17
src/util/config/types/LimitConfigurations.ts
Normal file
17
src/util/config/types/LimitConfigurations.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {
|
||||||
|
ChannelLimits,
|
||||||
|
GlobalRateLimits,
|
||||||
|
GuildLimits,
|
||||||
|
MessageLimits,
|
||||||
|
RateLimits,
|
||||||
|
UserLimits,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
export class LimitsConfiguration {
|
||||||
|
user: UserLimits = new UserLimits();
|
||||||
|
guild: GuildLimits = new GuildLimits();
|
||||||
|
message: MessageLimits = new MessageLimits();
|
||||||
|
channel: ChannelLimits = new ChannelLimits();
|
||||||
|
rate: RateLimits = new RateLimits();
|
||||||
|
absoluteRate: GlobalRateLimits = new GlobalRateLimits();
|
||||||
|
}
|
4
src/util/config/types/LoginConfiguration.ts
Normal file
4
src/util/config/types/LoginConfiguration.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export class LoginConfiguration {
|
||||||
|
requireCaptcha: boolean = false;
|
||||||
|
requireVerification: boolean = false;
|
||||||
|
}
|
3
src/util/config/types/MetricsConfiguration.ts
Normal file
3
src/util/config/types/MetricsConfiguration.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export class MetricsConfiguration {
|
||||||
|
timeout: number = 30000;
|
||||||
|
}
|
3
src/util/config/types/PasswordResetConfiguration.ts
Normal file
3
src/util/config/types/PasswordResetConfiguration.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export class PasswordResetConfiguration {
|
||||||
|
requireCaptcha: boolean = false;
|
||||||
|
}
|
3
src/util/config/types/RabbitMQConfiguration.ts
Normal file
3
src/util/config/types/RabbitMQConfiguration.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export class RabbitMQConfiguration {
|
||||||
|
host: string | null = null;
|
||||||
|
}
|
16
src/util/config/types/RegionConfiguration.ts
Normal file
16
src/util/config/types/RegionConfiguration.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Region } from ".";
|
||||||
|
|
||||||
|
export class RegionConfiguration {
|
||||||
|
default: string = "valkyrie";
|
||||||
|
useDefaultAsOptimal: boolean = true;
|
||||||
|
available: Region[] = [
|
||||||
|
{
|
||||||
|
id: "valkyrie",
|
||||||
|
name: "valkyrie",
|
||||||
|
endpoint: "127.0.0.1:3004",
|
||||||
|
vip: false,
|
||||||
|
custom: false,
|
||||||
|
deprecated: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
21
src/util/config/types/RegisterConfiguration.ts
Normal file
21
src/util/config/types/RegisterConfiguration.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {
|
||||||
|
DateOfBirthConfiguration,
|
||||||
|
PasswordConfiguration,
|
||||||
|
RegistrationEmailConfiguration,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
export class RegisterConfiguration {
|
||||||
|
email: RegistrationEmailConfiguration =
|
||||||
|
new RegistrationEmailConfiguration();
|
||||||
|
dateOfBirth: DateOfBirthConfiguration = new DateOfBirthConfiguration();
|
||||||
|
password: PasswordConfiguration = new PasswordConfiguration();
|
||||||
|
disabled: boolean = false;
|
||||||
|
requireCaptcha: boolean = true;
|
||||||
|
requireInvite: boolean = false;
|
||||||
|
guestsRequireInvite: boolean = true;
|
||||||
|
allowNewRegistration: boolean = true;
|
||||||
|
allowMultipleAccounts: boolean = true;
|
||||||
|
blockProxies: boolean = true;
|
||||||
|
incrementingDiscriminators: boolean = false; // random otherwise
|
||||||
|
defaultRights: string = "875069521787904"; // See `npm run generate:rights`
|
||||||
|
}
|
19
src/util/config/types/SecurityConfiguration.ts
Normal file
19
src/util/config/types/SecurityConfiguration.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { CaptchaConfiguration, TwoFactorConfiguration } from ".";
|
||||||
|
|
||||||
|
export class SecurityConfiguration {
|
||||||
|
captcha: CaptchaConfiguration = new CaptchaConfiguration();
|
||||||
|
twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration();
|
||||||
|
autoUpdate: boolean | number = true;
|
||||||
|
requestSignature: string = crypto.randomBytes(32).toString("base64");
|
||||||
|
jwtSecret: string = crypto.randomBytes(256).toString("base64");
|
||||||
|
// header to get the real user ip address
|
||||||
|
// X-Forwarded-For for nginx/reverse proxies
|
||||||
|
// CF-Connecting-IP for cloudflare
|
||||||
|
forwardedFor: string | null = null;
|
||||||
|
ipdataApiKey: string | null =
|
||||||
|
"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9";
|
||||||
|
mfaBackupCodeCount: number = 10;
|
||||||
|
statsWorldReadable: boolean = true;
|
||||||
|
defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week
|
||||||
|
}
|
9
src/util/config/types/SentryConfiguration.ts
Normal file
9
src/util/config/types/SentryConfiguration.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { hostname } from "os";
|
||||||
|
|
||||||
|
export class SentryConfiguration {
|
||||||
|
enabled: boolean = false;
|
||||||
|
endpoint: string =
|
||||||
|
"https://ac1113c8cf9bf615a6e6b5a729d61bfa@o565598.ingest.us.sentry.io/4507888903520256";
|
||||||
|
traceSampleRate: number = 1.0;
|
||||||
|
environment: string = hostname();
|
||||||
|
}
|
6
src/util/config/types/TemplateConfiguration.ts
Normal file
6
src/util/config/types/TemplateConfiguration.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export class TemplateConfiguration {
|
||||||
|
enabled: boolean = true;
|
||||||
|
allowTemplateCreation: boolean = true;
|
||||||
|
allowDiscordTemplates: boolean = true;
|
||||||
|
allowRaws: boolean = true;
|
||||||
|
}
|
21
src/util/config/types/index.ts
Normal file
21
src/util/config/types/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export * from "./ApiConfiguration";
|
||||||
|
export * from "./CdnConfiguration";
|
||||||
|
export * from "./DefaultsConfiguration";
|
||||||
|
export * from "./EmailConfiguration";
|
||||||
|
export * from "./EndpointConfiguration";
|
||||||
|
export * from "./ExternalTokensConfiguration";
|
||||||
|
export * from "./GeneralConfiguration";
|
||||||
|
export * from "./GifConfiguration";
|
||||||
|
export * from "./GuildConfiguration";
|
||||||
|
export * from "./KafkaConfiguration";
|
||||||
|
export * from "./LimitConfigurations";
|
||||||
|
export * from "./LoginConfiguration";
|
||||||
|
export * from "./MetricsConfiguration";
|
||||||
|
export * from "./PasswordResetConfiguration";
|
||||||
|
export * from "./RabbitMQConfiguration";
|
||||||
|
export * from "./RegionConfiguration";
|
||||||
|
export * from "./RegisterConfiguration";
|
||||||
|
export * from "./SecurityConfiguration";
|
||||||
|
export * from "./SentryConfiguration";
|
||||||
|
export * from "./subconfigurations";
|
||||||
|
export * from "./TemplateConfiguration";
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class ClientReleaseConfiguration {
|
||||||
|
useLocalRelease: boolean = true; //TODO
|
||||||
|
upstreamVersion: string = "0.0.264";
|
||||||
|
}
|
1
src/util/config/types/subconfigurations/client/index.ts
Normal file
1
src/util/config/types/subconfigurations/client/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./ClientReleaseConfiguration";
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class GuildDefaults {
|
||||||
|
maxPresences: number = 250000;
|
||||||
|
maxVideoChannelUsers: number = 200;
|
||||||
|
afkTimeout: number = 300;
|
||||||
|
defaultMessageNotifications: number = 1;
|
||||||
|
explicitContentFilter: number = 0;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class UserDefaults {
|
||||||
|
premium: boolean = true;
|
||||||
|
premiumType: number = 2;
|
||||||
|
verified: boolean = true;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./GuildDefaults";
|
||||||
|
export * from "./UserDefaults";
|
4
src/util/config/types/subconfigurations/email/MailGun.ts
Normal file
4
src/util/config/types/subconfigurations/email/MailGun.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export class MailGunConfiguration {
|
||||||
|
apiKey: string | null = null;
|
||||||
|
domain: string | null = null;
|
||||||
|
}
|
4
src/util/config/types/subconfigurations/email/MailJet.ts
Normal file
4
src/util/config/types/subconfigurations/email/MailJet.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export class MailJetConfiguration {
|
||||||
|
apiKey: string | null = null;
|
||||||
|
apiSecret: string | null = null;
|
||||||
|
}
|
7
src/util/config/types/subconfigurations/email/SMTP.ts
Normal file
7
src/util/config/types/subconfigurations/email/SMTP.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export class SMTPConfiguration {
|
||||||
|
host: string | null = null;
|
||||||
|
port: number | null = null;
|
||||||
|
secure: boolean | null = null;
|
||||||
|
username: string | null = null;
|
||||||
|
password: string | null = null;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class SendGridConfiguration {
|
||||||
|
apiKey: string | null = null;
|
||||||
|
}
|
3
src/util/config/types/subconfigurations/email/index.ts
Normal file
3
src/util/config/types/subconfigurations/email/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./MailGun";
|
||||||
|
export * from "./MailJet";
|
||||||
|
export * from "./SMTP";
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class AutoJoinConfiguration {
|
||||||
|
enabled: boolean = true;
|
||||||
|
guilds: string[] = [];
|
||||||
|
canLeave: boolean = true;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class DiscoveryConfiguration {
|
||||||
|
showAllGuilds: boolean = false;
|
||||||
|
useRecommendation: boolean = false; // TODO: Recommendation, privacy concern?
|
||||||
|
offset: number = 0;
|
||||||
|
limit: number = 24;
|
||||||
|
}
|
2
src/util/config/types/subconfigurations/guild/index.ts
Normal file
2
src/util/config/types/subconfigurations/guild/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./AutoJoin";
|
||||||
|
export * from "./Discovery";
|
8
src/util/config/types/subconfigurations/index.ts
Normal file
8
src/util/config/types/subconfigurations/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export * from "./client";
|
||||||
|
export * from "./defaults";
|
||||||
|
export * from "./guild";
|
||||||
|
export * from "./kafka";
|
||||||
|
export * from "./limits";
|
||||||
|
export * from "./region";
|
||||||
|
export * from "./register";
|
||||||
|
export * from "./security";
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface KafkaBroker {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}
|
1
src/util/config/types/subconfigurations/kafka/index.ts
Normal file
1
src/util/config/types/subconfigurations/kafka/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./KafkaBroker";
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class ChannelLimits {
|
||||||
|
maxPins: number = 500;
|
||||||
|
maxTopic: number = 1024;
|
||||||
|
maxWebhooks: number = 100;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
export class GlobalRateLimits {
|
||||||
|
register: GlobalRateLimit = {
|
||||||
|
limit: 25,
|
||||||
|
window: 60 * 60 * 1000,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
sendMessage: GlobalRateLimit = {
|
||||||
|
limit: 200,
|
||||||
|
window: 60 * 1000,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GlobalRateLimit {
|
||||||
|
limit: number = 100;
|
||||||
|
window: number = 60 * 60 * 1000;
|
||||||
|
enabled: boolean = true;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
export class GuildLimits {
|
||||||
|
maxRoles: number = 1000;
|
||||||
|
maxEmojis: number = 2000;
|
||||||
|
maxMembers: number = 25000000;
|
||||||
|
maxChannels: number = 65535;
|
||||||
|
maxBulkBanUsers: number = 200;
|
||||||
|
maxChannelsInCategory: number = 65535;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
export class MessageLimits {
|
||||||
|
maxCharacters: number = 1048576;
|
||||||
|
maxTTSCharacters: number = 160;
|
||||||
|
maxReactions: number = 2048;
|
||||||
|
maxAttachmentSize: number = 1024 * 1024 * 1024;
|
||||||
|
maxBulkDelete: number = 1000;
|
||||||
|
maxEmbedDownloadSize: number = 1024 * 1024 * 5;
|
||||||
|
}
|
18
src/util/config/types/subconfigurations/limits/RateLimits.ts
Normal file
18
src/util/config/types/subconfigurations/limits/RateLimits.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { RateLimitOptions, RouteRateLimit } from ".";
|
||||||
|
|
||||||
|
export class RateLimits {
|
||||||
|
enabled: boolean = false;
|
||||||
|
ip: RateLimitOptions = {
|
||||||
|
count: 500,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
global: RateLimitOptions = {
|
||||||
|
count: 250,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
error: RateLimitOptions = {
|
||||||
|
count: 10,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
routes: RouteRateLimit = new RouteRateLimit();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class UserLimits {
|
||||||
|
maxGuilds: number = 1048576;
|
||||||
|
maxUsername: number = 32;
|
||||||
|
maxFriends: number = 5000;
|
||||||
|
maxBio: number = 190;
|
||||||
|
}
|
7
src/util/config/types/subconfigurations/limits/index.ts
Normal file
7
src/util/config/types/subconfigurations/limits/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export * from "./ChannelLimits";
|
||||||
|
export * from "./GlobalRateLimits";
|
||||||
|
export * from "./GuildLimits";
|
||||||
|
export * from "./MessageLimits";
|
||||||
|
export * from "./RateLimits";
|
||||||
|
export * from "./UserLimits";
|
||||||
|
export * from "./ratelimits/index";
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { RateLimitOptions } from "./RateLimitOptions";
|
||||||
|
|
||||||
|
export class AuthRateLimit {
|
||||||
|
login: RateLimitOptions = {
|
||||||
|
count: 5,
|
||||||
|
window: 60,
|
||||||
|
};
|
||||||
|
register: RateLimitOptions = {
|
||||||
|
count: 2,
|
||||||
|
window: 60 * 60 * 12,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface RateLimitOptions {
|
||||||
|
bot?: number;
|
||||||
|
count: number;
|
||||||
|
window: number;
|
||||||
|
onyIp?: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { AuthRateLimit } from ".";
|
||||||
|
import { RateLimitOptions } from "./RateLimitOptions";
|
||||||
|
|
||||||
|
export class RouteRateLimit {
|
||||||
|
guild: RateLimitOptions = {
|
||||||
|
count: 5,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
webhook: RateLimitOptions = {
|
||||||
|
count: 10,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
channel: RateLimitOptions = {
|
||||||
|
count: 10,
|
||||||
|
window: 5,
|
||||||
|
};
|
||||||
|
auth: AuthRateLimit = new AuthRateLimit();
|
||||||
|
// TODO: rate limit configuration for all routes
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./Auth";
|
||||||
|
export * from "./RateLimitOptions";
|
||||||
|
export * from "./Route";
|
12
src/util/config/types/subconfigurations/region/Region.ts
Normal file
12
src/util/config/types/subconfigurations/region/Region.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export interface Region {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
endpoint: string;
|
||||||
|
location?: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
vip: boolean;
|
||||||
|
custom: boolean;
|
||||||
|
deprecated: boolean;
|
||||||
|
}
|
1
src/util/config/types/subconfigurations/region/index.ts
Normal file
1
src/util/config/types/subconfigurations/region/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./Region";
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class DateOfBirthConfiguration {
|
||||||
|
required: boolean = true;
|
||||||
|
minimum: number = 13; // in years
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class RegistrationEmailConfiguration {
|
||||||
|
required: boolean = false;
|
||||||
|
allowlist: boolean = false;
|
||||||
|
blocklist: boolean = true;
|
||||||
|
domains: string[] = []; // TODO: efficiently save domain blocklist in database
|
||||||
|
// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class PasswordConfiguration {
|
||||||
|
required: boolean = false;
|
||||||
|
minLength: number = 8;
|
||||||
|
minNumbers: number = 2;
|
||||||
|
minUpperCase: number = 2;
|
||||||
|
minSymbols: number = 0;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./DateOfBirth";
|
||||||
|
export * from "./Email";
|
||||||
|
export * from "./Password";
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class CaptchaConfiguration {
|
||||||
|
enabled: boolean = false;
|
||||||
|
service: "recaptcha" | "hcaptcha" | null = null; // TODO: hcaptcha, custom
|
||||||
|
sitekey: string | null = null;
|
||||||
|
secret: string | null = null;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class TwoFactorConfiguration {
|
||||||
|
generateBackupCodes: boolean = true;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./Captcha";
|
||||||
|
export * from "./TwoFactor";
|
100
src/util/connections/Connection.ts
Normal file
100
src/util/connections/Connection.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas";
|
||||||
|
import { Config, DiscordApiErrors } from "../util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection that can be used to connect to an external service.
|
||||||
|
*/
|
||||||
|
export abstract class Connection {
|
||||||
|
id: string;
|
||||||
|
settings: { enabled: boolean };
|
||||||
|
states: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
abstract init(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an authorization url for the connection.
|
||||||
|
* @param args
|
||||||
|
*/
|
||||||
|
abstract getAuthorizationUrl(userId: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the redirect_uri for a connection type
|
||||||
|
* @returns redirect_uri for this connection
|
||||||
|
*/
|
||||||
|
getRedirectUri() {
|
||||||
|
const endpointPublic =
|
||||||
|
Config.get().general.frontPage ?? "http://localhost:3001";
|
||||||
|
return `${endpointPublic}/connections/${this.id}/callback`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the callback
|
||||||
|
* @param args Callback arguments
|
||||||
|
*/
|
||||||
|
abstract handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a user id from state
|
||||||
|
* @param state the state to get the user id from
|
||||||
|
* @returns the user id associated with the state
|
||||||
|
*/
|
||||||
|
getUserId(state: string): string {
|
||||||
|
if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE;
|
||||||
|
return this.states.get(state) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a state
|
||||||
|
* @param user_id The user id to generate a state for.
|
||||||
|
* @returns a new state
|
||||||
|
*/
|
||||||
|
createState(userId: string): string {
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
this.states.set(state, userId);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a state and checks if it is valid, and deletes it.
|
||||||
|
* @param state The state to check.
|
||||||
|
*/
|
||||||
|
validateState(state: string): void {
|
||||||
|
if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE;
|
||||||
|
this.states.delete(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Connected Account in the database.
|
||||||
|
* @param data connected account data
|
||||||
|
* @returns the new connected account
|
||||||
|
*/
|
||||||
|
async createConnection(
|
||||||
|
data: ConnectedAccountSchema,
|
||||||
|
): Promise<ConnectedAccount> {
|
||||||
|
const ca = ConnectedAccount.create({ ...data });
|
||||||
|
await ca.save();
|
||||||
|
return ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user has an exist connected account for the given extenal id.
|
||||||
|
* @param userId the user id
|
||||||
|
* @param externalId the connection id to find
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async hasConnection(userId: string, externalId: string): Promise<boolean> {
|
||||||
|
const existing = await ConnectedAccount.findOne({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
external_id: externalId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!existing;
|
||||||
|
}
|
||||||
|
}
|
80
src/util/connections/ConnectionConfig.ts
Normal file
80
src/util/connections/ConnectionConfig.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { ConnectionConfigEntity } from "../entities/ConnectionConfigEntity";
|
||||||
|
|
||||||
|
let config: any;
|
||||||
|
let pairs: ConnectionConfigEntity[];
|
||||||
|
|
||||||
|
export const ConnectionConfig = {
|
||||||
|
init: async function init() {
|
||||||
|
if (config) return config;
|
||||||
|
console.log("[Connections] Loading configuration...");
|
||||||
|
pairs = await ConnectionConfigEntity.find();
|
||||||
|
config = pairsToConfig(pairs);
|
||||||
|
|
||||||
|
return this.set(config);
|
||||||
|
},
|
||||||
|
get: function get() {
|
||||||
|
if (!config) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
set: function set(val: Partial<any>) {
|
||||||
|
if (!config || !val) return;
|
||||||
|
config = val.merge(config);
|
||||||
|
|
||||||
|
// return applyConfig(config);
|
||||||
|
return applyConfig(val);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyConfig(val: any) {
|
||||||
|
async function apply(obj: any, key = ""): Promise<any> {
|
||||||
|
if (typeof obj === "object" && obj !== null && !(obj instanceof Date))
|
||||||
|
return Promise.all(
|
||||||
|
Object.keys(obj).map((k) =>
|
||||||
|
apply(obj[k], key ? `${key}_${k}` : k),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pair = pairs.find((x) => x.key === key);
|
||||||
|
if (!pair) pair = new ConnectionConfigEntity();
|
||||||
|
|
||||||
|
pair.key = key;
|
||||||
|
|
||||||
|
if (pair.value !== obj) {
|
||||||
|
pair.value = obj;
|
||||||
|
if (!pair.key || pair.key == null) {
|
||||||
|
console.log(`[Connections] WARN: Empty config key`);
|
||||||
|
console.log(pair);
|
||||||
|
} else return pair.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pairsToConfig(pairs: ConnectionConfigEntity[]) {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
pairs.forEach((p) => {
|
||||||
|
const keys = p.key.split("_");
|
||||||
|
let obj = value;
|
||||||
|
let prev = "";
|
||||||
|
let prevObj = obj;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!isNaN(Number(key)) && !prevObj[prev]?.length)
|
||||||
|
prevObj[prev] = obj = [];
|
||||||
|
if (i++ === keys.length - 1) obj[key] = p.value;
|
||||||
|
else if (!obj[key]) obj[key] = {};
|
||||||
|
|
||||||
|
prev = key;
|
||||||
|
prevObj = obj;
|
||||||
|
obj = obj[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
66
src/util/connections/ConnectionLoader.ts
Normal file
66
src/util/connections/ConnectionLoader.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { Connection } from "@valkyrie/util";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { ConnectionConfig } from "./ConnectionConfig";
|
||||||
|
import { ConnectionStore } from "./ConnectionStore";
|
||||||
|
|
||||||
|
const root = path.join(__dirname, "..", "..", "connections");
|
||||||
|
const connectionsLoaded = false;
|
||||||
|
|
||||||
|
export class ConnectionLoader {
|
||||||
|
public static async loadConnections() {
|
||||||
|
if (connectionsLoaded) return;
|
||||||
|
ConnectionConfig.init();
|
||||||
|
const dirs = fs.readdirSync(root).filter((x) => {
|
||||||
|
try {
|
||||||
|
fs.readdirSync(path.join(root, x));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dirs.forEach(async (x) => {
|
||||||
|
const modPath = path.resolve(path.join(root, x));
|
||||||
|
const mod = new (require(modPath).default)() as Connection;
|
||||||
|
ConnectionStore.connections.set(mod.id, mod);
|
||||||
|
|
||||||
|
mod.init();
|
||||||
|
// console.log(`[Connections] Loaded connection '${mod.id}'`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getConnectionConfig<T>(id: string, defaults?: unknown): T {
|
||||||
|
let cfg = ConnectionConfig.get()[id];
|
||||||
|
if (defaults) {
|
||||||
|
if (cfg) cfg = Object.assign({}, defaults, cfg);
|
||||||
|
else {
|
||||||
|
cfg = defaults;
|
||||||
|
this.setConnectionConfig(id, cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg?.enabled) console.log(`[Connections] ${id} enabled`);
|
||||||
|
|
||||||
|
// if (!cfg)
|
||||||
|
// console.log(
|
||||||
|
// `[ConnectionConfig/WARN] Getting connection settings for '${id}' returned null! (Did you forget to add settings?)`,
|
||||||
|
// );
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async setConnectionConfig(
|
||||||
|
id: string,
|
||||||
|
config: Partial<unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!config)
|
||||||
|
console.warn(`[Connections/WARN] ${id} tried to set config=null!`);
|
||||||
|
|
||||||
|
await ConnectionConfig.set({
|
||||||
|
[id]: Object.assign(
|
||||||
|
config,
|
||||||
|
ConnectionLoader.getConnectionConfig(id) || {},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
7
src/util/connections/ConnectionStore.ts
Normal file
7
src/util/connections/ConnectionStore.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Connection } from "./Connection";
|
||||||
|
import { RefreshableConnection } from "./RefreshableConnection";
|
||||||
|
|
||||||
|
export class ConnectionStore {
|
||||||
|
public static connections: Map<string, Connection | RefreshableConnection> =
|
||||||
|
new Map();
|
||||||
|
}
|
31
src/util/connections/RefreshableConnection.ts
Normal file
31
src/util/connections/RefreshableConnection.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces";
|
||||||
|
import { Connection } from "./Connection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection that can refresh its token.
|
||||||
|
*/
|
||||||
|
export abstract class RefreshableConnection extends Connection {
|
||||||
|
refreshEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the token for a connected account.
|
||||||
|
* @param connectedAccount The connected account to refresh
|
||||||
|
*/
|
||||||
|
abstract refreshToken(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the token for a connected account and saves it to the database.
|
||||||
|
* @param connectedAccount The connected account to refresh
|
||||||
|
*/
|
||||||
|
async refresh(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
const tokenData = await this.refreshToken(connectedAccount);
|
||||||
|
connectedAccount.token_data = { ...tokenData, fetched_at: Date.now() };
|
||||||
|
await connectedAccount.save();
|
||||||
|
return tokenData;
|
||||||
|
}
|
||||||
|
}
|
5
src/util/connections/index.ts
Normal file
5
src/util/connections/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from "./Connection";
|
||||||
|
export * from "./ConnectionConfig";
|
||||||
|
export * from "./ConnectionLoader";
|
||||||
|
export * from "./ConnectionStore";
|
||||||
|
export * from "./RefreshableConnection";
|
43
src/util/dtos/ConnectedAccountDTO.ts
Normal file
43
src/util/dtos/ConnectedAccountDTO.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
|
||||||
|
export class ConnectedAccountDTO {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
access_token?: string;
|
||||||
|
friend_sync?: boolean;
|
||||||
|
name: string;
|
||||||
|
revoked?: boolean;
|
||||||
|
show_activity?: number;
|
||||||
|
type: string;
|
||||||
|
verified?: boolean;
|
||||||
|
visibility?: number;
|
||||||
|
integrations?: string[];
|
||||||
|
metadata_?: unknown;
|
||||||
|
metadata_visibility?: number;
|
||||||
|
two_way_link?: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
with_token: boolean = false,
|
||||||
|
) {
|
||||||
|
this.id = connectedAccount.external_id;
|
||||||
|
this.user_id = connectedAccount.user_id;
|
||||||
|
this.access_token =
|
||||||
|
connectedAccount.token_data && with_token
|
||||||
|
? connectedAccount.token_data.access_token
|
||||||
|
: undefined;
|
||||||
|
this.friend_sync = connectedAccount.friend_sync;
|
||||||
|
this.name = connectedAccount.name;
|
||||||
|
this.revoked = connectedAccount.revoked;
|
||||||
|
this.show_activity = connectedAccount.show_activity;
|
||||||
|
this.type = connectedAccount.type;
|
||||||
|
this.verified = connectedAccount.verified;
|
||||||
|
this.visibility = +(connectedAccount.visibility || false);
|
||||||
|
this.integrations = connectedAccount.integrations;
|
||||||
|
this.metadata_ = connectedAccount.metadata_;
|
||||||
|
this.metadata_visibility = +(
|
||||||
|
connectedAccount.metadata_visibility || false
|
||||||
|
);
|
||||||
|
this.two_way_link = connectedAccount.two_way_link;
|
||||||
|
}
|
||||||
|
}
|
50
src/util/dtos/DmChannelDTO.ts
Normal file
50
src/util/dtos/DmChannelDTO.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { MinimalPublicUserDTO } from "./UserDTO";
|
||||||
|
import { Channel, PublicUserProjection, User } from "../entities";
|
||||||
|
|
||||||
|
export class DmChannelDTO {
|
||||||
|
icon: string | null;
|
||||||
|
id: string;
|
||||||
|
last_message_id: string | null;
|
||||||
|
name: string | null;
|
||||||
|
origin_channel_id: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
recipients: MinimalPublicUserDTO[];
|
||||||
|
type: number;
|
||||||
|
|
||||||
|
static async from(
|
||||||
|
channel: Channel,
|
||||||
|
excluded_recipients: string[] = [],
|
||||||
|
origin_channel_id?: string,
|
||||||
|
) {
|
||||||
|
const obj = new DmChannelDTO();
|
||||||
|
obj.icon = channel.icon || null;
|
||||||
|
obj.id = channel.id;
|
||||||
|
obj.last_message_id = channel.last_message_id || null;
|
||||||
|
obj.name = channel.name || null;
|
||||||
|
obj.origin_channel_id = origin_channel_id || null;
|
||||||
|
obj.owner_id = channel.owner_id;
|
||||||
|
obj.type = channel.type;
|
||||||
|
obj.recipients = (
|
||||||
|
await Promise.all(
|
||||||
|
channel.recipients
|
||||||
|
?.filter((r) => !excluded_recipients.includes(r.user_id))
|
||||||
|
.map(async (r) => {
|
||||||
|
return await User.findOneOrFail({
|
||||||
|
where: { id: r.user_id },
|
||||||
|
select: PublicUserProjection,
|
||||||
|
});
|
||||||
|
}) || [],
|
||||||
|
)
|
||||||
|
).map((u) => new MinimalPublicUserDTO(u));
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
excludedRecipients(excluded_recipients: string[]): DmChannelDTO {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
recipients: this.recipients.filter(
|
||||||
|
(r) => !excluded_recipients.includes(r.id),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
226
src/util/dtos/ReadyGuildDTO.ts
Normal file
226
src/util/dtos/ReadyGuildDTO.ts
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import {
|
||||||
|
Channel,
|
||||||
|
ChannelOverride,
|
||||||
|
ChannelType,
|
||||||
|
Emoji,
|
||||||
|
Guild,
|
||||||
|
PublicUser,
|
||||||
|
Role,
|
||||||
|
Sticker,
|
||||||
|
UserGuildSettings,
|
||||||
|
PublicMember,
|
||||||
|
} from "../entities";
|
||||||
|
|
||||||
|
// TODO: this is not the best place for this type
|
||||||
|
export type ReadyUserGuildSettingsEntries = Omit<
|
||||||
|
UserGuildSettings,
|
||||||
|
"channel_overrides"
|
||||||
|
> & {
|
||||||
|
channel_overrides: (ChannelOverride & { channel_id: string })[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: probably should move somewhere else
|
||||||
|
export interface ReadyPrivateChannel {
|
||||||
|
id: string;
|
||||||
|
flags: number;
|
||||||
|
is_spam: boolean;
|
||||||
|
last_message_id?: string;
|
||||||
|
recipients: PublicUser[];
|
||||||
|
type: ChannelType.DM | ChannelType.GROUP_DM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuildOrUnavailable =
|
||||||
|
| { id: string; unavailable: boolean }
|
||||||
|
| (Guild & { joined_at?: Date; unavailable: undefined });
|
||||||
|
|
||||||
|
const guildIsAvailable = (
|
||||||
|
guild: GuildOrUnavailable,
|
||||||
|
): guild is Guild & { joined_at: Date; unavailable: false } => {
|
||||||
|
return guild.unavailable != true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IReadyGuildDTO {
|
||||||
|
application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
|
||||||
|
channels: Channel[];
|
||||||
|
data_mode: string; // what is this
|
||||||
|
emojis: Emoji[];
|
||||||
|
guild_scheduled_events: unknown[]; // TODO
|
||||||
|
id: string;
|
||||||
|
large: boolean | undefined;
|
||||||
|
lazy: boolean;
|
||||||
|
member_count: number | undefined;
|
||||||
|
members: PublicMember[];
|
||||||
|
premium_subscription_count: number | undefined;
|
||||||
|
properties: {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
icon?: string | null;
|
||||||
|
splash?: string | null;
|
||||||
|
banner?: string | null;
|
||||||
|
features: string[];
|
||||||
|
preferred_locale?: string | null;
|
||||||
|
owner_id?: string | null;
|
||||||
|
application_id?: string | null;
|
||||||
|
afk_channel_id?: string | null;
|
||||||
|
afk_timeout: number | undefined;
|
||||||
|
system_channel_id?: string | null;
|
||||||
|
verification_level: number | undefined;
|
||||||
|
explicit_content_filter: number | undefined;
|
||||||
|
default_message_notifications: number | undefined;
|
||||||
|
mfa_level: number | undefined;
|
||||||
|
vanity_url_code?: string | null;
|
||||||
|
premium_tier: number | undefined;
|
||||||
|
premium_progress_bar_enabled: boolean;
|
||||||
|
system_channel_flags: number | undefined;
|
||||||
|
discovery_splash?: string | null;
|
||||||
|
rules_channel_id?: string | null;
|
||||||
|
public_updates_channel_id?: string | null;
|
||||||
|
max_video_channel_users: number | undefined;
|
||||||
|
max_members: number | undefined;
|
||||||
|
nsfw_level: number | undefined;
|
||||||
|
hub_type?: unknown | null; // ????
|
||||||
|
|
||||||
|
home_header: null; // TODO
|
||||||
|
latest_onboarding_question_id: null; // TODO
|
||||||
|
safety_alerts_channel_id: null; // TODO
|
||||||
|
max_stage_video_channel_users: 50; // TODO
|
||||||
|
nsfw: boolean;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
roles: Role[];
|
||||||
|
stage_instances: unknown[];
|
||||||
|
stickers: Sticker[];
|
||||||
|
threads: unknown[];
|
||||||
|
version: string;
|
||||||
|
guild_hashes: unknown;
|
||||||
|
unavailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadyGuildDTO implements IReadyGuildDTO {
|
||||||
|
application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
|
||||||
|
channels: Channel[];
|
||||||
|
data_mode: string; // what is this
|
||||||
|
emojis: Emoji[];
|
||||||
|
guild_scheduled_events: unknown[];
|
||||||
|
id: string;
|
||||||
|
large: boolean | undefined;
|
||||||
|
lazy: boolean;
|
||||||
|
member_count: number | undefined;
|
||||||
|
members: PublicMember[];
|
||||||
|
premium_subscription_count: number | undefined;
|
||||||
|
properties: {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
icon?: string | null;
|
||||||
|
splash?: string | null;
|
||||||
|
banner?: string | null;
|
||||||
|
features: string[];
|
||||||
|
preferred_locale?: string | null;
|
||||||
|
owner_id?: string | null;
|
||||||
|
application_id?: string | null;
|
||||||
|
afk_channel_id?: string | null;
|
||||||
|
afk_timeout: number | undefined;
|
||||||
|
system_channel_id?: string | null;
|
||||||
|
verification_level: number | undefined;
|
||||||
|
explicit_content_filter: number | undefined;
|
||||||
|
default_message_notifications: number | undefined;
|
||||||
|
mfa_level: number | undefined;
|
||||||
|
vanity_url_code?: string | null;
|
||||||
|
premium_tier: number | undefined;
|
||||||
|
premium_progress_bar_enabled: boolean;
|
||||||
|
system_channel_flags: number | undefined;
|
||||||
|
discovery_splash?: string | null;
|
||||||
|
rules_channel_id?: string | null;
|
||||||
|
public_updates_channel_id?: string | null;
|
||||||
|
max_video_channel_users: number | undefined;
|
||||||
|
max_members: number | undefined;
|
||||||
|
nsfw_level: number | undefined;
|
||||||
|
hub_type?: unknown | null; // ????
|
||||||
|
|
||||||
|
home_header: null; // TODO
|
||||||
|
latest_onboarding_question_id: null; // TODO
|
||||||
|
safety_alerts_channel_id: null; // TODO
|
||||||
|
max_stage_video_channel_users: 50; // TODO
|
||||||
|
nsfw: boolean;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
roles: Role[];
|
||||||
|
stage_instances: unknown[];
|
||||||
|
stickers: Sticker[];
|
||||||
|
threads: unknown[];
|
||||||
|
version: string;
|
||||||
|
guild_hashes: unknown;
|
||||||
|
unavailable: boolean;
|
||||||
|
joined_at: Date;
|
||||||
|
|
||||||
|
constructor(guild: GuildOrUnavailable) {
|
||||||
|
if (!guildIsAvailable(guild)) {
|
||||||
|
this.id = guild.id;
|
||||||
|
this.unavailable = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.application_command_counts = {
|
||||||
|
1: 5,
|
||||||
|
2: 2,
|
||||||
|
3: 2,
|
||||||
|
}; // ?????
|
||||||
|
this.channels = guild.channels;
|
||||||
|
this.data_mode = "full";
|
||||||
|
this.emojis = guild.emojis;
|
||||||
|
this.guild_scheduled_events = [];
|
||||||
|
this.id = guild.id;
|
||||||
|
this.large = guild.large;
|
||||||
|
this.lazy = true; // ??????????
|
||||||
|
this.member_count = guild.member_count;
|
||||||
|
this.members = guild.members?.map((x) => x.toPublicMember());
|
||||||
|
this.premium_subscription_count = guild.premium_subscription_count;
|
||||||
|
this.properties = {
|
||||||
|
name: guild.name,
|
||||||
|
description: guild.description,
|
||||||
|
icon: guild.icon,
|
||||||
|
splash: guild.splash,
|
||||||
|
banner: guild.banner,
|
||||||
|
features: guild.features,
|
||||||
|
preferred_locale: guild.preferred_locale,
|
||||||
|
owner_id: guild.owner_id,
|
||||||
|
application_id: null, // ?????
|
||||||
|
afk_channel_id: guild.afk_channel_id,
|
||||||
|
afk_timeout: guild.afk_timeout,
|
||||||
|
system_channel_id: guild.system_channel_id,
|
||||||
|
verification_level: guild.verification_level,
|
||||||
|
explicit_content_filter: guild.explicit_content_filter,
|
||||||
|
default_message_notifications: guild.default_message_notifications,
|
||||||
|
mfa_level: guild.mfa_level,
|
||||||
|
vanity_url_code: null, // ?????
|
||||||
|
premium_tier: guild.premium_tier,
|
||||||
|
premium_progress_bar_enabled: guild.premium_progress_bar_enabled,
|
||||||
|
system_channel_flags: guild.system_channel_flags,
|
||||||
|
discovery_splash: guild.discovery_splash,
|
||||||
|
rules_channel_id: guild.rules_channel_id,
|
||||||
|
public_updates_channel_id: guild.public_updates_channel_id,
|
||||||
|
max_video_channel_users: guild.max_video_channel_users,
|
||||||
|
max_members: guild.max_members,
|
||||||
|
nsfw_level: guild.nsfw_level,
|
||||||
|
hub_type: null,
|
||||||
|
|
||||||
|
home_header: null,
|
||||||
|
id: guild.id,
|
||||||
|
latest_onboarding_question_id: null,
|
||||||
|
max_stage_video_channel_users: 50, // TODO
|
||||||
|
nsfw: guild.nsfw,
|
||||||
|
safety_alerts_channel_id: null,
|
||||||
|
};
|
||||||
|
this.roles = guild.roles.map((x) => x.toJSON());
|
||||||
|
this.stage_instances = [];
|
||||||
|
this.stickers = guild.stickers;
|
||||||
|
this.threads = [];
|
||||||
|
this.version = "1"; // ??????
|
||||||
|
this.guild_hashes = {};
|
||||||
|
this.joined_at = guild.joined_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return this as IReadyGuildDTO;
|
||||||
|
}
|
||||||
|
}
|
19
src/util/dtos/UserDTO.ts
Normal file
19
src/util/dtos/UserDTO.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { User } from "../entities";
|
||||||
|
|
||||||
|
export class MinimalPublicUserDTO {
|
||||||
|
avatar?: string | null;
|
||||||
|
discriminator: string;
|
||||||
|
id: string;
|
||||||
|
public_flags: number;
|
||||||
|
username: string;
|
||||||
|
badge_ids?: string[] | null;
|
||||||
|
|
||||||
|
constructor(user: User) {
|
||||||
|
this.avatar = user.avatar;
|
||||||
|
this.discriminator = user.discriminator;
|
||||||
|
this.id = user.id;
|
||||||
|
this.public_flags = user.public_flags;
|
||||||
|
this.username = user.username;
|
||||||
|
this.badge_ids = user.badge_ids;
|
||||||
|
}
|
||||||
|
}
|
4
src/util/dtos/index.ts
Normal file
4
src/util/dtos/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./ConnectedAccountDTO";
|
||||||
|
export * from "./DmChannelDTO";
|
||||||
|
export * from "./ReadyGuildDTO";
|
||||||
|
export * from "./UserDTO";
|
174
src/util/entities/Application.ts
Normal file
174
src/util/entities/Application.ts
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToOne,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Team } from "./Team";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "applications",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Application extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
summary: string = "";
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
type?: object; // TODO: this type is bad
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
hook: boolean = true;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
bot_public?: boolean = true;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
bot_require_code_grant?: boolean = false;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
verify_key: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "owner_id" })
|
||||||
|
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||||
|
owner: User;
|
||||||
|
|
||||||
|
// TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags
|
||||||
|
@Column()
|
||||||
|
flags: number = 0;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array", nullable: true })
|
||||||
|
redirect_uris: string[] = [];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
rpc_application_state: number = 0;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
store_application_state: number = 1;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
verification_state: number = 1;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
interactions_endpoint_url?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
integration_public: boolean = true;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
integration_require_code_grant: boolean = false;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
discoverability_state: number = 1;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
discovery_eligibility_flags: number = 2240;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "bot_user_id" })
|
||||||
|
@OneToOne(() => User, { onDelete: "CASCADE" })
|
||||||
|
bot?: User;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array", nullable: true })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
cover_image?: string; // the application's default rich presence invite cover image hash
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
install_params?: { scopes: string[]; permissions: string };
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
terms_of_service_url?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
privacy_policy_url?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((application: Application) => application.guild)
|
||||||
|
guild_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild)
|
||||||
|
guild?: Guild; // guild to which the app is linked, e.g. a developer support server
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
custom_install_url?: string;
|
||||||
|
|
||||||
|
//just for us
|
||||||
|
|
||||||
|
//@Column({ type: "simple-array", nullable: true })
|
||||||
|
//rpc_origins?: string[];
|
||||||
|
|
||||||
|
//@Column({ nullable: true })
|
||||||
|
//primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
|
||||||
|
|
||||||
|
//@Column({ nullable: true })
|
||||||
|
//slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
|
||||||
|
|
||||||
|
@JoinColumn({ name: "team_id" })
|
||||||
|
@ManyToOne(() => Team, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
team?: Team;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationCommand {
|
||||||
|
id: string;
|
||||||
|
application_id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
options?: ApplicationCommandOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationCommandOption {
|
||||||
|
type: ApplicationCommandOptionType;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
required?: boolean;
|
||||||
|
choices?: ApplicationCommandOptionChoice[];
|
||||||
|
options?: ApplicationCommandOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationCommandOptionChoice {
|
||||||
|
name: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ApplicationCommandOptionType {
|
||||||
|
SUB_COMMAND = 1,
|
||||||
|
SUB_COMMAND_GROUP = 2,
|
||||||
|
STRING = 3,
|
||||||
|
INTEGER = 4,
|
||||||
|
BOOLEAN = 5,
|
||||||
|
USER = 6,
|
||||||
|
CHANNEL = 7,
|
||||||
|
ROLE = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationCommandInteractionData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
options?: ApplicationCommandInteractionDataOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationCommandInteractionDataOption {
|
||||||
|
name: string;
|
||||||
|
value?: unknown;
|
||||||
|
options?: ApplicationCommandInteractionDataOption[];
|
||||||
|
}
|
58
src/util/entities/Attachment.ts
Normal file
58
src/util/entities/Attachment.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import {
|
||||||
|
BeforeRemove,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { URL } from "url";
|
||||||
|
import { deleteFile } from "../util/cdn";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "attachments",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Attachment extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
filename: string; // name of file attached
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
size: number; // size of file in bytes
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
url: string; // source url of file
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
proxy_url: string; // a proxied url of file
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
height?: number; // height of file (if image)
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
width?: number; // width of file (if image)
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
content_type?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((attachment: Attachment) => attachment.message)
|
||||||
|
message_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "message_id" })
|
||||||
|
@ManyToOne(
|
||||||
|
() => require("./Message").Message,
|
||||||
|
(message: import("./Message").Message) => message.attachments,
|
||||||
|
{
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
message: import("./Message").Message;
|
||||||
|
|
||||||
|
@BeforeRemove()
|
||||||
|
onDelete() {
|
||||||
|
return deleteFile(new URL(this.url).pathname);
|
||||||
|
}
|
||||||
|
}
|
198
src/util/entities/AuditLog.ts
Normal file
198
src/util/entities/AuditLog.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { ChannelPermissionOverwrite } from "./Channel";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export enum AuditLogEvents {
|
||||||
|
// guild level
|
||||||
|
GUILD_UPDATE = 1,
|
||||||
|
GUILD_IMPORT = 2,
|
||||||
|
GUILD_EXPORTED = 3,
|
||||||
|
GUILD_ARCHIVE = 4,
|
||||||
|
GUILD_UNARCHIVE = 5,
|
||||||
|
// join-leave
|
||||||
|
USER_JOIN = 6,
|
||||||
|
USER_LEAVE = 7,
|
||||||
|
// channels
|
||||||
|
CHANNEL_CREATE = 10,
|
||||||
|
CHANNEL_UPDATE = 11,
|
||||||
|
CHANNEL_DELETE = 12,
|
||||||
|
// permission overrides
|
||||||
|
CHANNEL_OVERWRITE_CREATE = 13,
|
||||||
|
CHANNEL_OVERWRITE_UPDATE = 14,
|
||||||
|
CHANNEL_OVERWRITE_DELETE = 15,
|
||||||
|
// kick and ban
|
||||||
|
MEMBER_KICK = 20,
|
||||||
|
MEMBER_PRUNE = 21,
|
||||||
|
MEMBER_BAN_ADD = 22,
|
||||||
|
MEMBER_BAN_REMOVE = 23,
|
||||||
|
// member updates
|
||||||
|
MEMBER_UPDATE = 24,
|
||||||
|
MEMBER_ROLE_UPDATE = 25,
|
||||||
|
MEMBER_MOVE = 26,
|
||||||
|
MEMBER_DISCONNECT = 27,
|
||||||
|
BOT_ADD = 28,
|
||||||
|
// roles
|
||||||
|
ROLE_CREATE = 30,
|
||||||
|
ROLE_UPDATE = 31,
|
||||||
|
ROLE_DELETE = 32,
|
||||||
|
ROLE_SWAP = 33,
|
||||||
|
// invites
|
||||||
|
INVITE_CREATE = 40,
|
||||||
|
INVITE_UPDATE = 41,
|
||||||
|
INVITE_DELETE = 42,
|
||||||
|
// webhooks
|
||||||
|
WEBHOOK_CREATE = 50,
|
||||||
|
WEBHOOK_UPDATE = 51,
|
||||||
|
WEBHOOK_DELETE = 52,
|
||||||
|
WEBHOOK_SWAP = 53,
|
||||||
|
// custom emojis
|
||||||
|
EMOJI_CREATE = 60,
|
||||||
|
EMOJI_UPDATE = 61,
|
||||||
|
EMOJI_DELETE = 62,
|
||||||
|
EMOJI_SWAP = 63,
|
||||||
|
// deletion
|
||||||
|
MESSAGE_CREATE = 70, // messages sent using non-primary seat of the user only
|
||||||
|
MESSAGE_EDIT = 71, // non-self edits only
|
||||||
|
MESSAGE_DELETE = 72,
|
||||||
|
MESSAGE_BULK_DELETE = 73,
|
||||||
|
// pinning
|
||||||
|
MESSAGE_PIN = 74,
|
||||||
|
MESSAGE_UNPIN = 75,
|
||||||
|
// integrations
|
||||||
|
INTEGRATION_CREATE = 80,
|
||||||
|
INTEGRATION_UPDATE = 81,
|
||||||
|
INTEGRATION_DELETE = 82,
|
||||||
|
// stage actions
|
||||||
|
STAGE_INSTANCE_CREATE = 83,
|
||||||
|
STAGE_INSTANCE_UPDATE = 84,
|
||||||
|
STAGE_INSTANCE_DELETE = 85,
|
||||||
|
// stickers
|
||||||
|
STICKER_CREATE = 90,
|
||||||
|
STICKER_UPDATE = 91,
|
||||||
|
STICKER_DELETE = 92,
|
||||||
|
STICKER_SWAP = 93,
|
||||||
|
// threads
|
||||||
|
THREAD_CREATE = 110,
|
||||||
|
THREAD_UPDATE = 111,
|
||||||
|
THREAD_DELETE = 112,
|
||||||
|
// application commands
|
||||||
|
APPLICATION_COMMAND_PERMISSION_UPDATE = 121,
|
||||||
|
// automod
|
||||||
|
POLICY_CREATE = 140,
|
||||||
|
POLICY_UPDATE = 141,
|
||||||
|
POLICY_DELETE = 142,
|
||||||
|
MESSAGE_BLOCKED_BY_POLICIES = 143, // in valkyrie, blocked messages are stealth-dropped
|
||||||
|
// instance policies affecting the guild
|
||||||
|
GUILD_AFFECTED_BY_POLICIES = 216,
|
||||||
|
// message moves
|
||||||
|
IN_GUILD_MESSAGE_MOVE = 223,
|
||||||
|
CROSS_GUILD_MESSAGE_MOVE = 224,
|
||||||
|
// message routing
|
||||||
|
ROUTE_CREATE = 225,
|
||||||
|
ROUTE_UPDATE = 226,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "audit_logs",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class AuditLog extends BaseClass {
|
||||||
|
@JoinColumn({ name: "target_id" })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
target?: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((auditlog: AuditLog) => auditlog.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, (user: User) => user.id)
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
action_type: AuditLogEvents;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
options?: {
|
||||||
|
delete_member_days?: string;
|
||||||
|
members_removed?: string;
|
||||||
|
channel_id?: string;
|
||||||
|
messaged_id?: string;
|
||||||
|
count?: string;
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
role_name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
changes: AuditLogChange[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogChange {
|
||||||
|
new_value?: AuditLogChangeValue;
|
||||||
|
old_value?: AuditLogChangeValue;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogChangeValue {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
icon_hash?: string;
|
||||||
|
splash_hash?: string;
|
||||||
|
discovery_splash_hash?: string;
|
||||||
|
banner_hash?: string;
|
||||||
|
owner_id?: string;
|
||||||
|
region?: string;
|
||||||
|
preferred_locale?: string;
|
||||||
|
afk_channel_id?: string;
|
||||||
|
afk_timeout?: number;
|
||||||
|
rules_channel_id?: string;
|
||||||
|
public_updates_channel_id?: string;
|
||||||
|
mfa_level?: number;
|
||||||
|
verification_level?: number;
|
||||||
|
explicit_content_filter?: number;
|
||||||
|
default_message_notifications?: number;
|
||||||
|
vanity_url_code?: string;
|
||||||
|
$add?: object[]; // TODO: These types are bad.
|
||||||
|
$remove?: object[];
|
||||||
|
prune_delete_days?: number;
|
||||||
|
widget_enabled?: boolean;
|
||||||
|
widget_channel_id?: string;
|
||||||
|
system_channel_id?: string;
|
||||||
|
position?: number;
|
||||||
|
topic?: string;
|
||||||
|
bitrate?: number;
|
||||||
|
permission_overwrites?: ChannelPermissionOverwrite[];
|
||||||
|
nsfw?: boolean;
|
||||||
|
application_id?: string;
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
permissions?: string;
|
||||||
|
color?: number;
|
||||||
|
hoist?: boolean;
|
||||||
|
mentionable?: boolean;
|
||||||
|
allow?: string;
|
||||||
|
deny?: string;
|
||||||
|
code?: string;
|
||||||
|
channel_id?: string;
|
||||||
|
inviter_id?: string;
|
||||||
|
max_uses?: number;
|
||||||
|
uses?: number;
|
||||||
|
max_age?: number;
|
||||||
|
temporary?: boolean;
|
||||||
|
deaf?: boolean;
|
||||||
|
mute?: boolean;
|
||||||
|
nick?: string;
|
||||||
|
avatar_hash?: string;
|
||||||
|
id?: string;
|
||||||
|
type?: number;
|
||||||
|
enable_emoticons?: boolean;
|
||||||
|
expire_behavior?: number;
|
||||||
|
expire_grace_period?: number;
|
||||||
|
user_limit?: number;
|
||||||
|
}
|
39
src/util/entities/BackupCodes.ts
Normal file
39
src/util/entities/BackupCodes.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { User } from "./User";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "backup_codes",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class BackupCode extends BaseClass {
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
consumed: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
expired: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMfaBackupCodes(user_id: string) {
|
||||||
|
const backup_codes: BackupCode[] = [];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const code = BackupCode.create({
|
||||||
|
user: { id: user_id },
|
||||||
|
code: crypto.randomBytes(4).toString("hex"), // 8 characters
|
||||||
|
consumed: false,
|
||||||
|
expired: false,
|
||||||
|
});
|
||||||
|
backup_codes.push(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return backup_codes;
|
||||||
|
}
|
21
src/util/entities/Badge.ts
Normal file
21
src/util/entities/Badge.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClassWithoutId } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "badges",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Badge extends BaseClassWithoutId {
|
||||||
|
@Column({ primary: true })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
link?: string;
|
||||||
|
}
|
45
src/util/entities/Ban.ts
Normal file
45
src/util/entities/Ban.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "bans",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Ban extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((ban: Ban) => ban.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((ban: Ban) => ban.guild)
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((ban: Ban) => ban.executor)
|
||||||
|
executor_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "executor_id" })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
executor: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
ip: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
reason?: string;
|
||||||
|
}
|
79
src/util/entities/BaseClass.ts
Normal file
79
src/util/entities/BaseClass.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
|
FindOptionsWhere,
|
||||||
|
ObjectIdColumn,
|
||||||
|
PrimaryColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
import { Snowflake } from "../util/Snowflake";
|
||||||
|
import { getDatabase } from "../util/Database";
|
||||||
|
import { OrmUtils } from "../imports/OrmUtils";
|
||||||
|
|
||||||
|
export class BaseClassWithoutId extends BaseEntity {
|
||||||
|
private get construct() {
|
||||||
|
return this.constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get metadata() {
|
||||||
|
return getDatabase()?.getMetadata(this.construct);
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(props: object) {
|
||||||
|
OrmUtils.mergeDeep(this, props);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix eslint
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
toJSON(): any {
|
||||||
|
return Object.fromEntries(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/no-non-null-assertion
|
||||||
|
this.metadata!.columns // @ts-ignore
|
||||||
|
.map((x) => [x.propertyName, this[x.propertyName]])
|
||||||
|
.concat(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
this.metadata.relations.map((x) => [
|
||||||
|
x.propertyName,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
this[x.propertyName],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static increment<T extends BaseClass>(
|
||||||
|
conditions: FindOptionsWhere<T>,
|
||||||
|
propertyPath: string,
|
||||||
|
value: number | string,
|
||||||
|
) {
|
||||||
|
const repository = this.getRepository();
|
||||||
|
return repository.increment(conditions, propertyPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static decrement<T extends BaseClass>(
|
||||||
|
conditions: FindOptionsWhere<T>,
|
||||||
|
propertyPath: string,
|
||||||
|
value: number | string,
|
||||||
|
) {
|
||||||
|
const repository = this.getRepository();
|
||||||
|
return repository.decrement(conditions, propertyPath, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb")
|
||||||
|
? ObjectIdColumn
|
||||||
|
: PrimaryColumn;
|
||||||
|
|
||||||
|
export class BaseClass extends BaseClassWithoutId {
|
||||||
|
@PrimaryIdColumn()
|
||||||
|
id: string = Snowflake.generate();
|
||||||
|
|
||||||
|
@BeforeUpdate()
|
||||||
|
@BeforeInsert()
|
||||||
|
_do_validate() {
|
||||||
|
if (!this.id) this.id = Snowflake.generate();
|
||||||
|
}
|
||||||
|
}
|
41
src/util/entities/Categories.ts
Normal file
41
src/util/entities/Categories.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
// TODO: categories:
|
||||||
|
// [{
|
||||||
|
// "id": 16,
|
||||||
|
// "default": "Anime & Manga",
|
||||||
|
// "localizations": {
|
||||||
|
// "de": "Anime & Manga",
|
||||||
|
// "fr": "Anim\u00e9s et mangas",
|
||||||
|
// "ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "is_primary": false/true
|
||||||
|
// }]
|
||||||
|
// Also populate discord default categories
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "categories",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Categories extends BaseClassWithoutId {
|
||||||
|
// Not using snowflake
|
||||||
|
|
||||||
|
@PrimaryIdColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
localizations: string;
|
||||||
|
|
||||||
|
// Whether to show the category prominently (e.g. in a sidebar) instead of only secondary (e.g. in search results)
|
||||||
|
@Column({ nullable: true })
|
||||||
|
is_primary: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
icon?: string;
|
||||||
|
}
|
573
src/util/entities/Channel.ts
Normal file
573
src/util/entities/Channel.ts
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
import { HTTPError } from "lambert-server";
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { DmChannelDTO } from "../dtos";
|
||||||
|
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
|
||||||
|
import {
|
||||||
|
InvisibleCharacters,
|
||||||
|
Snowflake,
|
||||||
|
containsAll,
|
||||||
|
emitEvent,
|
||||||
|
getPermission,
|
||||||
|
trimSpecial,
|
||||||
|
} from "../util";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { Invite } from "./Invite";
|
||||||
|
import { Message } from "./Message";
|
||||||
|
import { ReadState } from "./ReadState";
|
||||||
|
import { Recipient } from "./Recipient";
|
||||||
|
import { PublicUserProjection, User } from "./User";
|
||||||
|
import { VoiceState } from "./VoiceState";
|
||||||
|
import { Webhook } from "./Webhook";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export enum ChannelType {
|
||||||
|
GUILD_TEXT = 0, // a text channel within a guild
|
||||||
|
DM = 1, // a direct message between users
|
||||||
|
GUILD_VOICE = 2, // a voice channel within a guild
|
||||||
|
GROUP_DM = 3, // a direct message between multiple users
|
||||||
|
GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels
|
||||||
|
GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route
|
||||||
|
GUILD_STORE = 6, // a channel in which game developers can sell their things
|
||||||
|
ENCRYPTED = 7, // end-to-end encrypted channel
|
||||||
|
ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
|
||||||
|
TRANSACTIONAL = 9, // event chain style transactional channel
|
||||||
|
GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
|
||||||
|
GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
|
||||||
|
GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
|
||||||
|
GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
|
||||||
|
DIRECTORY = 14, // guild directory listing channel
|
||||||
|
GUILD_FORUM = 15, // forum composed of IM threads
|
||||||
|
TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12
|
||||||
|
KANBAN = 34, // confluence like kanban board
|
||||||
|
VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage)
|
||||||
|
CUSTOM_START = 64, // start custom channel types from here
|
||||||
|
UNHANDLED = 255, // unhandled unowned pass-through channel type
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "channels",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Channel extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
icon?: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
type: ChannelType;
|
||||||
|
|
||||||
|
@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
recipients?: Recipient[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
last_message_id?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((channel: Channel) => channel.guild)
|
||||||
|
guild_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, (guild) => guild.channels, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
guild?: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((channel: Channel) => channel.parent)
|
||||||
|
parent_id: string | null;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "parent_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
parent?: Channel;
|
||||||
|
|
||||||
|
// for group DMs and owned custom channel types
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((channel: Channel) => channel.owner)
|
||||||
|
owner_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "owner_id" })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
owner: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
last_pin_timestamp?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
default_auto_archive_duration?: number;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
permission_overwrites?: ChannelPermissionOverwrite[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
video_quality_mode?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
bitrate?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
user_limit?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
nsfw: boolean = false;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
topic?: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Invite, (invite: Invite) => invite.channel, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
invites?: Invite[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
retention_policy_id?: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Message, (message: Message) => message.channel, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
messages?: Message[];
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => VoiceState,
|
||||||
|
(voice_state: VoiceState) => voice_state.channel,
|
||||||
|
{
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
voice_states?: VoiceState[];
|
||||||
|
|
||||||
|
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
read_states?: ReadState[];
|
||||||
|
|
||||||
|
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
webhooks?: Webhook[];
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
flags: number = 0;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
default_thread_rate_limit_per_user: number = 0;
|
||||||
|
|
||||||
|
/** Must be calculated Channel.calculatePosition */
|
||||||
|
position: number;
|
||||||
|
|
||||||
|
// TODO: DM channel
|
||||||
|
static async createChannel(
|
||||||
|
channel: Partial<Channel>,
|
||||||
|
user_id: string = "0",
|
||||||
|
opts?: {
|
||||||
|
keepId?: boolean;
|
||||||
|
skipExistsCheck?: boolean;
|
||||||
|
skipPermissionCheck?: boolean;
|
||||||
|
skipEventEmit?: boolean;
|
||||||
|
skipNameChecks?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (!opts?.skipPermissionCheck) {
|
||||||
|
// Always check if user has permission first
|
||||||
|
const permissions = await getPermission(user_id, channel.guild_id);
|
||||||
|
permissions.hasThrow("MANAGE_CHANNELS");
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await Guild.findOneOrFail({
|
||||||
|
where: { id: channel.guild_id },
|
||||||
|
select: {
|
||||||
|
features: !opts?.skipNameChecks,
|
||||||
|
channel_ordering: true,
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!opts?.skipNameChecks) {
|
||||||
|
if (
|
||||||
|
!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") &&
|
||||||
|
channel.name
|
||||||
|
) {
|
||||||
|
for (const character of InvisibleCharacters)
|
||||||
|
if (channel.name.includes(character))
|
||||||
|
throw new HTTPError(
|
||||||
|
"Channel name cannot include invalid characters",
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Categories skip these checks on discord.com
|
||||||
|
if (
|
||||||
|
channel.type !== ChannelType.GUILD_CATEGORY ||
|
||||||
|
guild.features.includes("IRC_LIKE_CATEGORY_NAMES")
|
||||||
|
) {
|
||||||
|
if (channel.name.includes(" "))
|
||||||
|
throw new HTTPError(
|
||||||
|
"Channel name cannot include invalid characters",
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channel.name.match(/--+/g))
|
||||||
|
throw new HTTPError(
|
||||||
|
"Channel name cannot include multiple adjacent dashes.",
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
channel.name.charAt(0) === "-" ||
|
||||||
|
channel.name.charAt(channel.name.length - 1) === "-"
|
||||||
|
)
|
||||||
|
throw new HTTPError(
|
||||||
|
"Channel name cannot start/end with dash.",
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
} else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
|
||||||
|
if (!channel.name)
|
||||||
|
throw new HTTPError("Channel name cannot be empty.", 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (channel.type) {
|
||||||
|
case ChannelType.GUILD_TEXT:
|
||||||
|
case ChannelType.GUILD_NEWS:
|
||||||
|
case ChannelType.GUILD_VOICE:
|
||||||
|
if (channel.parent_id && !opts?.skipExistsCheck) {
|
||||||
|
const exists = await Channel.findOneOrFail({
|
||||||
|
where: { id: channel.parent_id },
|
||||||
|
});
|
||||||
|
if (!exists)
|
||||||
|
throw new HTTPError(
|
||||||
|
"Parent id channel doesn't exist",
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
if (exists.guild_id !== channel.guild_id)
|
||||||
|
throw new HTTPError(
|
||||||
|
"The category channel needs to be in the guild",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ChannelType.GUILD_CATEGORY:
|
||||||
|
case ChannelType.UNHANDLED:
|
||||||
|
break;
|
||||||
|
case ChannelType.DM:
|
||||||
|
case ChannelType.GROUP_DM:
|
||||||
|
throw new HTTPError("You can't create a dm channel in a guild");
|
||||||
|
case ChannelType.GUILD_STORE:
|
||||||
|
default:
|
||||||
|
throw new HTTPError("Not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.permission_overwrites) channel.permission_overwrites = [];
|
||||||
|
// TODO: eagerly auto generate position of all guild channels
|
||||||
|
|
||||||
|
const position =
|
||||||
|
(channel.type === ChannelType.UNHANDLED ? 0 : channel.position) ||
|
||||||
|
0;
|
||||||
|
|
||||||
|
channel = {
|
||||||
|
...channel,
|
||||||
|
...(!opts?.keepId && { id: Snowflake.generate() }),
|
||||||
|
created_at: new Date(),
|
||||||
|
position,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ret = Channel.create(channel);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ret.save(),
|
||||||
|
!opts?.skipEventEmit
|
||||||
|
? emitEvent({
|
||||||
|
event: "CHANNEL_CREATE",
|
||||||
|
data: channel,
|
||||||
|
guild_id: channel.guild_id,
|
||||||
|
} as ChannelCreateEvent)
|
||||||
|
: Promise.resolve(),
|
||||||
|
Guild.insertChannelInOrder(guild.id, ret.id, position, guild),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createDMChannel(
|
||||||
|
recipients: string[],
|
||||||
|
creator_user_id: string,
|
||||||
|
name?: string,
|
||||||
|
) {
|
||||||
|
recipients = recipients.unique().filter((x) => x !== creator_user_id);
|
||||||
|
// TODO: check config for max number of recipients
|
||||||
|
/** if you want to disallow note to self channels, uncomment the conditional below
|
||||||
|
|
||||||
|
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
|
||||||
|
if (otherRecipientsUsers.length !== recipients.length) {
|
||||||
|
throw new HTTPError("Recipient/s not found");
|
||||||
|
}
|
||||||
|
**/
|
||||||
|
|
||||||
|
const type =
|
||||||
|
recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
|
||||||
|
|
||||||
|
let channel = null;
|
||||||
|
|
||||||
|
const channelRecipients = [...recipients, creator_user_id];
|
||||||
|
|
||||||
|
const userRecipients = await Recipient.find({
|
||||||
|
where: { user_id: creator_user_id },
|
||||||
|
relations: ["channel", "channel.recipients"],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const ur of userRecipients) {
|
||||||
|
if (!ur.channel.recipients) continue;
|
||||||
|
const re = ur.channel.recipients.map((r) => r.user_id);
|
||||||
|
if (re.length === channelRecipients.length) {
|
||||||
|
if (containsAll(re, channelRecipients)) {
|
||||||
|
if (channel == null) {
|
||||||
|
channel = ur.channel;
|
||||||
|
await ur.assign({ closed: false }).save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
name = trimSpecial(name);
|
||||||
|
|
||||||
|
channel = await Channel.create({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
owner_id: undefined,
|
||||||
|
created_at: new Date(),
|
||||||
|
last_message_id: undefined,
|
||||||
|
recipients: channelRecipients.map((x) =>
|
||||||
|
Recipient.create({
|
||||||
|
user_id: x,
|
||||||
|
closed: !(
|
||||||
|
type === ChannelType.GROUP_DM ||
|
||||||
|
x === creator_user_id
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
nsfw: false,
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel_dto = await DmChannelDTO.from(channel);
|
||||||
|
|
||||||
|
if (type === ChannelType.GROUP_DM && channel.recipients) {
|
||||||
|
for (const recipient of channel.recipients) {
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_CREATE",
|
||||||
|
data: channel_dto.excludedRecipients([recipient.user_id]),
|
||||||
|
user_id: recipient.user_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_CREATE",
|
||||||
|
data: channel_dto,
|
||||||
|
user_id: creator_user_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipients.length === 1) return channel_dto;
|
||||||
|
else return channel_dto.excludedRecipients([creator_user_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeRecipientFromChannel(channel: Channel, user_id: string) {
|
||||||
|
await Recipient.delete({ channel_id: channel.id, user_id: user_id });
|
||||||
|
channel.recipients = channel.recipients?.filter(
|
||||||
|
(r) => r.user_id !== user_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channel.recipients?.length === 0) {
|
||||||
|
await Channel.deleteChannel(channel);
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_DELETE",
|
||||||
|
data: await DmChannelDTO.from(channel, [user_id]),
|
||||||
|
user_id: user_id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_DELETE",
|
||||||
|
data: await DmChannelDTO.from(channel, [user_id]),
|
||||||
|
user_id: user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
//If the owner leave the server user is the new owner
|
||||||
|
if (channel.owner_id === user_id) {
|
||||||
|
channel.owner_id = "1"; // The channel is now owned by the server user
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_UPDATE",
|
||||||
|
data: await DmChannelDTO.from(channel, [user_id]),
|
||||||
|
channel_id: channel.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.save();
|
||||||
|
|
||||||
|
await emitEvent({
|
||||||
|
event: "CHANNEL_RECIPIENT_REMOVE",
|
||||||
|
data: {
|
||||||
|
channel_id: channel.id,
|
||||||
|
user: await User.findOneOrFail({
|
||||||
|
where: { id: user_id },
|
||||||
|
select: PublicUserProjection,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
channel_id: channel.id,
|
||||||
|
} as ChannelRecipientRemoveEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteChannel(channel: Channel) {
|
||||||
|
// TODO Delete attachments from the CDN for messages in the channel
|
||||||
|
await Channel.delete({ id: channel.id });
|
||||||
|
|
||||||
|
const guild = await Guild.findOneOrFail({
|
||||||
|
where: { id: channel.guild_id },
|
||||||
|
select: { channel_ordering: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedOrdering = guild.channel_ordering.filter(
|
||||||
|
(id) => id != channel.id,
|
||||||
|
);
|
||||||
|
await Guild.update(
|
||||||
|
{ id: channel.guild_id },
|
||||||
|
{ channel_ordering: updatedOrdering },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async calculatePosition(
|
||||||
|
channel_id: string,
|
||||||
|
guild_id: string,
|
||||||
|
guild?: Guild,
|
||||||
|
) {
|
||||||
|
if (!guild)
|
||||||
|
guild = await Guild.findOneOrFail({
|
||||||
|
where: { id: guild_id },
|
||||||
|
select: { channel_ordering: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return guild.channel_ordering.findIndex((id) => channel_id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getOrderedChannels(guild_id: string, guild?: Guild) {
|
||||||
|
if (!guild)
|
||||||
|
guild = await Guild.findOneOrFail({
|
||||||
|
where: { id: guild_id },
|
||||||
|
select: { channel_ordering: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const channels = await Promise.all(
|
||||||
|
guild.channel_ordering.map((id) =>
|
||||||
|
Channel.findOne({ where: { id } }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return channels
|
||||||
|
.filter((channel) => channel !== null)
|
||||||
|
.reduce((r, v) => {
|
||||||
|
v = v as Channel;
|
||||||
|
|
||||||
|
v.position = (guild as Guild).channel_ordering.indexOf(v.id);
|
||||||
|
r[v.position] = v;
|
||||||
|
return r;
|
||||||
|
}, [] as Array<Channel>);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDm() {
|
||||||
|
return (
|
||||||
|
this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the channel support sending messages ( eg categories do not )
|
||||||
|
isWritable() {
|
||||||
|
const disallowedChannelTypes = [
|
||||||
|
ChannelType.GUILD_CATEGORY,
|
||||||
|
ChannelType.GUILD_STAGE_VOICE,
|
||||||
|
ChannelType.VOICELESS_WHITEBOARD,
|
||||||
|
];
|
||||||
|
return disallowedChannelTypes.indexOf(this.type) == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
|
||||||
|
// these fields are not returned depending on the type of channel
|
||||||
|
bitrate: this.bitrate || undefined,
|
||||||
|
user_limit: this.user_limit || undefined,
|
||||||
|
rate_limit_per_user: this.rate_limit_per_user || undefined,
|
||||||
|
owner_id: this.owner_id || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelPermissionOverwrite {
|
||||||
|
allow: string;
|
||||||
|
deny: string;
|
||||||
|
id: string;
|
||||||
|
type: ChannelPermissionOverwriteType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelPermissionOverwriteType {
|
||||||
|
role = 0,
|
||||||
|
member = 1,
|
||||||
|
group = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
|
||||||
|
type: ChannelType.DM | ChannelType.GROUP_DM;
|
||||||
|
recipients: Recipient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: probably more props
|
||||||
|
export function isTextChannel(type: ChannelType): boolean {
|
||||||
|
switch (type) {
|
||||||
|
case ChannelType.GUILD_STORE:
|
||||||
|
case ChannelType.GUILD_STAGE_VOICE:
|
||||||
|
case ChannelType.GUILD_CATEGORY:
|
||||||
|
case ChannelType.GUILD_FORUM:
|
||||||
|
case ChannelType.DIRECTORY:
|
||||||
|
throw new HTTPError("not a text channel", 400);
|
||||||
|
case ChannelType.DM:
|
||||||
|
case ChannelType.GROUP_DM:
|
||||||
|
case ChannelType.GUILD_NEWS:
|
||||||
|
case ChannelType.GUILD_VOICE:
|
||||||
|
case ChannelType.GUILD_NEWS_THREAD:
|
||||||
|
case ChannelType.GUILD_PUBLIC_THREAD:
|
||||||
|
case ChannelType.GUILD_PRIVATE_THREAD:
|
||||||
|
case ChannelType.GUILD_TEXT:
|
||||||
|
case ChannelType.ENCRYPTED:
|
||||||
|
case ChannelType.ENCRYPTED_THREAD:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
throw new HTTPError("unimplemented", 400);
|
||||||
|
}
|
||||||
|
}
|
27
src/util/entities/ClientRelease.ts
Normal file
27
src/util/entities/ClientRelease.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "client_release",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Release extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
pub_date: Date;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
platform: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
notes?: string;
|
||||||
|
}
|
15
src/util/entities/Config.ts
Normal file
15
src/util/entities/Config.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "config",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class ConfigEntity extends BaseClassWithoutId {
|
||||||
|
@PrimaryIdColumn()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
value: number | boolean | null | string | undefined;
|
||||||
|
}
|
72
src/util/entities/ConnectedAccount.ts
Normal file
72
src/util/entities/ConnectedAccount.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { ConnectedAccountTokenData } from "../interfaces";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export type PublicConnectedAccount = Pick<
|
||||||
|
ConnectedAccount,
|
||||||
|
"name" | "type" | "verified"
|
||||||
|
>;
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "connected_accounts",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class ConnectedAccount extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
external_id: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((account: ConnectedAccount) => account.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column({ select: false })
|
||||||
|
friend_sync?: boolean = false;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ select: false })
|
||||||
|
revoked?: boolean = false;
|
||||||
|
|
||||||
|
@Column({ select: false })
|
||||||
|
show_activity?: number = 0;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
verified?: boolean = true;
|
||||||
|
|
||||||
|
@Column({ select: false })
|
||||||
|
visibility?: number = 0;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array" })
|
||||||
|
integrations?: string[] = [];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", name: "metadata", nullable: true })
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metadata_?: any;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
metadata_visibility?: number = 0;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
two_way_link?: boolean = false;
|
||||||
|
|
||||||
|
@Column({ select: false, nullable: true, type: "simple-json" })
|
||||||
|
token_data?: ConnectedAccountTokenData | null;
|
||||||
|
|
||||||
|
async revoke() {
|
||||||
|
this.revoked = true;
|
||||||
|
this.token_data = null;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
}
|
15
src/util/entities/ConnectionConfigEntity.ts
Normal file
15
src/util/entities/ConnectionConfigEntity.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "connection_config",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class ConnectionConfigEntity extends BaseClassWithoutId {
|
||||||
|
@PrimaryIdColumn()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
value: number | boolean | null | string | Date | undefined;
|
||||||
|
}
|
16
src/util/entities/EmbedCache.ts
Normal file
16
src/util/entities/EmbedCache.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Entity, Column } from "typeorm";
|
||||||
|
import { Embed } from "./Message";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "embed_cache",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class EmbedCache extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
embed: Embed;
|
||||||
|
}
|
49
src/util/entities/Emoji.ts
Normal file
49
src/util/entities/Emoji.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { User } from ".";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "emojis",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Emoji extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
animated: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
available: boolean; // whether this emoji can be used, may be false due to various reasons
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, (guild) => guild.emojis, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((emoji: Emoji) => emoji.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
managed: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
require_colons: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array" })
|
||||||
|
roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
|
||||||
|
|
||||||
|
@Column({ type: "simple-array", nullable: true })
|
||||||
|
groups: string[]; // user groups this emoji is whitelisted to (Valkyrie extension)
|
||||||
|
}
|
27
src/util/entities/Encryption.ts
Normal file
27
src/util/entities/Encryption.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "security_settings",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class SecuritySettings extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
channel_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
encryption_permission_mask: number;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array" })
|
||||||
|
allowed_algorithms: string[];
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
current_algorithm: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
used_since_message: string;
|
||||||
|
}
|
444
src/util/entities/Guild.ts
Normal file
444
src/util/entities/Guild.ts
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { Config, GuildWelcomeScreen, Snowflake, handleFile } from "..";
|
||||||
|
import { Ban } from "./Ban";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Channel } from "./Channel";
|
||||||
|
import { Emoji } from "./Emoji";
|
||||||
|
import { Invite } from "./Invite";
|
||||||
|
import { Member } from "./Member";
|
||||||
|
import { Role } from "./Role";
|
||||||
|
import { Sticker } from "./Sticker";
|
||||||
|
import { Template } from "./Template";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { VoiceState } from "./VoiceState";
|
||||||
|
import { Webhook } from "./Webhook";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
|
||||||
|
// TODO: guild_scheduled_events
|
||||||
|
// TODO: stage_instances
|
||||||
|
// TODO: threads
|
||||||
|
// TODO:
|
||||||
|
// "keywords": [
|
||||||
|
// "Genshin Impact",
|
||||||
|
// "Paimon",
|
||||||
|
// "Honkai Impact",
|
||||||
|
// "ARPG",
|
||||||
|
// "Open-World",
|
||||||
|
// "Waifu",
|
||||||
|
// "Anime",
|
||||||
|
// "Genshin",
|
||||||
|
// "miHoYo",
|
||||||
|
// "Gacha"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
export const PublicGuildRelations = [
|
||||||
|
"channels",
|
||||||
|
"emojis",
|
||||||
|
"roles",
|
||||||
|
"stickers",
|
||||||
|
"voice_states",
|
||||||
|
// "members", // TODO: These are public, but all members should not be fetched.
|
||||||
|
// "members.user",
|
||||||
|
];
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "guilds",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Guild extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.afk_channel)
|
||||||
|
afk_channel_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "afk_channel_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
afk_channel?: Channel;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
afk_timeout?: number;
|
||||||
|
|
||||||
|
// * commented out -> use owner instead
|
||||||
|
// application id of the guild creator if it is bot-created
|
||||||
|
// @Column({ nullable: true })
|
||||||
|
// application?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "ban_ids" })
|
||||||
|
@OneToMany(() => Ban, (ban: Ban) => ban.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
bans: Ban[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
banner?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
default_message_notifications?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
discovery_splash?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
explicit_content_filter?: number;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array" })
|
||||||
|
features: string[] = []; //TODO use enum
|
||||||
|
//TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
primary_category_id?: string; // TODO: this was number?
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
large?: boolean = false;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
max_members?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
max_presences?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
max_video_channel_users?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
member_count?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
presence_count?: number; // users online
|
||||||
|
|
||||||
|
@OneToMany(() => Member, (member: Member) => member.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
members: Member[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "role_ids" })
|
||||||
|
@OneToMany(() => Role, (role: Role) => role.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
roles: Role[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "channel_ids" })
|
||||||
|
@OneToMany(() => Channel, (channel: Channel) => channel.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
})
|
||||||
|
channels: Channel[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.template)
|
||||||
|
template_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "template_id", referencedColumnName: "id" })
|
||||||
|
@ManyToOne(() => Template)
|
||||||
|
template: Template;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "emoji_ids" })
|
||||||
|
@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
emojis: Emoji[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "sticker_ids" })
|
||||||
|
@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
stickers: Sticker[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "invite_ids" })
|
||||||
|
@OneToMany(() => Invite, (invite: Invite) => invite.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
invites: Invite[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "voice_state_ids" })
|
||||||
|
@OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
voice_states: VoiceState[];
|
||||||
|
|
||||||
|
@JoinColumn({ name: "webhook_ids" })
|
||||||
|
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
webhooks: Webhook[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
mfa_level?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.owner)
|
||||||
|
owner_id?: string; // optional to allow for ownerless guilds
|
||||||
|
|
||||||
|
@JoinColumn({ name: "owner_id", referencedColumnName: "id" })
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
owner?: User; // optional to allow for ownerless guilds
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
preferred_locale?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
premium_subscription_count?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
premium_tier?: number; // crowd premium level
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.public_updates_channel)
|
||||||
|
public_updates_channel_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "public_updates_channel_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
public_updates_channel?: Channel;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.rules_channel)
|
||||||
|
rules_channel_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "rules_channel_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
rules_channel?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
region?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
splash?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.system_channel)
|
||||||
|
system_channel_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "system_channel_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
system_channel?: Channel;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
system_channel_flags?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
unavailable: boolean = false;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
verification_level?: number;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
welcome_screen: GuildWelcomeScreen;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((guild: Guild) => guild.widget_channel)
|
||||||
|
widget_channel_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "widget_channel_id" })
|
||||||
|
@ManyToOne(() => Channel)
|
||||||
|
widget_channel?: Channel;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
widget_enabled: boolean = true;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
nsfw_level?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
nsfw: boolean = false;
|
||||||
|
|
||||||
|
// TODO: nested guilds
|
||||||
|
@Column({ nullable: true })
|
||||||
|
parent?: string;
|
||||||
|
|
||||||
|
// only for developer portal
|
||||||
|
permissions?: number;
|
||||||
|
|
||||||
|
//new guild settings, 11/08/2022:
|
||||||
|
@Column({ nullable: true })
|
||||||
|
premium_progress_bar_enabled: boolean = false;
|
||||||
|
|
||||||
|
@Column({ select: false, type: "simple-array" })
|
||||||
|
channel_ordering: string[];
|
||||||
|
|
||||||
|
static async createGuild(body: {
|
||||||
|
name?: string;
|
||||||
|
icon?: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
channels?: Partial<Channel>[];
|
||||||
|
}) {
|
||||||
|
const guild_id = Snowflake.generate();
|
||||||
|
|
||||||
|
const guild = await Guild.create({
|
||||||
|
id: guild_id,
|
||||||
|
name: body.name || "Valkyrie",
|
||||||
|
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
|
||||||
|
owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
|
||||||
|
presence_count: 0,
|
||||||
|
member_count: 0, // will automatically be increased by addMember()
|
||||||
|
mfa_level: 0,
|
||||||
|
preferred_locale: "en-US",
|
||||||
|
premium_subscription_count: 0,
|
||||||
|
premium_tier: 0,
|
||||||
|
system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
|
||||||
|
nsfw_level: 0,
|
||||||
|
verification_level: 0,
|
||||||
|
welcome_screen: {
|
||||||
|
enabled: false,
|
||||||
|
description: "",
|
||||||
|
welcome_channels: [],
|
||||||
|
},
|
||||||
|
channel_ordering: [],
|
||||||
|
|
||||||
|
afk_timeout: Config.get().defaults.guild.afkTimeout,
|
||||||
|
default_message_notifications:
|
||||||
|
Config.get().defaults.guild.defaultMessageNotifications,
|
||||||
|
explicit_content_filter:
|
||||||
|
Config.get().defaults.guild.explicitContentFilter,
|
||||||
|
features: Config.get().guild.defaultFeatures,
|
||||||
|
max_members: Config.get().limits.guild.maxMembers,
|
||||||
|
max_presences: Config.get().defaults.guild.maxPresences,
|
||||||
|
max_video_channel_users:
|
||||||
|
Config.get().defaults.guild.maxVideoChannelUsers,
|
||||||
|
region: Config.get().regions.default,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
|
||||||
|
// TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
|
||||||
|
await Role.create({
|
||||||
|
id: guild_id,
|
||||||
|
guild_id: guild_id,
|
||||||
|
color: 0,
|
||||||
|
hoist: false,
|
||||||
|
managed: false,
|
||||||
|
// NB: in Valkyrie, 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?
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
if (!body.channels || !body.channels.length)
|
||||||
|
body.channels = [
|
||||||
|
{ id: "01", type: 0, name: "general", nsfw: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ids = new Map();
|
||||||
|
|
||||||
|
body.channels.forEach((x) => {
|
||||||
|
if (x.id) {
|
||||||
|
ids.set(x.id, Snowflake.generate());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const channel of body.channels.sort((a) =>
|
||||||
|
a.parent_id ? 1 : -1,
|
||||||
|
)) {
|
||||||
|
const id = ids.get(channel.id) || Snowflake.generate();
|
||||||
|
|
||||||
|
const parent_id = ids.get(channel.parent_id);
|
||||||
|
|
||||||
|
const saved = await Channel.createChannel(
|
||||||
|
{ ...channel, guild_id, id, parent_id },
|
||||||
|
body.owner_id,
|
||||||
|
{
|
||||||
|
keepId: true,
|
||||||
|
skipExistsCheck: true,
|
||||||
|
skipPermissionCheck: true,
|
||||||
|
skipEventEmit: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await Guild.insertChannelInOrder(
|
||||||
|
guild.id,
|
||||||
|
saved.id,
|
||||||
|
parent_id ?? channel.position ?? 0,
|
||||||
|
guild,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return guild;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insert a channel into the guild ordering by parent channel id or position */
|
||||||
|
static async insertChannelInOrder(
|
||||||
|
guild_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
position: number,
|
||||||
|
guild?: Guild,
|
||||||
|
): Promise<number>;
|
||||||
|
static async insertChannelInOrder(
|
||||||
|
guild_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
parent_id: string,
|
||||||
|
guild?: Guild,
|
||||||
|
): Promise<number>;
|
||||||
|
static async insertChannelInOrder(
|
||||||
|
guild_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
insertPoint: string | number,
|
||||||
|
guild?: Guild,
|
||||||
|
): Promise<number>;
|
||||||
|
static async insertChannelInOrder(
|
||||||
|
guild_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
insertPoint: string | number,
|
||||||
|
guild?: Guild,
|
||||||
|
): Promise<number> {
|
||||||
|
if (!guild)
|
||||||
|
guild = await Guild.findOneOrFail({
|
||||||
|
where: { id: guild_id },
|
||||||
|
select: { channel_ordering: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
let position;
|
||||||
|
if (typeof insertPoint == "string")
|
||||||
|
position = guild.channel_ordering.indexOf(insertPoint) + 1;
|
||||||
|
else position = insertPoint;
|
||||||
|
|
||||||
|
guild.channel_ordering.remove(channel_id);
|
||||||
|
|
||||||
|
guild.channel_ordering.splice(position, 0, channel_id);
|
||||||
|
await Guild.update(
|
||||||
|
{ id: guild_id },
|
||||||
|
{ channel_ordering: guild.channel_ordering },
|
||||||
|
);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): Guild {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
unavailable: this.unavailable == false ? undefined : true,
|
||||||
|
channel_ordering: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
95
src/util/entities/Invite.ts
Normal file
95
src/util/entities/Invite.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
|
||||||
|
import { Channel } from "./Channel";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { Member } from "./Member";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export const PublicInviteRelation = ["inviter", "guild", "channel"];
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "invites",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Invite extends BaseClassWithoutId {
|
||||||
|
@PrimaryIdColumn()
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
temporary: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
uses: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
max_uses: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
max_age: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
expires_at?: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((invite: Invite) => invite.guild)
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, (guild) => guild.invites, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((invite: Invite) => invite.channel)
|
||||||
|
channel_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "channel_id" })
|
||||||
|
@ManyToOne(() => Channel, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
channel: Channel;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((invite: Invite) => invite.inviter)
|
||||||
|
inviter_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "inviter_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
inviter: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((invite: Invite) => invite.target_user)
|
||||||
|
target_user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "target_user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
target_user?: string; // could be used for "User specific invites" https://toastielab.dev/ValkyrieChat/Server/issues/326
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
target_user_type?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
vanity_url?: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
flags: number;
|
||||||
|
|
||||||
|
static async joinGuild(user_id: string, code: string) {
|
||||||
|
const invite = await Invite.findOneOrFail({ where: { code } });
|
||||||
|
if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0)
|
||||||
|
await Invite.delete({ code });
|
||||||
|
else await invite.save();
|
||||||
|
|
||||||
|
await Member.addToGuild(user_id, invite.guild_id);
|
||||||
|
return invite;
|
||||||
|
}
|
||||||
|
}
|
539
src/util/entities/Member.ts
Normal file
539
src/util/entities/Member.ts
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
import { HTTPError } from "lambert-server";
|
||||||
|
import {
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
Not,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { Ban, Channel, PublicGuildRelations } from ".";
|
||||||
|
import { ReadyGuildDTO } from "../dtos";
|
||||||
|
import {
|
||||||
|
GuildCreateEvent,
|
||||||
|
GuildDeleteEvent,
|
||||||
|
GuildMemberAddEvent,
|
||||||
|
GuildMemberRemoveEvent,
|
||||||
|
GuildMemberUpdateEvent,
|
||||||
|
MessageCreateEvent,
|
||||||
|
} from "../interfaces";
|
||||||
|
import { Config, emitEvent } from "../util";
|
||||||
|
import { DiscordApiErrors } from "../util/Constants";
|
||||||
|
import { BaseClassWithoutId } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { Message } from "./Message";
|
||||||
|
import { Role } from "./Role";
|
||||||
|
import { PublicUser, User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export const MemberPrivateProjection: (keyof Member)[] = [
|
||||||
|
"id",
|
||||||
|
"guild",
|
||||||
|
"guild_id",
|
||||||
|
"deaf",
|
||||||
|
"joined_at",
|
||||||
|
"last_message_id",
|
||||||
|
"mute",
|
||||||
|
"nick",
|
||||||
|
"pending",
|
||||||
|
"premium_since",
|
||||||
|
"roles",
|
||||||
|
"settings",
|
||||||
|
"user",
|
||||||
|
];
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "members",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
@Index(["id", "guild_id"], { unique: true })
|
||||||
|
export class Member extends BaseClassWithoutId {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
index: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@RelationId((member: Member) => member.user)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@RelationId((member: Member) => member.guild)
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
nick?: string;
|
||||||
|
|
||||||
|
@JoinTable({
|
||||||
|
name: "member_roles",
|
||||||
|
joinColumn: { name: "index", referencedColumnName: "index" },
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: "role_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ManyToMany(() => Role, { cascade: true })
|
||||||
|
roles: Role[];
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
joined_at: Date;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
premium_since?: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
deaf: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
mute: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
pending: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", select: false })
|
||||||
|
settings: UserGuildSettings;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
last_message_id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@JoinColumn({ name: "id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "DO NOTHING",
|
||||||
|
// do not auto-kick force-joined members just because their joiners left the server
|
||||||
|
}) **/
|
||||||
|
@Column({ nullable: true })
|
||||||
|
joined_by: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
banner: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
bio: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: "simple-array" })
|
||||||
|
theme_colors?: number[]; // TODO: Separate `User` and `UserProfile` models
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
pronouns?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
communication_disabled_until: Date;
|
||||||
|
|
||||||
|
// TODO: add this when we have proper read receipts
|
||||||
|
// @Column({ type: "simple-json" })
|
||||||
|
// read_state: ReadState;
|
||||||
|
|
||||||
|
@BeforeUpdate()
|
||||||
|
@BeforeInsert()
|
||||||
|
validate() {
|
||||||
|
if (this.nick) {
|
||||||
|
this.nick = this.nick.split("\n").join("");
|
||||||
|
this.nick = this.nick.split("\t").join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async IsInGuildOrFail(user_id: string, guild_id: string) {
|
||||||
|
if (
|
||||||
|
await Member.count({
|
||||||
|
where: { id: user_id, guild: { id: guild_id } },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
throw new HTTPError("You are not member of this guild", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeFromGuild(user_id: string, guild_id: string) {
|
||||||
|
const guild = await Guild.findOneOrFail({
|
||||||
|
select: ["owner_id"],
|
||||||
|
where: { id: guild_id },
|
||||||
|
});
|
||||||
|
if (guild.owner_id === user_id)
|
||||||
|
throw new Error("The owner cannot be removed of the guild");
|
||||||
|
const member = await Member.findOneOrFail({
|
||||||
|
where: { id: user_id, guild_id },
|
||||||
|
relations: ["user"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// use promise all to execute all promises at the same time -> save time
|
||||||
|
return Promise.all([
|
||||||
|
Member.delete({
|
||||||
|
id: user_id,
|
||||||
|
guild_id,
|
||||||
|
}),
|
||||||
|
Guild.decrement({ id: guild_id }, "member_count", -1),
|
||||||
|
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_DELETE",
|
||||||
|
data: {
|
||||||
|
id: guild_id,
|
||||||
|
},
|
||||||
|
user_id: user_id,
|
||||||
|
} as GuildDeleteEvent),
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_MEMBER_REMOVE",
|
||||||
|
data: { guild_id, user: member.user },
|
||||||
|
guild_id,
|
||||||
|
} as GuildMemberRemoveEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addRole(user_id: string, guild_id: string, role_id: string) {
|
||||||
|
const [member] = await Promise.all([
|
||||||
|
Member.findOneOrFail({
|
||||||
|
where: { id: user_id, guild_id },
|
||||||
|
relations: ["user", "roles"], // we don't want to load the role objects just the ids
|
||||||
|
select: {
|
||||||
|
index: true,
|
||||||
|
roles: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Role.findOneOrFail({
|
||||||
|
where: { id: role_id, guild_id },
|
||||||
|
select: ["id"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
member.roles.push(Role.create({ id: role_id }));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
member.save(),
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_MEMBER_UPDATE",
|
||||||
|
data: {
|
||||||
|
guild_id,
|
||||||
|
user: member.user,
|
||||||
|
roles: member.roles.map((x) => x.id),
|
||||||
|
},
|
||||||
|
guild_id,
|
||||||
|
} as GuildMemberUpdateEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeRole(
|
||||||
|
user_id: string,
|
||||||
|
guild_id: string,
|
||||||
|
role_id: string,
|
||||||
|
) {
|
||||||
|
const [member] = await Promise.all([
|
||||||
|
Member.findOneOrFail({
|
||||||
|
where: { id: user_id, guild_id },
|
||||||
|
relations: ["user", "roles"], // we don't want to load the role objects just the ids
|
||||||
|
select: {
|
||||||
|
index: true,
|
||||||
|
roles: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Role.findOneOrFail({ where: { id: role_id, guild_id } }),
|
||||||
|
]);
|
||||||
|
member.roles = member.roles.filter((x) => x.id !== role_id);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
member.save(),
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_MEMBER_UPDATE",
|
||||||
|
data: {
|
||||||
|
guild_id,
|
||||||
|
user: member.user,
|
||||||
|
roles: member.roles.map((x) => x.id),
|
||||||
|
},
|
||||||
|
guild_id,
|
||||||
|
} as GuildMemberUpdateEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async changeNickname(
|
||||||
|
user_id: string,
|
||||||
|
guild_id: string,
|
||||||
|
nickname: string,
|
||||||
|
) {
|
||||||
|
const member = await Member.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
id: user_id,
|
||||||
|
guild_id,
|
||||||
|
},
|
||||||
|
relations: ["user"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error Member nickname is nullable
|
||||||
|
member.nick = nickname || null;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
member.save(),
|
||||||
|
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_MEMBER_UPDATE",
|
||||||
|
data: {
|
||||||
|
guild_id,
|
||||||
|
user: member.user,
|
||||||
|
nick: nickname || null,
|
||||||
|
},
|
||||||
|
guild_id,
|
||||||
|
} as GuildMemberUpdateEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addToGuild(user_id: string, guild_id: string) {
|
||||||
|
const user = await User.getPublicUser(user_id);
|
||||||
|
const isBanned = await Ban.count({ where: { guild_id, user_id } });
|
||||||
|
if (isBanned) {
|
||||||
|
throw DiscordApiErrors.USER_BANNED;
|
||||||
|
}
|
||||||
|
const { maxGuilds } = Config.get().limits.user;
|
||||||
|
const guild_count = await Member.count({ where: { id: user_id } });
|
||||||
|
if (guild_count >= maxGuilds) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`You are at the ${maxGuilds} server limit.`,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await Guild.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
id: guild_id,
|
||||||
|
},
|
||||||
|
relations: PublicGuildRelations,
|
||||||
|
relationLoadStrategy: "query",
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const channel of guild.channels) {
|
||||||
|
channel.position = await Channel.calculatePosition(
|
||||||
|
channel.id,
|
||||||
|
guild_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberCount = await Member.count({ where: { guild_id } });
|
||||||
|
|
||||||
|
const memberPreview = (
|
||||||
|
await Member.find({
|
||||||
|
where: {
|
||||||
|
guild_id,
|
||||||
|
user: {
|
||||||
|
sessions: {
|
||||||
|
status: Not("invisible" as const), // lol typescript?
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ["user", "roles"],
|
||||||
|
take: 10,
|
||||||
|
})
|
||||||
|
).map((member) => member.toPublicMember());
|
||||||
|
|
||||||
|
if (
|
||||||
|
await Member.count({
|
||||||
|
where: { id: user.id, guild: { id: guild_id } },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw new HTTPError("You are already a member of this guild", 400);
|
||||||
|
|
||||||
|
const member = {
|
||||||
|
id: user_id,
|
||||||
|
guild_id,
|
||||||
|
nick: undefined,
|
||||||
|
roles: [guild_id], // @everyone role
|
||||||
|
joined_at: new Date(),
|
||||||
|
deaf: false,
|
||||||
|
mute: false,
|
||||||
|
pending: false,
|
||||||
|
bio: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
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(),
|
||||||
|
Guild.increment({ id: guild_id }, "member_count", 1),
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_MEMBER_ADD",
|
||||||
|
data: {
|
||||||
|
...member,
|
||||||
|
user,
|
||||||
|
guild_id,
|
||||||
|
},
|
||||||
|
guild_id,
|
||||||
|
} as GuildMemberAddEvent),
|
||||||
|
emitEvent({
|
||||||
|
event: "GUILD_CREATE",
|
||||||
|
data: {
|
||||||
|
...new ReadyGuildDTO(guild).toJSON(),
|
||||||
|
members: [...memberPreview, { ...member, user }],
|
||||||
|
member_count: memberCount + 1,
|
||||||
|
guild_hashes: {},
|
||||||
|
guild_scheduled_events: [],
|
||||||
|
joined_at: member.joined_at,
|
||||||
|
presences: [],
|
||||||
|
stage_instances: [],
|
||||||
|
threads: [],
|
||||||
|
embedded_activities: [],
|
||||||
|
voice_states: guild.voice_states,
|
||||||
|
},
|
||||||
|
user_id,
|
||||||
|
} as GuildCreateEvent),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (guild.system_channel_id) {
|
||||||
|
// Send a welcome message
|
||||||
|
const message = Message.create({
|
||||||
|
type: 7,
|
||||||
|
guild_id: guild.id,
|
||||||
|
channel_id: guild.system_channel_id,
|
||||||
|
author: user,
|
||||||
|
timestamp: new Date(),
|
||||||
|
reactions: [],
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
sticker_items: [],
|
||||||
|
edited_timestamp: undefined,
|
||||||
|
mentions: [],
|
||||||
|
mention_channels: [],
|
||||||
|
mention_roles: [],
|
||||||
|
mention_everyone: false,
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
message.save(),
|
||||||
|
emitEvent({
|
||||||
|
event: "MESSAGE_CREATE",
|
||||||
|
channel_id: message.channel_id,
|
||||||
|
data: message,
|
||||||
|
} as MessageCreateEvent),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toPublicMember() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const member: any = {};
|
||||||
|
PublicMemberProjection.forEach((x) => {
|
||||||
|
member[x] = this[x];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (member.roles) member.roles = member.roles.map((x: Role) => x.id);
|
||||||
|
if (member.user) member.user = member.user.toPublicUser();
|
||||||
|
|
||||||
|
return member as PublicMember;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelOverride {
|
||||||
|
message_notifications: number;
|
||||||
|
mute_config: MuteConfig;
|
||||||
|
muted: boolean;
|
||||||
|
channel_id: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserGuildSettings {
|
||||||
|
// channel_overrides: {
|
||||||
|
// channel_id: string;
|
||||||
|
// message_notifications: number;
|
||||||
|
// mute_config: MuteConfig;
|
||||||
|
// muted: boolean;
|
||||||
|
// }[];
|
||||||
|
|
||||||
|
channel_overrides: {
|
||||||
|
[channel_id: string]: ChannelOverride;
|
||||||
|
} | null;
|
||||||
|
message_notifications: number;
|
||||||
|
mobile_push: boolean;
|
||||||
|
mute_config: MuteConfig | null;
|
||||||
|
muted: boolean;
|
||||||
|
suppress_everyone: boolean;
|
||||||
|
suppress_roles: boolean;
|
||||||
|
version: number;
|
||||||
|
guild_id: string | null;
|
||||||
|
flags: number;
|
||||||
|
mute_scheduled_events: boolean;
|
||||||
|
hide_muted_channels: boolean;
|
||||||
|
notify_highlights: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultUserGuildSettings: UserGuildSettings = {
|
||||||
|
channel_overrides: null,
|
||||||
|
message_notifications: 1,
|
||||||
|
flags: 0,
|
||||||
|
hide_muted_channels: false,
|
||||||
|
mobile_push: true,
|
||||||
|
mute_config: null,
|
||||||
|
mute_scheduled_events: false,
|
||||||
|
muted: false,
|
||||||
|
notify_highlights: 0,
|
||||||
|
suppress_everyone: false,
|
||||||
|
suppress_roles: false,
|
||||||
|
version: 453, // ?
|
||||||
|
guild_id: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MuteConfig {
|
||||||
|
end_time: number;
|
||||||
|
selected_time_window: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PublicMemberKeys =
|
||||||
|
| "id"
|
||||||
|
| "guild_id"
|
||||||
|
| "nick"
|
||||||
|
| "roles"
|
||||||
|
| "joined_at"
|
||||||
|
| "pending"
|
||||||
|
| "deaf"
|
||||||
|
| "mute"
|
||||||
|
| "premium_since"
|
||||||
|
| "avatar";
|
||||||
|
|
||||||
|
export const PublicMemberProjection: PublicMemberKeys[] = [
|
||||||
|
"id",
|
||||||
|
"guild_id",
|
||||||
|
"nick",
|
||||||
|
"roles",
|
||||||
|
"joined_at",
|
||||||
|
"pending",
|
||||||
|
"deaf",
|
||||||
|
"mute",
|
||||||
|
"premium_since",
|
||||||
|
"avatar",
|
||||||
|
];
|
||||||
|
|
||||||
|
export type PublicMember = Omit<Pick<Member, PublicMemberKeys>, "roles"> & {
|
||||||
|
user: PublicUser;
|
||||||
|
roles: string[]; // only role ids not objects
|
||||||
|
};
|
439
src/util/entities/Message.ts
Normal file
439
src/util/entities/Message.ts
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
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 {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { Webhook } from "./Webhook";
|
||||||
|
import { Sticker } from "./Sticker";
|
||||||
|
import { Attachment } from "./Attachment";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export enum MessageType {
|
||||||
|
DEFAULT = 0,
|
||||||
|
RECIPIENT_ADD = 1,
|
||||||
|
RECIPIENT_REMOVE = 2,
|
||||||
|
CALL = 3,
|
||||||
|
CHANNEL_NAME_CHANGE = 4,
|
||||||
|
CHANNEL_ICON_CHANGE = 5,
|
||||||
|
CHANNEL_PINNED_MESSAGE = 6,
|
||||||
|
GUILD_MEMBER_JOIN = 7,
|
||||||
|
USER_PREMIUM_GUILD_SUBSCRIPTION = 8,
|
||||||
|
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
|
||||||
|
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
|
||||||
|
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
|
||||||
|
CHANNEL_FOLLOW_ADD = 12,
|
||||||
|
ACTION = 13, // /me messages
|
||||||
|
GUILD_DISCOVERY_DISQUALIFIED = 14,
|
||||||
|
GUILD_DISCOVERY_REQUALIFIED = 15,
|
||||||
|
ENCRYPTED = 16,
|
||||||
|
REPLY = 19,
|
||||||
|
APPLICATION_COMMAND = 20, // application command or self command invocation
|
||||||
|
ROUTE_ADDED = 41, // custom message routing: new route affecting that channel
|
||||||
|
ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel
|
||||||
|
SELF_COMMAND_SCRIPT = 43, // self command scripts
|
||||||
|
ENCRYPTION = 50,
|
||||||
|
CUSTOM_START = 63,
|
||||||
|
UNHANDLED = 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "messages",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
@Index(["channel_id", "id"], { unique: true })
|
||||||
|
export class Message extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.channel)
|
||||||
|
@Index()
|
||||||
|
channel_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "channel_id" })
|
||||||
|
@ManyToOne(() => Channel, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
channel: Channel;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.guild)
|
||||||
|
guild_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild?: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.author)
|
||||||
|
@Index()
|
||||||
|
author_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "author_id", referencedColumnName: "id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
author?: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.member)
|
||||||
|
member_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "member_id", referencedColumnName: "id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
member?: Member;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.webhook)
|
||||||
|
webhook_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "webhook_id" })
|
||||||
|
@ManyToOne(() => Webhook)
|
||||||
|
webhook?: Webhook;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((message: Message) => message.application)
|
||||||
|
application_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "application_id" })
|
||||||
|
@ManyToOne(() => Application)
|
||||||
|
application?: Application;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@CreateDateColumn()
|
||||||
|
timestamp: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
edited_timestamp?: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
tts?: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
mention_everyone?: boolean;
|
||||||
|
|
||||||
|
@JoinTable({ name: "message_user_mentions" })
|
||||||
|
@ManyToMany(() => User)
|
||||||
|
mentions: User[];
|
||||||
|
|
||||||
|
@JoinTable({ name: "message_role_mentions" })
|
||||||
|
@ManyToMany(() => Role)
|
||||||
|
mention_roles: Role[];
|
||||||
|
|
||||||
|
@JoinTable({ name: "message_channel_mentions" })
|
||||||
|
@ManyToMany(() => Channel)
|
||||||
|
mention_channels: Channel[];
|
||||||
|
|
||||||
|
@JoinTable({ name: "message_stickers" })
|
||||||
|
@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
|
||||||
|
sticker_items?: Sticker[];
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => Attachment,
|
||||||
|
(attachment: Attachment) => attachment.message,
|
||||||
|
{
|
||||||
|
cascade: true,
|
||||||
|
orphanedRowAction: "delete",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
attachments?: Attachment[];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
embeds: Embed[];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
reactions: Reaction[];
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
nonce?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
pinned?: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
type: MessageType;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
activity?: {
|
||||||
|
type: number;
|
||||||
|
party_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
flags: number;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
message_reference?: {
|
||||||
|
message_id: string;
|
||||||
|
channel_id?: string;
|
||||||
|
guild_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@JoinColumn({ name: "message_reference_id" })
|
||||||
|
@ManyToOne(() => Message)
|
||||||
|
referenced_message?: Message;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
interaction?: {
|
||||||
|
id: string;
|
||||||
|
type: InteractionType;
|
||||||
|
name: string;
|
||||||
|
user_id: string; // the user who invoked the interaction
|
||||||
|
// user: User; // TODO: autopopulate user
|
||||||
|
};
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
components?: ActionRowComponent[];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
poll?: Poll;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
toJSON(): Message {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
author_id: undefined,
|
||||||
|
member_id: undefined,
|
||||||
|
webhook_id: this.webhook_id ?? undefined,
|
||||||
|
application_id: undefined,
|
||||||
|
|
||||||
|
nonce: this.nonce ?? undefined,
|
||||||
|
tts: this.tts ?? false,
|
||||||
|
guild: this.guild ?? undefined,
|
||||||
|
webhook: this.webhook ?? undefined,
|
||||||
|
interaction: this.interaction ?? undefined,
|
||||||
|
reactions: this.reactions ?? undefined,
|
||||||
|
sticker_items: this.sticker_items ?? undefined,
|
||||||
|
message_reference: this.message_reference ?? undefined,
|
||||||
|
author: {
|
||||||
|
...(this.author?.toPublicUser() ?? undefined),
|
||||||
|
// Webhooks
|
||||||
|
username: this.username ?? this.author?.username,
|
||||||
|
avatar: this.avatar ?? this.author?.avatar,
|
||||||
|
},
|
||||||
|
activity: this.activity ?? undefined,
|
||||||
|
application: this.application ?? undefined,
|
||||||
|
components: this.components ?? undefined,
|
||||||
|
poll: this.poll ?? undefined,
|
||||||
|
content: this.content ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageComponent {
|
||||||
|
type: MessageComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionRowComponent extends MessageComponent {
|
||||||
|
type: MessageComponentType.ActionRow;
|
||||||
|
components: (
|
||||||
|
| ButtonComponent
|
||||||
|
| StringSelectMenuComponent
|
||||||
|
| SelectMenuComponent
|
||||||
|
| TextInputComponent
|
||||||
|
)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonComponent extends MessageComponent {
|
||||||
|
type: MessageComponentType.Button;
|
||||||
|
style: ButtonStyle;
|
||||||
|
label?: string;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
custom_id?: string;
|
||||||
|
sku_id?: string;
|
||||||
|
url?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ButtonStyle {
|
||||||
|
Primary = 1,
|
||||||
|
Secondary = 2,
|
||||||
|
Success = 3,
|
||||||
|
Danger = 4,
|
||||||
|
Link = 5,
|
||||||
|
Premium = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectMenuComponent extends MessageComponent {
|
||||||
|
type:
|
||||||
|
| MessageComponentType.StringSelect
|
||||||
|
| MessageComponentType.UserSelect
|
||||||
|
| MessageComponentType.RoleSelect
|
||||||
|
| MessageComponentType.MentionableSelect
|
||||||
|
| MessageComponentType.ChannelSelect;
|
||||||
|
custom_id: string;
|
||||||
|
channel_types?: number[];
|
||||||
|
placeholder?: string;
|
||||||
|
default_values?: SelectMenuDefaultOption[]; // only for non-string selects
|
||||||
|
min_values?: number;
|
||||||
|
max_values?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectMenuOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
description?: string;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectMenuDefaultOption {
|
||||||
|
id: string;
|
||||||
|
type: "user" | "role" | "channel";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringSelectMenuComponent extends SelectMenuComponent {
|
||||||
|
type: MessageComponentType.StringSelect;
|
||||||
|
options: SelectMenuOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextInputComponent extends MessageComponent {
|
||||||
|
type: MessageComponentType.TextInput;
|
||||||
|
custom_id: string;
|
||||||
|
style: TextInputStyle;
|
||||||
|
label: string;
|
||||||
|
min_length?: number;
|
||||||
|
max_length?: number;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TextInputStyle {
|
||||||
|
Short = 1,
|
||||||
|
Paragraph = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageComponentType {
|
||||||
|
Script = 0, // self command script
|
||||||
|
ActionRow = 1,
|
||||||
|
Button = 2,
|
||||||
|
StringSelect = 3,
|
||||||
|
TextInput = 4,
|
||||||
|
UserSelect = 5,
|
||||||
|
RoleSelect = 6,
|
||||||
|
MentionableSelect = 7,
|
||||||
|
ChannelSelect = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Embed {
|
||||||
|
title?: string; //title of embed
|
||||||
|
type?: EmbedType; // type of embed (always "rich" for webhook embeds)
|
||||||
|
description?: string; // description of embed
|
||||||
|
url?: string; // url of embed
|
||||||
|
timestamp?: Date; // timestamp of embed content
|
||||||
|
color?: number; // color code of the embed
|
||||||
|
footer?: {
|
||||||
|
text: string;
|
||||||
|
icon_url?: string;
|
||||||
|
proxy_icon_url?: string;
|
||||||
|
}; // footer object footer information
|
||||||
|
image?: EmbedImage; // image object image information
|
||||||
|
thumbnail?: EmbedImage; // thumbnail object thumbnail information
|
||||||
|
video?: EmbedImage; // video object video information
|
||||||
|
provider?: {
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}; // provider object provider information
|
||||||
|
author?: {
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
proxy_icon_url?: string;
|
||||||
|
}; // author object author information
|
||||||
|
fields?: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
inline?: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EmbedType {
|
||||||
|
rich = "rich",
|
||||||
|
image = "image",
|
||||||
|
video = "video",
|
||||||
|
gifv = "gifv",
|
||||||
|
article = "article",
|
||||||
|
link = "link",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbedImage {
|
||||||
|
url?: string;
|
||||||
|
proxy_url?: string;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Reaction {
|
||||||
|
count: number;
|
||||||
|
//// not saved in the database // me: boolean; // whether the current user reacted using this emoji
|
||||||
|
emoji: PartialEmoji;
|
||||||
|
user_ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartialEmoji {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
animated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllowedMentions {
|
||||||
|
parse?: ("users" | "roles" | "everyone")[];
|
||||||
|
roles?: string[];
|
||||||
|
users?: string[];
|
||||||
|
replied_user?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Poll {
|
||||||
|
question: PollMedia;
|
||||||
|
answers: PollAnswer[];
|
||||||
|
expiry: Date;
|
||||||
|
allow_multiselect: boolean;
|
||||||
|
results?: PollResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollMedia {
|
||||||
|
text?: string;
|
||||||
|
emoji?: PartialEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollAnswer {
|
||||||
|
answer_id?: string;
|
||||||
|
poll_media: PollMedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollResult {
|
||||||
|
is_finalized: boolean;
|
||||||
|
answer_counts: PollAnswerCount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollAnswerCount {
|
||||||
|
id: string;
|
||||||
|
count: number;
|
||||||
|
me_voted: boolean;
|
||||||
|
}
|
29
src/util/entities/Migration.ts
Normal file
29
src/util/entities/Migration.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
ObjectIdColumn,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
BaseEntity,
|
||||||
|
} from "typeorm";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith(
|
||||||
|
"mongodb",
|
||||||
|
)
|
||||||
|
? ObjectIdColumn
|
||||||
|
: PrimaryGeneratedColumn;
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "migrations",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Migration extends BaseEntity {
|
||||||
|
@PrimaryIdAutoGenerated()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: "bigint" })
|
||||||
|
timestamp: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
}
|
22
src/util/entities/Note.ts
Normal file
22
src/util/entities/Note.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "notes",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
@Unique(["owner", "target"])
|
||||||
|
export class Note extends BaseClass {
|
||||||
|
@JoinColumn({ name: "owner_id" })
|
||||||
|
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||||
|
owner: User;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "target_id" })
|
||||||
|
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||||
|
target: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
content: string;
|
||||||
|
}
|
21
src/util/entities/RateLimit.ts
Normal file
21
src/util/entities/RateLimit.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "rate_limits",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class RateLimit extends BaseClass {
|
||||||
|
@Column() // no relation as it also
|
||||||
|
executor_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
hits: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
blocked: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
expires_at: Date;
|
||||||
|
}
|
65
src/util/entities/ReadState.ts
Normal file
65
src/util/entities/ReadState.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Channel } from "./Channel";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
// for read receipts
|
||||||
|
// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable
|
||||||
|
// public read receipt ≥ notification cursor ≥ private fully read marker
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "read_states",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
@Index(["channel_id", "user_id"], { unique: true })
|
||||||
|
export class ReadState extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
@RelationId((read_state: ReadState) => read_state.channel)
|
||||||
|
channel_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "channel_id" })
|
||||||
|
@ManyToOne(() => Channel, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
channel: Channel;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@RelationId((read_state: ReadState) => read_state.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
// fully read marker
|
||||||
|
@Column({ nullable: true })
|
||||||
|
last_message_id: string;
|
||||||
|
|
||||||
|
// public read receipt
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public_ack: string;
|
||||||
|
|
||||||
|
// notification cursor / private read receipt
|
||||||
|
@Column({ nullable: true })
|
||||||
|
notifications_cursor: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
last_pin_timestamp?: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
mention_count: number;
|
||||||
|
|
||||||
|
// @Column({ nullable: true })
|
||||||
|
// TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true
|
||||||
|
manual: boolean;
|
||||||
|
}
|
34
src/util/entities/Recipient.ts
Normal file
34
src/util/entities/Recipient.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "recipients",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Recipient extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
@RelationId((recipient: Recipient) => recipient.channel)
|
||||||
|
channel_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "channel_id" })
|
||||||
|
@ManyToOne(() => require("./Channel").Channel, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
channel: import("./Channel").Channel;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@RelationId((recipient: Recipient) => recipient.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => require("./User").User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: import("./User").User;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
closed: boolean;
|
||||||
|
|
||||||
|
// TODO: settings/mute/nick/added at/encryption keys/read_state
|
||||||
|
}
|
60
src/util/entities/Relationship.ts
Normal file
60
src/util/entities/Relationship.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
RelationId,
|
||||||
|
} from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export enum RelationshipType {
|
||||||
|
outgoing = 4,
|
||||||
|
incoming = 3,
|
||||||
|
blocked = 2,
|
||||||
|
friends = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "relationships",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
@Index(["from_id", "to_id"], { unique: true })
|
||||||
|
export class Relationship extends BaseClass {
|
||||||
|
@Column({})
|
||||||
|
@RelationId((relationship: Relationship) => relationship.from)
|
||||||
|
from_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "from_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
from: User;
|
||||||
|
|
||||||
|
@Column({})
|
||||||
|
@RelationId((relationship: Relationship) => relationship.to)
|
||||||
|
to_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "to_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
to: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
type: RelationshipType;
|
||||||
|
|
||||||
|
toPublicRelationship() {
|
||||||
|
return {
|
||||||
|
id: this.to?.id || this.to_id,
|
||||||
|
type: this.type,
|
||||||
|
nickname: this.nickname,
|
||||||
|
user: this.to?.toPublicUser(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
65
src/util/entities/Role.ts
Normal file
65
src/util/entities/Role.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "roles",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Role extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
@RelationId((role: Role) => role.guild)
|
||||||
|
guild_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, (guild) => guild.roles, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild: Guild;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
color: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
hoist: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
managed: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
mentionable: boolean;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
permissions: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
position: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
unicode_emoji?: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
tags?: {
|
||||||
|
bot_id?: string;
|
||||||
|
integration_id?: string;
|
||||||
|
premium_subscriber?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
flags: number;
|
||||||
|
|
||||||
|
toJSON(): Role {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
tags: this.tags ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
32
src/util/entities/SecurityKey.ts
Normal file
32
src/util/entities/SecurityKey.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "security_keys",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class SecurityKey extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((key: SecurityKey) => key.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
key_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
public_key: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
counter: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
}
|
52
src/util/entities/Session.ts
Normal file
52
src/util/entities/Session.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { User } from "./User";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { ClientStatus, Status } from "../interfaces/Status";
|
||||||
|
import { Activity } from "../interfaces/Activity";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
//TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "sessions",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Session extends BaseClass {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((session: Session) => session.user)
|
||||||
|
user_id: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
//TODO check, should be 32 char long hex string
|
||||||
|
@Column({ nullable: false, select: false })
|
||||||
|
session_id: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
activities: Activity[];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", select: false })
|
||||||
|
client_info: {
|
||||||
|
client: string;
|
||||||
|
os: string;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
client_status: ClientStatus;
|
||||||
|
|
||||||
|
@Column({ nullable: false, type: "varchar" })
|
||||||
|
status: Status; //TODO enum
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateSessionProjection: (keyof Session)[] = [
|
||||||
|
"user_id",
|
||||||
|
"session_id",
|
||||||
|
"activities",
|
||||||
|
"client_info",
|
||||||
|
"status",
|
||||||
|
];
|
70
src/util/entities/Sticker.ts
Normal file
70
src/util/entities/Sticker.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { BaseClass } from "./BaseClass";
|
||||||
|
import { Guild } from "./Guild";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { dbEngine } from "../util/Database";
|
||||||
|
|
||||||
|
export enum StickerType {
|
||||||
|
STANDARD = 1,
|
||||||
|
GUILD = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StickerFormatType {
|
||||||
|
GIF = 0, // gif is a custom format type and not in discord spec
|
||||||
|
PNG = 1,
|
||||||
|
APNG = 2,
|
||||||
|
LOTTIE = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
name: "stickers",
|
||||||
|
engine: dbEngine,
|
||||||
|
})
|
||||||
|
export class Sticker extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
available?: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
tags?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@RelationId((sticker: Sticker) => sticker.pack)
|
||||||
|
pack_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "pack_id" })
|
||||||
|
@ManyToOne(() => require("./StickerPack").StickerPack, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
pack: import("./StickerPack").StickerPack;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
guild_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "guild_id" })
|
||||||
|
@ManyToOne(() => Guild, (guild) => guild.stickers, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
guild?: Guild;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
user_id?: string;
|
||||||
|
|
||||||
|
@JoinColumn({ name: "user_id" })
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
user?: User;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
type: StickerType;
|
||||||
|
|
||||||
|
@Column({ type: "int" })
|
||||||
|
format_type: StickerFormatType;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue