Merge branch 'master' into fix/claim_accounts
This commit is contained in:
		
						commit
						e1ebfe79d2
					
				| @ -3,7 +3,7 @@ | ||||
| </p> | ||||
| <h1 align="center">Fosscord Server</h1> | ||||
| 
 | ||||
| <p> | ||||
| <p align="center"> | ||||
|   <a href="https://discord.gg/ZrnGQP6p3d"> | ||||
|     <img src="https://img.shields.io/discord/806142446094385153?color=7489d5&logo=discord&logoColor=ffffff" /> | ||||
|   </a> | ||||
|  | ||||
							
								
								
									
										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/>. | ||||
							
								
								
									
										22129
									
								
								api/assets/schemas.json
									
									
									
									
									
								
							
							
						
						
									
										22129
									
								
								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}} האורך חייב להיות בין" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| { | ||||
| 	"login": { | ||||
| 		"INVALID_LOGIN": "E-mail lub telefon nie został znaleziony", | ||||
| 		"INVALID_LOGIN": "E-mail lub numer telefonu nie został znaleziony", | ||||
| 		"INVALID_PASSWORD": "Nieprawidłowe hasło", | ||||
| 		"ACCOUNT_DISABLED": "To konto jest nieaktywne" | ||||
| 	}, | ||||
| 	"register": { | ||||
| 		"REGISTRATION_DISABLED": "Rejestracja nowych użytkowników jest wyłączona", | ||||
| 		"INVITE_ONLY": "Aby się zarejestrować, musisz zostać zaproszony", | ||||
| 		"EMAIL_INVALID": "Nieprawidłowy email", | ||||
| 		"EMAIL_INVALID": "Nieprawidłowy E-mail", | ||||
| 		"EMAIL_ALREADY_REGISTERED": "E-mail jest już zarejestrowany", | ||||
| 		"DATE_OF_BIRTH_UNDERAGE": "Musisz mieć {{years}} lat lub więcej", | ||||
| 		"CONSENT_REQUIRED": "Musisz zaakceptować Regulamin i Politykę Prywatności.", | ||||
|  | ||||
| @ -10,9 +10,9 @@ | ||||
| 		"BASE_TYPE_OBJECT": "To pole musi być obiektem", | ||||
| 		"BASE_TYPE_ARRAY": "To pole musi być tablicą", | ||||
| 		"UNKOWN_FIELD": "Nieznany klucz: {{key}}", | ||||
| 		"BASE_TYPE_CONSTANT": "To pole musi być {{value}}", | ||||
| 		"BASE_TYPE_CONSTANT": "To pole musi wynosić {{value}}", | ||||
| 		"EMAIL_TYPE_INVALID_EMAIL": "Źle sformułowany adres e-mail", | ||||
| 		"DATE_TYPE_PARSE": "Nie można przetworzyć {{date}}. Powinno być ISO8601", | ||||
| 		"BASE_TYPE_BAD_LENGTH": "Długość musi wynosić między {{length}}" | ||||
| 		"BASE_TYPE_BAD_LENGTH": "Długość musi wynosić pomiędzy {{length}}" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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": "E-post eller telefon hittades inte", | ||||
| 		"INVALID_PASSWORD": "Ogiltigt lösenord", | ||||
| 		"ACCOUNT_DISABLED": "Detta konto är inaktiverat" | ||||
| 	}, | ||||
| 	"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": "Registrering av nya användare är inaktiverat", | ||||
| 		"INVITE_ONLY": "Du måste vara inbjuden för att registrera dig", | ||||
| 		"EMAIL_INVALID": "Ogiltig e-post", | ||||
| 		"EMAIL_ALREADY_REGISTERED": "E-postadressen är redan registrerad", | ||||
| 		"DATE_OF_BIRTH_UNDERAGE": "Du måste vara {{years}} år eller äldre", | ||||
| 		"CONSENT_REQUIRED": "Du måste godkänna användarvillkoren och sekretesspolicyn.", | ||||
| 		"USERNAME_TOO_MANY_USERS": "För många användare har detta användarnamn, försök med ett annat" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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_OBJECT": "This field must be an object", | ||||
| 		"BASE_TYPE_ARRAY": "This field must be an array", | ||||
| 		"UNKOWN_FIELD": "Unknown key: {{key}}", | ||||
| 		"BASE_TYPE_CONSTANT": "This field must be {{value}}", | ||||
| 		"EMAIL_TYPE_INVALID_EMAIL": "Not a well-formed email address", | ||||
| 		"DATE_TYPE_PARSE": "Could not parse {{date}}. Should be ISO8601", | ||||
| 		"BASE_TYPE_BAD_LENGTH": "Must be between {{length}} in length" | ||||
| 		"BASE_TYPE_REQUIRED": "Detta fältet krävs", | ||||
| 		"BASE_TYPE_STRING": "Detta fält måste vara en sträng", | ||||
| 		"BASE_TYPE_NUMBER": "Detta fält måste vara ett nummer", | ||||
| 		"BASE_TYPE_BIGINT": "Detta fält måste vara av typen bigint", | ||||
| 		"BASE_TYPE_BOOLEAN": "Detta fält måste vara booleskt", | ||||
| 		"BASE_TYPE_CHOICES": "Detta fält måste vara av typen av ett av följande ({{types}})", | ||||
| 		"BASE_TYPE_CLASS": "Det här fältet måste vara en instans av {{type}}", | ||||
| 		"BASE_TYPE_OBJECT": "Detta fält måste vara ett objekt", | ||||
| 		"BASE_TYPE_ARRAY": "Detta fält måste vara en array", | ||||
| 		"UNKOWN_FIELD": "Okänd nyckel: {{key}}", | ||||
| 		"BASE_TYPE_CONSTANT": "Det här fältet måste vara {{value}}", | ||||
| 		"EMAIL_TYPE_INVALID_EMAIL": "E-postadressen har inte korrekt format", | ||||
| 		"DATE_TYPE_PARSE": "Kunde inte tolka {{date}}. Bör vara ISO8601", | ||||
| 		"BASE_TYPE_BAD_LENGTH": "Måste vara mellan {{length}} i längd" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -30,7 +30,7 @@ | ||||
| 		"discord-open-source" | ||||
| 	], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "GPLV3", | ||||
| 	"license": "AGPL-3.0-only", | ||||
| 	"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,6 +45,12 @@ 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
 | ||||
| 		if (req.user_id) { | ||||
| 			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; | ||||
| @ -53,12 +60,12 @@ export default function rateLimit(opts: { | ||||
| 		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 +77,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 ( | ||||
| @ -151,7 +163,7 @@ export async function initRateLimits(app: Router) { | ||||
| 	app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); | ||||
| } | ||||
| 
 | ||||
| async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { | ||||
| async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) { | ||||
| 	const id = opts.executor_id + opts.bucket_id; | ||||
| 	var limit = Cache.get(id); | ||||
| 	if (!limit) { | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -4,8 +4,9 @@ import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| // TODO: check if message exists
 | ||||
| // TODO: public read receipts & privacy scoping
 | ||||
| // TODO: send read state event to all channel members
 | ||||
| // TODO: advance-only notification cursor
 | ||||
| 
 | ||||
| export interface MessageAcknowledgeSchema { | ||||
| 	manual?: boolean; | ||||
|  | ||||
| @ -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,95 @@ 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; | ||||
| 
 | ||||
| 	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); | ||||
| 
 | ||||
| 	const permissions = await getPermission(req.user_id, undefined, channel_id); | ||||
| 	 | ||||
| 	if (message.author_id !== req.user_id) permissions.hasThrow("READ_MESSAGE_HISTORY"); | ||||
| 
 | ||||
| 	return res.json(message); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id } = req.params; | ||||
| 
 | ||||
|  | ||||
| @ -101,7 +101,7 @@ router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request | ||||
| 	res.json(users); | ||||
| }); | ||||
| 
 | ||||
| router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY" }), async (req: Request, res: Response) => { | ||||
| router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), async (req: Request, res: Response) => { | ||||
| 	const { message_id, channel_id, user_id } = req.params; | ||||
| 	if (user_id !== "@me") throw new HTTPError("Invalid user"); | ||||
| 	const emoji = getEmoji(req.params.emoji); | ||||
|  | ||||
| @ -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 }))); | ||||
| 
 | ||||
|  | ||||
| @ -8,8 +8,10 @@ import { | ||||
| 	Embed, | ||||
| 	emitEvent, | ||||
| 	getPermission, | ||||
| 	getRights, | ||||
| 	Message, | ||||
| 	MessageCreateEvent, | ||||
| 	Snowflake, | ||||
| 	uploadFile, | ||||
| 	Member | ||||
| } from "@fosscord/util"; | ||||
| @ -29,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: | ||||
| @ -67,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[]; | ||||
| } | ||||
| 
 | ||||
| @ -83,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); | ||||
| 
 | ||||
| @ -97,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()), | ||||
| @ -119,15 +134,18 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 				delete x.user_ids; | ||||
| 			}); | ||||
| 			// @ts-ignore
 | ||||
| 			if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: "0", avatar: null }; | ||||
| 			if (!x.author) x.author = { id: "4", discriminator: "0000", username: "Fosscord Ghost", public_flags: "0", avatar: null }; | ||||
| 			x.attachments?.forEach((y: any) => { | ||||
| 				// dynamically set attachment proxy_url in case the endpoint changed
 | ||||
| 				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]; | ||||
| @ -147,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 6000 characters
 | ||||
| 
 | ||||
| // 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( | ||||
| 	"/", | ||||
| @ -167,7 +184,7 @@ router.post( | ||||
| 
 | ||||
| 		next(); | ||||
| 	}, | ||||
| 	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), | ||||
| 	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), | ||||
| 	async (req: Request, res: Response) => { | ||||
| 		const { channel_id } = req.params; | ||||
| 		var body = req.body as MessageCreateSchema; | ||||
| @ -182,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); | ||||
| @ -235,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); | ||||
| }); | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Guild } from "@fosscord/util"; | ||||
| import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Rights, Guild } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| 
 | ||||
| @ -52,27 +52,47 @@ router.put("/", route({}), async (req: Request, res: Response) => { | ||||
| 
 | ||||
| 	// TODO: Lurker mode
 | ||||
| 
 | ||||
| 	const rights = await getRights(req.user_id); | ||||
| 
 | ||||
| 	let { guild_id, member_id } = req.params; | ||||
| 	if (member_id === "@me") member_id = req.user_id; | ||||
| 	if (member_id === "@me") { | ||||
| 		member_id = req.user_id; | ||||
| 		rights.hasThrow("JOIN_GUILDS"); | ||||
| 	} else { | ||||
| 		// TODO: join others by controller	
 | ||||
| 	} | ||||
| 
 | ||||
| 	var guild = await Guild.findOneOrFail({ | ||||
| 		where: { id: guild_id }	}); | ||||
| 		where: { id: guild_id } | ||||
| 	}); | ||||
| 
 | ||||
| 	var emoji = await Emoji.find({ | ||||
| 		where: { guild_id: guild_id }	}); | ||||
| 		where: { guild_id: guild_id } | ||||
| 	}); | ||||
| 
 | ||||
| 	var roles = await Role.find({ | ||||
| 		where: { guild_id: guild_id }	}); | ||||
| 		where: { guild_id: guild_id } | ||||
| 	}); | ||||
| 
 | ||||
| 	var stickers = await Sticker.find({ | ||||
| 		where: { guild_id: guild_id }	}); | ||||
| 	 | ||||
| 		where: { guild_id: guild_id } | ||||
| 	}); | ||||
| 
 | ||||
| 	await Member.addToGuild(member_id, guild_id); | ||||
| 	res.send({...guild, emojis: emoji, roles: roles, stickers: stickers}); | ||||
| 	res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers }); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { | ||||
| router.delete("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const permission = await getPermission(req.user_id); | ||||
| 	const rights = await getRights(req.user_id); | ||||
| 	const { guild_id, member_id } = req.params; | ||||
| 	if (member_id !== "@me" || member_id === req.user_id) { | ||||
| 		// TODO: unless force-joined
 | ||||
| 		rights.hasThrow("SELF_LEAVE_GROUPS"); | ||||
| 	} else { | ||||
| 		rights.hasThrow("KICK_BAN_MEMBERS"); | ||||
| 		permission.hasThrow("KICK_MEMBERS"); | ||||
| 	} | ||||
| 
 | ||||
| 	await Member.removeFromGuild(member_id, guild_id); | ||||
| 	res.sendStatus(204); | ||||
|  | ||||
| @ -6,7 +6,6 @@ import { HTTPError } from "lambert-server"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| // TODO: not allowed for user -> only allowed for bots with privileged intents
 | ||||
| // TODO: send over websocket
 | ||||
| // TODO: check for GUILD_MEMBERS intent
 | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										68
									
								
								api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { RoleModifySchema } from "../"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, role_id } = req.params; | ||||
| 	await Member.IsInGuildOrFail(req.user_id, guild_id); | ||||
| 	const role = await Role.findOneOrFail({ guild_id, id: role_id }); | ||||
| 	return res.json(role); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id, role_id } = req.params; | ||||
| 	if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		Role.delete({ | ||||
| 			id: role_id, | ||||
| 			guild_id: guild_id | ||||
| 		}), | ||||
| 		emitEvent({ | ||||
| 			event: "GUILD_ROLE_DELETE", | ||||
| 			guild_id, | ||||
| 			data: { | ||||
| 				guild_id, | ||||
| 				role_id | ||||
| 			} | ||||
| 		} as GuildRoleDeleteEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| // TODO: check role hierarchy
 | ||||
| 
 | ||||
| router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { role_id, guild_id } = req.params; | ||||
| 	const body = req.body as RoleModifySchema; | ||||
| 
 | ||||
| 	if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); | ||||
| 
 | ||||
| 	const role = new Role({ | ||||
| 		...body, | ||||
| 		id: role_id, | ||||
| 		guild_id, | ||||
| 		permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) | ||||
| 	}); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		role.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "GUILD_ROLE_UPDATE", | ||||
| 			guild_id, | ||||
| 			data: { | ||||
| 				guild_id, | ||||
| 				role | ||||
| 			} | ||||
| 		} as GuildRoleUpdateEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	res.json(role); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
| @ -81,59 +81,6 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }) | ||||
| 	res.json(role); | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const guild_id = req.params.guild_id; | ||||
| 	const { role_id } = req.params; | ||||
| 	if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		Role.delete({ | ||||
| 			id: role_id, | ||||
| 			guild_id: guild_id | ||||
| 		}), | ||||
| 		emitEvent({ | ||||
| 			event: "GUILD_ROLE_DELETE", | ||||
| 			guild_id, | ||||
| 			data: { | ||||
| 				guild_id, | ||||
| 				role_id | ||||
| 			} | ||||
| 		} as GuildRoleDeleteEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| // TODO: check role hierarchy
 | ||||
| 
 | ||||
| router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { | ||||
| 	const { role_id, guild_id } = req.params; | ||||
| 	const body = req.body as RoleModifySchema; | ||||
| 
 | ||||
| 	if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);  | ||||
| 
 | ||||
| 	const role = new Role({ | ||||
| 		...body, | ||||
| 		id: role_id, | ||||
| 		guild_id, | ||||
| 		permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) | ||||
| 	}); | ||||
| 
 | ||||
| 	await Promise.all([ | ||||
| 		role.save(), | ||||
| 		emitEvent({ | ||||
| 			event: "GUILD_ROLE_UPDATE", | ||||
| 			guild_id, | ||||
| 			data: { | ||||
| 				guild_id, | ||||
| 				role | ||||
| 			} | ||||
| 		} as GuildRoleUpdateEvent) | ||||
| 	]); | ||||
| 
 | ||||
| 	res.json(role); | ||||
| }); | ||||
| 
 | ||||
| router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const { guild_id } = req.params; | ||||
| 	const body = req.body as RolePositionUpdateSchema; | ||||
| @ -13,7 +13,7 @@ router.get("/:code", route({}), async (req: Request, res: Response) => { | ||||
| 	res.status(200).send(invite); | ||||
| }); | ||||
| 
 | ||||
| router.post("/:code", route({right: "JOIN_GUILDS"}), async (req: Request, res: Response) => { | ||||
| router.post("/:code", route({right: "USE_MASS_INVITES"}), async (req: Request, res: Response) => { | ||||
| 	const { code } = req.params; | ||||
|     const { guild_id } = await Invite.findOneOrFail({ code }) | ||||
| 	const { features } = await Guild.findOneOrFail({ id: guild_id}); | ||||
|  | ||||
| @ -1,10 +1,26 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Config } from "@fosscord/util"; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get("/", route({}), (req: Request, res: Response) => { | ||||
| 	res.send("pong"); | ||||
| 	const { general } = Config.get(); | ||||
| 	res.send({ | ||||
| 		ping: "pong!", | ||||
| 		instance: { | ||||
| 			id: general.instanceId, | ||||
| 			name: general.instanceName, | ||||
| 			description: general.instanceDescription, | ||||
| 			image: general.image, | ||||
| 
 | ||||
| 			correspondenceEmail: general.correspondenceEmail, | ||||
| 			correspondenceUserID: general.correspondenceUserID, | ||||
| 
 | ||||
| 			frontPage: general.frontPage, | ||||
| 			tosPage: general.tosPage, | ||||
| 		}, | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -7,7 +7,12 @@ config(); | ||||
| import { FosscordServer } from "./Server"; | ||||
| import cluster from "cluster"; | ||||
| import os from "os"; | ||||
| const cores = Number(process.env.THREADS) || os.cpus().length; | ||||
| var cores = 1; | ||||
| try { | ||||
| 	cores = Number(process.env.THREADS) || os.cpus().length; | ||||
| } catch { | ||||
| 	console.log("[API] Failed to get thread count! Using 1...") | ||||
| } | ||||
| 
 | ||||
| if (cluster.isMaster && process.env.NODE_ENV == "production") { | ||||
| 	console.log(`Primary ${process.pid} is running`); | ||||
|  | ||||
| @ -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,6 +6,7 @@ import { | ||||
| 	FieldErrors, | ||||
| 	FosscordApiErrors, | ||||
| 	getPermission, | ||||
| 	getRights, | ||||
| 	PermissionResolvable, | ||||
| 	Permissions, | ||||
| 	RightResolvable, | ||||
| @ -105,6 +106,8 @@ export function route(opts: RouteOptions) { | ||||
| 
 | ||||
| 		if (opts.right) { | ||||
| 			const required = new Rights(opts.right); | ||||
| 			req.rights = await getRights(req.user_id); | ||||
| 
 | ||||
| 			if (!req.rights || !req.rights.has(required)) { | ||||
| 				throw FosscordApiErrors.MISSING_RIGHTS.withParams(opts.right as string); | ||||
| 			} | ||||
|  | ||||
| @ -13,7 +13,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored | ||||
|  *  - min <n> numbers | ||||
|  *  - min <n> symbols | ||||
|  *  - min <n> uppercase chars | ||||
|  *  - shannon entropy divided by password entropy | ||||
|  *  - shannon entropy folded into [0, 1) interval | ||||
|  * | ||||
|  * Returns: 0 > pw > 1 | ||||
|  */ | ||||
| @ -46,15 +46,15 @@ export function checkPassword(password: string): number { | ||||
| 		strength = 0; | ||||
| 	} | ||||
| 	 | ||||
| 	let entropyMap; | ||||
| 	let entropyMap: { [key: string]: number } = {}; | ||||
| 	for (let i = 0; i < password.length; i++) { | ||||
| 		if (entropyMap[password[i]]) entropyMap[password[i]]++; | ||||
| 		else entropyMap[password[i]] = 1; | ||||
| 	} | ||||
| 	 | ||||
| 	let entropies = Array(entropyMap); | ||||
| 		 | ||||
| 	let entropies = Object.values(entropyMap); | ||||
| 	 | ||||
| 	entropies.map(x => (x / entropyMap.length)); | ||||
| 	strength += entropies.reduceRight((a, x), a - (x * Math.log2(x))) / Math.log2(password.length);	 | ||||
| 	strength += entropies.reduceRight((a: number, x: number) => a - (x * Math.log2(x))) / Math.log2(password.length);	 | ||||
| 	return strength; | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								bundle/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -101,7 +101,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", | ||||
| @ -164,7 +164,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", | ||||
| @ -208,7 +208,7 @@ | ||||
| 			"name": "@fosscord/gateway", | ||||
| 			"version": "1.0.0", | ||||
| 			"hasInstallScript": true, | ||||
| 			"license": "GPLV3", | ||||
| 			"license": "AGPLV3", | ||||
| 			"dependencies": { | ||||
| 				"@fosscord/util": "file:../util", | ||||
| 				"amqplib": "^0.8.0", | ||||
|  | ||||
| @ -18,7 +18,7 @@ | ||||
| 	}, | ||||
| 	"keywords": [], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "AGPLV3", | ||||
| 	"license": "AGPL-3.0-only", | ||||
| 	"bugs": { | ||||
| 		"url": "https://github.com/fosscord/fosscord-server/issues" | ||||
| 	}, | ||||
|  | ||||
| @ -3,8 +3,13 @@ const cluster = require("cluster"); | ||||
| const WebSocket = require("ws"); | ||||
| const endpoint = process.env.GATEWAY || "ws://localhost:3001"; | ||||
| const connections = Number(process.env.CONNECTIONS) || 50; | ||||
| const threads = Number(process.env.THREADS) || require("os").cpus().length || 1; | ||||
| const token = process.env.TOKEN; | ||||
| var cores = 1; | ||||
| try { | ||||
| 	cores = Number(process.env.THREADS) || os.cpus().length; | ||||
| } catch { | ||||
| 	console.log("[Bundle] Failed to get thread count! Using 1...") | ||||
| } | ||||
| 
 | ||||
| if (!token) { | ||||
| 	console.error("TOKEN env var missing"); | ||||
|  | ||||
| @ -9,7 +9,12 @@ config(); | ||||
| import { execSync } from "child_process"; | ||||
| 
 | ||||
| // TODO: add socket event transmission
 | ||||
| let cores = Number(process.env.THREADS) || os.cpus().length; | ||||
| var cores = 1; | ||||
| try { | ||||
| 	cores = Number(process.env.THREADS) || os.cpus().length; | ||||
| } catch { | ||||
| 	console.log("[API] Failed to get thread count! Using 1...") | ||||
| } | ||||
| 
 | ||||
| if (cluster.isMaster) { | ||||
| 	function getCommitOrFail() { | ||||
|  | ||||
| @ -4,7 +4,13 @@ import { red } from "picocolors"; | ||||
| 
 | ||||
| export function initStats() { | ||||
| 	console.log(`[Path] running in ${__dirname}`); | ||||
| 	console.log(`[CPU] ${osu.cpu.model()} Cores x${osu.cpu.count()}`); | ||||
| 	try { | ||||
| 		console.log(`[CPU] ${osu.cpu.model()} Cores x${osu.cpu.count()}`); | ||||
| 	} | ||||
| 	catch { | ||||
| 		console.log('[CPU] Failed to get cpu model!') | ||||
| 	} | ||||
| 	 | ||||
| 	console.log(`[System] ${os.platform()} ${os.arch()}`); | ||||
| 	console.log(`[Process] running with PID: ${process.pid}`); | ||||
| 	if (process.getuid && process.getuid() === 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": "AGPL-3.0-only", | ||||
| 	"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": "AGPL-3.0-only", | ||||
| 	"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": "AGPL-3.0-only", | ||||
| 	"devDependencies": { | ||||
| 		"@types/amqplib": "^0.8.1", | ||||
| 		"@types/jsonwebtoken": "^8.5.0", | ||||
|  | ||||
| @ -29,8 +29,8 @@ const experiments: any = []; | ||||
| import { check } from "./instanceOf"; | ||||
| import { Recipient } from "@fosscord/util"; | ||||
| 
 | ||||
| // TODO: bot sharding
 | ||||
| // TODO: check priviliged intents
 | ||||
| // TODO: user sharding
 | ||||
| // TODO: check privileged intents, if defined in the config
 | ||||
| // TODO: check if already identified
 | ||||
| 
 | ||||
| export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| @ -87,7 +87,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 				user_id: this.user_id, | ||||
| 				session_id: session_id, | ||||
| 				// TODO: check if status is only one of: online, dnd, offline, idle
 | ||||
| 				status: identify.presence?.status || "online", //does the session always start as online?
 | ||||
| 				status: identify.presence?.status || "offline", //does the session always start as online?
 | ||||
| 				client_info: { | ||||
| 					//TODO read from identity
 | ||||
| 					client: "desktop", | ||||
| @ -101,7 +101,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 
 | ||||
| 	if (!user) return this.close(CLOSECODES.Authentication_failed); | ||||
| 
 | ||||
| 	if (!identify.intents) identify.intents = BigInt("0b11111111111111"); | ||||
| 	if (!identify.intents) identify.intents = BigInt("0x6ffffffff"); | ||||
| 	this.intents = new Intents(identify.intents); | ||||
| 	if (identify.shard) { | ||||
| 		this.shard_id = identify.shard[0]; | ||||
| @ -271,7 +271,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 		guild_join_requests: [], // TODO what is this?
 | ||||
| 		users: users.filter((x) => x).unique(), | ||||
| 		merged_members: merged_members, | ||||
| 		// shard // TODO: only for bots sharding
 | ||||
| 		// shard // TODO: only for user sharding
 | ||||
| 	}; | ||||
| 
 | ||||
| 	// TODO: send real proper data structure
 | ||||
|  | ||||
							
								
								
									
										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": "AGPL-3.0-only", | ||||
| 	"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/>. | ||||
							
								
								
									
										2980
									
								
								util/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2980
									
								
								util/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -23,7 +23,7 @@ | ||||
| 		"discord-open-source" | ||||
| 	], | ||||
| 	"author": "Fosscord", | ||||
| 	"license": "GPLV3", | ||||
| 	"license": "AGPL-3.0-only", | ||||
| 	"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[]; | ||||
| 
 | ||||
| } | ||||
| @ -727,21 +727,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), | ||||
| @ -787,3 +789,4 @@ function keyMirror(arr: string[]) { | ||||
| 	for (const value of arr) tmp[value] = value; | ||||
| 	return tmp; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -13,7 +13,12 @@ export function adjustEmail(email?: string): string | undefined { | ||||
| 	// TODO: check accounts with uncommon email domains
 | ||||
| 	if (domain === "gmail.com" || domain === "googlemail.com") { | ||||
| 		// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
 | ||||
| 		return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com"; | ||||
| 		let v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com"; | ||||
| 	} | ||||
| 	 | ||||
| 	if (domain === "google.com") { | ||||
| 		// replace .dots and +alternatives -> Google Staff GMail Dot Trick
 | ||||
| 		let v = user.replace(/[.]|(\+.*)/g, "") + "@google.com"; | ||||
| 	} | ||||
| 
 | ||||
| 	return email; | ||||
|  | ||||
| @ -2,20 +2,33 @@ import { BitField } from "./BitField"; | ||||
| 
 | ||||
| export class Intents extends BitField { | ||||
| 	static FLAGS = { | ||||
| 		GUILDS: BigInt(1) << BigInt(0), | ||||
| 		GUILD_MEMBERS: BigInt(1) << BigInt(1), | ||||
| 		GUILD_BANS: BigInt(1) << BigInt(2), | ||||
| 		GUILD_EMOJIS: BigInt(1) << BigInt(3), | ||||
| 		GUILD_INTEGRATIONS: BigInt(1) << BigInt(4), | ||||
| 		GUILD_WEBHOOKS: BigInt(1) << BigInt(5), | ||||
| 		GUILD_INVITES: BigInt(1) << BigInt(6), | ||||
| 		GUILD_VOICE_STATES: BigInt(1) << BigInt(7), | ||||
| 		GUILD_PRESENCES: BigInt(1) << BigInt(8), | ||||
| 		GUILD_MESSAGES: BigInt(1) << BigInt(9), | ||||
| 		GUILD_MESSAGE_REACTIONS: BigInt(1) << BigInt(10), | ||||
| 		GUILD_MESSAGE_TYPING: BigInt(1) << BigInt(11), | ||||
| 		DIRECT_MESSAGES: BigInt(1) << BigInt(12), | ||||
| 		DIRECT_MESSAGE_REACTIONS: BigInt(1) << BigInt(13), | ||||
| 		DIRECT_MESSAGE_TYPING: BigInt(1) << BigInt(14), | ||||
| 		GUILDS: BigInt(1) << BigInt(0), // guilds and guild merge-split events affecting the user
 | ||||
| 		GUILD_MEMBERS: BigInt(1) << BigInt(1), // memberships
 | ||||
| 		GUILD_BANS: BigInt(1) << BigInt(2), // bans and ban lists
 | ||||
| 		GUILD_EMOJIS: BigInt(1) << BigInt(3), // custom emojis
 | ||||
| 		GUILD_INTEGRATIONS: BigInt(1) << BigInt(4), // applications
 | ||||
| 		GUILD_WEBHOOKS: BigInt(1) << BigInt(5), // webhooks
 | ||||
| 		GUILD_INVITES: BigInt(1) << BigInt(6), // mass invites (no user can receive user specific invites of another user)
 | ||||
| 		GUILD_VOICE_STATES: BigInt(1) << BigInt(7), // voice updates
 | ||||
| 		GUILD_PRESENCES: BigInt(1) << BigInt(8), // presence updates
 | ||||
| 		GUILD_MESSAGES_METADATA: BigInt(1) << BigInt(9), // guild message metadata
 | ||||
| 		GUILD_MESSAGE_REACTIONS: BigInt(1) << BigInt(10), // guild message reactions
 | ||||
| 		GUILD_MESSAGE_TYPING: BigInt(1) << BigInt(11), // guild channel typing notifications
 | ||||
| 		DIRECT_MESSAGES: BigInt(1) << BigInt(12), // DM or orphan channels
 | ||||
| 		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
 | ||||
| 		JUMBO_EVENTS: BigInt(1) << BigInt(43), // jumbo events (size limits to be defined later)
 | ||||
| 		LOBBIES: BigInt(1) << BigInt(44), // lobbies
 | ||||
| 		INSTANCE_ROUTES: BigInt(1) << BigInt(60), // all message route changes 
 | ||||
| 		INSTANCE_GUILD_CHANGES: BigInt(1) << BigInt(61), // all guild create, guild object patch, split, merge and delete events
 | ||||
| 		INSTANCE_POLICY_UPDATES: BigInt(1) << BigInt(62), // all instance policy updates
 | ||||
| 		INSTANCE_USER_UPDATES: BigInt(1) << BigInt(63) // all instance user updates
 | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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
 | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| @ -71,6 +71,8 @@ export class Rights extends BitField { | ||||
| 		INITIATE_INTERACTIONS: BitFlag(40), // can initiate interactions
 | ||||
| 		RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions
 | ||||
| 		SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events
 | ||||
| 		USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request — can accept mass invites
 | ||||
| 		ACCEPT_INVITES: BitFlag(44) // added per @xnacly's request — can accept user-specific invites and DM requests
 | ||||
| 	}; | ||||
| 
 | ||||
| 	any(permission: RightResolvable, checkOperator = true) { | ||||
|  | ||||
| @ -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": "AGPL-3.0-only", | ||||
| 	"devDependencies": { | ||||
| 		"@types/node": "^15.6.1", | ||||
| 		"@types/ws": "^7.4.4", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Madeline
						Madeline