243 lines
9.1 KiB
JavaScript
Executable File
243 lines
9.1 KiB
JavaScript
Executable File
/**
|
|
* @apiDefine Thread Thread - Private Messaging
|
|
* Thread provides private messaging between tribe members with threads and invites.
|
|
*/
|
|
|
|
/**
|
|
* @apiDefine AuthHeaders
|
|
* @apiHeader {String} xalias User's alias for authentication
|
|
* @apiHeader {String} xtribe Tribe identifier
|
|
* @apiHeader {String} xlang Language preference (en/fr)
|
|
* @apiHeader {String} xdays Timestamp for authentication
|
|
* @apiHeader {String} xhash PGP signature hash
|
|
*/
|
|
|
|
const express = require("express");
|
|
const fs = require("fs-extra");
|
|
const path = require("path");
|
|
const Thread = require("../models/Thread.js");
|
|
const checkHeaders = require("../middlewares/checkHeaders.js");
|
|
const isAuthenticated = require("../middlewares/isAuthenticated.js");
|
|
const currentmod = "Thread";
|
|
const log = process.env.ACTIVELOG?.split(',').includes(currentmod);
|
|
|
|
const router = express.Router();
|
|
|
|
const handleError = (res, error, context) => {
|
|
const ref = `Thread.${context}`;
|
|
const status = error.status || 500;
|
|
const msg = status === 404 ? "thread.notfound"
|
|
: status === 400 ? "thread.invalid"
|
|
: "thread.error";
|
|
console.error(`[ThreadRoute] Error in ${context}:`, error);
|
|
res.status(status).json({ status, ref, msg, data: { error: error.message, context } });
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/apxtri/thread/:tribe - Add a message
|
|
* @apiName AddThreadMessage
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Adds a new message to a thread thread. If it's the first message for a given UUID, it creates the thread.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
*/
|
|
router.post("/:tribe", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const role = {
|
|
xalias: req.session.header.xalias,
|
|
xprofils: req.session.header.xprofils,
|
|
};
|
|
const result = await Thread.addMessage(req.params.tribe, req.body, role);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'POST /:tribe');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {get} /api/apxtri/thread/:tribe/list/:alias - List threads
|
|
* @apiName GetThreadThreads
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Lists all threads in a tribe (sorted by most recent activity).
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} alias Pagan alias (for logging)
|
|
*/
|
|
router.get("/:tribe/list/:alias", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const result = await Thread.getThreads(req.params.tribe, req.params.alias);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'GET /:tribe/list/:alias');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {get} /api/apxtri/thread/:tribe/:uuid - Get messages
|
|
* @apiName GetThreadMessages
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Gets all messages for a thread since a given timestamp.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
*/
|
|
router.get("/:tribe/:uuid", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const result = await Thread.getMessages(req.params.tribe, req.params.uuid, req.query.since);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'GET /:tribe/:uuid');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {get} /api/apxtri/thread/:tribe/:uuid/details - Get thread details
|
|
* @apiName GetThreadThreadDetails
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Gets the publicKey and aliases for a thread.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
*/
|
|
router.get("/:tribe/:uuid/details", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const result = await Thread.getThreadDetails(req.params.tribe, req.params.uuid);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'GET /:tribe/:uuid/details');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {delete} /api/apxtri/thread/:tribe/:uuid - Delete a whole thread
|
|
* @apiName DeleteThreadThread
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Deletes an entire thread (all messages + attachment blobs) owned by the authenticated alias.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
*/
|
|
router.delete("/:tribe/:uuid", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const alias = req.session.header.xalias;
|
|
const result = await Thread.deleteThread(req.params.tribe, req.params.uuid, alias);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'DELETE /:tribe/:uuid');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {delete} /api/apxtri/thread/:tribe/:uuid/:privatriid - Delete a message
|
|
* @apiName DeleteThreadMessage
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Deletes a single message (and its attachment blobs) owned by the authenticated alias.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
* @apiParam {String} privatriid Message id ({thread}_{timestamp})
|
|
*/
|
|
router.delete("/:tribe/:uuid/:privatriid", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const alias = req.session.header.xalias;
|
|
const result = await Thread.deleteMessage(req.params.tribe, req.params.privatriid, alias);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'DELETE /:tribe/:uuid/:privatriid');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {post} /api/apxtri/thread/:tribe/:uuid/:privatriid/resolve - Resolve an action tag
|
|
* @apiName ResolveThreadAction
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Marks an action tag as resolved on a message so its buttons are hidden. Owner only.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
* @apiParam {String} privatriid Message id ({thread}_{timestamp})
|
|
* @apiBody {String} action Action tag to resolve
|
|
*/
|
|
router.post("/:tribe/:uuid/:privatriid/resolve", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const alias = req.session.header.xalias;
|
|
const { action } = req.body;
|
|
if (!action) {
|
|
return res.status(400).json({ status: 400, ref: "Thread", msg: "action_required" });
|
|
}
|
|
const result = await Thread.resolveAction(req.params.tribe, req.params.privatriid, action, alias);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'POST /:tribe/:uuid/:privatriid/resolve');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {post} /api/apxtri/thread/:tribe/:uuid/invites - Create an invite
|
|
* @apiName CreateThreadInvite
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Creates an invitation for a user to join a thread.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
*/
|
|
router.post("/:tribe/:uuid/invites", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const { alias, email, encryptedPrivateKey } = req.body;
|
|
const result = await Thread.createInvite(req.params.tribe, req.params.uuid, alias, email, encryptedPrivateKey);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'POST /:tribe/:uuid/invites');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {get} /api/apxtri/thread/invites/:token - Get an invite
|
|
* @apiName GetThreadInvite
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Retrieves the invitation details. This is a public endpoint.
|
|
* @apiParam {String} token The invitation token
|
|
*/
|
|
router.get("/invites/:token", async (req, res) => {
|
|
try {
|
|
const result = await Thread.getInvite(req.params.token);
|
|
res.status(result.status).json(result);
|
|
} catch (error) {
|
|
handleError(res, error, 'GET /invites/:token');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api {get} /api/apxtri/thread/:tribe/:uuid/attachment/:name - Get thread attachment
|
|
* @apiName GetThreadAttachment
|
|
* @apiGroup 06-Thread
|
|
* @apiVersion 1.0.0
|
|
* @apiDescription Serves an encrypted attachment (.pgp) of a thread. The requester must be a participant of the thread.
|
|
* @apiParam {String} tribe Tribe identifier
|
|
* @apiParam {String} uuid Thread UUID
|
|
* @apiParam {String} name Attachment file name (e.g. {privatriid}_0.pgp)
|
|
*/
|
|
router.get("/:tribe/:uuid/attachment/:name", checkHeaders(), isAuthenticated(), async (req, res) => {
|
|
try {
|
|
const { tribe, uuid, name } = req.params;
|
|
const alias = req.session.header.xalias;
|
|
const allowed = await Thread.isParticipant(tribe, uuid, alias);
|
|
if (!allowed) {
|
|
return res.status(403).json({ status: 403, ref: "Thread", msg: "attachment_forbidden" });
|
|
}
|
|
const filePath = Thread.attachmentPath(tribe, name);
|
|
if (!await fs.pathExists(filePath)) {
|
|
return res.status(404).json({ status: 404, ref: "Thread", msg: "attachment_not_found" });
|
|
}
|
|
res.setHeader('Content-Type', 'application/octet-stream');
|
|
res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(path.basename(name)) + '"');
|
|
res.setHeader('Content-Length', (await fs.stat(filePath)).size);
|
|
fs.createReadStream(filePath).pipe(res);
|
|
} catch (error) {
|
|
handleError(res, error, 'GET /:tribe/:uuid/attachment/:name');
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|