@foru-ms/sdk 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -11
- package/dist/Client.d.ts +63 -4
- package/dist/Client.js +124 -22
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +95 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/resources/Integrations.d.ts +7 -0
- package/dist/resources/Integrations.js +6 -0
- package/dist/resources/Posts.d.ts +12 -0
- package/dist/resources/Posts.js +44 -0
- package/dist/resources/PrivateMessages.d.ts +4 -0
- package/dist/resources/PrivateMessages.js +6 -0
- package/dist/resources/SSO.d.ts +10 -0
- package/dist/resources/SSO.js +11 -0
- package/dist/resources/Tags.d.ts +8 -0
- package/dist/resources/Tags.js +24 -0
- package/dist/resources/Threads.d.ts +20 -0
- package/dist/resources/Threads.js +85 -0
- package/dist/resources/Users.d.ts +10 -0
- package/dist/resources/Users.js +26 -0
- package/dist/resources/Webhooks.d.ts +69 -0
- package/dist/resources/Webhooks.js +115 -0
- package/dist/response-types.d.ts +105 -0
- package/dist/response-types.js +2 -0
- package/dist/utils.d.ts +80 -0
- package/dist/utils.js +138 -0
- package/examples/README.md +38 -0
- package/examples/authentication.ts +79 -0
- package/examples/error-handling.ts +133 -0
- package/examples/managing-threads.ts +130 -0
- package/examples/pagination.ts +81 -0
- package/examples/webhooks.ts +176 -0
- package/package.json +1 -1
- package/src/Client.ts +165 -25
- package/src/errors.ts +95 -0
- package/src/index.ts +3 -0
- package/src/resources/Integrations.ts +11 -0
- package/src/resources/Posts.ts +56 -0
- package/src/resources/PrivateMessages.ts +10 -0
- package/src/resources/SSO.ts +17 -0
- package/src/resources/Tags.ts +32 -0
- package/src/resources/Threads.ts +109 -0
- package/src/resources/Users.ts +36 -0
- package/src/resources/Webhooks.ts +131 -0
- package/src/response-types.ts +113 -0
- package/src/utils.ts +182 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ForumClient, AuthenticationError } from '@foru-ms/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example: Authentication with Foru.ms SDK
|
|
5
|
+
* Demonstrates user registration, login, and token management
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
// Initialize client with API key
|
|
10
|
+
const client = new ForumClient({
|
|
11
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Example 1: User Registration
|
|
16
|
+
console.log('=== User Registration ===');
|
|
17
|
+
const newUser = await client.auth.register({
|
|
18
|
+
username: 'john_doe',
|
|
19
|
+
email: 'john@example.com',
|
|
20
|
+
password: 'securePassword123',
|
|
21
|
+
displayName: 'John Doe',
|
|
22
|
+
});
|
|
23
|
+
console.log('User registered:', newUser);
|
|
24
|
+
|
|
25
|
+
// Example 2: User Login
|
|
26
|
+
console.log('\n=== User Login ===');
|
|
27
|
+
const loginResponse = await client.auth.login({
|
|
28
|
+
login: 'john@example.com', // Can be username or email
|
|
29
|
+
password: 'securePassword123',
|
|
30
|
+
});
|
|
31
|
+
console.log('Login successful, token:', loginResponse.token);
|
|
32
|
+
|
|
33
|
+
// Store the token for authenticated requests
|
|
34
|
+
client.setToken(loginResponse.token);
|
|
35
|
+
|
|
36
|
+
// Example 3: Get Current User
|
|
37
|
+
console.log('\n=== Get Current User ===');
|
|
38
|
+
const currentUser = await client.auth.me();
|
|
39
|
+
console.log('Current user:', currentUser);
|
|
40
|
+
|
|
41
|
+
// Example 4: Password Reset Flow
|
|
42
|
+
console.log('\n=== Password Reset ===');
|
|
43
|
+
|
|
44
|
+
// Step 1: Request password reset
|
|
45
|
+
await client.auth.forgotPassword('john@example.com');
|
|
46
|
+
console.log('Password reset email sent');
|
|
47
|
+
|
|
48
|
+
// Step 2: Reset password with token (received via email)
|
|
49
|
+
// Note: In a real app, this would be done after user clicks the email link
|
|
50
|
+
const resetToken = 'token_from_email';
|
|
51
|
+
await client.auth.resetPassword({
|
|
52
|
+
password: 'newSecurePassword123',
|
|
53
|
+
token: resetToken,
|
|
54
|
+
});
|
|
55
|
+
console.log('Password reset successful');
|
|
56
|
+
|
|
57
|
+
// Example 5: Check Authentication Status
|
|
58
|
+
console.log('\n=== Authentication Status ===');
|
|
59
|
+
console.log('Is authenticated:', client.isAuthenticated());
|
|
60
|
+
console.log('Current token:', client.token);
|
|
61
|
+
|
|
62
|
+
// Example 6: Logout (clear token)
|
|
63
|
+
console.log('\n=== Logout ===');
|
|
64
|
+
client.clearToken();
|
|
65
|
+
console.log('Token cleared');
|
|
66
|
+
console.log('Is authenticated:', client.isAuthenticated());
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof AuthenticationError) {
|
|
70
|
+
console.error('Authentication failed:', error.message);
|
|
71
|
+
console.error('Status code:', error.statusCode);
|
|
72
|
+
} else {
|
|
73
|
+
console.error('An error occurred:', error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Run the example
|
|
79
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForumClient,
|
|
3
|
+
ForumAPIError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
AuthorizationError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServerError,
|
|
10
|
+
NetworkError
|
|
11
|
+
} from '@foru-ms/sdk';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Example: Error Handling
|
|
15
|
+
* Demonstrates proper error handling with custom error classes
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const client = new ForumClient({
|
|
20
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
21
|
+
maxRetries: 3,
|
|
22
|
+
enableRetry: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Example 1: Catch specific error types
|
|
26
|
+
console.log('=== Handling Specific Errors ===');
|
|
27
|
+
try {
|
|
28
|
+
await client.auth.login({
|
|
29
|
+
login: 'nonexistent@example.com',
|
|
30
|
+
password: 'wrong_password',
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof AuthenticationError) {
|
|
34
|
+
console.log('Authentication failed');
|
|
35
|
+
console.log('Message:', error.message);
|
|
36
|
+
console.log('Status:', error.statusCode);
|
|
37
|
+
} else if (error instanceof NetworkError) {
|
|
38
|
+
console.log('Network error occurred');
|
|
39
|
+
console.log('Cause:', error.cause);
|
|
40
|
+
} else {
|
|
41
|
+
console.log('Unexpected error:', error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Example 2: Handle not found errors
|
|
46
|
+
console.log('\n=== Handling Not Found ===');
|
|
47
|
+
try {
|
|
48
|
+
await client.threads.retrieve('non-existent-id');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof NotFoundError) {
|
|
51
|
+
console.log('Thread not found');
|
|
52
|
+
console.log('Status:', error.statusCode); // 404
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Example 3: Handle validation errors
|
|
57
|
+
console.log('\n=== Handling Validation Errors ===');
|
|
58
|
+
try {
|
|
59
|
+
await client.threads.create({
|
|
60
|
+
title: '', // Invalid: empty title
|
|
61
|
+
body: 'Test',
|
|
62
|
+
userId: 'user-123',
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof ValidationError) {
|
|
66
|
+
console.log('Validation failed');
|
|
67
|
+
console.log('Details:', error.response);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Example 4: Handle rate limits
|
|
72
|
+
console.log('\n=== Handling Rate Limits ===');
|
|
73
|
+
try {
|
|
74
|
+
// Make many requests quickly
|
|
75
|
+
for (let i = 0; i < 100; i++) {
|
|
76
|
+
await client.threads.list();
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof RateLimitError) {
|
|
80
|
+
console.log('Rate limit exceeded');
|
|
81
|
+
console.log('Retry after:', error.retryAfter, 'seconds');
|
|
82
|
+
console.log('Wait until:', new Date(Date.now() + error.retryAfter! * 1000));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Example 5: Handle server errors with retry
|
|
87
|
+
console.log('\n=== Handling Server Errors ===');
|
|
88
|
+
const clientWithoutRetry = new ForumClient({
|
|
89
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
90
|
+
enableRetry: false, // Disable auto-retry
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// This might fail with 500 error
|
|
95
|
+
await clientWithoutRetry.threads.list();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof ServerError) {
|
|
98
|
+
console.log('Server error occurred');
|
|
99
|
+
console.log('Status:', error.statusCode);
|
|
100
|
+
console.log('You might want to retry manually');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Example 6: Generic error handling
|
|
105
|
+
console.log('\n=== Generic Error Handling ===');
|
|
106
|
+
try {
|
|
107
|
+
await client.threads.delete('thread-123');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof ForumAPIError) {
|
|
110
|
+
// All API errors inherit from ForumAPIError
|
|
111
|
+
console.log('API Error:', error.message);
|
|
112
|
+
console.log('Status Code:', error.statusCode);
|
|
113
|
+
console.log('Response:', error.response);
|
|
114
|
+
} else {
|
|
115
|
+
// Non-API errors (network, etc.)
|
|
116
|
+
console.log('Unexpected error:', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Example 7: Authorization errors
|
|
121
|
+
console.log('\n=== Handling Authorization Errors ===');
|
|
122
|
+
try {
|
|
123
|
+
// Try to access admin-only endpoint
|
|
124
|
+
await client.users.delete('some-user-id');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof AuthorizationError) {
|
|
127
|
+
console.log('Permission denied');
|
|
128
|
+
console.log('Status:', error.statusCode); // 403
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ForumClient } from '@foru-ms/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example: Managing Threads
|
|
5
|
+
* Demonstrates thread creation, updates, and interactions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const client = new ForumClient({
|
|
10
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Set user token for authenticated actions
|
|
14
|
+
client.setToken(process.env.FORU_USER_TOKEN || 'user_token');
|
|
15
|
+
|
|
16
|
+
const userId = 'user-123';
|
|
17
|
+
|
|
18
|
+
// Example 1: Create a Thread
|
|
19
|
+
console.log('=== Creating a Thread ===');
|
|
20
|
+
const newThread = await client.threads.create({
|
|
21
|
+
title: 'How to use the SDK?',
|
|
22
|
+
body: 'I need help getting started with the Foru.ms SDK.',
|
|
23
|
+
userId,
|
|
24
|
+
tags: ['help', 'sdk'],
|
|
25
|
+
});
|
|
26
|
+
console.log('Thread created:', newThread.id);
|
|
27
|
+
|
|
28
|
+
// Example 2: Create a Thread with Poll
|
|
29
|
+
console.log('\n=== Creating a Thread with Poll ===');
|
|
30
|
+
const pollThread = await client.threads.create({
|
|
31
|
+
title: 'What\'s your favorite feature?',
|
|
32
|
+
body: 'Vote for your favorite SDK feature!',
|
|
33
|
+
userId,
|
|
34
|
+
poll: {
|
|
35
|
+
title: 'Favorite Feature',
|
|
36
|
+
options: [
|
|
37
|
+
{ title: 'Type Safety', color: '#3B82F6' },
|
|
38
|
+
{ title: 'Auto Pagination', color: '#10B981' },
|
|
39
|
+
{ title: 'Error Handling', color: '#F59E0B' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
console.log('Poll thread created:', pollThread.id);
|
|
44
|
+
|
|
45
|
+
// Example 3: Update a Thread
|
|
46
|
+
console.log('\n=== Updating a Thread ===');
|
|
47
|
+
const updatedThread = await client.threads.update(newThread.id, {
|
|
48
|
+
title: 'How to use the SDK? [SOLVED]',
|
|
49
|
+
locked: false,
|
|
50
|
+
pinned: true,
|
|
51
|
+
});
|
|
52
|
+
console.log('Thread updated');
|
|
53
|
+
|
|
54
|
+
// Example 4: Like a Thread
|
|
55
|
+
console.log('\n=== Liking a Thread ===');
|
|
56
|
+
await client.threads.like(newThread.id, userId);
|
|
57
|
+
console.log('Thread liked');
|
|
58
|
+
|
|
59
|
+
// Get likes
|
|
60
|
+
const likes = await client.threads.getLikes(newThread.id);
|
|
61
|
+
console.log('Total likes:', likes.count);
|
|
62
|
+
|
|
63
|
+
// Example 5: Subscribe to a Thread
|
|
64
|
+
console.log('\n=== Subscribing to Thread ===');
|
|
65
|
+
await client.threads.subscribe(newThread.id, userId);
|
|
66
|
+
console.log('Subscribed to thread');
|
|
67
|
+
|
|
68
|
+
// Example 6: Vote in a Poll
|
|
69
|
+
console.log('\n=== Voting in Poll ===');
|
|
70
|
+
await client.threads.vote(pollThread.id, 'option-id-1', userId);
|
|
71
|
+
console.log('Vote cast');
|
|
72
|
+
|
|
73
|
+
// Get poll results
|
|
74
|
+
const pollResults = await client.threads.getPoll(pollThread.id, userId);
|
|
75
|
+
console.log('Poll results:', pollResults);
|
|
76
|
+
|
|
77
|
+
// Example 7: Add Posts to Thread
|
|
78
|
+
console.log('\n=== Adding Posts ===');
|
|
79
|
+
const post = await client.posts.create({
|
|
80
|
+
threadId: newThread.id,
|
|
81
|
+
body: 'Thanks for posting! Check out our documentation.',
|
|
82
|
+
userId: 'moderator-123',
|
|
83
|
+
});
|
|
84
|
+
console.log('Post created:', post.id);
|
|
85
|
+
|
|
86
|
+
// Reply to the post
|
|
87
|
+
const reply = await client.posts.create({
|
|
88
|
+
threadId: newThread.id,
|
|
89
|
+
body: 'Thank you! That helps a lot.',
|
|
90
|
+
userId,
|
|
91
|
+
parentId: post.id, // This makes it a reply
|
|
92
|
+
});
|
|
93
|
+
console.log('Reply created:', reply.id);
|
|
94
|
+
|
|
95
|
+
// Example 8: Get Thread Posts
|
|
96
|
+
console.log('\n=== Getting Thread Posts ===');
|
|
97
|
+
const threadPosts = await client.threads.getPosts(newThread.id, {
|
|
98
|
+
filter: 'newest',
|
|
99
|
+
});
|
|
100
|
+
console.log('Total posts:', threadPosts.count);
|
|
101
|
+
|
|
102
|
+
// Example 9: Upvote/Downvote Threads
|
|
103
|
+
console.log('\n=== Voting on Threads ===');
|
|
104
|
+
await client.threads.upvote(newThread.id, userId);
|
|
105
|
+
console.log('Thread upvoted');
|
|
106
|
+
|
|
107
|
+
const upvotes = await client.threads.getUpvotes(newThread.id);
|
|
108
|
+
console.log('Total upvotes:', upvotes.count);
|
|
109
|
+
|
|
110
|
+
// Example 10: List Threads with Filters
|
|
111
|
+
console.log('\n=== Listing Threads ===');
|
|
112
|
+
const threads = await client.threads.list({
|
|
113
|
+
filter: 'newest',
|
|
114
|
+
tagId: 'help',
|
|
115
|
+
query: 'SDK',
|
|
116
|
+
});
|
|
117
|
+
console.log('Found threads:', threads.threads.length);
|
|
118
|
+
|
|
119
|
+
// Example 11: Get Subscribers
|
|
120
|
+
console.log('\n=== Getting Subscribers ===');
|
|
121
|
+
const subscribers = await client.threads.getSubscribers(newThread.id);
|
|
122
|
+
console.log('Total subscribers:', subscribers.count);
|
|
123
|
+
|
|
124
|
+
// Example 12: Delete a Thread
|
|
125
|
+
console.log('\n=== Deleting a Thread ===');
|
|
126
|
+
const deleted = await client.threads.delete(newThread.id, { userId });
|
|
127
|
+
console.log('Thread deleted:', deleted.deleted);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ForumClient } from '@foru-ms/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example: Working with Pagination
|
|
5
|
+
* Demonstrates different ways to paginate through results
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const client = new ForumClient({
|
|
10
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Example 1: Manual Pagination
|
|
14
|
+
console.log('=== Manual Pagination ===');
|
|
15
|
+
let cursor: string | undefined;
|
|
16
|
+
let pageCount = 0;
|
|
17
|
+
const maxPages = 3;
|
|
18
|
+
|
|
19
|
+
do {
|
|
20
|
+
const response = await client.threads.list({ cursor, filter: 'newest' });
|
|
21
|
+
console.log(`Page ${++pageCount}:`, response.threads.length, 'threads');
|
|
22
|
+
|
|
23
|
+
// Process threads
|
|
24
|
+
response.threads.forEach(thread => {
|
|
25
|
+
console.log(` - ${thread.title}`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
cursor = response.nextThreadCursor;
|
|
29
|
+
} while (cursor && pageCount < maxPages);
|
|
30
|
+
|
|
31
|
+
// Example 2: Auto-pagination with async iterator
|
|
32
|
+
console.log('\n=== Auto-Pagination (Async Iterator) ===');
|
|
33
|
+
let count = 0;
|
|
34
|
+
const maxItems = 10;
|
|
35
|
+
|
|
36
|
+
for await (const thread of client.pagination.paginateAll(
|
|
37
|
+
(cursor) => client.threads.list({ cursor, filter: 'newest' })
|
|
38
|
+
)) {
|
|
39
|
+
console.log(`Thread ${++count}:`, thread.title);
|
|
40
|
+
|
|
41
|
+
if (count >= maxItems) break; // Limit for demo
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Example 3: Fetch all pages at once
|
|
45
|
+
console.log('\n=== Fetch All Pages ===');
|
|
46
|
+
const allThreads = await client.pagination.fetchAllPages(
|
|
47
|
+
(cursor) => client.threads.list({ cursor }),
|
|
48
|
+
3 // Max 3 pages
|
|
49
|
+
);
|
|
50
|
+
console.log('Total threads fetched:', allThreads.length);
|
|
51
|
+
|
|
52
|
+
// Example 4: Paginate with filters
|
|
53
|
+
console.log('\n=== Pagination with Filters ===');
|
|
54
|
+
cursor = undefined;
|
|
55
|
+
pageCount = 0;
|
|
56
|
+
|
|
57
|
+
do {
|
|
58
|
+
const response = await client.posts.list({
|
|
59
|
+
cursor,
|
|
60
|
+
filter: 'newest',
|
|
61
|
+
type: 'created', // Only posts created by user
|
|
62
|
+
userId: 'user-123',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(`Page ${++pageCount}:`, response.posts.length, 'posts');
|
|
66
|
+
cursor = response.nextPostCursor;
|
|
67
|
+
} while (cursor && pageCount < 2);
|
|
68
|
+
|
|
69
|
+
// Example 5: Check rate limits during pagination
|
|
70
|
+
console.log('\n=== Rate Limit Monitoring ===');
|
|
71
|
+
await client.threads.list();
|
|
72
|
+
|
|
73
|
+
if (client.lastRateLimitInfo) {
|
|
74
|
+
console.log('Rate Limit Info:');
|
|
75
|
+
console.log(' Limit:', client.lastRateLimitInfo.limit);
|
|
76
|
+
console.log(' Remaining:', client.lastRateLimitInfo.remaining);
|
|
77
|
+
console.log(' Resets at:', new Date(client.lastRateLimitInfo.reset * 1000));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ForumClient } from '@foru-ms/sdk';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example: Webhooks
|
|
6
|
+
* Demonstrates setting up, handling, and verifying webhooks
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
async function setupWebhooks() {
|
|
10
|
+
const client = new ForumClient({
|
|
11
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Example 1: Create a Webhook
|
|
15
|
+
console.log('=== Creating a Webhook ===');
|
|
16
|
+
const webhook = await client.webhooks.create({
|
|
17
|
+
name: 'Thread Activity Monitor',
|
|
18
|
+
url: 'https://your-domain.com/webhooks/foru',
|
|
19
|
+
events: [
|
|
20
|
+
'thread.created',
|
|
21
|
+
'thread.updated',
|
|
22
|
+
'thread.deleted',
|
|
23
|
+
'post.created',
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
console.log('Webhook created:', webhook.webhook.id);
|
|
27
|
+
console.log('Secret:', webhook.webhook.secret); // Save this securely!
|
|
28
|
+
|
|
29
|
+
// Example 2: List All Webhooks
|
|
30
|
+
console.log('\n=== Listing Webhooks ===');
|
|
31
|
+
const webhooks = await client.webhooks.list();
|
|
32
|
+
console.log('Total webhooks:', webhooks.webhooks.length);
|
|
33
|
+
|
|
34
|
+
// Example 3: Update a Webhook
|
|
35
|
+
console.log('\n=== Updating a Webhook ===');
|
|
36
|
+
await client.webhooks.update(webhook.webhook.id, {
|
|
37
|
+
active: true,
|
|
38
|
+
events: [
|
|
39
|
+
'thread.created',
|
|
40
|
+
'thread.updated',
|
|
41
|
+
'thread.deleted',
|
|
42
|
+
'post.created',
|
|
43
|
+
'post.updated', // Added new event
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
console.log('Webhook updated');
|
|
47
|
+
|
|
48
|
+
// Example 4: Get Webhook Deliveries
|
|
49
|
+
console.log('\n=== Getting Webhook Deliveries ===');
|
|
50
|
+
const deliveries = await client.webhooks.getDeliveries(webhook.webhook.id);
|
|
51
|
+
console.log('Total deliveries:', deliveries.total);
|
|
52
|
+
console.log('Recent deliveries:', deliveries.deliveries.slice(0, 5));
|
|
53
|
+
|
|
54
|
+
return webhook.webhook.secret;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Example webhook handler using Express
|
|
59
|
+
*/
|
|
60
|
+
function setupWebhookHandler(secret: string) {
|
|
61
|
+
const app = express();
|
|
62
|
+
const client = new ForumClient({
|
|
63
|
+
apiKey: process.env.FORU_API_KEY || 'your_api_key',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Important: Use raw body for signature verification
|
|
67
|
+
app.use(express.json({
|
|
68
|
+
verify: (req: any, res, buf) => {
|
|
69
|
+
req.rawBody = buf.toString();
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
app.post('/webhooks/foru', (req: any, res) => {
|
|
74
|
+
// Example 5: Verify Webhook Signature with Timestamp
|
|
75
|
+
const signature = req.headers['x-webhook-signature'];
|
|
76
|
+
const timestamp = req.headers['x-webhook-timestamp'];
|
|
77
|
+
const event = req.headers['x-webhook-event'];
|
|
78
|
+
|
|
79
|
+
// Verify timestamp age (prevent replay attacks)
|
|
80
|
+
const age = Date.now() - parseInt(timestamp);
|
|
81
|
+
if (age > 5 * 60 * 1000) { // 5 minutes
|
|
82
|
+
console.error('Webhook timestamp too old');
|
|
83
|
+
return res.status(400).send('Webhook timestamp too old');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Verify signature
|
|
87
|
+
const isValid = client.webhooks.verifySignature(
|
|
88
|
+
req.body,
|
|
89
|
+
signature,
|
|
90
|
+
timestamp,
|
|
91
|
+
secret
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!isValid) {
|
|
95
|
+
console.error('Invalid webhook signature');
|
|
96
|
+
return res.status(401).send('Invalid signature');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Process webhook payload
|
|
100
|
+
console.log('Received webhook event:', event);
|
|
101
|
+
const payload = req.body;
|
|
102
|
+
|
|
103
|
+
switch (event) {
|
|
104
|
+
case 'thread.created':
|
|
105
|
+
handleThreadCreated(payload);
|
|
106
|
+
break;
|
|
107
|
+
case 'thread.updated':
|
|
108
|
+
handleThreadUpdated(payload);
|
|
109
|
+
break;
|
|
110
|
+
case 'thread.deleted':
|
|
111
|
+
handleThreadDeleted(payload);
|
|
112
|
+
break;
|
|
113
|
+
case 'post.created':
|
|
114
|
+
handlePostCreated(payload);
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
console.log('Unhandled event type:', event);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Always respond quickly to acknowledge receipt
|
|
121
|
+
res.status(200).send('OK');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const PORT = process.env.PORT || 3000;
|
|
125
|
+
app.listen(PORT, () => {
|
|
126
|
+
console.log(`\nWebhook handler listening on port ${PORT}`);
|
|
127
|
+
console.log(`Endpoint: http://localhost:${PORT}/webhooks/foru`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Event handler functions
|
|
132
|
+
function handleThreadCreated(thread: any) {
|
|
133
|
+
console.log('New thread created:', thread.id);
|
|
134
|
+
console.log('Title:', thread.title);
|
|
135
|
+
console.log('Author:', thread.userId);
|
|
136
|
+
|
|
137
|
+
// Example: Send notification, update database, etc.
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleThreadUpdated(thread: any) {
|
|
141
|
+
console.log('Thread updated:', thread.id);
|
|
142
|
+
console.log('Title:', thread.title);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleThreadDeleted(thread: any) {
|
|
146
|
+
console.log('Thread deleted:', thread.id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handlePostCreated(post: any) {
|
|
150
|
+
console.log('New post created:', post.id);
|
|
151
|
+
console.log('Thread:', post.threadId);
|
|
152
|
+
console.log('Author:', post.userId);
|
|
153
|
+
|
|
154
|
+
// Example: Check for spam, auto-moderate, etc.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Main execution
|
|
158
|
+
async function main() {
|
|
159
|
+
try {
|
|
160
|
+
// Setup webhooks
|
|
161
|
+
const secret = await setupWebhooks();
|
|
162
|
+
|
|
163
|
+
// Start webhook handler server
|
|
164
|
+
// setupWebhookHandler(secret);
|
|
165
|
+
|
|
166
|
+
console.log('\n=== Webhook Setup Complete ===');
|
|
167
|
+
console.log('Remember to save your webhook secret securely!');
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error setting up webhooks:', error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// For demonstration, we'll just setup webhooks
|
|
175
|
+
// Uncomment the line above to also start the webhook handler server
|
|
176
|
+
main().catch(console.error);
|