summary refs log tree commit diff
path: root/api/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/util')
-rw-r--r--api/src/util/ApiError.ts23
-rw-r--r--api/src/util/Channel.ts63
-rw-r--r--api/src/util/Constants.ts293
-rw-r--r--api/src/util/Member.ts222
-rw-r--r--api/src/util/Message.ts117
-rw-r--r--api/src/util/User.ts16
-rw-r--r--api/src/util/Voice.ts32
-rw-r--r--api/src/util/ipAddress.ts17
8 files changed, 371 insertions, 412 deletions
diff --git a/api/src/util/ApiError.ts b/api/src/util/ApiError.ts
new file mode 100644

index 00000000..2316cd71 --- /dev/null +++ b/api/src/util/ApiError.ts
@@ -0,0 +1,23 @@ +export class ApiError extends Error { + constructor(readonly message: string, public readonly code: number, public readonly httpStatus: number = 400, public readonly defaultParams?: string[]) { + super(message); + } + + withDefaultParams(): ApiError { + if(this.defaultParams) + return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus) + return this + } + + withParams(...params: string[]): ApiError { + return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus) + } +} + +export function applyParamsToString(s: string, params: string[]): string { + let newString = s + params.forEach(a => { + newString = newString.replace("{}", a) + }) + return newString +} diff --git a/api/src/util/Channel.ts b/api/src/util/Channel.ts deleted file mode 100644
index fb6f9c8c..00000000 --- a/api/src/util/Channel.ts +++ /dev/null
@@ -1,63 +0,0 @@ -import { - ChannelCreateEvent, - ChannelModel, - ChannelType, - emitEvent, - getPermission, - GuildModel, - Snowflake, - TextChannel, - toObject, - VoiceChannel -} from "@fosscord/util"; -import { HTTPError } from "lambert-server"; - -// TODO: DM channel -export async function createChannel( - channel: Partial<TextChannel | VoiceChannel>, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - } -) { - // Always check if user has permission first - const permissions = await getPermission(user_id, channel.guild_id); - permissions.hasThrow("MANAGE_CHANNELS"); - - switch (channel.type) { - case ChannelType.GUILD_TEXT: - case ChannelType.GUILD_VOICE: - if (channel.parent_id && !opts?.skipExistsCheck) { - 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"); - } - break; - case ChannelType.GUILD_CATEGORY: - 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"); - } - - if (!channel.permission_overwrites) channel.permission_overwrites = []; - // TODO: auto generate position - - channel = await new ChannelModel({ - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - // @ts-ignore - recipient_ids: null - }).save(); - - await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), guild_id: channel.guild_id } as ChannelCreateEvent); - - return channel; -} diff --git a/api/src/util/Constants.ts b/api/src/util/Constants.ts
index f3a8dd67..15fdc519 100644 --- a/api/src/util/Constants.ts +++ b/api/src/util/Constants.ts
@@ -1,3 +1,5 @@ +import {ApiError} from "./ApiError"; + export const WSCodes = { 1000: "WS_CLOSE_REQUESTED", 4004: "TOKEN_INVALID", @@ -421,6 +423,7 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] /** * An error encountered while performing an API request. Here are the potential errors: + * * GENERAL_ERROR * * UNKNOWN_ACCOUNT * * UNKNOWN_APPLICATION * * UNKNOWN_CHANNEL @@ -436,27 +439,70 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] * * UNKNOWN_USER * * UNKNOWN_EMOJI * * UNKNOWN_WEBHOOK + * * UNKNOWN_WEBHOOK_SERVICE + * * UNKNOWN_SESSION * * UNKNOWN_BAN + * * UNKNOWN_SKU + * * UNKNOWN_STORE_LISTING + * * UNKNOWN_ENTITLEMENT + * * UNKNOWN_BUILD + * * UNKNOWN_LOBBY + * * UNKNOWN_BRANCH + * * UNKNOWN_STORE_DIRECTORY_LAYOUT + * * UNKNOWN_REDISTRIBUTABLE + * * UNKNOWN_GIFT_CODE + * * UNKNOWN_STREAM + * * UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN * * UNKNOWN_GUILD_TEMPLATE + * * UNKNOWN_DISCOVERABLE_SERVER_CATEGORY + * * UNKNOWN_STICKER + * * UNKNOWN_INTERACTION + * * UNKNOWN_APPLICATION_COMMAND + * * UNKNOWN_APPLICATION_COMMAND_PERMISSIONS + * * UNKNOWN_STAGE_INSTANCE + * * UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM + * * UNKNOWN_GUILD_WELCOME_SCREEN + * * UNKNOWN_GUILD_SCHEDULED_EVENT + * * UNKNOWN_GUILD_SCHEDULED_EVENT_USER * * BOT_PROHIBITED_ENDPOINT * * BOT_ONLY_ENDPOINT - * * CHANNEL_HIT_WRITE_RATELIMIT + * * EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT + * * ACTION_NOT_AUTHORIZED_ON_APPLICATION + * * SLOWMODE_RATE_LIMIT + * * ONLY_OWNER + * * ANNOUNCEMENT_RATE_LIMITS + * * CHANNEL_WRITE_RATELIMIT + * * WORDS_NOT_ALLOWED + * * GUILD_PREMIUM_LEVEL_TOO_LOW * * MAXIMUM_GUILDS * * MAXIMUM_FRIENDS * * MAXIMUM_PINS + * * MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED * * MAXIMUM_ROLES * * MAXIMUM_WEBHOOKS + * * MAXIMUM_NUMBER_OF_EMOJIS_REACHED * * MAXIMUM_REACTIONS * * MAXIMUM_CHANNELS * * MAXIMUM_ATTACHMENTS * * MAXIMUM_INVITES + * * MAXIMUM_ANIMATED_EMOJIS + * * MAXIMUM_SERVER_MEMBERS + * * MAXIMUM_SERVER_CATEGORIES * * GUILD_ALREADY_HAS_TEMPLATE + * * MAXIMUM_THREAD_PARTICIPANTS + * * MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS + * * MAXIMUM_BANS_FETCHES + * * MAXIMUM_STICKERS + * * MAXIMUM_PRUNE_REQUESTS * * UNAUTHORIZED * * ACCOUNT_VERIFICATION_REQUIRED + * * OPENING_DIRECT_MESSAGES_TOO_FAST * * REQUEST_ENTITY_TOO_LARGE * * FEATURE_TEMPORARILY_DISABLED * * USER_BANNED + * * TARGET_USER_IS_NOT_CONNECTED_TO_VOICE * * ALREADY_CROSSPOSTED + * * APPLICATION_COMMAND_ALREADY_EXISTS * * MISSING_ACCESS * * INVALID_ACCOUNT_TYPE * * CANNOT_EXECUTE_ON_DM @@ -476,81 +522,196 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE * * INVALID_OAUTH_TOKEN + * * MISSING_REQUIRED_OAUTH2_SCOPE + * * INVALID_WEBHOOK_TOKEN_PROVIDED + * * INVALID_ROLE + * * INVALID_RECIPIENT * * BULK_DELETE_MESSAGE_TOO_OLD * * INVALID_FORM_BODY * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT * * INVALID_API_VERSION + * * FILE_EXCEEDS_MAXIMUM_SIZE + * * INVALID_FILE_UPLOADED + * * CANNOT_SELF_REDEEM_GIFT + * * PAYMENT_SOURCE_REQUIRED * * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL + * * INVALID_STICKER_SENT + * * CANNOT_EDIT_ARCHIVED_THREAD + * * INVALID_THREAD_NOTIFICATION_SETTINGS + * * BEFORE_EARLIER_THAN_THREAD_CREATION_DATE + * * SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION + * * SERVER_NEEDS_MONETIZATION_ENABLED + * * TWO_FACTOR_REQUIRED + * * NO_USERS_WITH_DISCORDTAG_EXIST * * REACTION_BLOCKED * * RESOURCE_OVERLOADED + * * STAGE_ALREADY_OPEN + * * THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE + * * THREAD_IS_LOCKED + * * MAXIMUM_NUMBER_OF_ACTIVE_THREADS + * * MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS + * * INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE + * * LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES + * * STICKER_MAXIMUM_FRAMERATE + * * STICKER_MAXIMUM_FRAME_COUNT + * * LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS + * * STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE + * * STICKER_ANIMATION_DURATION_MAXIMUM + * * UNKNOWN_VOICE_STATE * @typedef {string} APIError */ -export const APIErrors = { - UNKNOWN_ACCOUNT: 10001, - UNKNOWN_APPLICATION: 10002, - UNKNOWN_CHANNEL: 10003, - UNKNOWN_GUILD: 10004, - UNKNOWN_INTEGRATION: 10005, - UNKNOWN_INVITE: 10006, - UNKNOWN_MEMBER: 10007, - UNKNOWN_MESSAGE: 10008, - UNKNOWN_OVERWRITE: 10009, - UNKNOWN_PROVIDER: 10010, - UNKNOWN_ROLE: 10011, - UNKNOWN_TOKEN: 10012, - UNKNOWN_USER: 10013, - UNKNOWN_EMOJI: 10014, - UNKNOWN_WEBHOOK: 10015, - UNKNOWN_BAN: 10026, - UNKNOWN_GUILD_TEMPLATE: 10057, - BOT_PROHIBITED_ENDPOINT: 20001, - BOT_ONLY_ENDPOINT: 20002, - CHANNEL_HIT_WRITE_RATELIMIT: 20028, - MAXIMUM_GUILDS: 30001, - MAXIMUM_FRIENDS: 30002, - MAXIMUM_PINS: 30003, - MAXIMUM_ROLES: 30005, - MAXIMUM_WEBHOOKS: 30007, - MAXIMUM_REACTIONS: 30010, - MAXIMUM_CHANNELS: 30013, - MAXIMUM_ATTACHMENTS: 30015, - MAXIMUM_INVITES: 30016, - GUILD_ALREADY_HAS_TEMPLATE: 30031, - UNAUTHORIZED: 40001, - ACCOUNT_VERIFICATION_REQUIRED: 40002, - REQUEST_ENTITY_TOO_LARGE: 40005, - FEATURE_TEMPORARILY_DISABLED: 40006, - USER_BANNED: 40007, - ALREADY_CROSSPOSTED: 40033, - MISSING_ACCESS: 50001, - INVALID_ACCOUNT_TYPE: 50002, - CANNOT_EXECUTE_ON_DM: 50003, - EMBED_DISABLED: 50004, - CANNOT_EDIT_MESSAGE_BY_OTHER: 50005, - CANNOT_SEND_EMPTY_MESSAGE: 50006, - CANNOT_MESSAGE_USER: 50007, - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008, - CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009, - OAUTH2_APPLICATION_BOT_ABSENT: 50010, - MAXIMUM_OAUTH2_APPLICATIONS: 50011, - INVALID_OAUTH_STATE: 50012, - MISSING_PERMISSIONS: 50013, - INVALID_AUTHENTICATION_TOKEN: 50014, - NOTE_TOO_LONG: 50015, - INVALID_BULK_DELETE_QUANTITY: 50016, - CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, - INVALID_OR_TAKEN_INVITE_CODE: 50020, - CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, - INVALID_OAUTH_TOKEN: 50025, - BULK_DELETE_MESSAGE_TOO_OLD: 50034, - INVALID_FORM_BODY: 50035, - INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, - INVALID_API_VERSION: 50041, - CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074, - REACTION_BLOCKED: 90001, - RESOURCE_OVERLOADED: 130000, -}; +export const DiscordApiErrors = { + //https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes + GENERAL_ERROR: new ApiError("General error (such as a malformed request body, amongst other things)", 0), + UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001), + UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002), + UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003), + UNKNOWN_GUILD: new ApiError("Unknown guild", 10004), + UNKNOWN_INTEGRATION: new ApiError("Unknown integration", 10005), + UNKNOWN_INVITE: new ApiError("Unknown invite", 10006), + UNKNOWN_MEMBER: new ApiError("Unknown member", 10007), + UNKNOWN_MESSAGE: new ApiError("Unknown message", 10008), + UNKNOWN_OVERWRITE: new ApiError("Unknown permission overwrite", 10009), + UNKNOWN_PROVIDER: new ApiError("Unknown provider", 10010), + UNKNOWN_ROLE: new ApiError("Unknown role", 10011), + UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), + UNKNOWN_USER: new ApiError("Unknown user", 10013), + UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), + UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015), + UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), + UNKNOWN_SESSION: new ApiError("Unknown session", 10020), + UNKNOWN_BAN: new ApiError("Unknown ban", 10026), + UNKNOWN_SKU: new ApiError("Unknown SKU", 10027), + UNKNOWN_STORE_LISTING: new ApiError("Unknown Store Listing", 10028), + UNKNOWN_ENTITLEMENT: new ApiError("Unknown entitlement", 10029), + UNKNOWN_BUILD: new ApiError("Unknown build", 10030), + UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031), + UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032), + UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError("Unknown store directory layout", 10033), + UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036), + UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038), + UNKNOWN_STREAM: new ApiError("Unknown stream", 10049), + UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError("Unknown premium server subscribe cooldown", 10050), + UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057), + UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError("Unknown discoverable server category", 10059), + UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060), + UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062), + UNKNOWN_APPLICATION_COMMAND: new ApiError("Unknown application command", 10063), + UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError("Unknown application command permissions", 10066), + UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067), + UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError("Unknown Guild Member Verification Form", 10068), + UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError("Unknown Guild Welcome Screen", 10069), + UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError("Unknown Guild Scheduled Event", 10070), + UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError("Unknown Guild Scheduled Event User", 10071), + BOT_PROHIBITED_ENDPOINT: new ApiError("Bots cannot use this endpoint", 20001), + BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002), + EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError("Explicit content cannot be sent to the desired recipient(s)", 20009), + ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError("You are not authorized to perform this action on this application", 20012), + SLOWMODE_RATE_LIMIT: new ApiError("This action cannot be performed due to slowmode rate limit", 20016), + ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018), + ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022), + CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028), + WORDS_NOT_ALLOWED: new ApiError("Your Stage topic, server name, server description, or channel names contain words that are not allowed", 20031), + GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035), + MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]), + MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]), + MAXIMUM_PINS: new ApiError("Maximum number of pins reached for the channel ({})", 30003, undefined, ["50"]), + MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError("Maximum number of recipients reached ({})", 30004, undefined, ["10"]), + MAXIMUM_ROLES: new ApiError("Maximum number of guild roles reached ({})", 30005, undefined, ["250"]), + MAXIMUM_WEBHOOKS: new ApiError("Maximum number of webhooks reached ({})", 30007, undefined, ["10"]), + MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError("Maximum number of emojis reached", 30008), + MAXIMUM_REACTIONS: new ApiError("Maximum number of reactions reached ({})", 30010, undefined, ["20"]), + MAXIMUM_CHANNELS: new ApiError("Maximum number of guild channels reached ({})", 30013, undefined, ["500"]), + MAXIMUM_ATTACHMENTS: new ApiError("Maximum number of attachments in a message reached ({})", 30015, undefined, ["10"]), + MAXIMUM_INVITES: new ApiError("Maximum number of invites reached ({})", 30016, undefined, ["1000"]), + MAXIMUM_ANIMATED_EMOJIS: new ApiError("Maximum number of animated emojis reached", 30018), + MAXIMUM_SERVER_MEMBERS: new ApiError("Maximum number of server members reached", 30019), + MAXIMUM_SERVER_CATEGORIES: new ApiError("Maximum number of server categories has been reached ({})", 30030, undefined, ["5"]), + GUILD_ALREADY_HAS_TEMPLATE: new ApiError("Guild already has a template", 30031), + MAXIMUM_THREAD_PARTICIPANTS: new ApiError("Max number of thread participants has been reached", 30033), + MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError("Maximum number of bans for non-guild members have been exceeded", 30035), + MAXIMUM_BANS_FETCHES: new ApiError("Maximum number of bans fetches has been reached", 30037), + MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039), + MAXIMUM_PRUNE_REQUESTS: new ApiError("Maximum number of prune requests has been reached. Try again later", 30040), + UNAUTHORIZED: new ApiError("Unauthorized. Provide a valid token and try again", 40001), + ACCOUNT_VERIFICATION_REQUIRED: new ApiError("You need to verify your account in order to perform this action", 40002), + OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError("You are opening direct messages too fast", 40003), + REQUEST_ENTITY_TOO_LARGE: new ApiError("Request entity too large. Try sending something smaller in size", 40005), + FEATURE_TEMPORARILY_DISABLED: new ApiError("This feature has been temporarily disabled server-side", 40006), + USER_BANNED: new ApiError("The user is banned from this guild", 40007), + TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError("Target user is not connected to voice", 40032), + ALREADY_CROSSPOSTED: new ApiError("This message has already been crossposted", 40033), + APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError("An application command with that name already exists", 40041), + MISSING_ACCESS: new ApiError("Missing access", 50001), + INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002), + CANNOT_EXECUTE_ON_DM: new ApiError("Cannot execute action on a DM channel", 50003), + EMBED_DISABLED: new ApiError("Guild widget disabled", 50004), + CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError("Cannot edit a message authored by another user", 50005), + CANNOT_SEND_EMPTY_MESSAGE: new ApiError("Cannot send an empty message", 50006), + CANNOT_MESSAGE_USER: new ApiError("Cannot send messages to this user", 50007), + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError("Cannot send messages in a voice channel", 50008), + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError("Channel verification level is too high for you to gain access", 50009), + OAUTH2_APPLICATION_BOT_ABSENT: new ApiError("OAuth2 application does not have a bot", 50010), + MAXIMUM_OAUTH2_APPLICATIONS: new ApiError("OAuth2 application limit reached", 50011), + INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012), + MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action", 50013), + INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014), + NOTE_TOO_LONG: new ApiError("Note was too long", 50015), + INVALID_BULK_DELETE_QUANTITY: new ApiError("Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", 50016, undefined, ["2","100"]), + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError("A message can only be pinned to the channel it was sent in", 50019), + INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020), + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021), + CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError("Cannot execute action on this channel type", 50024), + INVALID_OAUTH_TOKEN: new ApiError("Invalid OAuth2 access token provided", 50025), + MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError("Missing required OAuth2 scope", 50026), + INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError("Invalid webhook token provided", 50027), + INVALID_ROLE: new ApiError("Invalid role", 50028), + INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), + BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034), + INVALID_FORM_BODY: new ApiError("Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", 50035), + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError("An invite was accepted to a guild the application's bot is not in", 50036), + INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), + FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045), + INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046), + CANNOT_SELF_REDEEM_GIFT: new ApiError("Cannot self-redeem this gift", 50054), + PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070), + CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError("Cannot delete a channel required for Community guilds", 50074), + INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), + CANNOT_EDIT_ARCHIVED_THREAD: new ApiError("Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", 50083), + INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084), + BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError("before value is earlier than the thread creation date", 50085), + SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095), + SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError("This server needs monetization enabled in order to perform this action", 50097), + TWO_FACTOR_REQUIRED: new ApiError("Two factor is required for this operation", 60003), + NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError("No users with DiscordTag exist", 80004), + REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001), + RESOURCE_OVERLOADED: new ApiError("API resource is currently overloaded. Try again a little later", 130000), + STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006), + THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError("A thread has already been created for this message", 160004), + THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005), + MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError("Maximum number of active threads reached", 160006), + MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError("Maximum number of active announcement threads reached", 160007), + INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError("Invalid JSON for uploaded Lottie file", 170001), + LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError("Uploaded Lotties cannot contain rasterized images such as PNG or JPEG", 170002), + STICKER_MAXIMUM_FRAMERATE: new ApiError("Sticker maximum framerate exceeded", 170003), + STICKER_MAXIMUM_FRAME_COUNT: new ApiError("Sticker frame count exceeds maximum of {} frames", 170004, undefined, ["1000"]), + LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError("Lottie animation maximum dimensions exceeded", 170005), + STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError("Sticker frame rate is either too small or too large", 170006), + STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError("Sticker animation duration exceeds maximum of {} seconds", 170007, undefined, ["5"]), + + + //Other errors + UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404), +} + +/** + * An error encountered while performing an API request (Fosscord only). Here are the potential errors: + */ +export const FosscordApiErrors = { + +} /** * The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types: diff --git a/api/src/util/Member.ts b/api/src/util/Member.ts deleted file mode 100644
index da02735c..00000000 --- a/api/src/util/Member.ts +++ /dev/null
@@ -1,222 +0,0 @@ -import { - Guild, - GuildCreateEvent, - GuildDeleteEvent, - GuildMemberAddEvent, - GuildMemberRemoveEvent, - GuildMemberUpdateEvent, - GuildModel, - MemberModel, - RoleModel, - toObject, - UserModel, - GuildDocument, - Config, - emitEvent -} from "@fosscord/util"; - -import { HTTPError } from "lambert-server"; - -import { getPublicUser } from "./User"; - -export const PublicMemberProjection = { - id: true, - guild_id: true, - nick: true, - roles: true, - joined_at: true, - pending: true, - deaf: true, - mute: true, - premium_since: true -}; - -export async function isMember(user_id: string, guild_id: string) { - const exists = await MemberModel.exists({ id: user_id, guild_id }); - if (!exists) throw new HTTPError("You are not a member of this guild", 403); - return exists; -} - -export async function addMember(user_id: string, guild_id: string, cache?: { guild?: GuildDocument }) { - const user = await getPublicUser(user_id, { guilds: true }); - - const { maxGuilds } = Config.get().limits.user; - if (user.guilds.length >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); - } - - const guild = cache?.guild || (await GuildModel.findOne({ id: guild_id }).exec()); - - if (!guild) throw new HTTPError("Guild not found", 404); - - if (await MemberModel.exists({ id: user.id, guild_id })) throw new HTTPError("You are already a member of this guild", 400); - - const member = { - id: user_id, - guild_id: guild_id, - nick: undefined, - roles: [guild_id], // @everyone role - joined_at: new Date(), - premium_since: undefined, - deaf: false, - mute: false, - pending: false - }; - - await Promise.all([ - new MemberModel({ - ...member, - read_state: {}, - settings: { - channel_overrides: [], - message_notifications: 0, - mobile_push: true, - mute_config: null, - muted: false, - suppress_everyone: false, - suppress_roles: false, - version: 0 - } - }).save(), - - UserModel.updateOne({ id: user_id }, { $push: { guilds: guild_id } }).exec(), - GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: 1 } }).exec(), - - emitEvent({ - event: "GUILD_MEMBER_ADD", - data: { - ...member, - user, - guild_id: guild_id - }, - guild_id: guild_id - } as GuildMemberAddEvent) - ]); - - await emitEvent({ - event: "GUILD_CREATE", - data: toObject( - await guild - .populate({ path: "members", match: { guild_id } }) - .populate({ path: "joined_at", match: { id: user.id } }) - .execPopulate() - ), - user_id - } as GuildCreateEvent); -} - -export async function removeMember(user_id: string, guild_id: string) { - const user = await getPublicUser(user_id); - - const guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec(); - if (!guild) throw new HTTPError("Guild not found", 404); - if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild"); - if (!(await MemberModel.exists({ id: user.id, guild_id }))) throw new HTTPError("Is not member of this guild", 404); - - // use promise all to execute all promises at the same time -> save time - return Promise.all([ - MemberModel.deleteOne({ - id: user_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(), - - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id - }, - user_id: user_id - } as GuildDeleteEvent), - emitEvent({ - event: "GUILD_MEMBER_REMOVE", - data: { - guild_id: guild_id, - user: user - }, - guild_id: guild_id - } as GuildMemberRemoveEvent) - ]); -} - -export async function addRole(user_id: string, guild_id: string, role_id: string) { - const user = await getPublicUser(user_id); - - const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec(); - if (!role) throw new HTTPError("role not found", 404); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { $push: { roles: role_id } }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - roles: memberObj.roles - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} - -export async function removeRole(user_id: string, guild_id: string, role_id: string) { - const user = await getPublicUser(user_id); - - const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec(); - if (!role) throw new HTTPError("role not found", 404); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { $pull: { roles: role_id } }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - roles: memberObj.roles - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} - -export async function changeNickname(user_id: string, guild_id: string, nickname: string) { - const user = await getPublicUser(user_id); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { nick: nickname }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - nick: nickname - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} diff --git a/api/src/util/Message.ts b/api/src/util/Message.ts
index 8a1e959e..fea553bc 100644 --- a/api/src/util/Message.ts +++ b/api/src/util/Message.ts
@@ -1,5 +1,5 @@ import { - ChannelModel, + Channel, Embed, emitEvent, Message, @@ -7,21 +7,23 @@ import { MessageUpdateEvent, getPermission, CHANNEL_MENTION, - toObject, - MessageModel, Snowflake, - PublicMemberProjection, USER_MENTION, ROLE_MENTION, - RoleModel, + Role, EVERYONE_MENTION, - HERE_MENTION + HERE_MENTION, + MessageType, + User, + Application, + Webhook, + Attachment } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import fetch from "node-fetch"; import cheerio from "cheerio"; +import { MessageCreateSchema } from "../schema/Message"; -import { MessageType } from "@fosscord/util/dist/util/Constants"; // TODO: check webhook, application, system author const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; @@ -37,19 +39,37 @@ const DEFAULT_FETCH_OPTIONS: any = { method: "GET" }; -export async function handleMessage(opts: Partial<Message>) { - const channel = await ChannelModel.findOne( - { id: opts.channel_id }, - { guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true } - ) - .lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids - .exec(); +export async function handleMessage(opts: MessageOptions): Promise<Message> { + const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: ["recipients"] }); if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); + + const message = new Message({ + ...opts, + guild_id: channel.guild_id, + channel_id: opts.channel_id, + attachments: opts.attachments || [], + embeds: opts.embeds || [], + reactions: /*opts.reactions ||*/ [], + type: opts.type ?? 0 + }); + // TODO: are tts messages allowed in dm channels? should permission be checked? + if (opts.author_id) { + message.author = await User.getPublicUser(opts.author_id); + } + if (opts.application_id) { + message.application = await Application.findOneOrFail({ id: opts.application_id }); + } + if (opts.webhook_id) { + message.webhook = await Webhook.findOneOrFail({ id: opts.webhook_id }); + } - // @ts-ignore - const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel }); + const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id); permission.hasThrow("SEND_MESSAGES"); + if (permission.cache.member) { + message.member = permission.cache.member; + } + if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); if (opts.message_reference) { permission.hasThrow("READ_MESSAGE_HISTORY"); @@ -57,24 +77,24 @@ export async function handleMessage(opts: Partial<Message>) { 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? // @ts-ignore - opts.type = MessageType.REPLY; + message.type = MessageType.REPLY; } - if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.stickers?.length && !opts.activity) { + // TODO: stickers/activity + if (!opts.content && !opts.embeds?.length && !opts.attachments?.length) { throw new HTTPError("Empty messages are not allowed", 50006); } var content = opts.content; - var mention_channels_ids = [] as string[]; + var mention_channel_ids = [] as string[]; var mention_role_ids = [] as string[]; var mention_user_ids = [] as string[]; var mention_everyone = false; - var mention_everyone = false; if (content) { - content = content.trim(); + message.content = content.trim(); for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) { - if (!mention_channels_ids.includes(mention)) mention_channels_ids.push(mention); + if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); } for (const [_, mention] of content.matchAll(USER_MENTION)) { @@ -83,7 +103,7 @@ export async function handleMessage(opts: Partial<Message>) { await Promise.all( Array.from(content.matchAll(ROLE_MENTION)).map(async ([_, mention]) => { - const role = await RoleModel.findOne({ id: mention, guild_id: channel.guild_id }).exec(); + const role = await Role.findOneOrFail({ id: mention, guild_id: channel.guild_id }); if (role.mentionable || permission.has("MANAGE_ROLES")) { mention_role_ids.push(mention); } @@ -95,20 +115,14 @@ export async function handleMessage(opts: Partial<Message>) { } } + message.mention_channels = mention_channel_ids.map((x) => new Channel({ id: x })); + message.mention_roles = mention_role_ids.map((x) => new Role({ id: x })); + message.mentions = mention_user_ids.map((x) => new User({ id: x })); + message.mention_everyone = mention_everyone; + // TODO: check and put it all in the body - return { - ...opts, - guild_id: channel.guild_id, - channel_id: opts.channel_id, - mention_channels_ids, - mention_role_ids, - mention_user_ids, - mention_everyone, - attachments: opts.attachments || [], - embeds: opts.embeds || [], - reactions: opts.reactions || [], - type: opts.type ?? 0 - }; + + return message; } // TODO: cache link result in db @@ -160,20 +174,33 @@ export async function postHandleMessage(message: Message) { channel_id: message.channel_id, data } as MessageUpdateEvent), - MessageModel.updateOne({ id: message.id, channel_id: message.channel_id }, data).exec() + Message.update({ id: message.id, channel_id: message.channel_id }, data) ]); } -export async function sendMessage(opts: Partial<Message>) { - const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() }); +export async function sendMessage(opts: MessageOptions) { + const message = await handleMessage({ ...opts, timestamp: new Date() }); - const data = toObject( - await new MessageModel(message).populate({ path: "member", select: PublicMemberProjection }).populate("referenced_message").save() - ); + await Promise.all([ + message.save(), + emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent) + ]); - await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data } as MessageCreateEvent); + postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error - postHandleMessage(data).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error + return message; +} - return data; +interface MessageOptions extends MessageCreateSchema { + id?: string; + type?: MessageType; + pinned?: boolean; + author_id?: string; + webhook_id?: string; + application_id?: string; + embeds?: Embed[]; + channel_id?: string; + attachments?: Attachment[]; + edited_timestamp?: Date; + timestamp?: Date; } diff --git a/api/src/util/User.ts b/api/src/util/User.ts deleted file mode 100644
index 392c7101..00000000 --- a/api/src/util/User.ts +++ /dev/null
@@ -1,16 +0,0 @@ -import { toObject, UserModel, PublicUserProjection } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; - -export { PublicUserProjection }; - -export async function getPublicUser(user_id: string, additional_fields?: any) { - const user = await UserModel.findOne( - { id: user_id }, - { - ...PublicUserProjection, - ...additional_fields - } - ).exec(); - if (!user) throw new HTTPError("User not found", 404); - return toObject(user); -} diff --git a/api/src/util/Voice.ts b/api/src/util/Voice.ts new file mode 100644
index 00000000..087bdfa8 --- /dev/null +++ b/api/src/util/Voice.ts
@@ -0,0 +1,32 @@ +import {Config} from "@fosscord/util"; +import {distanceBetweenLocations, IPAnalysis} from "./ipAddress"; + +export async function getVoiceRegions(ipAddress: string, vip: boolean) { + const regions = Config.get().regions; + const availableRegions = regions.available.filter(ar => vip ? true : !ar.vip); + let optimalId = regions.default + + if(!regions.useDefaultAsOptimal) { + const clientIpAnalysis = await IPAnalysis(ipAddress) + + let min = Number.POSITIVE_INFINITY + + for (let ar of availableRegions) { + //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call + const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))) + + if(dist < min) { + min = dist + optimalId = ar.id + } + } + } + + return availableRegions.map(ar => ({ + id: ar.id, + name: ar.name, + custom: ar.custom, + deprecated: ar.deprecated, + optimal: ar.id === optimalId + })) +} \ No newline at end of file diff --git a/api/src/util/ipAddress.ts b/api/src/util/ipAddress.ts
index 0a724daa..c6239426 100644 --- a/api/src/util/ipAddress.ts +++ b/api/src/util/ipAddress.ts
@@ -60,6 +60,7 @@ const exampleData = { status: 200 }; +//TODO add function that support both ip and domain names export async function IPAnalysis(ip: string): Promise<typeof exampleData> { const { ipdataApiKey } = Config.get().security; if (!ipdataApiKey) return { ...exampleData, ip }; @@ -79,3 +80,19 @@ export function getIpAdress(req: Request): string { // @ts-ignore return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress; } + + +export function distanceBetweenLocations(loc1: any, loc2: any): number { + return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude); +} + +//Haversine function +function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) { + const p = 0.017453292519943295; // Math.PI / 180 + const c = Math.cos; + const a = 0.5 - c((lat2 - lat1) * p) / 2 + + c(lat1 * p) * c(lat2 * p) * + (1 - c((lon2 - lon1) * p)) / 2; + + return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km +} \ No newline at end of file