1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-10 20:52:42 +01:00

Implemented DMs and group DMs

This commit is contained in:
AlTech98 2021-09-16 21:33:36 +02:00
parent 0506d40aa1
commit d630f09f80
19 changed files with 342 additions and 79 deletions

View File

@ -1,6 +1,7 @@
import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, ChannelType, ChannelPermissionOverwriteType } from "@fosscord/util";
import { Router, Response, Request } from "express";
import { Channel, ChannelDeleteEvent, ChannelPermissionOverwriteType, ChannelService, ChannelType, ChannelUpdateEvent, emitEvent, Recipient } from "@fosscord/util";
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
const router: Router = Router();
// TODO: delete channel
// TODO: Get channel
@ -16,14 +17,27 @@ router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res:
router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ id: channel_id });
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
// TODO: Dm channel "close" not delete
const data = channel;
if (channel.type === ChannelType.DM) {
const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } })
recipient.closed = true
await Promise.all([
recipient.save(),
emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent)
]);
await Promise.all([emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent), Channel.delete({ id: channel_id })]);
} else if (channel.type === ChannelType.GROUP_DM) {
await ChannelService.removeRecipientFromChannel(channel, req.user_id)
} else {
//TODO messages in this channel should be deleted before deleting the channel
await Promise.all([
Channel.delete({ id: channel_id }),
emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent)
]);
}
res.send(data);
res.send(channel);
});
export interface ChannelModifySchema {

View File

@ -26,7 +26,7 @@ router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Reques
data: {
channel_id,
message_id,
version: 496
version: 3763
}
} as MessageAckEvent);

View File

@ -1,9 +1,8 @@
import { Router, Response, Request } from "express";
import { Attachment, Channel, ChannelType, Embed, getPermission, Message } from "@fosscord/util";
import { Attachment, Channel, ChannelType, DmChannelDTO, Embed, emitEvent, getPermission, Message, MessageCreateEvent } from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api";
import { handleMessage, postHandleMessage, route } from "@fosscord/api";
import multer from "multer";
import { sendMessage } from "@fosscord/api";
import { uploadFile } from "@fosscord/api";
import { FindManyOptions, LessThan, MoreThan } from "typeorm";
@ -62,9 +61,9 @@ router.get("/", async (req: Request, res: Response) => {
if (!channel) throw new HTTPError("Channel not found", 404);
isTextChannel(channel.type);
const around = `${req.query.around}`;
const before = `${req.query.before}`;
const after = `${req.query.after}`;
const around = req.query.around ? `${req.query.around}` : undefined;
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");
@ -151,10 +150,12 @@ router.post(
return res.status(400).json(error);
}
}
//TODO querying the DB at every message post should be avoided, caching maybe?
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] })
const embeds = [];
if (body.embed) embeds.push(body.embed);
const data = await sendMessage({
let message = await handleMessage({
...body,
type: 0,
pinned: false,
@ -162,9 +163,36 @@ router.post(
embeds,
channel_id,
attachments,
edited_timestamp: undefined
edited_timestamp: undefined,
timestamp: new Date()
});
return res.json(data);
message = await message.save()
await channel.assign({ last_message_id: message.id }).save()
if (channel.isDm()) {
const channel_dto = await DmChannelDTO.from(channel)
for (let recipient of channel.recipients!) {
if (recipient.closed) {
await emitEvent({
event: "CHANNEL_CREATE",
data: channel_dto.excludedRecipients([recipient.user_id]),
user_id: recipient.user_id
})
}
}
await Promise.all(channel.recipients!.map(async r => {
r.closed = false;
return await r.save()
}));
}
await emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent)
postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
return res.json(message);
}
);

View File

@ -1,5 +1,57 @@
import { Router, Response, Request } from "express";
import { Request, Response, Router } from "express";
import { Channel, ChannelRecipientAddEvent, ChannelService, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util";
const router: Router = Router();
// TODO:
router.put("/:user_id", async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
if (channel.type !== ChannelType.GROUP_DM) {
const recipients = [
...channel.recipients!.map(r => r.user_id),
user_id
].unique()
const new_channel = await ChannelService.createDMChannel(recipients, req.user_id)
return res.status(201).json(new_channel);
} else {
if (channel.recipients!.map(r => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error?
}
channel.recipients!.push(new Recipient({ channel_id: channel_id, user_id: user_id }));
await channel.save()
await emitEvent({
event: "CHANNEL_CREATE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id
});
await emitEvent({
event: "CHANNEL_RECIPIENT_ADD", data: {
channel_id: channel_id,
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection })
}, channel_id: channel_id
} as ChannelRecipientAddEvent);
return res.sendStatus(204);
}
});
router.delete("/:user_id", async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id)))
throw DiscordApiErrors.MISSING_PERMISSIONS
if (!channel.recipients!.map(r => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error?
}
await ChannelService.removeRecipientFromChannel(channel, user_id)
return res.sendStatus(204);
});
export default router;

View File

@ -19,6 +19,7 @@ router.get("/", route({ response: { body: "UserProfileResponse" } }), async (req
connected_accounts: user.connected_accounts,
premium_guild_since: null, // TODO
premium_since: null, // TODO
mutual_guilds: [], // TODO {id: "", nick: null} when ?with_mutual_guilds=true
user: {
username: user.username,
discriminator: user.discriminator,

View File

@ -1,15 +1,21 @@
import { Router, Request, Response } from "express";
import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent, Recipient } from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { Request, Response, Router } from "express";
import { PublicUserProjection, Recipient, User, ChannelService } from "@fosscord/util";
import { route } from "@fosscord/api";
import { In } from "typeorm";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel"] });
const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel", "user"] });
res.json(recipients.map((x) => x.channel));
//TODO check if this is right
const aa = await Promise.all(recipients.map(async (x) => {
return {
...(x.channel),
recipients: await User.findOneOrFail({ where: { id: x.user_id }, select: PublicUserProjection }),
}
}))
res.json(aa);
});
export interface DmChannelCreateSchema {
@ -19,30 +25,7 @@ export interface DmChannelCreateSchema {
router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => {
const body = req.body as DmChannelCreateSchema;
body.recipients = body.recipients.filter((x) => x !== req.user_id).unique();
const recipients = await User.find({ where: body.recipients.map((x) => ({ id: x })) });
if (recipients.length !== body.recipients.length) {
throw new HTTPError("Recipient/s not found");
}
const type = body.recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
const name = trimSpecial(body.name);
const channel = await new Channel({
name,
type,
// owner_id only for group dm channels
created_at: new Date(),
last_message_id: null,
recipients: [...body.recipients.map((x) => new Recipient({ user_id: x })), new Recipient({ user_id: req.user_id })]
}).save();
await emitEvent({ event: "CHANNEL_CREATE", data: channel, user_id: req.user_id } as ChannelCreateEvent);
res.json(channel);
res.json(await ChannelService.createDMChannel(body.recipients, req.user_id, body.name));
});
export default router;

View File

@ -18,9 +18,19 @@ const router = Router();
const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection];
router.get("/", route({}), async (req: Request, res: Response) => {
const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships"] });
const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships", "relationships.to"] });
return res.json(user.relationships);
//TODO DTO
const related_users = user.relationships.map(r => {
return {
id: r.to.id,
type: r.type,
nickname: null,
user: r.to.toPublicUser(),
}
})
return res.json(related_users);
});
export interface RelationshipPutSchema {
@ -48,7 +58,10 @@ router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request,
await User.findOneOrFail({
relations: ["relationships", "relationships.to"],
select: userProjection,
where: req.body as { discriminator: string; username: string }
where: {
discriminator: String(req.body.discriminator,).padStart(4, '0'), //Discord send the discriminator as integer, we need to add leading zeroes
username: req.body.username
}
}),
req.body.type
);

View File

@ -88,20 +88,17 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const user_guild_settings_entries = members.map((x) => x.settings);
const recipients = await Recipient.find({
where: { user_id: this.user_id },
where: { user_id: this.user_id, closed: false },
relations: ["channel", "channel.recipients", "channel.recipients.user"],
// TODO: public user selection
});
const channels = recipients.map((x) => {
// @ts-ignore
x.channel.recipients = x.channel.recipients?.map((x) => x.user);
// @ts-ignore
users = users.concat(x.channel.recipients);
if (x.channel.type === ChannelType.DM) {
x.channel.recipients = [
// @ts-ignore
x.channel.recipients.find((x) => x.id !== this.user_id),
];
//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
//users = users.concat(x.channel.recipients);
if (x.channel.isDm()) {
x.channel.recipients = x.channel.recipients!.filter((x) => x.id !== this.user_id);
}
return x.channel;
});
@ -111,16 +108,19 @@ export async function onIdentify(this: WebSocket, data: Payload) {
});
if (!user) return this.close(CLOSECODES.Authentication_failed);
const public_user = {
username: user.username,
discriminator: user.discriminator,
id: user.id,
public_flags: user.public_flags,
avatar: user.avatar,
bot: user.bot,
bio: user.bio,
};
users.push(public_user);
for (let relation of user.relationships) {
const related_user = relation.to
const public_related_user = {
username: related_user.username,
discriminator: related_user.discriminator,
id: related_user.id,
public_flags: related_user.public_flags,
avatar: related_user.avatar,
bot: related_user.bot,
bio: related_user.bio,
};
users.push(public_related_user);
}
const session_id = genSessionId();
this.session_id = session_id; //Set the session of the WebSocket object
@ -201,7 +201,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// @ts-ignore
experiments: experiments, // TODO
guild_join_requests: [], // TODO what is this?
users: users.unique(), // TODO
users: users.unique(),
merged_members: merged_members,
// shard // TODO: only for bots sharding
// application // TODO for applications

View File

@ -21,5 +21,6 @@ export default {
8: onRequestGuildMembers,
// 9: Invalid Session
// 10: Hello
// 13: Dm_update
14: onLazyRequest,
};

View File

@ -0,0 +1,35 @@
import { MinimalPublicUserDTO } from "./UserDTO";
import { Channel, PublicUserProjection, User } from "../entities";
export class DmChannelDTO {
icon: string | null;
id: string;
last_message_id: string | null;
name: string | null;
origin_channel_id: string | null;
owner_id?: string;
recipients: MinimalPublicUserDTO[];
type: number;
static async from(channel: Channel, excluded_recipients: string[] = [], origin_channel_id?: string) {
const obj = new DmChannelDTO()
obj.icon = channel.icon || null
obj.id = channel.id
obj.last_message_id = channel.last_message_id || null
obj.name = channel.name || null
obj.origin_channel_id = origin_channel_id || null
obj.owner_id = channel.owner_id
obj.type = channel.type
obj.recipients = (await Promise.all(channel.recipients!.filter(r => !excluded_recipients.includes(r.user_id)).map(async r => {
return await User.findOneOrFail({ where: { id: r.user_id }, select: PublicUserProjection })
}))).map(u => new MinimalPublicUserDTO(u))
return obj
}
excludedRecipients(excluded_recipients: string[]): DmChannelDTO {
return {
...this,
recipients: this.recipients.filter(r => !excluded_recipients.includes(r.id))
}
}
}

17
util/src/dtos/UserDTO.ts Normal file
View File

@ -0,0 +1,17 @@
import { User } from "../entities";
export class MinimalPublicUserDTO {
avatar?: string | null;
discriminator: string;
id: string;
public_flags: number;
username: string;
constructor(user: User) {
this.avatar = user.avatar
this.discriminator = user.discriminator
this.id = user.id
this.public_flags = user.public_flags
this.username = user.username
}
}

2
util/src/dtos/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./DmChannelDTO";
export * from "./UserDTO";

View File

@ -1,7 +1,6 @@
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Message } from "./Message";
import { User } from "./User";
import { HTTPError } from "lambert-server";
import { emitEvent, getPermission, Snowflake } from "../util";
@ -31,6 +30,9 @@ export class Channel extends BaseClass {
@Column({ nullable: true })
name?: string;
@Column({ nullable: true })
icon?: string;
@Column({ type: "simple-enum", enum: ChannelType })
type: ChannelType;
@ -38,13 +40,8 @@ export class Channel extends BaseClass {
recipients?: Recipient[];
@Column({ nullable: true })
@RelationId((channel: Channel) => channel.last_message)
last_message_id: string;
@JoinColumn({ name: "last_message_id" })
@ManyToOne(() => Message)
last_message?: Message;
@Column({ nullable: true })
@RelationId((channel: Channel) => channel.guild)
guild_id?: string;
@ -162,6 +159,10 @@ export class Channel extends BaseClass {
return channel;
}
isDm() {
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM
}
}
export interface ChannelPermissionOverwrite {

View File

@ -19,5 +19,8 @@ export class Recipient extends BaseClass {
@ManyToOne(() => require("./User").User)
user: import("./User").User;
@Column({ default: false })
closed: boolean;
// TODO: settings/mute/nick/added at/encryption keys/read_state
}

View File

@ -124,7 +124,7 @@ export class User extends BaseClass {
flags: string; // UserFlags
@Column()
public_flags: string;
public_flags: number;
@JoinColumn({ name: "relationship_ids" })
@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from)

View File

@ -4,6 +4,8 @@ import "reflect-metadata";
export * from "./util/index";
export * from "./interfaces/index";
export * from "./entities/index";
export * from "./services/index";
export * from "./dtos/index";
// import Config from "../util/Config";
// import db, { MongooseCache, toObject } from "./util/Database";

View File

@ -127,6 +127,22 @@ export interface ChannelPinsUpdateEvent extends Event {
};
}
export interface ChannelRecipientAddEvent extends Event {
event: "CHANNEL_RECIPIENT_ADD";
data: {
channel_id: string;
user: User;
};
}
export interface ChannelRecipientRemoveEvent extends Event {
event: "CHANNEL_RECIPIENT_REMOVE";
data: {
channel_id: string;
user: User;
};
}
export interface GuildCreateEvent extends Event {
event: "GUILD_CREATE";
data: Guild & {
@ -436,6 +452,8 @@ export type EventData =
| ChannelUpdateEvent
| ChannelDeleteEvent
| ChannelPinsUpdateEvent
| ChannelRecipientAddEvent
| ChannelRecipientRemoveEvent
| GuildCreateEvent
| GuildUpdateEvent
| GuildDeleteEvent
@ -482,6 +500,8 @@ export enum EVENTEnum {
ChannelUpdate = "CHANNEL_UPDATE",
ChannelDelete = "CHANNEL_DELETE",
ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
ChannelRecipientAdd = "CHANNEL_RECIPIENT_ADD",
ChannelRecipientRemove = "CHANNEL_RECIPIENT_REMOVE",
GuildCreate = "GUILD_CREATE",
GuildUpdate = "GUILD_UPDATE",
GuildDelete = "GUILD_DELETE",
@ -525,6 +545,8 @@ export type EVENT =
| "CHANNEL_UPDATE"
| "CHANNEL_DELETE"
| "CHANNEL_PINS_UPDATE"
| "CHANNEL_RECIPIENT_ADD"
| "CHANNEL_RECIPIENT_REMOVE"
| "GUILD_CREATE"
| "GUILD_UPDATE"
| "GUILD_DELETE"

View File

@ -0,0 +1,88 @@
import { Channel, ChannelType, PublicUserProjection, Recipient, User } from "../entities";
import { HTTPError } from "lambert-server";
import { emitEvent, trimSpecial } from "../util";
import { DmChannelDTO } from "../dtos";
import { ChannelRecipientRemoveEvent } from "../interfaces";
export function checker(arr: any[], target: any[]) {
return target.every(v => arr.includes(v));
}
export class ChannelService {
public static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients = recipients.unique().filter((x) => x !== creator_user_id);
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
if (otherRecipientsUsers.length !== recipients.length) {
throw new HTTPError("Recipient/s not found");
}
const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
let channel = null;
const channelRecipients = [...recipients, creator_user_id]
const userRecipients = await Recipient.find({ where: { user_id: creator_user_id }, relations: ["channel", "channel.recipients"] })
for (let ur of userRecipients) {
let re = ur.channel.recipients!.map(r => r.user_id)
if (re.length === channelRecipients.length) {
if (checker(re, channelRecipients)) {
if (channel == null) {
channel = ur.channel
await ur.assign({ closed: false }).save()
}
}
}
}
if (channel == null) {
name = trimSpecial(name);
channel = await new Channel({
name,
type,
owner_id: (type === ChannelType.DM ? undefined : creator_user_id),
created_at: new Date(),
last_message_id: null,
recipients: channelRecipients.map((x) => new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })),
}).save();
}
const channel_dto = await DmChannelDTO.from(channel)
if (type === ChannelType.GROUP_DM) {
for (let recipient of channel.recipients!) {
await emitEvent({
event: "CHANNEL_CREATE",
data: channel_dto.excludedRecipients([recipient.user_id]),
user_id: recipient.user_id
})
}
} else {
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
}
return channel_dto.excludedRecipients([creator_user_id])
}
public static async removeRecipientFromChannel(channel: Channel, user_id: string) {
await Recipient.delete({ channel_id: channel.id, user_id: user_id })
await emitEvent({
event: "CHANNEL_DELETE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id
});
await emitEvent({
event: "CHANNEL_RECIPIENT_REMOVE", data: {
channel_id: channel.id,
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection })
}, channel_id: channel.id
} as ChannelRecipientRemoveEvent);
}
}

View File

@ -0,0 +1 @@
export * from "./ChannelService";