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