├── .travis.yml
├── LICENSE
├── README.md
└── api
├── api.go
├── api_test.go
├── catalog_example.json
├── example.json
└── example_test.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.2
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, moshee
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A 4chan API client for Go.
2 |
3 | [](https://travis-ci.org/moshee/go-4chan-api) [](https://godoc.org/github.com/moshee/go-4chan-api/api)
4 |
5 | Supports:
6 |
7 | - API revision [830712e on Apr 27 2018](https://github.com/4chan/4chan-API)
8 | * Single thread
9 | * Thread index
10 | * Board list
11 | * Board catalog
12 | * Thread list
13 | - HTTPS
14 | - Rate limiting
15 | - `If-Modified-Since`
16 | - In-place thread updating
17 |
18 | Pull requests welcome.
19 |
20 | #### To do
21 |
22 | - More useful `Thread` and `*Post` methods
23 | - Update & add more tests
24 | - ...
25 |
--------------------------------------------------------------------------------
/api/api.go:
--------------------------------------------------------------------------------
1 | // Package api pulls 4chan board and thread data from the JSON API into native Go data structures.
2 | package api
3 |
4 | import (
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | pathpkg "path"
10 | "sync"
11 | "time"
12 | )
13 |
14 | var (
15 | // Whether or not to use HTTPS for requests.
16 | SSL bool = false
17 | // Cooldown time for updating threads using (*Thread).Update().
18 | // If it is set to less than 10 seconds, it will be re-set to 10 seconds
19 | // before being used.
20 | UpdateCooldown time.Duration = 15 * time.Second
21 | cooldown <-chan time.Time
22 | cooldownMutex sync.Mutex
23 | )
24 |
25 | const (
26 | APIURL = "a.4cdn.org"
27 | ImageURL = "i.4cdn.org"
28 | StaticURL = "s.4cdn.org"
29 | )
30 |
31 | func prefix() string {
32 | if SSL {
33 | return "https://"
34 | } else {
35 | return "http://"
36 | }
37 | }
38 |
39 | func get(base, path string, modify func(*http.Request) error) (*http.Response, error) {
40 | url := prefix() + pathpkg.Join(base, path)
41 | cooldownMutex.Lock()
42 | if cooldown != nil {
43 | <-cooldown
44 | }
45 | req, err := http.NewRequest("GET", url, nil)
46 | if err != nil {
47 | return nil, err
48 | }
49 | if modify != nil {
50 | err = modify(req)
51 | if err != nil {
52 | return nil, err
53 | }
54 | }
55 |
56 | resp, err := http.DefaultClient.Do(req)
57 | cooldown = time.After(1 * time.Second)
58 | cooldownMutex.Unlock()
59 | return resp, err
60 | }
61 |
62 | func getDecode(base, path string, dest interface{}, modify func(*http.Request) error) error {
63 | resp, err := get(base, path, modify)
64 | if err != nil {
65 | return err
66 | }
67 | defer resp.Body.Close()
68 | return json.NewDecoder(resp.Body).Decode(dest)
69 | }
70 |
71 | // Direct mapping from the API's JSON to a Go type.
72 | type jsonPost struct {
73 | No int64 `json:"no"` // Post number 1-9999999999999
74 | Resto int64 `json:"resto"` // Reply to 0 (is thread), 1-999999999999
75 | Sticky int `json:"sticky"` // Stickied thread? 0 (no), 1 (yes)
76 | Closed int `json:"closed"` // Closed thread? 0 (no), 1 (yes)
77 | Now string `json:"now"` // Date and time MM\/DD\/YY(Day)HH:MM (:SS on some boards)
78 | Time int64 `json:"time"` // UNIX timestamp UNIX timestamp
79 | Name string `json:"name"` // Name text or empty
80 | Trip string `json:"trip"` // Tripcode text (format: !tripcode!!securetripcode)
81 | Id string `json:"id"` // ID text (8 characters), Mod, Admin
82 | Capcode string `json:"capcode"` // Capcode none, mod, admin, admin_highlight, developer
83 | Country string `json:"country"` // Country code ISO 3166-1 alpha-2, XX (unknown)
84 | CountryName string `json:"country_name"` // Country name text
85 | Email string `json:"email"` // Email text or empty
86 | Sub string `json:"sub"` // Subject text or empty
87 | Com string `json:"com"` // Comment text (includes escaped HTML) or empty
88 | Tim int64 `json:"tim"` // Renamed filename UNIX timestamp + microseconds
89 | FileName string `json:"filename"` // Original filename text
90 | Ext string `json:"ext"` // File extension .jpg, .png, .gif, .pdf, .swf
91 | Fsize int `json:"fsize"` // File size 1-8388608
92 | Md5 []byte `json:"md5"` // File MD5 byte slice
93 | Width int `json:"w"` // Image width 1-10000
94 | Height int `json:"h"` // Image height 1-10000
95 | TnW int `json:"tn_w"` // Thumbnail width 1-250
96 | TnH int `json:"tn_h"` // Thumbnail height 1-250
97 | FileDeleted int `json:"filedeleted"` // File deleted? 0 (no), 1 (yes)
98 | Spoiler int `json:"spoiler"` // Spoiler image? 0 (no), 1 (yes)
99 | CustomSpoiler int `json:"custom_spoiler"` // Custom spoilers? 1-99
100 | OmittedPosts int `json:"omitted_posts"` // # replies omitted 1-10000
101 | OmittedImages int `json:"omitted_images"` // # images omitted 1-10000
102 | Replies int `json:"replies"` // total # of replies 0-99999
103 | Images int `json:"images"` // total # of images 0-99999
104 | BumpLimit int `json:"bumplimit"` // bump limit? 0 (no), 1 (yes)
105 | ImageLimit int `json:"imagelimit"` // image limit? 0 (no), 1 (yes)
106 | CapcodeReplies map[string][]int `json:"capcode_replies"`
107 | LastModified int64 `json:"last_modified"`
108 | }
109 |
110 | // A Post represents all of the attributes of a 4chan post, organized in a more directly usable fashion.
111 | type Post struct {
112 | // Post info
113 | Id int64
114 | Thread *Thread
115 | Time time.Time
116 | Subject string
117 | LastModified int64
118 |
119 | // These are only present in an OP post. They are exposed through their
120 | // corresponding Thread getter methods.
121 | replies int
122 | images int
123 | omitted_posts int
124 | omitted_images int
125 | bump_limit bool
126 | image_limit bool
127 | sticky bool
128 | closed bool
129 | custom_spoiler int // the number of custom spoilers on a given board
130 |
131 | // Poster info
132 | Name string
133 | Trip string
134 | Email string
135 | Special string
136 | Capcode string
137 |
138 | // Country and CountryName are empty unless the board uses country info
139 | Country string
140 | CountryName string
141 |
142 | // Message body
143 | Comment string
144 |
145 | // File info if any, otherwise nil
146 | File *File
147 |
148 | // only when they do this on /q/
149 | CapcodeReplies map[string][]int
150 | }
151 |
152 | func (self *Post) String() (s string) {
153 | s += fmt.Sprintf("#%d %s%s on %s:\n", self.Id, self.Name, self.Trip, self.Time.Format(time.RFC822))
154 | if self.File != nil {
155 | s += self.File.String()
156 | }
157 | s += self.Comment
158 | return
159 | }
160 |
161 | // ImageURL constructs and returns the URL of the attached image. Returns the
162 | // empty string if there is none.
163 | func (self *Post) ImageURL() string {
164 | file := self.File
165 | if file == nil {
166 | return ""
167 | }
168 | return fmt.Sprintf("%s%s/%s/%d%s",
169 | prefix(), ImageURL, self.Thread.Board, file.Id, file.Ext)
170 | }
171 |
172 | // ThumbURL constructs and returns the thumbnail URL of the attached image.
173 | // Returns the empty string if there is none.
174 | func (self *Post) ThumbURL() string {
175 | file := self.File
176 | if file == nil {
177 | return ""
178 | }
179 | return fmt.Sprintf("%s%s/%s/%ds%s",
180 | prefix(), ImageURL, self.Thread.Board, file.Id, ".jpg")
181 | }
182 |
183 | // A File represents an uploaded file's metadata.
184 | type File struct {
185 | Id int64 // Id is what 4chan renames images to (UNIX + microtime, e.g. 1346971121077)
186 | Name string // Original filename
187 | Ext string
188 | Size int
189 | MD5 []byte
190 | Width int
191 | Height int
192 | ThumbWidth int
193 | ThumbHeight int
194 | Deleted bool
195 | Spoiler bool
196 | }
197 |
198 | func (self *File) String() string {
199 | return fmt.Sprintf("File: %s%s (%dx%d, %d bytes, md5 %x)\n",
200 | self.Name, self.Ext, self.Width, self.Height, self.Size, self.MD5)
201 | }
202 |
203 | // CountryFlagURL returns the URL of the post's country flag icon, if enabled
204 | // on the board in question.
205 | func (self *Post) CountryFlagURL() string {
206 | if self.Country == "" {
207 | return ""
208 | }
209 | // lol /pol/
210 | if self.Thread.Board == "pol" {
211 | return fmt.Sprintf("%s://%s/image/country/troll/%s.gif", prefix(), StaticURL, self.Country)
212 | }
213 | return fmt.Sprintf("%s://%s/image/country/%s.gif", prefix(), StaticURL, self.Country)
214 | }
215 |
216 | // A Thread represents a thread of posts. It may or may not contain the actual replies.
217 | type Thread struct {
218 | Posts []*Post
219 | OP *Post
220 | Board string // without slashes ex. "g" or "ic"
221 |
222 | date_recieved time.Time
223 | cooldown <-chan time.Time
224 | }
225 |
226 | // GetIndex hits the API for an index of thread stubs from the given board and
227 | // page.
228 | func GetIndex(board string, page int) ([]*Thread, error) {
229 | resp, err := get(APIURL, fmt.Sprintf("/%s/%d.json", board, page + 1), nil)
230 | if err != nil {
231 | return nil, err
232 | }
233 | defer resp.Body.Close()
234 |
235 | threads, err := ParseIndex(resp.Body, board)
236 | if err != nil {
237 | return nil, err
238 | }
239 |
240 | now := time.Now()
241 | for _, t := range threads {
242 | t.date_recieved = now
243 | }
244 | return threads, err
245 | }
246 |
247 | // GetThreads hits the API for a list of the thread IDs of all the active
248 | // threads on a given board.
249 | func GetThreads(board string) ([][]int64, error) {
250 | p := make([]struct {
251 | Page int `json:"page"`
252 | Threads []struct {
253 | No int64 `json:"no"`
254 | } `json:"threads"`
255 | }, 0, 10)
256 | if err := getDecode(APIURL, fmt.Sprintf("/%s/threads.json", board), &p, nil); err != nil {
257 | return nil, err
258 | }
259 | n := make([][]int64, len(p))
260 | for _, page := range p {
261 | // Pages are 1 based in the json api
262 | n[page.Page-1] = make([]int64, len(page.Threads))
263 | for j, thread := range page.Threads {
264 | n[page.Page-1][j] = thread.No
265 | }
266 | }
267 | return n, nil
268 | }
269 |
270 | // GetThread hits the API for a single thread and all its replies. board is
271 | // just the board name, without the surrounding slashes. If a thread is being
272 | // updated, use an existing thread's Update() method if possible because that
273 | // uses If-Modified-Since in the request, which reduces unnecessary server
274 | // load.
275 | func GetThread(board string, thread_id int64) (*Thread, error) {
276 | return getThread(board, thread_id, time.Unix(0, 0))
277 | }
278 |
279 | func getThread(board string, thread_id int64, stale_time time.Time) (*Thread, error) {
280 | resp, err := get(APIURL, fmt.Sprintf("/%s/thread/%d.json", board, thread_id), func(req *http.Request) error {
281 | if stale_time.Unix() != 0 {
282 | req.Header.Add("If-Modified-Since", stale_time.UTC().Format(http.TimeFormat))
283 | }
284 | return nil
285 | })
286 | if err != nil {
287 | return nil, err
288 | }
289 | defer resp.Body.Close()
290 |
291 | thread, err := ParseThread(resp.Body, board)
292 | thread.date_recieved = time.Now()
293 |
294 | return thread, err
295 | }
296 |
297 | // ParseIndex converts a JSON response for multiple threads into a native Go
298 | // data structure
299 | func ParseIndex(r io.Reader, board string) ([]*Thread, error) {
300 | var t struct {
301 | Threads []struct {
302 | Posts []*jsonPost `json:"posts"`
303 | } `json:"threads"`
304 | }
305 |
306 | if err := json.NewDecoder(r).Decode(&t); err != nil {
307 | return nil, err
308 | }
309 |
310 | threads := make([]*Thread, len(t.Threads))
311 | for i, json_thread := range t.Threads {
312 | thread := &Thread{Posts: make([]*Post, len(t.Threads[i].Posts)), Board: board}
313 | for k, v := range json_thread.Posts {
314 | thread.Posts[k] = json_to_native(v, thread)
315 | if v.No == 0 {
316 | thread.OP = thread.Posts[k]
317 | }
318 | }
319 | // TODO: fix this up
320 | if thread.OP == nil {
321 | thread.OP = thread.Posts[0]
322 | }
323 | threads[i] = thread
324 | }
325 |
326 | return threads, nil
327 | }
328 |
329 | // ParseThread converts a JSON response for one thread into a native Go data
330 | // structure.
331 | func ParseThread(r io.Reader, board string) (*Thread, error) {
332 | var t struct {
333 | Posts []*jsonPost `json:"posts"`
334 | }
335 |
336 | if err := json.NewDecoder(r).Decode(&t); err != nil {
337 | return nil, err
338 | }
339 |
340 | thread := &Thread{Posts: make([]*Post, len(t.Posts)), Board: board}
341 | for k, v := range t.Posts {
342 | thread.Posts[k] = json_to_native(v, thread)
343 | if v.No == 0 {
344 | thread.OP = thread.Posts[k]
345 | }
346 | }
347 | // TODO: fix this up
348 | if thread.OP == nil {
349 | thread.OP = thread.Posts[0]
350 | }
351 |
352 | return thread, nil
353 | }
354 |
355 | func json_to_native(v *jsonPost, thread *Thread) *Post {
356 | p := &Post{
357 | Id: v.No,
358 | sticky: v.Sticky == 1,
359 | closed: v.Closed == 1,
360 | Time: time.Unix(v.Time, 0),
361 | Name: v.Name,
362 | Trip: v.Trip,
363 | Special: v.Id,
364 | Capcode: v.Capcode,
365 | Country: v.Country,
366 | CountryName: v.CountryName,
367 | Email: v.Email,
368 | Subject: v.Sub,
369 | Comment: v.Com,
370 | custom_spoiler: v.CustomSpoiler,
371 | replies: v.Replies,
372 | images: v.Images,
373 | omitted_posts: v.OmittedPosts,
374 | omitted_images: v.OmittedImages,
375 | bump_limit: v.BumpLimit == 1,
376 | image_limit: v.ImageLimit == 1,
377 | Thread: thread,
378 | CapcodeReplies: v.CapcodeReplies,
379 | LastModified: v.LastModified,
380 | }
381 | if len(v.FileName) > 0 {
382 | p.File = &File{
383 | Id: v.Tim,
384 | Name: v.FileName,
385 | Ext: v.Ext,
386 | Size: v.Fsize,
387 | MD5: v.Md5,
388 | Width: v.Width,
389 | Height: v.Height,
390 | ThumbWidth: v.TnW,
391 | ThumbHeight: v.TnH,
392 | Deleted: v.FileDeleted == 1,
393 | Spoiler: v.Spoiler == 1,
394 | }
395 | }
396 | return p
397 | }
398 |
399 | // Update an existing thread in-place.
400 | func (self *Thread) Update() (new_posts, deleted_posts int, err error) {
401 | cooldownMutex.Lock()
402 | if self.cooldown != nil {
403 | <-self.cooldown
404 | }
405 | var thread *Thread
406 | thread, err = getThread(self.Board, self.Id(), self.date_recieved)
407 | if UpdateCooldown < 10*time.Second {
408 | UpdateCooldown = 10 * time.Second
409 | }
410 | self.cooldown = time.After(UpdateCooldown)
411 | cooldownMutex.Unlock()
412 | if err != nil {
413 | return 0, 0, err
414 | }
415 | var a, b int
416 | // traverse both threads in parallel to check for deleted/appended posts
417 | for a, b = 0, 0; a < len(self.Posts); a, b = a+1, b+1 {
418 | if self.Posts[a].Id == thread.Posts[b].Id {
419 | continue
420 | }
421 | // a post has been deleted, go back one to compare with the next
422 | b--
423 | deleted_posts++
424 | }
425 | new_posts = len(thread.Posts) - b
426 | self.Posts = thread.Posts
427 | return
428 | }
429 |
430 | // Id returns the thread OP's post ID.
431 | func (self *Thread) Id() int64 {
432 | return self.OP.Id
433 | }
434 |
435 | func (self *Thread) String() (s string) {
436 | for _, post := range self.Posts {
437 | s += post.String() + "\n\n"
438 | }
439 | return
440 | }
441 |
442 | // Replies returns the number of replies the thread OP has.
443 | func (self *Thread) Replies() int {
444 | return self.OP.replies
445 | }
446 |
447 | // Images returns the number of images in the thread.
448 | func (self *Thread) Images() int {
449 | return self.OP.images
450 | }
451 |
452 | // OmittedPosts returns the number of posts omitted in a thread list overview.
453 | func (self *Thread) OmittedPosts() int {
454 | return self.OP.omitted_posts
455 | }
456 |
457 | // OmittedImages returns the number of image posts omitted in a thread list overview.
458 | func (self *Thread) OmittedImages() int {
459 | return self.OP.omitted_images
460 | }
461 |
462 | // BumpLimit returns true if the thread is at its bump limit, or false otherwise.
463 | func (self *Thread) BumpLimit() bool {
464 | return self.OP.bump_limit
465 | }
466 |
467 | // ImageLimit returns true if the thread can no longer accept image posts, or false otherwise.
468 | func (self *Thread) ImageLimit() bool {
469 | return self.OP.image_limit
470 | }
471 |
472 | // Closed returns true if the thread is closed for replies, or false otherwise.
473 | func (self *Thread) Closed() bool {
474 | return self.OP.closed
475 | }
476 |
477 | // Sticky returns true if the thread is stickied, or false otherwise.
478 | func (self *Thread) Sticky() bool {
479 | return self.OP.sticky
480 | }
481 |
482 | // CustomSpoiler returns the ID of its custom spoiler image, if there is one.
483 | func (self *Thread) CustomSpoiler() int {
484 | return self.OP.custom_spoiler
485 | }
486 |
487 | // CustomSpoilerURL builds and returns the URL of the custom spoiler image, or
488 | // an empty string if none exists.
489 | func (self *Thread) CustomSpoilerURL(id int, ssl bool) string {
490 | if id > self.OP.custom_spoiler {
491 | return ""
492 | }
493 | return fmt.Sprintf("%s://%s/image/spoiler-%s%d.png", prefix(), StaticURL, self.Board, id)
494 | }
495 |
496 | // A Board is the name and title of a single board.
497 | type Board struct {
498 | Board string `json:"board"`
499 | Title string `json:"title"`
500 | }
501 |
502 | // Board names/descriptions will be cached here after a call to LookupBoard or GetBoards
503 | var Boards []Board
504 |
505 | // LookupBoard returns the Board corresponding to the board name (without slashes)
506 | func LookupBoard(name string) (Board, error) {
507 | if Boards == nil {
508 | _, err := GetBoards()
509 | if err != nil {
510 | return Board{}, fmt.Errorf("Board '%s' not found: %v", name, err)
511 | }
512 | }
513 | for _, b := range Boards {
514 | if name == b.Board {
515 | return b, nil
516 | }
517 | }
518 | return Board{}, fmt.Errorf("Board '%s' not found", name)
519 | }
520 |
521 | // Get the list of boards.
522 | func GetBoards() ([]Board, error) {
523 | var b struct {
524 | Boards []Board `json:"boards"`
525 | }
526 | err := getDecode(APIURL, "/boards.json", &b, nil)
527 | if err != nil {
528 | return nil, err
529 | }
530 | Boards = b.Boards
531 | return b.Boards, nil
532 | }
533 |
534 | // A Catalog contains a list of (truncated) threads on each page of a board.
535 | type Catalog []struct {
536 | Page int
537 | Threads []*Thread
538 | }
539 |
540 | type catalog []struct {
541 | Page int `json:"page"`
542 | Threads []*jsonPost `json:"threads"`
543 | }
544 |
545 | // GetCatalog hits the API for a catalog listing of a board.
546 | func GetCatalog(board string) (Catalog, error) {
547 | if len(board) == 0 {
548 | return nil, fmt.Errorf("api: GetCatalog: No board name given")
549 | }
550 | var c catalog
551 | err := getDecode(APIURL, fmt.Sprintf("/%s/catalog.json", board), &c, nil)
552 | if err != nil {
553 | return nil, err
554 | }
555 |
556 | cat := make(Catalog, len(c))
557 | for i, page := range c {
558 | extracted := struct {
559 | Page int
560 | Threads []*Thread
561 | }{page.Page, make([]*Thread, len(page.Threads))}
562 | for j, post := range page.Threads {
563 | thread := &Thread{Posts: make([]*Post, 1), Board: board}
564 | post := json_to_native(post, thread)
565 | thread.Posts[0] = post
566 | extracted.Threads[j] = thread
567 | if thread.OP == nil {
568 | thread.OP = thread.Posts[0]
569 | }
570 | }
571 | cat[i] = extracted
572 | }
573 | return cat, nil
574 | }
575 |
--------------------------------------------------------------------------------
/api/api_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func try(t *testing.T, err error) {
9 | if err != nil {
10 | t.Fatal(err)
11 | }
12 | }
13 |
14 | func assert(t *testing.T, expr bool, desc string) {
15 | if !expr {
16 | t.Fatal("Failed:", desc)
17 | }
18 | }
19 |
20 | func TestParseThread(t *testing.T) {
21 | file, err := os.Open("example.json")
22 | try(t, err)
23 | defer file.Close()
24 |
25 | thread, err := ParseThread(file, "ck")
26 | try(t, err)
27 |
28 | assert(t, thread.OP.Name == "Anonymous", "OP's name should be Anonymous")
29 | assert(t, thread.Id() == 3856791, "Thread id should be 3856791")
30 | assert(t, thread.OP.File != nil, "OP post should have a file")
31 | assert(t, len(thread.Posts) == 38, "Thread should have 38 posts")
32 | imageURL := thread.OP.ImageURL()
33 | assert(t, imageURL == "http://i.4cdn.org/ck/1346968817055.jpg", "Image URL should be 'http://i.4cdn.org/ck/1346968817055.jpg' (got '"+imageURL+"')")
34 | thumbURL := thread.OP.ThumbURL()
35 | assert(t, thumbURL == "http://i.4cdn.org/ck/1346968817055s.jpg", "Thumb URL should be 'http://i.4cdn.org/ck/1346968817055s.jpg' (got '"+thumbURL+"')")
36 | }
37 |
38 | func TestGetIndex(t *testing.T) {
39 | threads, err := GetIndex("a", 0)
40 | try(t, err)
41 | assert(t, len(threads) > 0, "Threads should exist")
42 | }
43 |
44 | func TestGetThreads(t *testing.T) {
45 | n, err := GetThreads("a")
46 | try(t, err)
47 | for _, q := range n {
48 | for _, p := range q {
49 | if p == 0 {
50 | t.Fatal("There are #0 posts")
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/api/catalog_example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "page": 0,
4 | "threads": [
5 | {
6 | "no": 79149278,
7 | "now": "01\/28\/13(Mon)13: 12",
8 | "name": "Anonymous",
9 | "com": "just started watching this, what does \/a\/ think of it?",
10 | "filename": "Mirai_Nikki_1167%20X%20931_1925",
11 | "ext": ".png",
12 | "w": 1167,
13 | "h": 931,
14 | "tn_w": 250,
15 | "tn_h": 199,
16 | "tim": 1359396727618,
17 | "time": 1359396727,
18 | "md5": "BUHVGqnNwVyy8TMVevQuMw==",
19 | "fsize": 866013,
20 | "resto": 0,
21 | "bumplimit": 0,
22 | "imagelimit": 0,
23 | "custom_spoiler": 1,
24 | "replies": 74,
25 | "images": 19
26 | },
27 | {
28 | "no": 79154415,
29 | "now": "01\/28\/13(Mon)15: 12",
30 | "name": "Anonymous",
31 | "com": "A-am I missing something here \/a\/?",
32 | "filename": "[Mazui]_Boku_Ha_Tomodachi_Ga_Sukunai_NEXT_-_01_[7F653193].mkv_snapshot_22.09_[2013.01.28_18.11.43]",
33 | "ext": ".jpg",
34 | "w": 1280,
35 | "h": 720,
36 | "tn_w": 250,
37 | "tn_h": 140,
38 | "tim": 1359403948358,
39 | "time": 1359403948,
40 | "md5": "EMMiugv5enGTDwkleAuv0g==",
41 | "fsize": 83453,
42 | "resto": 0,
43 | "bumplimit": 0,
44 | "imagelimit": 0,
45 | "custom_spoiler": 1,
46 | "replies": 41,
47 | "images": 12
48 | }
49 | ]
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/api/example.json:
--------------------------------------------------------------------------------
1 | {"posts": [{"no":3856791,"sticky":0,"closed":0,"now":"09\/06\/12(Thu)18:00","name":"Anonymous","email":"","sub":"","com":"All industrial food is based on something real you can make at home, and some people used to make at home.
How do I make white bread?","filename":"White-Bread","ext":".jpg","w":400,"h":280,"tn_w":250,"tn_h":175,"tim":1346968817055,"time":1346968817,"md5":"\/o72jJQixXCBZXFD0htPBg==","fsize":26089,"resto":0,"trip":""},{"no":3856796,"now":"09\/06\/12(Thu)18:02","name":"Anonymous","email":"","sub":"","com":"Do you want to make sliced bread or unsliced?","time":1346968934,"resto":3856791,"trip":""},{"no":3856800,"now":"09\/06\/12(Thu)18:03","name":"Anonymous","email":"","sub":"","com":">>3856796<\/a><\/span>
you comedian!","time":1346968990,"resto":3856791,"trip":""},{"no":3856806,"now":"09\/06\/12(Thu)18:05","name":"Anonymous","email":"","sub":"","com":">>3856796<\/a><\/span>
I think I know how to slice it.","time":1346969133,"resto":3856791,"trip":""},{"no":3856811,"now":"09\/06\/12(Thu)18:06","name":"RF360","email":"","sub":"","com":"Baking bread is a seriously complex order of cooking, and it's really much easier if you have a bread machine.
Since you're asking how, I'll assume you don't.
The short answer is, saunter out to a Goodwill and buy a used breadmachine, clean it up, use it 3 times to make bread and then get tired of making your own bread and return the machine to the goodwill again.
... hahaha.
If you are SERIOUS, though, I can give you a bread recipe.
Be aware.
Bread is delicate, complicated, and requires precision, timing, and a bit of luck in the environment!","time":1346969211,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856814,"now":"09\/06\/12(Thu)18:08","name":"Anonymous","email":"","sub":"","com":">>3856811<\/a><\/span>
No I'm pretty serious. One-purpose appliances and tools are dumb as fuck.
I'm probably not going to make my own bread. I just want to do it once. And who knows. Like that time I made my own butter.","time":1346969318,"resto":3856791,"trip":""},{"no":3856816,"now":"09\/06\/12(Thu)18:08","name":"Anonymous","email":"","sub":"","com":"I use this recipe:
Grandma VanDoren's White Bread(found on allrecipes)
3 cups warm water
3 tablespoons active dry yeast
3 teaspoons salt
4 tablespoons vegetable oil
1\/2 cup white sugar
8 cups bread flour
In a large bowl, combine warm water, yeast, salt, oil, sugar, and 4 cups flour. Mix thoroughly, and let sponge rise until doubled in size. Gradually add about 4 cups flour, kneading until smooth. Place dough in a greased bowl, and turn several times to coat. Cover with a damp cloth. Allow to rise until doubled. Punch down the dough, let it rest a few minutes. Divide dough into three equal parts. Shape into loaves, and place in three 8 1\/2 x 4 1\/2 inch greased bread pans. Let rise until almost doubled. Bake at 350 degrees F (175 degrees C) for 35 to 45 minutes(with a pan of water under them to lessen browning on the bottoms). The loaves may need to be covered for the last few minutes with foil to prevent excess browning.
Easy and nice light texture.","time":1346969339,"resto":3856791,"trip":""},{"no":3856820,"now":"09\/06\/12(Thu)18:11","name":"Anonymous","email":"","sub":"","com":">>3856816<\/a><\/span>
Can use a loaf pan?","time":1346969463,"resto":3856791,"trip":""},{"no":3856821,"now":"09\/06\/12(Thu)18:11","name":"Anonymous","email":"","sub":"","com":"How do I make marshmallows? And cotton candy? What about gummy worms? And nougat?","time":1346969517,"resto":3856791,"trip":""},{"no":3856823,"now":"09\/06\/12(Thu)18:13","name":"Anonymous","email":"","sub":"","com":">>3856811<\/a><\/span>
Don't listen to this tard. Couple of tries and you'll be a pro at basic bread(fancy breads will take some practice but worth it too). If you're really novice at baking in general try a no knead recipe to start- it will still be better than crummy sliced storebought bread.
As long as you follow instructions(and don't kill your yeast with overly hot water) you'll be fine.","time":1346969629,"resto":3856791,"trip":""},{"no":3856826,"now":"09\/06\/12(Thu)18:15","name":"Anonymous","email":"","sub":"","com":">>3856820<\/a><\/span>
>8 1\/2 x 4 1\/2 inch loaf pans<\/span>
if you have 9 x 5 that's fine too","time":1346969757,"resto":3856791,"trip":""},{"no":3856828,"now":"09\/06\/12(Thu)18:17","name":"Anonymous","email":"","sub":"","com":">>3856821<\/a><\/span>
Homemade Marshmallows
.75-oz unflavored gelatin (3 envelopes of Knox gelatin)
1\/2 cup cold water
2 cups granulated sugar
2\/3 cups light corn syrup
1\/4 cup water
1\/4 teaspoon salt
1 tablespoon vanilla extract
Line 9 x 9-inch pan with plastic wrap and lightly oil it. Set aside.
In the bowl of an electric mixer, sprinkle gelatin over 1\/2 cup cold water. Soak for about 10 minutes.
Meanwhile, combine sugar, corn syrup and 1\/4 cup water in a small saucepan. Bring the mixture to a rapid boil and boil hard for 1 minute.
Pour the boiling syrup into soaked gelatin and turn on the mixer, using the whisk attachment, to high speed. Add the salt and beat for 12 minutes. After 12 minutes, add in the vanilla extract beat to incorporate.
Scrape marshmallow into the prepared pan and spread evenly (Lightly greasing your hands and the spatula helps a lot here). Take another piece of lightly oiled plastic wrap and press lightly on top of the marshmallow, creating a seal. Let mixture sit for a few hours, or overnight, until cooled and firmly set.
In a shallow dish, combine equal parts cornstarch and confectioners\u2019 sugar. Remove marshmallow from pan and cut into equal pieces with scissors (the best tool for the job) or a chef\u2019s knife. Dredge each piece of marshmallow in confectioners\u2019 sugar mixture.
Store in an airtight container.
My batch pictured here made 36 big marshmallows. I often cut them down into smaller sizes. Enjoy!","time":1346969827,"resto":3856791,"trip":""},{"no":3856831,"now":"09\/06\/12(Thu)18:17","name":"Anonymous","email":"","sub":"","com":">>3856821<\/a><\/span>
I don't know about the rest, but gummi worms are easy.
Make very strong jello (use about 1\/10 the water you normally would). Add a little citric acid powder (available at many supermarkets as well as drugstores) if you want them sour. Omit the citric acid if you don't want them sour. While the gelatin mixture is still hot, squeeze it out of a pastry bag (or a ziploc bag with a corner cut off) onto a silpat or into a basin of ice water.","time":1346969845,"resto":3856791,"trip":""},{"no":3856832,"now":"09\/06\/12(Thu)18:18","name":"Anonymous","email":"","sub":"","com":">>3856821<\/a><\/span>
1 (3 ounce) box Jello gelatin , any flavor
7 envelopes unflavored gelatin
1\/2 cup water
Mix all ingredients in a saucepan until the mixture resembles playdough.Place the pan over low heat and stir until melted.Once completely melted, pour into plastic candy molds and place in freezer for 5 min. When very firm, remove from molds.
Candy isn't that complex either brah. Just takes patience and accurate temperature generally.","time":1346969939,"resto":3856791,"trip":""},{"no":3856833,"now":"09\/06\/12(Thu)18:19","name":"RF360","email":"","sub":"","com":"ok, then let's have a Workshop of it.
Time now for,
Cooking Experiment: Bread
PREFACE
Bread's about as old as stone tools. Yeast risen bread was first eaten in ancient Egypt. That's how old leavening is.
Fascinating.
EQUIPMENT CHECK!
Tell me what you have to use.
If you have a HEAVY DUTY MIXER - with a --Dough Hook-- yes, that bendy-shaped hooklike PRONG you NEVER had a use for - your workload just got fractioned. A food processor with doughblades can also be handy.
BREAD PANS are basically unrecognizable by today's youth, so I don't expect you have one or know if you do. If you do please tell me.
BREAD PANS AND\/OR PIZZA STONES: You're gonna need something to bake bread on. If you want bread quick, you want glass not metal.
There's no way you have a scoring tool. I'll make this short - to cut bread you want a very sharp knife.
PROOFING: You will NEED NEED NEED a glass or plastic ceramic bowl. You CAN NOT USE METAL.
a SPRAY BOTTLE that contains only water can be really helpful.
a DOUGH SCRAPER, a flat broad plastic or metal square, can also make life easier.
There's no way you have a baker's paddle
-3-
A SCALE would be EXTREMELY USEFUL.
So would an oven thermometer.
Lastly, you'll need a timer.
What do you have, OP?","time":1346969946,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856838,"now":"09\/06\/12(Thu)18:24","name":"Anonymous","email":"","sub":"","com":"not OP but I am interested in this as well.","time":1346970258,"resto":3856791,"trip":""},{"no":3856839,"now":"09\/06\/12(Thu)18:24","name":"Anonymous","email":"","sub":"","com":">>3856828<\/a><\/span>
>whisk on high for 12 minutes<\/span>
So it's basically air jello. Interesting.
>>3856833<\/a><\/span>
A hand mixer with hooks and whisks. A loaf pan. I guess I can find a spray bottle. I obviously have sharp knives. I don't have a pizza stone. I have glass bowls of many sizes. I don't know if it's oven proof though. I have a DIY dough scraper. No oven thermometer.
I didn't just wander into this board from \/b\/. I cook. I just haven't baked before.
Honestly, I doubt any housewife had all this shit a hundred years ago.","time":1346970263,"resto":3856791,"trip":""},{"no":3856840,"now":"09\/06\/12(Thu)18:25","name":"Anonymous","email":"","sub":"","com":">>3856833<\/a><\/span>
>mfw my mom makes shredded wheat bread all the time by rising in her stainless steel mixing bowl. <\/span>
where is your god now??","filename":"565e76364648","ext":".jpg","w":345,"h":345,"tn_w":125,"tn_h":125,"tim":1346970355466,"time":1346970355,"md5":"L7uk6UZPGoJaDV2lmg04Og==","fsize":92592,"resto":3856791,"trip":""},{"no":3856844,"now":"09\/06\/12(Thu)18:26","name":"Anonymous","email":"","sub":"","com":">white bread<\/span>","filename":"1323908759524","ext":".jpg","w":800,"h":600,"tn_w":125,"tn_h":93,"tim":1346970405978,"time":1346970405,"md5":"6\/gQE3EkK9PCaJ5LxMjkZA==","fsize":112973,"resto":3856791,"trip":""},{"no":3856846,"now":"09\/06\/12(Thu)18:27","name":"RF360","email":"","sub":"","com":">>3856839<\/a><\/span>
the glass bowl's not for baking, it's going to be for letting the bread dough rise. If you use metal it will react.
Next question is: How much time do you want to put into this bread? quickstarter breads can be done in an afternoon. But methods like the Sponge, or Indirect, can take DAYS. In tradeoff, you of course can get extremely high quality texture and the fully aged flavors you find only in European breads - not those pathetic mimicries at Safeway either.
I recommend something simple since you are just tinkering. How much time? A day or so?","time":1346970444,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856849,"now":"09\/06\/12(Thu)18:28","name":"RF360","email":"","sub":"","com":">>3856840<\/a><\/span>
Stainless steel's fine but I couldn't take my chances with OP knowing whether he had stainless steel.
You're just avoiding a yeast reaction with the proofing bowl.
PS. You're an asshat","time":1346970506,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856851,"now":"09\/06\/12(Thu)18:29","name":"Anonymous","email":"","sub":"","com":">>3856839<\/a><\/span>
They're just being douchey trying to make bread sound hard. Learn basic bread with simple recipes first- worry about their fancy nitpicky stuff later. A pan of water in the oven will result in a lighter, softer bottom crust though.","time":1346970583,"resto":3856791,"trip":""},{"no":3856855,"now":"09\/06\/12(Thu)18:31","name":"Anonymous","email":"","sub":"","com":">>3856849<\/a><\/span>
>PS. You're an asshat<\/span>
I know you are but what am I??","filename":"7855957579","ext":".jpg","w":600,"h":509,"tn_w":125,"tn_h":106,"tim":1346970679869,"time":1346970679,"md5":"O8WzOBaf8oXDDqVB2gvJRg==","fsize":78756,"resto":3856791,"trip":""},{"no":3856860,"now":"09\/06\/12(Thu)18:33","name":"Anonymous","email":"","sub":"","com":">>3856814<\/a><\/span>
bread machines don't really serve one purpose, there's a shitload you can do in a breadmaker even if you're not counting the bajillion kinds of bread you can make","time":1346970835,"resto":3856791,"trip":""},{"no":3856861,"now":"09\/06\/12(Thu)18:34","name":"RF360","email":"","sub":"","com":"while you decide how much time you want to spend cooking the bread, I'll go over some of the theory and application of breadmaking.","time":1346970843,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856864,"now":"09\/06\/12(Thu)18:35","name":"Anonymous","email":"","sub":"","com":">>3856849<\/a><\/span>
I want to make basic bread. Anything longer than a 24 hour rising period is outrageous.","time":1346970931,"resto":3856791,"trip":""},{"no":3856865,"now":"09\/06\/12(Thu)18:35","name":"Anonymous","email":"","sub":"","com":"Back then your wife would be doing all of that. You can't live your normal life and do everything the inconvenient way. theres no time.","time":1346970934,"resto":3856791,"trip":""},{"no":3856872,"now":"09\/06\/12(Thu)18:40","name":"RF360","email":"","sub":"","com":"KNEADING
is the word for beating the crap out of a lump of flour, water, leavening, and added ingredients.
it changes the basic ingredients into a smooth, and elastic bread dough, and it all works on the magic of Gluten. Gluten is a protein web that forms when two simple proteins in flour mix with liquid. It's like stretching and relaxing a rubber band - it gradually gets bigger and looser. You can do it with a mixer but many experienced bakers do it by hand - often because they simply enjoy doing it that way. When you start to knead a dough, it should be just a bit sticky. You should always have your hands greased or floured when you work a dough so it doesn't stick to the dough itself much. The dough will be smooth and elastic, and more... er... tacky than sticky.
RISING is what happens when the bread dough ferments. Yes, it ferments, from the Yeast. The gas Carbon Dioxide gets trapped in the sticky web of Gluten and streeetches and expands the bread just like a balloon, and the dough gets bigger.
Most doughs can only stand to about double, before the BUBBLE POPS, so to speak - and the dough falls back on itself T.T","time":1346971213,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856873,"now":"09\/06\/12(Thu)18:40","name":"RF360","email":"","sub":"","com":">>3856864<\/a><\/span>
ok, that's reasonable.
I'll draw up the recipe for you then","time":1346971250,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856882,"now":"09\/06\/12(Thu)18:48","name":"Anonymous","email":"","sub":"","com":"replicating shelf brand sandwich bread is going to be surprisingly difficult, actually. making nice sandwich bread isn't, but to exactly replicate industrially produced stuff is going to be a bit harder. is that precisely what you want to do?
cause otherwise something like this: http:\/\/www.wildyeastblog.com\/2011\/07\/14\/soft-sandwich-sourdough\/ will be nice","time":1346971682,"resto":3856791,"trip":""},{"no":3856883,"now":"09\/06\/12(Thu)18:48","name":"RF360","email":"","sub":"","com":"Generally when you make a bread, you
MIX it
KNEAD it
RISE it
SHAPE it
and RISE it one last time.
I'll give you the easiest quickest recipe I have, and then bombard you with details on the process - so you can proceed to hang out and garner as much info as you want before you give it a try.
Your ingredients are:
PART A:
2 cups bread flour (Please use bread flour! Not regular flour!)
1 tbsp sugar
1 package quick rising active yeast
1 1\/4 tbsp salt
PART B
1 cup very warm water (about 120 degrees F, get the temperature right, this is very important.)
2 tbsp melted butter
1 cup more bread flour
Some nice oil
The BASIC DIRECTIONS are:
Mix PART A in the bowl with the mixer.
Add PART B, first the liquids and then gradually adding the extra flour until the dough is moist, but NOT sticky.
Knead 10 minutes.
Have the glass bowl oiled; Transfer to the glass bowl. Cover with plastic wrap and let it rise in 80 degrees F until it doubles in volume - 30 to 45 mins.
Grease a 6-cup pan, punch down the dough, shape into a loaf, and place it seam-side-down into the pan.
Oil a piece of plastic wrap and set it over the top loosely.
Rise until it doubles again, once more 30 to 45 minutes.
Preheat to 450 degrees. Bake the bread 10 minutes.
Reduce to 350 degrees, 30 more minutes.
When the bread is done, it will sound hollow when you tap it.
Take the bread out of the pan, put it on a rack, and cool completely before you serve\/use\/eat it.
Now the basics are fully listed, let's go over how you ACTUALLY do this shit","time":1346971711,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856886,"now":"09\/06\/12(Thu)18:48","name":"Anonymous","email":"","sub":"","com":"Not trying to hijack this thread but I've seen quite a few threads with people talking about sourdough starters and "hydration." I somewhat understand the sourdough starter term but not he hydration term so much. I've only made bread before with home-made pizza dough so I'd really appreciate it if someone could help me get the the next level.","time":1346971738,"resto":3856791,"trip":""},{"no":3856899,"now":"09\/06\/12(Thu)18:53","name":"RF360","email":"","sub":"","com":"The Mixing Process
Attach the paddle blade if you have one. Start by taking 2\/3rds of the flour and all the other dry ingredients (the flour is pre-divided in the recipe, yay for that) and mix on low speed for 2-3 minutes while you add the liquid yeast mixture. You want to add as much flour as you need for the dough to clean the sides of the bowl. Now attach the dough hook if you have one. This will start the kneading process. Just add more flour to keep the dough from sticking.","time":1346972005,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856913,"now":"09\/06\/12(Thu)18:57","name":"Anonymous","email":"","sub":"","com":">>3856886<\/a><\/span>
Hydration refers to how much your water to flour ratio is. Basically this "A 100% hydration sourdough starter is a culture which is kept and fed with water and flour at equal weights. Like for instance 5 oz water to 5 oz flour. A 166% hydration starter is fed with equal volume of flour and water, which most typically is one cup of water (8.3 oz) and one cup of flour (5 oz)."
It's important to have the right type of starter for a recipe- if the hydration is wrong you may wind up with an overly wet or dry resulting dough.","time":1346972240,"resto":3856791,"trip":""},{"no":3856919,"now":"09\/06\/12(Thu)18:58","name":"Anonymous","email":"","sub":"","com":">>3856913<\/a><\/span>
PS- 100% hydration seems the most commonly used starter.","time":1346972305,"resto":3856791,"trip":""},{"no":3856925,"now":"09\/06\/12(Thu)18:59","name":"Anonymous","email":"","sub":"","com":">>3856886<\/a><\/span>
hydration simply refers to the amount of water proportional to the amount of flour. 100% hydration means a 50\/50 ratio of water to flour by weight. if the water and flour are equal by volume (e.g: a cup of water to a cup of flour) then it is 166% hydration. it generally refers to starter cultures.","time":1346972385,"resto":3856791,"trip":""},{"no":3856926,"now":"09\/06\/12(Thu)18:59","name":"RF360","email":"","sub":"","com":"The Kneading Step
Let's assume you don't have a fancy dough hook, though, and you need to knead by hand. That's the case with a lot of hand mixers as they may not have the POWER they need to knead the bread...
Butter or flour your hands so they won't stick. I recommend using a little bread flour, it's the easy answer, but some people swear by a thin smear of butter. Work the dough with the HEELS of your hands. Push firmly, and pressure it against he work surface. The dough should fold over itself as you work.
Push the dough away from yourself, shove it and peel it off the surface, reform it into a loose ball, and then give it a quarter turn and shove it some more. Do this about 10 minutes. If you have a scraper it can be handy to keep the dough together at this point.
Once the dough gets smooth and elastic, you have developed the GLUTEN, and the bread dough is ready to rise. Failing this step means your bread dough won't rise properly (like trying to blow a bubble with unchewed gum it JUST DOESN'T WORK.)
Here's how you can test for success. Slowly, gently, stretch a little piece of dough, turning it in a circle as you stretch it out. If the dough can form a sheer membrane, thin enough that light comes through it, your bread bubblegum is ready to rock.
You can also use the thermometer method if you have an instant read. The center of the activated bread will read 79 degrees F when it's perfect.","time":1346972391,"resto":3856791,"trip":"!!s1shuD45usb"},{"no":3856927,"now":"09\/06\/12(Thu)19:00","name":"Anonymous","email":"","sub":"","com":">>3856913<\/a><\/span>
huh you answered it for me with almost exactly the same info. i should refresh more.","time":1346972454,"resto":3856791,"trip":""}]}
2 |
--------------------------------------------------------------------------------
/api/example_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func ExampleVariables() {
9 | // All requests will be made with HTTPS
10 | SSL = true
11 |
12 | // will be pulled up to 10 seconds when first used
13 | UpdateCooldown = 5 * time.Second
14 |
15 | // get index, threads, etc
16 | }
17 |
18 | func ExampleGetIndex() {
19 | threads, err := GetIndex("a", 0)
20 | if err != nil {
21 | panic(err)
22 | }
23 | for _, thread := range threads {
24 | fmt.Println(thread)
25 | }
26 | }
27 |
28 | func ExampleThread() {
29 | thread, err := GetThread("a", 77777777)
30 | if err != nil {
31 | panic(err)
32 | }
33 | // will block until the cooldown is reached
34 | thread.Update()
35 | fmt.Println(thread)
36 | }
37 |
--------------------------------------------------------------------------------