~theorytoe/gochan

27890360aea550ea48952d5cde6f40ac53c82e39 — TheoryToE 1 year, 8 months ago main
Initial commit
7 files changed, 344 insertions(+), 0 deletions(-)

A Makefile
A README.md
A go.mod
A go.sum
A lib/apiHandler/BoardThreads.go
A lib/apiHandler/Thread.go
A main.go
A  => Makefile +5 -0
@@ 1,5 @@
# SRC := $(shell find . -type f -name "*.go")
BIN := ./gochan

all:
	go build -o $(BIN) main.go

A  => README.md +19 -0
@@ 1,19 @@
# gochan

A simple unix-like tool for browsing 4chan posts in text-only.

### Build

```
git clone https://git.sr.ht/~theorytoe/gochan
cd ./gochan
go mod tidy
go run main.go -h
```

Standalone binary
```
make
./gochan
```


A  => go.mod +11 -0
@@ 1,11 @@
module gochan

go 1.17

require github.com/JohannesKaufmann/html-to-markdown v1.3.3

require (
	github.com/PuerkitoBio/goquery v1.5.1 // indirect
	github.com/andybalholm/cascadia v1.1.0 // indirect
	golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect
)

A  => go.sum +37 -0
@@ 1,37 @@
github.com/JohannesKaufmann/html-to-markdown v1.3.3 h1:T2A3aYmbGokj0LeRVLr3sWXtpNlM9jWq06qPxcXOK4Y=
github.com/JohannesKaufmann/html-to-markdown v1.3.3/go.mod h1:JNSClIRYICFDiFhw6RBhBeWGnMSSKVZ6sPQA+TK4tyM=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s=
github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc=
github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

A  => lib/apiHandler/BoardThreads.go +48 -0
@@ 1,48 @@
package apiHandler

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
)

// a recreation of the json data from the api request.
// []Page referes to the page of the post
// []Threads refers to the threads on each page
// attributes No ... Replies are from a []Thread
type ReqBoardThreads []struct {
	Page    int `json:"page"`
	Threads []struct {
		No           int `json:"no"`
		LastModified int `json:"last_modified"`
		Replies      int `json:"replies"`
	} `json:"threads"`
}

// Simple wrapper for creating a http req, and returning
// a struct with a board listing.
// 'b' is the board to fetch data from
func GetBoardThreads(b string) ReqBoardThreads {

	// reqest data from api with 't' being the board
	req, err := http.Get("https://a.4cdn.org/" + b + "/threads.json")
	if err != nil {
		panic(err)
	}

	// defer closing
	defer req.Body.Close()

	// read data to []byte
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		panic(err)
	}

	// create a struct with the data
	data := ReqBoardThreads{}

	// interpert + return
	json.Unmarshal([]byte(body), &data)
	return data
}

A  => lib/apiHandler/Thread.go +66 -0
@@ 1,66 @@
package apiHandler

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

// a recreation of a thread from an api reqest
type ReqThread struct {
	Posts []struct {
		No          int    `json:"no"`
		Sticky      int    `json:"sticky"`
		Closed      int    `json:"closed"`
		Now         string `json:"now"`
		Name        string `json:"name"`
		Sub         string `json:"sub"`
		Com         string `json:"com"`
		Filename    string `json:"filename"`
		Ext         string `json:"ext"`
		W           int    `json:"w"`
		H           int    `json:"h"`
		TnW         int    `json:"tn_w"`
		TnH         int    `json:"tn_h"`
		Tim         int64  `json:"tim"`
		Time        int    `json:"time"`
		Md5         string `json:"md5"`
		Fsize       int    `json:"fsize"`
		Resto       int    `json:"resto"`
		Capcode     string `json:"capcode"`
		SemanticURL string `json:"semantic_url,omitempty"`
		Replies     int    `json:"replies,omitempty"`
		Images      int    `json:"images,omitempty"`
		UniqueIps   int    `json:"unique_ips,omitempty"`
	} `json:"posts"`
}

// Simple wrapper for creating a http req, and returning
// a struct with a thread listing.
// 'b' is the board to fetch data from
// 't' is the id of the thread
func GetThread(b string, t int) ReqThread {

	// reqest the content of given thread
	req, err := http.Get("https://a.4cdn.org/" + b + "/thread/" + fmt.Sprint(t) + ".json")
	if err != nil {
		panic(err)
	}

	// defer closing
	defer req.Body.Close()

	// read data to []byte
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		panic(err)
	}

	// create data struct
	data := ReqThread{}

	// write + return
	json.Unmarshal([]byte(body), &data)
	return data
}

A  => main.go +158 -0
@@ 1,158 @@
package main

import (
	"os"
	"flag"
	"fmt"
	md "github.com/JohannesKaufmann/html-to-markdown"
	"gochan/lib/apiHandler"
)

// command line flags
var (
	fBoard    string // the board being picked
	fPageNum  int    // the page number of threads in that board
	fThreadId int
	fPostId   int // the post id
	fPostOP   int // used with -i, the original thread to search for a comment via id

	fHelp     bool // print help dialouge

	MaxPages  int = 10 // the maxium of pages in a request, this is the hard limit
)

func getPage(request apiHandler.ReqBoardThreads, converter md.Converter) {
	// length of request
	reqLen := len(request[fPageNum].Threads)
	for i := 0; i < reqLen; i++ {
		// get the content of a thread, including
		// title, author, date, and content (in markdown)
		fmt.Print("-------------BEGIN CONTENT #")
		fmt.Println(i)
		thread := apiHandler.GetThread(fBoard, int(request[fPageNum].Threads[i].No))
		for j := 0; j < len(thread.Posts); j++ {

			// if the thread title is empty, just print
			// the post title
			threadTitle := thread.Posts[j].Sub
			if threadTitle == "" {
				fmt.Print("Post #")
				fmt.Println(j)
			} else {
				fmt.Println(threadTitle)
			}

			fmt.Println(thread.Posts[j].Name) // print author name (90% of the time anonymous)
			fmt.Println(thread.Posts[j].Now)  // print date of post
			fmt.Println(thread.Posts[j].No)   // print ID of post (for comment links)

			// convert the html markup of the post into
			// markdown for easier reading
			fmt.Println("Post Content:")
			content, err := converter.ConvertString(thread.Posts[j].Com)
			if err != nil {
				panic("Issue with converting post" + string(j) + "'s content into markdown")
			}
			fmt.Println(content)

			// bar for cleanliness
			fmt.Println("---------------------")

		}
		// bar for cleanliness
		fmt.Print("-------------END CONTENT #")
		fmt.Println(i)
	}

}

// search a thread for a certain post/comment (by id)
func getPostById(op int, id int, converter md.Converter) {

	// if the op is the id, then just print the entire thread
	if op == id {
		thread := apiHandler.GetThread(fBoard, op)
		for i := 0; i < len(thread.Posts); i++ {

			// print title and date of post
			fmt.Println(thread.Posts[i].Name) // print author name (90% of the time anonymous)
			fmt.Println(thread.Posts[i].Now)  // print date of post
			fmt.Println(thread.Posts[i].No)   // print ID of post (for comment links)

			// convert html markup to markdown for post body
			content, err := converter.ConvertString(thread.Posts[i].Com)
			if err != nil {
				panic("Issue with converting post" + string(i) + "'s content into markdown")
			}
			fmt.Println(content)
		}
		// search for a post of id <id> in op thread
	} else {
		thread := apiHandler.GetThread(fBoard, op)
		for i := 0; i < len(thread.Posts); i++ {
			if thread.Posts[i].No == id {

				// print title and date of post
				fmt.Println(thread.Posts[i].Name) // print author name (90% of the time anonymous)
				fmt.Println(thread.Posts[i].Now)  // print date of post
				fmt.Println(thread.Posts[i].No)   // print ID of post (for comment links)

				// convert html markup to markdown for post body
				content, err := converter.ConvertString(thread.Posts[i].Com)
				if err != nil {
					panic("Issue with converting post" + string(i) + "'s content into markdown")
				}
				fmt.Println(content)
			}
		}
	}
}

func main() {
	// get and parse command line flags
	flag.StringVar(&fBoard, "b", "g", "Board to grab results from")
	flag.IntVar(&fPageNum, "p", 1, "Page of threads")
	flag.IntVar(&fPostId, "i", 0, "Id of individual thread")
	flag.IntVar(&fPostOP, "op", 0, "Id of OP to search for a comment")
	flag.BoolVar(&fHelp, "h", false, "Print help dialouge")
	flag.Parse()

	// print help dialouge
	if fHelp == true {
		flag.PrintDefaults()
		os.Exit(0)
	}

	// panic if the fPageNum is above the actual returned amount of
	// boards
	if fPageNum > MaxPages {
		panic("Too large of a page number! (range: 1-10)")
	}

	// make struct from board request
	request := apiHandler.GetBoardThreads(fBoard)

	// decalre a html -> markdown converter for
	// displaying content
	mdcov := md.NewConverter("", true, nil)

	// if a postid query is passed
	if fPostId != 0 {
		// check if an opriginal post id is passed
		if fPostOP != 0 {
			getPostById(fPostOP, fPostId, *mdcov)
		} else {
			fmt.Println("-i requires specifing an OP post with --op!")
		}
	} else if fThreadId != 0 {
		getPage(request, *mdcov)

		// fetch latest threads
	} else {
		getPage(request, *mdcov)
	}

	// list the threads of the specified page number
	// fmt.Println(request[fPageNum].Threads)

}