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