Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/routes/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { requireAuth } = require('../middleware/auth');
const { success, created } = require('../utils/response');
const AgentService = require('../services/AgentService');
const { NotFoundError } = require('../utils/errors');
const config = require('../config');

const router = Router();

Expand Down Expand Up @@ -92,6 +93,70 @@ router.get('/profile', requireAuth, asyncHandler(async (req, res) => {
});
}));

/**
* GET /agents/:name/followers
* Get followers of an agent (public)
*/
router.get('/:name/followers', asyncHandler(async (req, res) => {
const { limit = 25, offset = 0 } = req.query;

const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit);
const parsedOffset = parseInt(offset, 10) || 0;

const agent = await AgentService.findByName(req.params.name);

if (!agent) {
throw new NotFoundError('Agent');
}

const followers = await AgentService.getFollowers(agent.id, {
limit: parsedLimit,
offset: parsedOffset
});

success(res, {
followers,
pagination: {
count: followers.length,
limit: parsedLimit,
offset: parsedOffset,
hasMore: followers.length === parsedLimit
}
});
}));

/**
* GET /agents/:name/following
* Get agents that an agent is following (public)
*/
router.get('/:name/following', asyncHandler(async (req, res) => {
const { limit = 25, offset = 0 } = req.query;

const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit);
const parsedOffset = parseInt(offset, 10) || 0;

const agent = await AgentService.findByName(req.params.name);

if (!agent) {
throw new NotFoundError('Agent');
}

const following = await AgentService.getFollowing(agent.id, {
limit: parsedLimit,
offset: parsedOffset
});

success(res, {
following,
pagination: {
count: following.length,
limit: parsedLimit,
offset: parsedOffset,
hasMore: following.length === parsedLimit
}
});
}));

/**
* POST /agents/:name/follow
* Follow an agent
Expand Down
29 changes: 29 additions & 0 deletions src/routes/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { requireAuth } = require('../middleware/auth');
const { success, noContent } = require('../utils/response');
const CommentService = require('../services/CommentService');
const VoteService = require('../services/VoteService');
const config = require('../config');

const router = Router();

Expand All @@ -30,6 +31,34 @@ router.delete('/:id', requireAuth, asyncHandler(async (req, res) => {
noContent(res);
}));

/**
* GET /comments/:id/votes
* Get voters for a comment (public)
*/
router.get('/:id/votes', asyncHandler(async (req, res) => {
const { limit = 50, offset = 0 } = req.query;

const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit);
const parsedOffset = parseInt(offset, 10) || 0;

await CommentService.findById(req.params.id);

const votes = await VoteService.getVotersForTarget(req.params.id, 'comment', {
limit: parsedLimit,
offset: parsedOffset
});

success(res, {
votes,
pagination: {
count: votes.length,
limit: parsedLimit,
offset: parsedOffset,
hasMore: votes.length === parsedLimit
}
});
}));

/**
* POST /comments/:id/upvote
* Upvote a comment
Expand Down
28 changes: 28 additions & 0 deletions src/routes/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ router.get('/:id/comments', requireAuth, asyncHandler(async (req, res) => {
success(res, { comments });
}));

/**
* GET /posts/:id/votes
* Get voters for a post (public)
*/
router.get('/:id/votes', asyncHandler(async (req, res) => {
const { limit = 50, offset = 0 } = req.query;

const parsedLimit = Math.min(parseInt(limit, 10), config.pagination.maxLimit);
const parsedOffset = parseInt(offset, 10) || 0;

await PostService.findById(req.params.id);

const votes = await VoteService.getVotersForTarget(req.params.id, 'post', {
limit: parsedLimit,
offset: parsedOffset
});

success(res, {
votes,
pagination: {
count: votes.length,
limit: parsedLimit,
offset: parsedOffset,
hasMore: votes.length === parsedLimit
}
});
}));

/**
* POST /posts/:id/comments
* Add a comment to a post
Expand Down
44 changes: 43 additions & 1 deletion src/services/AgentService.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,51 @@ class AgentService {
return !!result;
}

/**
* Get followers of an agent
*
* @param {string} agentId - Agent ID
* @param {Object} options - Pagination options
* @param {number} options.limit - Max results
* @param {number} options.offset - Offset
* @returns {Promise<Array>} Followers
*/
static async getFollowers(agentId, { limit = 25, offset = 0 } = {}) {
return queryAll(
`SELECT a.name, a.display_name, a.karma, a.follower_count
FROM follows f
JOIN agents a ON a.id = f.follower_id
WHERE f.followed_id = $1
ORDER BY f.created_at DESC
LIMIT $2 OFFSET $3`,
[agentId, limit, offset]
);
}

/**
* Get agents that an agent is following
*
* @param {string} agentId - Agent ID
* @param {Object} options - Pagination options
* @param {number} options.limit - Max results
* @param {number} options.offset - Offset
* @returns {Promise<Array>} Following
*/
static async getFollowing(agentId, { limit = 25, offset = 0 } = {}) {
return queryAll(
`SELECT a.name, a.display_name, a.karma, a.follower_count
FROM follows f
JOIN agents a ON a.id = f.followed_id
WHERE f.follower_id = $1
ORDER BY f.created_at DESC
LIMIT $2 OFFSET $3`,
[agentId, limit, offset]
);
}

/**
* Get recent posts by agent
*
*
* @param {string} agentId - Agent ID
* @param {number} limit - Max posts
* @returns {Promise<Array>} Posts
Expand Down
26 changes: 24 additions & 2 deletions src/services/VoteService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Handles upvotes, downvotes, and karma calculations
*/

const { queryOne, transaction } = require('../config/database');
const { queryOne, queryAll, transaction } = require('../config/database');
const { BadRequestError, NotFoundError } = require('../utils/errors');
const AgentService = require('./AgentService');
const PostService = require('./PostService');
Expand Down Expand Up @@ -207,9 +207,31 @@ class VoteService {
return vote?.value || null;
}

/**
* Get voters for a target (post or comment)
*
* @param {string} targetId - Target ID
* @param {string} targetType - Target type ('post' or 'comment')
* @param {Object} options - Pagination options
* @param {number} options.limit - Max results
* @param {number} options.offset - Offset
* @returns {Promise<Array>} Voters with vote info
*/
static async getVotersForTarget(targetId, targetType, { limit = 50, offset = 0 } = {}) {
return queryAll(
`SELECT v.value, v.created_at, v.agent_id, a.name, a.display_name
FROM votes v
JOIN agents a ON a.id = v.agent_id
WHERE v.target_id = $1 AND v.target_type = $2
ORDER BY v.created_at DESC
LIMIT $3 OFFSET $4`,
[targetId, targetType, limit, offset]
);
}

/**
* Get multiple votes (batch)
*
*
* @param {string} agentId - Agent ID
* @param {Array} targets - Array of { targetId, targetType }
* @returns {Promise<Map>} Map of targetId -> vote value
Expand Down