mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-13 23:03:23 +00:00
✨ api
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user