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