summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--api/assets/openapi.json6
-rw-r--r--api/assets/preload-plugins/fosscord-login.js12
-rw-r--r--api/assets/schemas.json4
-rw-r--r--api/src/routes/guilds/#guild_id/vanity-url.ts33
-rw-r--r--api/src/util/handlers/Message.ts10
-rw-r--r--util/src/entities/Channel.ts31
-rw-r--r--util/src/entities/ConnectedAccount.ts4
-rw-r--r--util/src/entities/Member.ts6
-rw-r--r--util/src/interfaces/Interaction.ts2
-rw-r--r--util/src/util/Rights.ts2
10 files changed, 79 insertions, 31 deletions
diff --git a/api/assets/openapi.json b/api/assets/openapi.json
index 03550323..a8a657b2 100644
--- a/api/assets/openapi.json
+++ b/api/assets/openapi.json
@@ -2,7 +2,7 @@
 	"openapi": "3.0.0",
 	"servers": [
 		{
-			"url": "https://api.fosscord.com/v{version}",
+			"url": "https://api.fosscord.com/api/v{version}",
 			"description": "Official fosscord instance",
 			"variables": {
 				"version": {
@@ -2960,7 +2960,7 @@
 					"type": {
 						"type": "string"
 					},
-					"verifie": {
+					"verified": {
 						"type": "boolean"
 					},
 					"visibility": {
@@ -2980,7 +2980,7 @@
 					"type",
 					"user",
 					"user_id",
-					"verifie",
+					"verified",
 					"visibility"
 				]
 			},
diff --git a/api/assets/preload-plugins/fosscord-login.js b/api/assets/preload-plugins/fosscord-login.js
new file mode 100644
index 00000000..38f82200
--- /dev/null
+++ b/api/assets/preload-plugins/fosscord-login.js
@@ -0,0 +1,12 @@
+// Remove `<link id="logincss" rel="stylesheet" href="/assets/fosscord-login.css" />` from header when we're not accessing `/login` or `/register`
+// fosscord-login.css replaces discord's TOS tooltip with something more fitting for fosscord, which when included in the main app, causes other tooltips
+// to be affected, which is potentially unwanted.
+//
+// This script removes fosscord-login.css when a user reloads the page. From testing, it appears fosscord already properly removes
+// fosscord-login.css after login is successful, but not if you reload the page after logging in. This script is to remove fosscord-login.css in
+// that specific case.
+
+var token = JSON.parse(localStorage.getItem("token"));
+if (!token && location.pathname !== "/login" && location.pathname !== "/register") {
+	document.getElementById("logincss").remove();
+}
diff --git a/api/assets/schemas.json b/api/assets/schemas.json
index d531df21..441752ad 100644
--- a/api/assets/schemas.json
+++ b/api/assets/schemas.json
@@ -355,11 +355,11 @@
 					"type": {
 						"type": "string"
 					},
-					"verifie": {
+					"verified": {
 						"type": "boolean"
 					}
 				},
-				"required": ["name", "type", "verifie"]
+				"required": ["name", "type", "verified"]
 			}
 		},
 		"$schema": "http://json-schema.org/draft-07/schema#"
diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts
index 63173345..29cd25e2 100644
--- a/api/src/routes/guilds/#guild_id/vanity-url.ts
+++ b/api/src/routes/guilds/#guild_id/vanity-url.ts
@@ -9,11 +9,19 @@ const InviteRegex = /\W/g;
 
 router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
+	const guild = await Guild.findOneOrFail({ id: guild_id });
 
-	const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } });
-	if (!invite) return res.json({ code: null });
+	if (!guild.features.includes("ALIASABLE_NAMES")) {
+		const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } });
+		if (!invite) return res.json({ code: null });
 
-	return res.json({ code: invite.code, uses: invite.uses });
+		return res.json({ code: invite.code, uses: invite.uses });
+	} else {
+		const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } });
+		if (!invite || invite.length == 0) return res.json({ code: null });
+
+		return res.json(invite.map((x) => ({ code: x.code, uses: x.uses })));
+	}
 });
 
 export interface VanityUrlSchema {
@@ -24,18 +32,33 @@ export interface VanityUrlSchema {
 	code?: string;
 }
 
-// TODO: check if guild is elgible for vanity url
 router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 	const body = req.body as VanityUrlSchema;
 	const code = body.code?.replace(InviteRegex, "");
 
+	const guild = await Guild.findOneOrFail({ id: guild_id });
+	if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls");
+
+	if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty");
+
 	const invite = await Invite.findOne({ code });
 	if (invite) throw new HTTPError("Invite already exists");
 
 	const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT });
 
-	await Invite.update({ vanity_url: true, guild_id }, { code: code, channel_id: id });
+	await new Invite({
+		vanity_url: true,
+		code: code,
+		temporary: false,
+		uses: 0,
+		max_uses: 0,
+		max_age: 0,
+		created_at: new Date(),
+		expires_at: new Date(),
+		guild_id: guild_id,
+		channel_id: id
+	}).save();
 
 	return res.json({ code: code });
 });
diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts
index 21664368..2d9f7032 100644
--- a/api/src/util/handlers/Message.ts
+++ b/api/src/util/handlers/Message.ts
@@ -82,10 +82,12 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
 	if (opts.message_reference) {
 		permission.hasThrow("READ_MESSAGE_HISTORY");
 		// code below has to be redone when we add custom message routing and cross-channel replies
-		const guild = await Guild.findOneOrFail({ id: channel.guild_id });
-		if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
-			if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild");
-			if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
+		if (message.guild_id !== null) {
+			const guild = await Guild.findOneOrFail({ id: channel.guild_id });
+			if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
+				if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild");
+				if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
+			}
 		}
 		// TODO: should be checked if the referenced message exists?
 		// @ts-ignore
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 1cc4a538..08be1e02 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -14,12 +14,12 @@ import { Webhook } from "./Webhook";
 import { DmChannelDTO } from "../dtos";

 

 export enum ChannelType {

-	GUILD_TEXT = 0, // a text channel within a server

+	GUILD_TEXT = 0, // a text channel within a guild

 	DM = 1, // a direct message between users

-	GUILD_VOICE = 2, // a voice channel within a server

+	GUILD_VOICE = 2, // a voice channel within a guild

 	GROUP_DM = 3, // a direct message between multiple users

-	GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels

-	GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server

+	GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels

+	GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route

 	GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord

 	ENCRYPTED = 7, // end-to-end encrypted channel

 	ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel

@@ -72,7 +72,7 @@ export class Channel extends BaseClass {
 	@ManyToOne(() => Channel)

 	parent?: Channel;

 

-	// only for group dms

+	// for group DMs and owned custom channel types

 	@Column({ nullable: true })

 	@RelationId((channel: Channel) => channel.owner)

 	owner_id: string;

@@ -117,6 +117,9 @@ export class Channel extends BaseClass {
 	})

 	invites?: Invite[];

 

+	@Column({ nullable: true })

+	retention_policy_id?: string;

+

 	@OneToMany(() => Message, (message: Message) => message.channel, {

 		cascade: true,

 		orphanedRowAction: "delete",

@@ -140,7 +143,7 @@ export class Channel extends BaseClass {
 		orphanedRowAction: "delete",

 	})

 	webhooks?: Webhook[];

-

+	

 	// TODO: DM channel

 	static async createChannel(

 		channel: Partial<Channel>,

@@ -182,6 +185,7 @@ export class Channel extends BaseClass {
 

 		switch (channel.type) {

 			case ChannelType.GUILD_TEXT:

+			case ChannelType.GUILD_NEWS:

 			case ChannelType.GUILD_VOICE:

 				if (channel.parent_id && !opts?.skipExistsCheck) {

 					const exists = await Channel.findOneOrFail({ id: channel.parent_id });

@@ -191,25 +195,24 @@ export class Channel extends BaseClass {
 				}

 				break;

 			case ChannelType.GUILD_CATEGORY:

+			case ChannelType.UNHANDLED:

 				break;

 			case ChannelType.DM:

 			case ChannelType.GROUP_DM:

 				throw new HTTPError("You can't create a dm channel in a guild");

-			// TODO: check if guild is community server

 			case ChannelType.GUILD_STORE:

-			case ChannelType.GUILD_NEWS:

 			default:

 				throw new HTTPError("Not yet supported");

 		}

 

 		if (!channel.permission_overwrites) channel.permission_overwrites = [];

-		// TODO: auto generate position

+		// TODO: eagerly auto generate position of all guild channels

 

 		channel = {

 			...channel,

 			...(!opts?.keepId && { id: Snowflake.generate() }),

 			created_at: new Date(),

-			position: channel.position || 0,

+			position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0,

 		};

 

 		await Promise.all([

@@ -231,11 +234,13 @@ export class Channel extends BaseClass {
 		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });

 

 		// TODO: check config for max number of recipients

+		/** if you want to disallow note to self channels, uncomment the conditional below

 		if (otherRecipientsUsers.length !== recipients.length) {

 			throw new HTTPError("Recipient/s not found");

 		}

+		**/

 

-		const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;

+		const type = recipients.length > 1 ? ChannelType.DM : ChannelType.GROUP_DM;

 

 		let channel = null;

 

@@ -288,7 +293,8 @@ export class Channel extends BaseClass {
 			await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });

 		}

 

-		return channel_dto.excludedRecipients([creator_user_id]);

+		if (recipients.length === 1) return channel_dto; 

+		else return channel_dto.excludedRecipients([creator_user_id]);

 	}

 

 	static async removeRecipientFromChannel(channel: Channel, user_id: string) {

@@ -354,4 +360,5 @@ export interface ChannelPermissionOverwrite {
 export enum ChannelPermissionOverwriteType {

 	role = 0,

 	member = 1,

+	group = 2,

 }

diff --git a/util/src/entities/ConnectedAccount.ts b/util/src/entities/ConnectedAccount.ts
index b8aa2889..09ae30ab 100644
--- a/util/src/entities/ConnectedAccount.ts
+++ b/util/src/entities/ConnectedAccount.ts
@@ -2,7 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { User } from "./User";
 
-export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verifie"> {}
+export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {}
 
 @Entity("connected_accounts")
 export class ConnectedAccount extends BaseClass {
@@ -35,7 +35,7 @@ export class ConnectedAccount extends BaseClass {
 	type: string;
 
 	@Column()
-	verifie: boolean;
+	verified: boolean;
 
 	@Column({ select: false })
 	visibility: number;
diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index a246b891..928a25d7 100644
--- a/util/src/entities/Member.ts
+++ b/util/src/entities/Member.ts
@@ -85,8 +85,8 @@ export class Member extends BaseClassWithoutId {
 	@Column()
 	joined_at: Date;
 
-	@Column({ type: "bigint", nullable: true })
-	premium_since?: number;
+	@Column()
+	premium_since?: Date;
 
 	@Column()
 	deaf: boolean;
@@ -245,7 +245,7 @@ export class Member extends BaseClassWithoutId {
 			nick: undefined,
 			roles: [guild_id], // @everyone role
 			joined_at: new Date(),
-			premium_since: (new Date()).getTime(),
+			premium_since: new Date(),
 			deaf: false,
 			mute: false,
 			pending: false,
diff --git a/util/src/interfaces/Interaction.ts b/util/src/interfaces/Interaction.ts
index 3cafb2d5..5d3aae24 100644
--- a/util/src/interfaces/Interaction.ts
+++ b/util/src/interfaces/Interaction.ts
@@ -12,11 +12,13 @@ export interface Interaction {
 }
 
 export enum InteractionType {
+	SelfCommand = 0,
 	Ping = 1,
 	ApplicationCommand = 2,
 }
 
 export enum InteractionResponseType {
+	SelfCommandResponse = 0,
 	Pong = 1,
 	Acknowledge = 2,
 	ChannelMessage = 3,
diff --git a/util/src/util/Rights.ts b/util/src/util/Rights.ts
index 9a99d393..db5384d0 100644
--- a/util/src/util/Rights.ts
+++ b/util/src/util/Rights.ts
@@ -65,6 +65,8 @@ export class Rights extends BitField {
 		// inverts the presence confidentiality default (OPERATOR's presence is not routed by default, others' are) for a given user
 		SELF_ADD_DISCOVERABLE: BitFlag(36), // can mark discoverable guilds that they have permissions to mark as discoverable
 		MANAGE_GUILD_DIRECTORY: BitFlag(37), // can change anything in the primary guild directory
+		POGGERS: BitFlag(38), // can send confetti, screenshake, random user mention (@someone)
+		USE_ACHIEVEMENTS: BitFlag(39), // can use achievements and cheers
 		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