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