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