4chanapi.ts 0.1.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.

Potentially problematic release.


This version of 4chanapi.ts might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +516 -0
  3. package/package.json +34 -0
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omerg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,516 @@
1
+ # 4capi
2
+
3
+ A typed TypeScript client for the [4chan API](https://github.com/4chan/4chan-API), designed for React Native.
4
+
5
+ - Full TypeScript types for every endpoint
6
+ - Built-in rate limiting (≤ 1 request/sec, per API rules)
7
+ - `If-Modified-Since` support — returns `null` on HTTP 304
8
+ - URL helpers for images, thumbnails, flags, and spoilers
9
+ - No Node.js dependencies — uses `fetch` natively available in React Native
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install 4capi
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ```ts
24
+ import { FourChanClient } from "4capi";
25
+
26
+ const client = new FourChanClient();
27
+
28
+ const thread = await client.getThread("g", 100000000);
29
+ if (thread) {
30
+ const op = thread.posts[0];
31
+ console.log(op.sub); // thread subject
32
+ console.log(op.com); // comment (HTML-escaped)
33
+ }
34
+ ```
35
+
36
+ ---
37
+
38
+ ## API Reference
39
+
40
+ ### `new FourChanClient()`
41
+
42
+ Creates a client with a built-in rate limiter. All requests are serialised with at least 1 second between dispatches.
43
+
44
+ ```ts
45
+ const client = new FourChanClient();
46
+ ```
47
+
48
+ ---
49
+
50
+ ### `getBoards()`
51
+
52
+ Fetches all boards and their settings.
53
+
54
+ ```ts
55
+ const boards = await client.getBoards();
56
+ ```
57
+
58
+ **Response: `Board[]`**
59
+
60
+ ```json
61
+ [
62
+ {
63
+ "board": "g",
64
+ "title": "Technology",
65
+ "ws_board": 1,
66
+ "per_page": 15,
67
+ "pages": 10,
68
+ "max_filesize": 4096,
69
+ "max_webm_filesize": 3072,
70
+ "max_comment_chars": 2000,
71
+ "max_webm_duration": 120,
72
+ "bump_limit": 310,
73
+ "image_limit": 150,
74
+ "cooldowns": {
75
+ "threads": 600,
76
+ "replies": 60,
77
+ "images": 60
78
+ },
79
+ "meta_description": "...",
80
+ "is_archived": 1
81
+ }
82
+ ]
83
+ ```
84
+
85
+ **`Board` fields**
86
+
87
+ | Field | Type | Description |
88
+ |---|---|---|
89
+ | `board` | `string` | Board directory name (e.g. `"g"`, `"po"`) |
90
+ | `title` | `string` | Human-readable board title |
91
+ | `ws_board` | `0 \| 1` | 1 = worksafe |
92
+ | `per_page` | `number` | Threads per index page |
93
+ | `pages` | `number` | Total number of index pages |
94
+ | `max_filesize` | `number` | Max non-webm file size in KB |
95
+ | `max_webm_filesize` | `number` | Max webm file size in KB |
96
+ | `max_comment_chars` | `number` | Max characters in a comment |
97
+ | `max_webm_duration` | `number` | Max webm duration in seconds |
98
+ | `bump_limit` | `number` | Replies before thread stops bumping |
99
+ | `image_limit` | `number` | Max image replies per thread |
100
+ | `cooldowns` | `BoardCooldowns` | `{ threads, replies, images }` in seconds |
101
+ | `is_archived?` | `0 \| 1` | Archive enabled on this board |
102
+ | `country_flags?` | `0 \| 1` | Country flags enabled |
103
+ | `user_ids?` | `0 \| 1` | Poster IDs enabled |
104
+ | `board_flags?` | `Record<string, string>` | Map of flag code → name |
105
+ | `spoilers?` | `0 \| 1` | Spoiler images enabled |
106
+ | `custom_spoilers?` | `number` | Number of custom spoiler variants |
107
+
108
+ ---
109
+
110
+ ### `getThread(board, threadId, opts?)`
111
+
112
+ Fetches a full thread including all replies.
113
+
114
+ Returns `null` if the thread has not changed since `opts.ifModifiedSince`.
115
+
116
+ ```ts
117
+ const thread = await client.getThread("po", 570368);
118
+
119
+ // With If-Modified-Since (returns null on HTTP 304)
120
+ const lastFetch = new Date();
121
+ const updated = await client.getThread("po", 570368, {
122
+ ifModifiedSince: lastFetch,
123
+ });
124
+ if (updated === null) {
125
+ console.log("No new posts");
126
+ }
127
+ ```
128
+
129
+ **Response: `Thread | null`**
130
+
131
+ ```json
132
+ {
133
+ "posts": [
134
+ {
135
+ "no": 570368,
136
+ "resto": 0,
137
+ "sticky": 1,
138
+ "now": "01/01/24(Mon)00:00",
139
+ "time": 1704067200,
140
+ "name": "Anonymous",
141
+ "sub": "Welcome to /po/",
142
+ "com": "Paper &amp; origami thread.",
143
+ "tim": 1704067200123,
144
+ "filename": "origami",
145
+ "ext": ".jpg",
146
+ "fsize": 204800,
147
+ "md5": "abc123def456ghi789jkl0==",
148
+ "w": 1200,
149
+ "h": 800,
150
+ "tn_w": 250,
151
+ "tn_h": 166,
152
+ "replies": 42,
153
+ "images": 18,
154
+ "unique_ips": 15,
155
+ "last_modified": 1704099600,
156
+ "semantic_url": "welcome-to-po"
157
+ },
158
+ {
159
+ "no": 570400,
160
+ "resto": 570368,
161
+ "now": "01/01/24(Mon)01:30",
162
+ "time": 1704072600,
163
+ "name": "Anonymous",
164
+ "com": "Nice thread, here&#039;s my latest crane.",
165
+ "tim": 1704072600456,
166
+ "filename": "crane",
167
+ "ext": ".png",
168
+ "fsize": 102400,
169
+ "md5": "xyz789abc012def345ghi6==",
170
+ "w": 800,
171
+ "h": 600,
172
+ "tn_w": 250,
173
+ "tn_h": 187
174
+ }
175
+ ]
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ### `getCatalog(board, opts?)`
182
+
183
+ Fetches all threads on a board grouped by page, including the most recent reply previews.
184
+
185
+ ```ts
186
+ const catalog = await client.getCatalog("g");
187
+
188
+ // With cache check
189
+ const catalog = await client.getCatalog("g", { ifModifiedSince: lastFetch });
190
+ if (catalog === null) return; // not modified
191
+
192
+ for (const page of catalog) {
193
+ for (const thread of page.threads) {
194
+ console.log(`[${thread.no}] ${thread.sub ?? "(no subject)"} — ${thread.replies} replies`);
195
+ }
196
+ }
197
+ ```
198
+
199
+ **Response: `CatalogPage[] | null`**
200
+
201
+ ```json
202
+ [
203
+ {
204
+ "page": 1,
205
+ "threads": [
206
+ {
207
+ "no": 100000001,
208
+ "resto": 0,
209
+ "now": "03/29/26(Sun)12:00",
210
+ "time": 1743249600,
211
+ "name": "Anonymous",
212
+ "sub": "Programming thread",
213
+ "com": "Post your projects.",
214
+ "tim": 1743249600789,
215
+ "filename": "code",
216
+ "ext": ".png",
217
+ "w": 1920,
218
+ "h": 1080,
219
+ "tn_w": 250,
220
+ "tn_h": 140,
221
+ "replies": 87,
222
+ "images": 12,
223
+ "omitted_posts": 82,
224
+ "omitted_images": 10,
225
+ "last_modified": 1743260000,
226
+ "semantic_url": "programming-thread",
227
+ "last_replies": [
228
+ {
229
+ "no": 100000088,
230
+ "resto": 100000001,
231
+ "now": "03/29/26(Sun)14:55",
232
+ "time": 1743260100,
233
+ "name": "Anonymous",
234
+ "com": "Just finished my Rust project."
235
+ }
236
+ ]
237
+ }
238
+ ]
239
+ }
240
+ ]
241
+ ```
242
+
243
+ ---
244
+
245
+ ### `getThreadList(board)`
246
+
247
+ Fetches a lightweight list of all threads and their last-modified timestamps. Useful for polling — much smaller than the full catalog.
248
+
249
+ ```ts
250
+ const pages = await client.getThreadList("g");
251
+
252
+ for (const page of pages) {
253
+ for (const thread of page.threads) {
254
+ console.log(thread.no, thread.last_modified, thread.replies);
255
+ }
256
+ }
257
+ ```
258
+
259
+ **Response: `ThreadListPage[]`**
260
+
261
+ ```json
262
+ [
263
+ {
264
+ "page": 1,
265
+ "threads": [
266
+ { "no": 100000001, "last_modified": 1743260000, "replies": 87 },
267
+ { "no": 100000002, "last_modified": 1743259000, "replies": 12 }
268
+ ]
269
+ },
270
+ {
271
+ "page": 2,
272
+ "threads": [
273
+ { "no": 99999900, "last_modified": 1743240000, "replies": 310 }
274
+ ]
275
+ }
276
+ ]
277
+ ```
278
+
279
+ ---
280
+
281
+ ### `getIndex(board, page, opts?)`
282
+
283
+ Fetches a single index page (threads + preview replies). Pages are 1-based.
284
+
285
+ ```ts
286
+ const indexPage = await client.getIndex("g", 1);
287
+ if (indexPage) {
288
+ for (const thread of indexPage) {
289
+ const op = thread.posts[0];
290
+ console.log(op.sub, op.replies);
291
+ }
292
+ }
293
+ ```
294
+
295
+ **Response: `IndexPage | null`** — an array of `{ posts: Post[] }` objects
296
+
297
+ ---
298
+
299
+ ### `getArchive(board)`
300
+
301
+ Fetches the list of archived thread IDs. Returns an empty array for boards without archives.
302
+
303
+ ```ts
304
+ const archivedIds = await client.getArchive("g");
305
+ // [571958, 572866, 54195, ...]
306
+ ```
307
+
308
+ **Response: `number[]`**
309
+
310
+ ```json
311
+ [571958, 572866, 54195, 12345, 67890]
312
+ ```
313
+
314
+ ---
315
+
316
+ ## URL Helpers
317
+
318
+ ### `getImageUrl(board, tim, ext)`
319
+
320
+ Full-size image URL.
321
+
322
+ ```ts
323
+ import { getImageUrl } from "4capi";
324
+
325
+ const url = getImageUrl("g", post.tim!, post.ext!);
326
+ // "https://i.4cdn.org/g/1743249600789.png"
327
+ ```
328
+
329
+ ### `getThumbnailUrl(board, tim)`
330
+
331
+ Thumbnail URL (always JPEG).
332
+
333
+ ```ts
334
+ import { getThumbnailUrl } from "4capi";
335
+
336
+ const thumb = getThumbnailUrl("g", post.tim!);
337
+ // "https://i.4cdn.org/g/1743249600789s.jpg"
338
+ ```
339
+
340
+ ### `getCountryFlagUrl(countryCode)`
341
+
342
+ Country flag GIF (boards with `country_flags` enabled).
343
+
344
+ ```ts
345
+ import { getCountryFlagUrl } from "4capi";
346
+
347
+ const flag = getCountryFlagUrl(post.country!);
348
+ // "https://s.4cdn.org/image/country/us.gif"
349
+ ```
350
+
351
+ ### `getBoardFlagUrl(board, flagCode)`
352
+
353
+ Board-specific flag GIF (boards with `board_flags` enabled).
354
+
355
+ ```ts
356
+ import { getBoardFlagUrl } from "4capi";
357
+
358
+ const flag = getBoardFlagUrl("pol", post.board_flag!);
359
+ // "https://s.4cdn.org/image/flags/pol/EU.gif"
360
+ ```
361
+
362
+ ### `getSpoilerUrl(board, customIndex?)`
363
+
364
+ Spoiler placeholder image. Pass a `customIndex` (1–10) for boards with custom spoilers.
365
+
366
+ ```ts
367
+ import { getSpoilerUrl } from "4capi";
368
+
369
+ getSpoilerUrl("b"); // default spoiler
370
+ getSpoilerUrl("co", 3); // custom spoiler #3 for /co/
371
+ ```
372
+
373
+ ### `icons`
374
+
375
+ Static icon URLs as constants.
376
+
377
+ ```ts
378
+ import { icons } from "4capi";
379
+
380
+ icons.sticky // https://s.4cdn.org/image/sticky.gif
381
+ icons.closed // https://s.4cdn.org/image/closed.gif
382
+ icons.admin // https://s.4cdn.org/image/adminicon.gif
383
+ icons.mod // https://s.4cdn.org/image/modicon.gif
384
+ icons.developer // https://s.4cdn.org/image/developericon.gif
385
+ icons.manager // https://s.4cdn.org/image/managericon.gif
386
+ icons.founder // https://s.4cdn.org/image/foundericon.gif
387
+ icons.fileDeletedOp // https://s.4cdn.org/image/filedeleted.gif
388
+ icons.fileDeletedReply // https://s.4cdn.org/image/filedeleted-res.gif
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Error Handling
394
+
395
+ All methods throw `FourChanApiError` on non-200/304 responses (e.g. 404 for a thread that no longer exists).
396
+
397
+ ```ts
398
+ import { FourChanClient, FourChanApiError } from "4capi";
399
+
400
+ const client = new FourChanClient();
401
+
402
+ try {
403
+ const thread = await client.getThread("g", 1);
404
+ } catch (err) {
405
+ if (err instanceof FourChanApiError) {
406
+ console.error(`HTTP ${err.status} — ${err.url}`);
407
+ if (err.status === 404) {
408
+ // thread was deleted
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Efficient Polling Pattern
417
+
418
+ Use `getThreadList` to detect changes cheaply, then only fetch threads that have actually updated.
419
+
420
+ ```ts
421
+ const client = new FourChanClient();
422
+
423
+ let knownThreads = new Map<number, number>(); // threadId → last_modified
424
+
425
+ async function poll() {
426
+ const pages = await client.getThreadList("g");
427
+
428
+ for (const page of pages) {
429
+ for (const entry of page.threads) {
430
+ const prev = knownThreads.get(entry.no);
431
+
432
+ if (!prev || prev < entry.last_modified) {
433
+ const thread = await client.getThread("g", entry.no, {
434
+ ifModifiedSince: prev ? new Date(prev * 1000) : undefined,
435
+ });
436
+ if (thread) {
437
+ knownThreads.set(entry.no, entry.last_modified);
438
+ // handle updated thread...
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ // Poll every 30 seconds (the client enforces the per-request 1s rate limit)
446
+ setInterval(poll, 30_000);
447
+ ```
448
+
449
+ ---
450
+
451
+ ## Post Field Reference
452
+
453
+ All fields on `Post` except `no`, `resto`, `now`, `time`, and `name` are optional — they only appear when applicable.
454
+
455
+ | Field | Type | Present when |
456
+ |---|---|---|
457
+ | `no` | `number` | Always |
458
+ | `resto` | `number` | Always — `0` for OP |
459
+ | `time` | `number` | Always — UNIX timestamp |
460
+ | `now` | `string` | Always — `MM/DD/YY(Day)HH:MM` |
461
+ | `name` | `string` | Always — defaults to `"Anonymous"` |
462
+ | `sub` | `string` | OP only, if subject was set |
463
+ | `com` | `string` | If a comment was included |
464
+ | `trip` | `string` | If poster used a tripcode |
465
+ | `id` | `string` | On boards with user IDs |
466
+ | `capcode` | `Capcode` | Staff posts only |
467
+ | `country` | `string` | On boards with country flags |
468
+ | `board_flag` | `string` | On boards with board flags |
469
+ | `since4pass` | `number` | If poster used 4chan pass option |
470
+ | `tim` | `number` | If post has an attachment |
471
+ | `filename` | `string` | If post has an attachment |
472
+ | `ext` | `string` | If post has an attachment |
473
+ | `fsize` | `number` | If post has an attachment |
474
+ | `md5` | `string` | If post has an attachment |
475
+ | `w` / `h` | `number` | If post has an attachment |
476
+ | `tn_w` / `tn_h` | `number` | If post has an attachment |
477
+ | `spoiler` | `1` | If file is spoilered |
478
+ | `custom_spoiler` | `number` | If board has custom spoilers |
479
+ | `filedeleted` | `1` | If file was deleted |
480
+ | `m_img` | `1` | If mobile-optimised image exists |
481
+ | `sticky` | `1` | OP — if thread is pinned |
482
+ | `closed` | `1` | OP — if thread is locked |
483
+ | `bumplimit` | `1` | OP — if bump limit reached |
484
+ | `imagelimit` | `1` | OP — if image limit reached |
485
+ | `replies` | `number` | OP — total reply count |
486
+ | `images` | `number` | OP — total image reply count |
487
+ | `omitted_posts` | `number` | OP — replies not shown in preview |
488
+ | `omitted_images` | `number` | OP — image replies not shown in preview |
489
+ | `last_modified` | `number` | OP — UNIX timestamp of last activity |
490
+ | `semantic_url` | `string` | OP — SEO slug |
491
+ | `unique_ips` | `number` | OP — unique poster count (live threads) |
492
+ | `last_replies` | `Post[]` | OP — preview of most recent replies |
493
+ | `archived` | `1` | OP — if thread is archived |
494
+ | `archived_on` | `number` | OP — UNIX timestamp of archival |
495
+ | `tag` | `string` | OP — `/f/` flash category |
496
+
497
+ ---
498
+
499
+ ## Building
500
+
501
+ ```sh
502
+ npm run build # compiles to dist/
503
+ npm run typecheck # type-check only, no output
504
+ ```
505
+
506
+ ---
507
+
508
+ ## 4chan API Terms of Service
509
+
510
+ - Do not use "4chan" in your app name, product, or service name.
511
+ - Do not use the 4chan name, logo, or brand to promote your app.
512
+ - Credit the source as 4chan with a link.
513
+ - Do not claim your app is official.
514
+ - Do not clone 4chan or re-host/repackage the API JSON with ads.
515
+
516
+ Full terms: [https://github.com/4chan/4chan-API](https://github.com/4chan/4chan-API)
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "4chanapi.ts",
3
+ "author": "Honosal <honosal875@proton.me> (https://github.com/honosal)",
4
+ "version": "0.1.0",
5
+ "description": "Typed TypeScript client for the 4chan API",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "echo \"No tests specified\" && exit 0"
15
+ },
16
+ "keywords": [
17
+ "4chan",
18
+ "api",
19
+ "typescript",
20
+ "react-native"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/honosal/4chanapi.ts.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/honosal/4chanapi.ts/issues"
29
+ },
30
+ "homepage": "https://github.com/honosal/4chanapi.ts",
31
+ "devDependencies": {
32
+ "typescript": "^5.4.5"
33
+ }
34
+ }