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