Merge branch 'fosscord:master' into maddyrtc
This commit is contained in:
		
						commit
						82216f1513
					
				
							
								
								
									
										14
									
								
								api/LICENSE
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								api/LICENSE
									
									
									
									
									
								
							| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
							
								
								
									
										20626
									
								
								api/assets/schemas.json
									
									
									
									
									
								
							
							
						
						
									
										20626
									
								
								api/assets/schemas.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -24,20 +24,20 @@ | ||||
| 				ASSET_ENDPOINT: "", | ||||
| 				MEDIA_PROXY_ENDPOINT: "https://media.discordapp.net", | ||||
| 				WIDGET_ENDPOINT: `//${location.host}/widget`, | ||||
| 				INVITE_HOST: `${location.host}/invite`, | ||||
| 				GUILD_TEMPLATE_HOST: "discord.new", | ||||
| 				GIFT_CODE_HOST: "discord.gift", | ||||
| 				INVITE_HOST: `${location.hostname}/invite`, | ||||
| 				GUILD_TEMPLATE_HOST: "${location.host}", | ||||
| 				GIFT_CODE_HOST: "${location.hostname}", | ||||
| 				RELEASE_CHANNEL: "stable", | ||||
| 				MARKETING_ENDPOINT: "//discord.com", | ||||
| 				BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387", | ||||
| 				STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi", | ||||
| 				NETWORKING_ENDPOINT: "//router.discordapp.net", | ||||
| 				RTC_LATENCY_ENDPOINT: "//latency.discord.media/rtc", | ||||
| 				RTC_LATENCY_ENDPOINT: "//${location.hostname}/rtc", | ||||
| 				PROJECT_ENV: "production", | ||||
| 				REMOTE_AUTH_ENDPOINT: "//localhost:3020", | ||||
| 				SENTRY_TAGS: { buildId: "75e36d9", buildType: "normal" }, | ||||
| 				MIGRATION_SOURCE_ORIGIN: "https://discordapp.com", | ||||
| 				MIGRATION_DESTINATION_ORIGIN: "https://discord.com", | ||||
| 				MIGRATION_SOURCE_ORIGIN: "https://${location.hostname}", | ||||
| 				MIGRATION_DESTINATION_ORIGIN: "https://${location.hostname}", | ||||
| 				HTML_TIMESTAMP: Date.now(), | ||||
| 				ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0" | ||||
| 			}; | ||||
|  | ||||
| @ -1,3 +0,0 @@ | ||||
| files: | ||||
|     - source: /locales/en/*.json | ||||
|       translation: /locales/%two_letters_code%/%original_file_name% | ||||
| @ -1,16 +1,16 @@ | ||||
| { | ||||
| 	"login": { | ||||
| 		"INVALID_LOGIN": "E-Mail or Phone not found", | ||||
| 		"INVALID_PASSWORD": "Invalid Password", | ||||
| 		"ACCOUNT_DISABLED": "This account is disabled" | ||||
| 		"INVALID_LOGIN": "מייל או מספר טלפון לא נמצאים במאגר", | ||||
| 		"INVALID_PASSWORD": "סיסמא שגויה", | ||||
| 		"ACCOUNT_DISABLED": "משתמש זה חסום / מבוטל" | ||||
| 	}, | ||||
| 	"register": { | ||||
| 		"REGISTRATION_DISABLED": "New user registration is disabled", | ||||
| 		"INVITE_ONLY": "You must be invited to register", | ||||
| 		"EMAIL_INVALID": "Invalid Email", | ||||
| 		"EMAIL_ALREADY_REGISTERED": "Email is already registered", | ||||
| 		"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older", | ||||
| 		"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.", | ||||
| 		"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another" | ||||
| 		"REGISTRATION_DISABLED": "לא ניתן לאפשר רישום משתמשים חדשים", | ||||
| 		"INVITE_ONLY": "עליך להיות מוזמן בכדי להרשם", | ||||
| 		"EMAIL_INVALID": "מייל שגוי", | ||||
| 		"EMAIL_ALREADY_REGISTERED": "מייל זה כבר רשום", | ||||
| 		"DATE_OF_BIRTH_UNDERAGE": "{{years}} עלייך להיות מעל גיל", | ||||
| 		"CONSENT_REQUIRED": ".עליך להסכים לתנאי השירות ולמדיניות הפרטיות", | ||||
| 		"USERNAME_TOO_MANY_USERS": "ליותר מדי משתמשים יש שם משתמש זהה, אנא נסה אחר" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,18 @@ | ||||
| { | ||||
| 	"field": { | ||||
| 		"BASE_TYPE_REQUIRED": "This field is required", | ||||
| 		"BASE_TYPE_STRING": "This field must be a string", | ||||
| 		"BASE_TYPE_NUMBER": "This field must be a number", | ||||
| 		"BASE_TYPE_BIGINT": "This field must be a bigint", | ||||
| 		"BASE_TYPE_BOOLEAN": "This field must be a boolean", | ||||
| 		"BASE_TYPE_CHOICES": "This field must be one of ({{types}})", | ||||
| 		"BASE_TYPE_CLASS": "This field must be an instance of {{type}}", | ||||
| 		"BASE_TYPE_REQUIRED": "שדה זה חובה", | ||||
| 		"BASE_TYPE_STRING": "שדה זה חייב להיות כטקסט", | ||||
| 		"BASE_TYPE_NUMBER": "שדה זה חייב להיות מספר", | ||||
| 		"BASE_TYPE_BIGINT": "השדה הזה חייב להיות ביגינט", | ||||
| 		"BASE_TYPE_BOOLEAN": "השדה הזה חייב להיות בוליאני", | ||||
| 		"BASE_TYPE_CHOICES": "({{types}}) שדה זה חייב להיות אחד מ", | ||||
| 		"BASE_TYPE_CLASS": "{{type}} מסוג instance שדה זה חייב להיות", | ||||
| 		"BASE_TYPE_OBJECT": "שדה זה חייב להיות אובייקט", | ||||
| 		"BASE_TYPE_ARRAY": "שדה זה חייב להיות מערך", | ||||
| 		"UNKOWN_FIELD": "מפתח לא ידוע: {{key}}", | ||||
| 		"BASE_TYPE_CONSTANT": "שדה זה להיות {{value}}", | ||||
| 		"UNKOWN_FIELD": "{{key}} :מפתח לא ידוע", | ||||
| 		"BASE_TYPE_CONSTANT": "{{value}} שדה זה חייב להיות", | ||||
| 		"EMAIL_TYPE_INVALID_EMAIL": "כתובת דואר אלקטרוני לא חוקית", | ||||
| 		"DATE_TYPE_PARSE": "לא ניתן לנתח {{date}}. צריך להיות ISO8601", | ||||
| 		"BASE_TYPE_BAD_LENGTH": "האורך חייב להיות בין {{length}}" | ||||
| 		"DATE_TYPE_PARSE": "ISO8601 אמור להיות {{date}} לא ניתן לאתר", | ||||
| 		"BASE_TYPE_BAD_LENGTH": "{{length}} האורך חייב להיות בין" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -30,7 +30,7 @@ | ||||
| 		"discord-open-source" | ||||
| 	], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "GPLV3", | ||||
| 	"license": "AGPLV3", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
|  | ||||
| @ -31,7 +31,6 @@ const Excluded = [ | ||||
| ]; | ||||
| 
 | ||||
| function modify(obj) { | ||||
| 	delete obj.additionalProperties; | ||||
| 	for (var k in obj) { | ||||
| 		if (typeof obj[k] === "object" && obj[k] !== null) { | ||||
| 			modify(obj[k]); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Config, listenEvent } from "@fosscord/util"; | ||||
| import { Config, getRights, listenEvent, Rights } from "@fosscord/util"; | ||||
| import { NextFunction, Request, Response, Router } from "express"; | ||||
| import { getIpAdress } from "@fosscord/api"; | ||||
| import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; | ||||
| @ -9,6 +9,7 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; | ||||
| 
 | ||||
| /* | ||||
| ? bucket limit? Max actions/sec per bucket? | ||||
| (ANSWER: a small fosscord instance might not need a complex rate limiting system) | ||||
| 
 | ||||
| TODO: delay database requests to include multiple queries | ||||
| TODO: different for methods (GET/POST) | ||||
| @ -44,21 +45,25 @@ export default function rateLimit(opts: { | ||||
| 	onlyIp?: boolean; | ||||
| }): any { | ||||
| 	return async (req: Request, res: Response, next: NextFunction): Promise<any> => { | ||||
| 		// exempt user? if so, immediately short circuit
 | ||||
| 		const rights = await getRights(req.user_id); | ||||
| 		if (rights.has("BYPASS_RATE_LIMITS")) return; | ||||
| 		 | ||||
| 		const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); | ||||
| 		var executor_id = getIpAdress(req); | ||||
| 		if (!opts.onlyIp && req.user_id) executor_id = req.user_id; | ||||
| 		if (!opts.onlyIp && req.user_id) executor_id = req.user_id;		 | ||||
| 
 | ||||
| 		var max_hits = opts.count; | ||||
| 		if (opts.bot && req.user_bot) max_hits = opts.bot; | ||||
| 		if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; | ||||
| 		else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; | ||||
| 
 | ||||
| 		const offender = Cache.get(executor_id + bucket_id); | ||||
| 		let offender = Cache.get(executor_id + bucket_id); | ||||
| 
 | ||||
| 		if (offender) { | ||||
| 			const reset = offender.expires_at.getTime(); | ||||
| 			const resetAfterMs = reset - Date.now(); | ||||
| 			const resetAfterSec = resetAfterMs / 1000; | ||||
| 			let reset = offender.expires_at.getTime(); | ||||
| 			let resetAfterMs = reset - Date.now(); | ||||
| 			let resetAfterSec = Math.ceil(resetAfterMs / 1000); | ||||
| 
 | ||||
| 			if (resetAfterMs <= 0) { | ||||
| 				offender.hits = 0; | ||||
| @ -70,6 +75,11 @@ export default function rateLimit(opts: { | ||||
| 
 | ||||
| 			if (offender.blocked) { | ||||
| 				const global = bucket_id === "global"; | ||||
| 				// each block violation pushes the expiry one full window further
 | ||||
| 				reset += opts.window * 1000; | ||||
| 				offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); | ||||
| 				resetAfterMs = reset - Date.now(); | ||||
| 				resetAfterSec = Math.ceil(resetAfterMs / 1000); | ||||
| 
 | ||||
| 				console.log("blocked bucket: " + bucket_id, { resetAfterMs }); | ||||
| 				return ( | ||||
|  | ||||
| @ -128,7 +128,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re | ||||
| 		throw FieldErrors({ | ||||
| 			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||
| 		}); | ||||
| 	} else if (register.dateOfBirth.minimum) { | ||||
| 	} else if (register.dateOfBirth.required && register.dateOfBirth.minimum) { | ||||
| 		const minimum = new Date(); | ||||
| 		minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); | ||||
| 		body.date_of_birth = new Date(body.date_of_birth as Date); | ||||
|  | ||||
| @ -1,12 +1,38 @@ | ||||
| import { Channel, emitEvent, getPermission, getRights, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; | ||||
| import { | ||||
| 	Attachment, | ||||
| 	Channel, | ||||
| 	Embed, | ||||
| 	DiscordApiErrors, | ||||
| 	emitEvent, | ||||
| 	FosscordApiErrors, | ||||
| 	getPermission, | ||||
| 	getRights, | ||||
|  	Message, | ||||
| 	MessageCreateEvent, | ||||
| 	MessageDeleteEvent, | ||||
| 	MessageUpdateEvent, | ||||
| 	Snowflake, | ||||
| 	uploadFile  | ||||
| } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import multer from "multer"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { handleMessage, postHandleMessage } from "@fosscord/api"; | ||||
| import { MessageCreateSchema } from "../index"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| // TODO: message content/embed string length limit
 | ||||
| 
 | ||||
| const messageUpload = multer({ | ||||
| 	limits: { | ||||
| 		fileSize: 1024 * 1024 * 100, | ||||
| 		fields: 10, | ||||
| 		files: 1 | ||||
| 	}, | ||||
| 	storage: multer.memoryStorage() | ||||
| }); // max upload 50 mb
 | ||||
| 
 | ||||
| router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 	var body = req.body as MessageCreateSchema; | ||||
| @ -51,6 +77,83 @@ router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGE | ||||
| 	return res.json(message); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| // Backfill message with specific timestamp
 | ||||
| router.put( | ||||
| 	"/", | ||||
| 	messageUpload.single("file"), | ||||
| 	async (req, res, next) => { | ||||
| 		if (req.body.payload_json) { | ||||
| 			req.body = JSON.parse(req.body.payload_json); | ||||
| 		} | ||||
| 
 | ||||
| 		next(); | ||||
| 	}, | ||||
| 	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { channel_id, message_id } = req.params; | ||||
| 		var body = req.body as MessageCreateSchema; | ||||
| 		const attachments: Attachment[] = []; | ||||
| 		 | ||||
| 		const rights = await getRights(req.user_id); | ||||
| 		rights.hasThrow("SEND_MESSAGES"); | ||||
| 
 | ||||
| 		// regex to check if message contains anything other than numerals ( also no decimals )
 | ||||
| 		if (!message_id.match(/^\+?\d+$/)) { | ||||
| 			throw new HTTPError("Message IDs must be positive integers", 400); | ||||
| 		} | ||||
| 
 | ||||
| 		const snowflake = Snowflake.deconstruct(message_id) | ||||
| 		if (Date.now() < snowflake.timestamp) { | ||||
| 			// message is in the future
 | ||||
| 			throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE; | ||||
| 		} | ||||
| 
 | ||||
| 		const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); | ||||
| 		if (exists) { | ||||
| 			throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; | ||||
| 		} | ||||
| 
 | ||||
| 		if (req.file) { | ||||
| 			try { | ||||
| 				const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); | ||||
| 				attachments.push({ ...file, proxy_url: file.url }); | ||||
| 			} catch (error) { | ||||
| 				return res.status(400).json(error); | ||||
| 			} | ||||
| 		} | ||||
| 		const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); | ||||
| 
 | ||||
| 		const embeds = body.embeds || []; | ||||
| 		if (body.embed) embeds.push(body.embed); | ||||
| 		let message = await handleMessage({ | ||||
| 			...body, | ||||
| 			type: 0, | ||||
| 			pinned: false, | ||||
| 			author_id: req.user_id, | ||||
| 			id: message_id, | ||||
| 			embeds, | ||||
| 			channel_id, | ||||
| 			attachments, | ||||
| 			edited_timestamp: undefined, | ||||
| 			timestamp: new Date(snowflake.timestamp), | ||||
| 		}); | ||||
| 
 | ||||
| 		//Fix for the client bug
 | ||||
| 		delete message.member | ||||
| 		 | ||||
| 		await Promise.all([ | ||||
| 			message.save(), | ||||
| 			emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), | ||||
| 			channel.save() | ||||
| 		]); | ||||
| 
 | ||||
| 		postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
 | ||||
| 
 | ||||
| 		return res.json(message); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
| router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; | ||||
| import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { In } from "typeorm"; | ||||
| @ -12,22 +12,28 @@ export interface BulkDeleteSchema { | ||||
| 	messages: string[]; | ||||
| } | ||||
| 
 | ||||
| // TODO: should users be able to bulk delete messages or only bots?
 | ||||
| // TODO: should this request fail, if you provide messages older than 14 days/invalid ids?
 | ||||
| // should users be able to bulk delete messages or only bots? ANSWER: all users
 | ||||
| // should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO
 | ||||
| // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
 | ||||
| router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); | ||||
| 
 | ||||
| 	const rights = await getRights(req.user_id); | ||||
| 	rights.hasThrow("SELF_DELETE_MESSAGES"); | ||||
| 	 | ||||
| 	let superuser = rights.has("MANAGE_MESSAGES"); | ||||
| 	const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); | ||||
| 	permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 
 | ||||
| 		 | ||||
| 	const { maxBulkDelete } = Config.get().limits.message; | ||||
| 
 | ||||
| 	const { messages } = req.body as { messages: string[] }; | ||||
| 	if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete"); | ||||
| 	if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); | ||||
| 	if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete"); | ||||
| 	if (!superuser) { | ||||
| 		permission.hasThrow("MANAGE_MESSAGES"); | ||||
| 		if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); | ||||
| 	} | ||||
| 
 | ||||
| 	await Message.delete(messages.map((x) => ({ id: x }))); | ||||
| 
 | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
| 	getRights, | ||||
| 	Message, | ||||
| 	MessageCreateEvent, | ||||
| 	Snowflake, | ||||
| 	uploadFile, | ||||
| 	Member | ||||
| } from "@fosscord/util"; | ||||
| @ -30,6 +31,8 @@ export function isTextChannel(type: ChannelType): boolean { | ||||
| 		case ChannelType.GUILD_VOICE: | ||||
| 		case ChannelType.GUILD_STAGE_VOICE: | ||||
| 		case ChannelType.GUILD_CATEGORY: | ||||
| 		case ChannelType.GUILD_FORUM: | ||||
| 		case ChannelType.DIRECTORY: | ||||
| 			throw new HTTPError("not a text channel", 400); | ||||
| 		case ChannelType.DM: | ||||
| 		case ChannelType.GROUP_DM: | ||||
| @ -68,7 +71,11 @@ export interface MessageCreateSchema { | ||||
| 	}; | ||||
| 	payload_json?: string; | ||||
| 	file?: any; | ||||
| 	attachments?: any[]; //TODO we should create an interface for attachments
 | ||||
| 	/** | ||||
| 	TODO: we should create an interface for attachments | ||||
| 	TODO: OpenWAAO<-->attachment-style metadata conversion | ||||
| 	**/ | ||||
| 	attachments?: any[]; | ||||
| 	sticker_ids?: string[]; | ||||
| } | ||||
| 
 | ||||
| @ -84,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	const before = req.query.before ? `${req.query.before}` : undefined; | ||||
| 	const after = req.query.after ? `${req.query.after}` : undefined; | ||||
| 	const limit = Number(req.query.limit) || 50; | ||||
| 	if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100"); | ||||
| 	if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100", 422); | ||||
| 
 | ||||
| 	var halfLimit = Math.floor(limit / 2); | ||||
| 
 | ||||
| @ -98,9 +105,16 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 		where: { channel_id }, | ||||
| 		relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] | ||||
| 	}; | ||||
| 	 | ||||
| 
 | ||||
| 	if (after) query.where.id = MoreThan(after); | ||||
| 	else if (before) query.where.id = LessThan(before); | ||||
| 	if (after) { | ||||
| 		if (after > new Snowflake()) return res.status(422); | ||||
| 		query.where.id = MoreThan(after); | ||||
| 	} | ||||
| 	else if (before) {  | ||||
| 		if (before < req.params.channel_id) return res.status(422); | ||||
| 		query.where.id = LessThan(before); | ||||
| 	} | ||||
| 	else if (around) { | ||||
| 		query.where.id = [ | ||||
| 			MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), | ||||
| @ -126,9 +140,12 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 				const uri = y.proxy_url.startsWith("http") ? y.proxy_url : `https://example.org${y.proxy_url}`; | ||||
| 				y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`; | ||||
| 			}); | ||||
| 
 | ||||
| 			//Some clients ( discord.js ) only check if a property exists within the response,
 | ||||
| 			//which causes erorrs when, say, the `application` property is `null`.
 | ||||
| 			 | ||||
| 			/** | ||||
| 			Some clients ( discord.js ) only check if a property exists within the response, | ||||
| 			which causes erorrs when, say, the `application` property is `null`. | ||||
| 			**/ | ||||
| 			 | ||||
| 			for (var curr in x) { | ||||
| 				if (x[curr] === null) | ||||
| 					delete x[curr]; | ||||
| @ -148,15 +165,14 @@ const messageUpload = multer({ | ||||
| 	}, | ||||
| 	storage: multer.memoryStorage() | ||||
| }); // max upload 50 mb
 | ||||
| /** | ||||
|  TODO: dynamically change limit of MessageCreateSchema with config | ||||
| 
 | ||||
| // TODO: dynamically change limit of MessageCreateSchema with config
 | ||||
| // TODO: check: sum of all characters in an embed structure must not exceed instance limits
 | ||||
| 
 | ||||
| // https://discord.com/developers/docs/resources/channel#create-message
 | ||||
| // TODO: text channel slowdown
 | ||||
| // TODO: trim and replace message content and every embed field
 | ||||
| // TODO: check allowed_mentions
 | ||||
| 
 | ||||
|  https://discord.com/developers/docs/resources/channel#create-message
 | ||||
|  TODO: text channel slowdown (per-user and across-users) | ||||
|  Q: trim and replace message content and every embed field A: NO, given this cannot be implemented in E2EE channels | ||||
|  TODO: only dispatch notifications for mentions denoted in allowed_mentions | ||||
| **/ | ||||
| // Send message
 | ||||
| router.post( | ||||
| 	"/", | ||||
| @ -183,6 +199,9 @@ router.post( | ||||
| 			} | ||||
| 		} | ||||
| 		const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); | ||||
| 		if (!channel.isWritable()) { | ||||
| 			throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400) | ||||
| 		} | ||||
| 
 | ||||
| 		const embeds = body.embeds || []; | ||||
| 		if (body.embed) embeds.push(body.embed); | ||||
| @ -236,3 +255,4 @@ router.post( | ||||
| 		return res.json(message); | ||||
| 	} | ||||
| ); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										84
									
								
								api/src/routes/channels/#channel_id/purge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								api/src/routes/channels/#channel_id/purge.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { isTextChannel } from "./messages"; | ||||
| import { FindManyOptions, Between, Not } from "typeorm"; | ||||
| import { | ||||
| 	Attachment, | ||||
| 	Channel, | ||||
| 	Config, | ||||
| 	Embed, | ||||
| 	DiscordApiErrors, | ||||
| 	emitEvent, | ||||
| 	FosscordApiErrors, | ||||
| 	getPermission, | ||||
| 	getRights, | ||||
|  	Message, | ||||
| 	MessageDeleteBulkEvent, | ||||
| 	Snowflake, | ||||
| 	uploadFile  | ||||
| } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import multer from "multer"; | ||||
| import { handleMessage, postHandleMessage } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| export default router; | ||||
| 
 | ||||
| export interface PurgeSchema { | ||||
| 	before: string; | ||||
| 	after: string | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| TODO: apply the delete bit by bit to prevent client and database stress | ||||
| **/ | ||||
| router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	 | ||||
| 	if (!channel.guild_id) throw new HTTPError("Can't purge dm channels", 400); | ||||
| 	isTextChannel(channel.type); | ||||
| 
 | ||||
| 	const rights = await getRights(req.user_id); | ||||
| 	if (!rights.has("MANAGE_MESSAGES")) { | ||||
| 		const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); | ||||
| 		permissions.hasThrow("MANAGE_MESSAGES"); | ||||
| 		permissions.hasThrow("MANAGE_CHANNELS"); | ||||
| 	} | ||||
| 	 | ||||
| 	const { before, after } = req.body as PurgeSchema; | ||||
| 
 | ||||
| 	// TODO: send the deletion event bite-by-bite to prevent client stress
 | ||||
| 
 | ||||
| 	var query: FindManyOptions<Message> & { where: { id?: any; }; } = { | ||||
| 		order: { id: "ASC" }, | ||||
| 		// take: limit,
 | ||||
| 		where: { | ||||
| 		 channel_id, | ||||
| 		 id: Between(after, before), // the right way around
 | ||||
| 		 author_id: rights.has("SELF_DELETE_MESSAGES") ? undefined : Not(req.user_id) | ||||
| 		 // if you lack the right of self-deletion, you can't delete your own messages, even in purges
 | ||||
| 		 }, | ||||
| 		relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] | ||||
| 	}; | ||||
| 	 | ||||
| 
 | ||||
| 	const messages = await Message.find(query); | ||||
| 	const endpoint = Config.get().cdn.endpointPublic; | ||||
| 			 | ||||
| 	if (messages.length == 0) {  | ||||
| 		res.sendStatus(304); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	await Message.delete(messages.map((x) => ({ id: x }))); | ||||
| 	 | ||||
| 	await emitEvent({ | ||||
| 		event: "MESSAGE_DELETE_BULK", | ||||
| 		channel_id, | ||||
| 		data: { ids: messages.map(x => x.id), channel_id, guild_id: channel.guild_id } | ||||
| 	} as MessageDeleteBulkEvent); | ||||
| 
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| @ -11,6 +11,10 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n | ||||
| 	//Snowflake should have `generateFromTime` method? Or similar?
 | ||||
| 	var minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22); | ||||
| 
 | ||||
| 	/** | ||||
| 	idea: ability to customise the cutoff variable | ||||
| 	possible candidates: public read receipt, last presence, last VC leave | ||||
| 	**/ | ||||
| 	var members = await Member.find({ | ||||
| 		where: [ | ||||
| 			{ | ||||
| @ -47,7 +51,7 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n | ||||
| 	return members; | ||||
| }; | ||||
| 
 | ||||
| router.get("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const days = parseInt(req.query.days as string); | ||||
| 
 | ||||
| 	var roles = req.query.include_roles; | ||||
| @ -65,7 +69,7 @@ export interface PruneSchema { | ||||
| 	days: number; | ||||
| } | ||||
| 
 | ||||
| router.post("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| 	const days = parseInt(req.body.days); | ||||
| 
 | ||||
| 	var roles = req.query.include_roles; | ||||
|  | ||||
| @ -46,8 +46,6 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	user.assign(body); | ||||
| 
 | ||||
| 	if (body.new_password) { | ||||
| 		if (!body.password && !user.email) { | ||||
| 			throw FieldErrors({ | ||||
| @ -66,6 +64,7 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 	user.assign(body); | ||||
| 	await user.save(); | ||||
| 
 | ||||
| 	// @ts-ignore
 | ||||
|  | ||||
| @ -91,7 +91,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { | ||||
| 				if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); | ||||
| 			} | ||||
| 		} | ||||
| 		// Q: should be checked if the referenced message exists? ANSWER: NO
 | ||||
| 		/** Q: should be checked if the referenced message exists? ANSWER: NO | ||||
| 		 otherwise backfilling won't work **/ | ||||
| 		// @ts-ignore
 | ||||
| 		message.type = MessageType.REPLY; | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										6
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -102,7 +102,7 @@ | ||||
| 			"name": "@fosscord/api", | ||||
| 			"version": "1.0.0", | ||||
| 			"hasInstallScript": true, | ||||
| 			"license": "GPLV3", | ||||
| 			"license": "AGPLV3", | ||||
| 			"dependencies": { | ||||
| 				"@babel/preset-env": "^7.15.8", | ||||
| 				"@babel/preset-typescript": "^7.15.0", | ||||
| @ -165,7 +165,7 @@ | ||||
| 		"../cdn": { | ||||
| 			"name": "@fosscord/cdn", | ||||
| 			"version": "1.0.0", | ||||
| 			"license": "GPLV3", | ||||
| 			"license": "AGPLV3", | ||||
| 			"dependencies": { | ||||
| 				"@aws-sdk/client-s3": "^3.36.1", | ||||
| 				"@aws-sdk/node-http-handler": "^3.36.0", | ||||
| @ -209,7 +209,7 @@ | ||||
| 			"name": "@fosscord/gateway", | ||||
| 			"version": "1.0.0", | ||||
| 			"hasInstallScript": true, | ||||
| 			"license": "GPLV3", | ||||
| 			"license": "AGPLV3", | ||||
| 			"dependencies": { | ||||
| 				"@fosscord/util": "file:../util", | ||||
| 				"amqplib": "^0.8.0", | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| # CONTRIBUTE | ||||
| 
 | ||||
| ### Setup: | ||||
| 
 | ||||
| ``` | ||||
| npm i | ||||
| npm start | ||||
| ``` | ||||
| 
 | ||||
| ### Run tests: | ||||
| 
 | ||||
| ``` | ||||
| npm test | ||||
| ``` | ||||
| 
 | ||||
| #### common errors: | ||||
| 
 | ||||
| -   db not connecting --> start mongod in a seperate terminal window | ||||
| @ -14,8 +14,8 @@ | ||||
| 		"url": "git+https://github.com/fosscord/fosscord-server.git" | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "", | ||||
| 	"license": "GPLV3", | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "AGPLV3", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
|  | ||||
| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| @ -14,8 +14,8 @@ | ||||
| 		"url": "git+https://github.com/fosscord/fosscord-server.git" | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "", | ||||
| 	"license": "GPLV3", | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "AGPLV3", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
|  | ||||
| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| @ -1,51 +0,0 @@ | ||||
| require("missing-native-js-functions"); | ||||
| const WebSocket = require("ws"); | ||||
| const Constants = require("./dist/util/Constants"); | ||||
| 
 | ||||
| // const ws = new WebSocket("ws://127.0.0.1:8080");
 | ||||
| const ws = new WebSocket("wss://dev.fosscord.com"); | ||||
| 
 | ||||
| ws.on("open", () => { | ||||
| 	// ws.send(JSON.stringify({ req_type: "new_auth" }));
 | ||||
| 	// ws.send(JSON.stringify({ req_type: "check_auth", token: "" }));
 | ||||
| 	// op: 0,
 | ||||
| 	// d: {},
 | ||||
| 	// s: 42,
 | ||||
| 	// t: "GATEWAY_EVENT_NAME",
 | ||||
| }); | ||||
| 
 | ||||
| function send(data) { | ||||
| 	ws.send(JSON.stringify(data)); | ||||
| } | ||||
| 
 | ||||
| ws.on("message", (buffer) => { | ||||
| 	let data = JSON.parse(buffer.toString()); | ||||
| 	console.log(data); | ||||
| 
 | ||||
| 	switch (data.op) { | ||||
| 		case 10: | ||||
| 			setIntervalNow(() => { | ||||
| 				send({ op: 1 }); | ||||
| 			}, data.d.heartbeat_interval); | ||||
| 
 | ||||
| 			// send({
 | ||||
| 			// 	op: 2,
 | ||||
| 			// 	d: {
 | ||||
| 			// 		token:
 | ||||
| 			// 			"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjgxMTY0MjkxNzQzMjA2NjA0OCIsImlhdCI6MTYxMzU4MTE1MX0.7Qj_z2lYIgJ0rc7NfGtpW5DKGqecQfv1mLpoBUQHKDc",
 | ||||
| 			// 		intents: 0n,
 | ||||
| 			// 		properties: {},
 | ||||
| 			// 	},
 | ||||
| 			// });
 | ||||
| 
 | ||||
| 			send({ | ||||
| 				op: 6, | ||||
| 			}); | ||||
| 
 | ||||
| 			break; | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| ws.on("close", (code, reason) => { | ||||
| 	console.log(code, reason, Constants.CLOSECODES[code]); | ||||
| }); | ||||
| @ -13,7 +13,7 @@ | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "GPLV3", | ||||
| 	"license": "AGPLV3", | ||||
| 	"devDependencies": { | ||||
| 		"@types/amqplib": "^0.8.1", | ||||
| 		"@types/jsonwebtoken": "^8.5.0", | ||||
|  | ||||
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| { | ||||
| 	"name": "fosscord-server", | ||||
| 	"version": "1.0.0", | ||||
| 	"description": "A Fosscord server written in Node.js", | ||||
| 	"workspaces": ["api", "cdn", "gateway"], | ||||
| 	"scripts": {}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "git+https://github.com/fosscord/fosscord-server.git" | ||||
| 	}, | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "AGPLV3", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
| 	"homepage": "https://fosscord.com" | ||||
| } | ||||
							
								
								
									
										14
									
								
								rtc/LICENSE
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								rtc/LICENSE
									
									
									
									
									
								
							| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
							
								
								
									
										14
									
								
								util/LICENSE
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								util/LICENSE
									
									
									
									
									
								
							| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| @ -23,7 +23,7 @@ | ||||
| 		"discord-open-source" | ||||
| 	], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "GPLV3", | ||||
| 	"license": "AGPLV3", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
|  | ||||
| @ -4,41 +4,93 @@ import { ChannelPermissionOverwrite } from "./Channel"; | ||||
| import { User } from "./User"; | ||||
| 
 | ||||
| export enum AuditLogEvents { | ||||
| 	GUILD_UPDATE = 1, | ||||
| 	CHANNEL_CREATE = 10, | ||||
| 	// guild level
 | ||||
| 	GUILD_UPDATE = 1,  | ||||
| 	GUILD_IMPORT = 2, | ||||
| 	GUILD_EXPORTED = 3, | ||||
| 	GUILD_ARCHIVE = 4, | ||||
| 	GUILD_UNARCHIVE = 5, | ||||
| 	// join-leave
 | ||||
| 	USER_JOIN = 6,  | ||||
| 	USER_LEAVE = 7, | ||||
| 	// channels
 | ||||
| 	CHANNEL_CREATE = 10,  | ||||
| 	CHANNEL_UPDATE = 11, | ||||
| 	CHANNEL_DELETE = 12, | ||||
| 	CHANNEL_OVERWRITE_CREATE = 13, | ||||
| 	// permission overrides
 | ||||
| 	CHANNEL_OVERWRITE_CREATE = 13,  | ||||
| 	CHANNEL_OVERWRITE_UPDATE = 14, | ||||
| 	CHANNEL_OVERWRITE_DELETE = 15, | ||||
| 	MEMBER_KICK = 20, | ||||
| 	// kick and ban
 | ||||
| 	MEMBER_KICK = 20,  | ||||
| 	MEMBER_PRUNE = 21, | ||||
| 	MEMBER_BAN_ADD = 22, | ||||
| 	MEMBER_BAN_REMOVE = 23, | ||||
| 	// member updates
 | ||||
| 	MEMBER_UPDATE = 24, | ||||
| 	MEMBER_ROLE_UPDATE = 25, | ||||
| 	MEMBER_MOVE = 26, | ||||
| 	MEMBER_DISCONNECT = 27, | ||||
| 	BOT_ADD = 28, | ||||
| 	// roles
 | ||||
| 	ROLE_CREATE = 30, | ||||
| 	ROLE_UPDATE = 31, | ||||
| 	ROLE_DELETE = 32, | ||||
| 	ROLE_SWAP = 33, | ||||
| 	// invites
 | ||||
| 	INVITE_CREATE = 40, | ||||
| 	INVITE_UPDATE = 41, | ||||
| 	INVITE_DELETE = 42, | ||||
| 	// webhooks
 | ||||
| 	WEBHOOK_CREATE = 50, | ||||
| 	WEBHOOK_UPDATE = 51, | ||||
| 	WEBHOOK_DELETE = 52, | ||||
| 	WEBHOOK_SWAP = 53, | ||||
| 	// custom emojis
 | ||||
| 	EMOJI_CREATE = 60, | ||||
| 	EMOJI_UPDATE = 61, | ||||
| 	EMOJI_DELETE = 62, | ||||
| 	EMOJI_SWAP = 63, | ||||
| 	// deletion
 | ||||
| 	MESSAGE_CREATE = 70, // messages sent using non-primary seat of the user only
 | ||||
| 	MESSAGE_EDIT = 71, // non-self edits only
 | ||||
| 	MESSAGE_DELETE = 72, | ||||
| 	MESSAGE_BULK_DELETE = 73, | ||||
| 	// pinning
 | ||||
| 	MESSAGE_PIN = 74, | ||||
| 	MESSAGE_UNPIN = 75, | ||||
| 	// integrations
 | ||||
| 	INTEGRATION_CREATE = 80, | ||||
| 	INTEGRATION_UPDATE = 81, | ||||
| 	INTEGRATION_DELETE = 82, | ||||
| 	// stage actions
 | ||||
| 	STAGE_INSTANCE_CREATE = 83, | ||||
| 	STAGE_INSTANCE_UPDATE = 84, | ||||
| 	STAGE_INSTANCE_DELETE = 85, | ||||
| 	// stickers
 | ||||
| 	STICKER_CREATE = 90, | ||||
| 	STICKER_UPDATE = 91, | ||||
| 	STICKER_DELETE = 92, | ||||
| 	STICKER_SWAP = 93, | ||||
| 	// threads
 | ||||
| 	THREAD_CREATE = 110, | ||||
| 	THREAD_UPDATE = 111, | ||||
| 	THREAD_DELETE = 112, | ||||
| 	// application commands
 | ||||
| 	APPLICATION_COMMAND_PERMISSION_UPDATE = 121, | ||||
| 	// automod
 | ||||
| 	POLICY_CREATE = 140,  | ||||
| 	POLICY_UPDATE = 141, | ||||
| 	POLICY_DELETE = 142, | ||||
| 	MESSAGE_BLOCKED_BY_POLICIES = 143,  // in fosscord, blocked messages are stealth-dropped
 | ||||
| 	// instance policies affecting the guild
 | ||||
| 	GUILD_AFFECTED_BY_POLICIES = 216, | ||||
| 	// message moves
 | ||||
| 	IN_GUILD_MESSAGE_MOVE = 223, | ||||
| 	CROSS_GUILD_MESSAGE_MOVE = 224, | ||||
| 	// message routing
 | ||||
| 	ROUTE_CREATE = 225,  | ||||
| 	ROUTE_UPDATE = 226, | ||||
| } | ||||
| 
 | ||||
| @Entity("audit_logs") | ||||
|  | ||||
| @ -28,6 +28,8 @@ export enum ChannelType { | ||||
| 	GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
 | ||||
| 	GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
 | ||||
| 	GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
 | ||||
| 	DIRECTORY = 14, // guild directory listing channel
 | ||||
| 	GUILD_FORUM = 15, // forum composed of IM threads
 | ||||
| 	TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12
 | ||||
| 	KANBAN = 34, // confluence like kanban board
 | ||||
| 	VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage)
 | ||||
| @ -352,6 +354,16 @@ export class Channel extends BaseClass { | ||||
| 	isDm() { | ||||
| 		return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; | ||||
| 	} | ||||
| 
 | ||||
| 	// Does the channel support sending messages ( eg categories do not )
 | ||||
| 	isWritable() { | ||||
| 		const disallowedChannelTypes = [ | ||||
| 			ChannelType.GUILD_CATEGORY, | ||||
| 			ChannelType.GUILD_STAGE_VOICE, | ||||
| 			ChannelType.VOICELESS_WHITEBOARD, | ||||
| 		]; | ||||
| 		return disallowedChannelTypes.indexOf(this.type) == -1; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export interface ChannelPermissionOverwrite { | ||||
|  | ||||
| @ -324,7 +324,7 @@ export const DefaultConfigOptions: ConfigValue = { | ||||
| 			// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
 | ||||
| 		}, | ||||
| 		dateOfBirth: { | ||||
| 			required: false, | ||||
| 			required: true, | ||||
| 			minimum: 13, | ||||
| 		}, | ||||
| 		disabled: false, | ||||
|  | ||||
							
								
								
									
										33
									
								
								util/src/entities/Group.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								util/src/entities/Group.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; | ||||
| 
 | ||||
| import { BaseClass } from "./BaseClass"; | ||||
| 
 | ||||
| @Entity("groups") | ||||
| export class UserGroup extends BaseClass { | ||||
|   @Column({ nullable: true }) | ||||
|   parent?: BigInt; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	color: number; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	hoist: boolean; | ||||
| 
 | ||||
|  	@Column() | ||||
| 	mentionable: boolean; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	name: string; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	rights: BigInt; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	position: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	icon: BigInt; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	unicode_emoji: BigInt; | ||||
| } | ||||
| @ -39,13 +39,15 @@ export enum MessageType { | ||||
| 	USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, | ||||
| 	USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, | ||||
| 	CHANNEL_FOLLOW_ADD = 12, | ||||
| 	ACTION = 13, // /me messages
 | ||||
| 	GUILD_DISCOVERY_DISQUALIFIED = 14, | ||||
| 	GUILD_DISCOVERY_REQUALIFIED = 15, | ||||
| 	ENCRYPTED = 16, | ||||
| 	REPLY = 19, | ||||
| 	APPLICATION_COMMAND = 20, | ||||
| 	APPLICATION_COMMAND = 20, // application command or self command invocation
 | ||||
| 	ROUTE_ADDED = 41, // custom message routing: new route affecting that channel
 | ||||
| 	ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel
 | ||||
| 	SELF_COMMAND_SCRIPT = 43, // self command scripts
 | ||||
| 	ENCRYPTION = 50, | ||||
| 	CUSTOM_START = 63, | ||||
| 	UNHANDLED = 255 | ||||
|  | ||||
| @ -163,6 +163,10 @@ export class User extends BaseClass { | ||||
| 
 | ||||
| 	@Column({ type: "simple-json", select: false }) | ||||
| 	settings: UserSettings; | ||||
| 		 | ||||
| 	// workaround to prevent fossord-unaware clients from deleting settings not used by them
 | ||||
| 	@Column({ type: "simple-json", select: false }) | ||||
| 	extended_settings: string; | ||||
| 
 | ||||
| 	@Column({ type: "simple-json" }) | ||||
| 	notes: { [key: string]: string };	//key is ID of user
 | ||||
| @ -273,6 +277,7 @@ export class User extends BaseClass { | ||||
| 				valid_tokens_since: new Date(), | ||||
| 			}, | ||||
| 			settings: { ...defaultSettings, locale: language }, | ||||
| 			extended_settings: {}, | ||||
| 			fingerprints: [], | ||||
| 			notes: {}, | ||||
| 		}); | ||||
|  | ||||
							
								
								
									
										37
									
								
								util/src/entities/UserGroup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								util/src/entities/UserGroup.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; | ||||
| 
 | ||||
| import { BaseClass } from "./BaseClass"; | ||||
| import { Guild } from "./Guild"; | ||||
| import { User } from "./User"; | ||||
| 
 | ||||
| @Entity("groups") | ||||
| export class UserGroup extends BaseClass { | ||||
| 	@Column() | ||||
| 	color: number; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	hoist: boolean; | ||||
| 	 | ||||
| 	@JoinColumn({ name: "controller", referencedColumnName: "id" }) | ||||
| 	@ManyToOne(() => User) | ||||
| 	controller?: User; | ||||
| 	  | ||||
| 	@Column() | ||||
| 	mentionable_by?: string; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	name: string; | ||||
| 
 | ||||
| 	@Column() | ||||
| 	rights: string; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	icon: string; | ||||
| 	 | ||||
| 	@Column({ nullable: true }) | ||||
| 	parent?: string; | ||||
| 	 | ||||
| 	@Column({ type: "simple-array", nullable: true}) | ||||
| 	associciations: string[]; | ||||
| 
 | ||||
| } | ||||
| @ -731,21 +731,23 @@ export const DiscordApiErrors = { | ||||
|  * An error encountered while performing an API request (Fosscord only). Here are the potential errors: | ||||
|  */ | ||||
| export const FosscordApiErrors = { | ||||
| 	MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1), | ||||
| 	MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500), | ||||
| 	PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001), | ||||
| 	NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002), | ||||
| 	GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003), | ||||
| 	GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403), | ||||
| 	CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), | ||||
| 	USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010), | ||||
| 	USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), | ||||
| 	CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050), | ||||
| 	CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403), | ||||
| 	CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051), | ||||
| 	CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052), | ||||
| 	CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059), | ||||
| 	EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060), | ||||
| 	DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061), | ||||
| 	FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006), | ||||
| 	CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403), | ||||
| 	EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403), | ||||
| 	DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403), | ||||
| 	FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501), | ||||
| 	MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]), | ||||
| 	CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409), | ||||
| 	CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003), | ||||
| 	CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050), | ||||
| 	ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]), | ||||
| 	CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061), | ||||
| @ -791,3 +793,4 @@ function keyMirror(arr: string[]) { | ||||
| 	for (const value of arr) tmp[value] = value; | ||||
| 	return tmp; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -18,6 +18,8 @@ export class Intents extends BitField { | ||||
| 		DIRECT_MESSAGE_REACTIONS: BigInt(1) << BigInt(13), // DM or orphan channel message reactions
 | ||||
| 		DIRECT_MESSAGE_TYPING: BigInt(1) << BigInt(14), // DM typing notifications
 | ||||
| 		GUILD_MESSAGES_CONTENT: BigInt(1) << BigInt(15), // guild message content
 | ||||
| 		GUILD_POLICIES: BigInt(1) << BigInt(20), // guild policies
 | ||||
| 		GUILD_POLICY_EXECUTION: BigInt(1) << BigInt(21), // guild policy execution
 | ||||
| 		LIVE_MESSAGE_COMPOSITION: BigInt(1) << BigInt(32), // allow composing messages using the gateway
 | ||||
| 		GUILD_ROUTES: BigInt(1) << BigInt(41), // message routes affecting the guild
 | ||||
| 		DIRECT_MESSAGES_THREADS: BigInt(1) << BigInt(42),  // direct message threads
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| // https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js
 | ||||
| // Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
 | ||||
| // based on https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js
 | ||||
| // Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah, 2022 Erkin Alp Güney
 | ||||
| 
 | ||||
| import { BitField } from "./BitField"; | ||||
| 
 | ||||
| @ -8,7 +8,13 @@ export class MessageFlags extends BitField { | ||||
| 		CROSSPOSTED: BigInt(1) << BigInt(0), | ||||
| 		IS_CROSSPOST: BigInt(1) << BigInt(1), | ||||
| 		SUPPRESS_EMBEDS: BigInt(1) << BigInt(2), | ||||
| 		SOURCE_MESSAGE_DELETED: BigInt(1) << BigInt(3), | ||||
| 		// SOURCE_MESSAGE_DELETED: BigInt(1) << BigInt(3), // fosscord will delete them from destination too, making this redundant
 | ||||
| 		URGENT: BigInt(1) << BigInt(4), | ||||
| 		// HAS_THREAD: BigInt(1) << BigInt(5) // does not apply to fosscord due to infrastructural differences
 | ||||
| 		PRIVATE_ROUTE: BigInt(1) << BigInt(6), // it that has been routed to only some of the users that can see the channel
 | ||||
| 		INTERACTION_WAIT: BigInt(1) << BigInt(7), // discord.com calls this LOADING
 | ||||
| 		// FAILED_TO_MENTION_SOME_ROLES_IN_THREAD: BigInt(1) << BigInt(8)
 | ||||
| 		SCRIPT_WAIT: BigInt(1) << BigInt(24), // waiting for the self command to complete
 | ||||
| 		IMPORT_WAIT: BigInt(1) << BigInt(25), // latest message of a bulk import, waiting for the rest of the channel to be backfilled
 | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,12 @@ export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; | ||||
| 
 | ||||
| export function checkToken(token: string, jwtSecret: string): Promise<any> { | ||||
| 	return new Promise((res, rej) => { | ||||
| 		token = token.replace("Bot ", ""); // TODO: proper bot support
 | ||||
| 		token = token.replace("Bot ", ""); | ||||
| 		/** | ||||
| 		in fosscord, even with instances that have bot distinction; we won't enforce "Bot" prefix, | ||||
| 		as we don't really have separate pathways for bots  | ||||
| 		**/ | ||||
| 		 | ||||
| 		jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { | ||||
| 			if (err || !decoded) return rej("Invalid Token"); | ||||
| 
 | ||||
|  | ||||
| @ -1,14 +0,0 @@ | ||||
| Copyright (C) 2021 Fosscord and contributors | ||||
| 
 | ||||
| This program is free software: you can redistribute it and/or modify | ||||
| it under the terms of the GNU Affero General Public License as | ||||
| published by the Free Software Foundation, either version 3 of the | ||||
| License, or (at your option) any later version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU Affero General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU Affero General Public License | ||||
| along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| @ -9,8 +9,8 @@ | ||||
| 		"start": "npm run build && node dist/start.js" | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "", | ||||
| 	"license": "ISC", | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "AGPLV3", | ||||
| 	"devDependencies": { | ||||
| 		"@types/node": "^15.6.1", | ||||
| 		"@types/sdp-transform": "^2.4.5", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Madeline
						Madeline