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