restructure to single project

This commit is contained in:
Flam3rboy
2022-08-13 22:00:55 +02:00
committed by TheArcaneBrony
parent 5abd6bb7e0
commit 5e86d7ab9c
626 changed files with 7256 additions and 2449 deletions
-110
View File
@@ -1,110 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
yarn.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
.DS_Store
# Compiled TypeScript code
dist/
database.db
-1
View File
@@ -1 +0,0 @@
!dist/
-5
View File
@@ -1,5 +0,0 @@
{
"tabWidth": 4,
"useTabs": true,
"printWidth": 120
}
-27
View File
@@ -1,27 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"sourceMaps": true,
"type": "node",
"request": "launch",
"name": "Launch Util",
"program": "${workspaceFolder}/dist/index.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand"],
"preLaunchTask": "tsc: build - tsconfig.json",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
-29
View File
@@ -1,29 +0,0 @@
<p align="center">
<img width="100" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Icon-Rounded-Subtract.svg" />
</p>
<h1 align="center">Fosscord server util</h1>
<p>
<a href="https://discord.gg/ZrnGQP6p3d">
<img src="https://img.shields.io/discord/806142446094385153?color=7489d5&logo=discord&logoColor=ffffff" />
</a>
<img src="https://img.shields.io/static/v1?label=Status&message=Development&color=blue">
<a title="Crowdin" target="_blank" href="https://translate.fosscord.com/"><img src="https://badges.crowdin.net/fosscord/localized.svg"></a>
<a href="https://opencollective.com/fosscord">
<img src="https://opencollective.com/fosscord/tiers/badge.svg">
</a>
</p>
## [About](https://fosscord.com)
Fosscord is a free open source selfhostable chat, voice and video discord-compatible platform.
Fosscord server util contains all necessary logic that is shared between the [api](https://github.com/fosscord/fosscord-server/tree/master/api), [gateway](https://github.com/fosscord/fosscord-server/tree/master/gateway) and [cdn](https://github.com/fosscord/fosscord-server/tree/master/cdn).
It contains all mongoose database models and utility functions.
## Installation
```bash
npm install @fosscord/server-util
```
BIN
View File
Binary file not shown.
-59
View File
@@ -1,59 +0,0 @@
{
"name": "@fosscord/util",
"version": "1.0.0",
"description": "Utility functions for the all server repositories",
"main": "dist/index.js",
"types": "src/index.ts",
"scripts": {
"start": "npm run build && node dist/",
"test": "npm run build && npx jest",
"postinstall": "npm run build",
"build": "npx tsc -p .",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fosscord/fosscord-server.git"
},
"keywords": [
"discord",
"fosscord",
"fosscord-server",
"discord open source",
"discord-open-source"
],
"author": "Fosscord",
"license": "AGPL-3.0-only",
"bugs": {
"url": "https://github.com/fosscord/fosscord-server/issues"
},
"homepage": "https://docs.fosscord.com/",
"devDependencies": {
"@types/amqplib": "^0.8.1",
"@types/jsonwebtoken": "^8.5.8",
"@types/multer": "^1.4.7",
"@types/node": "^18.0.6",
"jest": "^28.1.3",
"ts-node": "^10.2.1"
},
"dependencies": {
"@types/node-fetch": "^2.6.2",
"amqplib": "^0.10.0",
"form-data": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"lambert-server": "^1.2.12",
"missing-native-js-functions": "^1.2.18",
"multer": "^1.4.5-lts.1",
"node-fetch": "^2.6.7",
"picocolors": "^1.0.0",
"proxy-agent": "^5.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.7",
"typescript": "^4.7.4"
},
"jest": {
"setupFilesAfterEnv": [
"./tests/setupJest.js"
]
}
}
-28
View File
@@ -1,28 +0,0 @@
const typeorm = require("typeorm");
const Models = require("../dist/entities");
const { PrimaryColumn } = require("typeorm");
function shouldIncludeEntity(name) {
return ![Models.BaseClassWithoutId, PrimaryColumn, Models.BaseClass, Models.PrimaryGeneratedColumn]
.map((x) => x?.name)
.includes(name);
}
async function main() {
console.log("starting");
const db = new typeorm.DataSource({
type: "sqlite",
database: ":memory:",
entities: Object.values(Models).filter((x) => x.constructor.name == "Function" && shouldIncludeEntity(x.name)),
synchronize: true,
});
await db.initialize();
console.log("Initialized database");
for (var i = 0; i < 100; i++) {
await Models.User.register({ username: "User" + i });
console.log("registered user " + i);
}
}
main();
-109
View File
@@ -1,109 +0,0 @@
const { config } = require("dotenv");
config();
const { createConnection } = require("typeorm");
const { initDatabase } = require("../../dist/util/Database");
require("missing-native-js-functions");
const {
Application,
Attachment,
Ban,
Channel,
ConfigEntity,
ConnectedAccount,
Emoji,
Guild,
Invite,
Member,
Message,
ReadState,
Recipient,
Relationship,
Role,
Sticker,
Team,
TeamMember,
Template,
User,
VoiceState,
Webhook,
} = require("../../dist/entities/index");
async function main() {
if (!process.env.TO) throw new Error("TO database env connection string not set");
// manually arrange them because of foreign keys
const entities = [
ConfigEntity,
User,
Guild,
Channel,
Invite,
Role,
Ban,
Application,
Emoji,
ConnectedAccount,
Member,
ReadState,
Recipient,
Relationship,
Sticker,
Team,
TeamMember,
Template,
VoiceState,
Webhook,
Message,
Attachment,
];
const oldDB = await initDatabase();
const type = process.env.TO.includes("://") ? process.env.TO.split(":")[0]?.replace("+srv", "") : "sqlite";
const isSqlite = type.includes("sqlite");
// @ts-ignore
const newDB = await createConnection({
type,
url: isSqlite ? undefined : process.env.TO,
database: isSqlite ? process.env.TO : undefined,
entities,
name: "new",
synchronize: true,
});
let i = 0;
try {
for (const entity of entities) {
const entries = await oldDB.manager.find(entity);
// @ts-ignore
console.log("migrating " + entries.length + " " + entity.name + " ...");
for (const entry of entries) {
console.log(i++);
try {
await newDB.manager.insert(entity, entry);
} catch (error) {
try {
if (!entry.id) throw new Error("object doesn't have a unique id: " + entry);
await newDB.manager.update(entity, { id: entry.id }, entry);
} catch (error) {
console.error("couldn't migrate " + i + " " + entity.name, error);
}
}
}
// @ts-ignore
console.log("migrated " + entries.length + " " + entity.name);
}
} catch (error) {
console.error(error.message);
}
console.log("SUCCESS migrated all data");
await newDB.close();
}
main().caught();
-22
View File
@@ -1,22 +0,0 @@
import { ApiConfiguration, ClientConfiguration, DefaultsConfiguration, EndpointConfiguration, GeneralConfiguration, GifConfiguration, GuildConfiguration, KafkaConfiguration, LimitsConfiguration, LoginConfiguration, MetricsConfiguration, RabbitMQConfiguration, RegionConfiguration, RegisterConfiguration, SecurityConfiguration, SentryConfiguration, TemplateConfiguration } from "../config";
export class ConfigValue {
gateway: EndpointConfiguration = new EndpointConfiguration();
cdn: EndpointConfiguration = new EndpointConfiguration();
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();
client: ClientConfiguration = new ClientConfiguration();
metrics: MetricsConfiguration = new MetricsConfiguration();
sentry: SentryConfiguration = new SentryConfiguration();
defaults: DefaultsConfiguration = new DefaultsConfiguration();
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./Config";
export * from "./types/index";
@@ -1,5 +0,0 @@
export class ApiConfiguration {
defaultVersion: string = "9";
activeVersions: string[] = ["6", "7", "8", "9"];
useFosscordEnhancements: boolean = true;
}
@@ -1,8 +0,0 @@
import { ClientReleaseConfiguration } from ".";
export class ClientConfiguration {
//classes
releases: ClientReleaseConfiguration = new ClientReleaseConfiguration();
//base types
useTestClient: boolean = true;
}
@@ -1,6 +0,0 @@
import { GuildDefaults, UserDefaults } from ".";
export class DefaultsConfiguration {
guild: GuildDefaults = new GuildDefaults();
user: UserDefaults = new UserDefaults();
}
@@ -1,5 +0,0 @@
export class EndpointConfiguration {
endpointClient: string | null = null;
endpointPrivate: string | null = null;
endpointPublic: string | null = null;
}
@@ -1,12 +0,0 @@
import { Snowflake } from "../../util";
export class GeneralConfiguration {
instanceName: string = "Fosscord Instance";
instanceDescription: string | null = "This is a Fosscord instance made in the pre-release days";
frontPage: string | null = null;
tosPage: string | null = null;
correspondenceEmail: string | null = "noreply@localhost.local";
correspondenceUserID: string | null = null;
image: string | null = null;
instanceId: string = Snowflake.generate();
}
@@ -1,5 +0,0 @@
export class GifConfiguration {
enabled: boolean = true;
provider: "tenor" = "tenor"; // more coming soon
apiKey?: string = "LIVDSRZULELA";
}
@@ -1,6 +0,0 @@
import { DiscoveryConfiguration, AutoJoinConfiguration } from ".";
export class GuildConfiguration {
discovery: DiscoveryConfiguration = new DiscoveryConfiguration();
autoJoin: AutoJoinConfiguration = new AutoJoinConfiguration();
}
@@ -1,5 +0,0 @@
import { KafkaBroker } from ".";
export class KafkaConfiguration {
brokers: KafkaBroker[] | null = null;
}
@@ -1,9 +0,0 @@
import { ChannelLimits, 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();
}
@@ -1,3 +0,0 @@
export class LoginConfiguration {
requireCaptcha: boolean = false;
}
@@ -1,3 +0,0 @@
export class MetricsConfiguration {
timeout: number = 30000;
}
@@ -1,3 +0,0 @@
export class RabbitMQConfiguration {
host: string | null = null;
}
@@ -1,16 +0,0 @@
import { Region } from ".";
export class RegionConfiguration {
default: string = "fosscord";
useDefaultAsOptimal: boolean = true;
available: Region[] = [
{
id: "fosscord",
name: "Fosscord",
endpoint: "127.0.0.1:3004",
vip: false,
custom: false,
deprecated: false,
},
];
}
@@ -1,18 +0,0 @@
import { DateOfBirthConfiguration, EmailConfiguration, PasswordConfiguration } from ".";
export class RegisterConfiguration {
//classes
email: EmailConfiguration = new EmailConfiguration();
dateOfBirth: DateOfBirthConfiguration = new DateOfBirthConfiguration();
password: PasswordConfiguration = new PasswordConfiguration();
//base types
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 = "0";
}
@@ -1,17 +0,0 @@
import crypto from "crypto";
import { CaptchaConfiguration, TwoFactorConfiguration } from ".";
export class SecurityConfiguration {
//classes
captcha: CaptchaConfiguration = new CaptchaConfiguration();
twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration();
//base types
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
forwadedFor: string | null = null;
ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9";
}
@@ -1,8 +0,0 @@
import { hostname } from "os";
export class SentryConfiguration {
enabled: boolean = false;
endpoint: string = "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6";
traceSampleRate: number = 1.0;
environment: string = hostname();
}
@@ -1,6 +0,0 @@
export class TemplateConfiguration {
enabled: boolean = true;
allowTemplateCreation: boolean = true;
allowDiscordTemplates: boolean = true;
allowRaws: boolean = true;
}
-18
View File
@@ -1,18 +0,0 @@
export * from "./ApiConfiguration";
export * from "./ClientConfiguration";
export * from "./DefaultsConfiguration";
export * from "./EndpointConfiguration";
export * from "./GeneralConfiguration";
export * from "./GifConfiguration";
export * from "./GuildConfiguration";
export * from "./KafkaConfiguration";
export * from "./LimitConfigurations";
export * from "./LoginConfiguration";
export * from "./MetricsConfiguration";
export * from "./RabbitMQConfiguration";
export * from "./RegionConfiguration";
export * from "./RegisterConfiguration";
export * from "./SecurityConfiguration";
export * from "./SentryConfiguration";
export * from "./TemplateConfiguration";
export * from "./subconfigurations/index";
@@ -1,4 +0,0 @@
export class ClientReleaseConfiguration {
useLocalRelease: boolean = true; //TODO
upstreamVersion: string = "0.0.264";
}
@@ -1 +0,0 @@
export * from "./ClientReleaseConfiguration";
@@ -1,8 +0,0 @@
export class GuildDefaults {
maxPresences: number = 250000;
maxVideoChannelUsers: number = 200;
afkTimeout: number = 300;
defaultMessageNotifications: number = 1;
explicitContentFilter: number = 0;
test: number = 123;
}
@@ -1,5 +0,0 @@
export class UserDefaults {
premium: boolean = false;
premium_type: number = 2;
verified: boolean = true;
}
@@ -1,2 +0,0 @@
export * from "./GuildDefaults";
export * from "./UserDefaults";
@@ -1,5 +0,0 @@
export class AutoJoinConfiguration {
enabled: boolean = true;
guilds: string[] = [];
canLeave: boolean = true;
}
@@ -1,6 +0,0 @@
export class DiscoveryConfiguration {
showAllGuilds: boolean = false;
useRecommendation: boolean = false; // TODO: Recommendation, privacy concern?
offset: number = 0;
limit: number = 24;
}
@@ -1,2 +0,0 @@
export * from "./AutoJoin";
export * from "./Discovery";
@@ -1,8 +0,0 @@
export * from "./client/index";
export * from "./defaults/index";
export * from "./guild/index";
export * from "./kafka/index";
export * from "./limits/index";
export * from "./region/index";
export * from "./register/index";
export * from "./security/index";
@@ -1,4 +0,0 @@
export interface KafkaBroker {
ip: string;
port: number;
}
@@ -1 +0,0 @@
export * from "./KafkaBroker";
@@ -1,5 +0,0 @@
export class ChannelLimits {
maxPins: number = 500;
maxTopic: number = 1024;
maxWebhooks: number = 100;
}
@@ -1,8 +0,0 @@
export class GuildLimits {
maxRoles: number = 1000;
maxEmojis: number = 2000;
maxMembers: number = 25000000;
maxChannels: number = 65535;
maxChannelsInCategory: number = 65535;
hideOfflineMember: number = 3;
}
@@ -1,8 +0,0 @@
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;
}
@@ -1,18 +0,0 @@
import { RouteRateLimit, RateLimitOptions } from ".";
export class RateLimits {
disabled: boolean = true;
ip: Omit<RateLimitOptions, "bot_count"> = {
count: 500,
window: 5
};
global: RateLimitOptions = {
count: 250,
window: 5
};
error: RateLimitOptions = {
count: 10,
window: 5
};
routes: RouteRateLimit;
}
@@ -1,5 +0,0 @@
export class UserLimits {
maxGuilds: number = 1048576;
maxUsername: number = 127;
maxFriends: number = 5000;
}
@@ -1,6 +0,0 @@
export * from "./ChannelLimits";
export * from "./GuildLimits";
export * from "./MessageLimits";
export * from "./RateLimits";
export * from "./UserLimits";
export * from "./ratelimits/index";
@@ -1,12 +0,0 @@
import { RateLimitOptions } from "./RateLimitOptions";
export class AuthRateLimit {
login: RateLimitOptions = {
count: 5,
window: 60
};
register: RateLimitOptions = {
count: 2,
window: 60 * 60 * 12
};
}
@@ -1,6 +0,0 @@
export interface RateLimitOptions {
bot?: number;
count: number;
window: number;
onyIp?: boolean;
}
@@ -1,19 +0,0 @@
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;
// TODO: rate limit configuration for all routes
}
@@ -1,3 +0,0 @@
export * from "./Auth";
export * from "./RateLimitOptions";
export * from "./Route";
@@ -1,12 +0,0 @@
export interface Region {
id: string;
name: string;
endpoint: string;
location?: {
latitude: number;
longitude: number;
};
vip: boolean;
custom: boolean;
deprecated: boolean;
}
@@ -1 +0,0 @@
export * from "./Region";
@@ -1,4 +0,0 @@
export class DateOfBirthConfiguration {
required: boolean = true;
minimum: number = 13; // in years
}
@@ -1,7 +0,0 @@
export class EmailConfiguration {
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"),
}
@@ -1,7 +0,0 @@
export class PasswordConfiguration {
required: boolean = false;
minLength: number = 8;
minNumbers: number = 2;
minUpperCase: number =2;
minSymbols: number = 0;
}
@@ -1,3 +0,0 @@
export * from "./DateOfBirth";
export * from "./Email";
export * from "./Password";
@@ -1,6 +0,0 @@
export class CaptchaConfiguration {
enabled: boolean = false;
service: "recaptcha" | "hcaptcha" | null = null; // TODO: hcaptcha, custom
sitekey: string | null = null;
secret: string | null = null;
}
@@ -1,3 +0,0 @@
export class TwoFactorConfiguration {
generateBackupCodes: boolean = true;
}
@@ -1,2 +0,0 @@
export * from "./Captcha";
export * from "./TwoFactor";
-41
View File
@@ -1,41 +0,0 @@
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)),
};
}
}
-17
View File
@@ -1,17 +0,0 @@
import { User } from "../entities";
export class MinimalPublicUserDTO {
avatar?: string | null;
discriminator: string;
id: string;
public_flags: number;
username: string;
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;
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./DmChannelDTO";
export * from "./UserDTO";
-156
View File
@@ -1,156 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Team } from "./Team";
import { User } from "./User";
@Entity("applications")
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?: any;
@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)
owner: User;
@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)
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;
//just for us
//@Column({ type: "simple-array", nullable: true })
//rpc_origins?: string[];
//@JoinColumn({ name: "guild_id" })
//@ManyToOne(() => Guild)
//guild?: Guild; // if this application is a game sold, this field will be the guild to which it has been linked
//@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?: any;
options?: ApplicationCommandInteractionDataOption[];
}
-43
View File
@@ -1,43 +0,0 @@
import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { URL } from "url";
import { deleteFile } from "../util/cdn";
import { BaseClass } from "./BaseClass";
@Entity("attachments")
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);
}
}
-194
View File
@@ -1,194 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { ChannelPermissionOverwrite } from "./Channel";
import { User } from "./User";
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 fosscord, 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("audit_logs")
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?: {}[];
$remove?: {}[];
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;
}
-19
View File
@@ -1,19 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@Entity("backup_codes")
export class BackupCode extends BaseClass {
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, { onDelete: "CASCADE" })
user: User;
@Column()
code: string;
@Column()
consumed: boolean;
@Column()
expired: boolean;
}
-41
View File
@@ -1,41 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
@Entity("bans")
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;
}
-26
View File
@@ -1,26 +0,0 @@
import "reflect-metadata";
import { BaseEntity, ObjectIdColumn, PrimaryColumn, SaveOptions } from "typeorm";
import { Snowflake } from "../util/Snowflake";
export class BaseClassWithoutId extends BaseEntity {
constructor() {
super();
}
}
export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
export class BaseClass extends BaseClassWithoutId {
@PrimaryIdColumn()
id: string;
constructor() {
super();
if (!this.id) this.id = Snowflake.generate();
}
save(options?: SaveOptions | undefined): Promise<this> {
if (!this.id) this.id = Snowflake.generate();
return super.save(options);
}
}
-33
View File
@@ -1,33 +0,0 @@
import { PrimaryColumn, Column, Entity} from "typeorm";
import { BaseClassWithoutId } from "./BaseClass";
// 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("categories")
export class Categories extends BaseClassWithoutId { // Not using snowflake
@PrimaryColumn()
id: number;
@Column({ nullable: true })
name: string;
@Column({ type: "simple-json" })
localizations: string;
@Column({ nullable: true })
is_primary: boolean;
}
-391
View File
@@ -1,391 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { OrmUtils } from "../util/imports/OrmUtils";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { PublicUserProjection, User } from "./User";
import { HTTPError } from "../util/imports/HTTPError";
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { Recipient } from "./Recipient";
import { Message } from "./Message";
import { ReadState } from "./ReadState";
import { Invite } from "./Invite";
import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook";
import { DmChannelDTO } from "../dtos";
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("channels")
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, {
onDelete: "CASCADE",
})
guild: Guild;
@Column({ nullable: true })
@RelationId((channel: Channel) => channel.parent)
parent_id: string;
@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({ nullable: true })
position?: 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({ nullable: true })
nsfw?: boolean;
@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({ nullable: true })
flags?: number = 0;
@Column({ nullable: true })
default_thread_rate_limit_per_user?: number = 0;
// 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");
}
if (!opts?.skipNameChecks) {
const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } });
if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
for (let character of InvisibleCharacters)
if (channel.name.includes(character))
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);
}
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
channel = {
...channel,
...(!opts?.keepId && { id: Snowflake.generate() }),
created_at: new Date(),
position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0,
};
await Promise.all([
OrmUtils.mergeDeep(new Channel(), channel).save(),
!opts?.skipEventEmit
? emitEvent({
event: "CHANNEL_CREATE",
data: channel,
guild_id: channel.guild_id,
} as ChannelCreateEvent)
: Promise.resolve(),
]);
return channel;
}
static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients = recipients.unique().filter((x) => x !== creator_user_id);
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
// TODO: check config for max number of recipients
/** if you want to disallow note to self channels, uncomment the conditional below
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 (let ur of userRecipients) {
let re = ur.channel.recipients!.map((r) => r.user_id);
if (re.length === channelRecipients.length) {
if (containsAll(re, channelRecipients)) {
if (channel == null) {
channel = ur.channel;
ur = OrmUtils.mergeDeep(ur, { closed: false });
await ur.save();
}
}
}
}
if (channel == null) {
name = trimSpecial(name);
channel = await (
OrmUtils.mergeDeep(new Channel(), {
name,
type,
owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server
created_at: new Date(),
last_message_id: null,
recipients: channelRecipients.map((x) =>
OrmUtils.mergeDeep(new Recipient(), {
user_id: x,
closed: !(type === ChannelType.GROUP_DM || x === creator_user_id),
})
),
}) as Channel
).save();
}
const channel_dto = await DmChannelDTO.from(channel);
if (type === ChannelType.GROUP_DM) {
for (let 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) {
await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
//TODO before deleting the channel we should check and delete other relations
await Channel.delete({ id: channel.id });
}
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;
}
}
export interface ChannelPermissionOverwrite {
allow: string;
deny: string;
id: string;
type: ChannelPermissionOverwriteType;
}
export enum ChannelPermissionOverwriteType {
role = 0,
member = 1,
group = 2,
}
-26
View File
@@ -1,26 +0,0 @@
import { Column, Entity} from "typeorm";
import { BaseClass } from "./BaseClass";
@Entity("client_release")
export class Release extends BaseClass {
@Column()
name: string;
@Column()
pub_date: string;
@Column()
url: string;
@Column()
deb_url: string;
@Column()
osx_url: string;
@Column()
win_url: string;
@Column({ nullable: true })
notes?: string;
}
-11
View File
@@ -1,11 +0,0 @@
import { Column, Entity } from "typeorm";
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
@Entity("config")
export class ConfigEntity extends BaseClassWithoutId {
@PrimaryIdColumn()
key: string;
@Column({ type: "simple-json", nullable: true })
value: number | boolean | null | string | undefined;
}
-42
View File
@@ -1,42 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {}
@Entity("connected_accounts")
export class ConnectedAccount extends BaseClass {
@Column({ nullable: true })
@RelationId((account: ConnectedAccount) => account.user)
user_id: string;
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
user: User;
@Column({ select: false })
access_token: string;
@Column({ select: false })
friend_sync: boolean;
@Column()
name: string;
@Column({ select: false })
revoked: boolean;
@Column({ select: false })
show_activity: boolean;
@Column()
type: string;
@Column()
verified: boolean;
@Column({ select: false })
visibility: number;
}
-46
View File
@@ -1,46 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { User } from ".";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Role } from "./Role";
@Entity("emojis")
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, {
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 (Fosscord extension)
}
-35
View File
@@ -1,35 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { PublicUserProjection, User } from "./User";
import { HTTPError } from "..";
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField";
import { Recipient } from "./Recipient";
import { Message } from "./Message";
import { ReadState } from "./ReadState";
import { Invite } from "./Invite";
import { DmChannelDTO } from "../dtos";
@Entity("security_settings")
export class SecuritySettings extends BaseClass {
@Column({nullable: true})
guild_id: Snowflake;
@Column({nullable: true})
channel_id: Snowflake;
@Column()
encryption_permission_mask: BitField;
@Column()
allowed_algorithms: string[];
@Column()
current_algorithm: string;
@Column({nullable: true})
used_since_message: Snowflake;
}
-33
View File
@@ -1,33 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
@Entity("groups")
export class UserGroup extends BaseClass {
@Column({ nullable: true })
parent?: BigInt;
@Column()
color: number;
@Column()
hoist: boolean;
@Column()
mentionable: boolean;
@Column()
name: string;
@Column()
rights: BigInt;
@Column()
position: number;
@Column({ nullable: true })
icon: BigInt;
@Column({ nullable: true })
unicode_emoji: BigInt;
}
-370
View File
@@ -1,370 +0,0 @@
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
import { OrmUtils } from "../util/imports/OrmUtils";
import { Config, handleFile, Snowflake } 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";
// 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",
"members",
"roles",
"stickers",
"voice_states",
"members.user",
];
@Entity("guilds")
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 = Config.get().defaults.guild.afkTimeout;
// * 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 = Config.get().defaults.guild.defaultMessageNotifications;
@Column({ nullable: true })
description?: string;
@Column({ nullable: true })
discovery_splash?: string;
@Column({ nullable: true })
explicit_content_filter?: number = Config.get().defaults.guild.explicitContentFilter;
@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: number;
@Column({ nullable: true })
icon?: string;
@Column({ nullable: true })
large?: boolean;
@Column({ nullable: true })
max_members?: number = Config.get().limits.guild.maxMembers; // e.g. default 100.000
@Column({ nullable: true })
max_presences?: number = Config.get().defaults.guild.maxPresences;
@Column({ nullable: true })
max_video_channel_users?: number = Config.get().defaults.guild.maxVideoChannelUsers; // ? default: 25, is this max 25 streaming or watching
@Column({ nullable: true })
member_count?: number = 0;
@Column({ nullable: true })
presence_count?: number = 0; // 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({ nullable: true })
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({ nullable: true })
unavailable?: boolean;
@Column({ nullable: true })
verification_level?: number;
@Column({ type: "simple-json" })
welcome_screen: {
enabled: boolean;
description: string;
welcome_channels: {
description: string;
emoji_id?: string;
emoji_name: string;
channel_id: string;
}[];
};
@Column({ nullable: true })
@RelationId((guild: Guild) => guild.widget_channel)
widget_channel_id?: string;
@JoinColumn({ name: "widget_channel_id" })
@ManyToOne(() => Channel)
widget_channel?: Channel;
@Column({ nullable: true })
widget_enabled?: boolean;
@Column({ nullable: true })
nsfw_level?: number;
@Column({ nullable: true })
nsfw?: boolean;
// 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;
static async createGuild(body: {
name?: string;
icon?: string | null;
owner_id?: string;
channels?: Partial<Channel>[];
}) {
const guild_id = Snowflake.generate();
const guild: Guild = OrmUtils.mergeDeep(new Guild(), {
name: body.name || "Fosscord",
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
region: Config.get().regions.default,
owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
afk_timeout: 300,
default_message_notifications: 1, // defaults effect: setting the push default at mentions-only will save a lot
explicit_content_filter: 0,
features: [],
primary_category_id: null,
id: guild_id,
max_members: 250000,
max_presences: 250000,
max_video_channel_users: 200,
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
unavailable: false,
nsfw: false,
nsfw_level: 0,
verification_level: 0,
welcome_screen: {
enabled: false,
description: "Fill in your description",
welcome_channels: [],
},
widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
});
await guild.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
let role: Role = OrmUtils.mergeDeep(new Role(), {
id: guild_id,
guild_id: guild_id,
color: 0,
hoist: false,
managed: false,
// NB: in Fosscord, 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: null,
unicode_emoji: null,
});
await role.save();
if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
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, b) => (a.parent_id ? 1 : -1))) {
let id = ids.get(channel.id) || Snowflake.generate();
let parent_id = ids.get(channel.parent_id);
await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
keepId: true,
skipExistsCheck: true,
skipPermissionCheck: true,
skipEventEmit: true,
});
}
return guild;
}
}
-88
View File
@@ -1,88 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm";
import { Member } from "./Member";
import { BaseClassWithoutId } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
import { User } from "./User";
import { random } from "@fosscord/api";
export const PublicInviteRelation = ["inviter", "guild", "channel"];
@Entity("invites")
export class Invite extends BaseClassWithoutId {
@PrimaryColumn()
code: string = random();
@Column()
temporary: boolean = true;
@Column()
uses: number = 0;
@Column()
max_uses: number;
@Column()
max_age: number;
@Column()
created_at: Date = new Date();
@Column()
expires_at: Date;
@Column({ nullable: true })
@RelationId((invite: Invite) => invite.guild)
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
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://github.com/fosscord/fosscord/issues/62
@Column({ nullable: true })
target_user_type?: number;
@Column({ nullable: true })
vanity_url?: boolean;
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;
}
}
-360
View File
@@ -1,360 +0,0 @@
import { PublicUser, User } from "./User";
import { BaseClass } from "./BaseClass";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
RelationId,
} from "typeorm";
import { Guild } from "./Guild";
import { Config, emitEvent } from "../util";
import {
GuildCreateEvent,
GuildDeleteEvent,
GuildMemberAddEvent,
GuildMemberRemoveEvent,
GuildMemberUpdateEvent,
} from "../interfaces";
import { HTTPError } from "../util/imports/HTTPError";
import { Role } from "./Role";
import { BaseClassWithoutId } from "./BaseClass";
import { Ban, PublicGuildRelations } from ".";
import { DiscordApiErrors } from "../util/Constants";
import { OrmUtils } from "../util/imports/OrmUtils";
export const MemberPrivateProjection: (keyof Member)[] = [
"id",
"guild",
"guild_id",
"deaf",
"joined_at",
"last_message_id",
"mute",
"nick",
"pending",
"premium_since",
"roles",
"settings",
"user",
];
@Entity("members")
@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({ nullable: true })
premium_since?: Date;
@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;
// TODO: add this when we have proper read receipts
// @Column({ type: "simple-json" })
// read_state: ReadState;
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", "member_count"], 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
//TODO: check for bugs
if (guild.member_count) guild.member_count--;
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, role] = await Promise.all([
// @ts-ignore
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"],
}),
Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }),
]);
member.roles.push(OrmUtils.mergeDeep(new Role(), { 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([
// @ts-ignore
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"],
}),
await 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"],
});
member.nick = nickname;
await Promise.all([
member.save(),
emitEvent({
event: "GUILD_MEMBER_UPDATE",
data: {
guild_id,
user: member.user,
nick: nickname,
},
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,
});
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(),
premium_since: null,
deaf: false,
mute: false,
pending: false,
};
//TODO: check for bugs
if (guild.member_count) guild.member_count++;
await Promise.all([
OrmUtils.mergeDeep(new Member(), {
...member,
roles: [OrmUtils.mergeDeep(new Role(), { id: guild_id })],
// read_state: {},
settings: {
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: {
...guild,
members: [...guild.members, { ...member, user }],
member_count: (guild.member_count || 0) + 1,
guild_hashes: {},
guild_scheduled_events: [],
joined_at: member.joined_at,
presences: [],
stage_instances: [],
threads: [],
},
user_id,
} as GuildCreateEvent),
]);
}
}
export interface UserGuildSettings {
channel_overrides: {
channel_id: string;
message_notifications: number;
mute_config: MuteConfig;
muted: boolean;
}[];
message_notifications: number;
mobile_push: boolean;
mute_config: MuteConfig;
muted: boolean;
suppress_everyone: boolean;
suppress_roles: boolean;
version: number;
}
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";
export const PublicMemberProjection: PublicMemberKeys[] = [
"id",
"guild_id",
"nick",
"roles",
"joined_at",
"pending",
"deaf",
"mute",
"premium_since",
];
// @ts-ignore
export type PublicMember = Pick<Member, Omit<PublicMemberKeys, "roles">> & {
user: PublicUser;
roles: string[]; // only role ids not objects
};
-284
View File
@@ -1,284 +0,0 @@
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,
RemoveOptions,
UpdateDateColumn,
} from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Webhook } from "./Webhook";
import { Sticker } from "./Sticker";
import { Attachment } from "./Attachment";
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("messages")
@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({ nullable: true })
flags?: string;
@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?: MessageComponent[];
}
export interface MessageComponent {
type: number;
style?: number;
label?: string;
emoji?: PartialEmoji;
custom_id?: string;
url?: string;
disabled?: boolean;
components: MessageComponent[];
}
export enum MessageComponentType {
Script = 0, // self command script
ActionRow = 1,
Button = 2,
}
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;
}
-18
View File
@@ -1,18 +0,0 @@
import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
import { BaseClassWithoutId } from ".";
export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
? ObjectIdColumn
: PrimaryGeneratedColumn;
@Entity("migrations")
export class Migration extends BaseClassWithoutId {
@PrimaryIdAutoGenerated()
id: number;
@Column({ type: "bigint" })
timestamp: number;
@Column()
name: string;
}
-18
View File
@@ -1,18 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
@Entity("notes")
@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;
}
-17
View File
@@ -1,17 +0,0 @@
import { Column, Entity } from "typeorm";
import { BaseClass } from "./BaseClass";
@Entity("rate_limits")
export class RateLimit extends BaseClass {
@Column() // no relation as it also
executor_id: string;
@Column()
hits: number;
@Column()
blocked: boolean;
@Column()
expires_at: Date;
}
-55
View File
@@ -1,55 +0,0 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { Message } from "./Message";
import { User } from "./User";
// 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("read_states")
@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;
}
-30
View File
@@ -1,30 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
@Entity("recipients")
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
}
-49
View File
@@ -1,49 +0,0 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
export enum RelationshipType {
outgoing = 4,
incoming = 3,
blocked = 2,
friends = 1,
}
@Entity("relationships")
@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(),
};
}
}
-51
View File
@@ -1,51 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
@Entity("roles")
export class Role extends BaseClass {
@Column({ nullable: true })
@RelationId((role: Role) => role.guild)
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
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;
};
}
-46
View File
@@ -1,46 +0,0 @@
import { User } from "./User";
import { BaseClass } from "./BaseClass";
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { Status } from "../interfaces/Status";
import { Activity } from "../interfaces/Activity";
//TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
@Entity("sessions")
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[];
// TODO client_status
@Column({ type: "simple-json", select: false })
client_info: {
client: string;
os: string;
version: number;
};
@Column({ nullable: false, type: "varchar" })
status: Status; //TODO enum
}
export const PrivateSessionProjection: (keyof Session)[] = [
"user_id",
"session_id",
"activities",
"client_info",
"status",
];
-66
View File
@@ -1,66 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { User } from "./User";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
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("stickers")
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, {
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;
}
-31
View File
@@ -1,31 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
import { Sticker } from ".";
import { BaseClass } from "./BaseClass";
@Entity("sticker_packs")
export class StickerPack extends BaseClass {
@Column()
name: string;
@Column({ nullable: true })
description?: string;
@Column({ nullable: true })
banner_asset_id?: string;
@OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
cascade: true,
orphanedRowAction: "delete",
})
stickers: Sticker[];
// sku_id: string
@Column({ nullable: true })
@RelationId((pack: StickerPack) => pack.cover_sticker)
cover_sticker_id?: string;
@ManyToOne(() => Sticker, { nullable: true })
@JoinColumn()
cover_sticker?: Sticker;
}
-27
View File
@@ -1,27 +0,0 @@
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { TeamMember } from "./TeamMember";
import { User } from "./User";
@Entity("teams")
export class Team extends BaseClass {
@Column({ nullable: true })
icon?: string;
@JoinColumn({ name: "member_ids" })
@OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
orphanedRowAction: "delete",
})
members: TeamMember[];
@Column()
name: string;
@Column({ nullable: true })
@RelationId((team: Team) => team.owner_user)
owner_user_id: string;
@JoinColumn({ name: "owner_user_id" })
@ManyToOne(() => User)
owner_user: User;
}
-37
View File
@@ -1,37 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
export enum TeamMemberState {
INVITED = 1,
ACCEPTED = 2,
}
@Entity("team_members")
export class TeamMember extends BaseClass {
@Column({ type: "int" })
membership_state: TeamMemberState;
@Column({ type: "simple-array" })
permissions: string[];
@Column({ nullable: true })
@RelationId((member: TeamMember) => member.team)
team_id: string;
@JoinColumn({ name: "team_id" })
@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
onDelete: "CASCADE",
})
team: import("./Team").Team;
@Column({ nullable: true })
@RelationId((member: TeamMember) => member.user)
user_id: string;
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
user: User;
}
-44
View File
@@ -1,44 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
@Entity("templates")
export class Template extends BaseClass {
@Column({ unique: true })
code: string;
@Column()
name: string;
@Column({ nullable: true })
description?: string;
@Column({ nullable: true })
usage_count?: number;
@Column({ nullable: true })
@RelationId((template: Template) => template.creator)
creator_id: string;
@JoinColumn({ name: "creator_id" })
@ManyToOne(() => User)
creator: User;
@Column()
created_at: Date;
@Column()
updated_at: Date;
@Column({ nullable: true })
@RelationId((template: Template) => template.source_guild)
source_guild_id: string;
@JoinColumn({ name: "source_guild_id" })
@ManyToOne(() => Guild)
source_guild: Guild;
@Column({ type: "simple-json" })
serialized_source_guild: Guild;
}
-324
View File
@@ -1,324 +0,0 @@
import { Column, Entity, FindOneOptions, FindOptionsSelectByString, JoinColumn, OneToMany, OneToOne } from "typeorm";
import { OrmUtils } from "../util/imports/OrmUtils";
import { BaseClass } from "./BaseClass";
import { BitField } from "../util/BitField";
import { Relationship } from "./Relationship";
import { ConnectedAccount } from "./ConnectedAccount";
import { Config, FieldErrors, Snowflake, trimSpecial } from "..";
import { Member, Session, UserSettings } from ".";
export enum PublicUserEnum {
username,
discriminator,
id,
public_flags,
avatar,
accent_color,
banner,
bio,
bot,
premium_since,
}
export type PublicUserKeys = keyof typeof PublicUserEnum;
export enum PrivateUserEnum {
flags,
mfa_enabled,
email,
phone,
verified,
nsfw_allowed,
premium,
premium_type,
disabled,
settings,
// locale
}
export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys;
export const PublicUserProjection = Object.values(PublicUserEnum).filter(
(x) => typeof x === "string"
) as PublicUserKeys[];
export const PrivateUserProjection = [
...PublicUserProjection,
...Object.values(PrivateUserEnum).filter((x) => typeof x === "string"),
] as PrivateUserKeys[];
// Private user data that should never get sent to the client
export type PublicUser = Pick<User, PublicUserKeys>;
export interface UserPublic extends Pick<User, PublicUserKeys> {}
export interface UserPrivate extends Pick<User, PrivateUserKeys> {
locale: string;
}
// TODO: add purchased_flags, premium_usage_flags
@Entity("users")
export class User extends BaseClass {
@Column()
username: string; // username max length 32, min 2 (should be configurable)
@Column()
discriminator: string; // opaque string: 4 digits on discord.com
setDiscriminator(val: string) {
const number = Number(val);
if (isNaN(number)) throw new Error("invalid discriminator");
if (number <= 0 || number >= 10000) throw new Error("discriminator must be between 1 and 9999");
this.discriminator = val.toString().padStart(4, "0");
}
@Column({ nullable: true })
avatar?: string; // hash of the user avatar
@Column({ nullable: true })
accent_color?: number; // banner color of user
@Column({ nullable: true })
banner?: string; // hash of the user banner
@Column({ nullable: true, select: false })
phone?: string; // phone number of the user
@Column({ select: false })
desktop: boolean = false; // if the user has desktop app installed
@Column({ select: false })
mobile: boolean = false; // if the user has mobile app installed
@Column()
premium: boolean = Config.get().defaults.user.premium; // if user bought individual premium
@Column()
premium_type: number = Config.get().defaults.user.premium_type; // individual premium level
@Column()
bot: boolean = false; // if user is bot
@Column()
bio: string; // short description of the user (max 190 chars -> should be configurable)
@Column()
system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author
@Column({ select: false })
nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age
@Column({ select: false })
mfa_enabled: boolean; // if multi factor authentication is enabled
@Column({ select: false, nullable: true })
totp_secret?: string;
@Column({ nullable: true, select: false })
totp_last_ticket?: string;
@Column()
created_at: Date = new Date(); // registration date
@Column({ nullable: true })
premium_since: Date = new Date(); // premium date
@Column({ select: false })
verified: boolean = Config.get().defaults.user.verified; // if the user is offically verified
@Column()
disabled: boolean = false; // if the account is disabled
@Column()
deleted: boolean = false; // if the user was deleted
@Column({ nullable: true, select: false })
email?: string; // email of the user
@Column()
flags: string = "0"; // UserFlags // TODO: generate
@Column()
public_flags: number = 0;
@Column({ type: "bigint" })
rights: string = Config.get().register.defaultRights; // Rights
@OneToMany(() => Session, (session: Session) => session.user)
sessions: Session[];
@JoinColumn({ name: "relationship_ids" })
@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
cascade: true,
orphanedRowAction: "delete",
})
relationships: Relationship[];
@JoinColumn({ name: "connected_account_ids" })
@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, {
cascade: true,
orphanedRowAction: "delete",
})
connected_accounts: ConnectedAccount[];
@Column({ type: "simple-json", select: false })
data: {
valid_tokens_since: Date; // all tokens with a previous issue date are invalid
hash?: string; // hash of the password, salt is saved in password (bcrypt)
};
@Column({ type: "simple-array", select: false })
fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts
@OneToOne(()=> UserSettings, {
cascade: true,
orphanedRowAction: "delete",
eager: false
})
@JoinColumn()
settings: UserSettings;
// workaround to prevent fossord-unaware clients from deleting settings not used by them
@Column({ type: "simple-json", select: false })
extended_settings: string = "{}";
@Column({ type: "simple-json" })
notes: { [key: string]: string } = {}; //key is ID of user
async save(): Promise<any> {
if(!this.settings) this.settings = new UserSettings();
this.settings.id = this.id;
//await this.settings.save();
return super.save();
}
toPublicUser() {
const user: any = {};
PublicUserProjection.forEach((x) => {
user[x] = this[x];
});
return user as PublicUser;
}
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
return await User.findOneOrFail({
where: { id: user_id },
select: [...PublicUserProjection, ...((opts?.select as FindOptionsSelectByString<User>) || [])],
...opts,
});
}
public static async generateDiscriminator(username: string): Promise<string | undefined> {
if (Config.get().register.incrementingDiscriminators) {
// discriminator will be incrementally generated
// First we need to figure out the currently highest discrimnator for the given username and then increment it
const users = await User.find({ where: { username }, select: ["discriminator"] });
const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator)));
const discriminator = highestDiscriminator + 1;
if (discriminator >= 10000) {
return undefined;
}
return discriminator.toString().padStart(4, "0");
} else {
// discriminator will be randomly generated
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database?
for (let tries = 0; tries < 5; tries++) {
const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
if (!exists) return discriminator;
}
return undefined;
}
}
static async register({
email,
username,
password,
date_of_birth,
req,
}: {
username: string;
password?: string;
email?: string;
date_of_birth?: Date; // "2000-04-03"
req?: any;
}) {
// trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username);
const discriminator = await User.generateDiscriminator(username);
if (!discriminator) {
// We've failed to generate a valid and unused discriminator
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
message: req?.t("auth:register.USERNAME_TOO_MANY_USERS"),
},
});
}
// TODO: save date_of_birth
// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
const language = req?.language === "en" ? "en-US" : req?.language || "en-US";
const user = OrmUtils.mergeDeep(new User(), {
//required:
username: username,
discriminator,
id: Snowflake.generate(),
email: email,
data: {
hash: password,
valid_tokens_since: new Date(),
},
settings: { ...new UserSettings(), locale: language }
});
await user.save();
await user.settings.save();
setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) {
for (const guild of Config.get().guild.autoJoin.guilds || []) {
await Member.addToGuild(user.id, guild).catch((e) => {});
}
}
});
return user;
}
}
export const CUSTOM_USER_FLAG_OFFSET = BigInt(1) << BigInt(32);
export class UserFlags extends BitField {
static FLAGS = {
DISCORD_EMPLOYEE: BigInt(1) << BigInt(0),
PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1),
HYPESQUAD_EVENTS: BigInt(1) << BigInt(2),
BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3),
MFA_SMS: BigInt(1) << BigInt(4),
PREMIUM_PROMO_DISMISSED: BigInt(1) << BigInt(5),
HOUSE_BRAVERY: BigInt(1) << BigInt(6),
HOUSE_BRILLIANCE: BigInt(1) << BigInt(7),
HOUSE_BALANCE: BigInt(1) << BigInt(8),
EARLY_SUPPORTER: BigInt(1) << BigInt(9),
TEAM_USER: BigInt(1) << BigInt(10),
TRUST_AND_SAFETY: BigInt(1) << BigInt(11),
SYSTEM: BigInt(1) << BigInt(12),
HAS_UNREAD_URGENT_MESSAGES: BigInt(1) << BigInt(13),
BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
UNDERAGE_DELETED: BigInt(1) << BigInt(15),
VERIFIED_BOT: BigInt(1) << BigInt(16),
EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
CERTIFIED_MODERATOR: BigInt(1) << BigInt(18),
BOT_HTTP_INTERACTIONS: BigInt(1) << BigInt(19),
};
}
-37
View File
@@ -1,37 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
@Entity("groups")
export class UserGroup extends BaseClass {
@Column()
color: number;
@Column()
hoist: boolean;
@JoinColumn({ name: "controller", referencedColumnName: "id" })
@ManyToOne(() => User)
controller?: User;
@Column()
mentionable_by?: string;
@Column()
name: string;
@Column()
rights: string;
@Column({ nullable: true })
icon: string;
@Column({ nullable: true })
parent?: string;
@Column({ type: "simple-array", nullable: true})
associciations: string[];
}
-119
View File
@@ -1,119 +0,0 @@
import { Column, Entity, JoinColumn } from "typeorm";
import { BaseClassWithoutId, PrimaryIdColumn } from ".";
@Entity("user_settings")
export class UserSettings extends BaseClassWithoutId {
@PrimaryIdColumn()
id: string;
@Column({ nullable: true })
afk_timeout: number = 3600;
@Column({ nullable: true })
allow_accessibility_detection: boolean = true;
@Column({ nullable: true })
animate_emoji: boolean = true;
@Column({ nullable: true })
animate_stickers: number = 0;
@Column({ nullable: true })
contact_sync_enabled: boolean = false;
@Column({ nullable: true })
convert_emoticons: boolean = false;
@Column({ nullable: true, type: "simple-json" })
custom_status: CustomStatus | null = null;
@Column({ nullable: true })
default_guilds_restricted: boolean = false;
@Column({ nullable: true })
detect_platform_accounts: boolean = false;
@Column({ nullable: true })
developer_mode: boolean = true;
@Column({ nullable: true })
disable_games_tab: boolean = true;
@Column({ nullable: true })
enable_tts_command: boolean = false;
@Column({ nullable: true })
explicit_content_filter: number = 0;
@Column({ nullable: true, type: "simple-json" })
friend_source_flags: FriendSourceFlags = { all: true };
@Column({ nullable: true })
gateway_connected: boolean = false;
@Column({ nullable: true })
gif_auto_play: boolean = false;
@Column({ nullable: true, type: "simple-json" })
guild_folders: GuildFolder[] = []; // every top guild is displayed as a "folder"
@Column({ nullable: true, type: "simple-json" })
guild_positions: string[] = []; // guild ids ordered by position
@Column({ nullable: true })
inline_attachment_media: boolean = true;
@Column({ nullable: true })
inline_embed_media: boolean = true;
@Column({ nullable: true })
locale: string = "en-US"; // en_US
@Column({ nullable: true })
message_display_compact: boolean = false;
@Column({ nullable: true })
native_phone_integration_enabled: boolean = true;
@Column({ nullable: true })
render_embeds: boolean = true;
@Column({ nullable: true })
render_reactions: boolean = true;
@Column({ nullable: true, type: "simple-json" })
restricted_guilds: string[] = [];
@Column({ nullable: true })
show_current_game: boolean = true;
@Column({ nullable: true })
status: "online" | "offline" | "dnd" | "idle" | "invisible" = "online";
@Column({ nullable: true })
stream_notifications_enabled: boolean = false;
@Column({ nullable: true })
theme: "dark" | "white" = "dark"; // dark
@Column({ nullable: true })
timezone_offset: number = 0; // e.g -60
}
interface CustomStatus {
emoji_id?: string;
emoji_name?: string;
expires_at?: number;
text?: string;
}
interface GuildFolder {
color: number;
guild_ids: string[];
id: number;
name: string;
}
interface FriendSourceFlags {
all: boolean
}
-77
View File
@@ -1,77 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
import { User } from "./User";
import { Member } from "./Member";
//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex
@Entity("voice_states")
export class VoiceState extends BaseClass {
@Column({ nullable: true })
@RelationId((voice_state: VoiceState) => voice_state.guild)
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
onDelete: "CASCADE",
})
guild?: Guild;
@Column({ nullable: true })
@RelationId((voice_state: VoiceState) => voice_state.channel)
channel_id: string;
@JoinColumn({ name: "channel_id" })
@ManyToOne(() => Channel, {
onDelete: "CASCADE",
})
channel: Channel;
@Column({ nullable: true })
@RelationId((voice_state: VoiceState) => voice_state.user)
user_id: string;
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
user: User;
// @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
// @ManyToOne(() => Member, {
// onDelete: "CASCADE",
// })
//TODO find a way to make it work without breaking Guild.voice_states
member: Member;
@Column()
session_id: string;
@Column({ nullable: true })
token: string;
@Column()
deaf: boolean;
@Column()
mute: boolean;
@Column()
self_deaf: boolean;
@Column()
self_mute: boolean;
@Column({ nullable: true })
self_stream?: boolean;
@Column()
self_video: boolean;
@Column()
suppress: boolean; // whether this user is muted by the current user
@Column({ nullable: true, default: null })
request_to_speak_timestamp?: Date;
}
-76
View File
@@ -1,76 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { Application } from "./Application";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
import { User } from "./User";
export enum WebhookType {
Incoming = 1,
ChannelFollower = 2,
}
@Entity("webhooks")
export class Webhook extends BaseClass {
@Column({ type: "int" })
type: WebhookType;
@Column({ nullable: true })
name?: string;
@Column({ nullable: true })
avatar?: string;
@Column({ nullable: true })
token?: string;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.guild)
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
onDelete: "CASCADE",
})
guild: Guild;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.channel)
channel_id: string;
@JoinColumn({ name: "channel_id" })
@ManyToOne(() => Channel, {
onDelete: "CASCADE",
})
channel: Channel;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.application)
application_id: string;
@JoinColumn({ name: "application_id" })
@ManyToOne(() => Application, {
onDelete: "CASCADE",
})
application: Application;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.user)
user_id: string;
@JoinColumn({ name: "user_id" })
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
user: User;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.guild)
source_guild_id: string;
@JoinColumn({ name: "source_guild_id" })
@ManyToOne(() => Guild, {
onDelete: "CASCADE",
})
source_guild: Guild;
}
-33
View File
@@ -1,33 +0,0 @@
export * from "./Application";
export * from "./Attachment";
export * from "./AuditLog";
export * from "./Ban";
export * from "./BaseClass";
export * from "./Categories";
export * from "./Channel";
export * from "./Config";
export * from "./ConnectedAccount";
export * from "./Emoji";
export * from "./Guild";
export * from "./Invite";
export * from "./Member";
export * from "./Message";
export * from "./Migration";
export * from "./RateLimit";
export * from "./ReadState";
export * from "./Recipient";
export * from "./Relationship";
export * from "./Role";
export * from "./Session";
export * from "./Sticker";
export * from "./StickerPack";
export * from "./Team";
export * from "./TeamMember";
export * from "./Template";
export * from "./User";
export * from "./VoiceState";
export * from "./Webhook";
export * from "./ClientRelease";
export * from "./BackupCodes";
export * from "./Note";
export * from "./UserSettings";
-8
View File
@@ -1,8 +0,0 @@
import "reflect-metadata";
export * from "./util/index";
export * from "./interfaces/index";
export * from "./entities/index";
export * from "./dtos/index";
export * from "./util/MFA";
export * from "./schemas";

Some files were not shown because too many files have changed in this diff Show More