1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-10 20:52:42 +01:00
This commit is contained in:
Flam3rboy 2021-09-19 18:59:54 +02:00
commit e6839719a6
29 changed files with 1176 additions and 692 deletions

File diff suppressed because it is too large Load Diff

130
api/package-lock.json generated
View File

@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@fosscord/util": "file:../util", "@fosscord/util": "file:../util",
"@types/morgan": "^1.9.3",
"ajv": "8.6.2", "ajv": "8.6.2",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"amqplib": "^0.8.0", "amqplib": "^0.8.0",
@ -34,6 +35,7 @@
"mongoose": "^5.12.3", "mongoose": "^5.12.3",
"mongoose-autopopulate": "^0.12.3", "mongoose-autopopulate": "^0.12.3",
"mongoose-long": "^0.3.2", "mongoose-long": "^0.3.2",
"morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
@ -1452,6 +1454,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/morgan": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
"integrity": "sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/multer": { "node_modules/@types/multer": {
"version": "1.4.7", "version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
@ -2271,6 +2281,22 @@
} }
] ]
}, },
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
@ -8021,6 +8047,42 @@
"mongoose": "*" "mongoose": "*"
} }
}, },
"node_modules/morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/morgan/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/morgan/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/morgan/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/morphdom": { "node_modules/morphdom": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz", "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz",
@ -8582,6 +8644,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/on-net-listen": { "node_modules/on-net-listen": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz",
@ -14113,6 +14183,14 @@
} }
} }
}, },
"@types/morgan": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
"integrity": "sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==",
"requires": {
"@types/node": "*"
}
},
"@types/multer": { "@types/multer": {
"version": "1.4.7", "version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
@ -14793,6 +14871,21 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
}, },
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"bcrypt": { "bcrypt": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
@ -19500,6 +19593,38 @@
"integrity": "sha512-5gTjPH6HUmtNhamv8MPwExWo01Z4d9CT5njZlupqqbmxzMXTbDOgCuP/jnK+9SV0Fs7nuyYlXv7pJ/nA2pAAuA==", "integrity": "sha512-5gTjPH6HUmtNhamv8MPwExWo01Z4d9CT5njZlupqqbmxzMXTbDOgCuP/jnK+9SV0Fs7nuyYlXv7pJ/nA2pAAuA==",
"requires": {} "requires": {}
}, },
"morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"morphdom": { "morphdom": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz", "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz",
@ -19964,6 +20089,11 @@
"ee-first": "1.1.1" "ee-first": "1.1.1"
} }
}, },
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
},
"on-net-listen": { "on-net-listen": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz",

View File

@ -62,6 +62,7 @@
}, },
"dependencies": { "dependencies": {
"@fosscord/util": "file:../util", "@fosscord/util": "file:../util",
"@types/morgan": "^1.9.3",
"ajv": "8.6.2", "ajv": "8.6.2",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"amqplib": "^0.8.0", "amqplib": "^0.8.0",
@ -85,6 +86,7 @@
"mongoose": "^5.12.3", "mongoose": "^5.12.3",
"mongoose-autopopulate": "^0.12.3", "mongoose-autopopulate": "^0.12.3",
"mongoose-long": "^0.3.2", "mongoose-long": "^0.3.2",
"morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",

View File

@ -1,3 +1,4 @@
import { OptionsJson } from 'body-parser';
import "missing-native-js-functions"; import "missing-native-js-functions";
import { Connection } from "mongoose"; import { Connection } from "mongoose";
import { Server, ServerOptions } from "lambert-server"; import { Server, ServerOptions } from "lambert-server";
@ -11,6 +12,7 @@ import path from "path";
import { initRateLimits } from "./middlewares/RateLimit"; import { initRateLimits } from "./middlewares/RateLimit";
import TestClient from "./middlewares/TestClient"; import TestClient from "./middlewares/TestClient";
import { initTranslation } from "./middlewares/Translation"; import { initTranslation } from "./middlewares/Translation";
import morgan from "morgan";
export interface FosscordServerOptions extends ServerOptions {} export interface FosscordServerOptions extends ServerOptions {}
@ -36,6 +38,29 @@ export class FosscordServer extends Server {
await Config.init(); await Config.init();
await initEvent(); await initEvent();
/*
DOCUMENTATION: uses LOG_REQUESTS environment variable
# only log 200 and 204
LOG_REQUESTS=200 204
# log everything except 200 and 204
LOG_REQUESTS=-200 204
# log all requests
LOG_REQUESTS=-
*/
let logRequests = process.env["LOG_REQUESTS"] != undefined;
if(logRequests) {
this.app.use(morgan("combined", {
skip: (req, res) => {
var skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false);
if(process.env["LOG_REQUESTS"]?.charAt(0) == '-') skip = !skip;
return skip;
}
}));
}
this.app.use(CORS); this.app.use(CORS);
this.app.use(BodyParser({ inflate: true, limit: "10mb" })); this.app.use(BodyParser({ inflate: true, limit: "10mb" }));
@ -65,6 +90,9 @@ export class FosscordServer extends Server {
this.app.use(ErrorHandler); this.app.use(ErrorHandler);
TestClient(this.app); TestClient(this.app);
if(logRequests){
console.log("Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!");
}
return super.start(); return super.start();
} }
} }

View File

@ -1,6 +1,7 @@
import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, ChannelType, ChannelPermissionOverwriteType } from "@fosscord/util"; import { Channel, ChannelDeleteEvent, ChannelPermissionOverwriteType, ChannelType, ChannelUpdateEvent, emitEvent, Recipient } from "@fosscord/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import { route } from "@fosscord/api"; import { handleFile, route } from "@fosscord/api";
const router: Router = Router(); const router: Router = Router();
// TODO: delete channel // TODO: delete channel
// TODO: Get channel // TODO: Get channel
@ -16,23 +17,37 @@ router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res:
router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
const { channel_id } = req.params; 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 if (channel.type === ChannelType.DM) {
const data = channel; 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 Channel.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 { export interface ChannelModifySchema {
/** /**
* @maxLength 100 * @maxLength 100
*/ */
name: string; name?: string;
type: ChannelType; type?: ChannelType;
topic?: string; topic?: string;
icon?: string | null;
bitrate?: number; bitrate?: number;
user_limit?: number; user_limit?: number;
rate_limit_per_user?: number; rate_limit_per_user?: number;
@ -53,6 +68,7 @@ export interface ChannelModifySchema {
router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
var payload = req.body as ChannelModifySchema; var payload = req.body as ChannelModifySchema;
const { channel_id } = req.params; const { channel_id } = req.params;
if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon);
const channel = await Channel.findOneOrFail({ id: channel_id }); const channel = await Channel.findOneOrFail({ id: channel_id });
channel.assign(payload); channel.assign(payload);

View File

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

View File

@ -1,9 +1,19 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import { Attachment, Channel, ChannelType, Embed, getPermission, Message, uploadFile } from "@fosscord/util"; import {
Attachment,
Channel,
ChannelType,
DmChannelDTO,
Embed,
emitEvent,
getPermission,
Message,
MessageCreateEvent,
uploadFile
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { handleMessage, postHandleMessage, route } from "@fosscord/api";
import multer from "multer"; import multer from "multer";
import { sendMessage } from "@fosscord/api";
import { FindManyOptions, LessThan, MoreThan } from "typeorm"; import { FindManyOptions, LessThan, MoreThan } from "typeorm";
const router: Router = Router(); const router: Router = Router();
@ -61,9 +71,9 @@ router.get("/", async (req: Request, res: Response) => {
if (!channel) throw new HTTPError("Channel not found", 404); if (!channel) throw new HTTPError("Channel not found", 404);
isTextChannel(channel.type); isTextChannel(channel.type);
const around = `${req.query.around}`; const around = req.query.around ? `${req.query.around}` : undefined;
const before = `${req.query.before}`; const before = req.query.before ? `${req.query.before}` : undefined;
const after = `${req.query.after}`; const after = req.query.after ? `${req.query.after}` : undefined;
const limit = Number(req.query.limit) || 50; const limit = Number(req.query.limit) || 50;
if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100"); if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100");
@ -150,10 +160,11 @@ router.post(
return res.status(400).json(error); return res.status(400).json(error);
} }
} }
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
const embeds = []; const embeds = [];
if (body.embed) embeds.push(body.embed); if (body.embed) embeds.push(body.embed);
const data = await sendMessage({ let message = await handleMessage({
...body, ...body,
type: 0, type: 0,
pinned: false, pinned: false,
@ -161,9 +172,41 @@ router.post(
embeds, embeds,
channel_id, channel_id,
attachments, 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
});
}
}
//Only one recipients should be closed here, since in group DMs the recipient is deleted not closed
await Promise.all(
channel
.recipients!.filter((r) => r.closed)
.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, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util";
const router: Router = Router(); 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 Channel.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 Channel.removeRecipientFromChannel(channel, user_id)
return res.sendStatus(204);
});
export default router; export default router;

View File

@ -0,0 +1,18 @@
import { Request, Response, Router } from "express";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
//TODO
res.json({
id: "",
stickers: [],
name: "",
sku_id: "",
cover_sticker_id: "",
description: "",
banner_asset_id: ""
}).status(200);
});
export default router;

View File

@ -0,0 +1,10 @@
import { Request, Response, Router } from "express";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
//TODO
res.json({ sticker_packs: [] }).status(200);
});
export default router;

View File

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

View File

@ -1,15 +1,12 @@
import { Router, Request, Response } from "express"; import { Request, Response, Router } from "express";
import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent, Recipient } from "@fosscord/util"; import { Recipient, DmChannelDTO, Channel } from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { In } from "typeorm";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { 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, closed: false }, relations: ["channel", "channel.recipients"] });
res.json(await Promise.all(recipients.map(r => DmChannelDTO.from(r.channel, [req.user_id]))));
res.json(recipients.map((x) => x.channel));
}); });
export interface DmChannelCreateSchema { export interface DmChannelCreateSchema {
@ -19,30 +16,7 @@ export interface DmChannelCreateSchema {
router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => {
const body = req.body as DmChannelCreateSchema; const body = req.body as DmChannelCreateSchema;
res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name));
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);
}); });
export default router; export default router;

View File

@ -0,0 +1,11 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
//TODO
res.json([]).status(200);
});
export default router;

View File

@ -18,9 +18,19 @@ const router = Router();
const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection];
router.get("/", route({}), async (req: Request, res: Response) => { 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 { export interface RelationshipPutSchema {
@ -48,7 +58,10 @@ router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request,
await User.findOneOrFail({ await User.findOneOrFail({
relations: ["relationships", "relationships.to"], relations: ["relationships", "relationships.to"],
select: userProjection, 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 req.body.type
); );

View File

@ -58,6 +58,9 @@ export class CDNServer extends Server {
this.app.use("/team-icons/", avatarsRoute); this.app.use("/team-icons/", avatarsRoute);
this.log("verbose", "[Server] Route /team-icons registered"); this.log("verbose", "[Server] Route /team-icons registered");
this.app.use("/channel-icons/", avatarsRoute);
this.log("verbose", "[Server] Route /channel-icons registered");
return super.start(); return super.start();
} }

View File

@ -32,7 +32,7 @@ export async function setupListener(this: WebSocket) {
}); });
const guilds = members.map((x) => x.guild); const guilds = members.map((x) => x.guild);
const recipients = await Recipient.find({ const recipients = await Recipient.find({
where: { user_id: this.user_id }, where: { user_id: this.user_id, closed: false },
relations: ["channel"], relations: ["channel"],
}); });
const dm_channels = recipients.map((x) => x.channel); const dm_channels = recipients.map((x) => x.channel);
@ -116,7 +116,7 @@ async function consume(this: WebSocket, opts: EventOpts) {
.has("VIEW_CHANNEL") .has("VIEW_CHANNEL")
) )
return; return;
// TODO: check if user has permission to channel //No break needed here, we need to call the listenEvent function below
case "GUILD_CREATE": case "GUILD_CREATE":
this.events[id] = await listenEvent(id, consumer, listenOpts); this.events[id] = await listenEvent(id, consumer, listenOpts);
break; break;

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

View File

@ -21,5 +21,6 @@ export default {
8: onRequestGuildMembers, 8: onRequestGuildMembers,
// 9: Invalid Session // 9: Invalid Session
// 10: Hello // 10: Hello
// 13: Dm_update
14: onLazyRequest, 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,12 +1,13 @@
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 { BaseClass } from "./BaseClass";
import { Guild } from "./Guild"; import { Guild } from "./Guild";
import { Message } from "./Message"; import { PublicUserProjection, User } from "./User";
import { User } from "./User";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { emitEvent, getPermission, Snowflake } from "../util"; import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util";
import { ChannelCreateEvent } from "../interfaces"; import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { Recipient } from "./Recipient"; import { Recipient } from "./Recipient";
import { DmChannelDTO } from "../dtos";
import { Message } from "./Message";
export enum ChannelType { export enum ChannelType {
GUILD_TEXT = 0, // a text channel within a server GUILD_TEXT = 0, // a text channel within a server
@ -31,6 +32,9 @@ export class Channel extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
name?: string; name?: string;
@Column({ type: 'text', nullable: true })
icon?: string | null;
@Column({ type: "simple-enum", enum: ChannelType }) @Column({ type: "simple-enum", enum: ChannelType })
type: ChannelType; type: ChannelType;
@ -38,13 +42,8 @@ export class Channel extends BaseClass {
recipients?: Recipient[]; recipients?: Recipient[];
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.last_message)
last_message_id: string; last_message_id: string;
@JoinColumn({ name: "last_message_id" })
@ManyToOne(() => Message)
last_message?: Message;
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.guild) @RelationId((channel: Channel) => channel.guild)
guild_id?: string; guild_id?: string;
@ -100,7 +99,6 @@ export class Channel extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
topic?: string; topic?: string;
// TODO: DM channel
static async createChannel( static async createChannel(
channel: Partial<Channel>, channel: Partial<Channel>,
user_id: string = "0", user_id: string = "0",
@ -162,6 +160,117 @@ export class Channel extends BaseClass {
return channel; return channel;
} }
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 })) });
// TODO: check config for max number of recipients
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 (containsAll(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])
}
static async removeRecipientFromChannel(channel: Channel, user_id: string) {
await Recipient.delete({ channel_id: channel.id, user_id: user_id })
channel.recipients = channel.recipients?.filter(r => r.user_id !== user_id)
if (channel.recipients?.length === 0) {
await Channel.deleteChannel(channel);
await emitEvent({
event: "CHANNEL_DELETE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id
});
return
}
await emitEvent({
event: "CHANNEL_DELETE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id
});
//If the owner leave we make the first recipient in the list the new owner
if (channel.owner_id === user_id) {
channel.owner_id = channel.recipients!.find(r => r.user_id !== user_id)!.user_id //Is there a criteria to choose the new owner?
await emitEvent({
event: "CHANNEL_UPDATE",
data: await DmChannelDTO.from(channel, [user_id]),
channel_id: channel.id
});
}
await channel.save()
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);
}
static async deleteChannel(channel: Channel) {
await Message.delete({ channel_id: channel.id }) //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
//TODO before deleting the channel we should check and delete other relations
await Channel.delete({ id: channel.id })
}
isDm() {
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM
}
} }
export interface ChannelPermissionOverwrite { export interface ChannelPermissionOverwrite {

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import "reflect-metadata";
export * from "./util/index"; export * from "./util/index";
export * from "./interfaces/index"; export * from "./interfaces/index";
export * from "./entities/index"; export * from "./entities/index";
export * from "./dtos/index";
// import Config from "../util/Config"; // import Config from "../util/Config";
// import db, { MongooseCache, toObject } from "./util/Database"; // 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 { export interface GuildCreateEvent extends Event {
event: "GUILD_CREATE"; event: "GUILD_CREATE";
data: Guild & { data: Guild & {
@ -436,6 +452,8 @@ export type EventData =
| ChannelUpdateEvent | ChannelUpdateEvent
| ChannelDeleteEvent | ChannelDeleteEvent
| ChannelPinsUpdateEvent | ChannelPinsUpdateEvent
| ChannelRecipientAddEvent
| ChannelRecipientRemoveEvent
| GuildCreateEvent | GuildCreateEvent
| GuildUpdateEvent | GuildUpdateEvent
| GuildDeleteEvent | GuildDeleteEvent
@ -482,6 +500,8 @@ export enum EVENTEnum {
ChannelUpdate = "CHANNEL_UPDATE", ChannelUpdate = "CHANNEL_UPDATE",
ChannelDelete = "CHANNEL_DELETE", ChannelDelete = "CHANNEL_DELETE",
ChannelPinsUpdate = "CHANNEL_PINS_UPDATE", ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
ChannelRecipientAdd = "CHANNEL_RECIPIENT_ADD",
ChannelRecipientRemove = "CHANNEL_RECIPIENT_REMOVE",
GuildCreate = "GUILD_CREATE", GuildCreate = "GUILD_CREATE",
GuildUpdate = "GUILD_UPDATE", GuildUpdate = "GUILD_UPDATE",
GuildDelete = "GUILD_DELETE", GuildDelete = "GUILD_DELETE",
@ -525,6 +545,8 @@ export type EVENT =
| "CHANNEL_UPDATE" | "CHANNEL_UPDATE"
| "CHANNEL_DELETE" | "CHANNEL_DELETE"
| "CHANNEL_PINS_UPDATE" | "CHANNEL_PINS_UPDATE"
| "CHANNEL_RECIPIENT_ADD"
| "CHANNEL_RECIPIENT_REMOVE"
| "GUILD_CREATE" | "GUILD_CREATE"
| "GUILD_UPDATE" | "GUILD_UPDATE"
| "GUILD_DELETE" | "GUILD_DELETE"

3
util/src/util/Array.ts Normal file
View File

@ -0,0 +1,3 @@
export function containsAll(arr: any[], target: any[]) {
return target.every(v => arr.includes(v));
}

View File

@ -92,6 +92,7 @@ export class Permissions extends BitField {
} }
overwriteChannel(overwrites: ChannelPermissionOverwrite[]) { overwriteChannel(overwrites: ChannelPermissionOverwrite[]) {
if (!overwrites) return this
if (!this.cache) throw new Error("permission chache not available"); if (!this.cache) throw new Error("permission chache not available");
overwrites = overwrites.filter((x) => { overwrites = overwrites.filter((x) => {
if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true; if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true;

View File

@ -12,3 +12,4 @@ export * from "./RabbitMQ";
export * from "./Regex"; export * from "./Regex";
export * from "./Snowflake"; export * from "./Snowflake";
export * from "./String"; export * from "./String";
export * from "./Array";