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