honk.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "bytes"
20 "database/sql"
21 "fmt"
22 "html"
23 "html/template"
24 "io"
25 "io/ioutil"
26 "log"
27 notrand "math/rand"
28 "net/http"
29 "net/url"
30 "os"
31 "sort"
32 "strconv"
33 "strings"
34 "time"
35
36 "github.com/gorilla/mux"
37 "humungus.tedunangst.com/r/webs/css"
38 "humungus.tedunangst.com/r/webs/htfilter"
39 "humungus.tedunangst.com/r/webs/httpsig"
40 "humungus.tedunangst.com/r/webs/image"
41 "humungus.tedunangst.com/r/webs/junk"
42 "humungus.tedunangst.com/r/webs/login"
43 "humungus.tedunangst.com/r/webs/rss"
44 "humungus.tedunangst.com/r/webs/templates"
45)
46
47type WhatAbout struct {
48 ID int64
49 Name string
50 Display string
51 About string
52 Key string
53 URL string
54 SkinnyCSS bool
55}
56
57type Honk struct {
58 ID int64
59 UserID int64
60 Username string
61 What string
62 Honker string
63 Handle string
64 Oonker string
65 Oondle string
66 XID string
67 RID string
68 Date time.Time
69 URL string
70 Noise string
71 Precis string
72 Convoy string
73 Audience []string
74 Public bool
75 Whofore int64
76 Replies []*Honk
77 Flags int64
78 HTML template.HTML
79 Style string
80 Open string
81 Donks []*Donk
82 Onts []string
83}
84
85const (
86 flagIsAcked = 1
87 flagIsBonked = 2
88)
89
90func (honk *Honk) IsAcked() bool {
91 return honk.Flags&flagIsAcked != 0
92}
93
94func (honk *Honk) IsBonked() bool {
95 return honk.Flags&flagIsBonked != 0
96}
97
98type Donk struct {
99 FileID int64
100 XID string
101 Name string
102 URL string
103 Media string
104 Local bool
105 Content []byte
106}
107
108type Honker struct {
109 ID int64
110 UserID int64
111 Name string
112 XID string
113 Handle string
114 Flavor string
115 Combos []string
116}
117
118var serverName string
119var iconName = "icon.png"
120var serverMsg = "Things happen."
121
122var userSep = "u"
123var honkSep = "h"
124
125var readviews *templates.Template
126
127func getuserstyle(u *login.UserInfo) template.CSS {
128 if u == nil {
129 return ""
130 }
131 user, _ := butwhatabout(u.Username)
132 if user.SkinnyCSS {
133 return "main { max-width: 700px; }"
134 }
135 return ""
136}
137
138func getInfo(r *http.Request) map[string]interface{} {
139 u := login.GetUserInfo(r)
140 templinfo := make(map[string]interface{})
141 templinfo["StyleParam"] = getstyleparam("views/style.css")
142 templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
143 templinfo["UserStyle"] = getuserstyle(u)
144 templinfo["ServerName"] = serverName
145 templinfo["IconName"] = iconName
146 templinfo["UserInfo"] = u
147 templinfo["UserSep"] = userSep
148 return templinfo
149}
150
151var donotfedafterdark = make(map[string]bool)
152
153func stealthed(r *http.Request) bool {
154 addr := r.Header.Get("X-Forwarded-For")
155 fake := donotfedafterdark[addr]
156 if fake {
157 log.Printf("faking 404 for %s", addr)
158 }
159 return fake
160}
161
162func homepage(w http.ResponseWriter, r *http.Request) {
163 templinfo := getInfo(r)
164 u := login.GetUserInfo(r)
165 var honks []*Honk
166 var userid int64 = -1
167 if r.URL.Path == "/front" || u == nil {
168 honks = getpublichonks()
169 } else {
170 userid = u.UserID
171 if r.URL.Path == "/atme" {
172 templinfo["PageName"] = "atme"
173 honks = gethonksforme(userid)
174 } else {
175 templinfo["PageName"] = "home"
176 honks = gethonksforuser(userid)
177 honks = osmosis(honks, userid)
178 }
179 if len(honks) > 0 {
180 templinfo["TopXID"] = honks[0].XID
181 }
182 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
183 }
184
185 tname := "honkpage.html"
186 if topxid := r.FormValue("topxid"); topxid != "" {
187 for i, h := range honks {
188 if h.XID == topxid {
189 honks = honks[0:i]
190 break
191 }
192 }
193 log.Printf("topxid %d frags", len(honks))
194 tname = "honkfrags.html"
195 }
196
197 reverbolate(userid, honks)
198
199 templinfo["Honks"] = honks
200 templinfo["ShowRSS"] = true
201 templinfo["ServerMessage"] = serverMsg
202 if u == nil {
203 w.Header().Set("Cache-Control", "max-age=60")
204 } else {
205 w.Header().Set("Cache-Control", "max-age=0")
206 }
207 w.Header().Set("Content-Type", "text/html; charset=utf-8")
208
209 err := readviews.Execute(w, tname, templinfo)
210 if err != nil {
211 log.Print(err)
212 }
213}
214
215func showfunzone(w http.ResponseWriter, r *http.Request) {
216 var emunames, memenames []string
217 dir, err := os.Open("emus")
218 if err == nil {
219 emunames, _ = dir.Readdirnames(0)
220 dir.Close()
221 }
222 for i, e := range emunames {
223 if len(e) > 4 {
224 emunames[i] = e[:len(e)-4]
225 }
226 }
227 dir, err = os.Open("memes")
228 if err == nil {
229 memenames, _ = dir.Readdirnames(0)
230 dir.Close()
231 }
232 templinfo := getInfo(r)
233 templinfo["Emus"] = emunames
234 templinfo["Memes"] = memenames
235 err = readviews.Execute(w, "funzone.html", templinfo)
236 if err != nil {
237 log.Print(err)
238 }
239
240}
241
242func showrss(w http.ResponseWriter, r *http.Request) {
243 name := mux.Vars(r)["name"]
244
245 var honks []*Honk
246 if name != "" {
247 honks = gethonksbyuser(name, false)
248 } else {
249 honks = getpublichonks()
250 }
251 if len(honks) > 20 {
252 honks = honks[0:20]
253 }
254 reverbolate(-1, honks)
255
256 home := fmt.Sprintf("https://%s/", serverName)
257 base := home
258 if name != "" {
259 home += "u/" + name
260 name += " "
261 }
262 feed := rss.Feed{
263 Title: name + "honk",
264 Link: home,
265 Description: name + "honk rss",
266 Image: &rss.Image{
267 URL: base + "icon.png",
268 Title: name + "honk rss",
269 Link: home,
270 },
271 }
272 var modtime time.Time
273 for _, honk := range honks {
274 if !firstclass(honk) {
275 continue
276 }
277 desc := string(honk.HTML)
278 for _, d := range honk.Donks {
279 desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
280 d.URL, html.EscapeString(d.Name))
281 }
282
283 feed.Items = append(feed.Items, &rss.Item{
284 Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
285 Description: rss.CData{desc},
286 Link: honk.URL,
287 PubDate: honk.Date.Format(time.RFC1123),
288 Guid: &rss.Guid{IsPermaLink: true, Value: honk.URL},
289 })
290 if honk.Date.After(modtime) {
291 modtime = honk.Date
292 }
293 }
294 w.Header().Set("Cache-Control", "max-age=300")
295 w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
296
297 err := feed.Write(w)
298 if err != nil {
299 log.Printf("error writing rss: %s", err)
300 }
301}
302
303func butwhatabout(name string) (*WhatAbout, error) {
304 row := stmtWhatAbout.QueryRow(name)
305 var user WhatAbout
306 var options string
307 err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
308 user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
309 user.SkinnyCSS = strings.Contains(options, " skinny ")
310 return &user, err
311}
312
313func crappola(j junk.Junk) bool {
314 t, _ := j.GetString("type")
315 a, _ := j.GetString("actor")
316 o, _ := j.GetString("object")
317 if t == "Delete" && a == o {
318 log.Printf("crappola from %s", a)
319 return true
320 }
321 return false
322}
323
324func ping(user *WhatAbout, who string) {
325 box, err := getboxes(who)
326 if err != nil {
327 log.Printf("no inbox for ping: %s", err)
328 return
329 }
330 j := junk.New()
331 j["@context"] = itiswhatitis
332 j["type"] = "Ping"
333 j["id"] = user.URL + "/ping/" + xfiltrate()
334 j["actor"] = user.URL
335 j["to"] = who
336 keyname, key := ziggy(user.Name)
337 err = PostJunk(keyname, key, box.In, j)
338 if err != nil {
339 log.Printf("can't send ping: %s", err)
340 return
341 }
342 log.Printf("sent ping to %s: %s", who, j["id"])
343}
344
345func pong(user *WhatAbout, who string, obj string) {
346 box, err := getboxes(who)
347 if err != nil {
348 log.Printf("no inbox for pong %s : %s", who, err)
349 return
350 }
351 j := junk.New()
352 j["@context"] = itiswhatitis
353 j["type"] = "Pong"
354 j["id"] = user.URL + "/pong/" + xfiltrate()
355 j["actor"] = user.URL
356 j["to"] = who
357 j["object"] = obj
358 keyname, key := ziggy(user.Name)
359 err = PostJunk(keyname, key, box.In, j)
360 if err != nil {
361 log.Printf("can't send pong: %s", err)
362 return
363 }
364}
365
366func inbox(w http.ResponseWriter, r *http.Request) {
367 name := mux.Vars(r)["name"]
368 user, err := butwhatabout(name)
369 if err != nil {
370 http.NotFound(w, r)
371 return
372 }
373 var buf bytes.Buffer
374 io.Copy(&buf, r.Body)
375 payload := buf.Bytes()
376 j, err := junk.Read(bytes.NewReader(payload))
377 if err != nil {
378 log.Printf("bad payload: %s", err)
379 io.WriteString(os.Stdout, "bad payload\n")
380 os.Stdout.Write(payload)
381 io.WriteString(os.Stdout, "\n")
382 return
383 }
384 if crappola(j) {
385 return
386 }
387 keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
388 if err != nil {
389 log.Printf("inbox message failed signature: %s", err)
390 if keyname != "" {
391 keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
392 if err != nil {
393 log.Printf("still failed: %s", err)
394 }
395 }
396 if err != nil {
397 return
398 }
399 }
400 what, _ := j.GetString("type")
401 if what == "Like" {
402 return
403 }
404 who, _ := j.GetString("actor")
405 origin := keymatch(keyname, who)
406 if origin == "" {
407 log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
408 return
409 }
410 objid, _ := j.GetString("id")
411 if thoudostbitethythumb(user.ID, []string{who}, objid) {
412 log.Printf("ignoring thumb sucker %s", who)
413 return
414 }
415 switch what {
416 case "Ping":
417 obj, _ := j.GetString("id")
418 log.Printf("ping from %s: %s", who, obj)
419 pong(user, who, obj)
420 case "Pong":
421 obj, _ := j.GetString("object")
422 log.Printf("pong from %s: %s", who, obj)
423 case "Follow":
424 obj, _ := j.GetString("object")
425 if obj == user.URL {
426 log.Printf("updating honker follow: %s", who)
427 stmtSaveDub.Exec(user.ID, who, who, "dub")
428 go rubadubdub(user, j)
429 } else {
430 log.Printf("can't follow %s", obj)
431 }
432 case "Accept":
433 log.Printf("updating honker accept: %s", who)
434 _, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
435 if err != nil {
436 log.Printf("error updating honker: %s", err)
437 return
438 }
439 case "Update":
440 obj, ok := j.GetMap("object")
441 if ok {
442 what, _ := obj.GetString("type")
443 switch what {
444 case "Person":
445 return
446 case "Question":
447 return
448 }
449 }
450 log.Printf("unknown Update activity")
451 fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
452 j.Write(fd)
453 io.WriteString(fd, "\n")
454 fd.Close()
455
456 case "Undo":
457 obj, ok := j.GetMap("object")
458 if !ok {
459 log.Printf("unknown undo no object")
460 } else {
461 what, _ := obj.GetString("type")
462 switch what {
463 case "Follow":
464 log.Printf("updating honker undo: %s", who)
465 _, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
466 if err != nil {
467 log.Printf("error updating honker: %s", err)
468 return
469 }
470 case "Announce":
471 xid, _ := obj.GetString("object")
472 log.Printf("undo announce: %s", xid)
473 case "Like":
474 default:
475 log.Printf("unknown undo: %s", what)
476 }
477 }
478 default:
479 go consumeactivity(user, j, origin)
480 }
481}
482
483func ximport(w http.ResponseWriter, r *http.Request) {
484 xid := r.FormValue("xid")
485 p, _ := investigate(xid)
486 if p != nil {
487 xid = p.XID
488 }
489 j, err := GetJunk(xid)
490 if err != nil {
491 http.Error(w, "error getting external object", http.StatusInternalServerError)
492 log.Printf("error getting external object: %s", err)
493 return
494 }
495 log.Printf("importing %s", xid)
496 u := login.GetUserInfo(r)
497 user, _ := butwhatabout(u.Username)
498
499 what, _ := j.GetString("type")
500 if isactor(what) {
501 outbox, _ := j.GetString("outbox")
502 gimmexonks(user, outbox)
503 http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
504 return
505 }
506 xonk := xonkxonk(user, j, originate(xid))
507 convoy := ""
508 if xonk != nil {
509 convoy = xonk.Convoy
510 savexonk(user, xonk)
511 }
512 http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
513}
514
515func xzone(w http.ResponseWriter, r *http.Request) {
516 u := login.GetUserInfo(r)
517 rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
518 if err != nil {
519 log.Printf("query err: %s", err)
520 return
521 }
522 defer rows.Close()
523 var honkers []Honker
524 for rows.Next() {
525 var xid string
526 rows.Scan(&xid)
527 honkers = append(honkers, Honker{XID: xid})
528 }
529 rows.Close()
530 for i, _ := range honkers {
531 _, honkers[i].Handle = handles(honkers[i].XID)
532 }
533 templinfo := getInfo(r)
534 templinfo["XCSRF"] = login.GetCSRF("ximport", r)
535 templinfo["Honkers"] = honkers
536 err = readviews.Execute(w, "xzone.html", templinfo)
537 if err != nil {
538 log.Print(err)
539 }
540}
541
542func outbox(w http.ResponseWriter, r *http.Request) {
543 name := mux.Vars(r)["name"]
544 user, err := butwhatabout(name)
545 if err != nil {
546 http.NotFound(w, r)
547 return
548 }
549 if stealthed(r) {
550 http.NotFound(w, r)
551 return
552 }
553
554 honks := gethonksbyuser(name, false)
555 if len(honks) > 20 {
556 honks = honks[0:20]
557 }
558
559 var jonks []junk.Junk
560 for _, h := range honks {
561 j, _ := jonkjonk(user, h)
562 jonks = append(jonks, j)
563 }
564
565 j := junk.New()
566 j["@context"] = itiswhatitis
567 j["id"] = user.URL + "/outbox"
568 j["type"] = "OrderedCollection"
569 j["totalItems"] = len(jonks)
570 j["orderedItems"] = jonks
571
572 w.Header().Set("Content-Type", theonetruename)
573 j.Write(w)
574}
575
576func emptiness(w http.ResponseWriter, r *http.Request) {
577 name := mux.Vars(r)["name"]
578 user, err := butwhatabout(name)
579 if err != nil {
580 http.NotFound(w, r)
581 return
582 }
583 colname := "/followers"
584 if strings.HasSuffix(r.URL.Path, "/following") {
585 colname = "/following"
586 }
587 j := junk.New()
588 j["@context"] = itiswhatitis
589 j["id"] = user.URL + colname
590 j["type"] = "OrderedCollection"
591 j["totalItems"] = 0
592 j["orderedItems"] = []junk.Junk{}
593
594 w.Header().Set("Content-Type", theonetruename)
595 j.Write(w)
596}
597
598func showuser(w http.ResponseWriter, r *http.Request) {
599 name := mux.Vars(r)["name"]
600 user, err := butwhatabout(name)
601 if err != nil {
602 log.Printf("user not found %s: %s", name, err)
603 http.NotFound(w, r)
604 return
605 }
606 if friendorfoe(r.Header.Get("Accept")) {
607 j := asjonker(user)
608 w.Header().Set("Content-Type", theonetruename)
609 j.Write(w)
610 return
611 }
612 u := login.GetUserInfo(r)
613 honks := gethonksbyuser(name, u != nil && u.Username == name)
614 honkpage(w, r, u, user, honks, "")
615}
616
617func showhonker(w http.ResponseWriter, r *http.Request) {
618 u := login.GetUserInfo(r)
619 name := mux.Vars(r)["name"]
620 var honks []*Honk
621 if name == "" {
622 name = r.FormValue("xid")
623 honks = gethonksbyxonker(u.UserID, name)
624 } else {
625 honks = gethonksbyhonker(u.UserID, name)
626 }
627 name = html.EscapeString(name)
628 msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
629 honkpage(w, r, u, nil, honks, template.HTML(msg))
630}
631
632func showcombo(w http.ResponseWriter, r *http.Request) {
633 name := mux.Vars(r)["name"]
634 u := login.GetUserInfo(r)
635 honks := gethonksbycombo(u.UserID, name)
636 honks = osmosis(honks, u.UserID)
637 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
638}
639func showconvoy(w http.ResponseWriter, r *http.Request) {
640 c := r.FormValue("c")
641 u := login.GetUserInfo(r)
642 honks := gethonksbyconvoy(u.UserID, c)
643 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
644}
645func showsearch(w http.ResponseWriter, r *http.Request) {
646 q := r.FormValue("q")
647 u := login.GetUserInfo(r)
648 honks := gethonksbysearch(u.UserID, q)
649 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks for search: "+q)))
650}
651func showontology(w http.ResponseWriter, r *http.Request) {
652 name := mux.Vars(r)["name"]
653 u := login.GetUserInfo(r)
654 var userid int64 = -1
655 if u != nil {
656 userid = u.UserID
657 }
658 honks := gethonksbyontology(userid, "#"+name)
659 honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by ontology: "+name)))
660}
661
662func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
663 u := login.GetUserInfo(r)
664 var userid int64 = -1
665 if u != nil {
666 userid = u.UserID
667 }
668 rows, err := stmtSelectOnts.Query(userid)
669 if err != nil {
670 log.Printf("selection error: %s", err)
671 return
672 }
673 var onts [][]string
674 for rows.Next() {
675 var o string
676 err := rows.Scan(&o)
677 if err != nil {
678 log.Printf("error scanning ont: %s", err)
679 continue
680 }
681 onts = append(onts, []string{o, o[1:]})
682 }
683 if u == nil {
684 w.Header().Set("Cache-Control", "max-age=300")
685 }
686 templinfo := getInfo(r)
687 templinfo["Onts"] = onts
688 err = readviews.Execute(w, "onts.html", templinfo)
689 if err != nil {
690 log.Print(err)
691 }
692}
693
694func showhonk(w http.ResponseWriter, r *http.Request) {
695 name := mux.Vars(r)["name"]
696 user, err := butwhatabout(name)
697 if err != nil {
698 http.NotFound(w, r)
699 return
700 }
701 if stealthed(r) {
702 http.NotFound(w, r)
703 return
704 }
705
706 xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
707 honk := getxonk(user.ID, xid)
708 if honk == nil {
709 http.NotFound(w, r)
710 return
711 }
712 u := login.GetUserInfo(r)
713 if u != nil && u.UserID != user.ID {
714 u = nil
715 }
716 if !honk.Public {
717 if u == nil {
718 http.NotFound(w, r)
719 return
720
721 }
722 honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
723 return
724 }
725 rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
726 if friendorfoe(r.Header.Get("Accept")) {
727 for _, h := range rawhonks {
728 if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
729 honk.Replies = append(honk.Replies, h)
730 }
731 }
732 donksforhonks([]*Honk{honk})
733 _, j := jonkjonk(user, honk)
734 j["@context"] = itiswhatitis
735 w.Header().Set("Content-Type", theonetruename)
736 j.Write(w)
737 return
738 }
739 var honks []*Honk
740 for _, h := range rawhonks {
741 if h.Public && (h.Whofore == 2 || h.IsAcked()) {
742 honks = append(honks, h)
743 }
744 }
745
746 honkpage(w, r, u, nil, honks, "one honk maybe more")
747}
748
749func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
750 honks []*Honk, infomsg template.HTML) {
751 templinfo := getInfo(r)
752 var userid int64 = -1
753 if u != nil {
754 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
755 userid = u.UserID
756 }
757 if u == nil {
758 w.Header().Set("Cache-Control", "max-age=60")
759 }
760 reverbolate(userid, honks)
761 if user != nil {
762 filt := htfilter.New()
763 templinfo["Name"] = user.Name
764 whatabout := user.About
765 whatabout = obfusbreak(user.About)
766 templinfo["WhatAbout"], _ = filt.String(whatabout)
767 }
768 templinfo["Honks"] = honks
769 templinfo["ServerMessage"] = infomsg
770 err := readviews.Execute(w, "honkpage.html", templinfo)
771 if err != nil {
772 log.Print(err)
773 }
774}
775
776func saveuser(w http.ResponseWriter, r *http.Request) {
777 whatabout := r.FormValue("whatabout")
778 u := login.GetUserInfo(r)
779 db := opendatabase()
780 options := ""
781 if r.FormValue("skinny") == "skinny" {
782 options += " skinny "
783 }
784 _, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
785 if err != nil {
786 log.Printf("error bouting what: %s", err)
787 }
788
789 http.Redirect(w, r, "/account", http.StatusSeeOther)
790}
791
792func gethonkers(userid int64) []*Honker {
793 rows, err := stmtHonkers.Query(userid)
794 if err != nil {
795 log.Printf("error querying honkers: %s", err)
796 return nil
797 }
798 defer rows.Close()
799 var honkers []*Honker
800 for rows.Next() {
801 var f Honker
802 var combos string
803 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
804 f.Combos = strings.Split(strings.TrimSpace(combos), " ")
805 if err != nil {
806 log.Printf("error scanning honker: %s", err)
807 return nil
808 }
809 honkers = append(honkers, &f)
810 }
811 return honkers
812}
813
814func getdubs(userid int64) []*Honker {
815 rows, err := stmtDubbers.Query(userid)
816 if err != nil {
817 log.Printf("error querying dubs: %s", err)
818 return nil
819 }
820 defer rows.Close()
821 var honkers []*Honker
822 for rows.Next() {
823 var f Honker
824 err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
825 if err != nil {
826 log.Printf("error scanning honker: %s", err)
827 return nil
828 }
829 honkers = append(honkers, &f)
830 }
831 return honkers
832}
833
834func allusers() []login.UserInfo {
835 var users []login.UserInfo
836 rows, _ := opendatabase().Query("select userid, username from users")
837 defer rows.Close()
838 for rows.Next() {
839 var u login.UserInfo
840 rows.Scan(&u.UserID, &u.Username)
841 users = append(users, u)
842 }
843 return users
844}
845
846func getxonk(userid int64, xid string) *Honk {
847 h := new(Honk)
848 var dt, aud, onts string
849 row := stmtOneXonk.QueryRow(userid, xid)
850 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
851 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
852 if err != nil {
853 if err != sql.ErrNoRows {
854 log.Printf("error scanning xonk: %s", err)
855 }
856 return nil
857 }
858 h.Date, _ = time.Parse(dbtimeformat, dt)
859 h.Audience = strings.Split(aud, " ")
860 h.Public = !keepitquiet(h.Audience)
861 if len(onts) > 0 {
862 h.Onts = strings.Split(onts, " ")
863 }
864 return h
865}
866
867func getbonk(userid int64, xid string) *Honk {
868 h := new(Honk)
869 var dt, aud, onts string
870 row := stmtOneBonk.QueryRow(userid, xid)
871 err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
872 &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
873 if err != nil {
874 if err != sql.ErrNoRows {
875 log.Printf("error scanning xonk: %s", err)
876 }
877 return nil
878 }
879 h.Date, _ = time.Parse(dbtimeformat, dt)
880 h.Audience = strings.Split(aud, " ")
881 h.Public = !keepitquiet(h.Audience)
882 if len(onts) > 0 {
883 h.Onts = strings.Split(onts, " ")
884 }
885 return h
886}
887
888func getpublichonks() []*Honk {
889 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
890 rows, err := stmtPublicHonks.Query(dt)
891 return getsomehonks(rows, err)
892}
893func gethonksbyuser(name string, includeprivate bool) []*Honk {
894 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
895 whofore := 2
896 if includeprivate {
897 whofore = 3
898 }
899 rows, err := stmtUserHonks.Query(whofore, name, dt)
900 return getsomehonks(rows, err)
901}
902func gethonksforuser(userid int64) []*Honk {
903 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
904 rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
905 return getsomehonks(rows, err)
906}
907func gethonksforme(userid int64) []*Honk {
908 dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
909 rows, err := stmtHonksForMe.Query(userid, dt, userid)
910 return getsomehonks(rows, err)
911}
912func gethonksbyhonker(userid int64, honker string) []*Honk {
913 rows, err := stmtHonksByHonker.Query(userid, honker, userid)
914 return getsomehonks(rows, err)
915}
916func gethonksbyxonker(userid int64, xonker string) []*Honk {
917 rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
918 return getsomehonks(rows, err)
919}
920func gethonksbycombo(userid int64, combo string) []*Honk {
921 combo = "% " + combo + " %"
922 rows, err := stmtHonksByCombo.Query(userid, combo, userid)
923 return getsomehonks(rows, err)
924}
925func gethonksbyconvoy(userid int64, convoy string) []*Honk {
926 rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
927 honks := getsomehonks(rows, err)
928 for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
929 honks[i], honks[j] = honks[j], honks[i]
930 }
931 return honks
932}
933func gethonksbysearch(userid int64, q string) []*Honk {
934 q = "%" + q + "%"
935 rows, err := stmtHonksBySearch.Query(userid, q)
936 honks := getsomehonks(rows, err)
937 return honks
938}
939func gethonksbyontology(userid int64, name string) []*Honk {
940 rows, err := stmtHonksByOntology.Query(name, userid, userid)
941 honks := getsomehonks(rows, err)
942 return honks
943}
944
945func getsomehonks(rows *sql.Rows, err error) []*Honk {
946 if err != nil {
947 log.Printf("error querying honks: %s", err)
948 return nil
949 }
950 defer rows.Close()
951 var honks []*Honk
952 for rows.Next() {
953 var h Honk
954 var dt, aud, onts string
955 err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID,
956 &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags, &onts)
957 if err != nil {
958 log.Printf("error scanning honks: %s", err)
959 return nil
960 }
961 h.Date, _ = time.Parse(dbtimeformat, dt)
962 h.Audience = strings.Split(aud, " ")
963 h.Public = !keepitquiet(h.Audience)
964 if len(onts) > 0 {
965 h.Onts = strings.Split(onts, " ")
966 }
967 honks = append(honks, &h)
968 }
969 rows.Close()
970 donksforhonks(honks)
971 return honks
972}
973
974func donksforhonks(honks []*Honk) {
975 db := opendatabase()
976 var ids []string
977 hmap := make(map[int64]*Honk)
978 for _, h := range honks {
979 ids = append(ids, fmt.Sprintf("%d", h.ID))
980 hmap[h.ID] = h
981 }
982 q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
983 rows, err := db.Query(q)
984 if err != nil {
985 log.Printf("error querying donks: %s", err)
986 return
987 }
988 defer rows.Close()
989 for rows.Next() {
990 var hid int64
991 var d Donk
992 err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
993 if err != nil {
994 log.Printf("error scanning donk: %s", err)
995 continue
996 }
997 h := hmap[hid]
998 h.Donks = append(h.Donks, &d)
999 }
1000}
1001
1002func savebonk(w http.ResponseWriter, r *http.Request) {
1003 xid := r.FormValue("xid")
1004 userinfo := login.GetUserInfo(r)
1005 user, _ := butwhatabout(userinfo.Username)
1006
1007 log.Printf("bonking %s", xid)
1008
1009 xonk := getxonk(userinfo.UserID, xid)
1010 if xonk == nil {
1011 return
1012 }
1013 if !xonk.Public {
1014 return
1015 }
1016 donksforhonks([]*Honk{xonk})
1017
1018 _, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
1019 if err != nil {
1020 log.Printf("error acking bonk: %s", err)
1021 }
1022
1023 oonker := xonk.Oonker
1024 if oonker == "" {
1025 oonker = xonk.Honker
1026 }
1027 dt := time.Now().UTC()
1028 bonk := Honk{
1029 UserID: userinfo.UserID,
1030 Username: userinfo.Username,
1031 What: "bonk",
1032 Honker: user.URL,
1033 XID: xonk.XID,
1034 Date: dt,
1035 Donks: xonk.Donks,
1036 Convoy: xonk.Convoy,
1037 Audience: []string{thewholeworld, oonker},
1038 Public: true,
1039 }
1040
1041 aud := strings.Join(bonk.Audience, " ")
1042 whofore := 2
1043 onts := xonk.Onts
1044 res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
1045 dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
1046 xonk.Precis, oonker, 0, strings.Join(onts, " "))
1047 if err != nil {
1048 log.Printf("error saving bonk: %s", err)
1049 return
1050 }
1051 bonk.ID, _ = res.LastInsertId()
1052 for _, d := range bonk.Donks {
1053 _, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
1054 if err != nil {
1055 log.Printf("err saving donk: %s", err)
1056 return
1057 }
1058 }
1059 for _, o := range onts {
1060 _, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
1061 if err != nil {
1062 log.Printf("error saving ont: %s", err)
1063 }
1064 }
1065
1066 go honkworldwide(user, &bonk)
1067}
1068
1069func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
1070 zonk := Honk{
1071 What: what,
1072 XID: xonk.XID,
1073 Date: time.Now().UTC(),
1074 Audience: oneofakind(xonk.Audience),
1075 }
1076 zonk.Public = !keepitquiet(zonk.Audience)
1077
1078 log.Printf("announcing %sed honk: %s", what, xonk.XID)
1079 go honkworldwide(user, &zonk)
1080}
1081
1082func zonkit(w http.ResponseWriter, r *http.Request) {
1083 wherefore := r.FormValue("wherefore")
1084 what := r.FormValue("what")
1085 userinfo := login.GetUserInfo(r)
1086 user, _ := butwhatabout(userinfo.Username)
1087
1088 if wherefore == "ack" {
1089 xonk := getxonk(userinfo.UserID, what)
1090 if xonk != nil {
1091 _, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1092 if err != nil {
1093 log.Printf("error acking: %s", err)
1094 }
1095 sendzonkofsorts(xonk, user, "ack")
1096 }
1097 return
1098 }
1099
1100 if wherefore == "deack" {
1101 xonk := getxonk(userinfo.UserID, what)
1102 if xonk != nil {
1103 _, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1104 if err != nil {
1105 log.Printf("error deacking: %s", err)
1106 }
1107 sendzonkofsorts(xonk, user, "deack")
1108 }
1109 return
1110 }
1111
1112 if wherefore == "unbonk" {
1113 xonk := getbonk(userinfo.UserID, what)
1114 if xonk != nil {
1115 _, err := stmtZonkDonks.Exec(xonk.ID)
1116 if err != nil {
1117 log.Printf("error zonking: %s", err)
1118 }
1119 _, err = stmtZonkIt.Exec(xonk.ID)
1120 if err != nil {
1121 log.Printf("error zonking: %s", err)
1122 }
1123 xonk = getxonk(userinfo.UserID, what)
1124 _, err = stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1125 if err != nil {
1126 log.Printf("error unbonking: %s", err)
1127 }
1128 sendzonkofsorts(xonk, user, "unbonk")
1129 }
1130 return
1131 }
1132
1133 log.Printf("zonking %s %s", wherefore, what)
1134 if wherefore == "zonk" {
1135 xonk := getxonk(userinfo.UserID, what)
1136 if xonk != nil {
1137 _, err := stmtZonkDonks.Exec(xonk.ID)
1138 if err != nil {
1139 log.Printf("error zonking: %s", err)
1140 }
1141 _, err = stmtZonkIt.Exec(xonk.ID)
1142 if err != nil {
1143 log.Printf("error zonking: %s", err)
1144 }
1145 if xonk.Whofore == 2 || xonk.Whofore == 3 {
1146 sendzonkofsorts(xonk, user, "zonk")
1147 }
1148 }
1149 }
1150 _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1151 if err != nil {
1152 log.Printf("error saving zonker: %s", err)
1153 return
1154 }
1155}
1156
1157func savehonk(w http.ResponseWriter, r *http.Request) {
1158 rid := r.FormValue("rid")
1159 noise := r.FormValue("noise")
1160
1161 userinfo := login.GetUserInfo(r)
1162 user, _ := butwhatabout(userinfo.Username)
1163
1164 dt := time.Now().UTC()
1165 xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1166 what := "honk"
1167 if rid != "" {
1168 what = "tonk"
1169 }
1170 honk := Honk{
1171 UserID: userinfo.UserID,
1172 Username: userinfo.Username,
1173 What: "honk",
1174 Honker: user.URL,
1175 XID: xid,
1176 Date: dt,
1177 }
1178 if strings.HasPrefix(noise, "DZ:") {
1179 idx := strings.Index(noise, "\n")
1180 if idx == -1 {
1181 honk.Precis = noise
1182 noise = ""
1183 } else {
1184 honk.Precis = noise[:idx]
1185 noise = noise[idx+1:]
1186 }
1187 }
1188 noise = hooterize(noise)
1189 noise = strings.TrimSpace(noise)
1190 honk.Precis = strings.TrimSpace(honk.Precis)
1191
1192 var convoy string
1193 if rid != "" {
1194 xonk := getxonk(userinfo.UserID, rid)
1195 if xonk != nil {
1196 if xonk.Public {
1197 honk.Audience = append(honk.Audience, xonk.Audience...)
1198 }
1199 convoy = xonk.Convoy
1200 } else {
1201 xonkaud, c := whosthere(rid)
1202 honk.Audience = append(honk.Audience, xonkaud...)
1203 convoy = c
1204 }
1205 for i, a := range honk.Audience {
1206 if a == thewholeworld {
1207 honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1208 break
1209 }
1210 }
1211 honk.RID = rid
1212 } else {
1213 honk.Audience = []string{thewholeworld}
1214 }
1215 if noise != "" && noise[0] == '@' {
1216 honk.Audience = append(grapevine(noise), honk.Audience...)
1217 } else {
1218 honk.Audience = append(honk.Audience, grapevine(noise)...)
1219 }
1220 if convoy == "" {
1221 convoy = "data:,electrichonkytonk-" + xfiltrate()
1222 }
1223 butnottooloud(honk.Audience)
1224 honk.Audience = oneofakind(honk.Audience)
1225 if len(honk.Audience) == 0 {
1226 log.Printf("honk to nowhere")
1227 http.Error(w, "honk to nowhere...", http.StatusNotFound)
1228 return
1229 }
1230 honk.Public = !keepitquiet(honk.Audience)
1231 noise = obfusbreak(noise)
1232 honk.Noise = noise
1233 honk.Convoy = convoy
1234
1235 donkxid := r.FormValue("donkxid")
1236 if donkxid == "" {
1237 file, filehdr, err := r.FormFile("donk")
1238 if err == nil {
1239 var buf bytes.Buffer
1240 io.Copy(&buf, file)
1241 file.Close()
1242 data := buf.Bytes()
1243 xid := xfiltrate()
1244 var media, name string
1245 img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1246 if err == nil {
1247 data = img.Data
1248 format := img.Format
1249 media = "image/" + format
1250 if format == "jpeg" {
1251 format = "jpg"
1252 }
1253 name = xid + "." + format
1254 xid = name
1255 } else {
1256 maxsize := 100000
1257 if len(data) > maxsize {
1258 log.Printf("bad image: %s too much text: %d", err, len(data))
1259 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1260 return
1261 }
1262 for i := 0; i < len(data); i++ {
1263 if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1264 log.Printf("bad image: %s not text: %d", err, data[i])
1265 http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1266 return
1267 }
1268 }
1269 media = "text/plain"
1270 name = filehdr.Filename
1271 if name == "" {
1272 name = xid + ".txt"
1273 }
1274 xid += ".txt"
1275 }
1276 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1277 res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1278 if err != nil {
1279 log.Printf("unable to save image: %s", err)
1280 return
1281 }
1282 var d Donk
1283 d.FileID, _ = res.LastInsertId()
1284 d.XID = name
1285 d.Name = name
1286 d.Media = media
1287 d.URL = url
1288 d.Local = true
1289 honk.Donks = append(honk.Donks, &d)
1290 donkxid = d.XID
1291 }
1292 } else {
1293 xid := donkxid
1294 url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1295 var donk Donk
1296 row := stmtFindFile.QueryRow(url)
1297 err := row.Scan(&donk.FileID)
1298 if err == nil {
1299 donk.XID = xid
1300 donk.Local = true
1301 donk.URL = url
1302 honk.Donks = append(honk.Donks, &donk)
1303 } else {
1304 log.Printf("can't find file: %s", xid)
1305 }
1306 }
1307 herd := herdofemus(honk.Noise)
1308 for _, e := range herd {
1309 donk := savedonk(e.ID, e.Name, "image/png", true)
1310 if donk != nil {
1311 donk.Name = e.Name
1312 honk.Donks = append(honk.Donks, donk)
1313 }
1314 }
1315 memetize(&honk)
1316
1317 aud := strings.Join(honk.Audience, " ")
1318 whofore := 2
1319 if !honk.Public {
1320 whofore = 3
1321 }
1322 if r.FormValue("preview") == "preview" {
1323 honks := []*Honk{&honk}
1324 reverbolate(userinfo.UserID, honks)
1325 templinfo := getInfo(r)
1326 templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1327 templinfo["Honks"] = honks
1328 templinfo["InReplyTo"] = r.FormValue("rid")
1329 templinfo["Noise"] = r.FormValue("noise")
1330 templinfo["SavedFile"] = donkxid
1331 templinfo["ServerMessage"] = "honk preview"
1332 err := readviews.Execute(w, "honkpage.html", templinfo)
1333 if err != nil {
1334 log.Print(err)
1335 }
1336 return
1337 }
1338 honk.Onts = oneofakind(ontologies(honk.Noise))
1339 res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1340 dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1341 honk.Precis, honk.Oonker, 0, strings.Join(honk.Onts, " "))
1342 if err != nil {
1343 log.Printf("error saving honk: %s", err)
1344 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1345 return
1346 }
1347 honk.ID, _ = res.LastInsertId()
1348 for _, d := range honk.Donks {
1349 _, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1350 if err != nil {
1351 log.Printf("err saving donk: %s", err)
1352 http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1353 return
1354 }
1355 }
1356 for _, o := range honk.Onts {
1357 _, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1358 if err != nil {
1359 log.Printf("error saving ont: %s", err)
1360 }
1361 }
1362
1363 go honkworldwide(user, &honk)
1364
1365 http.Redirect(w, r, xid, http.StatusSeeOther)
1366}
1367
1368func showhonkers(w http.ResponseWriter, r *http.Request) {
1369 userinfo := login.GetUserInfo(r)
1370 templinfo := getInfo(r)
1371 templinfo["Honkers"] = gethonkers(userinfo.UserID)
1372 templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1373 err := readviews.Execute(w, "honkers.html", templinfo)
1374 if err != nil {
1375 log.Print(err)
1376 }
1377}
1378
1379func showcombos(w http.ResponseWriter, r *http.Request) {
1380 userinfo := login.GetUserInfo(r)
1381 templinfo := getInfo(r)
1382 honkers := gethonkers(userinfo.UserID)
1383 var combos []string
1384 for _, h := range honkers {
1385 combos = append(combos, h.Combos...)
1386 }
1387 for i, c := range combos {
1388 if c == "-" {
1389 combos[i] = ""
1390 }
1391 }
1392 combos = oneofakind(combos)
1393 sort.Strings(combos)
1394 templinfo["Combos"] = combos
1395 err := readviews.Execute(w, "combos.html", templinfo)
1396 if err != nil {
1397 log.Print(err)
1398 }
1399}
1400
1401func savehonker(w http.ResponseWriter, r *http.Request) {
1402 u := login.GetUserInfo(r)
1403 name := r.FormValue("name")
1404 url := r.FormValue("url")
1405 peep := r.FormValue("peep")
1406 combos := r.FormValue("combos")
1407 honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1408
1409 if honkerid > 0 {
1410 goodbye := r.FormValue("goodbye")
1411 if goodbye == "F" {
1412 db := opendatabase()
1413 row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1414 honkerid, u.UserID)
1415 var xid string
1416 err := row.Scan(&xid)
1417 if err != nil {
1418 log.Printf("can't get honker xid: %s", err)
1419 return
1420 }
1421 log.Printf("unsubscribing from %s", xid)
1422 user, _ := butwhatabout(u.Username)
1423 go itakeitallback(user, xid)
1424 _, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1425 if err != nil {
1426 log.Printf("error updating honker: %s", err)
1427 return
1428 }
1429
1430 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1431 return
1432 }
1433 combos = " " + strings.TrimSpace(combos) + " "
1434 _, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1435 if err != nil {
1436 log.Printf("update honker err: %s", err)
1437 return
1438 }
1439 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1440 }
1441
1442 flavor := "presub"
1443 if peep == "peep" {
1444 flavor = "peep"
1445 }
1446 p, err := investigate(url)
1447 if err != nil {
1448 http.Error(w, "error investigating: "+err.Error(), http.StatusInternalServerError)
1449 log.Printf("failed to investigate honker")
1450 return
1451 }
1452 url = p.XID
1453 if name == "" {
1454 name = p.Handle
1455 }
1456 _, err = stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1457 if err != nil {
1458 log.Print(err)
1459 return
1460 }
1461 if flavor == "presub" {
1462 user, _ := butwhatabout(u.Username)
1463 go subsub(user, url)
1464 }
1465 http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1466}
1467
1468type Zonker struct {
1469 ID int64
1470 Name string
1471 Wherefore string
1472}
1473
1474func zonkzone(w http.ResponseWriter, r *http.Request) {
1475 userinfo := login.GetUserInfo(r)
1476 rows, err := stmtGetZonkers.Query(userinfo.UserID)
1477 if err != nil {
1478 log.Printf("err: %s", err)
1479 return
1480 }
1481 defer rows.Close()
1482 var zonkers []Zonker
1483 for rows.Next() {
1484 var z Zonker
1485 rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1486 zonkers = append(zonkers, z)
1487 }
1488 sort.Slice(zonkers, func(i, j int) bool {
1489 w1 := zonkers[i].Wherefore
1490 w2 := zonkers[j].Wherefore
1491 if w1 == w2 {
1492 return zonkers[i].Name < zonkers[j].Name
1493 }
1494 if w1 == "zonvoy" {
1495 w1 = "zzzzzzz"
1496 }
1497 if w2 == "zonvoy" {
1498 w2 = "zzzzzzz"
1499 }
1500 return w1 < w2
1501 })
1502
1503 templinfo := getInfo(r)
1504 templinfo["Zonkers"] = zonkers
1505 templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1506 err = readviews.Execute(w, "zonkers.html", templinfo)
1507 if err != nil {
1508 log.Print(err)
1509 }
1510}
1511
1512func zonkzonk(w http.ResponseWriter, r *http.Request) {
1513 userinfo := login.GetUserInfo(r)
1514 itsok := r.FormValue("itsok")
1515 if itsok == "iforgiveyou" {
1516 zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1517 db := opendatabase()
1518 db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1519 userinfo.UserID, zonkerid)
1520 bitethethumbs()
1521 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1522 return
1523 }
1524 wherefore := r.FormValue("wherefore")
1525 name := r.FormValue("name")
1526 if name == "" {
1527 return
1528 }
1529 switch wherefore {
1530 case "zonker":
1531 case "zomain":
1532 case "zonvoy":
1533 case "zord":
1534 case "zilence":
1535 default:
1536 return
1537 }
1538 db := opendatabase()
1539 db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1540 userinfo.UserID, name, wherefore)
1541 if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1542 bitethethumbs()
1543 }
1544
1545 http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1546}
1547
1548func accountpage(w http.ResponseWriter, r *http.Request) {
1549 u := login.GetUserInfo(r)
1550 user, _ := butwhatabout(u.Username)
1551 templinfo := getInfo(r)
1552 templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1553 templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1554 templinfo["User"] = user
1555 err := readviews.Execute(w, "account.html", templinfo)
1556 if err != nil {
1557 log.Print(err)
1558 }
1559}
1560
1561func dochpass(w http.ResponseWriter, r *http.Request) {
1562 err := login.ChangePassword(w, r)
1563 if err != nil {
1564 log.Printf("error changing password: %s", err)
1565 }
1566 http.Redirect(w, r, "/account", http.StatusSeeOther)
1567}
1568
1569func fingerlicker(w http.ResponseWriter, r *http.Request) {
1570 orig := r.FormValue("resource")
1571
1572 log.Printf("finger lick: %s", orig)
1573
1574 if strings.HasPrefix(orig, "acct:") {
1575 orig = orig[5:]
1576 }
1577
1578 name := orig
1579 idx := strings.LastIndexByte(name, '/')
1580 if idx != -1 {
1581 name = name[idx+1:]
1582 if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1583 log.Printf("foreign request rejected")
1584 name = ""
1585 }
1586 } else {
1587 idx = strings.IndexByte(name, '@')
1588 if idx != -1 {
1589 name = name[:idx]
1590 if name+"@"+serverName != orig {
1591 log.Printf("foreign request rejected")
1592 name = ""
1593 }
1594 }
1595 }
1596 user, err := butwhatabout(name)
1597 if err != nil {
1598 http.NotFound(w, r)
1599 return
1600 }
1601
1602 j := junk.New()
1603 j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1604 j["aliases"] = []string{user.URL}
1605 var links []junk.Junk
1606 l := junk.New()
1607 l["rel"] = "self"
1608 l["type"] = `application/activity+json`
1609 l["href"] = user.URL
1610 links = append(links, l)
1611 j["links"] = links
1612
1613 w.Header().Set("Cache-Control", "max-age=3600")
1614 w.Header().Set("Content-Type", "application/jrd+json")
1615 j.Write(w)
1616}
1617
1618func somedays() string {
1619 secs := 432000 + notrand.Int63n(432000)
1620 return fmt.Sprintf("%d", secs)
1621}
1622
1623func avatate(w http.ResponseWriter, r *http.Request) {
1624 n := r.FormValue("a")
1625 a := avatar(n)
1626 w.Header().Set("Cache-Control", "max-age="+somedays())
1627 w.Write(a)
1628}
1629
1630func servecss(w http.ResponseWriter, r *http.Request) {
1631 data, _ := ioutil.ReadFile("views" + r.URL.Path)
1632 s := css.Process(string(data))
1633 w.Header().Set("Cache-Control", "max-age=7776000")
1634 w.Header().Set("Content-Type", "text/css; charset=utf-8")
1635 w.Write([]byte(s))
1636}
1637func servehtml(w http.ResponseWriter, r *http.Request) {
1638 templinfo := getInfo(r)
1639 err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1640 if err != nil {
1641 log.Print(err)
1642 }
1643}
1644func serveemu(w http.ResponseWriter, r *http.Request) {
1645 xid := mux.Vars(r)["xid"]
1646 w.Header().Set("Cache-Control", "max-age="+somedays())
1647 http.ServeFile(w, r, "emus/"+xid)
1648}
1649func servememe(w http.ResponseWriter, r *http.Request) {
1650 xid := mux.Vars(r)["xid"]
1651 w.Header().Set("Cache-Control", "max-age="+somedays())
1652 http.ServeFile(w, r, "memes/"+xid)
1653}
1654
1655func servefile(w http.ResponseWriter, r *http.Request) {
1656 xid := mux.Vars(r)["xid"]
1657 row := stmtFileData.QueryRow(xid)
1658 var media string
1659 var data []byte
1660 err := row.Scan(&media, &data)
1661 if err != nil {
1662 log.Printf("error loading file: %s", err)
1663 http.NotFound(w, r)
1664 return
1665 }
1666 w.Header().Set("Content-Type", media)
1667 w.Header().Set("X-Content-Type-Options", "nosniff")
1668 w.Header().Set("Cache-Control", "max-age="+somedays())
1669 w.Write(data)
1670}
1671
1672func nomoroboto(w http.ResponseWriter, r *http.Request) {
1673 io.WriteString(w, "User-agent: *\n")
1674 io.WriteString(w, "Disallow: /a\n")
1675 io.WriteString(w, "Disallow: /d\n")
1676 io.WriteString(w, "Disallow: /meme\n")
1677 for _, u := range allusers() {
1678 fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1679 }
1680}
1681
1682func serve() {
1683 db := opendatabase()
1684 login.Init(db)
1685
1686 listener, err := openListener()
1687 if err != nil {
1688 log.Fatal(err)
1689 }
1690 go redeliverator()
1691
1692 debug := false
1693 getconfig("debug", &debug)
1694 readviews = templates.Load(debug,
1695 "views/honkpage.html",
1696 "views/honkfrags.html",
1697 "views/honkers.html",
1698 "views/zonkers.html",
1699 "views/combos.html",
1700 "views/honkform.html",
1701 "views/honk.html",
1702 "views/account.html",
1703 "views/about.html",
1704 "views/funzone.html",
1705 "views/login.html",
1706 "views/xzone.html",
1707 "views/header.html",
1708 "views/onts.html",
1709 )
1710 if !debug {
1711 s := "views/style.css"
1712 savedstyleparams[s] = getstyleparam(s)
1713 s = "views/local.css"
1714 savedstyleparams[s] = getstyleparam(s)
1715 }
1716
1717 bitethethumbs()
1718
1719 mux := mux.NewRouter()
1720 mux.Use(login.Checker)
1721
1722 posters := mux.Methods("POST").Subrouter()
1723 getters := mux.Methods("GET").Subrouter()
1724
1725 getters.HandleFunc("/", homepage)
1726 getters.HandleFunc("/home", homepage)
1727 getters.HandleFunc("/front", homepage)
1728 getters.HandleFunc("/robots.txt", nomoroboto)
1729 getters.HandleFunc("/rss", showrss)
1730 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1731 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1732 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1733 posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1734 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1735 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1736 getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1737 getters.HandleFunc("/a", avatate)
1738 getters.HandleFunc("/o", thelistingoftheontologies)
1739 getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1740 getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1741 getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1742 getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1743 getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1744
1745 getters.HandleFunc("/style.css", servecss)
1746 getters.HandleFunc("/local.css", servecss)
1747 getters.HandleFunc("/about", servehtml)
1748 getters.HandleFunc("/login", servehtml)
1749 posters.HandleFunc("/dologin", login.LoginFunc)
1750 getters.HandleFunc("/logout", login.LogoutFunc)
1751
1752 loggedin := mux.NewRoute().Subrouter()
1753 loggedin.Use(login.Required)
1754 loggedin.HandleFunc("/account", accountpage)
1755 loggedin.HandleFunc("/funzone", showfunzone)
1756 loggedin.HandleFunc("/chpass", dochpass)
1757 loggedin.HandleFunc("/atme", homepage)
1758 loggedin.HandleFunc("/zonkzone", zonkzone)
1759 loggedin.HandleFunc("/xzone", xzone)
1760 loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1761 loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1762 loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1763 loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1764 loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1765 loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1766 loggedin.HandleFunc("/honkers", showhonkers)
1767 loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1768 loggedin.HandleFunc("/h", showhonker)
1769 loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1770 loggedin.HandleFunc("/c", showcombos)
1771 loggedin.HandleFunc("/t", showconvoy)
1772 loggedin.HandleFunc("/q", showsearch)
1773 loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1774
1775 err = http.Serve(listener, mux)
1776 if err != nil {
1777 log.Fatal(err)
1778 }
1779}
1780
1781func cleanupdb(arg string) {
1782 db := opendatabase()
1783 days, err := strconv.Atoi(arg)
1784 if err != nil {
1785 honker := arg
1786 expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1787 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1788 doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1789 } else {
1790 expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1791 doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1792 doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1793 }
1794 doordie(db, "delete from files where fileid not in (select fileid from donks)")
1795 for _, u := range allusers() {
1796 doordie(db, "delete from zonkers where userid = ? and wherefore = 'zonvoy' and zonkerid < (select zonkerid from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 1 offset 200)", u.UserID, u.UserID)
1797 }
1798}
1799
1800var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1801var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1802var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1803var stmtHonksBySearch, stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1804var stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1805var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1806var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1807var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1808var stmtSelectOnts, stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1809
1810func preparetodie(db *sql.DB, s string) *sql.Stmt {
1811 stmt, err := db.Prepare(s)
1812 if err != nil {
1813 log.Fatalf("error %s: %s", err, s)
1814 }
1815 return stmt
1816}
1817
1818func prepareStatements(db *sql.DB) {
1819 stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1820 stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1821 stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1822 stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1823 stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1824 stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1825
1826 selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore, flags, onts from honks join users on honks.userid = users.userid "
1827 limit := " order by honks.honkid desc limit 250"
1828 butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1829 stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1830 stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1831 stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1832 stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1833 stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1834 stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1835 stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1836 stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1837 stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1838 stmtHonksBySearch = preparetodie(db, selecthonks+"where honks.userid = ? and noise like ?"+limit)
1839 stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1840 stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1841
1842 stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1843 stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1844 stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1845 stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1846 stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1847 stmtZonkIt = preparetodie(db, "delete from honks where honkid = ?")
1848 stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1849 stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1850 stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1851 stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1852 stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1853 stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1854 stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1855 stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1856 stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1857 stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1858 stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1859 stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1860 stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1861 stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1862 stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1863 stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1864 stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? and honker not in (select xid from honkers where userid = ? and flavor = 'sub') order by honkid desc limit 100")
1865 stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1866 stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1867 stmtSelectOnts = preparetodie(db, "select distinct(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2)")
1868}
1869
1870func ElaborateUnitTests() {
1871}
1872
1873func main() {
1874 cmd := "run"
1875 if len(os.Args) > 1 {
1876 cmd = os.Args[1]
1877 }
1878 switch cmd {
1879 case "init":
1880 initdb()
1881 case "upgrade":
1882 upgradedb()
1883 }
1884 db := opendatabase()
1885 dbversion := 0
1886 getconfig("dbversion", &dbversion)
1887 if dbversion != myVersion {
1888 log.Fatal("incorrect database version. run upgrade.")
1889 }
1890 getconfig("servermsg", &serverMsg)
1891 getconfig("servername", &serverName)
1892 getconfig("usersep", &userSep)
1893 getconfig("honksep", &honkSep)
1894 getconfig("dnf", &donotfedafterdark)
1895 prepareStatements(db)
1896 switch cmd {
1897 case "adduser":
1898 adduser()
1899 case "cleanup":
1900 arg := "30"
1901 if len(os.Args) > 2 {
1902 arg = os.Args[2]
1903 }
1904 cleanupdb(arg)
1905 case "ping":
1906 if len(os.Args) < 4 {
1907 fmt.Printf("usage: honk ping from to\n")
1908 return
1909 }
1910 name := os.Args[2]
1911 targ := os.Args[3]
1912 user, err := butwhatabout(name)
1913 if err != nil {
1914 log.Printf("unknown user")
1915 return
1916 }
1917 ping(user, targ)
1918 case "peep":
1919 peeppeep()
1920 case "run":
1921 serve()
1922 case "test":
1923 ElaborateUnitTests()
1924 default:
1925 log.Fatal("unknown command")
1926 }
1927}