This commit is contained in:
Flam3rboy
2021-08-12 20:09:35 +02:00
parent a1c85f0b16
commit 08e837bf55
233 changed files with 0 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+";
// binary to string lookup table
const b2s = alphabet.split("");
// string to binary lookup table
// 123 == 'z'.charCodeAt(0) + 1
const s2b = new Array(123);
for (let i = 0; i < alphabet.length; i++) {
s2b[alphabet.charCodeAt(i)] = i;
}
// number to base64
export const ntob = (n: number): string => {
if (n < 0) return `-${ntob(-n)}`;
let lo = n >>> 0;
let hi = (n / 4294967296) >>> 0;
let right = "";
while (hi > 0) {
right = b2s[0x3f & lo] + right;
lo >>>= 6;
lo |= (0x3f & hi) << 26;
hi >>>= 6;
}
let left = "";
do {
left = b2s[0x3f & lo] + left;
lo >>>= 6;
} while (lo > 0);
return left + right;
};
// base64 to number
export const bton = (base64: string) => {
let number = 0;
const sign = base64.charAt(0) === "-" ? 1 : 0;
for (let i = sign; i < base64.length; i++) {
number = number * 64 + s2b[base64.charCodeAt(i)];
}
return sign ? -number : number;
};
+56
View File
@@ -0,0 +1,56 @@
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") {
// 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) {
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,
id: Snowflake.generate(),
created_at: new Date(),
// @ts-ignore
recipient_ids: null
}).save();
await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id: channel.guild_id } as ChannelCreateEvent);
return channel;
}
+372
View File
@@ -0,0 +1,372 @@
// @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;
timespan: number;
}
export interface DefaultOptions {
gateway: string;
general: {
instance_id: string;
};
permissions: {
user: {
createGuilds: boolean;
};
};
limits: {
user: {
maxGuilds: number;
maxUsername: number;
maxFriends: number;
};
guild: {
maxRoles: number;
maxMembers: number;
maxChannels: number;
maxChannelsInCategory: number;
hideOfflineMember: number;
};
message: {
characters: number;
ttsCharacters: number;
maxReactions: number;
maxAttachmentSize: number;
maxBulkDelete: number;
};
channel: {
maxPins: number;
maxTopic: number;
};
rate: {
ip: {
enabled: boolean;
count: number;
timespan: number;
};
routes: {
auth?: {
login?: RateLimitOptions;
register?: RateLimitOptions;
};
channel?: string;
// TODO: rate limit configuration for all routes
};
};
};
security: {
jwtSecret: string;
forwadedFor: string | null;
captcha: {
enabled: boolean;
service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
sitekey: string | null;
secret: string | null;
};
};
login: {
requireCaptcha: boolean;
};
register: {
email: {
necessary: boolean;
allowlist: boolean;
blocklist: boolean;
domains: string[];
};
dateOfBirth: {
necessary: boolean;
minimum: number; // in years
};
requireCaptcha: boolean;
requireInvite: boolean;
allowNewRegistration: boolean;
allowMultipleAccounts: boolean;
password: {
minLength: number;
minNumbers: number;
minUpperCase: number;
minSymbols: number;
blockInsecureCommonPasswords: boolean; // TODO: efficiently save password blocklist in database
};
};
}
const schema: JSONSchemaType<DefaultOptions> & {
definitions: {
rateLimitOptions: JSONSchemaType<RateLimitOptions>;
};
} = {
type: "object",
definitions: {
rateLimitOptions: {
type: "object",
properties: {
count: { type: "number" },
timespan: { type: "number" }
},
required: ["count", "timespan"]
}
},
properties: {
gateway: {
type: "string"
},
general: {
type: "object",
properties: {
instance_id: {
type: "string"
}
},
required: ["instance_id"],
additionalProperties: false
},
permissions: {
type: "object",
properties: {
user: {
type: "object",
properties: {
createGuilds: {
type: "boolean"
}
},
required: ["createGuilds"],
additionalProperties: false
}
},
required: ["user"],
additionalProperties: false
},
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
},
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
},
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 });
+593
View File
@@ -0,0 +1,593 @@
export const WSCodes = {
1000: "WS_CLOSE_REQUESTED",
4004: "TOKEN_INVALID",
4010: "SHARDING_INVALID",
4011: "SHARDING_REQUIRED",
4013: "INVALID_INTENTS",
4014: "DISALLOWED_INTENTS",
};
const AllowedImageFormats = ["webp", "png", "jpg", "jpeg", "gif"];
const AllowedImageSizes = Array.from({ length: 9 }, (e, i) => 2 ** (i + 4));
function makeImageUrl(root: string, { format = "webp", size = 512 } = {}) {
if (format && !AllowedImageFormats.includes(format)) throw new Error("IMAGE_FORMAT: " + format);
if (size && !AllowedImageSizes.includes(size)) throw new RangeError("IMAGE_SIZE: " + size);
return `${root}.${format}${size ? `?size=${size}` : ""}`;
}
/**
* Options for Image URLs.
* @typedef {Object} ImageURLOptions
* @property {string} [format] One of `webp`, `png`, `jpg`, `jpeg`, `gif`. If no format is provided,
* defaults to `webp`.
* @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for
* animated avatars; the default is false.
* @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048`, `4096`
*/
export const Endpoints = {
CDN(root: string) {
return {
Emoji: (emojiID: string, format = "png") => `${root}/emojis/${emojiID}.${format}`,
Asset: (name: string) => `${root}/assets/${name}`,
DefaultAvatar: (discriminator: string) => `${root}/embed/avatars/${discriminator}.png`,
Avatar: (user_id: string, hash: string, format = "webp", size: number, dynamic = false) => {
if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
return makeImageUrl(`${root}/avatars/${user_id}/${hash}`, { format, size });
},
Banner: (guildID: string, hash: string, format = "webp", size: number) =>
makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }),
Icon: (guildID: string, hash: string, format = "webp", size: number, dynamic = false) => {
if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size });
},
AppIcon: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }),
AppAsset: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
makeImageUrl(`${root}/app-assets/${clientID}/${hash}`, { size, format }),
GDMIcon: (channelID: string, hash: string, format = "webp", size: number) =>
makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }),
Splash: (guildID: string, hash: string, format = "webp", size: number) =>
makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }),
DiscoverySplash: (guildID: string, hash: string, format = "webp", size: number) =>
makeImageUrl(`${root}/discovery-splashes/${guildID}/${hash}`, { size, format }),
TeamIcon: (teamID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }),
};
},
invite: (root: string, code: string) => `${root}/${code}`,
botGateway: "/gateway/bot",
};
/**
* The current status of the client. Here are the available statuses:
* * READY: 0
* * CONNECTING: 1
* * RECONNECTING: 2
* * IDLE: 3
* * NEARLY: 4
* * DISCONNECTED: 5
* * WAITING_FOR_GUILDS: 6
* * IDENTIFYING: 7
* * RESUMING: 8
* @typedef {number} Status
*/
export const Status = {
READY: 0,
CONNECTING: 1,
RECONNECTING: 2,
IDLE: 3,
NEARLY: 4,
DISCONNECTED: 5,
WAITING_FOR_GUILDS: 6,
IDENTIFYING: 7,
RESUMING: 8,
};
/**
* The current status of a voice connection. Here are the available statuses:
* * CONNECTED: 0
* * CONNECTING: 1
* * AUTHENTICATING: 2
* * RECONNECTING: 3
* * DISCONNECTED: 4
* @typedef {number} VoiceStatus
*/
export const VoiceStatus = {
CONNECTED: 0,
CONNECTING: 1,
AUTHENTICATING: 2,
RECONNECTING: 3,
DISCONNECTED: 4,
};
export const OPCodes = {
DISPATCH: 0,
HEARTBEAT: 1,
IDENTIFY: 2,
STATUS_UPDATE: 3,
VOICE_STATE_UPDATE: 4,
VOICE_GUILD_PING: 5,
RESUME: 6,
RECONNECT: 7,
REQUEST_GUILD_MEMBERS: 8,
INVALID_SESSION: 9,
HELLO: 10,
HEARTBEAT_ACK: 11,
};
export const VoiceOPCodes = {
IDENTIFY: 0,
SELECT_PROTOCOL: 1,
READY: 2,
HEARTBEAT: 3,
SESSION_DESCRIPTION: 4,
SPEAKING: 5,
HELLO: 8,
CLIENT_CONNECT: 12,
CLIENT_DISCONNECT: 13,
};
export const Events = {
RATE_LIMIT: "rateLimit",
CLIENT_READY: "ready",
GUILD_CREATE: "guildCreate",
GUILD_DELETE: "guildDelete",
GUILD_UPDATE: "guildUpdate",
GUILD_UNAVAILABLE: "guildUnavailable",
GUILD_AVAILABLE: "guildAvailable",
GUILD_MEMBER_ADD: "guildMemberAdd",
GUILD_MEMBER_REMOVE: "guildMemberRemove",
GUILD_MEMBER_UPDATE: "guildMemberUpdate",
GUILD_MEMBER_AVAILABLE: "guildMemberAvailable",
GUILD_MEMBER_SPEAKING: "guildMemberSpeaking",
GUILD_MEMBERS_CHUNK: "guildMembersChunk",
GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate",
GUILD_ROLE_CREATE: "roleCreate",
GUILD_ROLE_DELETE: "roleDelete",
INVITE_CREATE: "inviteCreate",
INVITE_DELETE: "inviteDelete",
GUILD_ROLE_UPDATE: "roleUpdate",
GUILD_EMOJI_CREATE: "emojiCreate",
GUILD_EMOJI_DELETE: "emojiDelete",
GUILD_EMOJI_UPDATE: "emojiUpdate",
GUILD_BAN_ADD: "guildBanAdd",
GUILD_BAN_REMOVE: "guildBanRemove",
CHANNEL_CREATE: "channelCreate",
CHANNEL_DELETE: "channelDelete",
CHANNEL_UPDATE: "channelUpdate",
CHANNEL_PINS_UPDATE: "channelPinsUpdate",
MESSAGE_CREATE: "message",
MESSAGE_DELETE: "messageDelete",
MESSAGE_UPDATE: "messageUpdate",
MESSAGE_BULK_DELETE: "messageDeleteBulk",
MESSAGE_REACTION_ADD: "messageReactionAdd",
MESSAGE_REACTION_REMOVE: "messageReactionRemove",
MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll",
MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji",
USER_UPDATE: "userUpdate",
PRESENCE_UPDATE: "presenceUpdate",
VOICE_SERVER_UPDATE: "voiceServerUpdate",
VOICE_STATE_UPDATE: "voiceStateUpdate",
VOICE_BROADCAST_SUBSCRIBE: "subscribe",
VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe",
TYPING_START: "typingStart",
TYPING_STOP: "typingStop",
WEBHOOKS_UPDATE: "webhookUpdate",
ERROR: "error",
WARN: "warn",
DEBUG: "debug",
SHARD_DISCONNECT: "shardDisconnect",
SHARD_ERROR: "shardError",
SHARD_RECONNECTING: "shardReconnecting",
SHARD_READY: "shardReady",
SHARD_RESUME: "shardResume",
INVALIDATED: "invalidated",
RAW: "raw",
};
export const ShardEvents = {
CLOSE: "close",
DESTROYED: "destroyed",
INVALID_SESSION: "invalidSession",
READY: "ready",
RESUMED: "resumed",
ALL_READY: "allReady",
};
/**
* The type of Structure allowed to be a partial:
* * USER
* * CHANNEL (only affects DMChannels)
* * GUILD_MEMBER
* * MESSAGE
* * REACTION
* <warn>Partials require you to put checks in place when handling data, read the Partials topic listed in the
* sidebar for more information.</warn>
* @typedef {string} PartialType
*/
export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]);
/**
* The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
* * READY
* * RESUMED
* * GUILD_CREATE
* * GUILD_DELETE
* * GUILD_UPDATE
* * INVITE_CREATE
* * INVITE_DELETE
* * GUILD_MEMBER_ADD
* * GUILD_MEMBER_REMOVE
* * GUILD_MEMBER_UPDATE
* * GUILD_MEMBERS_CHUNK
* * GUILD_INTEGRATIONS_UPDATE
* * GUILD_ROLE_CREATE
* * GUILD_ROLE_DELETE
* * GUILD_ROLE_UPDATE
* * GUILD_BAN_ADD
* * GUILD_BAN_REMOVE
* * GUILD_EMOJIS_UPDATE
* * CHANNEL_CREATE
* * CHANNEL_DELETE
* * CHANNEL_UPDATE
* * CHANNEL_PINS_UPDATE
* * MESSAGE_CREATE
* * MESSAGE_DELETE
* * MESSAGE_UPDATE
* * MESSAGE_DELETE_BULK
* * MESSAGE_REACTION_ADD
* * MESSAGE_REACTION_REMOVE
* * MESSAGE_REACTION_REMOVE_ALL
* * MESSAGE_REACTION_REMOVE_EMOJI
* * USER_UPDATE
* * PRESENCE_UPDATE
* * TYPING_START
* * VOICE_STATE_UPDATE
* * VOICE_SERVER_UPDATE
* * WEBHOOKS_UPDATE
* @typedef {string} WSEventType
*/
export const WSEvents = keyMirror([
"READY",
"RESUMED",
"GUILD_CREATE",
"GUILD_DELETE",
"GUILD_UPDATE",
"INVITE_CREATE",
"INVITE_DELETE",
"GUILD_MEMBER_ADD",
"GUILD_MEMBER_REMOVE",
"GUILD_MEMBER_UPDATE",
"GUILD_MEMBERS_CHUNK",
"GUILD_INTEGRATIONS_UPDATE",
"GUILD_ROLE_CREATE",
"GUILD_ROLE_DELETE",
"GUILD_ROLE_UPDATE",
"GUILD_BAN_ADD",
"GUILD_BAN_REMOVE",
"GUILD_EMOJIS_UPDATE",
"CHANNEL_CREATE",
"CHANNEL_DELETE",
"CHANNEL_UPDATE",
"CHANNEL_PINS_UPDATE",
"MESSAGE_CREATE",
"MESSAGE_DELETE",
"MESSAGE_UPDATE",
"MESSAGE_DELETE_BULK",
"MESSAGE_REACTION_ADD",
"MESSAGE_REACTION_REMOVE",
"MESSAGE_REACTION_REMOVE_ALL",
"MESSAGE_REACTION_REMOVE_EMOJI",
"USER_UPDATE",
"PRESENCE_UPDATE",
"TYPING_START",
"VOICE_STATE_UPDATE",
"VOICE_SERVER_UPDATE",
"WEBHOOKS_UPDATE",
]);
/**
* The type of a message, e.g. `DEFAULT`. Here are the available types:
* * DEFAULT
* * RECIPIENT_ADD
* * RECIPIENT_REMOVE
* * CALL
* * CHANNEL_NAME_CHANGE
* * CHANNEL_ICON_CHANGE
* * PINS_ADD
* * GUILD_MEMBER_JOIN
* * USER_PREMIUM_GUILD_SUBSCRIPTION
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3
* * CHANNEL_FOLLOW_ADD
* * GUILD_DISCOVERY_DISQUALIFIED
* * GUILD_DISCOVERY_REQUALIFIED
* * REPLY
* @typedef {string} MessageType
*/
export const MessageTypes = [
"DEFAULT",
"RECIPIENT_ADD",
"RECIPIENT_REMOVE",
"CALL",
"CHANNEL_NAME_CHANGE",
"CHANNEL_ICON_CHANGE",
"PINS_ADD",
"GUILD_MEMBER_JOIN",
"USER_PREMIUM_GUILD_SUBSCRIPTION",
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1",
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2",
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3",
"CHANNEL_FOLLOW_ADD",
null,
"GUILD_DISCOVERY_DISQUALIFIED",
"GUILD_DISCOVERY_REQUALIFIED",
null,
null,
null,
"REPLY",
];
/**
* The types of messages that are `System`. The available types are `MessageTypes` excluding:
* * DEFAULT
* * REPLY
* @typedef {string} SystemMessageType
*/
export const SystemMessageTypes = MessageTypes.filter((type: string | null) => type && type !== "DEFAULT" && type !== "REPLY");
/**
* <info>Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users</info>
* The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types:
* * PLAYING
* * STREAMING
* * LISTENING
* * WATCHING
* * CUSTOM_STATUS
* * COMPETING
* @typedef {string} ActivityType
*/
export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"];
export const ChannelTypes = {
TEXT: 0,
DM: 1,
VOICE: 2,
GROUP: 3,
CATEGORY: 4,
NEWS: 5,
STORE: 6,
};
export const ClientApplicationAssetTypes = {
SMALL: 1,
BIG: 2,
};
export const Colors = {
DEFAULT: 0x000000,
WHITE: 0xffffff,
AQUA: 0x1abc9c,
GREEN: 0x2ecc71,
BLUE: 0x3498db,
YELLOW: 0xffff00,
PURPLE: 0x9b59b6,
LUMINOUS_VIVID_PINK: 0xe91e63,
GOLD: 0xf1c40f,
ORANGE: 0xe67e22,
RED: 0xe74c3c,
GREY: 0x95a5a6,
NAVY: 0x34495e,
DARK_AQUA: 0x11806a,
DARK_GREEN: 0x1f8b4c,
DARK_BLUE: 0x206694,
DARK_PURPLE: 0x71368a,
DARK_VIVID_PINK: 0xad1457,
DARK_GOLD: 0xc27c0e,
DARK_ORANGE: 0xa84300,
DARK_RED: 0x992d22,
DARK_GREY: 0x979c9f,
DARKER_GREY: 0x7f8c8d,
LIGHT_GREY: 0xbcc0c0,
DARK_NAVY: 0x2c3e50,
BLURPLE: 0x7289da,
GREYPLE: 0x99aab5,
DARK_BUT_NOT_BLACK: 0x2c2f33,
NOT_QUITE_BLACK: 0x23272a,
};
/**
* The value set for the explicit content filter levels for a guild:
* * DISABLED
* * MEMBERS_WITHOUT_ROLES
* * ALL_MEMBERS
* @typedef {string} ExplicitContentFilterLevel
*/
export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"];
/**
* The value set for the verification levels for a guild:
* * NONE
* * LOW
* * MEDIUM
* * HIGH
* * VERY_HIGH
* @typedef {string} VerificationLevel
*/
export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
/**
* An error encountered while performing an API request. Here are the potential errors:
* * UNKNOWN_ACCOUNT
* * UNKNOWN_APPLICATION
* * UNKNOWN_CHANNEL
* * UNKNOWN_GUILD
* * UNKNOWN_INTEGRATION
* * UNKNOWN_INVITE
* * UNKNOWN_MEMBER
* * UNKNOWN_MESSAGE
* * UNKNOWN_OVERWRITE
* * UNKNOWN_PROVIDER
* * UNKNOWN_ROLE
* * UNKNOWN_TOKEN
* * UNKNOWN_USER
* * UNKNOWN_EMOJI
* * UNKNOWN_WEBHOOK
* * UNKNOWN_BAN
* * UNKNOWN_GUILD_TEMPLATE
* * BOT_PROHIBITED_ENDPOINT
* * BOT_ONLY_ENDPOINT
* * CHANNEL_HIT_WRITE_RATELIMIT
* * MAXIMUM_GUILDS
* * MAXIMUM_FRIENDS
* * MAXIMUM_PINS
* * MAXIMUM_ROLES
* * MAXIMUM_WEBHOOKS
* * MAXIMUM_REACTIONS
* * MAXIMUM_CHANNELS
* * MAXIMUM_ATTACHMENTS
* * MAXIMUM_INVITES
* * GUILD_ALREADY_HAS_TEMPLATE
* * UNAUTHORIZED
* * ACCOUNT_VERIFICATION_REQUIRED
* * REQUEST_ENTITY_TOO_LARGE
* * FEATURE_TEMPORARILY_DISABLED
* * USER_BANNED
* * ALREADY_CROSSPOSTED
* * MISSING_ACCESS
* * INVALID_ACCOUNT_TYPE
* * CANNOT_EXECUTE_ON_DM
* * EMBED_DISABLED
* * CANNOT_EDIT_MESSAGE_BY_OTHER
* * CANNOT_SEND_EMPTY_MESSAGE
* * CANNOT_MESSAGE_USER
* * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL
* * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH
* * OAUTH2_APPLICATION_BOT_ABSENT
* * MAXIMUM_OAUTH2_APPLICATIONS
* * INVALID_OAUTH_STATE
* * MISSING_PERMISSIONS
* * INVALID_AUTHENTICATION_TOKEN
* * NOTE_TOO_LONG
* * INVALID_BULK_DELETE_QUANTITY
* * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL
* * INVALID_OR_TAKEN_INVITE_CODE
* * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE
* * INVALID_OAUTH_TOKEN
* * BULK_DELETE_MESSAGE_TOO_OLD
* * INVALID_FORM_BODY
* * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT
* * INVALID_API_VERSION
* * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL
* * REACTION_BLOCKED
* * RESOURCE_OVERLOADED
* @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,
};
/**
* The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types:
* * ALL
* * MENTIONS
* @typedef {string} DefaultMessageNotifications
*/
export const DefaultMessageNotifications = ["ALL", "MENTIONS"];
/**
* The value set for a team members's membership state:
* * INVITED
* * ACCEPTED
* @typedef {string} MembershipStates
*/
export const MembershipStates = [
// They start at 1
null,
"INVITED",
"ACCEPTED",
];
/**
* The value set for a webhook's type:
* * Incoming
* * Channel Follower
* @typedef {string} WebhookTypes
*/
export const WebhookTypes = [
// They start at 1
null,
"Incoming",
"Channel Follower",
];
function keyMirror(arr: string[]) {
let tmp = Object.create(null);
for (const value of arr) tmp[value] = value;
return tmp;
}
+26
View File
@@ -0,0 +1,26 @@
import { Config, Event, EventModel, RabbitMQ } from "@fosscord/server-util";
export async function emitEvent(payload: Omit<Event, "created_at">) {
if (RabbitMQ.connection) {
const id = (payload.channel_id || payload.user_id || payload.guild_id) as string;
if (!id) console.error("event doesn't contain any id", payload);
const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission
await RabbitMQ.channel?.assertExchange(id, "fanout", { durable: false });
// assertQueue isn't needed, because a queue will automatically created if it doesn't exist
const successful = RabbitMQ.channel?.publish(id, "", Buffer.from(`${data}`), { type: payload.event });
if (!successful) throw new Error("failed to send event");
} else {
// use mongodb for event transmission
// TODO: use event emitter for local server bundle
const obj = {
created_at: new Date(), // in seconds
...payload
};
// TODO: bigint isn't working
return await new EventModel(obj).save();
}
}
export async function emitAuditLog(payload: any) {}
+218
View File
@@ -0,0 +1,218 @@
import {
Guild,
GuildCreateEvent,
GuildDeleteEvent,
GuildMemberAddEvent,
GuildMemberRemoveEvent,
GuildMemberUpdateEvent,
GuildModel,
MemberModel,
RoleModel,
toObject,
UserModel,
GuildDocument,
Config
} from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { emitEvent } from "./Event";
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 } }
).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 } }
).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 }
).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);
}
+136
View File
@@ -0,0 +1,136 @@
import { ChannelModel, Embed, Message, MessageCreateEvent, MessageUpdateEvent } 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 { HTTPError } from "lambert-server";
import fetch from "node-fetch";
import cheerio from "cheerio";
import { emitEvent } from "./Event";
import { MessageType } from "@fosscord/server-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;
const DEFAULT_FETCH_OPTIONS: any = {
redirect: "follow",
follow: 1,
headers: {
"user-agent": "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"
},
size: 1024 * 1024 * 1,
compress: true,
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();
if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404);
// TODO: are tts messages allowed in dm channels? should permission be checked?
// @ts-ignore
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.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;
}
if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.stickers?.length && !opts.activity) {
throw new HTTPError("Empty messages are not allowed", 50006);
}
// 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: opts.attachments || [], // TODO: message attachments
embeds: opts.embeds || [],
reactions: opts.reactions || [],
type: opts.type ?? 0
};
}
// TODO: cache link result in db
export async function postHandleMessage(message: Message) {
var links = message.content?.match(LINK_REGEX);
if (!links) return;
const data = { ...message };
data.embeds = data.embeds.filter((x) => x.type !== "link");
links = links.slice(0, 5); // embed max 5 links
for (const link of links) {
try {
const request = await fetch(link, DEFAULT_FETCH_OPTIONS);
const text = await request.text();
const $ = cheerio.load(text);
const title = $('meta[property="og:title"]').attr("content");
const provider_name = $('meta[property="og:site_name"]').text();
const author_name = $('meta[property="article:author"]').attr("content");
const description = $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content");
const image = $('meta[property="og:image"]').attr("content");
const url = $('meta[property="og:url"]').attr("content");
// TODO: color
const embed: Embed = {
provider: {
url: link,
name: provider_name
}
};
if (author_name) embed.author = { name: author_name };
if (image) embed.thumbnail = { proxy_url: image, url: image };
if (title) embed.title = title;
if (url) embed.url = url;
if (description) embed.description = description;
if (title || description) {
data.embeds.push(embed);
}
} catch (error) {}
}
await Promise.all([
emitEvent({
event: "MESSAGE_UPDATE",
guild_id: message.guild_id,
channel_id: message.channel_id,
data
} as MessageUpdateEvent),
MessageModel.updateOne({ id: message.id, channel_id: message.channel_id }, data).exec()
]);
}
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 }).populate("referenced_message").save()
);
await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data, guild_id: message.guild_id } as MessageCreateEvent);
postHandleMessage(data).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
return data;
}
+12
View File
@@ -0,0 +1,12 @@
export function random(length = 6) {
// Declare all characters
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// Pick characers randomly
let str = "";
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
}
+18
View File
@@ -0,0 +1,18 @@
import { Request } from "express";
import { ntob } from "./Base64";
import { FieldErrors } from "./instanceOf";
export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
if (str.length < min || str.length > max) {
throw FieldErrors({
[key]: {
code: "BASE_TYPE_BAD_LENGTH",
message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` }),
},
});
}
}
export function generateCode() {
return ntob(Date.now() + Math.randomIntBetween(0, 10000));
}
+16
View File
@@ -0,0 +1,16 @@
import { toObject, UserModel, PublicUserProjection } from "@fosscord/server-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);
}
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
import { Config } from "@fosscord/server-util";
import FormData from "form-data";
import { HTTPError } from "lambert-server";
import fetch from "node-fetch";
export async function uploadFile(path: string, file: Express.Multer.File) {
const form = new FormData();
form.append("file", file.buffer, {
contentType: file.mimetype,
filename: file.originalname
});
const response = await fetch(`${Config.get().cdn.endpoint || "http://localhost:3003"}${path}`, {
headers: {
signature: Config.get().security.requestSignature,
...form.getHeaders()
},
method: "POST",
body: form
});
const result = await response.json();
if (response.status !== 200) throw result;
return result;
}
export async function handleFile(path: string, body?: string): Promise<string | undefined> {
if (!body || !body.startsWith("data:")) return body;
try {
const mimetype = body.split(":")[1].split(";")[0];
const buffer = Buffer.from(body.split(",")[1], "base64");
// @ts-ignore
const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
return id;
} catch (error) {
console.error(error);
throw new HTTPError("Invalid " + path);
}
}
+214
View File
@@ -0,0 +1,214 @@
// different version of lambert-server instanceOf with discord error format
import { NextFunction, Request, Response } from "express";
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 function check(schema: any) {
return (req: Request, res: Response, next: NextFunction) => {
try {
const result = instanceOf(schema, req.body, { path: "body", req, ref: { obj: null, key: "" } });
if (result === true) return next();
throw result;
} catch (error) {
return res.status(400).json({ code: 50035, message: "Invalid Form Body", success: false, errors: error });
}
};
}
export function FieldErrors(fields: Record<string, { code?: string; message: string }>) {
return new FieldError(
50035,
"Invalid Form Body",
fields.map(({ message, code }) => ({
_errors: [
{
message,
code: code || "BASE_TYPE_INVALID"
}
]
}))
);
}
// TODO: implement Image data type: Data URI scheme that supports JPG, GIF, and PNG formats. An example Data URI format is: data:image/jpeg;base64,BASE64_ENCODED_JPEG_IMAGE_DATA
// Ensure you use the proper content type (image/jpeg, image/png, image/gif) that matches the image data being provided.
export class FieldError extends Error {
constructor(public code: string | number, public message: string, public errors?: any) {
super(message);
}
}
export class Email {
constructor(public email: string) {}
check() {
return !!this.email.match(EMAIL_REGEX);
}
}
export class Length {
constructor(public type: any, public min: number, public max: number) {}
check(value: string) {
if (typeof value === "string" || Array.isArray(value)) return value.length >= this.min && value.length <= this.max;
if (typeof value === "number" || typeof value === "bigint") return value >= this.min && value <= this.max;
return false;
}
}
export function instanceOf(
type: any,
value: any,
{
path = "",
optional = false,
errors = {},
req,
ref
}: { path?: string; optional?: boolean; errors?: any; req: Request; ref?: { key: string | number; obj: any } }
): Boolean {
if (!ref) ref = { obj: null, key: "" };
if (!path) path = "body";
if (!type) return true; // no type was specified
try {
if (value == null) {
if (optional) return true;
throw new FieldError("BASE_TYPE_REQUIRED", req.t("common:field.BASE_TYPE_REQUIRED"));
}
switch (type) {
case String:
value = `${value}`;
ref.obj[ref.key] = value;
if (typeof value === "string") return true;
throw new FieldError("BASE_TYPE_STRING", req.t("common:field.BASE_TYPE_STRING"));
case Number:
value = Number(value);
ref.obj[ref.key] = value;
if (typeof value === "number" && !isNaN(value)) return true;
throw new FieldError("BASE_TYPE_NUMBER", req.t("common:field.BASE_TYPE_NUMBER"));
case BigInt:
try {
value = BigInt(value);
ref.obj[ref.key] = value;
if (typeof value === "bigint") return true;
} catch (error) {}
throw new FieldError("BASE_TYPE_BIGINT", req.t("common:field.BASE_TYPE_BIGINT"));
case Boolean:
if (value == "true") value = true;
if (value == "false") value = false;
ref.obj[ref.key] = value;
if (typeof value === "boolean") return true;
throw new FieldError("BASE_TYPE_BOOLEAN", req.t("common:field.BASE_TYPE_BOOLEAN"));
case Email:
if (new Email(value).check()) return true;
throw new FieldError("EMAIL_TYPE_INVALID_EMAIL", req.t("common:field.EMAIL_TYPE_INVALID_EMAIL"));
case Date:
value = new Date(value);
ref.obj[ref.key] = value;
// value.getTime() can be < 0, if it is before 1970
if (!isNaN(value)) return true;
throw new FieldError("DATE_TYPE_PARSE", req.t("common:field.DATE_TYPE_PARSE"));
}
if (typeof type === "object") {
if (Array.isArray(type)) {
if (!Array.isArray(value)) throw new FieldError("BASE_TYPE_ARRAY", req.t("common:field.BASE_TYPE_ARRAY"));
if (!type.length) return true; // type array didn't specify any type
return (
value.every((val, i) => {
errors[i] = {};
if (
instanceOf(type[0], val, {
path: `${path}[${i}]`,
optional,
errors: errors[i],
req,
ref: { key: i, obj: value }
}) === true
) {
delete errors[i];
return true;
}
return false;
}) || errors
);
} else if (type?.constructor?.name != "Object") {
if (type instanceof Tuple) {
if ((<Tuple>type).types.some((x) => instanceOf(x, value, { path, optional, errors, req, ref }))) return true;
throw new FieldError("BASE_TYPE_CHOICES", req.t("common:field.BASE_TYPE_CHOICES", { types: type.types }));
} else if (type instanceof Length) {
let length = <Length>type;
if (instanceOf(length.type, value, { path, optional, req, ref, errors }) !== true) return errors;
let val = ref.obj[ref.key];
if ((<Length>type).check(val)) return true;
throw new FieldError(
"BASE_TYPE_BAD_LENGTH",
req.t("common:field.BASE_TYPE_BAD_LENGTH", {
length: `${type.min} - ${type.max}`
})
);
}
try {
if (value instanceof type) return true;
} catch (error) {
throw new FieldError("BASE_TYPE_CLASS", req.t("common:field.BASE_TYPE_CLASS", { type }));
}
}
if (typeof value !== "object") throw new FieldError("BASE_TYPE_OBJECT", req.t("common:field.BASE_TYPE_OBJECT"));
const diff = Object.keys(value).missing(
Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
);
if (diff.length) throw new FieldError("UNKOWN_FIELD", req.t("common:field.UNKOWN_FIELD", { key: diff }));
return (
Object.keys(type).every((key) => {
let newKey = key;
const OPTIONAL = key.startsWith(OPTIONAL_PREFIX);
if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
errors[newKey] = {};
if (
instanceOf(type[key], value[newKey], {
path: `${path}.${newKey}`,
optional: OPTIONAL,
errors: errors[newKey],
req,
ref: { key: newKey, obj: value }
}) === true
) {
delete errors[newKey];
return true;
}
return false;
}) || errors
);
} else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {
if (value === type) return true;
throw new FieldError("BASE_TYPE_CONSTANT", req.t("common:field.BASE_TYPE_CONSTANT", { value: type }));
} else if (typeof type === "bigint") {
if (BigInt(value) === type) return true;
throw new FieldError("BASE_TYPE_CONSTANT", req.t("common:field.BASE_TYPE_CONSTANT", { value: type }));
}
return type == value;
} catch (error) {
let e = error as FieldError;
errors._errors = [{ message: e.message, code: e.code }];
return errors;
}
}
+81
View File
@@ -0,0 +1,81 @@
import { Config } from "@fosscord/server-util";
import { Request } from "express";
// use ipdata package instead of simple fetch because of integrated caching
import fetch from "node-fetch";
const exampleData = {
ip: "",
is_eu: true,
city: "",
region: "",
region_code: "",
country_name: "",
country_code: "",
continent_name: "",
continent_code: "",
latitude: 0,
longitude: 0,
postal: "",
calling_code: "",
flag: "",
emoji_flag: "",
emoji_unicode: "",
asn: {
asn: "",
name: "",
domain: "",
route: "",
type: "isp"
},
languages: [
{
name: "",
native: ""
}
],
currency: {
name: "",
code: "",
symbol: "",
native: "",
plural: ""
},
time_zone: {
name: "",
abbr: "",
offset: "",
is_dst: true,
current_time: ""
},
threat: {
is_tor: false,
is_proxy: false,
is_anonymous: false,
is_known_attacker: false,
is_known_abuser: false,
is_threat: false,
is_bogon: false
},
count: 0,
status: 200
};
export async function IPAnalysis(ip: string): Promise<typeof exampleData> {
const { ipdataApiKey } = Config.get().security;
if (!ipdataApiKey) return { ...exampleData, ip };
return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json();
}
export function isProxy(data: typeof exampleData) {
if (!data || !data.asn || !data.threat) return false;
if (data.asn.type !== "isp") return true;
if (Object.values(data.threat).some((x) => x)) return true;
return false;
}
export function getIpAdress(req: Request): string {
// @ts-ignore
return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress;
}
+49
View File
@@ -0,0 +1,49 @@
import { Config } from "@fosscord/server-util";
import "missing-native-js-functions";
const reNUMBER = /[0-9]/g;
const reUPPERCASELETTER = /[A-Z]/g;
const reSYMBOLS = /[A-Z,a-z,0-9]/g;
const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored in db
/*
* https://en.wikipedia.org/wiki/Password_policy
* password must meet following criteria, to be perfect:
* - min <n> chars
* - min <n> numbers
* - min <n> symbols
* - min <n> uppercase chars
*
* Returns: 0 > pw > 1
*/
export function check(password: string): number {
const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
var strength = 0;
// checks for total password len
if (password.length >= minLength - 1) {
strength += 0.25;
}
// checks for amount of Numbers
if (password.count(reNUMBER) >= minNumbers - 1) {
strength += 0.25;
}
// checks for amount of Uppercase Letters
if (password.count(reUPPERCASELETTER) >= minUpperCase - 1) {
strength += 0.25;
}
// checks for amount of symbols
if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) {
strength += 0.25;
}
// checks if password only consists of numbers or only consists of chars
if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) {
strength = 0;
}
return strength;
}
+154
View File
@@ -0,0 +1,154 @@
aol.com
att.net
comcast.net
facebook.com
gmail.com
gmx.com
googlemail.com
google.com
hotmail.com
hotmail.co.uk
mac.com
me.com
mail.com
msn.com
live.com
sbcglobal.net
verizon.net
yahoo.com
yahoo.co.uk
email.com
fastmail.fm
games.com
gmx.net
hush.com
hushmail.com
icloud.com
iname.com
inbox.com
lavabit.com
love.com
outlook.com
pobox.com
protonmail.ch
protonmail.com
tutanota.de
tutanota.com
tutamail.com
tuta.io
keemail.me
rocketmail.com
safe-mail.net
wow.com
ygm.com
ymail.com
zoho.com
yandex.com
bellsouth.net
charter.net
cox.net
earthlink.net
juno.com
btinternet.com
virginmedia.com
blueyonder.co.uk
freeserve.co.uk
live.co.uk
ntlworld.com
o2.co.uk
orange.net
sky.com
talktalk.co.uk
tiscali.co.uk
virgin.net
wanadoo.co.uk
bt.com
sina.com
sina.cn
qq.com
naver.com
hanmail.net
daum.net
nate.com
yahoo.co.jp
yahoo.co.kr
yahoo.co.id
yahoo.co.in
yahoo.com.sg
yahoo.com.ph
163.com
yeah.net
126.com
21cn.com
aliyun.com
foxmail.com
hotmail.fr
live.fr
laposte.net
yahoo.fr
wanadoo.fr
orange.fr
gmx.fr
sfr.fr
neuf.fr
free.fr
gmx.de
hotmail.de
live.de
online.de
t-online.de
web.de
yahoo.de
libero.it
virgilio.it
hotmail.it
aol.it
tiscali.it
alice.it
live.it
yahoo.it
email.it
tin.it
poste.it
teletu.it
mail.ru
rambler.ru
yandex.ru
ya.ru
list.ru
hotmail.be
live.be
skynet.be
voo.be
tvcablenet.be
telenet.be
hotmail.com.ar
live.com.ar
yahoo.com.ar
fibertel.com.ar
speedy.com.ar
arnet.com.ar
yahoo.com.mx
live.com.mx
hotmail.es
hotmail.com.mx
prodigy.net.mx
yahoo.ca
hotmail.ca
bell.net
shaw.ca
sympatico.ca
rogers.com
yahoo.com.br
hotmail.com.br
outlook.com.br
uol.com.br
bol.com.br
terra.com.br
ig.com.br
itelefonica.com.br
r7.com
zipmail.com.br
globo.com
globomail.com
oi.com.br