web.go (view raw)
1//
2// Copyright (c) 2019-2024 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 "context"
21 "crypto/sha512"
22 "database/sql"
23 "errors"
24 "fmt"
25 "html/template"
26 "io"
27 "log"
28 notrand "math/rand"
29 "mime/multipart"
30 "net"
31 "net/http"
32 "net/http/fcgi"
33 "net/url"
34 "os"
35 "os/signal"
36 "path"
37 "path/filepath"
38 "regexp"
39 "runtime/pprof"
40 "sort"
41 "strconv"
42 "strings"
43 "sync"
44 "syscall"
45 "time"
46 "unicode/utf8"
47
48 "github.com/gorilla/handlers"
49 "github.com/gorilla/mux"
50 "humungus.tedunangst.com/r/gonix"
51 "humungus.tedunangst.com/r/webs/gencache"
52 "humungus.tedunangst.com/r/webs/httpsig"
53 "humungus.tedunangst.com/r/webs/junk"
54 "humungus.tedunangst.com/r/webs/login"
55 "humungus.tedunangst.com/r/webs/rss"
56 "humungus.tedunangst.com/r/webs/templates"
57)
58
59var readviews *templates.Template
60
61var userSep = "u"
62var honkSep = "h"
63
64var develMode = false
65
66var allemus []Emu
67
68func getuserstyle(u *login.UserInfo) template.HTMLAttr {
69 if u == nil {
70 return ""
71 }
72 user, _ := butwhatabout(u.Username)
73 class := template.HTMLAttr("")
74 if user.Options.SkinnyCSS {
75 class += `class="skinny"`
76 }
77 return class
78}
79
80func getmaplink(u *login.UserInfo) string {
81 if u == nil {
82 return "osm"
83 }
84 user, _ := butwhatabout(u.Username)
85 ml := user.Options.MapLink
86 if ml == "" {
87 ml = "osm"
88 }
89 return ml
90}
91
92func getInfo(r *http.Request) map[string]interface{} {
93 templinfo := make(map[string]interface{})
94 templinfo["StyleParam"] = getassetparam(viewDir + "/views/style.css")
95 templinfo["LocalStyleParam"] = getassetparam(dataDir + "/views/local.css")
96 templinfo["JSParam"] = getassetparam(viewDir + "/views/honkpage.js")
97 templinfo["MiscJSParam"] = getassetparam(viewDir + "/views/misc.js")
98 templinfo["LocalJSParam"] = getassetparam(dataDir + "/views/local.js")
99 templinfo["ServerName"] = serverName
100 templinfo["IconName"] = iconName
101 templinfo["UserSep"] = userSep
102 if r == nil {
103 return templinfo
104 }
105 if u := login.GetUserInfo(r); u != nil {
106 templinfo["UserInfo"], _ = butwhatabout(u.Username)
107 templinfo["UserStyle"] = getuserstyle(u)
108 combos, _ := combocache.Get(UserID(u.UserID))
109 templinfo["Combos"] = combos
110 }
111 return templinfo
112}
113
114var oldnews = gencache.New(gencache.Options[string, []byte]{
115 Fill: func(url string) ([]byte, bool) {
116 templinfo := getInfo(nil)
117 var honks []*Honk
118 var userid UserID = -1
119
120 templinfo["ServerMessage"] = serverMsg
121 switch url {
122 case "/events":
123 honks = geteventhonks(userid)
124 templinfo["ServerMessage"] = "some recent and upcoming events"
125 default:
126 templinfo["ShowRSS"] = true
127 honks = getpublichonks()
128 }
129 reverbolate(userid, honks)
130 templinfo["Honks"] = honks
131 templinfo["MapLink"] = getmaplink(nil)
132 var buf bytes.Buffer
133 err := readviews.Execute(&buf, "honkpage.html", templinfo)
134 if err != nil {
135 elog.Print(err)
136 }
137 return buf.Bytes(), true
138
139 },
140 Duration: 1 * time.Minute,
141})
142
143func lonelypage(w http.ResponseWriter, r *http.Request) {
144 page, _ := oldnews.Get(r.URL.Path)
145 if !develMode {
146 w.Header().Set("Cache-Control", "max-age=60")
147 }
148 w.Write(page)
149}
150
151func homepage(w http.ResponseWriter, r *http.Request) {
152 u := login.GetUserInfo(r)
153 if u == nil {
154 lonelypage(w, r)
155 return
156 }
157 templinfo := getInfo(r)
158 var honks []*Honk
159 var userid UserID = -1
160
161 templinfo["ServerMessage"] = serverMsg
162 if u == nil || r.URL.Path == "/front" {
163 switch r.URL.Path {
164 case "/events":
165 honks = geteventhonks(userid)
166 templinfo["ServerMessage"] = "some recent and upcoming events"
167 default:
168 templinfo["ShowRSS"] = true
169 honks = getpublichonks()
170 }
171 } else {
172 userid = UserID(u.UserID)
173 switch r.URL.Path {
174 case "/atme":
175 templinfo["ServerMessage"] = "at me!"
176 templinfo["PageName"] = "atme"
177 honks = gethonksforme(userid, 0)
178 honks = osmosis(honks, userid, false)
179 menewnone(userid)
180 templinfo["UserInfo"], _ = butwhatabout(u.Username)
181 case "/longago":
182 templinfo["ServerMessage"] = "long ago and far away!"
183 templinfo["PageName"] = "longago"
184 honks = gethonksfromlongago(userid, 0)
185 honks = osmosis(honks, userid, false)
186 case "/events":
187 templinfo["ServerMessage"] = "some recent and upcoming events"
188 templinfo["PageName"] = "events"
189 honks = geteventhonks(userid)
190 honks = osmosis(honks, userid, true)
191 case "/first":
192 templinfo["PageName"] = "first"
193 honks = gethonksforuserfirstclass(userid, 0)
194 honks = osmosis(honks, userid, true)
195 case "/saved":
196 templinfo["ServerMessage"] = "saved honks"
197 templinfo["PageName"] = "saved"
198 honks = getsavedhonks(userid, 0)
199 default:
200 templinfo["PageName"] = "home"
201 honks = gethonksforuser(userid, 0)
202 honks = osmosis(honks, userid, true)
203 }
204 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
205 }
206
207 honkpage(w, u, honks, templinfo)
208}
209
210func showemus(w http.ResponseWriter, r *http.Request) {
211 templinfo := getInfo(r)
212 templinfo["Emus"] = allemus
213 err := readviews.Execute(w, "emus.html", templinfo)
214 if err != nil {
215 elog.Print(err)
216 }
217}
218
219func showfunzone(w http.ResponseWriter, r *http.Request) {
220 var emunames, memenames []string
221 emuext := make(map[string]string)
222 dir, err := os.Open(dataDir + "/emus")
223 if err == nil {
224 emunames, _ = dir.Readdirnames(0)
225 dir.Close()
226 }
227 for i, e := range emunames {
228 if len(e) > 4 {
229 emunames[i] = e[:len(e)-4]
230 emuext[emunames[i]] = e[len(e)-4:]
231 }
232 }
233 dir, err = os.Open(dataDir + "/memes")
234 if err == nil {
235 memenames, _ = dir.Readdirnames(0)
236 dir.Close()
237 }
238 sort.Strings(emunames)
239 sort.Strings(memenames)
240 templinfo := getInfo(r)
241 templinfo["Emus"] = emunames
242 templinfo["Emuext"] = emuext
243 templinfo["Memes"] = memenames
244 err = readviews.Execute(w, "funzone.html", templinfo)
245 if err != nil {
246 elog.Print(err)
247 }
248}
249
250func showrss(w http.ResponseWriter, r *http.Request) {
251 name := mux.Vars(r)["name"]
252
253 var honks []*Honk
254 if name != "" {
255 honks = gethonksbyuser(name, false, 0)
256 } else {
257 honks = getpublichonks()
258 }
259 reverbolate(-1, honks)
260
261 home := serverURL("/")
262 base := home
263 if name != "" {
264 home += "u/" + name
265 name += " "
266 }
267 feed := rss.Feed{
268 Title: name + "honk",
269 Link: home,
270 Description: name + "honk rss",
271 Image: &rss.Image{
272 URL: base + "icon.png",
273 Title: name + "honk rss",
274 Link: home,
275 },
276 }
277 var modtime time.Time
278 for _, honk := range honks {
279 if !firstclass(honk) {
280 continue
281 }
282 desc := string(honk.HTML)
283 if t := honk.Time; t != nil {
284 desc += fmt.Sprintf(`<p>Time: %s`, t.StartTime.Local().Format("03:04PM MST Mon Jan 02"))
285 if t.Duration != 0 {
286 desc += fmt.Sprintf(`<br>Duration: %s`, t.Duration)
287 }
288 }
289 if p := honk.Place; p != nil {
290 desc += string(templates.Sprintf(`<p>Location: <a href="%s">%s</a> %f %f`,
291 p.Url, p.Name, p.Latitude, p.Longitude))
292 }
293 for _, d := range honk.Donks {
294 desc += string(templates.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
295 d.URL, d.Desc))
296 if strings.HasPrefix(d.Media, "image") {
297 desc += string(templates.Sprintf(`<img src="%s">`, d.URL))
298 }
299 }
300
301 feed.Items = append(feed.Items, &rss.Item{
302 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
303 Description: rss.CData{Data: desc},
304 Link: honk.URL,
305 PubDate: honk.Date.Format(time.RFC1123),
306 Guid: &rss.Guid{IsPermaLink: true, Value: honk.URL},
307 })
308 if honk.Date.After(modtime) {
309 modtime = honk.Date
310 }
311 }
312 if !develMode {
313 w.Header().Set("Cache-Control", "max-age=300")
314 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
315 }
316
317 err := feed.Write(w)
318 if err != nil {
319 elog.Printf("error writing rss: %s", err)
320 }
321}
322
323func crappola(j junk.Junk) bool {
324 t := firstofmany(j, "type")
325 if t == "Delete" {
326 a, _ := j.GetString("actor")
327 o, _ := j.GetString("object")
328 if a == o {
329 dlog.Printf("crappola from %s", a)
330 return true
331 }
332 }
333 if t == "Announce" {
334 if obj, ok := j.GetMap("object"); ok {
335 j = obj
336 t = firstofmany(j, "type")
337 }
338 }
339 if t == "Undo" {
340 if obj, ok := j.GetMap("object"); ok {
341 j = obj
342 t = firstofmany(j, "type")
343 }
344 }
345 if t == "Like" || t == "Dislike" || t == "Listen" {
346 return true
347 }
348 if t == "EmojiReact" {
349 o, _ := j.GetString("object")
350 if originate(o) != serverName {
351 return true
352 }
353 }
354 return false
355}
356
357func ping(user *WhatAbout, who string) {
358 if targ := fullname(who, user.ID); targ != "" {
359 who = targ
360 }
361 if !strings.HasPrefix(who, "https://") {
362 who = gofish(who)
363 }
364 if who == "" {
365 ilog.Printf("nobody to ping!")
366 return
367 }
368 box, ok := boxofboxes.Get(who)
369 if !ok {
370 ilog.Printf("no inbox to ping %s", who)
371 return
372 }
373 ilog.Printf("sending ping to %s", box.In)
374 j := junk.New()
375 j["@context"] = itiswhatitis
376 j["type"] = "Ping"
377 j["id"] = user.URL + "/ping/" + xfiltrate()
378 j["actor"] = user.URL
379 j["to"] = who
380 ki := ziggy(user.ID)
381 if ki == nil {
382 return
383 }
384 err := PostJunk(ki.keyname, ki.seckey, box.In, j)
385 if err != nil {
386 elog.Printf("can't send ping: %s", err)
387 return
388 }
389 ilog.Printf("sent ping to %s: %s", who, j["id"])
390}
391
392func pong(user *WhatAbout, who string, obj string) {
393 box, ok := boxofboxes.Get(who)
394 if !ok {
395 ilog.Printf("no inbox to pong %s", who)
396 return
397 }
398 j := junk.New()
399 j["@context"] = itiswhatitis
400 j["type"] = "Pong"
401 j["id"] = user.URL + "/pong/" + xfiltrate()
402 j["actor"] = user.URL
403 j["to"] = who
404 j["object"] = obj
405 ki := ziggy(user.ID)
406 if ki == nil {
407 return
408 }
409 err := PostJunk(ki.keyname, ki.seckey, box.In, j)
410 if err != nil {
411 elog.Printf("can't send pong: %s", err)
412 return
413 }
414}
415
416func getinbox(w http.ResponseWriter, r *http.Request) {
417 name := mux.Vars(r)["name"]
418 user, err := butwhatabout(name)
419 if err != nil {
420 http.NotFound(w, r)
421 return
422 }
423 u := login.GetUserInfo(r)
424 if user.ID != UserID(u.UserID) {
425 http.Error(w, "that's not you!", http.StatusForbidden)
426 return
427 }
428
429 honks := gethonksforuser(user.ID, 0)
430 if len(honks) > 60 {
431 honks = honks[0:60]
432 }
433
434 jonks := make([]junk.Junk, 0, len(honks))
435 for _, h := range honks {
436 j, _ := jonkjonk(user, h)
437 jonks = append(jonks, j)
438 }
439
440 j := junk.New()
441 j["@context"] = itiswhatitis
442 j["id"] = user.URL + "/inbox"
443 j["attributedTo"] = user.URL
444 j["type"] = "OrderedCollection"
445 j["totalItems"] = len(jonks)
446 j["orderedItems"] = jonks
447
448 w.Header().Set("Content-Type", theonetruename)
449 j.Write(w)
450}
451
452func postinbox(w http.ResponseWriter, r *http.Request) {
453 if !friendorfoe(r.Header.Get("Content-Type")) {
454 http.Error(w, "speak activity please", http.StatusNotAcceptable)
455 return
456 }
457 name := mux.Vars(r)["name"]
458 user, err := butwhatabout(name)
459 if err != nil {
460 http.NotFound(w, r)
461 return
462 }
463 if stealthmode(user.ID, r) {
464 http.NotFound(w, r)
465 return
466 }
467 var buf bytes.Buffer
468 limiter := io.LimitReader(r.Body, 1*1024*1024)
469 io.Copy(&buf, limiter)
470 payload := buf.Bytes()
471 j, err := junk.FromBytes(payload)
472 if err != nil {
473 ilog.Printf("bad payload: %s", err)
474 ilog.Writer().Write(payload)
475 ilog.Writer().Write([]byte{'\n'})
476 return
477 }
478
479 if crappola(j) {
480 return
481 }
482 what := firstofmany(j, "type")
483 who, _ := j.GetString("actor")
484 if rejectactor(user.ID, who) {
485 return
486 }
487
488 keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
489 if err != nil && keyname != "" {
490 savingthrow(keyname)
491 keyname, err = httpsig.VerifyRequest(r, payload, zaggy)
492 }
493 if err != nil {
494 ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err)
495 if keyname != "" {
496 ilog.Printf("bad signature from %s", keyname)
497 }
498 http.Error(w, "what did you call me?", http.StatusUnauthorized)
499 return
500 }
501 origin := keymatch(keyname, who)
502 if origin == "" {
503 ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who)
504 if what == "Create" {
505 var xid string
506 obj, ok := j.GetMap("object")
507 if ok {
508 xid, _ = obj.GetString("id")
509 } else {
510 xid, _ = j.GetString("object")
511 }
512 if xid != "" {
513 dlog.Printf("getting forwarded create from %s: %s", keyname, xid)
514 go grabhonk(user, xid)
515 }
516 }
517 return
518 }
519
520 switch what {
521 case "Ping":
522 id, _ := j.GetString("id")
523 ilog.Printf("ping from %s: %s", who, id)
524 pong(user, who, id)
525 case "Pong":
526 obj, _ := j.GetString("object")
527 ilog.Printf("pong from %s: %s", who, obj)
528 case "Follow":
529 obj, _ := j.GetString("object")
530 if obj != user.URL {
531 ilog.Printf("can't follow %s", obj)
532 return
533 }
534 followme(user, who, who, j)
535 case "Accept":
536 followyou2(user, j)
537 case "Reject":
538 nofollowyou2(user, j)
539 case "Update":
540 obj, ok := j.GetMap("object")
541 if ok {
542 what := firstofmany(obj, "type")
543 switch what {
544 case "Service":
545 fallthrough
546 case "Person":
547 return
548 case "Question":
549 return
550 }
551 }
552 go xonksaver(user, j, origin)
553 case "Undo":
554 obj, ok := j.GetMap("object")
555 if !ok {
556 folxid, ok := j.GetString("object")
557 if ok && originate(folxid) == origin {
558 unfollowme(user, "", "", j)
559 }
560 return
561 }
562 what := firstofmany(obj, "type")
563 switch what {
564 case "Follow":
565 unfollowme(user, who, who, j)
566 case "Announce":
567 xid, _ := obj.GetString("object")
568 dlog.Printf("undo announce: %s", xid)
569 case "Like":
570 default:
571 ilog.Printf("unknown undo: %s", what)
572 }
573 case "EmojiReact":
574 obj, ok := j.GetString("object")
575 if ok {
576 content, _ := j.GetString("content")
577 addreaction(user, obj, who, content)
578 }
579 default:
580 go saveandcheck(user, j, origin)
581 }
582}
583
584func saveandcheck(user *WhatAbout, j junk.Junk, origin string) {
585 xonk := xonksaver(user, j, origin)
586 if xonk == nil {
587 return
588 }
589 if sname := shortname(user.ID, xonk.Honker); sname == "" {
590 dlog.Printf("received unexpected activity from %s", xonk.Honker)
591 if xonk.Whofore == 0 {
592 dlog.Printf("it's not even for me!")
593 }
594 }
595}
596
597func serverinbox(w http.ResponseWriter, r *http.Request) {
598 user := getserveruser()
599 if stealthmode(user.ID, r) {
600 http.NotFound(w, r)
601 return
602 }
603 var buf bytes.Buffer
604 io.Copy(&buf, r.Body)
605 payload := buf.Bytes()
606 j, err := junk.FromBytes(payload)
607 if err != nil {
608 ilog.Printf("bad payload: %s", err)
609 ilog.Writer().Write(payload)
610 ilog.Writer().Write([]byte{'\n'})
611 return
612 }
613 if crappola(j) {
614 return
615 }
616 keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
617 if err != nil && keyname != "" {
618 savingthrow(keyname)
619 keyname, err = httpsig.VerifyRequest(r, payload, zaggy)
620 }
621 if err != nil {
622 ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err)
623 if keyname != "" {
624 ilog.Printf("bad signature from %s", keyname)
625 }
626 http.Error(w, "what did you call me?", http.StatusUnauthorized)
627 return
628 }
629 who, _ := j.GetString("actor")
630 origin := keymatch(keyname, who)
631 if origin == "" {
632 ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who)
633 return
634 }
635 if rejectactor(user.ID, who) {
636 return
637 }
638 re_ont := regexp.MustCompile(serverURL("/o") + "/([\\pL[:digit:]]+)")
639 what := firstofmany(j, "type")
640 dlog.Printf("server got a %s", what)
641 switch what {
642 case "Follow":
643 obj, _ := j.GetString("object")
644 if obj == user.URL {
645 ilog.Printf("can't follow the server!")
646 return
647 }
648 m := re_ont.FindStringSubmatch(obj)
649 if len(m) != 2 {
650 ilog.Printf("not sure how to handle this")
651 return
652 }
653 ont := "#" + m[1]
654
655 followme(user, who, ont, j)
656 case "Undo":
657 obj, ok := j.GetMap("object")
658 if !ok {
659 ilog.Printf("unknown undo no object")
660 return
661 }
662 what := firstofmany(obj, "type")
663 if what != "Follow" {
664 ilog.Printf("unknown undo: %s", what)
665 return
666 }
667 targ, _ := obj.GetString("object")
668 m := re_ont.FindStringSubmatch(targ)
669 if len(m) != 2 {
670 ilog.Printf("not sure how to handle this")
671 return
672 }
673 ont := "#" + m[1]
674 unfollowme(user, who, ont, j)
675 default:
676 ilog.Printf("unhandled server activity: %s", what)
677 dumpactivity(j)
678 }
679}
680
681func serveractor(w http.ResponseWriter, r *http.Request) {
682 user := getserveruser()
683 if stealthmode(user.ID, r) {
684 http.NotFound(w, r)
685 return
686 }
687 j := junkuser(user)
688 j.Write(w)
689}
690
691func ximport(w http.ResponseWriter, r *http.Request) {
692 u := login.GetUserInfo(r)
693 xid := strings.TrimSpace(r.FormValue("q"))
694 xonk := getxonk(UserID(u.UserID), xid)
695 if xonk == nil {
696 p, _ := investigate(xid)
697 if p != nil {
698 xid = p.XID
699 }
700 j, err := GetJunk(UserID(u.UserID), xid)
701 if err != nil {
702 http.Error(w, "error getting external object", http.StatusInternalServerError)
703 ilog.Printf("error getting external object: %s", err)
704 return
705 }
706 allinjest(originate(xid), j)
707 dlog.Printf("importing %s", xid)
708 user, _ := butwhatabout(u.Username)
709
710 info, _ := somethingabout(j)
711 if info == nil {
712 xonk = xonksaver(user, j, originate(xid))
713 } else if info.What == SomeActor {
714 outbox, _ := j.GetString("outbox")
715 gimmexonks(user, outbox)
716 http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
717 return
718 } else if info.What == SomeCollection {
719 gimmexonks(user, xid)
720 http.Redirect(w, r, "/xzone", http.StatusSeeOther)
721 return
722 }
723 }
724 convoy := ""
725 if xonk != nil {
726 convoy = xonk.Convoy
727 }
728 http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
729}
730
731func xzone(w http.ResponseWriter, r *http.Request) {
732 u := login.GetUserInfo(r)
733 rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
734 if err != nil {
735 elog.Printf("query err: %s", err)
736 return
737 }
738 defer rows.Close()
739 honkers := make([]Honker, 0, 256)
740 for rows.Next() {
741 var xid string
742 rows.Scan(&xid)
743 honkers = append(honkers, Honker{XID: xid})
744 }
745 rows.Close()
746 for i := range honkers {
747 _, honkers[i].Handle = handles(honkers[i].XID)
748 }
749 templinfo := getInfo(r)
750 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
751 templinfo["Honkers"] = honkers
752 err = readviews.Execute(w, "xzone.html", templinfo)
753 if err != nil {
754 elog.Print(err)
755 }
756}
757
758var oldoutbox = gencache.New(gencache.Options[string, []byte]{Fill: func(name string) ([]byte, bool) {
759 user, err := butwhatabout(name)
760 if err != nil {
761 return nil, false
762 }
763 honks := gethonksbyuser(name, false, 0)
764 if len(honks) > 20 {
765 honks = honks[0:20]
766 }
767
768 jonks := make([]junk.Junk, 0, len(honks))
769 for _, h := range honks {
770 j, _ := jonkjonk(user, h)
771 jonks = append(jonks, j)
772 }
773
774 j := junk.New()
775 j["@context"] = itiswhatitis
776 j["id"] = user.URL + "/outbox"
777 j["attributedTo"] = user.URL
778 j["type"] = "OrderedCollection"
779 j["totalItems"] = len(jonks)
780 j["orderedItems"] = jonks
781
782 return j.ToBytes(), true
783}, Duration: 1 * time.Minute})
784
785func getoutbox(w http.ResponseWriter, r *http.Request) {
786 name := mux.Vars(r)["name"]
787 user, err := butwhatabout(name)
788 if err != nil {
789 http.NotFound(w, r)
790 return
791 }
792 if stealthmode(user.ID, r) {
793 http.NotFound(w, r)
794 return
795 }
796 j, ok := oldoutbox.Get(name)
797 if ok {
798 w.Header().Set("Content-Type", theonetruename)
799 w.Write(j)
800 } else {
801 http.NotFound(w, r)
802 }
803}
804
805func postoutbox(w http.ResponseWriter, r *http.Request) {
806 name := mux.Vars(r)["name"]
807 user, err := butwhatabout(name)
808 if err != nil {
809 http.NotFound(w, r)
810 return
811 }
812 u := login.GetUserInfo(r)
813 if user.ID != UserID(u.UserID) {
814 http.Error(w, "that's not you!", http.StatusForbidden)
815 return
816 }
817
818 limiter := io.LimitReader(r.Body, 1*1024*1024)
819 j, err := junk.Read(limiter)
820 if err != nil {
821 http.Error(w, "that's not json!", http.StatusBadRequest)
822 return
823 }
824
825 who, _ := j.GetString("actor")
826 if who != user.URL {
827 http.Error(w, "that's not you!", http.StatusForbidden)
828 return
829 }
830 what := firstofmany(j, "type")
831 switch what {
832 case "Create":
833 honk := xonksaver2(user, j, serverName, true)
834 if honk == nil {
835 dlog.Printf("returned nil")
836 return
837 }
838 go honkworldwide(user, honk)
839 case "Follow":
840 defer honkerinvalidator.Clear(user.ID)
841 url, _ := j.GetString("object")
842 honkerid, flavor, err := savehonker(user, url, "", "presub", "", "{}")
843 if err != nil {
844 http.Error(w, "had some trouble with that: "+err.Error(), http.StatusInternalServerError)
845 return
846 }
847 if flavor == "presub" {
848 followyou(user, honkerid, false)
849 }
850 default:
851 http.Error(w, "not sure about that", http.StatusBadRequest)
852 }
853}
854
855var oldempties = gencache.New(gencache.Options[string, []byte]{Fill: func(url string) ([]byte, bool) {
856 colname := "/followers"
857 if strings.HasSuffix(url, "/following") {
858 colname = "/following"
859 }
860 user := serverURL("%s", url[:len(url)-10])
861 j := junk.New()
862 j["@context"] = itiswhatitis
863 j["id"] = user + colname
864 j["attributedTo"] = user
865 j["type"] = "OrderedCollection"
866 j["totalItems"] = 0
867 j["orderedItems"] = []junk.Junk{}
868
869 return j.ToBytes(), true
870}})
871
872func emptiness(w http.ResponseWriter, r *http.Request) {
873 name := mux.Vars(r)["name"]
874 user, err := butwhatabout(name)
875 if err != nil {
876 http.NotFound(w, r)
877 return
878 }
879 if stealthmode(user.ID, r) {
880 http.NotFound(w, r)
881 return
882 }
883 j, ok := oldempties.Get(r.URL.Path)
884 if ok {
885 w.Header().Set("Content-Type", theonetruename)
886 w.Write(j)
887 } else {
888 http.NotFound(w, r)
889 }
890}
891
892func showuser(w http.ResponseWriter, r *http.Request) {
893 name := mux.Vars(r)["name"]
894 user, err := butwhatabout(name)
895 if err != nil {
896 ilog.Printf("user not found %s: %s", name, err)
897 http.NotFound(w, r)
898 return
899 }
900 if stealthmode(user.ID, r) {
901 http.NotFound(w, r)
902 return
903 }
904 wantjson := false
905 if strings.HasSuffix(r.URL.Path, ".json") {
906 wantjson = true
907 }
908 if friendorfoe(r.Header.Get("Accept")) || wantjson {
909 j, ok := asjonker(name)
910 if ok {
911 w.Header().Set("Content-Type", theonetruename)
912 w.Write(j)
913 } else {
914 http.NotFound(w, r)
915 }
916 return
917 }
918 u := login.GetUserInfo(r)
919 if u != nil && u.Username != name {
920 u = nil
921 }
922 honks := gethonksbyuser(name, u != nil, 0)
923 templinfo := getInfo(r)
924 templinfo["PageName"] = "user"
925 templinfo["PageArg"] = name
926 templinfo["Name"] = user.Name
927 templinfo["Honkology"] = oguser(user)
928 templinfo["WhatAbout"] = user.HTAbout
929 templinfo["ServerMessage"] = ""
930 templinfo["APAltLink"] = templates.Sprintf("<link href='%s' rel='alternate' type='application/activity+json'>", user.URL)
931 if u != nil {
932 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
933 }
934 honkpage(w, u, honks, templinfo)
935}
936
937func showhonker(w http.ResponseWriter, r *http.Request) {
938 u := login.GetUserInfo(r)
939 name := mux.Vars(r)["name"]
940 var honks []*Honk
941 if name == "" {
942 name = r.FormValue("xid")
943 honks = gethonksbyxonker(UserID(u.UserID), name, 0)
944 } else {
945 honks = gethonksbyhonker(UserID(u.UserID), name, 0)
946 }
947 miniform := templates.Sprintf(`<form action="/submithonker" method="POST">
948<input type="hidden" name="CSRF" value="%s">
949<input type="hidden" name="url" value="%s">
950<button tabindex=1 name="add honker" value="add honker">add honker</button>
951</form>`, login.GetCSRF("submithonker", r), name)
952 msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, name, name, miniform)
953 templinfo := getInfo(r)
954 templinfo["PageName"] = "honker"
955 templinfo["PageArg"] = name
956 templinfo["ServerMessage"] = msg
957 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
958 honkpage(w, u, honks, templinfo)
959}
960
961func showcombo(w http.ResponseWriter, r *http.Request) {
962 name := mux.Vars(r)["name"]
963 u := login.GetUserInfo(r)
964 honks := gethonksbycombo(UserID(u.UserID), name, 0)
965 honks = osmosis(honks, UserID(u.UserID), true)
966 templinfo := getInfo(r)
967 templinfo["PageName"] = "combo"
968 templinfo["PageArg"] = name
969 templinfo["ServerMessage"] = "honks by combo: " + name
970 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
971 honkpage(w, u, honks, templinfo)
972}
973func showconvoy(w http.ResponseWriter, r *http.Request) {
974 c := r.FormValue("c")
975 u := login.GetUserInfo(r)
976 honks := gethonksbyconvoy(UserID(u.UserID), c, 0)
977 templinfo := getInfo(r)
978 if len(honks) > 0 {
979 templinfo["TopHID"] = honks[0].ID
980 }
981 honks = osmosis(honks, UserID(u.UserID), false)
982 //reversehonks(honks)
983 honks = threadsort(honks)
984 templinfo["PageName"] = "convoy"
985 templinfo["PageArg"] = c
986 templinfo["ServerMessage"] = "honks in convoy: " + c
987 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
988 honkpage(w, u, honks, templinfo)
989}
990func showsearch(w http.ResponseWriter, r *http.Request) {
991 q := r.FormValue("q")
992 if strings.HasPrefix(q, "https://") {
993 ximport(w, r)
994 return
995 }
996 u := login.GetUserInfo(r)
997 honks := gethonksbysearch(UserID(u.UserID), q, 0)
998 templinfo := getInfo(r)
999 templinfo["PageName"] = "search"
1000 templinfo["PageArg"] = q
1001 templinfo["ServerMessage"] = "honks for search: " + q
1002 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1003 honkpage(w, u, honks, templinfo)
1004}
1005func showontology(w http.ResponseWriter, r *http.Request) {
1006 name := mux.Vars(r)["name"]
1007 u := login.GetUserInfo(r)
1008 var userid UserID = -1
1009 if u != nil {
1010 userid = UserID(u.UserID)
1011 }
1012 honks := gethonksbyontology(userid, "#"+name, 0)
1013 if friendorfoe(r.Header.Get("Accept")) {
1014 if len(honks) > 40 {
1015 honks = honks[0:40]
1016 }
1017
1018 xids := make([]string, 0, len(honks))
1019 for _, h := range honks {
1020 xids = append(xids, h.XID)
1021 }
1022
1023 user := getserveruser()
1024
1025 j := junk.New()
1026 j["@context"] = itiswhatitis
1027 j["id"] = serverURL("/o/%s", name)
1028 j["name"] = "#" + name
1029 j["attributedTo"] = user.URL
1030 j["type"] = "OrderedCollection"
1031 j["totalItems"] = len(xids)
1032 j["orderedItems"] = xids
1033
1034 j.Write(w)
1035 return
1036 }
1037
1038 templinfo := getInfo(r)
1039 templinfo["ServerMessage"] = "honks by ontology: " + name
1040 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1041 honkpage(w, u, honks, templinfo)
1042}
1043
1044type Ont struct {
1045 Name string
1046 Count int64
1047}
1048
1049func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
1050 u := login.GetUserInfo(r)
1051 var userid UserID = -1
1052 if u != nil {
1053 userid = UserID(u.UserID)
1054 }
1055 rows, err := stmtAllOnts.Query(userid)
1056 if err != nil {
1057 elog.Printf("selection error: %s", err)
1058 return
1059 }
1060 defer rows.Close()
1061 onts := make([]Ont, 0, 1024)
1062 pops := make([]Ont, 0, 128)
1063 for rows.Next() {
1064 var o Ont
1065 err := rows.Scan(&o.Name, &o.Count)
1066 if err != nil {
1067 elog.Printf("error scanning ont: %s", err)
1068 continue
1069 }
1070 if utf8.RuneCountInString(o.Name) > 24 {
1071 continue
1072 }
1073 if o.Count < 3 {
1074 continue
1075 }
1076 o.Name = o.Name[1:]
1077 onts = append(onts, o)
1078 pops = append(pops, o)
1079 }
1080 if len(onts) > 1024 {
1081 sort.Slice(onts, func(i, j int) bool {
1082 return onts[i].Count > onts[j].Count
1083 })
1084 onts = onts[:1024]
1085 }
1086 sort.Slice(onts, func(i, j int) bool {
1087 return onts[i].Name < onts[j].Name
1088 })
1089 sort.Slice(pops, func(i, j int) bool {
1090 return pops[i].Count > pops[j].Count
1091 })
1092 if len(pops) > 40 {
1093 pops = pops[:40]
1094 }
1095 letters := make([]string, 0, 64)
1096 var lastrune rune = -1
1097 for _, o := range onts {
1098 if r := firstRune(o.Name); r != lastrune {
1099 letters = append(letters, string(r))
1100 lastrune = r
1101 }
1102 }
1103 if u == nil && !develMode {
1104 w.Header().Set("Cache-Control", "max-age=300")
1105 }
1106 templinfo := getInfo(r)
1107 templinfo["Pops"] = pops
1108 templinfo["Onts"] = onts
1109 templinfo["Letters"] = letters
1110 templinfo["FirstRune"] = firstRune
1111 err = readviews.Execute(w, "onts.html", templinfo)
1112 if err != nil {
1113 elog.Print(err)
1114 }
1115}
1116
1117type Track struct {
1118 xid string
1119 who string
1120}
1121
1122func getbacktracks(xid string) []string {
1123 c := make(chan bool)
1124 dumptracks <- c
1125 <-c
1126 row := stmtGetTracks.QueryRow(xid)
1127 var rawtracks string
1128 err := row.Scan(&rawtracks)
1129 if err != nil {
1130 if err != sql.ErrNoRows {
1131 elog.Printf("error scanning tracks: %s", err)
1132 }
1133 return nil
1134 }
1135 var rcpts []string
1136 for _, f := range strings.Split(rawtracks, " ") {
1137 idx := strings.LastIndexByte(f, '#')
1138 if idx != -1 {
1139 f = f[:idx]
1140 }
1141 if !strings.HasPrefix(f, "https://") {
1142 f = fmt.Sprintf("%%https://%s/inbox", f)
1143 }
1144 rcpts = append(rcpts, f)
1145 }
1146 return rcpts
1147}
1148
1149func savetracks(tracks map[string][]string) {
1150 db := opendatabase()
1151 tx, err := db.Begin()
1152 if err != nil {
1153 elog.Printf("savetracks begin error: %s", err)
1154 return
1155 }
1156 defer func() {
1157 err := tx.Commit()
1158 if err != nil {
1159 elog.Printf("savetracks commit error: %s", err)
1160 }
1161
1162 }()
1163 stmtGetTracks, err := tx.Prepare("select fetches from tracks where xid = ?")
1164 if err != nil {
1165 elog.Printf("savetracks error: %s", err)
1166 return
1167 }
1168 stmtNewTracks, err := tx.Prepare("insert into tracks (xid, fetches) values (?, ?)")
1169 if err != nil {
1170 elog.Printf("savetracks error: %s", err)
1171 return
1172 }
1173 stmtUpdateTracks, err := tx.Prepare("update tracks set fetches = ? where xid = ?")
1174 if err != nil {
1175 elog.Printf("savetracks error: %s", err)
1176 return
1177 }
1178 count := 0
1179 for xid, f := range tracks {
1180 count += len(f)
1181 var prev string
1182 row := stmtGetTracks.QueryRow(xid)
1183 err := row.Scan(&prev)
1184 if err == sql.ErrNoRows {
1185 f = oneofakind(f)
1186 stmtNewTracks.Exec(xid, strings.Join(f, " "))
1187 } else if err == nil {
1188 all := append(strings.Split(prev, " "), f...)
1189 all = oneofakind(all)
1190 stmtUpdateTracks.Exec(strings.Join(all, " "))
1191 } else {
1192 elog.Printf("savetracks error: %s", err)
1193 }
1194 }
1195 dlog.Printf("saved %d new fetches", count)
1196}
1197
1198var trackchan = make(chan Track, 4)
1199var dumptracks = make(chan chan bool)
1200
1201func tracker() {
1202 timeout := 4 * time.Minute
1203 sleeper := time.NewTimer(timeout)
1204 tracks := make(map[string][]string)
1205 workinprogress++
1206 for {
1207 select {
1208 case track := <-trackchan:
1209 tracks[track.xid] = append(tracks[track.xid], track.who)
1210 case <-sleeper.C:
1211 if len(tracks) > 0 {
1212 go savetracks(tracks)
1213 tracks = make(map[string][]string)
1214 }
1215 sleeper.Reset(timeout)
1216 case c := <-dumptracks:
1217 if len(tracks) > 0 {
1218 savetracks(tracks)
1219 tracks = make(map[string][]string)
1220 }
1221 c <- true
1222 case <-endoftheworld:
1223 if len(tracks) > 0 {
1224 savetracks(tracks)
1225 tracks = make(map[string][]string)
1226 }
1227 readyalready <- true
1228 return
1229 }
1230 }
1231}
1232
1233var re_keyholder = regexp.MustCompile(`keyId="([^"]+)"`)
1234
1235func requestActor(r *http.Request) string {
1236 if sig := r.Header.Get("Signature"); sig != "" {
1237 if m := re_keyholder.FindStringSubmatch(sig); len(m) == 2 {
1238 return m[1]
1239 }
1240 }
1241 return ""
1242}
1243
1244func trackback(xid string, r *http.Request) {
1245 who := requestActor(r)
1246 if who != "" {
1247 select {
1248 case trackchan <- Track{xid: xid, who: who}:
1249 default:
1250 }
1251 }
1252}
1253
1254func sameperson(h1, h2 *Honk) bool {
1255 n1, n2 := h1.Honker, h2.Honker
1256 if h1.Oonker != "" {
1257 n1 = h1.Oonker
1258 }
1259 if h2.Oonker != "" {
1260 n2 = h2.Oonker
1261 }
1262 return n1 == n2
1263}
1264
1265func threadposes(honks []*Honk, wanted int64) ([]*Honk, []int) {
1266 var poses []int
1267 var newhonks []*Honk
1268 for i, honk := range honks {
1269 if honk.ID > wanted {
1270 newhonks = append(newhonks, honk)
1271 poses = append(poses, i)
1272 }
1273 }
1274 return newhonks, poses
1275}
1276
1277func threadsort(honks []*Honk) []*Honk {
1278 sort.Slice(honks, func(i, j int) bool {
1279 return honks[i].Date.Before(honks[j].Date)
1280 })
1281 honkx := make(map[string]*Honk, len(honks))
1282 kids := make(map[string][]*Honk, len(honks))
1283 for _, h := range honks {
1284 honkx[h.XID] = h
1285 rid := h.RID
1286 kids[rid] = append(kids[rid], h)
1287 }
1288 done := make(map[*Honk]bool, len(honks))
1289 thread := make([]*Honk, 0, len(honks))
1290 var nextlevel func(p *Honk)
1291 level := 0
1292 hasreply := func(p *Honk, who string) bool {
1293 for _, h := range kids[p.XID] {
1294 if h.Honker == who {
1295 return true
1296 }
1297 }
1298 return false
1299 }
1300 nextlevel = func(p *Honk) {
1301 levelup := level < 4
1302 if pp := honkx[p.RID]; p.RID == "" || (pp != nil && sameperson(p, pp)) {
1303 levelup = false
1304 }
1305 if level > 0 && len(kids[p.RID]) == 1 {
1306 if pp := honkx[p.RID]; pp != nil && len(kids[pp.RID]) == 1 {
1307 levelup = false
1308 }
1309 }
1310 if levelup {
1311 level++
1312 }
1313 p.Style += fmt.Sprintf(" level%d", level)
1314 childs := kids[p.XID]
1315 sort.SliceStable(childs, func(i, j int) bool {
1316 var ipts, jpts int
1317 if sameperson(childs[i], p) {
1318 ipts += 1
1319 } else if hasreply(childs[i], p.Honker) {
1320 ipts += 2
1321 }
1322 if sameperson(childs[j], p) {
1323 jpts += 1
1324 } else if hasreply(childs[j], p.Honker) {
1325 jpts += 2
1326 }
1327 return ipts > jpts
1328 })
1329 for _, h := range childs {
1330 if !done[h] {
1331 done[h] = true
1332 thread = append(thread, h)
1333 nextlevel(h)
1334 }
1335 }
1336 if levelup {
1337 level--
1338 }
1339 }
1340 for _, h := range honks {
1341 if !done[h] && h.RID == "" {
1342 done[h] = true
1343 thread = append(thread, h)
1344 nextlevel(h)
1345 }
1346 }
1347 for _, h := range honks {
1348 if !done[h] {
1349 done[h] = true
1350 thread = append(thread, h)
1351 nextlevel(h)
1352 }
1353 }
1354 return thread
1355}
1356
1357func oguser(user *WhatAbout) template.HTML {
1358 short := user.About
1359 if len(short) > 160 {
1360 short = short[0:160] + "..."
1361 }
1362 title := user.Display
1363 imgurl := avatarURL(user)
1364 return templates.Sprintf(
1365 `<meta property="og:title" content="%s" />
1366<meta property="og:type" content="website" />
1367<meta property="og:url" content="%s" />
1368<meta property="og:image" content="%s" />
1369<meta property="og:description" content="%s" />`,
1370 title, user.URL, imgurl, short)
1371}
1372
1373func honkology(honk *Honk) template.HTML {
1374 user, ok := somenumberedusers.Get(honk.UserID)
1375 if !ok {
1376 return ""
1377 }
1378 title := fmt.Sprintf("%s: %s", user.Display, honk.Precis)
1379 imgurl := avatarURL(user)
1380 for _, d := range honk.Donks {
1381 if d.Local && strings.HasPrefix(d.Media, "image") {
1382 imgurl = d.URL
1383 break
1384 }
1385 }
1386 short := honk.Noise
1387 if len(short) > 160 {
1388 short = short[0:160] + "..."
1389 }
1390 return templates.Sprintf(
1391 `<meta property="og:title" content="%s" />
1392<meta property="og:type" content="article" />
1393<meta property="article:author" content="%s" />
1394<meta property="og:url" content="%s" />
1395<meta property="og:image" content="%s" />
1396<meta property="og:description" content="%s" />`,
1397 title, user.URL, honk.XID, imgurl, short)
1398}
1399
1400func showonehonk(w http.ResponseWriter, r *http.Request) {
1401 name := mux.Vars(r)["name"]
1402 user, err := butwhatabout(name)
1403 if err != nil {
1404 http.NotFound(w, r)
1405 return
1406 }
1407 if stealthmode(user.ID, r) {
1408 http.NotFound(w, r)
1409 return
1410 }
1411 wantjson := false
1412 path := r.URL.Path
1413 if strings.HasSuffix(path, ".json") {
1414 path = path[:len(path)-5]
1415 wantjson = true
1416 }
1417 xid := serverURL("%s", path)
1418
1419 if friendorfoe(r.Header.Get("Accept")) || wantjson {
1420 j, ok := gimmejonk(xid)
1421 if ok {
1422 trackback(xid, r)
1423 w.Header().Set("Content-Type", theonetruename)
1424 w.Write(j)
1425 } else {
1426 http.NotFound(w, r)
1427 }
1428 return
1429 }
1430 honk := getxonk(user.ID, xid)
1431 if honk == nil {
1432 http.NotFound(w, r)
1433 return
1434 }
1435 u := login.GetUserInfo(r)
1436 if u != nil && UserID(u.UserID) != user.ID {
1437 u = nil
1438 }
1439 if !honk.Public {
1440 if u == nil {
1441 http.NotFound(w, r)
1442 return
1443
1444 }
1445 honks := []*Honk{honk}
1446 donksforhonks(honks)
1447 templinfo := getInfo(r)
1448 templinfo["ServerMessage"] = "one honk maybe more"
1449 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1450 honkpage(w, u, honks, templinfo)
1451 return
1452 }
1453
1454 templinfo := getInfo(r)
1455 rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1456 //reversehonks(rawhonks)
1457 rawhonks = threadsort(rawhonks)
1458 honks := make([]*Honk, 0, len(rawhonks))
1459 for i, h := range rawhonks {
1460 if h.XID == xid {
1461 templinfo["Honkology"] = honkology(h)
1462 if i > 0 {
1463 h.Style += " glow"
1464 }
1465 }
1466 if h.Public && (h.Whofore == 2 || h.IsAcked()) {
1467 honks = append(honks, h)
1468 }
1469 }
1470
1471 templinfo["ServerMessage"] = "one honk maybe more"
1472 if u != nil {
1473 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1474 }
1475 templinfo["APAltLink"] = templates.Sprintf("<link href='%s' rel='alternate' type='application/activity+json'>", xid)
1476 honkpage(w, u, honks, templinfo)
1477}
1478
1479func honkpage(w http.ResponseWriter, u *login.UserInfo, honks []*Honk, templinfo map[string]interface{}) {
1480 var userid UserID = -1
1481 if u != nil {
1482 userid = UserID(u.UserID)
1483 templinfo["User"], _ = butwhatabout(u.Username)
1484 }
1485 reverbolate(userid, honks)
1486 templinfo["Honks"] = honks
1487 templinfo["MapLink"] = getmaplink(u)
1488 if templinfo["TopHID"] == nil {
1489 if len(honks) > 0 {
1490 templinfo["TopHID"] = honks[0].ID
1491 } else {
1492 templinfo["TopHID"] = 0
1493 }
1494 }
1495 if u == nil && !develMode {
1496 w.Header().Set("Cache-Control", "max-age=60")
1497 }
1498 err := readviews.Execute(w, "honkpage.html", templinfo)
1499 if err != nil {
1500 elog.Print(err)
1501 }
1502}
1503
1504func saveuser(w http.ResponseWriter, r *http.Request) {
1505 whatabout := r.FormValue("whatabout")
1506 whatabout = strings.Replace(whatabout, "\r", "", -1)
1507 u := login.GetUserInfo(r)
1508 user, _ := butwhatabout(u.Username)
1509 db := opendatabase()
1510
1511 options := user.Options
1512 options.SkinnyCSS = r.FormValue("skinny") == "skinny"
1513 options.OmitImages = r.FormValue("omitimages") == "omitimages"
1514 options.MentionAll = r.FormValue("mentionall") == "mentionall"
1515 options.InlineQuotes = r.FormValue("inlineqts") == "inlineqts"
1516 options.MapLink = r.FormValue("maps")
1517 options.Reaction = r.FormValue("reaction")
1518
1519 sendupdate := false
1520 if r.FormValue("displayname") != "" {
1521 options.CustomDisplay = r.FormValue("displayname")
1522 _, err := db.Exec("update users set displayname = ? where username = ?", options.CustomDisplay, u.Username)
1523 if err != nil {
1524 elog.Printf("error setting displayname: %s", err)
1525 }
1526 sendupdate = true
1527 } else {
1528 options.CustomDisplay = ""
1529 }
1530
1531 options.Reaction = r.FormValue("reaction")
1532
1533 ava := re_avatar.FindString(whatabout)
1534 if ava != "" {
1535 whatabout = re_avatar.ReplaceAllString(whatabout, "")
1536 ava = ava[7:]
1537 if ava[0] == ' ' {
1538 ava = ava[1:]
1539 }
1540 ava = serverURL("/meme/%s", ava)
1541 }
1542 if ava != options.Avatar {
1543 options.Avatar = ava
1544 sendupdate = true
1545 }
1546 ban := re_banner.FindString(whatabout)
1547 if ban != "" {
1548 whatabout = re_banner.ReplaceAllString(whatabout, "")
1549 ban = ban[7:]
1550 if ban[0] == ' ' {
1551 ban = ban[1:]
1552 }
1553 ban = serverURL("/meme/%s", ban)
1554 }
1555 if ban != options.Banner {
1556 options.Banner = ban
1557 sendupdate = true
1558 }
1559 whatabout = strings.TrimSpace(whatabout)
1560 if whatabout != user.About {
1561 sendupdate = true
1562 }
1563 j, err := jsonify(options)
1564 if err == nil {
1565 _, err = db.Exec("update users set about = ?, options = ? where username = ?", whatabout, j, u.Username)
1566 }
1567 if err != nil {
1568 elog.Printf("error bouting what: %s", err)
1569 }
1570 somenamedusers.Clear(u.Username)
1571 somenumberedusers.Clear(user.ID)
1572 oldjonkers.Clear(u.Username)
1573
1574 if sendupdate {
1575 updateMe(u.Username)
1576 }
1577
1578 http.Redirect(w, r, "/account", http.StatusSeeOther)
1579}
1580
1581func bonkit(xid string, user *WhatAbout) {
1582 dlog.Printf("bonking %s", xid)
1583
1584 xonk := getxonk(user.ID, xid)
1585 if xonk == nil {
1586 return
1587 }
1588 if !xonk.Public {
1589 return
1590 }
1591 if xonk.IsBonked() {
1592 return
1593 }
1594 donksforhonks([]*Honk{xonk})
1595
1596 _, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
1597 if err != nil {
1598 elog.Printf("error acking bonk: %s", err)
1599 }
1600
1601 oonker := xonk.Oonker
1602 if oonker == "" {
1603 oonker = xonk.Honker
1604 }
1605 dt := time.Now().UTC()
1606 bonk := &Honk{
1607 UserID: user.ID,
1608 Username: user.Name,
1609 What: "bonk",
1610 Honker: user.URL,
1611 Oonker: oonker,
1612 XID: xonk.XID,
1613 RID: xonk.RID,
1614 Noise: xonk.Noise,
1615 Precis: xonk.Precis,
1616 URL: xonk.URL,
1617 Date: dt,
1618 Donks: xonk.Donks,
1619 Whofore: 2,
1620 Convoy: xonk.Convoy,
1621 Audience: []string{thewholeworld, oonker},
1622 Public: true,
1623 Format: xonk.Format,
1624 Place: xonk.Place,
1625 Onts: xonk.Onts,
1626 Time: xonk.Time,
1627 }
1628
1629 err = savehonk(bonk)
1630 if err != nil {
1631 elog.Printf("uh oh")
1632 return
1633 }
1634
1635 go honkworldwide(user, bonk)
1636}
1637
1638func submitbonk(w http.ResponseWriter, r *http.Request) {
1639 xid := r.FormValue("xid")
1640 userinfo := login.GetUserInfo(r)
1641 user, _ := butwhatabout(userinfo.Username)
1642
1643 bonkit(xid, user)
1644
1645 if r.FormValue("js") != "1" {
1646 templinfo := getInfo(r)
1647 templinfo["ServerMessage"] = "Bonked!"
1648 err := readviews.Execute(w, "msg.html", templinfo)
1649 if err != nil {
1650 elog.Print(err)
1651 }
1652 }
1653}
1654
1655func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string, aux string) {
1656 zonk := &Honk{
1657 Honker: user.URL,
1658 What: what,
1659 XID: xonk.XID,
1660 Date: time.Now().UTC(),
1661 Audience: oneofakind(xonk.Audience),
1662 Noise: aux,
1663 }
1664 zonk.Public = loudandproud(zonk.Audience)
1665
1666 dlog.Printf("announcing %sed honk: %s", what, xonk.XID)
1667 go honkworldwide(user, zonk)
1668}
1669
1670func zonkit(w http.ResponseWriter, r *http.Request) {
1671 wherefore := r.FormValue("wherefore")
1672 what := r.FormValue("what")
1673 u := login.GetUserInfo(r)
1674 user, _ := butwhatabout(u.Username)
1675
1676 if wherefore == "save" {
1677 xonk := getxonk(user.ID, what)
1678 if xonk != nil {
1679 _, err := stmtUpdateFlags.Exec(flagIsSaved, xonk.ID)
1680 if err != nil {
1681 elog.Printf("error saving: %s", err)
1682 }
1683 }
1684 return
1685 }
1686
1687 if wherefore == "unsave" {
1688 xonk := getxonk(user.ID, what)
1689 if xonk != nil {
1690 _, err := stmtClearFlags.Exec(flagIsSaved, xonk.ID)
1691 if err != nil {
1692 elog.Printf("error unsaving: %s", err)
1693 }
1694 }
1695 return
1696 }
1697
1698 if wherefore == "react" {
1699 reaction := user.Options.Reaction
1700 if r2 := r.FormValue("reaction"); r2 != "" {
1701 reaction = r2
1702 }
1703 if reaction == "none" {
1704 return
1705 }
1706 xonk := getxonk(user.ID, what)
1707 if xonk != nil {
1708 _, err := stmtUpdateFlags.Exec(flagIsReacted, xonk.ID)
1709 if err != nil {
1710 elog.Printf("error saving: %s", err)
1711 }
1712 sendzonkofsorts(xonk, user, "react", reaction)
1713 }
1714 return
1715 }
1716
1717 // my hammer is too big, oh well
1718 defer oldjonks.Flush()
1719
1720 if wherefore == "ack" {
1721 xonk := getxonk(user.ID, what)
1722 if xonk != nil && !xonk.IsAcked() {
1723 _, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1724 if err != nil {
1725 elog.Printf("error acking: %s", err)
1726 }
1727 sendzonkofsorts(xonk, user, "ack", "")
1728 }
1729 return
1730 }
1731
1732 if wherefore == "deack" {
1733 xonk := getxonk(user.ID, what)
1734 if xonk != nil && xonk.IsAcked() {
1735 _, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1736 if err != nil {
1737 elog.Printf("error deacking: %s", err)
1738 }
1739 sendzonkofsorts(xonk, user, "deack", "")
1740 }
1741 return
1742 }
1743
1744 if wherefore == "bonk" {
1745 bonkit(what, user)
1746 return
1747 }
1748
1749 if wherefore == "unbonk" {
1750 xonk := getbonk(user.ID, what)
1751 if xonk != nil {
1752 deletehonk(xonk.ID)
1753 xonk = getxonk(user.ID, what)
1754 _, err := stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1755 if err != nil {
1756 elog.Printf("error unbonking: %s", err)
1757 }
1758 sendzonkofsorts(xonk, user, "unbonk", "")
1759 }
1760 return
1761 }
1762
1763 if wherefore == "untag" {
1764 xonk := getxonk(user.ID, what)
1765 if xonk != nil {
1766 _, err := stmtUpdateFlags.Exec(flagIsUntagged, xonk.ID)
1767 if err != nil {
1768 elog.Printf("error untagging: %s", err)
1769 }
1770 }
1771 var badparents map[string]bool
1772 untagged.GetAndLock(user.ID, &badparents)
1773 badparents[what] = true
1774 untagged.Unlock()
1775 return
1776 }
1777
1778 ilog.Printf("zonking %s %s", wherefore, what)
1779 if wherefore == "zonk" {
1780 xonk := getxonk(user.ID, what)
1781 if xonk != nil {
1782 deletehonk(xonk.ID)
1783 if xonk.Whofore == 2 || xonk.Whofore == 3 {
1784 sendzonkofsorts(xonk, user, "zonk", "")
1785 }
1786 }
1787 }
1788 _, err := stmtSaveZonker.Exec(user.ID, what, wherefore)
1789 if err != nil {
1790 elog.Printf("error saving zonker: %s", err)
1791 return
1792 }
1793}
1794
1795func edithonkpage(w http.ResponseWriter, r *http.Request) {
1796 u := login.GetUserInfo(r)
1797 user, _ := butwhatabout(u.Username)
1798 xid := r.FormValue("xid")
1799 honk := getxonk(user.ID, xid)
1800 if !canedithonk(user, honk) {
1801 http.Error(w, "no editing that please", http.StatusInternalServerError)
1802 return
1803 }
1804 noise := honk.Noise
1805
1806 honks := []*Honk{honk}
1807 donksforhonks(honks)
1808 var savedfiles []string
1809 for _, d := range honk.Donks {
1810 savedfiles = append(savedfiles, fmt.Sprintf("%s:%d", d.XID, d.FileID))
1811 }
1812 reverbolate(user.ID, honks)
1813
1814 templinfo := getInfo(r)
1815 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1816 templinfo["Honks"] = honks
1817 templinfo["MapLink"] = getmaplink(u)
1818 templinfo["Noise"] = noise
1819 templinfo["SavedPlace"] = honk.Place
1820 if tm := honk.Time; tm != nil {
1821 templinfo["ShowTime"] = " "
1822 templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04")
1823 if tm.Duration != 0 {
1824 templinfo["Duration"] = tm.Duration
1825 }
1826 }
1827 templinfo["Onties"] = honk.Onties
1828 templinfo["SeeAlso"] = honk.SeeAlso
1829 templinfo["Link"] = honk.Link
1830 templinfo["LegalName"] = honk.LegalName
1831 templinfo["ServerMessage"] = "honk edit"
1832 templinfo["IsPreview"] = true
1833 templinfo["UpdateXID"] = honk.XID
1834 if len(savedfiles) > 0 {
1835 templinfo["SavedFile"] = strings.Join(savedfiles, ",")
1836 }
1837 err := readviews.Execute(w, "honkpage.html", templinfo)
1838 if err != nil {
1839 elog.Print(err)
1840 }
1841}
1842
1843func newhonkpage(w http.ResponseWriter, r *http.Request) {
1844 u := login.GetUserInfo(r)
1845 rid := r.FormValue("rid")
1846 noise := ""
1847
1848 xonk := getxonk(UserID(u.UserID), rid)
1849 if xonk != nil {
1850 _, replto := handles(xonk.Honker)
1851 if replto != "" {
1852 noise = "@" + replto + " "
1853 }
1854 }
1855
1856 templinfo := getInfo(r)
1857 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1858 templinfo["InReplyTo"] = rid
1859 templinfo["Noise"] = noise
1860 templinfo["ServerMessage"] = "compose honk"
1861 templinfo["IsPreview"] = true
1862 err := readviews.Execute(w, "honkpage.html", templinfo)
1863 if err != nil {
1864 elog.Print(err)
1865 }
1866}
1867
1868func canedithonk(user *WhatAbout, honk *Honk) bool {
1869 if honk == nil || honk.Honker != user.URL || honk.What == "bonk" {
1870 return false
1871 }
1872 return true
1873}
1874
1875func submitdonk(w http.ResponseWriter, r *http.Request) ([]*Donk, error) {
1876 if !strings.HasPrefix(strings.ToLower(r.Header.Get("Content-Type")), "multipart/form-data") {
1877 return nil, nil
1878 }
1879 var donks []*Donk
1880 for i, hdr := range r.MultipartForm.File["donk"] {
1881 if i > 16 {
1882 break
1883 }
1884 donk, err := formtodonk(w, r, hdr)
1885 if err != nil {
1886 return nil, err
1887 }
1888 donks = append(donks, donk)
1889 }
1890 return donks, nil
1891}
1892
1893func formtodonk(w http.ResponseWriter, r *http.Request, filehdr *multipart.FileHeader) (*Donk, error) {
1894 file, err := filehdr.Open()
1895 if err != nil {
1896 if err == http.ErrMissingFile {
1897 return nil, nil
1898 }
1899 elog.Printf("error reading donk: %s", err)
1900 http.Error(w, "error reading donk", http.StatusUnsupportedMediaType)
1901 return nil, err
1902 }
1903 var buf bytes.Buffer
1904 io.Copy(&buf, file)
1905 file.Close()
1906 data := buf.Bytes()
1907 var media, name string
1908 var donkmeta DonkMeta
1909 img, err := bigshrink(data)
1910 if err == nil {
1911 data = img.Data
1912 donkmeta.Width = img.Width
1913 donkmeta.Height = img.Height
1914 format := img.Format
1915 media = "image/" + format
1916 if format == "jpeg" {
1917 format = "jpg"
1918 }
1919 if format == "svg+xml" {
1920 format = "svg"
1921 }
1922 name = xfiltrate() + "." + format
1923 } else {
1924 ct := http.DetectContentType(data)
1925 switch ct {
1926 case "application/pdf":
1927 maxsize := 10000000
1928 if len(data) > maxsize {
1929 ilog.Printf("bad image: %s too much pdf: %d", err, len(data))
1930 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1931 return nil, err
1932 }
1933 media = ct
1934 name = filehdr.Filename
1935 if name == "" {
1936 name = xfiltrate() + ".pdf"
1937 }
1938 default:
1939 maxsize := 100000
1940 if len(data) > maxsize {
1941 ilog.Printf("bad image: %s too much text: %d", err, len(data))
1942 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1943 return nil, err
1944 }
1945 for i := 0; i < len(data); i++ {
1946 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1947 ilog.Printf("bad image: %s not text: %d", err, data[i])
1948 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1949 return nil, err
1950 }
1951 }
1952 media = "text/plain"
1953 name = filehdr.Filename
1954 if name == "" {
1955 name = xfiltrate() + ".txt"
1956 }
1957 }
1958 }
1959 donkmeta.Length = len(data)
1960 desc := strings.TrimSpace(r.FormValue("donkdesc"))
1961 if desc == "" {
1962 desc = name
1963 }
1964 fileid, xid, err := savefileandxid(name, desc, "", media, true, data, &donkmeta)
1965 if err != nil {
1966 elog.Printf("unable to save image: %s", err)
1967 http.Error(w, "failed to save attachment", http.StatusUnsupportedMediaType)
1968 return nil, err
1969 }
1970 d := &Donk{
1971 FileID: fileid,
1972 XID: xid,
1973 Desc: desc,
1974 Local: true,
1975 }
1976 return d, nil
1977}
1978
1979func websubmithonk(w http.ResponseWriter, r *http.Request) {
1980 h := submithonk(w, r)
1981 if h == nil {
1982 return
1983 }
1984 redir := h.XID[len(serverURL("")):]
1985 http.Redirect(w, r, redir, http.StatusSeeOther)
1986}
1987
1988// what a hot mess this function is
1989func submithonk(w http.ResponseWriter, r *http.Request) *Honk {
1990 rid := r.FormValue("rid")
1991 noise := r.FormValue("noise")
1992 format := r.FormValue("format")
1993 if format == "" {
1994 format = "markdown"
1995 }
1996 if !(format == "markdown" || format == "html") {
1997 http.Error(w, "unknown format", 500)
1998 return nil
1999 }
2000
2001 u := login.GetUserInfo(r)
2002 user, _ := butwhatabout(u.Username)
2003
2004 dt := time.Now().UTC()
2005 updatexid := r.FormValue("updatexid")
2006 var honk *Honk
2007 if updatexid != "" {
2008 honk = getxonk(user.ID, updatexid)
2009 if !canedithonk(user, honk) {
2010 http.Error(w, "no editing that please", http.StatusInternalServerError)
2011 return nil
2012 }
2013 honk.Date = dt
2014 honk.What = "update"
2015 honk.Format = format
2016 } else {
2017 xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
2018 what := "honk"
2019 honk = &Honk{
2020 UserID: user.ID,
2021 Username: user.Name,
2022 What: what,
2023 Honker: user.URL,
2024 XID: xid,
2025 Date: dt,
2026 Format: format,
2027 }
2028 }
2029 honk.SeeAlso = strings.TrimSpace(r.FormValue("seealso"))
2030 honk.Onties = strings.TrimSpace(r.FormValue("onties"))
2031 honk.Link = strings.TrimSpace(r.FormValue("link"))
2032 honk.LegalName = strings.TrimSpace(r.FormValue("legalname"))
2033
2034 var convoy string
2035 noise = strings.Replace(noise, "\r", "", -1)
2036 if updatexid == "" && rid == "" {
2037 noise = re_convoy.ReplaceAllStringFunc(noise, func(m string) string {
2038 convoy = m[7:]
2039 convoy = strings.TrimSpace(convoy)
2040 if !re_convalidate.MatchString(convoy) {
2041 convoy = ""
2042 }
2043 return ""
2044 })
2045 }
2046 noise = quickrename(noise, user.ID)
2047 honk.Noise = noise
2048 precipitate(honk)
2049 noise = honk.Noise
2050 translate(honk)
2051
2052 if rid != "" {
2053 xonk := getxonk(user.ID, rid)
2054 if xonk == nil {
2055 http.Error(w, "replyto disappeared", http.StatusNotFound)
2056 return nil
2057 }
2058 if xonk.Public {
2059 honk.Audience = append(honk.Audience, xonk.Audience...)
2060 }
2061 convoy = xonk.Convoy
2062 for i, a := range honk.Audience {
2063 if a == thewholeworld {
2064 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
2065 break
2066 }
2067 }
2068 honk.RID = rid
2069 if xonk.Precis != "" && honk.Precis == "" {
2070 honk.Precis = xonk.Precis
2071 if !re_dangerous.MatchString(honk.Precis) {
2072 honk.Precis = "re: " + honk.Precis
2073 }
2074 }
2075 } else if updatexid == "" {
2076 honk.Audience = []string{thewholeworld}
2077 }
2078 if noise != "" && noise[0] == '@' {
2079 honk.Audience = append(grapevine(honk.Mentions), honk.Audience...)
2080 } else {
2081 honk.Audience = append(honk.Audience, grapevine(honk.Mentions)...)
2082 }
2083 honk.Convoy = convoy
2084
2085 if convoy == "" {
2086 convoy = fmt.Sprintf("data:,%s-", masqName) + xfiltrate()
2087 fmt.Println("convoy:", convoy)
2088 }
2089
2090 honk.Convoy = convoy
2091 butnottooloud(honk.Audience)
2092 honk.Audience = oneofakind(honk.Audience)
2093 if len(honk.Audience) == 0 {
2094 ilog.Printf("honk to nowhere")
2095 http.Error(w, "honk to nowhere...", http.StatusNotFound)
2096 return nil
2097 }
2098 honk.Public = loudandproud(honk.Audience)
2099
2100 donkxid := strings.Join(r.Form["donkxid"], ",")
2101 if donkxid == "" {
2102 donks, err := submitdonk(w, r)
2103 if err != nil && err != http.ErrMissingFile {
2104 return nil
2105 }
2106 if len(donks) > 0 {
2107 honk.Donks = append(honk.Donks, donks...)
2108 var xids []string
2109 for _, d := range honk.Donks {
2110 xids = append(xids, fmt.Sprintf("%s:%d", d.XID, d.FileID))
2111 }
2112 donkxid = strings.Join(xids, ",")
2113 }
2114 } else {
2115 xids := strings.Split(donkxid, ",")
2116 for i, xid := range xids {
2117 if i > 16 {
2118 break
2119 }
2120 p := strings.Split(xid, ":")
2121 xid = p[0]
2122 url := serverURL("/d/%s", xid)
2123 var donk *Donk
2124 if len(p) > 1 {
2125 fileid, _ := strconv.ParseInt(p[1], 10, 0)
2126 donk = finddonkid(fileid, url)
2127 } else {
2128 donk = finddonk(url)
2129 }
2130 if donk != nil {
2131 honk.Donks = append(honk.Donks, donk)
2132 } else {
2133 ilog.Printf("can't find file: %s", xid)
2134 }
2135 }
2136 }
2137 memetize(honk)
2138 imaginate(honk)
2139
2140 placename := strings.TrimSpace(r.FormValue("placename"))
2141 placelat := strings.TrimSpace(r.FormValue("placelat"))
2142 placelong := strings.TrimSpace(r.FormValue("placelong"))
2143 placeurl := strings.TrimSpace(r.FormValue("placeurl"))
2144 if placename != "" || placelat != "" || placelong != "" || placeurl != "" {
2145 p := new(Place)
2146 p.Name = placename
2147 p.Latitude, _ = strconv.ParseFloat(placelat, 64)
2148 p.Longitude, _ = strconv.ParseFloat(placelong, 64)
2149 p.Url = placeurl
2150 honk.Place = p
2151 }
2152 timestart := strings.TrimSpace(r.FormValue("timestart"))
2153 if timestart != "" {
2154 t := new(Time)
2155 now := time.Now().Local()
2156 for _, layout := range []string{"2006-01-02 3:04pm", "2006-01-02 15:04", "3:04pm", "15:04"} {
2157 start, err := time.ParseInLocation(layout, timestart, now.Location())
2158 if err == nil {
2159 if start.Year() == 0 {
2160 start = time.Date(now.Year(), now.Month(), now.Day(), start.Hour(), start.Minute(), 0, 0, now.Location())
2161 }
2162 t.StartTime = start
2163 break
2164 }
2165 }
2166 timeend := r.FormValue("timeend")
2167 dur := parseDuration(timeend)
2168 if dur != 0 {
2169 t.Duration = Duration(dur)
2170 }
2171 if !t.StartTime.IsZero() {
2172 honk.What = "event"
2173 honk.Time = t
2174 }
2175 }
2176
2177 if honk.Public {
2178 honk.Whofore = 2
2179 } else {
2180 honk.Whofore = 3
2181 }
2182
2183 // back to markdown
2184 honk.Noise = noise
2185
2186 if r.FormValue("preview") == "preview" {
2187 honks := []*Honk{honk}
2188 reverbolate(user.ID, honks)
2189 templinfo := getInfo(r)
2190 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
2191 templinfo["Honks"] = honks
2192 templinfo["MapLink"] = getmaplink(u)
2193 templinfo["InReplyTo"] = r.FormValue("rid")
2194 templinfo["Noise"] = r.FormValue("noise")
2195 templinfo["Onties"] = honk.Onties
2196 templinfo["SeeAlso"] = honk.SeeAlso
2197 templinfo["Link"] = honk.Link
2198 templinfo["LegalName"] = honk.LegalName
2199 templinfo["SavedFile"] = donkxid
2200 if tm := honk.Time; tm != nil {
2201 templinfo["ShowTime"] = " "
2202 templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04")
2203 if tm.Duration != 0 {
2204 templinfo["Duration"] = tm.Duration
2205 }
2206 }
2207 templinfo["IsPreview"] = true
2208 templinfo["UpdateXID"] = updatexid
2209 templinfo["ServerMessage"] = "honk preview"
2210 err := readviews.Execute(w, "honkpage.html", templinfo)
2211 if err != nil {
2212 elog.Print(err)
2213 }
2214 return nil
2215 }
2216
2217 if updatexid != "" {
2218 updatehonk(honk)
2219 oldjonks.Clear(honk.XID)
2220 } else {
2221 err := savehonk(honk)
2222 if err != nil {
2223 elog.Printf("uh oh")
2224 return nil
2225 }
2226 }
2227
2228 // reload for consistency
2229 honk.Donks = nil
2230 donksforhonks([]*Honk{honk})
2231
2232 go honkworldwide(user, honk)
2233
2234 return honk
2235}
2236
2237func firstRune(s string) rune { r, _ := utf8.DecodeRuneInString(s); return r }
2238
2239func showhonkers(w http.ResponseWriter, r *http.Request) {
2240 userid := UserID(login.GetUserInfo(r).UserID)
2241 honkers := gethonkers(userid)
2242 letters := make([]string, 0, 64)
2243 var lastrune rune = -1
2244 for _, h := range honkers {
2245 if r := firstRune(h.Name); r != lastrune {
2246 letters = append(letters, string(r))
2247 lastrune = r
2248 }
2249 }
2250 templinfo := getInfo(r)
2251 templinfo["FirstRune"] = firstRune
2252 templinfo["Letters"] = letters
2253 templinfo["Honkers"] = honkers
2254 templinfo["HonkerCSRF"] = login.GetCSRF("submithonker", r)
2255 err := readviews.Execute(w, "honkers.html", templinfo)
2256 if err != nil {
2257 elog.Print(err)
2258 }
2259}
2260
2261func showchatter(w http.ResponseWriter, r *http.Request) {
2262 u := login.GetUserInfo(r)
2263 chatnewnone(UserID(u.UserID))
2264 chatter := loadchatter(UserID(u.UserID))
2265 for _, chat := range chatter {
2266 for _, ch := range chat.Chonks {
2267 filterchonk(ch)
2268 }
2269 }
2270
2271 templinfo := getInfo(r)
2272 templinfo["Chatter"] = chatter
2273 templinfo["ChonkCSRF"] = login.GetCSRF("sendchonk", r)
2274 err := readviews.Execute(w, "chatter.html", templinfo)
2275 if err != nil {
2276 elog.Print(err)
2277 }
2278}
2279
2280func submitchonk(w http.ResponseWriter, r *http.Request) {
2281 u := login.GetUserInfo(r)
2282 user, _ := butwhatabout(u.Username)
2283 noise := r.FormValue("noise")
2284 target := r.FormValue("target")
2285 format := "markdown"
2286 dt := time.Now().UTC()
2287 xid := fmt.Sprintf("%s/%s/%s", user.URL, "chonk", xfiltrate())
2288
2289 if !strings.HasPrefix(target, "https://") {
2290 target = fullname(target, user.ID)
2291 }
2292 if target == "" {
2293 http.Error(w, "who is that?", http.StatusInternalServerError)
2294 return
2295 }
2296 ch := Chonk{
2297 UserID: user.ID,
2298 XID: xid,
2299 Who: user.URL,
2300 Target: target,
2301 Date: dt,
2302 Noise: noise,
2303 Format: format,
2304 }
2305 donks, err := submitdonk(w, r)
2306 if err != nil && err != http.ErrMissingFile {
2307 return
2308 }
2309 if len(donks) > 0 {
2310 ch.Donks = append(ch.Donks, donks...)
2311 }
2312
2313 translatechonk(&ch)
2314 savechonk(&ch)
2315 // reload for consistency
2316 ch.Donks = nil
2317 donksforchonks([]*Chonk{&ch})
2318 go sendchonk(user, &ch)
2319
2320 http.Redirect(w, r, "/chatter", http.StatusSeeOther)
2321}
2322
2323var combocache = gencache.New(gencache.Options[UserID, []string]{Fill: func(userid UserID) ([]string, bool) {
2324 honkers := gethonkers(userid)
2325 combos := make([]string, 0, len(honkers))
2326 for _, h := range honkers {
2327 combos = append(combos, h.Combos...)
2328 }
2329 for i, c := range combos {
2330 if c == "-" {
2331 combos[i] = ""
2332 }
2333 }
2334 combos = oneofakind(combos)
2335 sort.Strings(combos)
2336 return combos, true
2337}, Invalidator: &honkerinvalidator})
2338
2339func showcombos(w http.ResponseWriter, r *http.Request) {
2340 templinfo := getInfo(r)
2341 err := readviews.Execute(w, "combos.html", templinfo)
2342 if err != nil {
2343 elog.Print(err)
2344 }
2345}
2346
2347func websubmithonker(w http.ResponseWriter, r *http.Request) {
2348 h := submithonker(w, r)
2349 if h == nil {
2350 return
2351 }
2352 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
2353}
2354
2355func submithonker(w http.ResponseWriter, r *http.Request) *Honker {
2356 u := login.GetUserInfo(r)
2357 user, _ := butwhatabout(u.Username)
2358 name := strings.TrimSpace(r.FormValue("name"))
2359 url := strings.TrimSpace(r.FormValue("url"))
2360 peep := r.FormValue("peep")
2361 combos := strings.TrimSpace(r.FormValue("combos"))
2362 combos = " " + combos + " "
2363 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
2364
2365 re_namecheck := regexp.MustCompile("^[\\pL[:digit:]_.-]+$")
2366 if name != "" && !re_namecheck.MatchString(name) {
2367 http.Error(w, "please use a plainer name", http.StatusInternalServerError)
2368 return nil
2369 }
2370
2371 var meta HonkerMeta
2372 meta.Notes = strings.TrimSpace(r.FormValue("notes"))
2373 mj, _ := jsonify(&meta)
2374
2375 defer honkerinvalidator.Clear(user.ID)
2376
2377 // mostly dummy, fill in later...
2378 h := &Honker{
2379 ID: honkerid,
2380 }
2381
2382 if honkerid > 0 {
2383 if r.FormValue("delete") == "delete" {
2384 unfollowyou(user, honkerid, false)
2385 stmtDeleteHonker.Exec(honkerid)
2386 return h
2387 }
2388 if r.FormValue("unsub") == "unsub" {
2389 unfollowyou(user, honkerid, false)
2390 }
2391 if r.FormValue("sub") == "sub" {
2392 followyou(user, honkerid, false)
2393 }
2394 _, err := stmtUpdateHonker.Exec(name, combos, mj, honkerid, user.ID)
2395 if err != nil {
2396 elog.Printf("update honker err: %s", err)
2397 return nil
2398 }
2399 return h
2400 }
2401
2402 if url == "" {
2403 http.Error(w, "subscribing to nothing?", http.StatusInternalServerError)
2404 return nil
2405 }
2406
2407 flavor := "presub"
2408 if peep == "peep" {
2409 flavor = "peep"
2410 }
2411
2412 var err error
2413 honkerid, flavor, err = savehonker(user, url, name, flavor, combos, mj)
2414 if err != nil {
2415 http.Error(w, "had some trouble with that: "+err.Error(), http.StatusInternalServerError)
2416 return nil
2417 }
2418 if flavor == "presub" {
2419 followyou(user, honkerid, false)
2420 }
2421 h.ID = honkerid
2422 return h
2423}
2424
2425func hfcspage(w http.ResponseWriter, r *http.Request) {
2426 u := login.GetUserInfo(r)
2427
2428 filters := getfilters(UserID(u.UserID), filtAny)
2429
2430 templinfo := getInfo(r)
2431 templinfo["Filters"] = filters
2432 templinfo["FilterCSRF"] = login.GetCSRF("filter", r)
2433 err := readviews.Execute(w, "hfcs.html", templinfo)
2434 if err != nil {
2435 elog.Print(err)
2436 }
2437}
2438
2439func accountpage(w http.ResponseWriter, r *http.Request) {
2440 u := login.GetUserInfo(r)
2441 user, _ := butwhatabout(u.Username)
2442 templinfo := getInfo(r)
2443 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
2444 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
2445 templinfo["User"] = user
2446 about := user.About
2447 if ava := user.Options.Avatar; ava != "" {
2448 about += "\n\navatar: " + ava[strings.LastIndexByte(ava, '/')+1:]
2449 }
2450 if ban := user.Options.Banner; ban != "" {
2451 about += "\n\nbanner: " + ban[strings.LastIndexByte(ban, '/')+1:]
2452 }
2453 templinfo["WhatAbout"] = about
2454 err := readviews.Execute(w, "account.html", templinfo)
2455 if err != nil {
2456 elog.Print(err)
2457 }
2458}
2459
2460func dochpass(w http.ResponseWriter, r *http.Request) {
2461 err := login.ChangePassword(w, r)
2462 if err != nil {
2463 elog.Printf("error changing password: %s", err)
2464 }
2465 http.Redirect(w, r, "/account", http.StatusSeeOther)
2466}
2467
2468var oldfingers = gencache.New(gencache.Options[string, []byte]{Fill: func(orig string) ([]byte, bool) {
2469 if strings.HasPrefix(orig, "acct:") {
2470 orig = orig[5:]
2471 } else {
2472 orig, _ = url.QueryUnescape(orig)
2473 }
2474
2475 name := orig
2476 idx := strings.LastIndexByte(name, '/')
2477 if idx != -1 {
2478 name = name[idx+1:]
2479 url := fmt.Sprintf("https://%s/%s/%s", serverName, "u", name)
2480 if strings.HasPrefix(name, "@") {
2481 url = fmt.Sprintf("https://%s/%s", serverName, name)
2482 name = name[1:]
2483 }
2484 log.Printf("orig: %s url: %s\n", orig, url)
2485 if url != orig {
2486 ilog.Printf("foreign request rejected")
2487 name = ""
2488 }
2489 } else {
2490 idx = strings.IndexByte(name, '@')
2491 if idx != -1 {
2492 name = name[:idx]
2493 if !(name+"@"+serverName == orig || name+"@"+masqName == orig) {
2494 ilog.Printf("foreign request rejected")
2495 name = ""
2496 }
2497 }
2498 }
2499 user, err := butwhatabout(name)
2500 if err != nil {
2501 return nil, false
2502 }
2503
2504 j := junk.New()
2505 pretty := fmt.Sprintf("https://%s/@%s", serverName, name)
2506 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, masqName)
2507 j["aliases"] = []string{pretty, user.URL}
2508 l := junk.New()
2509 l["rel"] = "self"
2510 l["type"] = `application/activity+json`
2511 l["href"] = user.URL
2512 j["links"] = []junk.Junk{l}
2513 l2 := junk.New()
2514 l2["rel"] = "http://webfinger.net/rel/profile-page"
2515 l2["type"] = "text/html"
2516 l2["href"] = pretty
2517 l3 := junk.New()
2518 l3["rel"] = "http://webfinger.net/rel/avatar"
2519
2520 ext := filepath.Ext(user.Options.Avatar)
2521 if ext[1:] == "jpg" {
2522 l3["type"] = "image/jpeg"
2523 } else if ext[1:] == "png" {
2524 l3["type"] = "image/png"
2525 }
2526 l3["href"] = user.Options.Avatar
2527 j["links"] = []junk.Junk{l, l2, l3}
2528
2529 return j.ToBytes(), true
2530}})
2531
2532func fingerlicker(w http.ResponseWriter, r *http.Request) {
2533 orig := r.FormValue("resource")
2534
2535 dlog.Printf("finger lick: %s", orig)
2536
2537 j, ok := oldfingers.Get(orig)
2538 if ok {
2539 w.Header().Set("Content-Type", "application/jrd+json")
2540 w.Write(j)
2541 } else {
2542 http.NotFound(w, r)
2543 }
2544}
2545
2546func knowninformation(w http.ResponseWriter, r *http.Request) {
2547 j := junk.New()
2548 l := junk.New()
2549
2550 l["rel"] = `http://nodeinfo.diaspora.software/ns/schema/2.0`
2551 l["href"] = fmt.Sprintf("https://%s/nodeinfo/2.0", serverName)
2552 j["links"] = []junk.Junk{l}
2553
2554 w.Header().Set("Content-Type", "application/json")
2555 j.Write(w)
2556}
2557
2558func actualinformation(w http.ResponseWriter, r *http.Request) {
2559 j := junk.New()
2560
2561 soft := junk.New()
2562 soft["name"] = "honk"
2563 soft["version"] = softwareVersion
2564
2565 services := junk.New()
2566 services["inbound"] = []string{}
2567 services["outbound"] = []string{"rss2.0"}
2568
2569 users := junk.New()
2570 users["total"] = getusercount()
2571 users["activeHalfyear"] = getactiveusercount(6)
2572 users["activeMonth"] = getactiveusercount(1)
2573
2574 usage := junk.New()
2575 usage["users"] = users
2576 usage["localPosts"] = getlocalhonkcount()
2577
2578 j["version"] = "2.0"
2579 j["protocols"] = []string{"activitypub"}
2580 j["software"] = soft
2581 j["services"] = services
2582 j["openRegistrations"] = false
2583 j["usage"] = usage
2584
2585 w.Header().Set("Content-Type", "application/json")
2586 j.Write(w)
2587}
2588
2589func somedays() string {
2590 secs := 432000 + notrand.Int63n(432000)
2591 return fmt.Sprintf("%d", secs)
2592}
2593
2594func isurl(s string) bool {
2595 u, err := url.Parse(s)
2596 return err == nil && u.Scheme != "" && u.Host != ""
2597}
2598
2599func avatateautogen(n string) []byte {
2600 return genAvatar(n)
2601}
2602
2603func avatate(w http.ResponseWriter, r *http.Request) {
2604 if develMode {
2605 loadAvatarColors()
2606 }
2607 var a []byte
2608 n := r.FormValue("a")
2609 uinfo := login.GetUserInfo(r)
2610 a = avatator(n, uinfo)
2611
2612 if !develMode {
2613 w.Header().Set("Cache-Control", "max-age="+somedays())
2614 }
2615
2616 w.Write(a)
2617}
2618
2619func serveviewasset(w http.ResponseWriter, r *http.Request) {
2620 serveasset(w, r, viewDir)
2621}
2622func servedataasset(w http.ResponseWriter, r *http.Request) {
2623 serveasset(w, r, dataDir)
2624}
2625
2626func serveasset(w http.ResponseWriter, r *http.Request, basedir string) {
2627 if !develMode {
2628 w.Header().Set("Cache-Control", "max-age=7776000")
2629 }
2630 http.ServeFile(w, r, basedir+"/views"+r.URL.Path)
2631}
2632func servehelp(w http.ResponseWriter, r *http.Request) {
2633 name := mux.Vars(r)["name"]
2634 if !develMode {
2635 w.Header().Set("Cache-Control", "max-age=3600")
2636 }
2637 http.ServeFile(w, r, viewDir+"/docs/"+name)
2638}
2639func servehtml(w http.ResponseWriter, r *http.Request) {
2640 u := login.GetUserInfo(r)
2641 templinfo := getInfo(r)
2642 templinfo["AboutMsg"] = aboutMsg
2643 templinfo["LoginMsg"] = loginMsg
2644 templinfo["HonkVersion"] = softwareVersion
2645 if r.URL.Path == "/about" {
2646 templinfo["Sensors"] = getSensors()
2647 }
2648 if u == nil && !develMode {
2649 w.Header().Set("Cache-Control", "max-age=60")
2650 }
2651 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
2652 if err != nil {
2653 elog.Print(err)
2654 }
2655}
2656func serveemu(w http.ResponseWriter, r *http.Request) {
2657 emu := mux.Vars(r)["emu"]
2658
2659 w.Header().Set("Cache-Control", "max-age="+somedays())
2660 http.ServeFile(w, r, dataDir+"/emus/"+emu)
2661}
2662func servememe(w http.ResponseWriter, r *http.Request) {
2663 meme := mux.Vars(r)["meme"]
2664
2665 w.Header().Set("Cache-Control", "max-age="+somedays())
2666 _, err := os.Stat(dataDir + "/memes/" + meme)
2667 if err == nil {
2668 http.ServeFile(w, r, dataDir+"/memes/"+meme)
2669 } else {
2670 mux.Vars(r)["xid"] = meme
2671 servefile(w, r)
2672 }
2673}
2674
2675func refetchfile(xid string) ([]byte, error) {
2676 donk := getfileinfo(xid)
2677 if donk == nil {
2678 return nil, errors.New("filemeta not found")
2679 }
2680 dlog.Printf("refetching missing file data %s", donk.URL)
2681 return fetchsome(donk.URL)
2682}
2683
2684func servefile(w http.ResponseWriter, r *http.Request) {
2685 if friendorfoe(r.Header.Get("Accept")) {
2686 dlog.Printf("incompatible accept for donk")
2687 http.Error(w, "there are no activities here", http.StatusNotAcceptable)
2688 return
2689 }
2690 xid := mux.Vars(r)["xid"]
2691 preview := r.FormValue("preview") == "1"
2692 var media string
2693 var data sql.RawBytes
2694 rows, err := stmtGetFileData.Query(xid)
2695 if err == nil {
2696 defer rows.Close()
2697 rows.Next()
2698 err = rows.Scan(&media, &data)
2699 }
2700 if err != nil {
2701 elog.Printf("error loading file: %s", err)
2702 http.NotFound(w, r)
2703 return
2704 }
2705 if preview && strings.HasPrefix(media, "image") {
2706 img, err := lilshrink(data)
2707 if err == nil {
2708 data = img.Data
2709 }
2710 }
2711 w.Header().Set("Content-Type", media)
2712 w.Header().Set("X-Content-Type-Options", "nosniff")
2713 w.Header().Set("Cache-Control", "max-age="+somedays())
2714 w.Write(data)
2715}
2716
2717func nomoroboto(w http.ResponseWriter, r *http.Request) {
2718 io.WriteString(w, "User-agent: *\n")
2719 io.WriteString(w, "Disallow: /a\n")
2720 io.WriteString(w, "Disallow: /d/\n")
2721 io.WriteString(w, "Disallow: /meme/\n")
2722 io.WriteString(w, "Disallow: /o\n")
2723 io.WriteString(w, "Disallow: /o/\n")
2724 io.WriteString(w, "Disallow: /help/\n")
2725 for _, u := range allusers() {
2726 fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
2727 }
2728}
2729
2730type Hydration struct {
2731 Tophid int64
2732 Srvmsg template.HTML
2733 Honks string
2734 MeCount int64
2735 ChatCount int64
2736 Poses []int
2737}
2738
2739func webhydra(w http.ResponseWriter, r *http.Request) {
2740 u := login.GetUserInfo(r)
2741 userid := UserID(u.UserID)
2742 templinfo := getInfo(r)
2743 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
2744 page := r.FormValue("page")
2745
2746 wanted, _ := strconv.ParseInt(r.FormValue("tophid"), 10, 0)
2747
2748 var hydra Hydration
2749
2750 var honks []*Honk
2751 switch page {
2752 case "atme":
2753 honks = gethonksforme(userid, wanted)
2754 honks = osmosis(honks, userid, false)
2755 menewnone(userid)
2756 hydra.Srvmsg = "at me!"
2757 case "longago":
2758 honks = gethonksfromlongago(userid, wanted)
2759 honks = osmosis(honks, userid, false)
2760 hydra.Srvmsg = "from long ago"
2761 case "home":
2762 honks = gethonksforuser(userid, wanted)
2763 honks = osmosis(honks, userid, true)
2764 hydra.Srvmsg = serverMsg
2765 case "first":
2766 honks = gethonksforuserfirstclass(userid, wanted)
2767 honks = osmosis(honks, userid, true)
2768 hydra.Srvmsg = "first class only"
2769 case "saved":
2770 honks = getsavedhonks(userid, wanted)
2771 templinfo["PageName"] = "saved"
2772 hydra.Srvmsg = "saved honks"
2773 case "combo":
2774 c := r.FormValue("c")
2775 honks = gethonksbycombo(userid, c, wanted)
2776 honks = osmosis(honks, userid, false)
2777 hydra.Srvmsg = templates.Sprintf("honks by combo: %s", c)
2778 case "convoy":
2779 c := r.FormValue("c")
2780 honks = gethonksbyconvoy(userid, c, 0)
2781 honks = osmosis(honks, userid, false)
2782 honks = threadsort(honks)
2783 honks, hydra.Poses = threadposes(honks, wanted)
2784 hydra.Srvmsg = templates.Sprintf("honks in convoy: %s", c)
2785 case "honker":
2786 xid := r.FormValue("xid")
2787 honks = gethonksbyxonker(userid, xid, wanted)
2788 miniform := templates.Sprintf(`<form action="/submithonker" method="POST">
2789 <input type="hidden" name="CSRF" value="%s">
2790 <input type="hidden" name="url" value="%s">
2791 <button tabindex=1 name="add honker" value="add honker">add honker</button>
2792 </form>`, login.GetCSRF("submithonker", r), xid)
2793 msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, xid, xid, miniform)
2794 hydra.Srvmsg = msg
2795 case "user":
2796 uname := r.FormValue("uname")
2797 honks = gethonksbyuser(uname, u != nil && u.Username == uname, wanted)
2798 hydra.Srvmsg = templates.Sprintf("honks by user: %s", uname)
2799 default:
2800 http.NotFound(w, r)
2801 }
2802
2803 if len(honks) > 0 {
2804 if page == "convoy" {
2805 hydra.Tophid = honks[len(honks)-1].ID
2806 } else {
2807 hydra.Tophid = honks[0].ID
2808 }
2809 } else {
2810 hydra.Tophid = wanted
2811 }
2812 reverbolate(userid, honks)
2813
2814 user, _ := butwhatabout(u.Username)
2815
2816 var buf strings.Builder
2817 templinfo["Honks"] = honks
2818 templinfo["MapLink"] = getmaplink(u)
2819 templinfo["User"], _ = butwhatabout(u.Username)
2820 err := readviews.Execute(&buf, "honkfrags.html", templinfo)
2821 if err != nil {
2822 elog.Printf("frag error: %s", err)
2823 return
2824 }
2825 hydra.Honks = buf.String()
2826 hydra.MeCount = user.Options.MeCount
2827 hydra.ChatCount = user.Options.ChatCount
2828 w.Header().Set("Content-Type", "application/json")
2829 j, _ := jsonify(&hydra)
2830 io.WriteString(w, j)
2831}
2832
2833var honkline = make(chan bool)
2834
2835func honkhonkline() {
2836 for {
2837 select {
2838 case honkline <- true:
2839 default:
2840 return
2841 }
2842 }
2843}
2844
2845func apihandler(w http.ResponseWriter, r *http.Request) {
2846 u := login.GetUserInfo(r)
2847 userid := UserID(u.UserID)
2848 action := r.FormValue("action")
2849 wait, _ := strconv.ParseInt(r.FormValue("wait"), 10, 0)
2850 dlog.Printf("api request '%s' on behalf of %s", action, u.Username)
2851 switch action {
2852 case "honk":
2853 h := submithonk(w, r)
2854 if h == nil {
2855 return
2856 }
2857
2858 fmt.Fprintf(w, "%s", h.XID)
2859 case "donk":
2860 donks, err := submitdonk(w, r)
2861 if err != nil {
2862 http.Error(w, err.Error(), http.StatusBadRequest)
2863 return
2864 }
2865 if len(donks) == 0 {
2866 http.Error(w, "missing donk", http.StatusBadRequest)
2867 return
2868 }
2869 d := donks[0]
2870 donkxid := fmt.Sprintf("%s:%d", d.XID, d.FileID)
2871 w.Write([]byte(donkxid))
2872 case "zonkit":
2873 zonkit(w, r)
2874 case "gethonks":
2875 var honks []*Honk
2876 wanted, _ := strconv.ParseInt(r.FormValue("after"), 10, 0)
2877 page := r.FormValue("page")
2878 var waitchan <-chan time.Time
2879 requery:
2880 switch page {
2881 case "atme":
2882 honks = gethonksforme(userid, wanted)
2883 honks = osmosis(honks, userid, false)
2884 menewnone(userid)
2885 case "longago":
2886 honks = gethonksfromlongago(userid, wanted)
2887 honks = osmosis(honks, userid, false)
2888 case "home":
2889 honks = gethonksforuser(userid, wanted)
2890 honks = osmosis(honks, userid, true)
2891 case "myhonks":
2892 honks = gethonksbyuser(u.Username, true, wanted)
2893 honks = osmosis(honks, userid, true)
2894 case "saved":
2895 honks = getsavedhonks(userid, wanted)
2896 case "combo":
2897 c := r.FormValue("c")
2898 honks = gethonksbycombo(userid, c, wanted)
2899 honks = osmosis(honks, userid, false)
2900 case "convoy":
2901 c := r.FormValue("c")
2902 honks = gethonksbyconvoy(userid, c, 0)
2903 honks = osmosis(honks, userid, false)
2904 honks = threadsort(honks)
2905 honks, _ = threadposes(honks, wanted)
2906 case "honker":
2907 xid := r.FormValue("xid")
2908 honks = gethonksbyxonker(userid, xid, wanted)
2909 case "search":
2910 q := r.FormValue("q")
2911 honks = gethonksbysearch(userid, q, wanted)
2912 default:
2913 http.Error(w, "unknown page", http.StatusNotFound)
2914 return
2915 }
2916 if len(honks) == 0 && wait > 0 {
2917 if waitchan == nil {
2918 waitchan = time.After(time.Duration(wait) * time.Second)
2919 }
2920 select {
2921 case <-honkline:
2922 goto requery
2923 case <-waitchan:
2924 }
2925 }
2926 reverbolate(userid, honks)
2927 user, _ := butwhatabout(u.Username)
2928 j := junk.New()
2929 j["honks"] = honks
2930 j["mecount"] = user.Options.MeCount
2931 j["chatcount"] = user.Options.ChatCount
2932 j.Write(w)
2933 case "sendactivity":
2934 public := r.FormValue("public") == "1"
2935 user, _ := butwhatabout(u.Username)
2936 rcpts := boxuprcpts(user, r.Form["rcpt"], public)
2937 msg := []byte(r.FormValue("msg"))
2938 for rcpt := range rcpts {
2939 go deliverate(userid, rcpt, msg)
2940 }
2941 case "gethonkers":
2942 j := junk.New()
2943 j["honkers"] = gethonkers(userid)
2944 j.Write(w)
2945 case "savehonker":
2946 h := submithonker(w, r)
2947 if h == nil {
2948 return
2949 }
2950 fmt.Fprintf(w, "%d", h.ID)
2951 default:
2952 http.Error(w, "unknown action", http.StatusNotFound)
2953 return
2954 }
2955}
2956
2957func fiveoh(w http.ResponseWriter, r *http.Request) {
2958 if !develMode {
2959 return
2960 }
2961 fd, err := os.OpenFile("violations.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
2962 if err != nil {
2963 elog.Printf("error opening violations! %s", err)
2964 return
2965 }
2966 defer fd.Close()
2967 io.Copy(fd, r.Body)
2968 fd.WriteString("\n")
2969}
2970
2971var endoftheworld = make(chan bool)
2972var readyalready = make(chan bool)
2973var workinprogress = 0
2974var requestWG sync.WaitGroup
2975var listenSocket net.Listener
2976
2977func enditall() {
2978 sig := make(chan os.Signal, 1)
2979 signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
2980 <-sig
2981 ilog.Printf("stopping...")
2982 listenSocket.Close()
2983 for i := 0; i < workinprogress; i++ {
2984 endoftheworld <- true
2985 }
2986 if *cpuprofile != "" {
2987 pprof.StopCPUProfile()
2988 }
2989 if *memprofile != "" {
2990 pprof.WriteHeapProfile(memprofilefd)
2991 memprofilefd.Close()
2992 }
2993 ilog.Printf("waiting...")
2994 go func() {
2995 time.Sleep(10 * time.Second)
2996 elog.Printf("timed out waiting for requests to finish")
2997 os.Exit(0)
2998 }()
2999 for i := 0; i < workinprogress; i++ {
3000 <-readyalready
3001 }
3002 requestWG.Wait()
3003 ilog.Printf("apocalypse")
3004 closedatabases()
3005 os.Exit(0)
3006}
3007
3008func bgmonitor() {
3009 for {
3010 when := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat)
3011 _, err := stmtDeleteOldXonkers.Exec("pubkey", when)
3012 if err != nil {
3013 elog.Printf("error deleting old xonkers: %s", err)
3014 }
3015 zaggies.Flush()
3016 time.Sleep(50 * time.Minute)
3017 }
3018}
3019
3020func addcspheaders(next http.Handler) http.Handler {
3021 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3022 requestWG.Add(1)
3023 defer requestWG.Done()
3024 policy := "default-src 'none'; script-src 'self'; connect-src 'self'; style-src 'self'; img-src 'self'; media-src 'self'"
3025 if develMode {
3026 policy += "; report-uri /csp-violation"
3027 }
3028 w.Header().Set("Content-Security-Policy", policy)
3029 next.ServeHTTP(w, r)
3030 })
3031}
3032
3033func emuinit() {
3034 var emunames []string
3035 dir, err := os.Open(dataDir + "/emus")
3036 if err == nil {
3037 emunames, _ = dir.Readdirnames(0)
3038 dir.Close()
3039 }
3040 allemus = make([]Emu, 0, len(emunames))
3041 for _, e := range emunames {
3042 if len(e) <= 4 {
3043 continue
3044 }
3045 ext := e[len(e)-4:]
3046 emu := Emu{
3047 ID: fmt.Sprintf("/emu/%s", e),
3048 Name: e[:len(e)-4],
3049 Type: "image/" + ext[1:],
3050 }
3051 allemus = append(allemus, emu)
3052 }
3053 sort.Slice(allemus, func(i, j int) bool {
3054 return allemus[i].Name < allemus[j].Name
3055 })
3056}
3057
3058func redirectPretty(w http.ResponseWriter, r *http.Request) {
3059 last := path.Base(r.URL.Path)
3060 name := mux.Vars(r)["name"]
3061 aturl := "/@" + name
3062
3063 if last == name {
3064 last = ""
3065 }
3066 http.Redirect(w, r, path.Join(aturl, last), http.StatusMovedPermanently)
3067}
3068
3069var savedassetparams = make(map[string]string)
3070
3071func getassetparam(file string) string {
3072 if p, ok := savedassetparams[file]; ok {
3073 return p
3074 }
3075 data, err := os.ReadFile(file)
3076 if err != nil {
3077 return ""
3078 }
3079 hasher := sha512.New()
3080 hasher.Write(data)
3081
3082 return fmt.Sprintf("?v=%.8x", hasher.Sum(nil))
3083}
3084
3085func startWatcher() {
3086 watcher, err := gonix.NewWatcher()
3087 if err != nil {
3088 return
3089 }
3090 go func() {
3091 s := dataDir + "/views/local.css"
3092 for {
3093 err := watcher.WatchFile(s)
3094 if err != nil {
3095 break
3096 }
3097 err = watcher.WaitForChange()
3098 if err != nil {
3099 dlog.Printf("can't wait: %s", err)
3100 break
3101 }
3102 dlog.Printf("local.css changed")
3103 delete(savedassetparams, s)
3104 savedassetparams[s] = getassetparam(s)
3105 }
3106 }()
3107}
3108
3109var usefcgi bool
3110
3111func serve() {
3112 db := opendatabase()
3113 login.Init(login.InitArgs{Db: db, Logger: ilog, Insecure: develMode, SameSiteStrict: !develMode})
3114
3115 listener, err := openListener()
3116 if err != nil {
3117 elog.Fatal(err)
3118 }
3119 runBackendServer()
3120 go enditall()
3121 go redeliverator()
3122 go tracker()
3123 go syndicator()
3124 go bgmonitor()
3125 go qotd()
3126 loadLingo()
3127 emuinit()
3128
3129 var toload []string
3130 dents, _ := os.ReadDir(viewDir + "/views")
3131 for _, dent := range dents {
3132 name := dent.Name()
3133 if strings.HasSuffix(name, ".html") {
3134 toload = append(toload, viewDir+"/views/"+name)
3135 }
3136 }
3137
3138 readviews = templates.Load(develMode, toload...)
3139 if !develMode {
3140 assets := []string{
3141 viewDir + "/views/style.css",
3142 dataDir + "/views/local.css",
3143 viewDir + "/views/honkpage.js",
3144 viewDir + "/views/mecount.js",
3145 viewDir + "/views/misc.js",
3146 dataDir + "/views/local.js",
3147 viewDir + "/views/manifest.webmanifest",
3148 viewDir + "/views/sw.js",
3149 }
3150 for _, s := range assets {
3151 savedassetparams[s] = getassetparam(s)
3152 }
3153 loadAvatarColors()
3154 }
3155 startWatcher()
3156
3157 securitizeweb()
3158
3159 mux := mux.NewRouter()
3160 mux.Use(login.Checker)
3161
3162 mux.Handle("/api", login.TokenRequired(http.HandlerFunc(apihandler)))
3163
3164 posters := mux.Methods("POST").Subrouter()
3165 getters := mux.Methods("GET").Subrouter()
3166
3167 getters.HandleFunc("/", homepage)
3168 getters.HandleFunc("/home", homepage)
3169 getters.HandleFunc("/front", homepage)
3170 getters.HandleFunc("/events", homepage)
3171 getters.HandleFunc("/robots.txt", nomoroboto)
3172 getters.HandleFunc("/rss", showrss)
3173 getters.HandleFunc("/@{name:[\\pL[:digit:]]+}", showuser)
3174 getters.HandleFunc("/@{name:[\\pL[:digit:]]+}.json", showuser)
3175 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}.json", showuser)
3176 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}", redirectPretty)
3177 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}", showonehonk)
3178 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}.json", showonehonk)
3179 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/rss", showrss)
3180 posters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", postinbox)
3181 getters.Handle("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", login.TokenRequired(http.HandlerFunc(getinbox)))
3182 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", getoutbox)
3183 posters.Handle("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", login.TokenRequired(http.HandlerFunc(postoutbox)))
3184 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/followers", emptiness)
3185 getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/following", emptiness)
3186 getters.HandleFunc("/a", avatate)
3187 getters.HandleFunc("/o", thelistingoftheontologies)
3188 getters.HandleFunc("/o/{name:.+}", showontology)
3189 getters.HandleFunc("/d/{xid:[\\pL[:digit:].]+}", servefile)
3190 getters.HandleFunc("/emu/{emu:[^.]*[^/]+}", serveemu)
3191 getters.HandleFunc("/meme/{meme:[^.]*[^/]+}", servememe)
3192 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
3193 getters.HandleFunc("/.well-known/nodeinfo", knowninformation)
3194 getters.HandleFunc("/nodeinfo/2.0", actualinformation)
3195
3196 getters.HandleFunc("/flag/{code:.+}", showflag)
3197
3198 getters.HandleFunc("/server", serveractor)
3199 posters.HandleFunc("/server/inbox", serverinbox)
3200 posters.HandleFunc("/inbox", serverinbox)
3201
3202 posters.HandleFunc("/csp-violation", fiveoh)
3203
3204 getters.HandleFunc("/style.css", serveviewasset)
3205 getters.HandleFunc("/sw.js", serveviewasset)
3206 getters.HandleFunc("/honkpage.js", serveviewasset)
3207 getters.HandleFunc("/mecount.js", serveviewasset)
3208 getters.HandleFunc("/misc.js", serveviewasset)
3209 getters.HandleFunc("/local.css", servedataasset)
3210 getters.HandleFunc("/local.js", servedataasset)
3211 getters.HandleFunc("/icon.png", servedataasset)
3212 getters.HandleFunc("/favicon.ico", servedataasset)
3213 getters.HandleFunc("/apple-touch-icon.png", servedataasset)
3214 getters.HandleFunc("/manifest.webmanifest", serveviewasset)
3215
3216 getters.HandleFunc("/about", servehtml)
3217 getters.HandleFunc("/login", servehtml)
3218 posters.HandleFunc("/dologin", login.LoginFunc)
3219 getters.HandleFunc("/logout", login.LogoutFunc)
3220 getters.HandleFunc("/help/{name:[\\pL[:digit:]_.-]+}", servehelp)
3221
3222 loggedin := mux.NewRoute().Subrouter()
3223 loggedin.Use(login.Required)
3224 loggedin.HandleFunc("/first", homepage)
3225 loggedin.HandleFunc("/chatter", showchatter)
3226 loggedin.Handle("/sendchonk", login.CSRFWrap("sendchonk", http.HandlerFunc(submitchonk)))
3227 loggedin.HandleFunc("/saved", homepage)
3228 loggedin.HandleFunc("/account", accountpage)
3229 loggedin.HandleFunc("/funzone", showfunzone)
3230 loggedin.HandleFunc("/chpass", dochpass)
3231 loggedin.HandleFunc("/atme", homepage)
3232 loggedin.HandleFunc("/longago", homepage)
3233 loggedin.HandleFunc("/hfcs", hfcspage)
3234 loggedin.HandleFunc("/xzone", xzone)
3235 loggedin.HandleFunc("/newhonk", newhonkpage)
3236 loggedin.HandleFunc("/edit", edithonkpage)
3237 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(websubmithonk)))
3238 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(submitbonk)))
3239 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
3240 loggedin.Handle("/savehfcs", login.CSRFWrap("filter", http.HandlerFunc(savehfcs)))
3241 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
3242 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
3243 loggedin.HandleFunc("/honkers", showhonkers)
3244 loggedin.HandleFunc("/h/{name:[\\pL[:digit:]_.-]+}", showhonker)
3245 loggedin.HandleFunc("/h", showhonker)
3246 loggedin.HandleFunc("/c/{name:[\\pL[:digit:]#_.-]+}", showcombo)
3247 loggedin.HandleFunc("/c", showcombos)
3248 loggedin.HandleFunc("/t", showconvoy)
3249 loggedin.HandleFunc("/q", showsearch)
3250 loggedin.HandleFunc("/hydra", webhydra)
3251 loggedin.HandleFunc("/emus", showemus)
3252 loggedin.Handle("/submithonker", login.CSRFWrap("submithonker", http.HandlerFunc(websubmithonker)))
3253
3254 // mastoshit
3255 mastopost := mux.Methods("POST").Subrouter()
3256 mastoget := mux.Methods("GET").Subrouter()
3257
3258 mastoget.HandleFunc("/oauth/authorize", showoauthlogin)
3259 mastopost.HandleFunc("/oauth/authorize", oauthorize)
3260 mastopost.HandleFunc("/oauth/token", oauthtoken)
3261 mastoget.HandleFunc("/api/v1/instance", instance)
3262 mastopost.HandleFunc("/api/v1/apps", apiapps)
3263 mastoget.HandleFunc("/api/v1/accounts/verify_credentials", checktoken(verifycreds))
3264 mastoget.HandleFunc("/api/v1/timelines/home", checktoken(hometimeline))
3265
3266 loggedmux := handlers.LoggingHandler(os.Stdout, mux)
3267 err = http.Serve(listener, loggedmux)
3268 if err != nil {
3269 elog.Fatal(err)
3270 }
3271 if usefcgi {
3272 err = fcgi.Serve(listener, mux)
3273 } else {
3274 err = http.Serve(listener, mux)
3275 }
3276 if err != nil && !errors.Is(err, net.ErrClosed) {
3277 elog.Printf("serve error: %s", err)
3278 }
3279 time.Sleep(15 * time.Second)
3280 elog.Printf("fell off the bottom")
3281}
3282
3283// Verifies that accesstoken is valid and injects the associated
3284// MastoApp in the request context
3285func checktoken(h http.HandlerFunc) http.HandlerFunc {
3286 return func(w http.ResponseWriter, r *http.Request) {
3287 authHeader := r.Header.Get("Authorization")
3288 split := strings.Split(authHeader, "Bearer")
3289 if len(split) != 2 {
3290 elog.Println("masto: bad access token format or lack thereof")
3291 w.WriteHeader(http.StatusBadRequest)
3292 return
3293 }
3294
3295 token := strings.ReplaceAll(split[1], " ", "")
3296 app := getMastoAppFromAccessToken(token)
3297 if app == nil {
3298 elog.Println("masto: invalid access token")
3299 w.WriteHeader(http.StatusUnauthorized)
3300 return
3301 }
3302
3303 fmt.Printf("logged in! app: %s\n", app.Name)
3304
3305 ctx := context.WithValue(r.Context(), "app", app)
3306 r = r.WithContext(ctx)
3307 h(w, r)
3308 }
3309}