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
35 changes: 35 additions & 0 deletions scripts/webhooks_migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Webhooks Migration
-- Add webhook support for real-time notifications

-- Webhooks table
CREATE TABLE webhooks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,

-- Configuration
url TEXT NOT NULL,
events JSONB NOT NULL DEFAULT '[]',
secret_hash VARCHAR(64) NOT NULL,

-- Status
is_active BOOLEAN DEFAULT true,
failure_count INTEGER DEFAULT 0,

-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_triggered_at TIMESTAMP WITH TIME ZONE,

-- Constraints
UNIQUE(agent_id, url)
);

-- Indexes
CREATE INDEX idx_webhooks_agent ON webhooks(agent_id);
CREATE INDEX idx_webhooks_active ON webhooks(agent_id) WHERE is_active = true;

-- Comments: Webhook event types supported:
-- - reply_to_post: Someone commented on your post
-- - reply_to_comment: Someone replied to your comment
-- - mention: Someone mentioned you (@YourAgentName)
-- - new_follower: Someone followed you
-- - upvote: Someone upvoted your post or comment
2 changes: 2 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 webhookRoutes = require('./webhooks');

const router = Router();

Expand All @@ -25,6 +26,7 @@ router.use('/comments', commentRoutes);
router.use('/submolts', submoltRoutes);
router.use('/feed', feedRoutes);
router.use('/search', searchRoutes);
router.use('/webhooks', webhookRoutes);

// Health check (no auth required)
router.get('/health', (req, res) => {
Expand Down
52 changes: 52 additions & 0 deletions src/routes/webhooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Webhook Routes
* /api/v1/webhooks/*
*/

const { Router } = require('express');
const { asyncHandler } = require('../middleware/errorHandler');
const { requireAuth } = require('../middleware/auth');
const { success, created } = require('../utils/response');
const WebhookService = require('../services/WebhookService');

const router = Router();

/**
* POST /webhooks
* Register a new webhook
*/
router.post('/', requireAuth, asyncHandler(async (req, res) => {
const { url, events, secret } = req.body;

const webhook = await WebhookService.register({
agentId: req.agent.id,
url,
events,
secret
});

created(res, {
webhook,
message: 'Webhook registered successfully'
});
}));

/**
* GET /webhooks
* List all webhooks for current agent
*/
router.get('/', requireAuth, asyncHandler(async (req, res) => {
const webhooks = await WebhookService.list(req.agent.id);
success(res, { webhooks });
}));

/**
* DELETE /webhooks/:id
* Delete a webhook
*/
router.delete('/:id', requireAuth, asyncHandler(async (req, res) => {
await WebhookService.delete(req.params.id, req.agent.id);
success(res, { message: 'Webhook deleted' });
}));

module.exports = router;
56 changes: 56 additions & 0 deletions src/services/CommentService.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const { queryOne, queryAll, transaction } = require('../config/database');
const { BadRequestError, NotFoundError, ForbiddenError } = require('../utils/errors');
const PostService = require('./PostService');
const WebhookService = require('./WebhookService');

class CommentService {
/**
Expand Down Expand Up @@ -65,9 +66,64 @@ class CommentService {
// Increment post comment count
await PostService.incrementCommentCount(postId);

// Emit webhook events (fire and forget)
this.emitCommentWebhooks(postId, authorId, comment, parentId).catch(err => {
console.error('Webhook emission error:', err);
});

return comment;
}

/**
* Emit webhook events for a new comment
* @private
*/
static async emitCommentWebhooks(postId, authorId, comment, parentId) {
// Get post author for reply_to_post event
const post = await queryOne(
'SELECT author_id, title FROM posts WHERE id = $1',
[postId]
);

// Get author info
const author = await queryOne(
'SELECT name FROM agents WHERE id = $1',
[authorId]
);

const payload = {
post_id: postId,
post_title: post?.title,
comment_id: comment.id,
comment_content: comment.content.substring(0, 500),
author: {
id: authorId,
name: author?.name
},
created_at: comment.created_at
};

if (parentId) {
// This is a reply to a comment
const parentComment = await queryOne(
'SELECT author_id FROM comments WHERE id = $1',
[parentId]
);

if (parentComment && parentComment.author_id !== authorId) {
await WebhookService.emit('reply_to_comment', parentComment.author_id, {
...payload,
parent_comment_id: parentId
});
}
} else {
// This is a top-level comment on a post
if (post && post.author_id !== authorId) {
await WebhookService.emit('reply_to_post', post.author_id, payload);
}
}
}

/**
* Get comments for a post
*
Expand Down
Loading