fix groups more

This commit is contained in:
MathMan05 2025-11-20 14:34:23 -06:00
parent e5177a8f07
commit c512a76e14
4 changed files with 42 additions and 116 deletions

Binary file not shown.

Binary file not shown.

View File

@ -452,7 +452,6 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const channelUsers = channel.recipients?.map((recipient) => recipient.user.toPublicUser()); const channelUsers = channel.recipients?.map((recipient) => recipient.user.toPublicUser());
if (channelUsers && channelUsers.length > 0) channelUsers.forEach((user) => users.add(user)); if (channelUsers && channelUsers.length > 0) channelUsers.forEach((user) => users.add(user));
return { return {
id: channel.id, id: channel.id,
flags: channel.flags, flags: channel.flags,
@ -462,6 +461,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
icon: channel.icon, icon: channel.icon,
name: channel.name, name: channel.name,
is_spam: false, // TODO is_spam: false, // TODO
owner_id: channel.owner_id,
}; };
}); });
const generateDmChannelsTime = taskSw.getElapsedAndReset(); const generateDmChannelsTime = taskSw.getElapsedAndReset();

View File

@ -17,14 +17,7 @@
*/ */
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
RelationId,
} from "typeorm";
import { DmChannelDTO } from "../dtos"; import { DmChannelDTO } from "../dtos";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, BitField } from "../util"; import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, BitField } from "../util";
@ -135,14 +128,10 @@ export class Channel extends BaseClass {
}) })
messages?: Message[]; messages?: Message[];
@OneToMany( @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
() => VoiceState, cascade: true,
(voice_state: VoiceState) => voice_state.channel, orphanedRowAction: "delete",
{ })
cascade: true,
orphanedRowAction: "delete",
},
)
voice_states?: VoiceState[]; voice_states?: VoiceState[];
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
@ -194,48 +183,22 @@ export class Channel extends BaseClass {
}); });
if (!opts?.skipNameChecks) { if (!opts?.skipNameChecks) {
if ( if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403);
channel.name
) {
for (const character of InvisibleCharacters)
if (channel.name.includes(character))
throw new HTTPError(
"Channel name cannot include invalid characters",
403,
);
// Categories skip these checks on discord.com // Categories skip these checks on discord.com
if ( if (channel.type !== ChannelType.GUILD_CATEGORY || guild.features.includes("IRC_LIKE_CATEGORY_NAMES")) {
channel.type !== ChannelType.GUILD_CATEGORY || if (channel.name.includes(" ")) throw new HTTPError("Channel name cannot include invalid characters", 403);
guild.features.includes("IRC_LIKE_CATEGORY_NAMES")
) {
if (channel.name.includes(" "))
throw new HTTPError(
"Channel name cannot include invalid characters",
403,
);
if (channel.name.match(/--+/g)) if (channel.name.match(/--+/g)) throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403);
throw new HTTPError(
"Channel name cannot include multiple adjacent dashes.",
403,
);
if ( if (channel.name.charAt(0) === "-" || channel.name.charAt(channel.name.length - 1) === "-")
channel.name.charAt(0) === "-" || throw new HTTPError("Channel name cannot start/end with dash.", 403);
channel.name.charAt(channel.name.length - 1) === "-"
)
throw new HTTPError(
"Channel name cannot start/end with dash.",
403,
);
} else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com } else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
} }
if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
if (!channel.name) if (!channel.name) throw new HTTPError("Channel name cannot be empty.", 403);
throw new HTTPError("Channel name cannot be empty.", 403);
} }
} }
@ -247,15 +210,8 @@ export class Channel extends BaseClass {
const exists = await Channel.findOneOrFail({ const exists = await Channel.findOneOrFail({
where: { id: channel.parent_id }, where: { id: channel.parent_id },
}); });
if (!exists) if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
throw new HTTPError( if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild");
"Parent id channel doesn't exist",
400,
);
if (exists.guild_id !== channel.guild_id)
throw new HTTPError(
"The category channel needs to be in the guild",
);
} }
break; break;
case ChannelType.GUILD_CATEGORY: case ChannelType.GUILD_CATEGORY:
@ -272,9 +228,7 @@ 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 = const position = (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0;
(channel.type === ChannelType.UNHANDLED ? 0 : channel.position) ||
0;
channel = { channel = {
...channel, ...channel,
@ -300,11 +254,7 @@ export class Channel extends BaseClass {
return ret; return ret;
} }
static async createDMChannel( static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients: string[],
creator_user_id: string,
name?: string,
) {
recipients = recipients.distinct().filter((x) => x !== creator_user_id); recipients = recipients.distinct().filter((x) => x !== creator_user_id);
// TODO: check config for max number of recipients // TODO: check config for max number of recipients
/** if you want to disallow note to self channels, uncomment the conditional below /** if you want to disallow note to self channels, uncomment the conditional below
@ -315,8 +265,7 @@ export class Channel extends BaseClass {
} }
**/ **/
const type = const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
let channel = null; let channel = null;
@ -346,16 +295,13 @@ export class Channel extends BaseClass {
channel = await Channel.create({ channel = await Channel.create({
name, name,
type, type,
owner_id: undefined, owner_id: creator_user_id,
created_at: new Date(), created_at: new Date(),
last_message_id: undefined, last_message_id: undefined,
recipients: channelRecipients.map((x) => recipients: channelRecipients.map((x) =>
Recipient.create({ Recipient.create({
user_id: x, user_id: x,
closed: !( closed: !(type === ChannelType.GROUP_DM || x === creator_user_id),
type === ChannelType.GROUP_DM ||
x === creator_user_id
),
}), }),
), ),
nsfw: false, nsfw: false,
@ -386,9 +332,7 @@ export class Channel extends BaseClass {
static async removeRecipientFromChannel(channel: Channel, user_id: string) { static async removeRecipientFromChannel(channel: Channel, user_id: string) {
await Recipient.delete({ channel_id: channel.id, user_id: user_id }); await Recipient.delete({ channel_id: channel.id, user_id: user_id });
channel.recipients = channel.recipients?.filter( channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
(r) => r.user_id !== user_id,
);
if (channel.recipients?.length === 0) { if (channel.recipients?.length === 0) {
await Channel.deleteChannel(channel); await Channel.deleteChannel(channel);
@ -440,20 +384,11 @@ export class Channel extends BaseClass {
select: { channel_ordering: true }, select: { channel_ordering: true },
}); });
const updatedOrdering = guild.channel_ordering.filter( const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
(id) => id != channel.id, await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
);
await Guild.update(
{ id: channel.guild_id },
{ channel_ordering: updatedOrdering },
);
} }
static async calculatePosition( static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) {
channel_id: string,
guild_id: string,
guild?: Guild,
) {
if (!guild) if (!guild)
guild = await Guild.findOneOrFail({ guild = await Guild.findOneOrFail({
where: { id: guild_id }, where: { id: guild_id },
@ -470,11 +405,7 @@ export class Channel extends BaseClass {
select: { channel_ordering: true }, select: { channel_ordering: true },
}); });
const channels = await Promise.all( const channels = await Promise.all(guild.channel_ordering.map((id) => Channel.findOne({ where: { id } })));
guild.channel_ordering.map((id) =>
Channel.findOne({ where: { id } }),
),
);
return channels return channels
.filter((channel) => channel !== null) .filter((channel) => channel !== null)
@ -488,22 +419,16 @@ export class Channel extends BaseClass {
} }
isDm() { isDm() {
return ( return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM
);
} }
// Does the channel support sending messages ( eg categories do not ) // Does the channel support sending messages ( eg categories do not )
isWritable() { isWritable() {
const disallowedChannelTypes = [ const disallowedChannelTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STAGE_VOICE, ChannelType.VOICELESS_WHITEBOARD];
ChannelType.GUILD_CATEGORY,
ChannelType.GUILD_STAGE_VOICE,
ChannelType.VOICELESS_WHITEBOARD,
];
return disallowedChannelTypes.indexOf(this.type) == -1; return disallowedChannelTypes.indexOf(this.type) == -1;
} }
async getUserPermissions(opts: {user_id?: string, user?: User, member?: Member, guild?: Guild}): Promise<Permissions> { async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise<Permissions> {
let guild = opts.guild; let guild = opts.guild;
if (!guild) { if (!guild) {
if (this.guild) guild = this.guild; if (this.guild) guild = this.guild;
@ -523,20 +448,21 @@ export class Channel extends BaseClass {
let member = opts.member; let member = opts.member;
if (!member) { if (!member) {
if (opts.user) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user.id }, relations: [ "roles" ] }); if (opts.user) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user.id }, relations: ["roles"] });
else if (opts.user_id) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user_id }, relations: [ "roles" ] }); else if (opts.user_id) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user_id }, relations: ["roles"] });
else { else {
console.error("Channel.getUserPermissions: called without user or member for non-DM channel."); console.error("Channel.getUserPermissions: called without user or member for non-DM channel.");
return Permissions.NONE; return Permissions.NONE;
} }
} }
const roles = (member.roles || (await Member.findOneOrFail({ where: { guild_id: guild.id, index: member.index }, relations: [ "roles" ] })).roles) const roles = (member.roles || (await Member.findOneOrFail({ where: { guild_id: guild.id, index: member.index }, relations: ["roles"] })).roles).sort(
.sort((a, b) => a.position - b.position); // ascending by position (a, b) => a.position - b.position,
); // ascending by position
// calculate user's channel perms - should in theory match https://docs.discord.food/topics/permissions#permission-overwrites // calculate user's channel perms - should in theory match https://docs.discord.food/topics/permissions#permission-overwrites
// start at role permissions // start at role permissions
let userPerms = new Permissions(new BitField(0).add(roles.map(r => r.permissions))); let userPerms = new Permissions(new BitField(0).add(roles.map((r) => r.permissions)));
// TODO: do we want to have an instance-wide opt out of this behavior? It would just be an extra if statement here // TODO: do we want to have an instance-wide opt out of this behavior? It would just be an extra if statement here
if (userPerms.has(Permissions.FLAGS.ADMINISTRATOR)) return userPerms; if (userPerms.has(Permissions.FLAGS.ADMINISTRATOR)) return userPerms;
@ -544,11 +470,11 @@ export class Channel extends BaseClass {
// apply channel overrides // apply channel overrides
if (this.permission_overwrites) { if (this.permission_overwrites) {
// role overwrites - TODO: this probably violates the geneva conventions - we should probably be ordering roles here // role overwrites - TODO: this probably violates the geneva conventions - we should probably be ordering roles here
for (const overwrite of this.permission_overwrites.filter(o => o.type === ChannelPermissionOverwriteType.role && roles.map(r => r.id).includes(o.id))) for (const overwrite of this.permission_overwrites.filter((o) => o.type === ChannelPermissionOverwriteType.role && roles.map((r) => r.id).includes(o.id)))
userPerms = new Permissions(userPerms.remove(overwrite.deny).add(overwrite.allow)); userPerms = new Permissions(userPerms.remove(overwrite.deny).add(overwrite.allow));
// member overwrite, throws if somehow we have multiple overwrites for the same member // member overwrite, throws if somehow we have multiple overwrites for the same member
const memberOverwrite = this.permission_overwrites.single(o => o.type === ChannelPermissionOverwriteType.member && o.id === member?.id); const memberOverwrite = this.permission_overwrites.single((o) => o.type === ChannelPermissionOverwriteType.member && o.id === member?.id);
if (memberOverwrite) userPerms = new Permissions(userPerms.remove(memberOverwrite.deny).add(memberOverwrite.allow)); if (memberOverwrite) userPerms = new Permissions(userPerms.remove(memberOverwrite.deny).add(memberOverwrite.allow));
} }
@ -556,8 +482,8 @@ export class Channel extends BaseClass {
} }
// TODO: should we throw for missing args? // TODO: should we throw for missing args?
async canViewChannel(opts: {user_id?: string, user?: User, member?: Member, guild?: Guild}): Promise<boolean> { async canViewChannel(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise<boolean> {
if(this.isDm()) return await this.canViewDmChannel(opts.user_id, opts.user); if (this.isDm()) return await this.canViewDmChannel(opts.user_id, opts.user);
const userPerms = await this.getUserPermissions(opts); const userPerms = await this.getUserPermissions(opts);
return userPerms.has("VIEW_CHANNEL"); return userPerms.has("VIEW_CHANNEL");
@ -570,9 +496,9 @@ export class Channel extends BaseClass {
return false; return false;
} }
if (!user) return false; if (!user) return false;
if (this.recipients) if (this.recipients) return this.recipients.some((r) => r.user_id === user.id && !r.closed);
return this.recipients.some((r) => r.user_id === user.id && !r.closed); else {
else { // we dont have recipients on hand // we dont have recipients on hand
const recipient = await Recipient.findOne({ where: { channel_id: this.id, user_id: user.id } }); const recipient = await Recipient.findOne({ where: { channel_id: this.id, user_id: user.id } });
return recipient == null ? false : !recipient.closed; return recipient == null ? false : !recipient.closed;
} }