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 } else {
874 xonkaud := whosthere(rid)
875 honk.Audience = append(honk.Audience, xonkaud...)
876 }
877 }
878 honk.Audience = oneofakind(honk.Audience)
879 noise = obfusbreak(noise)
880 honk.Noise = noise
881
882 file, _, err := r.FormFile("donk")
883 if err == nil {
884 var buf bytes.Buffer
885 io.Copy(&buf, file)
886 file.Close()
887 data := buf.Bytes()
888 img, format, err := image.Decode(&buf)
889 if err != nil {
890 log.Printf("bad image: %s", err)
891 return
892 }
893 data, format, err = vacuumwrap(img, format)
894 if err != nil {
895 log.Printf("can't vacuum image: %s", err)
896 return
897 }
898 name := xfiltrate()
899 media := "image/" + format
900 if format == "jpeg" {
901 format = "jpg"
902 }
903 name = name + "." + format
904 url := fmt.Sprintf("https://%s/d/%s", serverName, name)
905 res, err := stmtSaveFile.Exec(name, name, url, media, data)
906 if err != nil {
907 log.Printf("unable to save image: %s", err)
908 return
909 }
910 var d Donk
911 d.FileID, _ = res.LastInsertId()
912 d.XID = name
913 d.Name = name
914 d.Media = media
915 d.URL = url
916 honk.Donks = append(honk.Donks, &d)
917 }
918
919 aud := strings.Join(honk.Audience, " ")
920 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
921 dt.Format(dbtimeformat), "", aud, noise)
922 if err != nil {
923 log.Printf("error saving honk: %s", err)
924 return
925 }
926 honk.ID, _ = res.LastInsertId()
927 for _, d := range honk.Donks {
928 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
929 if err != nil {
930 log.Printf("err saving donk: %s", err)
931 return
932 }
933 }
934
935 user, _ := butwhatabout(userinfo.Username)
936
937 go honkworldwide(user, &honk)
938
939 http.Redirect(w, r, "/", http.StatusSeeOther)
940}
941
942func showhonkers(w http.ResponseWriter, r *http.Request) {
943 userinfo := GetUserInfo(r)
944 templinfo := getInfo(r)
945 templinfo["Honkers"] = gethonkers(userinfo.UserID)
946 templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
947 err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
948 if err != nil {
949 log.Print(err)
950 }
951}
952
953var handfull = make(map[string]string)
954var handlock sync.Mutex
955
956func gofish(name string) string {
957 if name[0] == '@' {
958 name = name[1:]
959 }
960 m := strings.Split(name, "@")
961 if len(m) != 2 {
962 log.Printf("bad far name: %s", name)
963 return ""
964 }
965 handlock.Lock()
966 defer handlock.Unlock()
967 ref, ok := handfull[name]
968 if ok {
969 return ref
970 }
971 j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
972 if err != nil {
973 log.Printf("failed to get far name: %s", err)
974 handfull[name] = ""
975 return ""
976 }
977 links, _ := jsongetarray(j, "links")
978 for _, l := range links {
979 href, _ := jsongetstring(l, "href")
980 rel, _ := jsongetstring(l, "rel")
981 t, _ := jsongetstring(l, "type")
982 if rel == "self" && friendorfoe(t) {
983 handfull[name] = href
984 return href
985 }
986 }
987 handfull[name] = ""
988 return ""
989}
990
991func savehonker(w http.ResponseWriter, r *http.Request) {
992 name := r.FormValue("name")
993 url := r.FormValue("url")
994 peep := r.FormValue("peep")
995 flavor := "presub"
996 if peep == "peep" {
997 flavor = "peep"
998 }
999
1000 if url == "" {
1001 return
1002 }
1003 if url[0] == '@' {
1004 url = gofish(url)
1005 }
1006 if url == "" {
1007 return
1008 }
1009
1010 u := GetUserInfo(r)
1011 db := opendatabase()
1012 _, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
1013 u.UserID, name, url, flavor)
1014 if err != nil {
1015 log.Print(err)
1016 }
1017 if flavor == "presub" {
1018 user, _ := butwhatabout(u.Username)
1019 go subsub(user, url)
1020 }
1021 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1022}
1023
1024func avatate(w http.ResponseWriter, r *http.Request) {
1025 n := r.FormValue("a")
1026 a := avatar(n)
1027 w.Header().Set("Cache-Control", "max-age=76000")
1028 w.Write(a)
1029}
1030
1031func servecss(w http.ResponseWriter, r *http.Request) {
1032 w.Header().Set("Cache-Control", "max-age=7776000")
1033 http.ServeFile(w, r, "views"+r.URL.Path)
1034}
1035func servehtml(w http.ResponseWriter, r *http.Request) {
1036 templinfo := getInfo(r)
1037 err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1038 if err != nil {
1039 log.Print(err)
1040 }
1041}
1042
1043func servefile(w http.ResponseWriter, r *http.Request) {
1044 xid := mux.Vars(r)["xid"]
1045 row := stmtFileData.QueryRow(xid)
1046 var data []byte
1047 err := row.Scan(&data)
1048 if err != nil {
1049 log.Printf("error loading file: %s", err)
1050 http.NotFound(w, r)
1051 return
1052 }
1053 w.Header().Set("Cache-Control", "max-age=432000")
1054 w.Write(data)
1055}
1056
1057func serve() {
1058 db := opendatabase()
1059 LoginInit(db)
1060
1061 getconfig("servername", &serverName)
1062 listener, err := openListener()
1063 if err != nil {
1064 log.Fatal(err)
1065 }
1066 debug := false
1067 getconfig("debug", &debug)
1068 readviews = ParseTemplates(debug,
1069 "views/homepage.html",
1070 "views/honkpage.html",
1071 "views/honkers.html",
1072 "views/honkform.html",
1073 "views/honk.html",
1074 "views/login.html",
1075 "views/header.html",
1076 )
1077 if !debug {
1078 savedstyleparam = getstyleparam()
1079 }
1080
1081 mux := mux.NewRouter()
1082 mux.Use(LoginChecker)
1083
1084 posters := mux.Methods("POST").Subrouter()
1085 getters := mux.Methods("GET").Subrouter()
1086
1087 getters.HandleFunc("/", homepage)
1088 getters.HandleFunc("/rss", showrss)
1089 getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1090 getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1091 getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1092 posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1093 getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1094 getters.HandleFunc("/a", avatate)
1095 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1096 getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1097 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1098
1099 getters.HandleFunc("/style.css", servecss)
1100 getters.HandleFunc("/login", servehtml)
1101 posters.HandleFunc("/dologin", dologin)
1102 getters.HandleFunc("/logout", dologout)
1103
1104 loggedin := mux.NewRoute().Subrouter()
1105 loggedin.Use(LoginRequired)
1106 loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1107 loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1108 loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1109 loggedin.HandleFunc("/honkers", showhonkers)
1110 loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1111
1112 err = http.Serve(listener, mux)
1113 if err != nil {
1114 log.Fatal(err)
1115 }
1116}
1117
1118var stmtHonkers, stmtDubbers, stmtOneHonk, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1119var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1120var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1121var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1122
1123func prepareStatements(db *sql.DB) {
1124 var err error
1125 stmtHonkers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1126 if err != nil {
1127 log.Fatal(err)
1128 }
1129 stmtDubbers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1130 if err != nil {
1131 log.Fatal(err)
1132 }
1133 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")
1134 if err != nil {
1135 log.Fatal(err)
1136 }
1137 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 = ?")
1138 if err != nil {
1139 log.Fatal(err)
1140 }
1141 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")
1142 if err != nil {
1143 log.Fatal(err)
1144 }
1145 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")
1146 if err != nil {
1147 log.Fatal(err)
1148 }
1149 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")
1150 if err != nil {
1151 log.Fatal(err)
1152 }
1153 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")
1154 if err != nil {
1155 log.Fatal(err)
1156 }
1157 stmtSaveHonk, err = db.Prepare("insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
1158 if err != nil {
1159 log.Fatal(err)
1160 }
1161 stmtFileData, err = db.Prepare("select content from files where xid = ?")
1162 if err != nil {
1163 log.Fatal(err)
1164 }
1165 stmtFindXonk, err = db.Prepare("select honkid from honks where userid = ? and xid = ? and what = ?")
1166 if err != nil {
1167 log.Fatal(err)
1168 }
1169 stmtSaveDonk, err = db.Prepare("insert into donks (honkid, fileid) values (?, ?)")
1170 if err != nil {
1171 log.Fatal(err)
1172 }
1173 stmtDeleteHonk, err = db.Prepare("update honks set what = 'zonk' where xid = ? and honker = ?")
1174 if err != nil {
1175 log.Fatal(err)
1176 }
1177 stmtFindFile, err = db.Prepare("select fileid from files where url = ?")
1178 if err != nil {
1179 log.Fatal(err)
1180 }
1181 stmtSaveFile, err = db.Prepare("insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1182 if err != nil {
1183 log.Fatal(err)
1184 }
1185 stmtWhatAbout, err = db.Prepare("select userid, username, displayname, about, pubkey from users where username = ?")
1186 if err != nil {
1187 log.Fatal(err)
1188 }
1189 stmtSaveDub, err = db.Prepare("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1190 if err != nil {
1191 log.Fatal(err)
1192 }
1193}
1194
1195func ElaborateUnitTests() {
1196}
1197
1198func finishusersetup() error {
1199 db := opendatabase()
1200 k, err := rsa.GenerateKey(rand.Reader, 2048)
1201 if err != nil {
1202 return err
1203 }
1204 pubkey, err := zem(&k.PublicKey)
1205 if err != nil {
1206 return err
1207 }
1208 seckey, err := zem(k)
1209 if err != nil {
1210 return err
1211 }
1212 _, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1213 if err != nil {
1214 return err
1215 }
1216 return nil
1217}
1218
1219func main() {
1220 cmd := "run"
1221 if len(os.Args) > 1 {
1222 cmd = os.Args[1]
1223 }
1224 if cmd != "init" {
1225 db := opendatabase()
1226 prepareStatements(db)
1227 }
1228 switch cmd {
1229 case "peep":
1230 peeppeep()
1231 case "init":
1232 initdb()
1233 case "run":
1234 serve()
1235 case "test":
1236 ElaborateUnitTests()
1237 default:
1238 log.Fatal("unknown command")
1239 }
1240}