@foru-ms/sdk 1.1.1 → 1.2.1

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.
Files changed (47) hide show
  1. package/README.md +293 -11
  2. package/dist/Client.d.ts +63 -4
  3. package/dist/Client.js +124 -22
  4. package/dist/errors.d.ts +52 -0
  5. package/dist/errors.js +95 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +3 -0
  8. package/dist/resources/Integrations.d.ts +7 -0
  9. package/dist/resources/Integrations.js +6 -0
  10. package/dist/resources/Posts.d.ts +12 -0
  11. package/dist/resources/Posts.js +44 -0
  12. package/dist/resources/PrivateMessages.d.ts +4 -0
  13. package/dist/resources/PrivateMessages.js +6 -0
  14. package/dist/resources/SSO.d.ts +10 -0
  15. package/dist/resources/SSO.js +11 -0
  16. package/dist/resources/Tags.d.ts +8 -0
  17. package/dist/resources/Tags.js +24 -0
  18. package/dist/resources/Threads.d.ts +20 -0
  19. package/dist/resources/Threads.js +85 -0
  20. package/dist/resources/Users.d.ts +10 -0
  21. package/dist/resources/Users.js +26 -0
  22. package/dist/resources/Webhooks.d.ts +69 -0
  23. package/dist/resources/Webhooks.js +115 -0
  24. package/dist/response-types.d.ts +105 -0
  25. package/dist/response-types.js +2 -0
  26. package/dist/utils.d.ts +80 -0
  27. package/dist/utils.js +138 -0
  28. package/examples/README.md +38 -0
  29. package/examples/authentication.ts +79 -0
  30. package/examples/error-handling.ts +133 -0
  31. package/examples/managing-threads.ts +130 -0
  32. package/examples/pagination.ts +81 -0
  33. package/examples/webhooks.ts +176 -0
  34. package/package.json +1 -1
  35. package/src/Client.ts +165 -25
  36. package/src/errors.ts +95 -0
  37. package/src/index.ts +3 -0
  38. package/src/resources/Integrations.ts +11 -0
  39. package/src/resources/Posts.ts +56 -0
  40. package/src/resources/PrivateMessages.ts +10 -0
  41. package/src/resources/SSO.ts +17 -0
  42. package/src/resources/Tags.ts +32 -0
  43. package/src/resources/Threads.ts +109 -0
  44. package/src/resources/Users.ts +36 -0
  45. package/src/resources/Webhooks.ts +131 -0
  46. package/src/response-types.ts +113 -0
  47. package/src/utils.ts +182 -0
package/src/Client.ts CHANGED
@@ -12,14 +12,58 @@ import { PrivateMessagesResource } from './resources/PrivateMessages';
12
12
  import { ReportsResource } from './resources/Reports';
13
13
  import { RolesResource } from './resources/Roles';
14
14
  import { SSOResource } from './resources/SSO';
15
+ import {
16
+ ForumAPIError,
17
+ AuthenticationError,
18
+ AuthorizationError,
19
+ NotFoundError,
20
+ ValidationError,
21
+ RateLimitError,
22
+ ServerError,
23
+ NetworkError
24
+ } from './errors';
25
+ import { PaginationHelper, RetryHelper } from './utils';
26
+ import { RateLimitInfo } from './response-types';
15
27
 
16
28
  // Polyfill fetch if needed (e.g. older Node versions)
17
29
  const fetch = globalThis.fetch || require('cross-fetch');
18
30
 
31
+ export interface ClientOptions {
32
+ /** API key for authentication */
33
+ apiKey: string;
34
+ /** Base URL for the API (default: https://api.foru.ms/v1) */
35
+ baseUrl?: string;
36
+ /** Maximum number of retry attempts for failed requests (default: 3) */
37
+ maxRetries?: number;
38
+ /** Enable automatic retry for rate limits and server errors (default: true) */
39
+ enableRetry?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Main client for interacting with the Foru.ms API
44
+ * @example
45
+ * ```typescript
46
+ * const client = new ForumClient({
47
+ * apiKey: 'your_api_key',
48
+ * baseUrl: 'https://api.foru.ms/v1'
49
+ * });
50
+ *
51
+ * // Set user token
52
+ * client.setToken('user_jwt_token');
53
+ *
54
+ * // Use resources
55
+ * const threads = await client.threads.list();
56
+ * ```
57
+ */
19
58
  export class ForumClient {
20
59
  public apiKey: string;
21
60
  public token: string | null = null;
22
61
  public baseUrl: string;
62
+ public maxRetries: number;
63
+ public enableRetry: boolean;
64
+
65
+ /** Last known rate limit info */
66
+ public lastRateLimitInfo?: RateLimitInfo;
23
67
 
24
68
  public auth: AuthResource;
25
69
  public threads: ThreadsResource;
@@ -36,9 +80,14 @@ export class ForumClient {
36
80
  public roles: RolesResource;
37
81
  public sso: SSOResource;
38
82
 
39
- constructor(options: { apiKey: string; baseUrl?: string }) {
83
+ /** Pagination helper for auto-paginating through results */
84
+ public pagination: PaginationHelper;
85
+
86
+ constructor(options: ClientOptions) {
40
87
  this.apiKey = options.apiKey;
41
88
  this.baseUrl = options.baseUrl || 'https://api.foru.ms/v1';
89
+ this.maxRetries = options.maxRetries ?? 3;
90
+ this.enableRetry = options.enableRetry ?? true;
42
91
 
43
92
  this.auth = new AuthResource(this);
44
93
  this.threads = new ThreadsResource(this);
@@ -54,40 +103,131 @@ export class ForumClient {
54
103
  this.reports = new ReportsResource(this);
55
104
  this.roles = new RolesResource(this);
56
105
  this.sso = new SSOResource(this);
106
+
107
+ this.pagination = new PaginationHelper();
57
108
  }
58
109
 
110
+ /**
111
+ * Make an HTTP request to the API
112
+ * @param path - API endpoint path
113
+ * @param options - Fetch options
114
+ * @returns Promise resolving to the response data
115
+ * @throws {ForumAPIError} When the API returns an error
116
+ * @throws {NetworkError} When the network request fails
117
+ */
59
118
  public async request<T>(path: string, options: RequestInit = {}): Promise<T> {
60
- const headers: Record<string, string> = {
61
- 'Content-Type': 'application/json',
62
- 'x-api-key': this.apiKey,
63
- ...(options.headers as Record<string, string>),
64
- };
119
+ const makeRequest = async (): Promise<T> => {
120
+ const headers: Record<string, string> = {
121
+ 'Content-Type': 'application/json',
122
+ 'x-api-key': this.apiKey,
123
+ ...(options.headers as Record<string, string>),
124
+ };
65
125
 
66
- if (this.token) {
67
- headers['Authorization'] = `Bearer ${this.token}`;
68
- }
126
+ if (this.token) {
127
+ headers['Authorization'] = `Bearer ${this.token}`;
128
+ }
69
129
 
70
- const response = await fetch(`${this.baseUrl}${path}`, {
71
- ...options,
72
- headers,
73
- });
74
-
75
- const contentType = response.headers.get('content-type');
76
- let data;
77
- if (contentType && contentType.includes('application/json')) {
78
- data = await response.json();
79
- } else {
80
- data = await response.text();
81
- }
130
+ let response: Response;
131
+
132
+ try {
133
+ response = await fetch(`${this.baseUrl}${path}`, {
134
+ ...options,
135
+ headers,
136
+ });
137
+ } catch (error: any) {
138
+ throw new NetworkError('Network request failed', error);
139
+ }
140
+
141
+ // Extract rate limit info
142
+ this.extractRateLimitInfo(response);
143
+
144
+ const contentType = response.headers.get('content-type');
145
+ let data;
146
+ if (contentType && contentType.includes('application/json')) {
147
+ data = await response.json();
148
+ } else {
149
+ data = await response.text();
150
+ }
151
+
152
+ if (!response.ok) {
153
+ this.handleErrorResponse(response.status, data);
154
+ }
155
+
156
+ return data as T;
157
+ };
82
158
 
83
- if (!response.ok) {
84
- throw new Error((data && (data as any).message) || (data as any).error || 'API Error');
159
+ // Apply retry logic if enabled
160
+ if (this.enableRetry) {
161
+ return RetryHelper.withRetry(makeRequest, this.maxRetries);
85
162
  }
86
163
 
87
- return data as T;
164
+ return makeRequest();
88
165
  }
89
166
 
90
- public setToken(token: string) {
167
+ /**
168
+ * Set the authentication token for user-scoped requests
169
+ * @param token - JWT token
170
+ */
171
+ public setToken(token: string): void {
91
172
  this.token = token;
92
173
  }
174
+
175
+ /**
176
+ * Clear the authentication token
177
+ */
178
+ public clearToken(): void {
179
+ this.token = null;
180
+ }
181
+
182
+ /**
183
+ * Check if client is authenticated
184
+ */
185
+ public isAuthenticated(): boolean {
186
+ return this.token !== null;
187
+ }
188
+
189
+ /**
190
+ * Extract and store rate limit information from response headers
191
+ */
192
+ private extractRateLimitInfo(response: Response): void {
193
+ const limit = response.headers.get('x-ratelimit-limit');
194
+ const remaining = response.headers.get('x-ratelimit-remaining');
195
+ const reset = response.headers.get('x-ratelimit-reset');
196
+
197
+ if (limit && remaining && reset) {
198
+ this.lastRateLimitInfo = {
199
+ limit: parseInt(limit, 10),
200
+ remaining: parseInt(remaining, 10),
201
+ reset: parseInt(reset, 10),
202
+ };
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handle error responses by throwing appropriate error types
208
+ */
209
+ private handleErrorResponse(status: number, data: any): never {
210
+ const message = (data && data.message) || data.error || 'API Error';
211
+
212
+ switch (status) {
213
+ case 401:
214
+ throw new AuthenticationError(message, data);
215
+ case 403:
216
+ throw new AuthorizationError(message, data);
217
+ case 404:
218
+ throw new NotFoundError(message, data);
219
+ case 422:
220
+ throw new ValidationError(message, data);
221
+ case 429:
222
+ const retryAfter = data.retryAfter || 60;
223
+ throw new RateLimitError(message, retryAfter, data);
224
+ case 500:
225
+ case 502:
226
+ case 503:
227
+ case 504:
228
+ throw new ServerError(message, status, data);
229
+ default:
230
+ throw new ForumAPIError(message, status, data);
231
+ }
232
+ }
93
233
  }
package/src/errors.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Base error class for all Foru.ms API errors
3
+ */
4
+ export class ForumAPIError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public statusCode: number,
8
+ public response?: any
9
+ ) {
10
+ super(message);
11
+ this.name = 'ForumAPIError';
12
+ Object.setPrototypeOf(this, ForumAPIError.prototype);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Error thrown when authentication fails or token is invalid
18
+ */
19
+ export class AuthenticationError extends ForumAPIError {
20
+ constructor(message: string = 'Authentication failed', response?: any) {
21
+ super(message, 401, response);
22
+ this.name = 'AuthenticationError';
23
+ Object.setPrototypeOf(this, AuthenticationError.prototype);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Error thrown when user doesn't have permission for an action
29
+ */
30
+ export class AuthorizationError extends ForumAPIError {
31
+ constructor(message: string = 'Permission denied', response?: any) {
32
+ super(message, 403, response);
33
+ this.name = 'AuthorizationError';
34
+ Object.setPrototypeOf(this, AuthorizationError.prototype);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Error thrown when a requested resource is not found
40
+ */
41
+ export class NotFoundError extends ForumAPIError {
42
+ constructor(message: string = 'Resource not found', response?: any) {
43
+ super(message, 404, response);
44
+ this.name = 'NotFoundError';
45
+ Object.setPrototypeOf(this, NotFoundError.prototype);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Error thrown when request validation fails
51
+ */
52
+ export class ValidationError extends ForumAPIError {
53
+ constructor(message: string = 'Validation failed', response?: any) {
54
+ super(message, 422, response);
55
+ this.name = 'ValidationError';
56
+ Object.setPrototypeOf(this, ValidationError.prototype);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Error thrown when rate limit is exceeded
62
+ */
63
+ export class RateLimitError extends ForumAPIError {
64
+ constructor(
65
+ message: string = 'Rate limit exceeded',
66
+ public retryAfter?: number,
67
+ response?: any
68
+ ) {
69
+ super(message, 429, response);
70
+ this.name = 'RateLimitError';
71
+ Object.setPrototypeOf(this, RateLimitError.prototype);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Error thrown when server encounters an error
77
+ */
78
+ export class ServerError extends ForumAPIError {
79
+ constructor(message: string = 'Server error', statusCode: number = 500, response?: any) {
80
+ super(message, statusCode, response);
81
+ this.name = 'ServerError';
82
+ Object.setPrototypeOf(this, ServerError.prototype);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Error thrown when network request fails
88
+ */
89
+ export class NetworkError extends Error {
90
+ constructor(message: string = 'Network request failed', public cause?: Error) {
91
+ super(message);
92
+ this.name = 'NetworkError';
93
+ Object.setPrototypeOf(this, NetworkError.prototype);
94
+ }
95
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from './Client';
2
2
  export * from './types';
3
+ export * from './errors';
4
+ export * from './response-types';
5
+ export * from './utils';
3
6
  export * from './resources/Auth';
4
7
  export * from './resources/Threads';
5
8
  export * from './resources/Posts';
@@ -31,6 +31,17 @@ export class IntegrationsResource {
31
31
  });
32
32
  }
33
33
 
34
+ async update(id: string, payload: {
35
+ name?: string;
36
+ config?: any;
37
+ active?: boolean;
38
+ }): Promise<{ integration: Integration }> {
39
+ return this.client.request<{ integration: Integration }>(`/integrations/${id}`, {
40
+ method: 'PATCH',
41
+ body: JSON.stringify(payload),
42
+ });
43
+ }
44
+
34
45
  async delete(id: string): Promise<{ success: boolean }> {
35
46
  return this.client.request<{ success: boolean }>(`/integrations/${id}`, {
36
47
  method: 'DELETE',
@@ -84,6 +84,20 @@ export class PostsResource {
84
84
  });
85
85
  }
86
86
 
87
+ async getLikes(id: string, params?: {
88
+ cursor?: string;
89
+ }): Promise<any> {
90
+ const searchParams = new URLSearchParams();
91
+ if (params) {
92
+ Object.entries(params).forEach(([key, value]) => {
93
+ if (value !== undefined) {
94
+ searchParams.append(key, value as string);
95
+ }
96
+ });
97
+ }
98
+ return this.client.request(`/post/${id}/likes?${searchParams.toString()}`, { method: 'GET' });
99
+ }
100
+
87
101
  async dislike(id: string, userId?: string, extendedData?: any): Promise<any> {
88
102
  return this.client.request(`/post/${id}/dislikes`, {
89
103
  method: 'POST',
@@ -97,6 +111,20 @@ export class PostsResource {
97
111
  });
98
112
  }
99
113
 
114
+ async getDislikes(id: string, params?: {
115
+ cursor?: string;
116
+ }): Promise<any> {
117
+ const searchParams = new URLSearchParams();
118
+ if (params) {
119
+ Object.entries(params).forEach(([key, value]) => {
120
+ if (value !== undefined) {
121
+ searchParams.append(key, value as string);
122
+ }
123
+ });
124
+ }
125
+ return this.client.request(`/post/${id}/dislikes?${searchParams.toString()}`, { method: 'GET' });
126
+ }
127
+
100
128
  async upvote(id: string, userId?: string, extendedData?: any): Promise<any> {
101
129
  return this.client.request(`/post/${id}/upvotes`, {
102
130
  method: 'POST',
@@ -110,6 +138,20 @@ export class PostsResource {
110
138
  });
111
139
  }
112
140
 
141
+ async getUpvotes(id: string, params?: {
142
+ cursor?: string;
143
+ }): Promise<any> {
144
+ const searchParams = new URLSearchParams();
145
+ if (params) {
146
+ Object.entries(params).forEach(([key, value]) => {
147
+ if (value !== undefined) {
148
+ searchParams.append(key, value as string);
149
+ }
150
+ });
151
+ }
152
+ return this.client.request(`/post/${id}/upvotes?${searchParams.toString()}`, { method: 'GET' });
153
+ }
154
+
113
155
  async downvote(id: string, userId?: string, extendedData?: any): Promise<any> {
114
156
  return this.client.request(`/post/${id}/downvotes`, {
115
157
  method: 'POST',
@@ -122,4 +164,18 @@ export class PostsResource {
122
164
  method: 'DELETE',
123
165
  });
124
166
  }
167
+
168
+ async getDownvotes(id: string, params?: {
169
+ cursor?: string;
170
+ }): Promise<any> {
171
+ const searchParams = new URLSearchParams();
172
+ if (params) {
173
+ Object.entries(params).forEach(([key, value]) => {
174
+ if (value !== undefined) {
175
+ searchParams.append(key, value as string);
176
+ }
177
+ });
178
+ }
179
+ return this.client.request(`/post/${id}/downvotes?${searchParams.toString()}`, { method: 'GET' });
180
+ }
125
181
  }
@@ -59,6 +59,16 @@ export class PrivateMessagesResource {
59
59
  });
60
60
  }
61
61
 
62
+ async update(id: string, payload: {
63
+ read?: boolean;
64
+ extendedData?: Record<string, any>;
65
+ }): Promise<PrivateMessage> {
66
+ return this.client.request<PrivateMessage>(`/private-message/${id}`, {
67
+ method: 'PATCH',
68
+ body: JSON.stringify(payload),
69
+ });
70
+ }
71
+
62
72
  async delete(id: string): Promise<PrivateMessage & { deleted: boolean }> {
63
73
  return this.client.request<PrivateMessage & { deleted: boolean }>(`/private-message/${id}`, {
64
74
  method: 'DELETE',
@@ -25,6 +25,23 @@ export class SSOResource {
25
25
  });
26
26
  }
27
27
 
28
+ async retrieve(id: string): Promise<{ ssoProvider: SSOProvider }> {
29
+ return this.client.request<{ ssoProvider: SSOProvider }>(`/sso/${id}`, {
30
+ method: 'GET',
31
+ });
32
+ }
33
+
34
+ async update(id: string, payload: {
35
+ domain?: string;
36
+ config?: any;
37
+ active?: boolean;
38
+ }): Promise<{ ssoProvider: SSOProvider }> {
39
+ return this.client.request<{ ssoProvider: SSOProvider }>(`/sso/${id}`, {
40
+ method: 'PATCH',
41
+ body: JSON.stringify(payload),
42
+ });
43
+ }
44
+
28
45
  async delete(id: string): Promise<{ success: boolean }> {
29
46
  return this.client.request<{ success: boolean }>(`/sso/${id}`, {
30
47
  method: 'DELETE',
@@ -64,6 +64,24 @@ export class TagsResource {
64
64
  });
65
65
  }
66
66
 
67
+ async getThreads(id: string, params?: {
68
+ query?: string;
69
+ cursor?: string;
70
+ filter?: 'newest' | 'oldest';
71
+ }): Promise<import('../types').ThreadListResponse> {
72
+ const searchParams = new URLSearchParams();
73
+ if (params) {
74
+ Object.entries(params).forEach(([key, value]) => {
75
+ if (value !== undefined) {
76
+ searchParams.append(key, value as string);
77
+ }
78
+ });
79
+ }
80
+ return this.client.request<import('../types').ThreadListResponse>(`/tag/${id}/threads?${searchParams.toString()}`, {
81
+ method: 'GET',
82
+ });
83
+ }
84
+
67
85
  async subscribe(id: string, userId: string): Promise<any> {
68
86
  return this.client.request(`/tag/${id}/subscribers`, {
69
87
  method: 'POST',
@@ -77,6 +95,20 @@ export class TagsResource {
77
95
  });
78
96
  }
79
97
 
98
+ async getSubscribers(id: string, params?: {
99
+ cursor?: string;
100
+ }): Promise<any> {
101
+ const searchParams = new URLSearchParams();
102
+ if (params) {
103
+ Object.entries(params).forEach(([key, value]) => {
104
+ if (value !== undefined) {
105
+ searchParams.append(key, value as string);
106
+ }
107
+ });
108
+ }
109
+ return this.client.request(`/tag/${id}/subscribers?${searchParams.toString()}`, { method: 'GET' });
110
+ }
111
+
80
112
  async listSubscribed(params: {
81
113
  userId: string;
82
114
  query?: string;
@@ -85,6 +85,20 @@ export class ThreadsResource {
85
85
  });
86
86
  }
87
87
 
88
+ async getLikes(id: string, params?: {
89
+ cursor?: string;
90
+ }): Promise<any> {
91
+ const searchParams = new URLSearchParams();
92
+ if (params) {
93
+ Object.entries(params).forEach(([key, value]) => {
94
+ if (value !== undefined) {
95
+ searchParams.append(key, value as string);
96
+ }
97
+ });
98
+ }
99
+ return this.client.request(`/thread/${id}/likes?${searchParams.toString()}`, { method: 'GET' });
100
+ }
101
+
88
102
  async dislike(id: string, userId?: string, extendedData?: any): Promise<any> {
89
103
  return this.client.request(`/thread/${id}/dislikes`, {
90
104
  method: 'POST',
@@ -98,6 +112,20 @@ export class ThreadsResource {
98
112
  });
99
113
  }
100
114
 
115
+ async getDislikes(id: string, params?: {
116
+ cursor?: string;
117
+ }): Promise<any> {
118
+ const searchParams = new URLSearchParams();
119
+ if (params) {
120
+ Object.entries(params).forEach(([key, value]) => {
121
+ if (value !== undefined) {
122
+ searchParams.append(key, value as string);
123
+ }
124
+ });
125
+ }
126
+ return this.client.request(`/thread/${id}/dislikes?${searchParams.toString()}`, { method: 'GET' });
127
+ }
128
+
101
129
  async subscribe(id: string, userId: string, extendedData?: any): Promise<any> {
102
130
  return this.client.request(`/thread/${id}/subscribers`, {
103
131
  method: 'POST',
@@ -111,6 +139,87 @@ export class ThreadsResource {
111
139
  });
112
140
  }
113
141
 
142
+ async getSubscribers(id: string, params?: {
143
+ cursor?: string;
144
+ }): Promise<any> {
145
+ const searchParams = new URLSearchParams();
146
+ if (params) {
147
+ Object.entries(params).forEach(([key, value]) => {
148
+ if (value !== undefined) {
149
+ searchParams.append(key, value as string);
150
+ }
151
+ });
152
+ }
153
+ return this.client.request(`/thread/${id}/subscribers?${searchParams.toString()}`, { method: 'GET' });
154
+ }
155
+
156
+ async upvote(id: string, userId?: string, extendedData?: any): Promise<any> {
157
+ return this.client.request(`/thread/${id}/upvotes`, {
158
+ method: 'POST',
159
+ body: JSON.stringify({ userId, extendedData }),
160
+ });
161
+ }
162
+
163
+ async unupvote(id: string, userId: string): Promise<any> {
164
+ return this.client.request(`/thread/${id}/upvotes?userId=${userId}`, {
165
+ method: 'DELETE',
166
+ });
167
+ }
168
+
169
+ async getUpvotes(id: string, params?: {
170
+ cursor?: string;
171
+ }): Promise<any> {
172
+ const searchParams = new URLSearchParams();
173
+ if (params) {
174
+ Object.entries(params).forEach(([key, value]) => {
175
+ if (value !== undefined) {
176
+ searchParams.append(key, value as string);
177
+ }
178
+ });
179
+ }
180
+ return this.client.request(`/thread/${id}/upvotes?${searchParams.toString()}`, { method: 'GET' });
181
+ }
182
+
183
+ async downvote(id: string, userId?: string, extendedData?: any): Promise<any> {
184
+ return this.client.request(`/thread/${id}/downvotes`, {
185
+ method: 'POST',
186
+ body: JSON.stringify({ userId, extendedData }),
187
+ });
188
+ }
189
+
190
+ async undownvote(id: string, userId: string): Promise<any> {
191
+ return this.client.request(`/thread/${id}/downvotes?userId=${userId}`, {
192
+ method: 'DELETE',
193
+ });
194
+ }
195
+
196
+ async getDownvotes(id: string, params?: {
197
+ cursor?: string;
198
+ }): Promise<any> {
199
+ const searchParams = new URLSearchParams();
200
+ if (params) {
201
+ Object.entries(params).forEach(([key, value]) => {
202
+ if (value !== undefined) {
203
+ searchParams.append(key, value as string);
204
+ }
205
+ });
206
+ }
207
+ return this.client.request(`/thread/${id}/downvotes?${searchParams.toString()}`, { method: 'GET' });
208
+ }
209
+
210
+ async getPoll(threadId: string, userId?: string): Promise<any> {
211
+ const searchParams = new URLSearchParams();
212
+ if (userId) {
213
+ searchParams.append('userId', userId);
214
+ }
215
+
216
+ const query = searchParams.toString();
217
+ return this.client.request(
218
+ `/thread/${threadId}/poll${query ? `?${query}` : ''}`,
219
+ { method: 'GET' }
220
+ );
221
+ }
222
+
114
223
  async vote(id: string, optionId: string, userId: string): Promise<any> {
115
224
  return this.client.request(`/thread/${id}/poll/votes`, {
116
225
  method: 'POST',