├── .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 | [![Build Status](https://travis-ci.org/moshee/go-4chan-api.svg?branch=master)](https://travis-ci.org/moshee/go-4chan-api) [![GoDoc](https://godoc.org/github.com/moshee/go-4chan-api/api?status.png)](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 | --------------------------------------------------------------------------------