summary refs log tree commit diff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/Channel.ts54
-rw-r--r--src/util/Config.ts363
-rw-r--r--src/util/Member.ts49
-rw-r--r--src/util/Message.ts54
-rw-r--r--src/util/instanceOf.ts17
-rw-r--r--src/util/passwordStrength.ts15
6 files changed, 416 insertions, 136 deletions
diff --git a/src/util/Channel.ts b/src/util/Channel.ts
new file mode 100644

index 00000000..c8df85bc --- /dev/null +++ b/src/util/Channel.ts
@@ -0,0 +1,54 @@ +import { + ChannelCreateEvent, + ChannelModel, + ChannelType, + getPermission, + GuildModel, + Snowflake, + TextChannel, + VoiceChannel +} from "@fosscord/server-util"; +import { HTTPError } from "lambert-server"; +import { emitEvent } from "./Event"; + +// TODO: DM channel +export async function createChannel(channel: Partial<TextChannel | VoiceChannel>, user_id: string = "0") { + if (!channel.permission_overwrites) channel.permission_overwrites = []; + + switch (channel.type) { + case ChannelType.GUILD_TEXT: + case ChannelType.GUILD_VOICE: + break; + case ChannelType.DM: + case ChannelType.GROUP_DM: + throw new HTTPError("You can't create a dm channel in a guild"); + // TODO: check if guild is community server + case ChannelType.GUILD_STORE: + case ChannelType.GUILD_NEWS: + default: + throw new HTTPError("Not yet supported"); + } + + const permissions = await getPermission(user_id, channel.guild_id); + permissions.hasThrow("MANAGE_CHANNELS"); + + if (channel.parent_id) { + const exists = await ChannelModel.findOne({ id: channel.parent_id }, { guild_id: true }).exec(); + 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"); + } + + // TODO: auto generate position + + channel = await new ChannelModel({ + ...channel, + id: Snowflake.generate(), + created_at: new Date(), + // @ts-ignore + recipients: null + }).save(); + + await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id: channel.guild_id } as ChannelCreateEvent); + + return channel; +} diff --git a/src/util/Config.ts b/src/util/Config.ts
index f1f0f458..e2e0d312 100644 --- a/src/util/Config.ts +++ b/src/util/Config.ts
@@ -1,19 +1,7 @@ -import { Config, Snowflake } from "@fosscord/server-util"; -import crypto from "crypto"; - -export default { - init() { - return Config.init({ api: DefaultOptions }); - }, - get(): DefaultOptions { - return Config.getAll().api; - }, - set(val: any) { - return Config.setAll({ api: val }); - }, - getAll: Config.getAll, - setAll: Config.setAll, -}; +// @ts-nocheck +import Ajv, { JSONSchemaType } from "ajv"; +import { getConfigPathForFile } from "@fosscord/server-util/dist/util/Config"; +import { Config } from "@fosscord/server-util"; export interface RateLimitOptions { count: number; @@ -21,6 +9,7 @@ export interface RateLimitOptions { } export interface DefaultOptions { + gateway: string; general: { instance_id: string; }; @@ -64,7 +53,7 @@ export interface DefaultOptions { login?: RateLimitOptions; register?: RateLimitOptions; }; - channel?: {}; + channel?: string; // TODO: rate limit configuration for all routes }; }; @@ -84,13 +73,13 @@ export interface DefaultOptions { }; register: { email: { - required: boolean; + necessary: boolean; allowlist: boolean; blocklist: boolean; domains: string[]; }; dateOfBirth: { - required: boolean; + necessary: boolean; minimum: number; // in years }; requireCaptcha: boolean; @@ -107,85 +96,277 @@ export interface DefaultOptions { }; } -export const DefaultOptions: DefaultOptions = { - general: { - instance_id: Snowflake.generate(), - }, - permissions: { - user: { - createGuilds: true, - }, +const schema: JSONSchemaType<DefaultOptions> & { + definitions: { + rateLimitOptions: JSONSchemaType<RateLimitOptions>; + }; +} = { + type: "object", + definitions: { + rateLimitOptions: { + type: "object", + properties: { + count: { type: "number" }, + timespan: { type: "number" } + }, + required: ["count", "timespan"] + } }, - limits: { - user: { - maxGuilds: 100, - maxUsername: 32, - maxFriends: 1000, - }, - guild: { - maxRoles: 250, - maxMembers: 250000, - maxChannels: 500, - maxChannelsInCategory: 50, - hideOfflineMember: 1000, - }, - message: { - characters: 2000, - ttsCharacters: 200, - maxReactions: 20, - maxAttachmentSize: 8388608, - maxBulkDelete: 100, - }, - channel: { - maxPins: 50, - maxTopic: 1024, + properties: { + gateway: { + type: "string" }, - rate: { - ip: { - enabled: true, - count: 1000, - timespan: 1000 * 60 * 10, + general: { + type: "object", + properties: { + instance_id: { + type: "string" + } }, - routes: {}, + required: ["instance_id"], + additionalProperties: false }, - }, - security: { - jwtSecret: crypto.randomBytes(256).toString("base64"), - forwadedFor: null, - // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy - // forwadedFor: "CF-Connecting-IP" // cloudflare: - captcha: { - enabled: false, - service: null, - sitekey: null, - secret: null, + permissions: { + type: "object", + properties: { + user: { + type: "object", + properties: { + createGuilds: { + type: "boolean" + } + }, + required: ["createGuilds"], + additionalProperties: false + } + }, + required: ["user"], + additionalProperties: false }, - }, - login: { - requireCaptcha: false, - }, - register: { - email: { - required: true, - allowlist: false, - blocklist: true, - domains: [], // TODO: efficiently save domain blocklist in database - // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"), + limits: { + type: "object", + properties: { + user: { + type: "object", + properties: { + maxFriends: { + type: "number" + }, + maxGuilds: { + type: "number" + }, + maxUsername: { + type: "number" + } + }, + required: ["maxFriends", "maxGuilds", "maxUsername"], + additionalProperties: false + }, + guild: { + type: "object", + properties: { + maxRoles: { + type: "number" + }, + maxMembers: { + type: "number" + }, + maxChannels: { + type: "number" + }, + maxChannelsInCategory: { + type: "number" + }, + hideOfflineMember: { + type: "number" + } + }, + required: ["maxRoles", "maxMembers", "maxChannels", "maxChannelsInCategory", "hideOfflineMember"], + additionalProperties: false + }, + message: { + type: "object", + properties: { + characters: { + type: "number" + }, + ttsCharacters: { + type: "number" + }, + maxReactions: { + type: "number" + }, + maxAttachmentSize: { + type: "number" + }, + maxBulkDelete: { + type: "number" + } + }, + required: ["characters", "ttsCharacters", "maxReactions", "maxAttachmentSize", "maxBulkDelete"], + additionalProperties: false + }, + channel: { + type: "object", + properties: { + maxPins: { + type: "number" + }, + maxTopic: { + type: "number" + } + }, + required: ["maxPins", "maxTopic"], + additionalProperties: false + }, + rate: { + type: "object", + properties: { + ip: { + type: "object", + properties: { + enabled: { type: "boolean" }, + count: { type: "number" }, + timespan: { type: "number" } + }, + required: ["enabled", "count", "timespan"], + additionalProperties: false + }, + routes: { + type: "object", + properties: { + auth: { + type: "object", + properties: { + login: { $ref: "#/definitions/rateLimitOptions" }, + register: { $ref: "#/definitions/rateLimitOptions" } + }, + nullable: true, + required: [], + additionalProperties: false + }, + channel: { + type: "string", + nullable: true + } + }, + required: [], + additionalProperties: false + } + }, + required: ["ip", "routes"] + } + }, + required: ["channel", "guild", "message", "rate", "user"], + additionalProperties: false }, - dateOfBirth: { - required: true, - minimum: 13, + security: { + type: "object", + properties: { + jwtSecret: { + type: "string" + }, + forwadedFor: { + type: "string", + nullable: true + }, + captcha: { + type: "object", + properties: { + enabled: { type: "boolean" }, + service: { + type: "string", + enum: ["hcaptcha", "recaptcha", null], + nullable: true + }, + sitekey: { + type: "string", + nullable: true + }, + secret: { + type: "string", + nullable: true + } + }, + required: ["enabled", "secret", "service", "sitekey"], + additionalProperties: false + } + }, + required: ["captcha", "forwadedFor", "jwtSecret"], + additionalProperties: false }, - requireInvite: false, - requireCaptcha: true, - allowNewRegistration: true, - allowMultipleAccounts: true, - password: { - minLength: 8, - minNumbers: 2, - minUpperCase: 2, - minSymbols: 0, - blockInsecureCommonPasswords: false, + login: { + type: "object", + properties: { + requireCaptcha: { type: "boolean" } + }, + required: ["requireCaptcha"], + additionalProperties: false }, + register: { + type: "object", + properties: { + email: { + type: "object", + properties: { + necessary: { type: "boolean" }, + allowlist: { type: "boolean" }, + blocklist: { type: "boolean" }, + domains: { + type: "array", + items: { + type: "string" + } + } + }, + required: ["allowlist", "blocklist", "domains", "necessary"], + additionalProperties: false + }, + dateOfBirth: { + type: "object", + properties: { + necessary: { type: "boolean" }, + minimum: { type: "number" } + }, + required: ["minimum", "necessary"], + additionalProperties: false + }, + requireCaptcha: { type: "boolean" }, + requireInvite: { type: "boolean" }, + allowNewRegistration: { type: "boolean" }, + allowMultipleAccounts: { type: "boolean" }, + password: { + type: "object", + properties: { + minLength: { type: "number" }, + minNumbers: { type: "number" }, + minUpperCase: { type: "number" }, + minSymbols: { type: "number" }, + blockInsecureCommonPasswords: { type: "boolean" } + }, + required: ["minLength", "minNumbers", "minUpperCase", "minSymbols", "blockInsecureCommonPasswords"], + additionalProperties: false + } + }, + required: [ + "allowMultipleAccounts", + "allowNewRegistration", + "dateOfBirth", + "email", + "password", + "requireCaptcha", + "requireInvite" + ], + additionalProperties: false + } }, + required: ["gateway", "general", "limits", "login", "permissions", "register", "security"], + additionalProperties: false }; + +const ajv = new Ajv(); +const validator = ajv.compile(schema); + +const configPath = getConfigPathForFile("fosscord", "api", ".json"); + +export const apiConfig = new Config<DefaultOptions>({ path: configPath, schemaValidator: validator, schema: schema }); diff --git a/src/util/Member.ts b/src/util/Member.ts
index fec5aac7..7b06720b 100644 --- a/src/util/Member.ts +++ b/src/util/Member.ts
@@ -11,10 +11,10 @@ import { toObject, UserModel, GuildDocument, + Config } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; -import Config from "./Config"; import { emitEvent } from "./Event"; import { getPublicUser } from "./User"; @@ -27,7 +27,7 @@ export const PublicMemberProjection = { pending: true, deaf: true, mute: true, - premium_since: true, + premium_since: true }; export async function isMember(user_id: string, guild_id: string) { @@ -59,12 +59,13 @@ export async function addMember(user_id: string, guild_id: string, cache?: { gui premium_since: undefined, deaf: false, mute: false, - pending: false, + pending: false }; await Promise.all([ new MemberModel({ ...member, + read_state: {}, settings: { channel_overrides: [], message_notifications: 0, @@ -73,8 +74,8 @@ export async function addMember(user_id: string, guild_id: string, cache?: { gui muted: false, suppress_everyone: false, suppress_roles: false, - version: 0, - }, + version: 0 + } }).save(), UserModel.updateOne({ id: user_id }, { $push: { guilds: guild_id } }).exec(), @@ -85,10 +86,10 @@ export async function addMember(user_id: string, guild_id: string, cache?: { gui data: { ...member, user, - guild_id: guild_id, + guild_id: guild_id }, - guild_id: guild_id, - } as GuildMemberAddEvent), + guild_id: guild_id + } as GuildMemberAddEvent) ]); await emitEvent({ @@ -99,7 +100,7 @@ export async function addMember(user_id: string, guild_id: string, cache?: { gui .populate({ path: "joined_at", match: { id: user.id } }) .execPopulate() ), - user_id, + user_id } as GuildCreateEvent); } @@ -115,7 +116,7 @@ export async function removeMember(user_id: string, guild_id: string) { return Promise.all([ MemberModel.deleteOne({ id: user_id, - guild_id: guild_id, + guild_id: guild_id }).exec(), UserModel.updateOne({ id: user.id }, { $pull: { guilds: guild_id } }).exec(), GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: -1 } }).exec(), @@ -123,18 +124,18 @@ export async function removeMember(user_id: string, guild_id: string) { emitEvent({ event: "GUILD_DELETE", data: { - id: guild_id, + id: guild_id }, - user_id: user_id, + user_id: user_id } as GuildDeleteEvent), emitEvent({ event: "GUILD_MEMBER_REMOVE", data: { guild_id: guild_id, - user: user, + user: user }, - guild_id: guild_id, - } as GuildMemberRemoveEvent), + guild_id: guild_id + } as GuildMemberRemoveEvent) ]); } @@ -147,7 +148,7 @@ export async function addRole(user_id: string, guild_id: string, role_id: string var memberObj = await MemberModel.findOneAndUpdate( { id: user_id, - guild_id: guild_id, + guild_id: guild_id }, { $push: { roles: role_id } } ).exec(); @@ -159,9 +160,9 @@ export async function addRole(user_id: string, guild_id: string, role_id: string data: { guild_id: guild_id, user: user, - roles: memberObj.roles, + roles: memberObj.roles }, - guild_id: guild_id, + guild_id: guild_id } as GuildMemberUpdateEvent); } @@ -174,7 +175,7 @@ export async function removeRole(user_id: string, guild_id: string, role_id: str var memberObj = await MemberModel.findOneAndUpdate( { id: user_id, - guild_id: guild_id, + guild_id: guild_id }, { $pull: { roles: role_id } } ).exec(); @@ -186,9 +187,9 @@ export async function removeRole(user_id: string, guild_id: string, role_id: str data: { guild_id: guild_id, user: user, - roles: memberObj.roles, + roles: memberObj.roles }, - guild_id: guild_id, + guild_id: guild_id } as GuildMemberUpdateEvent); } @@ -198,7 +199,7 @@ export async function changeNickname(user_id: string, guild_id: string, nickname var memberObj = await MemberModel.findOneAndUpdate( { id: user_id, - guild_id: guild_id, + guild_id: guild_id }, { nick: nickname } ).exec(); @@ -210,8 +211,8 @@ export async function changeNickname(user_id: string, guild_id: string, nickname data: { guild_id: guild_id, user: user, - nick: nickname, + nick: nickname }, - guild_id: guild_id, + guild_id: guild_id } as GuildMemberUpdateEvent); } diff --git a/src/util/Message.ts b/src/util/Message.ts new file mode 100644
index 00000000..0d3cdac7 --- /dev/null +++ b/src/util/Message.ts
@@ -0,0 +1,54 @@ +import { ChannelModel, MessageCreateEvent } from "@fosscord/server-util"; +import { Snowflake } from "@fosscord/server-util"; +import { MessageModel } from "@fosscord/server-util"; +import { PublicMemberProjection } from "@fosscord/server-util"; +import { toObject } from "@fosscord/server-util"; +import { getPermission } from "@fosscord/server-util"; +import { Message } from "@fosscord/server-util"; +import { HTTPError } from "lambert-server"; +import { emitEvent } from "./Event"; +// TODO: check webhook, application, system author + +export async function handleMessage(opts: Partial<Message>) { + const channel = await ChannelModel.findOne({ id: opts.channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec(); + if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); + // TODO: are tts messages allowed in dm channels? should permission be checked? + + const permissions = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel }); + permissions.hasThrow("SEND_MESSAGES"); + if (opts.tts) permissions.hasThrow("SEND_TTS_MESSAGES"); + if (opts.message_reference) { + permissions.hasThrow("READ_MESSAGE_HISTORY"); + if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); + } + + if (opts.message_reference) { + if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); + // TODO: should be checked if the referenced message exists? + } + + // TODO: check and put it all in the body + return { + ...opts, + guild_id: channel.guild_id, + channel_id: opts.channel_id, + // TODO: generate mentions and check permissions + mention_channels_ids: [], + mention_role_ids: [], + mention_user_ids: [], + attachments: [], // TODO: message attachments + embeds: opts.embeds || [], + reactions: opts.reactions || [], + type: opts.type ?? 0 + }; +} + +export async function sendMessage(opts: Partial<Message>) { + const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() }); + + const data = toObject(await new MessageModel(message).populate({ path: "member", select: PublicMemberProjection }).save()); + + await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data, guild_id: message.guild_id } as MessageCreateEvent); + + return data; +} diff --git a/src/util/instanceOf.ts b/src/util/instanceOf.ts
index e4e58092..b67bde27 100644 --- a/src/util/instanceOf.ts +++ b/src/util/instanceOf.ts
@@ -5,7 +5,8 @@ import { Tuple } from "lambert-server"; import "missing-native-js-functions"; export const OPTIONAL_PREFIX = "$"; -export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +export const EMAIL_REGEX = + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export function check(schema: any) { return (req: Request, res: Response, next: NextFunction) => { @@ -27,9 +28,9 @@ export function FieldErrors(fields: Record<string, { code?: string; message: str _errors: [ { message, - code: code || "BASE_TYPE_INVALID", - }, - ], + code: code || "BASE_TYPE_INVALID" + } + ] })) ); } @@ -68,7 +69,7 @@ export function instanceOf( optional = false, errors = {}, req, - ref, + ref }: { path?: string; optional?: boolean; errors?: any; req: Request; ref?: { key: string | number; obj: any } } ): Boolean { if (!ref) ref = { obj: null, key: "" }; @@ -131,7 +132,7 @@ export function instanceOf( optional, errors: errors[i], req, - ref: { key: i, obj: value }, + ref: { key: i, obj: value } }) === true ) { delete errors[i]; @@ -153,7 +154,7 @@ export function instanceOf( throw new FieldError( "BASE_TYPE_BAD_LENGTH", req.t("common:field.BASE_TYPE_BAD_LENGTH", { - length: `${type.min} - ${type.max}`, + length: `${type.min} - ${type.max}` }) ); } @@ -185,7 +186,7 @@ export function instanceOf( optional: OPTIONAL, errors: errors[newKey], req, - ref: { key: newKey, obj: value }, + ref: { key: newKey, obj: value } }) === true ) { delete errors[newKey]; diff --git a/src/util/passwordStrength.ts b/src/util/passwordStrength.ts
index f6cec9da..cc503843 100644 --- a/src/util/passwordStrength.ts +++ b/src/util/passwordStrength.ts
@@ -1,5 +1,5 @@ +import { Config } from "@fosscord/server-util"; import "missing-native-js-functions"; -import Config from "./Config"; const reNUMBER = /[0-9]/g; const reUPPERCASELETTER = /[A-Z]/g; @@ -17,13 +17,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored * Returns: 0 > pw > 1 */ export function check(password: string): number { - const { - minLength, - minNumbers, - minUpperCase, - minSymbols, - blockInsecureCommonPasswords, - } = Config.get().register.password; + const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password; var strength = 0; // checks for total password len @@ -51,10 +45,5 @@ export function check(password: string): number { strength = 0; } - if (blockInsecureCommonPasswords) { - if (blocklist.includes(password)) { - strength = 0; - } - } return strength; }