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