1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-25 11:43:07 +01:00

refactor channel positions in guild

This commit is contained in:
Madeline 2023-10-04 23:21:50 +11:00
parent 551d2bf5e6
commit 7795f1b36b
No known key found for this signature in database
GPG Key ID: 1958E017C36F2E47
9 changed files with 317 additions and 68 deletions

View File

@ -22,10 +22,10 @@ import {
ChannelModifySchema, ChannelModifySchema,
ChannelReorderSchema, ChannelReorderSchema,
ChannelUpdateEvent, ChannelUpdateEvent,
Guild,
emitEvent, emitEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.get( router.get(
@ -96,44 +96,72 @@ router.patch(
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as ChannelReorderSchema; const body = req.body as ChannelReorderSchema;
await Promise.all([ const guild = await Guild.findOneOrFail({
body.map(async (x) => { where: { id: guild_id },
if (x.position == null && !x.parent_id) select: { channelOrdering: true },
throw new HTTPError( });
`You need to at least specify position or parent_id`,
400, // The channels not listed for this query
const notMentioned = guild.channelOrdering.filter(
(x) => !body.find((c) => c.id == x),
); );
const opts: Partial<Channel> = {}; const withParents = body.filter((x) => x.parent_id != undefined);
if (x.position != null) opts.position = x.position; const withPositions = body.filter((x) => x.position != undefined);
if (x.parent_id) { await Promise.all(
opts.parent_id = x.parent_id; withPositions.map(async (opt) => {
const parent_channel = await Channel.findOneOrFail({
where: { id: x.parent_id, guild_id },
select: ["permission_overwrites"],
});
if (x.lock_permissions) {
opts.permission_overwrites =
parent_channel.permission_overwrites;
}
}
await Channel.update({ guild_id, id: x.id }, opts);
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({
where: { guild_id, id: x.id }, where: { id: opt.id },
}); });
channel.position = opt.position as number;
notMentioned.splice(opt.position as number, 0, channel.id);
await emitEvent({ await emitEvent({
event: "CHANNEL_UPDATE", event: "CHANNEL_UPDATE",
data: channel, data: channel,
channel_id: x.id, channel_id: channel.id,
guild_id, guild_id,
} as ChannelUpdateEvent); } as ChannelUpdateEvent);
}), }),
);
// have to do the parents after the positions
await Promise.all(
withParents.map(async (opt) => {
const [channel, parent] = await Promise.all([
Channel.findOneOrFail({
where: { id: opt.id },
}),
Channel.findOneOrFail({
where: { id: opt.parent_id as string },
select: { permission_overwrites: true },
}),
]); ]);
res.sendStatus(204); if (opt.lock_permissions)
await Channel.update(
{ id: channel.id },
{ permission_overwrites: parent.permission_overwrites },
);
const parentPos = notMentioned.indexOf(parent.id);
notMentioned.splice(parentPos + 1, 0, channel.id);
channel.position = (parentPos + 1) as number;
await emitEvent({
event: "CHANNEL_UPDATE",
data: channel,
channel_id: channel.id,
guild_id,
} as ChannelUpdateEvent);
}),
);
await Guild.update({ id: guild_id }, { channelOrdering: notMentioned });
return res.sendStatus(204);
}, },
); );

View File

@ -161,12 +161,6 @@ router.patch(
guild.assign(body); guild.assign(body);
if (body.public_updates_channel_id == "1") { if (body.public_updates_channel_id == "1") {
// move all channels up 1
await Channel.createQueryBuilder("channels")
.where({ guild: { id: guild_id } })
.update({ position: () => "position + 1" })
.execute();
// create an updates channel for them // create an updates channel for them
const channel = await Channel.createChannel( const channel = await Channel.createChannel(
{ {
@ -188,6 +182,8 @@ router.patch(
{ skipPermissionCheck: true }, { skipPermissionCheck: true },
); );
await Guild.insertChannelInOrder(guild.id, channel.id, 0, guild);
guild.public_updates_channel_id = channel.id; guild.public_updates_channel_id = channel.id;
} else if (body.public_updates_channel_id != undefined) { } else if (body.public_updates_channel_id != undefined) {
// ensure channel exists in this guild // ensure channel exists in this guild
@ -198,12 +194,6 @@ router.patch(
} }
if (body.rules_channel_id == "1") { if (body.rules_channel_id == "1") {
// move all channels up 1
await Channel.createQueryBuilder("channels")
.where({ guild: { id: guild_id } })
.update({ position: () => "position + 1" })
.execute();
// create a rules for them // create a rules for them
const channel = await Channel.createChannel( const channel = await Channel.createChannel(
{ {
@ -225,6 +215,8 @@ router.patch(
{ skipPermissionCheck: true }, { skipPermissionCheck: true },
); );
await Guild.insertChannelInOrder(guild.id, channel.id, 0, guild);
guild.rules_channel_id = channel.id; guild.rules_channel_id = channel.id;
} else if (body.rules_channel_id != undefined) { } else if (body.rules_channel_id != undefined) {
// ensure channel exists in this guild // ensure channel exists in this guild

View File

@ -77,12 +77,7 @@ router.get(
// Fetch voice channels, and the @everyone permissions object // Fetch voice channels, and the @everyone permissions object
const channels: { id: string; name: string; position: number }[] = []; const channels: { id: string; name: string; position: number }[] = [];
( (await Channel.getOrderedChannels(guild.id, guild)).filter((doc) => {
await Channel.find({
where: { guild_id: guild_id, type: 2 },
order: { position: "ASC" },
})
).filter((doc) => {
// Only return channels where @everyone has the CONNECT permission // Only return channels where @everyone has the CONNECT permission
if ( if (
doc.permission_overwrites === undefined || doc.permission_overwrites === undefined ||

View File

@ -54,6 +54,7 @@ import {
UserSettings, UserSettings,
checkToken, checkToken,
emitEvent, emitEvent,
getDatabase,
} from "@spacebar/util"; } from "@spacebar/util";
import { check } from "./instanceOf"; import { check } from "./instanceOf";
@ -167,7 +168,12 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// guild channels, emoji, roles, stickers // guild channels, emoji, roles, stickers
// but we do want almost everything from guild. // but we do want almost everything from guild.
// How do you do that without just enumerating the guild props? // How do you do that without just enumerating the guild props?
guild: true, guild: Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getDatabase()!
.getMetadata(Guild)
.columns.map((x) => [x.propertyName, true]),
),
}, },
relations: [ relations: [
"guild", "guild",
@ -253,7 +259,8 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const guilds: GuildOrUnavailable[] = members.map((member) => { const guilds: GuildOrUnavailable[] = members.map((member) => {
// filter guild channels we don't have permission to view // filter guild channels we don't have permission to view
// TODO: check if this causes issues when the user is granted other roles? // TODO: check if this causes issues when the user is granted other roles?
member.guild.channels = member.guild.channels.filter((channel) => { member.guild.channels = member.guild.channels
.filter((channel) => {
const perms = Permissions.finalPermission({ const perms = Permissions.finalPermission({
user: { user: {
id: member.id, id: member.id,
@ -264,7 +271,14 @@ export async function onIdentify(this: WebSocket, data: Payload) {
}); });
return perms.has("VIEW_CHANNEL"); return perms.has("VIEW_CHANNEL");
}); })
.map((channel) => {
channel.position = member.guild.channelOrdering.indexOf(
channel.id,
);
return channel;
})
.sort((a, b) => a.position - b.position);
if (user.bot) { if (user.bot) {
pending_guilds.push(member.guild); pending_guilds.push(member.guild);

View File

@ -126,9 +126,6 @@ export class Channel extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
default_auto_archive_duration?: number; default_auto_archive_duration?: number;
@Column({ nullable: true })
position?: number;
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
permission_overwrites?: ChannelPermissionOverwrite[]; permission_overwrites?: ChannelPermissionOverwrite[];
@ -193,6 +190,9 @@ export class Channel extends BaseClass {
@Column() @Column()
default_thread_rate_limit_per_user: number = 0; default_thread_rate_limit_per_user: number = 0;
/** Must be calculated Channel.calculatePosition */
position: number;
// TODO: DM channel // TODO: DM channel
static async createChannel( static async createChannel(
channel: Partial<Channel>, channel: Partial<Channel>,
@ -211,10 +211,16 @@ export class Channel extends BaseClass {
permissions.hasThrow("MANAGE_CHANNELS"); permissions.hasThrow("MANAGE_CHANNELS");
} }
if (!opts?.skipNameChecks) {
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id }, where: { id: channel.guild_id },
select: {
features: !opts?.skipNameChecks,
channelOrdering: true,
id: true,
},
}); });
if (!opts?.skipNameChecks) {
if ( if (
!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && !guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") &&
channel.name channel.name
@ -293,14 +299,15 @@ export class Channel extends BaseClass {
if (!channel.permission_overwrites) channel.permission_overwrites = []; if (!channel.permission_overwrites) channel.permission_overwrites = [];
// TODO: eagerly auto generate position of all guild channels // TODO: eagerly auto generate position of all guild channels
const position =
(channel.type === ChannelType.UNHANDLED ? 0 : channel.position) ||
0;
channel = { channel = {
...channel, ...channel,
...(!opts?.keepId && { id: Snowflake.generate() }), ...(!opts?.keepId && { id: Snowflake.generate() }),
created_at: new Date(), created_at: new Date(),
position: position,
(channel.type === ChannelType.UNHANDLED
? 0
: channel.position) || 0,
}; };
const ret = Channel.create(channel); const ret = Channel.create(channel);
@ -314,6 +321,7 @@ export class Channel extends BaseClass {
guild_id: channel.guild_id, guild_id: channel.guild_id,
} as ChannelCreateEvent) } as ChannelCreateEvent)
: Promise.resolve(), : Promise.resolve(),
Guild.insertChannelInOrder(guild.id, ret.id, position, guild),
]); ]);
return ret; return ret;
@ -456,6 +464,40 @@ export class Channel extends BaseClass {
await Channel.delete({ id: channel.id }); await Channel.delete({ id: channel.id });
} }
static async calculatePosition(
channel_id: string,
guild_id: string,
guild?: Guild,
) {
if (!guild)
guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: { channelOrdering: true },
});
return guild.channelOrdering.findIndex((id) => channel_id == id);
}
static async getOrderedChannels(guild_id: string, guild?: Guild) {
if (!guild)
guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: { channelOrdering: true },
});
const channels = await Promise.all(
guild.channelOrdering.map((id) =>
Channel.findOneOrFail({ where: { id } }),
),
);
return channels.reduce((r, v) => {
v.position = (guild as Guild).channelOrdering.indexOf(v.id);
r[v.position] = v;
return r;
}, [] as Array<Channel>);
}
isDm() { isDm() {
return ( return (
this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM

View File

@ -297,6 +297,9 @@ export class Guild extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
premium_progress_bar_enabled: boolean = false; premium_progress_bar_enabled: boolean = false;
@Column({ select: false, type: "simple-array" })
channelOrdering: string[];
static async createGuild(body: { static async createGuild(body: {
name?: string; name?: string;
icon?: string | null; icon?: string | null;
@ -324,6 +327,7 @@ export class Guild extends BaseClass {
description: "", description: "",
welcome_channels: [], welcome_channels: [],
}, },
channelOrdering: [],
afk_timeout: Config.get().defaults.guild.afkTimeout, afk_timeout: Config.get().defaults.guild.afkTimeout,
default_message_notifications: default_message_notifications:
@ -376,7 +380,7 @@ export class Guild extends BaseClass {
const parent_id = ids.get(channel.parent_id); const parent_id = ids.get(channel.parent_id);
await Channel.createChannel( const saved = await Channel.createChannel(
{ ...channel, guild_id, id, parent_id }, { ...channel, guild_id, id, parent_id },
body.owner_id, body.owner_id,
{ {
@ -386,15 +390,69 @@ export class Guild extends BaseClass {
skipEventEmit: true, skipEventEmit: true,
}, },
); );
await Guild.insertChannelInOrder(
guild.id,
saved.id,
parent_id ?? channel.position ?? 0,
guild,
);
} }
return guild; return guild;
} }
toJSON() { /** Insert a channel into the guild ordering by parent channel id or position */
static async insertChannelInOrder(
guild_id: string,
channel_id: string,
position: number,
guild?: Guild,
): Promise<number>;
static async insertChannelInOrder(
guild_id: string,
channel_id: string,
parent_id: string,
guild?: Guild,
): Promise<number>;
static async insertChannelInOrder(
guild_id: string,
channel_id: string,
insertPoint: string | number,
guild?: Guild,
): Promise<number>;
static async insertChannelInOrder(
guild_id: string,
channel_id: string,
insertPoint: string | number,
guild?: Guild,
): Promise<number> {
if (!guild)
guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: { channelOrdering: true },
});
let position;
if (typeof insertPoint == "string")
position = guild.channelOrdering.indexOf(insertPoint) + 1;
else position = insertPoint;
guild.channelOrdering.remove(channel_id);
guild.channelOrdering.splice(position, 0, channel_id);
await Guild.update(
{ id: guild_id },
{ channelOrdering: guild.channelOrdering },
);
return position;
}
toJSON(): Guild {
return { return {
...this, ...this,
unavailable: this.unavailable == false ? undefined : true, unavailable: this.unavailable == false ? undefined : true,
channelOrdering: undefined,
}; };
} }
} }

View File

@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class guildChannelOrdering1696420827239 implements MigrationInterface {
name = "guildChannelOrdering1696420827239";
public async up(queryRunner: QueryRunner): Promise<void> {
const guilds = await queryRunner.query(
`SELECT id FROM guilds`,
undefined,
true,
);
await queryRunner.query(
`ALTER TABLE guilds ADD channelOrdering text NOT NULL DEFAULT '[]'`,
);
for (const guild_id of guilds.records.map((x) => x.id)) {
const channels: Array<{ position: number; id: string }> = (
await queryRunner.query(
`SELECT id, position FROM channels WHERE guild_id = ?`,
[guild_id],
true,
)
).records;
channels.sort((a, b) => a.position - b.position);
await queryRunner.query(
`UPDATE guilds SET channelOrdering = ? WHERE id = ?`,
[JSON.stringify(channels.map((x) => x.id)), guild_id],
);
}
await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`);
}
public async down(): Promise<void> {
// don't care actually, sorry.
}
}

View File

@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class guildChannelOrdering1696420827239 implements MigrationInterface {
name = "guildChannelOrdering1696420827239";
public async up(queryRunner: QueryRunner): Promise<void> {
const guilds = await queryRunner.query(
`SELECT id FROM guilds`,
undefined,
true,
);
await queryRunner.query(
`ALTER TABLE guilds ADD channelOrdering text NOT NULL DEFAULT '[]'`,
);
for (const guild_id of guilds.records.map((x) => x.id)) {
const channels: Array<{ position: number; id: string }> = (
await queryRunner.query(
`SELECT id, position FROM channels WHERE guild_id = ?`,
[guild_id],
true,
)
).records;
channels.sort((a, b) => a.position - b.position);
await queryRunner.query(
`UPDATE guilds SET channelOrdering = ? WHERE id = ?`,
[JSON.stringify(channels.map((x) => x.id)), guild_id],
);
}
await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`);
}
public async down(): Promise<void> {
// don't care actually, sorry.
}
}

View File

@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class guildChannelOrdering1696420827239 implements MigrationInterface {
name = "guildChannelOrdering1696420827239";
public async up(queryRunner: QueryRunner): Promise<void> {
const guilds = await queryRunner.query(
`SELECT id FROM guilds`,
undefined,
true,
);
await queryRunner.query(
`ALTER TABLE guilds ADD channelOrdering text NOT NULL DEFAULT '[]'`,
);
for (const guild_id of guilds.records.map((x) => x.id)) {
const channels: Array<{ position: number; id: string }> = (
await queryRunner.query(
`SELECT id, position FROM channels WHERE guild_id = $1`,
[guild_id],
true,
)
).records;
channels.sort((a, b) => a.position - b.position);
await queryRunner.query(
`UPDATE guilds SET channelOrdering = $1 WHERE id = $2`,
[JSON.stringify(channels.map((x) => x.id)), guild_id],
);
}
await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`);
}
public async down(): Promise<void> {
// don't care actually, sorry.
}
}