Skip to content
This repository has been archived by the owner on Oct 11, 2024. It is now read-only.

Commit

Permalink
Get channel by ID, get channel messages
Browse files Browse the repository at this point in the history
Co-authoried-by: Hakase <hakase@litdevs.org>
  • Loading branch information
emilianya committed Jan 28, 2024
1 parent c48969d commit e1d93cf
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 103 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"express": "^4.18.2",
"file-type": "^18.0.0",
"form-data": "^4.0.0",
"jose": "^4.10.4",
"mongoose": "^8.0.0",
"sharp": "^0.32.6",
"tail": "^2.2.4",
Expand Down
6 changes: 3 additions & 3 deletions src/classes/permissions/PermissionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export default class PermissionManager {
r("EDIT_CHANNEL_NAME", "deny", "Edit channel name", ["READ_CHANNEL"]);
r("EDIT_CHANNEL", "deny", "Edit channel information", ["EDIT_CHANNEL_DESCRIPTION", "EDIT_CHANNEL_NAME"]);
r("DELETE_CHANNEL", "deny", "Delete a channel", ["EDIT_CHANNEL"]);
r("CREATE_CHANNEL", "deny", "Create channels", [], ["quark"]);
r("CHANNEL_MANAGER", "deny", "Manage channels. Grants all channel management permissions.", ["EDIT_CHANNEL", "DELETE_CHANNEL", "CREATE_CHANNEL"]);
r("CHANNEL_ADMIN", "deny", "Manage channels and messages. Grants Channel Manager and Message Admin permissions.", ["CHANNEL_MANAGER", "MESSAGE_ADMIN"]);
r("CREATE_CHANNEL", "deny", "Create channel", [], ["quark"]);
r("CHANNEL_MANAGER", "deny", "Manage channel. Grants all channel management permissions.", ["EDIT_CHANNEL", "DELETE_CHANNEL", "CREATE_CHANNEL"]);
r("CHANNEL_ADMIN", "deny", "Manage channel and messages. Grants Channel Manager and Message Admin permissions.", ["CHANNEL_MANAGER", "MESSAGE_ADMIN"]);

r("CREATE_EMOTE", "deny", "Add new emotes", [], ["quark"]);
r("EDIT_EMOTE", "deny", "Edit emotes", [], ["quark"]);
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ app.use("/", express.static("public"));

import authv3 from './routes/v3/auth.js';
import userv3 from './routes/v3/user.js';
import channelv3 from './routes/v3/channel.js'
app.use("/v3/auth", authv3);
app.use("/v3/user", userv3);
app.use("/v3/channel", channelv3);
app.use("/v3/channel", channelv3);

import home from './routes/home.js';
app.use("/", home)



app.all("*", (req, res) => {
res.reply(new NotFoundReply("Endpoint not found"));
})
Expand Down
94 changes: 49 additions & 45 deletions src/routes/v3/auth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import express from 'express';
import InvalidReplyMessage from "../../classes/reply/InvalidReplyMessage.js";
import ServerErrorReply from "../../classes/reply/ServerErrorReply.js";
import NotFoundReply from "../../classes/reply/NotFoundReply.js";
import crypto from 'crypto';
import Reply from "../../classes/reply/Reply.js";
import * as jose from "jose";
import {getNick} from "../../util/getNickname.js";
import Auth from "../../util/Auth.js";
import {networkInformation} from "../../index.js";
import RequiredProperties from "../../util/RequiredProperties.js";
import Database from "../../db.js";
import BadRequestReply from "../../classes/reply/BadRequestReply.js";
Expand Down Expand Up @@ -37,28 +32,33 @@ router.post("/register", RequiredProperties([
maxLength: 64
}
]), async (req, res) => {
if (await database.LoginUsers.findOne({email: req.body.email})) return res.reply(new InvalidReplyMessage("A user with this email already exists"));
// FIXME Some limitations please? Email verification at least??
let pwd = encryptPassword(req.body.password);
let user = new database.LoginUsers({
_id: new Types.ObjectId(),
email: req.body.email,
username: req.body.username.trim(),
passwordHash: pwd.hashedPassword,
salt: pwd.salt
})
await user.save();
let token = new database.Token({
access: new AccessToken(new Date(Date.now() + 1000 * 60 * 60 * 8)), // 8 hours
refresh: new RefreshToken(),
user: user._id
})
await token.save()
return res.reply(new Reply(201, true, {
message: "Account created",
access_token: token.access,
refresh_token: token.refresh
}))
try {
if (await database.LoginUsers.findOne({email: req.body.email})) return res.reply(new InvalidReplyMessage("A user with this email already exists"));
// FIXME Some limitations please? Email verification at least??
let pwd = encryptPassword(req.body.password);
let user = new database.LoginUsers({
_id: new Types.ObjectId(),
email: req.body.email,
username: req.body.username.trim(),
passwordHash: pwd.hashedPassword,
salt: pwd.salt
})
await user.save();
let token = new database.Token({
access: new AccessToken(new Date(Date.now() + 1000 * 60 * 60 * 8)), // 8 hours
refresh: new RefreshToken(),
user: user._id
})
await token.save()
return res.reply(new Reply(201, true, {
message: "Account created",
access_token: token.access,
refresh_token: token.refresh
}))
} catch (e) {
console.error(e);
res.reply(new ServerErrorReply())
}
})

router.post("/token", RequiredProperties([
Expand All @@ -72,26 +72,30 @@ router.post("/token", RequiredProperties([
type: "string"
}
]), async (req, res) => {
let user = await database.LoginUsers.findOne({email: req.body.email.trim()})
if (!user) return res.reply(new BadRequestReply("Invalid email and password combination."))
if (!user.passwordHash || !user.salt) return res.reply(new ServerErrorReply())
if (!checkPassword(req.body.password, user.passwordHash, user.salt)) return res.reply(new BadRequestReply("Invalid email and password combination."))

try {
let user = await database.LoginUsers.findOne({email: req.body.email.trim()})
if (!user) return res.reply(new BadRequestReply("Invalid email and password combination."))
if (!user.passwordHash || !user.salt) return res.reply(new ServerErrorReply())
if (!checkPassword(req.body.password, user.passwordHash, user.salt)) return res.reply(new BadRequestReply("Invalid email and password combination."))

// Congratulations! The password is correct.
let token = new database.Token({
access: new AccessToken(new Date(Date.now() + 1000 * 60 * 60 * 8)), // 8 hours
refresh: new RefreshToken(),
user: user._id
})
await token.save();
// Congratulations! The password is correct.
let token = new database.Token({
access: new AccessToken(new Date(Date.now() + 1000 * 60 * 60 * 8)), // 8 hours
refresh: new RefreshToken(),
user: user._id
})
await token.save();

return res.json(new Reply(200, true, {
message: "Here are your tokens!",
token_type: "Bearer",
access_token: token.access,
refresh_token: token.refresh
}))
return res.json(new Reply(200, true, {
message: "Here are your tokens!",
token_type: "Bearer",
access_token: token.access,
refresh_token: token.refresh
}))
} catch (e) {
console.error(e);
res.reply(new ServerErrorReply())
}
})

export default router;
Expand Down
33 changes: 33 additions & 0 deletions src/routes/v3/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import express from 'express';
import {Auth} from "./auth.js";
import Database from "../../db.js";
import P from "../../util/PermissionMiddleware.js";
import {isValidObjectId} from "mongoose";
import InvalidReplyMessage from "../../classes/reply/InvalidReplyMessage.js";
import NotFoundReply from "../../classes/reply/NotFoundReply.js";
import Reply from "../../classes/reply/Reply.js";
import ServerErrorReply from "../../classes/reply/ServerErrorReply.js";
import messages from "./channel/messages.js";


const router = express.Router();

const database = new Database()

router.use(Auth);

router.use("/:channelId/messages", messages)

router.get("/:channelId", P("READ_CHANNEL", "channel"), async (req, res) => {
try {
// Validity and existence are checked by permission manager
let channel = await database.Channels.findOne({ _id: req.params.channelId })
res.reply(new Reply(200, true, { message: "Here is the channel", channel }))
} catch (e) {
console.error(e);
res.reply(new ServerErrorReply())
}
})


export default router;
48 changes: 48 additions & 0 deletions src/routes/v3/channel/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import express from 'express';
import Database from "../../../db.js";
import {Auth} from "../auth.js";
import Reply from "../../../classes/reply/Reply.js";
import P from "../../../util/PermissionMiddleware.js";
import ServerErrorReply from "../../../classes/reply/ServerErrorReply.js";
import InvalidReplyMessage from "../../../classes/reply/InvalidReplyMessage.js";
import {Types} from "mongoose";
import messageSchema from "../../../schemas/messageSchema.js";
import {getNickBulk} from "../../../util/getNickname.js";

const router = express.Router({ mergeParams: true });

const database = new Database()

router.use(Auth);

router.get("/", P(["READ_CHANNEL_HISTORY", "READ_CHANNEL"], "channel"), async (req, res) => {
try {
// Optionally client can provide a timestamp to get messages before or after
let startTimestamp = Infinity;
let endTimestamp = 0;
if (req.query.startTimestamp) startTimestamp = Number(req.query.startTimestamp)
if (req.query.endTimestamp) endTimestamp = Number(req.query.endTimestamp)
if (isNaN(startTimestamp)) return res.status(400).json(new InvalidReplyMessage("Invalid startTimestamp"));
if (isNaN(endTimestamp)) return res.status(400).json(new InvalidReplyMessage("Invalid endTimestamp"));

let quark = await database.Quarks.findOne({ channels: new Types.ObjectId(req.params.channelId) });

// Find messages in specified range, if endTimestamp is specified get messages after that timestamp, otherwise get messages before that timestamp
// The naming is a bit backwards, isn't it?
let messages = await database.Messages.find({ channelId: req.params.channelId, timestamp: { $lt: startTimestamp, $gt: endTimestamp } }).sort({ timestamp: endTimestamp === 0 ? -1 : 1 }).limit(50)
// Replace author usernames with nicknames in this quark
let authorIds = messages.map(message => message.authorId);
let nicknames = await getNickBulk(authorIds, quark._id)
messages = messages.map(message => {
message.author.username = nicknames.find(nick => nick.userId.equals(message.authorId)).nickname || message.author.username;
return message;
})
res.reply(new Reply(200, true, {message: "Here are the messages", messages}));
} catch (e) {
console.error(e);
return res.status(500).json(new ServerErrorReply());
}
})


export default router;
48 changes: 24 additions & 24 deletions src/routes/v3/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {isValidObjectId} from "mongoose";
import InvalidReplyMessage from "../../classes/reply/InvalidReplyMessage.js";
import Database from "../../db.js";
import NotFoundReply from "../../classes/reply/NotFoundReply.js";

import ServerErrorReply from "../../classes/reply/ServerErrorReply.js";
import {safeUser} from "../../util/safeUser.js";

const router = express.Router();

Expand All @@ -14,32 +15,31 @@ const database = new Database()
router.use(Auth);

router.get("/me", (req, res) => {
res.reply(new Reply(200, true, {
message: "You are signed in. Here is your data",
jwtData: res.locals.user
}))
try {
res.reply(new Reply(200, true, {
message: "You are signed in. Here is your data",
jwtData: res.locals.user
}))
} catch (e) {
console.error(e)
return res.reply(new ServerErrorReply())
}
})

router.get("/:userId", async (req, res) => {
if (!isValidObjectId(req.params.userId)) return res.reply(new InvalidReplyMessage("Invalid user"));
let user = await database.LoginUsers.findOne({_id: req.params.userId});
if (!user) return res.reply(new NotFoundReply("No such user"))

res.reply(new Reply(200, true, {
message: "User found",
user: safeUser(user)
}))
})

export function safeUser(user) {
return {
username: user.username,
_id: user._id,
admin: user.admin,
isBot: user.isBot,
avatarUri: user.avatarUri,
status: user.status
try {
if (!isValidObjectId(req.params.userId)) return res.reply(new InvalidReplyMessage("Invalid user"));
let user = await database.LoginUsers.findOne({_id: req.params.userId});
if (!user) return res.reply(new NotFoundReply("No such user"))

res.reply(new Reply(200, true, {
message: "User found",
user: safeUser(user)
}))
} catch (e) {
console.error(e)
return res.reply(new ServerErrorReply())
}
}
})

export default router;
8 changes: 4 additions & 4 deletions src/routes/v3/user/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import networkInformation from "../../../networkInformation.js";
/**
* Turn status into a status without _id and userId, replace images with links to real images :tm:
* @param status
* @param {null|ObjectId} userId
*/
export function plainStatus(status) {
export function plainStatus(status, userId = null) {
if (!status) return undefined;
status = status.toJSON();
status.primaryImage = `${networkInformation.baseUrl}/v3/user/${status.userId}/status/primaryImage`
status.secondaryImage = `${networkInformation.baseUrl}/v3/user/${status.userId}/status/secondaryImage`
status.primaryImage = `${networkInformation.baseUrl}/v3/user/${userId || status.userId}/status/primaryImage`
status.secondaryImage = `${networkInformation.baseUrl}/v3/user/${userId || status.userId}/status/secondaryImage`
status._id = undefined;
status.userId = undefined;
status.__v = undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/loginUserSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const findHook = function (this: any, next: () => void) {
_recursed: true
},
transform: doc => {
if (doc.type) return plainStatus(doc) // If the document has a "type" property it is probably a status document
if (doc?.type) return plainStatus(doc) // If the document has a "type" property it is probably a status document
return doc
}
});
Expand Down
48 changes: 47 additions & 1 deletion src/schemas/messageSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Mongoose from "mongoose";
import {safeUser} from "../util/safeUser.js";
import {plainStatus} from "../routes/v3/user/status.js";

export default new Mongoose.Schema({
const schema = new Mongoose.Schema({
_id: Mongoose.Types.ObjectId,
authorId: Mongoose.Types.ObjectId,
content: String,
Expand All @@ -10,4 +12,48 @@ export default new Mongoose.Schema({
edited: { type: Boolean, default: false },
attachments: [String],
specialAttributes: [Object]
}, {
toObject: {
virtuals: true
},
toJSON: {
virtuals: true,
transform: function (doc, ret) {
delete ret.__v;
return ret;
}
},
});
schema.virtual("author", {
ref: "user",
localField: "authorId",
foreignField: "_id",
justOne: true
})
const findHook = function (this: any, next: () => void) {
if (this.options._recursed) {
return next();
}
this.populate({
path: "author",
options: {
_recursed: true
},
populate: {path: "avatarDoc"},
transform: doc => {
// If the document has a "passwordHash" property it is probably a user document
if (doc?.passwordHash) {
// Clean up the status object in the user if present
// (will never be present unless added to populate path)
if (doc?.status) doc.status = plainStatus(doc.status, doc._id)
return safeUser(doc)
}
return doc
}
});
next()
}
schema.pre('findOne', findHook);
schema.pre('find', findHook);

export default schema;
Loading

0 comments on commit e1d93cf

Please sign in to comment.