honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "bytes"
20 "crypto/rand"
21 "crypto/rsa"
22 "database/sql"
23 "fmt"
24 "html"
25 "html/template"
26 "image"
27 _ "image/gif"
28 _ "image/jpeg"
29 _ "image/png"
30 "io"
31 "log"
32 "net/http"
33 "os"
34 "regexp"
35 "sort"
36 "strings"
37 "sync"
38 "time"
39
40 "github.com/gorilla/mux"
41)
42
43type UserInfo struct {
44 UserID int64
45 Username string
46}
47
48type WhatAbout struct {
49 ID int64
50 Name string
51 Display string
52 About string
53 Key string
54 URL string
55}
56
57var serverName string
58var iconName = "icon.png"
59
60var readviews *Template
61
62func ziggy(user *WhatAbout) (keyname string, key *rsa.PrivateKey) {
63 db := opendatabase()
64 row := db.QueryRow("select seckey from users where userid = ?", user.ID)
65 var data string
66 row.Scan(&data)
67 var err error
68 key, _, err = pez(data)
69 if err != nil {
70 log.Printf("error loading %s seckey: %s", user.Name, err)
71 }
72 keyname = user.URL + "#key"
73 return
74}
75
76func zaggy(keyname string) (key *rsa.PublicKey) {
77 db := opendatabase()
78 row := db.QueryRow("select pubkey from honkers where flavor = 'key' and xid = ?", keyname)
79 var data string
80 err := row.Scan(&data)
81 savekey := false
82 if err != nil {
83 savekey = true
84 j, err := GetJunk(keyname)
85 if err != nil {
86 log.Printf("error getting %s pubkey: %s", keyname, err)
87 return
88 }
89 var ok bool
90 data, ok = jsonfindstring(j, []string{"publicKey", "publicKeyPem"})
91 if !ok {
92 log.Printf("error getting %s pubkey", keyname)
93 return
94 }
95 _, ok = jsonfindstring(j, []string{"publicKey", "owner"})
96 if !ok {
97 log.Printf("error getting %s pubkey owner", keyname)
98 return
99 }
100 }
101 _, key, err = pez(data)
102 if err != nil {
103 log.Printf("error getting %s pubkey: %s", keyname, err)
104 return
105 }
106 if savekey {
107 db.Exec("insert into honkers (name, xid, flavor, pubkey) values (?, ?, ?, ?)",
108 "", keyname, "key", data)
109 }
110 return
111}
112
113func keymatch(keyname string, actor string) bool {
114 return strings.HasPrefix(keyname, actor)
115}
116
117func getInfo(r *http.Request) map[string]interface{} {
118 templinfo := make(map[string]interface{})
119 templinfo["StyleParam"] = getstyleparam("views/style.css")
120 templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
121 templinfo["ServerName"] = serverName
122 templinfo["IconName"] = iconName
123 templinfo["UserInfo"] = GetUserInfo(r)
124 templinfo["LogoutCSRF"] = GetCSRF("logout", r)
125 return templinfo
126}
127
128var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
129
130func honkerhandle(h string) string {
131 m := re_unurl.FindStringSubmatch(h)
132 if len(m) > 2 {
133 return fmt.Sprintf("%s@%s", m[2], m[1])
134 }
135 return h
136}
137
138func reverbolate(honks []*Honk) {
139 for _, h := range honks {
140 h.What += "ed"
141 if h.Honker == "" {
142 h.Honker = "https://" + serverName + "/u/" + h.Username
143 if strings.IndexByte(h.XID, '/') == -1 {
144 h.URL = h.Honker + "/h/" + h.XID
145 } else {
146 h.URL = h.XID
147 }
148 } else {
149 idx := strings.LastIndexByte(h.Honker, '/')
150 if idx != -1 {
151 h.Username = honkerhandle(h.Honker)
152 } else {
153 h.Username = h.Honker
154 }
155 if h.URL == "" {
156 h.URL = h.XID
157 }
158 }
159 zap := make(map[*Donk]bool)
160 h.HTML = cleanstring(h.Noise)
161 emuxifier := func(e string) string {
162 for _, d := range h.Donks {
163 if d.Name == e {
164 zap[d] = true
165 return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
166 }
167 }
168 return e
169 }
170 h.HTML = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTML), emuxifier))
171 for i := 0; i < len(h.Donks); i++ {
172 if zap[h.Donks[i]] {
173 h.Donks = append(h.Donks[0:i], h.Donks[i+1:]...)
174 }
175 }
176 }
177}
178
179func homepage(w http.ResponseWriter, r *http.Request) {
180 templinfo := getInfo(r)
181 honks := gethonks("")
182 u := GetUserInfo(r)
183 if u != nil {
184 morehonks := gethonksforuser(u.UserID)
185 honks = append(honks, morehonks...)
186 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
187 }
188 sort.Slice(honks, func(i, j int) bool {
189 return honks[i].Date.After(honks[j].Date)
190 })
191 reverbolate(honks)
192
193 var modtime time.Time
194 if len(honks) > 0 {
195 modtime = honks[0].Date
196 }
197 debug := false
198 getconfig("debug", &debug)
199 imh := r.Header.Get("If-Modified-Since")
200 if !debug && imh != "" && !modtime.IsZero() {
201 ifmod, err := time.Parse(http.TimeFormat, imh)
202 if err == nil && !modtime.After(ifmod) {
203 w.WriteHeader(http.StatusNotModified)
204 return
205 }
206 }
207
208 msg := "Things happen."
209 getconfig("servermsg", &msg)
210 templinfo["Honks"] = honks
211 templinfo["ShowRSS"] = true
212 templinfo["ServerMessage"] = msg
213 w.Header().Set("Cache-Control", "max-age=0")
214 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
215 err := readviews.ExecuteTemplate(w, "homepage.html", templinfo)
216 if err != nil {
217 log.Print(err)
218 }
219}
220
221func showrss(w http.ResponseWriter, r *http.Request) {
222 name := mux.Vars(r)["name"]
223
224 honks := gethonks(name)
225 sort.Slice(honks, func(i, j int) bool {
226 return honks[i].Date.After(honks[j].Date)
227 })
228 reverbolate(honks)
229
230 home := fmt.Sprintf("https://%s/", serverName)
231 base := home
232 if name != "" {
233 home += "u/" + name
234 name += " "
235 }
236 feed := RssFeed{
237 Title: name + "honk",
238 Link: home,
239 Description: name + "honk rss",
240 FeedImage: &RssFeedImage{
241 URL: base + "icon.png",
242 Title: name + "honk rss",
243 Link: home,
244 },
245 }
246 var modtime time.Time
247 past := time.Now().UTC().Add(-3 * 24 * time.Hour)
248 for _, honk := range honks {
249 if honk.Date.Before(past) {
250 break
251 }
252 if honk.URL[0] == '/' {
253 honk.URL = "https://" + serverName + honk.URL
254 }
255 feed.Items = append(feed.Items, &RssItem{
256 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
257 Description: RssCData{string(honk.HTML)},
258 Link: honk.URL,
259 PubDate: honk.Date.Format(time.RFC1123),
260 })
261 if honk.Date.After(modtime) {
262 modtime = honk.Date
263 }
264 }
265 w.Header().Set("Cache-Control", "max-age=300")
266 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
267
268 err := feed.Write(w)
269 if err != nil {
270 log.Printf("error writing rss: %s", err)
271 }
272}
273
274func butwhatabout(name string) (*WhatAbout, error) {
275 row := stmtWhatAbout.QueryRow(name)
276 var user WhatAbout
277 err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
278 user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
279 return &user, err
280}
281
282func crappola(j map[string]interface{}) bool {
283 t, _ := jsongetstring(j, "type")
284 a, _ := jsongetstring(j, "actor")
285 o, _ := jsongetstring(j, "object")
286 if t == "Delete" && a == o {
287 log.Printf("crappola from %s", a)
288 return true
289 }
290 return false
291}
292
293func ping(user *WhatAbout, who string) {
294 inbox, _, err := getboxes(who)
295 if err != nil {
296 log.Printf("no inbox for ping: %s", err)
297 return
298 }
299 j := NewJunk()
300 j["@context"] = itiswhatitis
301 j["type"] = "Ping"
302 j["id"] = user.URL + "/ping/" + xfiltrate()
303 j["actor"] = user.URL
304 j["to"] = who
305 keyname, key := ziggy(user)
306 err = PostJunk(keyname, key, inbox, j)
307 if err != nil {
308 log.Printf("can't send ping: %s", err)
309 return
310 }
311 log.Printf("sent ping to %s: %s", who, j["id"])
312}
313
314func pong(user *WhatAbout, who string, obj string) {
315 inbox, _, err := getboxes(who)
316 if err != nil {
317 log.Printf("no inbox for pong %s : %s", who, err)
318 return
319 }
320 j := NewJunk()
321 j["@context"] = itiswhatitis
322 j["type"] = "Pong"
323 j["id"] = user.URL + "/pong/" + xfiltrate()
324 j["actor"] = user.URL
325 j["to"] = who
326 j["object"] = obj
327 keyname, key := ziggy(user)
328 err = PostJunk(keyname, key, inbox, j)
329 if err != nil {
330 log.Printf("can't send pong: %s", err)
331 return
332 }
333}
334
335func inbox(w http.ResponseWriter, r *http.Request) {
336 name := mux.Vars(r)["name"]
337 user, err := butwhatabout(name)
338 if err != nil {
339 http.NotFound(w, r)
340 return
341 }
342 var buf bytes.Buffer
343 io.Copy(&buf, r.Body)
344 payload := buf.Bytes()
345 j, err := ReadJunk(bytes.NewReader(payload))
346 if err != nil {
347 log.Printf("bad payload: %s", err)
348 io.WriteString(os.Stdout, "bad payload\n")
349 os.Stdout.Write(payload)
350 io.WriteString(os.Stdout, "\n")
351 return
352 }
353 if crappola(j) {
354 return
355 }
356 keyname, err := zag(r, payload)
357 if err != nil {
358 log.Printf("inbox message failed signature: %s", err)
359 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
360 io.WriteString(fd, "bad signature:\n")
361 WriteJunk(fd, j)
362 io.WriteString(fd, "\n")
363 fd.Close()
364 return
365 }
366 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
367 WriteJunk(fd, j)
368 io.WriteString(fd, "\n")
369 fd.Close()
370 who, _ := jsongetstring(j, "actor")
371 if !keymatch(keyname, who) {
372 log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
373 return
374 }
375 what, _ := jsongetstring(j, "type")
376 switch what {
377 case "Ping":
378 obj, _ := jsongetstring(j, "id")
379 log.Printf("ping from %s: %s", who, obj)
380 pong(user, who, obj)
381 case "Pong":
382 obj, _ := jsongetstring(j, "object")
383 log.Printf("pong from %s: %s", who, obj)
384 case "Follow":
385 log.Printf("updating honker follow: %s", who)
386 rubadubdub(user, j)
387 case "Accept":
388 db := opendatabase()
389 log.Printf("updating honker accept: %s", who)
390 db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
391 case "Undo":
392 obj, ok := jsongetmap(j, "object")
393 if !ok {
394 log.Printf("unknown undo no object")
395 } else {
396 what, _ := jsongetstring(obj, "type")
397 if what != "Follow" {
398 log.Printf("unknown undo: %s", what)
399 } else {
400 log.Printf("updating honker undo: %s", who)
401 db := opendatabase()
402 db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
403 }
404 }
405 default:
406 xonk := xonkxonk(j)
407 if xonk != nil && needxonk(user.ID, xonk) {
408 xonk.UserID = user.ID
409 savexonk(xonk)
410 }
411 }
412}
413
414func outbox(w http.ResponseWriter, r *http.Request) {
415 name := mux.Vars(r)["name"]
416 user, err := butwhatabout(name)
417 if err != nil {
418 http.NotFound(w, r)
419 return
420 }
421 honks := gethonks(name)
422
423 var jonks []map[string]interface{}
424 for _, h := range honks {
425 j, _ := jonkjonk(user, h)
426 jonks = append(jonks, j)
427 }
428
429 j := NewJunk()
430 j["@context"] = itiswhatitis
431 j["id"] = user.URL + "/outbox"
432 j["type"] = "OrderedCollection"
433 j["totalItems"] = len(jonks)
434 j["orderedItems"] = jonks
435
436 w.Header().Set("Content-Type", theonetruename)
437 WriteJunk(w, j)
438}
439
440func viewuser(w http.ResponseWriter, r *http.Request) {
441 name := mux.Vars(r)["name"]
442 user, err := butwhatabout(name)
443 if err != nil {
444 http.NotFound(w, r)
445 return
446 }
447 if friendorfoe(r.Header.Get("Accept")) {
448 j := asjonker(user)
449 w.Header().Set("Content-Type", theonetruename)
450 WriteJunk(w, j)
451 return
452 }
453 honks := gethonks(name)
454 u := GetUserInfo(r)
455 honkpage(w, r, u, user, honks)
456}
457
458func viewhonker(w http.ResponseWriter, r *http.Request) {
459 name := mux.Vars(r)["name"]
460 u := GetUserInfo(r)
461 honks := gethonksbyhonker(u.UserID, name)
462 honkpage(w, r, nil, nil, honks)
463}
464
465func fingerlicker(w http.ResponseWriter, r *http.Request) {
466 orig := r.FormValue("resource")
467
468 log.Printf("finger lick: %s", orig)
469
470 if strings.HasPrefix(orig, "acct:") {
471 orig = orig[5:]
472 }
473
474 name := orig
475 idx := strings.LastIndexByte(name, '/')
476 if idx != -1 {
477 name = name[idx+1:]
478 if "https://"+serverName+"/u/"+name != orig {
479 log.Printf("foreign request rejected")
480 name = ""
481 }
482 } else {
483 idx = strings.IndexByte(name, '@')
484 if idx != -1 {
485 name = name[:idx]
486 if name+"@"+serverName != orig {
487 log.Printf("foreign request rejected")
488 name = ""
489 }
490 }
491 }
492 user, err := butwhatabout(name)
493 if err != nil {
494 http.NotFound(w, r)
495 return
496 }
497
498 j := NewJunk()
499 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
500 j["aliases"] = []string{user.URL}
501 var links []map[string]interface{}
502 l := NewJunk()
503 l["rel"] = "self"
504 l["type"] = `application/activity+json`
505 l["href"] = user.URL
506 links = append(links, l)
507 j["links"] = links
508
509 w.Header().Set("Content-Type", "application/jrd+json")
510 WriteJunk(w, j)
511}
512
513func viewhonk(w http.ResponseWriter, r *http.Request) {
514 name := mux.Vars(r)["name"]
515 xid := mux.Vars(r)["xid"]
516 user, err := butwhatabout(name)
517 if err != nil {
518 http.NotFound(w, r)
519 return
520 }
521 h := getxonk(name, xid)
522 if h == nil {
523 http.NotFound(w, r)
524 return
525 }
526 if friendorfoe(r.Header.Get("Accept")) {
527 _, j := jonkjonk(user, h)
528 j["@context"] = itiswhatitis
529 w.Header().Set("Content-Type", theonetruename)
530 WriteJunk(w, j)
531 return
532 }
533 honkpage(w, r, nil, nil, []*Honk{h})
534}
535
536func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk) {
537 reverbolate(honks)
538 templinfo := getInfo(r)
539 if u != nil && u.Username == user.Name {
540 templinfo["UserCSRF"] = GetCSRF("saveuser", r)
541 templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
542 }
543 if user != nil {
544 templinfo["Name"] = user.Name
545 whatabout := user.About
546 templinfo["RawWhatAbout"] = whatabout
547 whatabout = obfusbreak(whatabout)
548 templinfo["WhatAbout"] = cleanstring(whatabout)
549 }
550 templinfo["Honks"] = honks
551 err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
552 if err != nil {
553 log.Print(err)
554 }
555}
556
557func saveuser(w http.ResponseWriter, r *http.Request) {
558 whatabout := r.FormValue("whatabout")
559 u := GetUserInfo(r)
560 db := opendatabase()
561 _, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
562 if err != nil {
563 log.Printf("error bouting what: %s", err)
564 }
565
566 http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
567}
568
569type Donk struct {
570 FileID int64
571 XID string
572 Name string
573 URL string
574 Media string
575 Content []byte
576}
577
578type Honk struct {
579 ID int64
580 UserID int64
581 Username string
582 What string
583 Honker string
584 XID string
585 RID string
586 Date time.Time
587 URL string
588 Noise string
589 Audience []string
590 HTML template.HTML
591 Donks []*Donk
592}
593
594type Honker struct {
595 ID int64
596 UserID int64
597 Name string
598 XID string
599 Flavor string
600}
601
602func gethonkers(userid int64) []*Honker {
603 rows, err := stmtHonkers.Query(userid)
604 if err != nil {
605 log.Printf("error querying honkers: %s", err)
606 return nil
607 }
608 defer rows.Close()
609 var honkers []*Honker
610 for rows.Next() {
611 var f Honker
612 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
613 if err != nil {
614 log.Printf("error scanning honker: %s", err)
615 return nil
616 }
617 honkers = append(honkers, &f)
618 }
619 return honkers
620}
621
622func getdubs(userid int64) []*Honker {
623 rows, err := stmtDubbers.Query(userid)
624 if err != nil {
625 log.Printf("error querying dubs: %s", err)
626 return nil
627 }
628 defer rows.Close()
629 var honkers []*Honker
630 for rows.Next() {
631 var f Honker
632 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
633 if err != nil {
634 log.Printf("error scanning honker: %s", err)
635 return nil
636 }
637 honkers = append(honkers, &f)
638 }
639 return honkers
640}
641
642func gethonk(honkid int64) *Honk {
643 var h Honk
644 var dt, aud string
645 row := stmtOneHonk.QueryRow(honkid)
646 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
647 &dt, &h.URL, &aud, &h.Noise)
648 if err != nil {
649 log.Printf("error scanning honk: %s", err)
650 return nil
651 }
652 h.Date, _ = time.Parse(dbtimeformat, dt)
653 h.Audience = strings.Split(aud, " ")
654 return &h
655}
656
657func getxonk(name, xid string) *Honk {
658 var h Honk
659 var dt, aud string
660 row := stmtOneXonk.QueryRow(xid)
661 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
662 &dt, &h.URL, &aud, &h.Noise)
663 if err != nil {
664 log.Printf("error scanning xonk: %s", err)
665 return nil
666 }
667 if name != "" && h.Username != name {
668 log.Printf("user xonk mismatch")
669 return nil
670 }
671 h.Date, _ = time.Parse(dbtimeformat, dt)
672 h.Audience = strings.Split(aud, " ")
673 donksforhonks([]*Honk{&h})
674 return &h
675}
676
677func gethonks(username string) []*Honk {
678 return getsomehonks(username, 0, "")
679}
680
681func gethonksforuser(userid int64) []*Honk {
682 return getsomehonks("", userid, "")
683}
684func gethonksbyhonker(userid int64, honker string) []*Honk {
685 return getsomehonks("", userid, honker)
686}
687
688func getsomehonks(username string, userid int64, honkername string) []*Honk {
689 var rows *sql.Rows
690 var err error
691 if username != "" {
692 rows, err = stmtUserHonks.Query(username)
693 } else if honkername != "" {
694 rows, err = stmtHonksByHonker.Query(userid, honkername)
695 } else if userid > 0 {
696 rows, err = stmtHonksForUser.Query(userid)
697 } else {
698 rows, err = stmtHonks.Query()
699 }
700 if err != nil {
701 log.Printf("error querying honks: %s", err)
702 return nil
703 }
704 defer rows.Close()
705 var honks []*Honk
706 for rows.Next() {
707 var h Honk
708 var dt, aud string
709 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
710 &dt, &h.URL, &aud, &h.Noise)
711 if err != nil {
712 log.Printf("error scanning honks: %s", err)
713 return nil
714 }
715 h.Date, _ = time.Parse(dbtimeformat, dt)
716 h.Audience = strings.Split(aud, " ")
717 honks = append(honks, &h)
718 }
719 rows.Close()
720 donksforhonks(honks)
721 return honks
722}
723
724func donksforhonks(honks []*Honk) {
725 db := opendatabase()
726 var ids []string
727 for _, h := range honks {
728 ids = append(ids, fmt.Sprintf("%d", h.ID))
729 }
730 q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
731 rows, err := db.Query(q)
732 if err != nil {
733 log.Printf("error querying donks: %s", err)
734 return
735 }
736 defer rows.Close()
737 for rows.Next() {
738 var hid int64
739 var d Donk
740 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
741 if err != nil {
742 log.Printf("error scanning donk: %s", err)
743 continue
744 }
745 for _, h := range honks {
746 if h.ID == hid {
747 h.Donks = append(h.Donks, &d)
748 }
749 }
750 }
751}
752
753func xfiltrate() string {
754 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
755 db := opendatabase()
756 for {
757 var x int64
758 var b [16]byte
759 rand.Read(b[:])
760 for i, c := range b {
761 b[i] = letters[c&63]
762 }
763 s := string(b[:])
764 r := db.QueryRow("select honkid from honks where xid = ?", s)
765 err := r.Scan(&x)
766 if err == nil {
767 continue
768 }
769 if err != sql.ErrNoRows {
770 log.Printf("err picking xid: %s", err)
771 return ""
772 }
773 r = db.QueryRow("select fileid from files where name = ?", s)
774 err = r.Scan(&x)
775 if err == nil {
776 continue
777 }
778 if err != sql.ErrNoRows {
779 log.Printf("err picking xid: %s", err)
780 return ""
781 }
782 return s
783 }
784}
785
786type Mention struct {
787 who string
788 where string
789}
790
791var re_mentions = regexp.MustCompile(`@[[:alnum:]]+@[[:alnum:].]+`)
792
793func grapevine(s string) []string {
794 m := re_mentions.FindAllString(s, -1)
795 var mentions []string
796 for i := range m {
797 where := gofish(m[i])
798 if where != "" {
799 mentions = append(mentions, where)
800 }
801 }
802 return mentions
803}
804
805func bunchofgrapes(s string) []Mention {
806 m := re_mentions.FindAllString(s, -1)
807 var mentions []Mention
808 for i := range m {
809 where := gofish(m[i])
810 if where != "" {
811 mentions = append(mentions, Mention{who: m[i], where: where})
812 }
813 }
814 return mentions
815}
816
817type Emu struct {
818 ID string
819 Name string
820}
821
822var re_link = regexp.MustCompile(`https?://[^\s"]+[\w/)]`)
823var re_emus = regexp.MustCompile(`:[[:alnum:]_]+:`)
824
825func herdofemus(noise string) []Emu {
826 m := re_emus.FindAllString(noise, -1)
827 var emus []Emu
828 for _, e := range m {
829 fname := e[1 : len(e)-1]
830 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
831 emus = append(emus, Emu{ID: url, Name: e})
832 }
833 return emus
834}
835
836func obfusbreak(s string) string {
837 s = strings.TrimSpace(s)
838 s = strings.Replace(s, "\r", "", -1)
839 s = html.EscapeString(s)
840 linkfn := func(url string) string {
841 addparen := false
842 adddot := false
843 if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
844 url = url[:len(url)-1]
845 addparen = true
846 }
847 if strings.HasSuffix(url, ".") {
848 url = url[:len(url)-1]
849 adddot = true
850 }
851 url = fmt.Sprintf(`<a href="%s">%s</a>`, url, url)
852 if adddot {
853 url += "."
854 }
855 if addparen {
856 url += ")"
857 }
858 return url
859 }
860 s = re_link.ReplaceAllStringFunc(s, linkfn)
861
862 s = strings.Replace(s, "\n", "<br>", -1)
863 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
864 return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(gofish(m)),
865 html.EscapeString(m))
866 })
867 return s
868}
869
870func prepend(s string, x []string) []string {
871 return append([]string{s}, x...)
872}
873
874func savebonk(w http.ResponseWriter, r *http.Request) {
875 xid := r.FormValue("xid")
876
877 log.Printf("bonking %s", xid)
878
879 xonk := getxonk("", xid)
880 if xonk == nil {
881 return
882 }
883 if xonk.Honker == "" {
884 xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
885 }
886
887 userinfo := GetUserInfo(r)
888
889 dt := time.Now().UTC()
890 bonk := Honk{
891 UserID: userinfo.UserID,
892 Username: userinfo.Username,
893 Honker: xonk.Honker,
894 What: "bonk",
895 XID: xonk.XID,
896 Date: dt,
897 Noise: xonk.Noise,
898 Donks: xonk.Donks,
899 Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
900 }
901
902 aud := strings.Join(bonk.Audience, " ")
903 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
904 dt.Format(dbtimeformat), "", aud, bonk.Noise)
905 if err != nil {
906 log.Printf("error saving bonk: %s", err)
907 return
908 }
909 bonk.ID, _ = res.LastInsertId()
910 for _, d := range bonk.Donks {
911 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
912 if err != nil {
913 log.Printf("err saving donk: %s", err)
914 return
915 }
916 }
917
918 user, _ := butwhatabout(userinfo.Username)
919
920 go honkworldwide(user, &bonk)
921
922}
923
924func savehonk(w http.ResponseWriter, r *http.Request) {
925 rid := r.FormValue("rid")
926 noise := r.FormValue("noise")
927
928 userinfo := GetUserInfo(r)
929
930 dt := time.Now().UTC()
931 xid := xfiltrate()
932 if xid == "" {
933 return
934 }
935 what := "honk"
936 if rid != "" {
937 what = "tonk"
938 }
939 honk := Honk{
940 UserID: userinfo.UserID,
941 Username: userinfo.Username,
942 What: "honk",
943 XID: xid,
944 RID: rid,
945 Date: dt,
946 }
947 if noise[0] == '@' {
948 honk.Audience = append(grapevine(noise), thewholeworld)
949 } else {
950 honk.Audience = append([]string{thewholeworld}, grapevine(noise)...)
951 }
952 if rid != "" {
953 xonk := getxonk("", rid)
954 if xonk != nil {
955 honk.Audience = append(honk.Audience, xonk.Audience...)
956 } else {
957 xonkaud := whosthere(rid)
958 honk.Audience = append(honk.Audience, xonkaud...)
959 }
960 }
961 honk.Audience = oneofakind(honk.Audience)
962 noise = obfusbreak(noise)
963 honk.Noise = noise
964
965 file, _, err := r.FormFile("donk")
966 if err == nil {
967 var buf bytes.Buffer
968 io.Copy(&buf, file)
969 file.Close()
970 data := buf.Bytes()
971 img, format, err := image.Decode(&buf)
972 if err != nil {
973 log.Printf("bad image: %s", err)
974 return
975 }
976 data, format, err = vacuumwrap(img, format)
977 if err != nil {
978 log.Printf("can't vacuum image: %s", err)
979 return
980 }
981 name := xfiltrate()
982 media := "image/" + format
983 if format == "jpeg" {
984 format = "jpg"
985 }
986 name = name + "." + format
987 url := fmt.Sprintf("https://%s/d/%s", serverName, name)
988 res, err := stmtSaveFile.Exec(name, name, url, media, data)
989 if err != nil {
990 log.Printf("unable to save image: %s", err)
991 return
992 }
993 var d Donk
994 d.FileID, _ = res.LastInsertId()
995 d.XID = name
996 d.Name = name
997 d.Media = media
998 d.URL = url
999 honk.Donks = append(honk.Donks, &d)
1000 }
1001 herd := herdofemus(honk.Noise)
1002 for _, e := range herd {
1003 donk := savedonk(e.ID, e.Name, "image/png")
1004 if donk != nil {
1005 honk.Donks = append(honk.Donks, donk)
1006 }
1007 }
1008
1009 aud := strings.Join(honk.Audience, " ")
1010 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
1011 dt.Format(dbtimeformat), "", aud, noise)
1012 if err != nil {
1013 log.Printf("error saving honk: %s", err)
1014 return
1015 }
1016 honk.ID, _ = res.LastInsertId()
1017 for _, d := range honk.Donks {
1018 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1019 if err != nil {
1020 log.Printf("err saving donk: %s", err)
1021 return
1022 }
1023 }
1024
1025 user, _ := butwhatabout(userinfo.Username)
1026
1027 go honkworldwide(user, &honk)
1028
1029 http.Redirect(w, r, "/", http.StatusSeeOther)
1030}
1031
1032func showhonkers(w http.ResponseWriter, r *http.Request) {
1033 userinfo := GetUserInfo(r)
1034 templinfo := getInfo(r)
1035 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1036 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
1037 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
1038 if err != nil {
1039 log.Print(err)
1040 }
1041}
1042
1043var handfull = make(map[string]string)
1044var handlock sync.Mutex
1045
1046func gofish(name string) string {
1047 if name[0] == '@' {
1048 name = name[1:]
1049 }
1050 m := strings.Split(name, "@")
1051 if len(m) != 2 {
1052 log.Printf("bad far name: %s", name)
1053 return ""
1054 }
1055 handlock.Lock()
1056 defer handlock.Unlock()
1057 ref, ok := handfull[name]
1058 if ok {
1059 return ref
1060 }
1061 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1062 if err != nil {
1063 log.Printf("failed to get far name: %s", err)
1064 handfull[name] = ""
1065 return ""
1066 }
1067 links, _ := jsongetarray(j, "links")
1068 for _, l := range links {
1069 href, _ := jsongetstring(l, "href")
1070 rel, _ := jsongetstring(l, "rel")
1071 t, _ := jsongetstring(l, "type")
1072 if rel == "self" && friendorfoe(t) {
1073 handfull[name] = href
1074 return href
1075 }
1076 }
1077 handfull[name] = ""
1078 return ""
1079}
1080
1081func savehonker(w http.ResponseWriter, r *http.Request) {
1082 name := r.FormValue("name")
1083 url := r.FormValue("url")
1084 peep := r.FormValue("peep")
1085 flavor := "presub"
1086 if peep == "peep" {
1087 flavor = "peep"
1088 }
1089
1090 if url == "" {
1091 return
1092 }
1093 if url[0] == '@' {
1094 url = gofish(url)
1095 }
1096 if url == "" {
1097 return
1098 }
1099
1100 u := GetUserInfo(r)
1101 db := opendatabase()
1102 _, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
1103 u.UserID, name, url, flavor)
1104 if err != nil {
1105 log.Print(err)
1106 }
1107 if flavor == "presub" {
1108 user, _ := butwhatabout(u.Username)
1109 go subsub(user, url)
1110 }
1111 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1112}
1113
1114func avatate(w http.ResponseWriter, r *http.Request) {
1115 n := r.FormValue("a")
1116 a := avatar(n)
1117 w.Header().Set("Cache-Control", "max-age=432000")
1118 w.Write(a)
1119}
1120
1121func servecss(w http.ResponseWriter, r *http.Request) {
1122 w.Header().Set("Cache-Control", "max-age=7776000")
1123 http.ServeFile(w, r, "views"+r.URL.Path)
1124}
1125func servehtml(w http.ResponseWriter, r *http.Request) {
1126 templinfo := getInfo(r)
1127 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1128 if err != nil {
1129 log.Print(err)
1130 }
1131}
1132func serveemu(w http.ResponseWriter, r *http.Request) {
1133 xid := mux.Vars(r)["xid"]
1134 w.Header().Set("Cache-Control", "max-age=432000")
1135 http.ServeFile(w, r, "emus/"+xid)
1136}
1137
1138func servefile(w http.ResponseWriter, r *http.Request) {
1139 xid := mux.Vars(r)["xid"]
1140 row := stmtFileData.QueryRow(xid)
1141 var data []byte
1142 err := row.Scan(&data)
1143 if err != nil {
1144 log.Printf("error loading file: %s", err)
1145 http.NotFound(w, r)
1146 return
1147 }
1148 w.Header().Set("Cache-Control", "max-age=432000")
1149 w.Write(data)
1150}
1151
1152func serve() {
1153 db := opendatabase()
1154 LoginInit(db)
1155
1156 listener, err := openListener()
1157 if err != nil {
1158 log.Fatal(err)
1159 }
1160 debug := false
1161 getconfig("debug", &debug)
1162 readviews = ParseTemplates(debug,
1163 "views/homepage.html",
1164 "views/honkpage.html",
1165 "views/honkers.html",
1166 "views/honkform.html",
1167 "views/honk.html",
1168 "views/login.html",
1169 "views/header.html",
1170 )
1171 if !debug {
1172 s := "views/style.css"
1173 savedstyleparams[s] = getstyleparam(s)
1174 s = "views/local.css"
1175 savedstyleparams[s] = getstyleparam(s)
1176 }
1177
1178 mux := mux.NewRouter()
1179 mux.Use(LoginChecker)
1180
1181 posters := mux.Methods("POST").Subrouter()
1182 getters := mux.Methods("GET").Subrouter()
1183
1184 getters.HandleFunc("/", homepage)
1185 getters.HandleFunc("/rss", showrss)
1186 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1187 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1188 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1189 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1190 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1191 getters.HandleFunc("/a", avatate)
1192 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1193 getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1194 getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1195 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1196
1197 getters.HandleFunc("/style.css", servecss)
1198 getters.HandleFunc("/local.css", servecss)
1199 getters.HandleFunc("/login", servehtml)
1200 posters.HandleFunc("/dologin", dologin)
1201 getters.HandleFunc("/logout", dologout)
1202
1203 loggedin := mux.NewRoute().Subrouter()
1204 loggedin.Use(LoginRequired)
1205 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1206 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1207 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1208 loggedin.HandleFunc("/honkers", showhonkers)
1209 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1210
1211 err = http.Serve(listener, mux)
1212 if err != nil {
1213 log.Fatal(err)
1214 }
1215}
1216
1217var stmtHonkers, stmtDubbers, stmtOneHonk, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1218var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1219var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1220var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1221
1222func prepareStatements(db *sql.DB) {
1223 var err error
1224 stmtHonkers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1225 if err != nil {
1226 log.Fatal(err)
1227 }
1228 stmtDubbers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1229 if err != nil {
1230 log.Fatal(err)
1231 }
1232 stmtOneHonk, err = db.Prepare("select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honkid = ? limit 50")
1233 if err != nil {
1234 log.Fatal(err)
1235 }
1236 stmtOneXonk, err = db.Prepare("select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where xid = ?")
1237 if err != nil {
1238 log.Fatal(err)
1239 }
1240 stmtHonks, err = db.Prepare("select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honker = '' order by honkid desc limit 50")
1241 if err != nil {
1242 log.Fatal(err)
1243 }
1244 stmtUserHonks, err = db.Prepare("select honkid, honks.userid, username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honker = '' and username = ? order by honkid desc limit 50")
1245 if err != nil {
1246 log.Fatal(err)
1247 }
1248 stmtHonksForUser, err = db.Prepare("select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid where honks.userid = ? and honker <> '' and what <> 'zonk' order by honkid desc limit 150")
1249 if err != nil {
1250 log.Fatal(err)
1251 }
1252 stmtHonksByHonker, err = db.Prepare("select honkid, honks.userid, users.username, what, honker, honks.xid, rid, dt, url, audience, noise from honks join users on honks.userid = users.userid join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ? order by honkid desc limit 50")
1253 if err != nil {
1254 log.Fatal(err)
1255 }
1256 stmtSaveHonk, err = db.Prepare("insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
1257 if err != nil {
1258 log.Fatal(err)
1259 }
1260 stmtFileData, err = db.Prepare("select content from files where xid = ?")
1261 if err != nil {
1262 log.Fatal(err)
1263 }
1264 stmtFindXonk, err = db.Prepare("select honkid from honks where userid = ? and xid = ? and what = ?")
1265 if err != nil {
1266 log.Fatal(err)
1267 }
1268 stmtSaveDonk, err = db.Prepare("insert into donks (honkid, fileid) values (?, ?)")
1269 if err != nil {
1270 log.Fatal(err)
1271 }
1272 stmtDeleteHonk, err = db.Prepare("update honks set what = 'zonk' where xid = ? and honker = ?")
1273 if err != nil {
1274 log.Fatal(err)
1275 }
1276 stmtFindFile, err = db.Prepare("select fileid from files where url = ?")
1277 if err != nil {
1278 log.Fatal(err)
1279 }
1280 stmtSaveFile, err = db.Prepare("insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1281 if err != nil {
1282 log.Fatal(err)
1283 }
1284 stmtWhatAbout, err = db.Prepare("select userid, username, displayname, about, pubkey from users where username = ?")
1285 if err != nil {
1286 log.Fatal(err)
1287 }
1288 stmtSaveDub, err = db.Prepare("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1289 if err != nil {
1290 log.Fatal(err)
1291 }
1292}
1293
1294func ElaborateUnitTests() {
1295}
1296
1297func finishusersetup() error {
1298 db := opendatabase()
1299 k, err := rsa.GenerateKey(rand.Reader, 2048)
1300 if err != nil {
1301 return err
1302 }
1303 pubkey, err := zem(&k.PublicKey)
1304 if err != nil {
1305 return err
1306 }
1307 seckey, err := zem(k)
1308 if err != nil {
1309 return err
1310 }
1311 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1312 if err != nil {
1313 return err
1314 }
1315 return nil
1316}
1317
1318func main() {
1319 cmd := "run"
1320 if len(os.Args) > 1 {
1321 cmd = os.Args[1]
1322 }
1323 if cmd != "init" {
1324 db := opendatabase()
1325 prepareStatements(db)
1326 getconfig("servername", &serverName)
1327 }
1328 switch cmd {
1329 case "ping":
1330 if len(os.Args) < 4 {
1331 fmt.Printf("usage: honk ping from to\n")
1332 return
1333 }
1334 name := os.Args[2]
1335 targ := os.Args[3]
1336 user, err := butwhatabout(name)
1337 if err != nil {
1338 log.Printf("unknown user")
1339 return
1340 }
1341 ping(user, targ)
1342 case "peep":
1343 peeppeep()
1344 case "init":
1345 initdb()
1346 case "run":
1347 serve()
1348 case "test":
1349 ElaborateUnitTests()
1350 default:
1351 log.Fatal("unknown command")
1352 }
1353}