Thread members ported from #876

This commit is contained in:
Rory&
2026-01-29 20:32:34 +01:00
parent 5c593e8b0d
commit 0edceb8059
4 changed files with 234 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2026 Spacebar and Spacebar 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/>.
*/
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm";
import { ThreadMembersUpdateEvent } from "../interfaces";
import { emitEvent } from "../util";
import { BaseClassWithoutId } from "./BaseClass";
import { Channel } from "./Channel";
import { HTTPError } from "lambert-server";
import { Member } from "./Member";
// TODO: move
interface ThreadMemberMuteConfig {
end_time?: Date;
selected_time_window?: number;
}
// TODO: move
export enum ThreadMemberFlags {
NONE = 0,
HAS_INTERACTED = 1 << 0,
ALL_MESSAGES = 1 << 1,
ONLY_MENTIONS = 1 << 2,
NO_MESSAGES = 1 << 3,
}
@Entity("thread_members")
@Index(["id", "member_idx"], { unique: true })
export class ThreadMember extends BaseClassWithoutId {
@PrimaryGeneratedColumn()
index: string;
@Column()
@RelationId((member: ThreadMember) => member.channel)
id: string;
@JoinColumn({ name: "id" })
@ManyToOne(() => Channel, {
onDelete: "CASCADE",
})
channel: Channel;
@Column()
@RelationId((member: ThreadMember) => member.member)
member_idx: string;
@JoinColumn({ name: "member_idx" })
@ManyToOne(() => Member, {
onDelete: "CASCADE",
})
member: Member;
@Column()
join_timestamp: Date;
@Column()
muted: boolean;
@Column({ nullable: true, type: "simple-json" })
mute_config?: ThreadMemberMuteConfig;
@Column()
flags: ThreadMemberFlags;
static async IsInThreadOrFail(member_id: string, thread_id: string) {
if (await ThreadMember.count({ where: { id: thread_id, member_idx: member_id } })) return true;
throw new HTTPError("You are not member of this thread", 403);
}
static async removeFromThread(member_id: string, thread_id: string) {
const channel = await Channel.findOneOrFail({ where: { id: thread_id } });
if (
!(await ThreadMember.count({
where: {
id: thread_id,
member_idx: member_id,
},
}))
)
throw new HTTPError("You are not member of this thread", 403);
// // use promise all to execute all promises at the same time -> save time
// TODO: check for bugs
if (channel.member_count) channel.member_count--;
return Promise.all([
ThreadMember.delete({
id: thread_id,
member_idx: member_id,
}),
// //Guild.decrement({ id: guild_id }, "member_count", -1),
emitEvent({
event: "THREAD_MEMBERS_UPDATE",
data: {
guild_id: channel.guild_id,
id: channel.id,
member_count: channel.member_count,
removed_member_ids: [member_id],
},
channel_id: thread_id,
} as ThreadMembersUpdateEvent),
]);
}
// static async addRole(user_id: string, guild_id: string, role_id: string) {
// const [member, role] = await Promise.all([
// // @ts-ignore
// Member.findOneOrFail({
// where: { id: user_id, guild_id },
// relations: ["user", "roles"], // we don't want to load the role objects just the ids
// select: ["index"]
// }),
// Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] })
// ]);
// member.roles.push(OrmUtils.mergeDeep(new Role(), { id: role_id }));
// await Promise.all([
// member.save(),
// emitEvent({
// event: "GUILD_MEMBER_UPDATE",
// data: {
// guild_id,
// user: member.user,
// roles: member.roles.map((x) => x.id)
// },
// guild_id
// } as GuildMemberUpdateEvent)
// ]);
// }
// static async removeRole(user_id: string, guild_id: string, role_id: string) {
// const [member] = await Promise.all([
// // @ts-ignore
// Member.findOneOrFail({
// where: { id: user_id, guild_id },
// relations: ["user", "roles"], // we don't want to load the role objects just the ids
// select: ["index"]
// }),
// await Role.findOneOrFail({ where: { id: role_id, guild_id } })
// ]);
// member.roles = member.roles.filter((x) => x.id == role_id);
// await Promise.all([
// member.save(),
// emitEvent({
// event: "GUILD_MEMBER_UPDATE",
// data: {
// guild_id,
// user: member.user,
// roles: member.roles.map((x) => x.id)
// },
// guild_id
// } as GuildMemberUpdateEvent)
// ]);
// }
// static async changeNickname(user_id: string, guild_id: string, nickname: string) {
// const member = await Member.findOneOrFail({
// where: {
// id: user_id,
// guild_id
// },
// relations: ["user"]
// });
// member.nick = nickname;
// await Promise.all([
// member.save(),
// emitEvent({
// event: "GUILD_MEMBER_UPDATE",
// data: {
// guild_id,
// user: member.user,
// nick: nickname
// },
// guild_id
// } as GuildMemberUpdateEvent)
// ]);
// }
}

View File

@@ -56,6 +56,7 @@ export * from "./StreamSession";
export * from "./Team";
export * from "./TeamMember";
export * from "./Template";
export * from "./ThreadMember";
export * from "./User";
export * from "./UserSettings";
export * from "./UserSettingsProtos";

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ThreadMembers1769653303971 implements MigrationInterface {
name = "ThreadMembers1769653303971";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "thread_members" ("index" SERIAL NOT NULL, "id" character varying NOT NULL, "member_id" integer NOT NULL, "join_timestamp" TIMESTAMP NOT NULL, "muted" boolean NOT NULL, "mute_config" text, "flags" integer NOT NULL, CONSTRAINT "PK_22232a9f7a08fb9967a9c78da53" PRIMARY KEY ("index"))`,
);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bde0970b6a26bdbd83508addd2" ON "thread_members" ("id", "member_id") `);
await queryRunner.query(
`ALTER TABLE "thread_members" ADD CONSTRAINT "FK_cf20e37d71b0e1bf1ab633861c8" FOREIGN KEY ("id") REFERENCES "channels"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "thread_members" ADD CONSTRAINT "FK_606ac45e8756d3440c584477f4e" FOREIGN KEY ("member_id") REFERENCES "members"("index") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "thread_members" DROP CONSTRAINT "FK_606ac45e8756d3440c584477f4e"`);
await queryRunner.query(`ALTER TABLE "thread_members" DROP CONSTRAINT "FK_cf20e37d71b0e1bf1ab633861c8"`);
await queryRunner.query(`DROP INDEX "public"."IDX_bde0970b6a26bdbd83508addd2"`);
await queryRunner.query(`DROP TABLE "thread_members"`);
}
}

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ThreadMembersIdx1769653303972 implements MigrationInterface {
name = "ThreadMembersIdx1769653303972";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE public.thread_members RENAME COLUMN member_id TO member_idx;`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE public.thread_members RENAME COLUMN member_idx TO member_id;`);
}
}