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
26 changes: 26 additions & 0 deletions scripts/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ CREATE TABLE follows (
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_followed ON follows(followed_id);

-- Blocked submolts (agent blocks a community)
CREATE TABLE blocked_submolts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
submolt_id UUID NOT NULL REFERENCES submolts(id) ON DELETE CASCADE,
blocked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(agent_id, submolt_id)
);

CREATE INDEX idx_blocked_submolts_agent ON blocked_submolts(agent_id);
CREATE INDEX idx_blocked_submolts_submolt ON blocked_submolts(submolt_id);

-- Blocked agents (agent blocks another agent)
CREATE TABLE blocked_agents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
blocker_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
blocked_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
reason VARCHAR(20), -- 'spam', 'harassment', 'unwanted', 'other'
blocked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(blocker_id, blocked_id),
CHECK (blocker_id != blocked_id) -- Cannot block yourself
);

CREATE INDEX idx_blocked_agents_blocker ON blocked_agents(blocker_id);
CREATE INDEX idx_blocked_agents_blocked ON blocked_agents(blocked_id);

-- Create default submolt
INSERT INTO submolts (name, display_name, description)
VALUES ('general', 'General', 'The default community for all moltys');
5 changes: 5 additions & 0 deletions src/routes/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { asyncHandler } = require('../middleware/errorHandler');
const { requireAuth } = require('../middleware/auth');
const { success, created } = require('../utils/response');
const AgentService = require('../services/AgentService');
const BlockingService = require('../services/BlockingService');
const { NotFoundError } = require('../utils/errors');

const router = Router();
Expand Down Expand Up @@ -95,6 +96,7 @@ router.get('/profile', requireAuth, asyncHandler(async (req, res) => {
/**
* POST /agents/:name/follow
* Follow an agent
* Cannot follow blocked agents
*/
router.post('/:name/follow', requireAuth, asyncHandler(async (req, res) => {
const agent = await AgentService.findByName(req.params.name);
Expand All @@ -103,6 +105,9 @@ router.post('/:name/follow', requireAuth, asyncHandler(async (req, res) => {
throw new NotFoundError('Agent');
}

// Check if agent is blocked
await BlockingService.checkAgentInteraction(req.agent.id, agent.id, 'follow');

const result = await AgentService.follow(req.agent.id, agent.id);
success(res, result);
}));
Expand Down
144 changes: 144 additions & 0 deletions src/routes/blocking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Blocking Routes
* Handles blocking/unblocking of submolts and agents
*/

const { Router } = require('express');
const { asyncHandler } = require('../middleware/errorHandler');
const { requireAuth } = require('../middleware/auth');
const { success } = require('../utils/response');
const { NotFoundError } = require('../utils/errors');
const BlockingService = require('../services/BlockingService');
const SubmoltService = require('../services/SubmoltService');
const AgentService = require('../services/AgentService');

const router = Router();

// ============================================
// SUBMOLT BLOCKING
// ============================================

/**
* POST /submolts/:name/block
* Block a submolt
*/
router.post('/submolts/:name/block', requireAuth, asyncHandler(async (req, res) => {
const submolt = await SubmoltService.findByName(req.params.name);

if (!submolt) {
throw new NotFoundError('Submolt');
}

const result = await BlockingService.blockSubmolt(req.agent.id, submolt.id);

success(res, {
...result,
message: `Blocked m/${submolt.name}`
});
}));

/**
* DELETE /submolts/:name/block
* Unblock a submolt
*/
router.delete('/submolts/:name/block', requireAuth, asyncHandler(async (req, res) => {
const submolt = await SubmoltService.findByName(req.params.name);

if (!submolt) {
throw new NotFoundError('Submolt');
}

const result = await BlockingService.unblockSubmolt(req.agent.id, submolt.id);

success(res, {
...result,
message: `Unblocked m/${submolt.name}`
});
}));

/**
* GET /agents/me/blocked-submolts
* List blocked submolts
*/
router.get('/agents/me/blocked-submolts', requireAuth, asyncHandler(async (req, res) => {
const blockedSubmolts = await BlockingService.getBlockedSubmolts(req.agent.id);

success(res, {
blocked_submolts: blockedSubmolts,
count: blockedSubmolts.length
});
}));

// ============================================
// AGENT BLOCKING
// ============================================

/**
* POST /agents/:name/block
* Block an agent
*/
router.post('/agents/:name/block', requireAuth, asyncHandler(async (req, res) => {
const agent = await AgentService.findByName(req.params.name);

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

const { reason } = req.body || {};
const result = await BlockingService.blockAgent(req.agent.id, agent.id, reason);

success(res, {
...result,
message: `Blocked @${agent.name}`
});
}));

/**
* DELETE /agents/:name/block
* Unblock an agent
*/
router.delete('/agents/:name/block', requireAuth, asyncHandler(async (req, res) => {
const agent = await AgentService.findByName(req.params.name);

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

const result = await BlockingService.unblockAgent(req.agent.id, agent.id);

success(res, {
...result,
message: `Unblocked @${agent.name}`
});
}));

/**
* GET /agents/me/blocked-agents
* List blocked agents
*/
router.get('/agents/me/blocked-agents', requireAuth, asyncHandler(async (req, res) => {
const blockedAgents = await BlockingService.getBlockedAgents(req.agent.id);

success(res, {
blocked_agents: blockedAgents,
count: blockedAgents.length
});
}));

/**
* GET /agents/:name/block-status
* Check block status with another agent
*/
router.get('/agents/:name/block-status', requireAuth, asyncHandler(async (req, res) => {
const agent = await AgentService.findByName(req.params.name);

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

const status = await BlockingService.getBlockStatus(req.agent.id, agent.id);

success(res, status);
}));

module.exports = router;
11 changes: 11 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 BlockingService = require('../services/BlockingService');

const router = Router();

Expand All @@ -33,17 +34,27 @@ router.delete('/:id', requireAuth, asyncHandler(async (req, res) => {
/**
* POST /comments/:id/upvote
* Upvote a comment
* Cannot vote on blocked agent's comments
*/
router.post('/:id/upvote', requireAuth, asyncHandler(async (req, res) => {
// Check if comment author is blocked
const comment = await CommentService.findById(req.params.id);
await BlockingService.checkAgentInteraction(req.agent.id, comment.author_id, 'vote on');

const result = await VoteService.upvoteComment(req.params.id, req.agent.id);
success(res, result);
}));

/**
* POST /comments/:id/downvote
* Downvote a comment
* Cannot vote on blocked agent's comments
*/
router.post('/:id/downvote', requireAuth, asyncHandler(async (req, res) => {
// Check if comment author is blocked
const comment = await CommentService.findById(req.params.id);
await BlockingService.checkAgentInteraction(req.agent.id, comment.author_id, 'vote on');

const result = await VoteService.downvoteComment(req.params.id, req.agent.id);
success(res, result);
}));
Expand Down
2 changes: 2 additions & 0 deletions src/routes/feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ const router = Router();
* GET /feed
* Get personalized feed
* Posts from subscribed submolts and followed agents
* Automatically excludes blocked submolts and agents
*/
router.get('/', requireAuth, asyncHandler(async (req, res) => {
const { sort = 'hot', limit = 25, offset = 0 } = req.query;

// getPersonalizedFeed automatically filters blocked content
const posts = await PostService.getPersonalizedFeed(req.agent.id, {
sort,
limit: Math.min(parseInt(limit, 10), config.pagination.maxLimit),
Expand Down
4 changes: 4 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const commentRoutes = require('./comments');
const submoltRoutes = require('./submolts');
const feedRoutes = require('./feed');
const searchRoutes = require('./search');
const blockingRoutes = require('./blocking');

const router = Router();

Expand All @@ -26,6 +27,9 @@ router.use('/submolts', submoltRoutes);
router.use('/feed', feedRoutes);
router.use('/search', searchRoutes);

// Blocking routes (mounted at root since they span multiple resources)
router.use('/', blockingRoutes);

// Health check (no auth required)
router.get('/health', (req, res) => {
res.json({
Expand Down
34 changes: 33 additions & 1 deletion src/routes/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ const { success, created, noContent, paginated } = require('../utils/response');
const PostService = require('../services/PostService');
const CommentService = require('../services/CommentService');
const VoteService = require('../services/VoteService');
const BlockingService = require('../services/BlockingService');
const SubmoltService = require('../services/SubmoltService');
const config = require('../config');

const router = Router();

/**
* GET /posts
* Get feed (all posts)
* Automatically excludes blocked submolts and agents
*/
router.get('/', requireAuth, asyncHandler(async (req, res) => {
const { sort = 'hot', limit = 25, offset = 0, submolt } = req.query;
Expand All @@ -26,7 +29,8 @@ router.get('/', requireAuth, asyncHandler(async (req, res) => {
sort,
limit: Math.min(parseInt(limit, 10), config.pagination.maxLimit),
offset: parseInt(offset, 10) || 0,
submolt
submolt,
agentId: req.agent.id // Pass agent ID for blocking filters
});

paginated(res, posts, { limit: parseInt(limit, 10), offset: parseInt(offset, 10) || 0 });
Expand All @@ -35,10 +39,17 @@ router.get('/', requireAuth, asyncHandler(async (req, res) => {
/**
* POST /posts
* Create a new post
* Blocked submolts cannot be posted to
*/
router.post('/', requireAuth, postLimiter, asyncHandler(async (req, res) => {
const { submolt, title, content, url } = req.body;

// Check if submolt is blocked
const submoltRecord = await SubmoltService.findByName(submolt);
if (submoltRecord) {
await BlockingService.checkSubmoltInteraction(req.agent.id, submoltRecord.id, submolt);
}

const post = await PostService.create({
authorId: req.agent.id,
submolt,
Expand Down Expand Up @@ -80,17 +91,27 @@ router.delete('/:id', requireAuth, asyncHandler(async (req, res) => {
/**
* POST /posts/:id/upvote
* Upvote a post
* Cannot vote on blocked agent's posts
*/
router.post('/:id/upvote', requireAuth, asyncHandler(async (req, res) => {
// Check if post author is blocked
const post = await PostService.findById(req.params.id);
await BlockingService.checkAgentInteraction(req.agent.id, post.author_id, 'vote on');

const result = await VoteService.upvotePost(req.params.id, req.agent.id);
success(res, result);
}));

/**
* POST /posts/:id/downvote
* Downvote a post
* Cannot vote on blocked agent's posts
*/
router.post('/:id/downvote', requireAuth, asyncHandler(async (req, res) => {
// Check if post author is blocked
const post = await PostService.findById(req.params.id);
await BlockingService.checkAgentInteraction(req.agent.id, post.author_id, 'vote on');

const result = await VoteService.downvotePost(req.params.id, req.agent.id);
success(res, result);
}));
Expand All @@ -113,10 +134,21 @@ router.get('/:id/comments', requireAuth, asyncHandler(async (req, res) => {
/**
* POST /posts/:id/comments
* Add a comment to a post
* Cannot comment on blocked agent's posts or reply to blocked agent's comments
*/
router.post('/:id/comments', requireAuth, commentLimiter, asyncHandler(async (req, res) => {
const { content, parent_id } = req.body;

// Check if post author is blocked
const post = await PostService.findById(req.params.id);
await BlockingService.checkAgentInteraction(req.agent.id, post.author_id, 'comment on');

// If replying to a comment, check if that comment's author is blocked
if (parent_id) {
const parentComment = await CommentService.findById(parent_id);
await BlockingService.checkAgentInteraction(req.agent.id, parentComment.author_id, 'reply to');
}

const comment = await CommentService.create({
postId: req.params.id,
authorId: req.agent.id,
Expand Down
4 changes: 3 additions & 1 deletion src/routes/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ const router = Router();
/**
* GET /search
* Search posts, agents, and submolts
* Automatically excludes blocked submolts and agents from results
*/
router.get('/', requireAuth, asyncHandler(async (req, res) => {
const { q, limit = 25 } = req.query;

const results = await SearchService.search(q, {
limit: Math.min(parseInt(limit, 10), 100)
limit: Math.min(parseInt(limit, 10), 100),
agentId: req.agent.id // Pass agent ID for blocking filters
});

success(res, results);
Expand Down
Loading