gonk.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "log"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "os/exec"
  11. "regexp"
  12. "sort"
  13. "strings"
  14. "golang.org/x/net/html"
  15. "gitea.com/lunny/html2md"
  16. )
  17. type Colour struct {
  18. Reset string
  19. Red string
  20. Green string
  21. Yellow string
  22. Blue string
  23. Purple string
  24. Cyan string
  25. Gray string
  26. White string
  27. }
  28. type Honk struct {
  29. ID int
  30. What string
  31. Handle string
  32. Oondle string
  33. XID string
  34. Date string
  35. Noise string
  36. Donks []struct {
  37. URL string
  38. }
  39. }
  40. type HonkSet struct {
  41. Honks []Honk
  42. }
  43. func isPgDn(b []byte) bool {
  44. return len(b) == 4 && b[0] == 27 && b[1] == 91 && b[2] == 54 && b[3] == 126
  45. }
  46. func dataHome() string {
  47. xdgDataHome, _ := os.LookupEnv("XDG_DATA_HOME")
  48. if xdgDataHome == "" {
  49. user, err := os.UserHomeDir()
  50. if err != nil {
  51. panic(err)
  52. }
  53. xdgDataHome = user + "/.local/share"
  54. }
  55. return xdgDataHome + "/gonk"
  56. }
  57. func configHome() string {
  58. xdgConfigHome, _ := os.LookupEnv("XDG_CONFIG_HOME")
  59. if xdgConfigHome == "" {
  60. user, err := os.UserHomeDir()
  61. if err != nil {
  62. panic(err)
  63. }
  64. xdgConfigHome = user + "/.config"
  65. }
  66. return xdgConfigHome + "/gonk"
  67. }
  68. func setUpColour(colourful bool) Colour {
  69. if colourful {
  70. return Colour{
  71. Reset: "\033[0m",
  72. Red: "\033[31m",
  73. Green: "\033[32m",
  74. Yellow: "\033[33m",
  75. Blue: "\033[34m",
  76. Purple: "\033[35m",
  77. Cyan: "\033[36m",
  78. Gray: "\033[37m",
  79. White: "\033[97m",
  80. }
  81. } else {
  82. return Colour{}
  83. }
  84. }
  85. func gettoken(server, username, password string) string {
  86. form := make(url.Values)
  87. form.Add("username", username)
  88. form.Add("password", password)
  89. form.Add("gettoken", "1")
  90. loginurl := fmt.Sprintf("https://%s/dologin", server)
  91. req, err := http.NewRequest("POST", loginurl, strings.NewReader(form.Encode()))
  92. if err != nil {
  93. log.Fatal(err)
  94. }
  95. req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  96. resp, err := http.DefaultClient.Do(req)
  97. if err != nil {
  98. log.Fatal(err)
  99. }
  100. defer resp.Body.Close()
  101. answer, err := ioutil.ReadAll(resp.Body)
  102. if err != nil {
  103. log.Fatal(err)
  104. }
  105. if resp.StatusCode != 200 {
  106. log.Fatalf("status: %d: %s", resp.StatusCode, answer)
  107. }
  108. return string(answer)
  109. }
  110. func logout(server, token string) {
  111. apiurl := fmt.Sprintf("https://%s/logout", server)
  112. req, err := http.NewRequest("GET", apiurl, nil)
  113. if err != nil {
  114. log.Fatal(err)
  115. }
  116. req.Header.Add("Authorization", "Bearer "+token)
  117. resp, err := http.DefaultClient.Do(req)
  118. if err != nil {
  119. log.Fatal(err)
  120. }
  121. defer resp.Body.Close()
  122. if resp.StatusCode != 200 {
  123. answer, _ := ioutil.ReadAll(resp.Body)
  124. log.Fatalf("status: %d: %s", resp.StatusCode, answer)
  125. }
  126. }
  127. func gethonks(server, token string, after int) HonkSet {
  128. form := make(url.Values)
  129. form.Add("action", "gethonks")
  130. form.Add("page", "home")
  131. form.Add("after", fmt.Sprintf("%d", after))
  132. apiurl := fmt.Sprintf("https://%s/api?%s", server, form.Encode())
  133. req, err := http.NewRequest("GET", apiurl, nil)
  134. if err != nil {
  135. log.Fatal(err)
  136. }
  137. req.Header.Add("Authorization", "Bearer "+token)
  138. resp, err := http.DefaultClient.Do(req)
  139. if err != nil {
  140. log.Fatal(err)
  141. }
  142. defer resp.Body.Close()
  143. if resp.StatusCode != 200 {
  144. answer, _ := ioutil.ReadAll(resp.Body)
  145. log.Fatalf("status: %d: %s", resp.StatusCode, answer)
  146. }
  147. var honks HonkSet
  148. d := json.NewDecoder(resp.Body)
  149. err = d.Decode(&honks)
  150. if err != nil {
  151. log.Fatal(err)
  152. }
  153. return honks
  154. }
  155. func readkey() []byte {
  156. exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
  157. exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
  158. defer exec.Command("stty", "-F", "/dev/tty", "echo").Run()
  159. var b []byte = make([]byte, 4)
  160. os.Stdin.Read(b)
  161. return b
  162. }
  163. func showHonks(honks HonkSet, after int, colour Colour) (int, bool) {
  164. broken := false
  165. honksNumber := len(honks.Honks)
  166. fmt.Println(colour.Cyan + "## o to open in browser, n/Space/PgDn for next, d to show donks, q to exit " + colour.Reset)
  167. for i, honk := range honks.Honks {
  168. md := html2md.Convert(honk.Noise)
  169. md = html.UnescapeString(md)
  170. md = changeA(md, colour)
  171. head := fmt.Sprintf(colour.White+"%d/%d\n%s %s %s (%s) \n%s\n"+colour.Reset, i+1, honksNumber, honk.Handle, honk.What, honk.Oondle, honk.Date, honk.XID)
  172. head = strings.ReplaceAll(head, " ", " ")
  173. fmt.Println(head)
  174. fmt.Printf("%s\n\n", md)
  175. if len(honk.Donks) > 0 {
  176. fmt.Println(colour.White + "## has donks" + colour.Reset)
  177. }
  178. for {
  179. key := readkey()
  180. if rune(key[0]) == 'n' || rune(key[0]) == ' ' || isPgDn(key) {
  181. after = honk.ID
  182. fmt.Printf("\n\n")
  183. break
  184. }
  185. if rune(key[0]) == 'o' {
  186. exec.Command("xdg-open", honk.XID).Run()
  187. continue
  188. }
  189. if rune(key[0]) == 'd' {
  190. for i, donk := range honk.Donks {
  191. fmt.Printf(colour.White+"Donk #%d: %s\n"+colour.Reset, i, donk.URL)
  192. }
  193. }
  194. if rune(key[0]) == 'q' {
  195. broken = true
  196. return after, broken
  197. }
  198. }
  199. }
  200. return after, broken
  201. }
  202. func changeA(noise string, colour Colour) string {
  203. reg := regexp.MustCompile("\\[([^]]+)\\]\\(([^)]+)\\)")
  204. indexes := reg.FindAllStringSubmatchIndex(noise, -1)
  205. last := 0
  206. renoise := ""
  207. for _, index := range indexes {
  208. s, t, s1, t1, s2, t2 := index[0], index[1], index[2], index[3], index[4], index[5]
  209. bf := noise[last:s]
  210. label := noise[s1:t1]
  211. href := noise[s2:t2]
  212. last = t
  213. if label == href {
  214. renoise = renoise + bf + colour.Blue + href + colour.Reset
  215. } else if label[0] == '#' {
  216. renoise = renoise + bf + colour.Green + label + colour.Reset
  217. } else if label[0] == '@' {
  218. instance, err := url.Parse(href)
  219. if err != nil {
  220. renoise = renoise + bf + "[" + colour.Green + label + colour.Reset + "](" + colour.Blue + href + colour.Reset + ")"
  221. } else {
  222. renoise = renoise + bf + colour.Green + label + "@" + instance.Host + colour.Reset
  223. }
  224. } else {
  225. renoise = renoise + bf + "[" + colour.Green + label + colour.Reset + "](" + colour.Blue + href + colour.Reset + ")"
  226. }
  227. }
  228. renoise = renoise + noise[last:]
  229. return renoise
  230. }
  231. func main() {
  232. server := ""
  233. username := ""
  234. password := ""
  235. after := 0
  236. broken := false
  237. colourful := true
  238. if len(os.Args) > 1 && os.Args[1] == "-t" {
  239. colourful = false
  240. }
  241. colour := setUpColour(colourful)
  242. err := os.MkdirAll(dataHome(), 0755)
  243. if err != nil {
  244. fmt.Println("[ERR] while making home directory: %v", err)
  245. os.Exit(1)
  246. }
  247. data, err := ioutil.ReadFile(configHome() + "/account")
  248. if err != nil {
  249. fmt.Printf("[ERR] cannot open account file (%s): %v\n", configHome()+"/account", err)
  250. os.Exit(1)
  251. }
  252. for _, line := range strings.Split(string(data), "\n") {
  253. if !strings.Contains(line, "=") {
  254. continue
  255. }
  256. kv := strings.Split(line, "=")
  257. k := strings.Trim(kv[0], " ")
  258. v := strings.Trim(kv[1], " ")
  259. switch k {
  260. case "username":
  261. username = v
  262. case "password":
  263. password = v
  264. case "server":
  265. server = v
  266. }
  267. }
  268. if server == "" || username == "" || password == "" {
  269. log.Println("[ERR] file 'account' with username, password, and server needed")
  270. os.Exit(1)
  271. }
  272. token := gettoken(server, username, password)
  273. data, err = ioutil.ReadFile(dataHome() + "/after")
  274. if err != nil {
  275. log.Printf("[WARN] file reading error: %s\nShowing all honks", err)
  276. }
  277. fmt.Sscanf(string(data), "%d", &after)
  278. honks := gethonks(server, token, after)
  279. sort.Slice(honks.Honks, func(i, j int) bool {
  280. return honks.Honks[i].ID < honks.Honks[j].ID
  281. })
  282. after, broken = showHonks(honks, after, colour)
  283. if !broken {
  284. fmt.Println(colour.Cyan + "## No more honks" + colour.Reset)
  285. }
  286. ioutil.WriteFile(dataHome()+"/after", []byte(fmt.Sprintf("%d", after)), 0644)
  287. logout(server, token)
  288. }