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