Files
apxtri/routes/thread.js
T
2026-10-02 12:35:57 +00:00

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;