all repos — honk @ b82c6fd98fb905ceb173ed95d4b52dd2fb874109

my fork of honk

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	if thoudostbitethythumb(user.ID, who) {
 324		log.Printf("ignoring thumb sucker %s", who)
 325		return
 326	}
 327	fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 328	WriteJunk(fd, j)
 329	io.WriteString(fd, "\n")
 330	fd.Close()
 331	switch what {
 332	case "Ping":
 333		obj, _ := jsongetstring(j, "id")
 334		log.Printf("ping from %s: %s", who, obj)
 335		pong(user, who, obj)
 336	case "Pong":
 337		obj, _ := jsongetstring(j, "object")
 338		log.Printf("pong from %s: %s", who, obj)
 339	case "Follow":
 340		log.Printf("updating honker follow: %s", who)
 341		rubadubdub(user, j)
 342	case "Accept":
 343		db := opendatabase()
 344		log.Printf("updating honker accept: %s", who)
 345		db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
 346	case "Undo":
 347		obj, ok := jsongetmap(j, "object")
 348		if !ok {
 349			log.Printf("unknown undo no object")
 350		} else {
 351			what, _ := jsongetstring(obj, "type")
 352			switch what {
 353			case "Follow":
 354				log.Printf("updating honker undo: %s", who)
 355				db := opendatabase()
 356				db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
 357			case "Like":
 358			default:
 359				log.Printf("unknown undo: %s", what)
 360			}
 361		}
 362	default:
 363		xonk := xonkxonk(j)
 364		if xonk != nil && needxonk(user, xonk) {
 365			xonk.UserID = user.ID
 366			savexonk(user, xonk)
 367		}
 368	}
 369}
 370
 371func outbox(w http.ResponseWriter, r *http.Request) {
 372	name := mux.Vars(r)["name"]
 373	user, err := butwhatabout(name)
 374	if err != nil {
 375		http.NotFound(w, r)
 376		return
 377	}
 378	honks := gethonksbyuser(name)
 379
 380	var jonks []map[string]interface{}
 381	for _, h := range honks {
 382		j, _ := jonkjonk(user, h)
 383		jonks = append(jonks, j)
 384	}
 385
 386	j := NewJunk()
 387	j["@context"] = itiswhatitis
 388	j["id"] = user.URL + "/outbox"
 389	j["type"] = "OrderedCollection"
 390	j["totalItems"] = len(jonks)
 391	j["orderedItems"] = jonks
 392
 393	w.Header().Set("Cache-Control", "max-age=60")
 394	w.Header().Set("Content-Type", theonetruename)
 395	WriteJunk(w, j)
 396}
 397
 398func viewuser(w http.ResponseWriter, r *http.Request) {
 399	name := mux.Vars(r)["name"]
 400	user, err := butwhatabout(name)
 401	if err != nil {
 402		http.NotFound(w, r)
 403		return
 404	}
 405	if friendorfoe(r.Header.Get("Accept")) {
 406		j := asjonker(user)
 407		w.Header().Set("Cache-Control", "max-age=600")
 408		w.Header().Set("Content-Type", theonetruename)
 409		WriteJunk(w, j)
 410		return
 411	}
 412	honks := gethonksbyuser(name)
 413	u := login.GetUserInfo(r)
 414	honkpage(w, r, u, user, honks, "")
 415}
 416
 417func viewhonker(w http.ResponseWriter, r *http.Request) {
 418	name := mux.Vars(r)["name"]
 419	u := login.GetUserInfo(r)
 420	honks := gethonksbyhonker(u.UserID, name)
 421	honkpage(w, r, u, nil, honks, "honks by honker: " + name)
 422}
 423
 424func viewcombo(w http.ResponseWriter, r *http.Request) {
 425	name := mux.Vars(r)["name"]
 426	u := login.GetUserInfo(r)
 427	honks := gethonksbycombo(u.UserID, name)
 428	honkpage(w, r, u, nil, honks, "honks by combo: " + name)
 429}
 430func viewconvoy(w http.ResponseWriter, r *http.Request) {
 431	c := r.FormValue("c")
 432	var userid int64 = -1
 433	u := login.GetUserInfo(r)
 434	if u != nil {
 435		userid = u.UserID
 436	}
 437	honks := gethonksbyconvoy(userid, c)
 438	honkpage(w, r, u, nil, honks, "honks in convoy: " + c)
 439}
 440
 441func fingerlicker(w http.ResponseWriter, r *http.Request) {
 442	orig := r.FormValue("resource")
 443
 444	log.Printf("finger lick: %s", orig)
 445
 446	if strings.HasPrefix(orig, "acct:") {
 447		orig = orig[5:]
 448	}
 449
 450	name := orig
 451	idx := strings.LastIndexByte(name, '/')
 452	if idx != -1 {
 453		name = name[idx+1:]
 454		if "https://"+serverName+"/u/"+name != orig {
 455			log.Printf("foreign request rejected")
 456			name = ""
 457		}
 458	} else {
 459		idx = strings.IndexByte(name, '@')
 460		if idx != -1 {
 461			name = name[:idx]
 462			if name+"@"+serverName != orig {
 463				log.Printf("foreign request rejected")
 464				name = ""
 465			}
 466		}
 467	}
 468	user, err := butwhatabout(name)
 469	if err != nil {
 470		http.NotFound(w, r)
 471		return
 472	}
 473
 474	j := NewJunk()
 475	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
 476	j["aliases"] = []string{user.URL}
 477	var links []map[string]interface{}
 478	l := NewJunk()
 479	l["rel"] = "self"
 480	l["type"] = `application/activity+json`
 481	l["href"] = user.URL
 482	links = append(links, l)
 483	j["links"] = links
 484
 485	w.Header().Set("Cache-Control", "max-age=3600")
 486	w.Header().Set("Content-Type", "application/jrd+json")
 487	WriteJunk(w, j)
 488}
 489
 490func viewhonk(w http.ResponseWriter, r *http.Request) {
 491	name := mux.Vars(r)["name"]
 492	xid := mux.Vars(r)["xid"]
 493	user, err := butwhatabout(name)
 494	if err != nil {
 495		http.NotFound(w, r)
 496		return
 497	}
 498	h := getxonk(name, xid)
 499	if h == nil {
 500		http.NotFound(w, r)
 501		return
 502	}
 503	if friendorfoe(r.Header.Get("Accept")) {
 504		_, j := jonkjonk(user, h)
 505		j["@context"] = itiswhatitis
 506		w.Header().Set("Cache-Control", "max-age=3600")
 507		w.Header().Set("Content-Type", theonetruename)
 508		WriteJunk(w, j)
 509		return
 510	}
 511	u := login.GetUserInfo(r)
 512	honkpage(w, r, u, nil, []*Honk{h}, "one honk")
 513}
 514
 515func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 516honks []*Honk, infomsg string) {
 517	reverbolate(honks)
 518	templinfo := getInfo(r)
 519	if u != nil {
 520		if user != nil && u.Username == user.Name {
 521			templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
 522		}
 523		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 524	}
 525	if u == nil {
 526		w.Header().Set("Cache-Control", "max-age=60")
 527	}
 528	if user != nil {
 529		templinfo["Name"] = user.Name
 530		whatabout := user.About
 531		templinfo["RawWhatAbout"] = whatabout
 532		whatabout = obfusbreak(whatabout)
 533		templinfo["WhatAbout"] = cleanstring(whatabout)
 534	}
 535	templinfo["Honks"] = honks
 536	templinfo["ServerMessage"] = infomsg
 537	err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
 538	if err != nil {
 539		log.Print(err)
 540	}
 541}
 542
 543func saveuser(w http.ResponseWriter, r *http.Request) {
 544	whatabout := r.FormValue("whatabout")
 545	u := login.GetUserInfo(r)
 546	db := opendatabase()
 547	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 548	if err != nil {
 549		log.Printf("error bouting what: %s", err)
 550	}
 551
 552	http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
 553}
 554
 555func gethonkers(userid int64) []*Honker {
 556	rows, err := stmtHonkers.Query(userid)
 557	if err != nil {
 558		log.Printf("error querying honkers: %s", err)
 559		return nil
 560	}
 561	defer rows.Close()
 562	var honkers []*Honker
 563	for rows.Next() {
 564		var f Honker
 565		var combos string
 566		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 567		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 568		if err != nil {
 569			log.Printf("error scanning honker: %s", err)
 570			return nil
 571		}
 572		honkers = append(honkers, &f)
 573	}
 574	return honkers
 575}
 576
 577func getdubs(userid int64) []*Honker {
 578	rows, err := stmtDubbers.Query(userid)
 579	if err != nil {
 580		log.Printf("error querying dubs: %s", err)
 581		return nil
 582	}
 583	defer rows.Close()
 584	var honkers []*Honker
 585	for rows.Next() {
 586		var f Honker
 587		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 588		if err != nil {
 589			log.Printf("error scanning honker: %s", err)
 590			return nil
 591		}
 592		honkers = append(honkers, &f)
 593	}
 594	return honkers
 595}
 596
 597func getxonk(name, xid string) *Honk {
 598	var h Honk
 599	var dt, aud string
 600	row := stmtOneXonk.QueryRow(xid)
 601	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 602		&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 603	if err != nil {
 604		if err != sql.ErrNoRows {
 605			log.Printf("error scanning xonk: %s", err)
 606		}
 607		return nil
 608	}
 609	if name != "" && h.Username != name {
 610		log.Printf("user xonk mismatch")
 611		return nil
 612	}
 613	h.Date, _ = time.Parse(dbtimeformat, dt)
 614	h.Audience = strings.Split(aud, " ")
 615	donksforhonks([]*Honk{&h})
 616	return &h
 617}
 618
 619func getpublichonks() []*Honk {
 620	rows, err := stmtPublicHonks.Query()
 621	return getsomehonks(rows, err)
 622}
 623func gethonksbyuser(name string) []*Honk {
 624	rows, err := stmtUserHonks.Query(name)
 625	return getsomehonks(rows, err)
 626}
 627func gethonksforuser(userid int64) []*Honk {
 628	dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
 629	rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
 630	return getsomehonks(rows, err)
 631}
 632func gethonksforme(userid int64) []*Honk {
 633	dt := time.Now().UTC().Add(-4 * 24 * time.Hour)
 634	rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
 635	return getsomehonks(rows, err)
 636}
 637func gethonksbyhonker(userid int64, honker string) []*Honk {
 638	rows, err := stmtHonksByHonker.Query(userid, honker)
 639	return getsomehonks(rows, err)
 640}
 641func gethonksbycombo(userid int64, combo string) []*Honk {
 642	combo = "% " + combo + " %"
 643	rows, err := stmtHonksByCombo.Query(userid, combo)
 644	return getsomehonks(rows, err)
 645}
 646func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 647	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 648	return getsomehonks(rows, err)
 649}
 650
 651func getsomehonks(rows *sql.Rows, err error) []*Honk {
 652	if err != nil {
 653		log.Printf("error querying honks: %s", err)
 654		return nil
 655	}
 656	defer rows.Close()
 657	var honks []*Honk
 658	for rows.Next() {
 659		var h Honk
 660		var dt, aud string
 661		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 662			&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 663		if err != nil {
 664			log.Printf("error scanning honks: %s", err)
 665			return nil
 666		}
 667		h.Date, _ = time.Parse(dbtimeformat, dt)
 668		h.Audience = strings.Split(aud, " ")
 669		honks = append(honks, &h)
 670	}
 671	rows.Close()
 672	donksforhonks(honks)
 673	return honks
 674}
 675
 676func donksforhonks(honks []*Honk) {
 677	db := opendatabase()
 678	var ids []string
 679	hmap := make(map[int64]*Honk)
 680	for _, h := range honks {
 681		if h.What == "zonk" {
 682			continue
 683		}
 684		ids = append(ids, fmt.Sprintf("%d", h.ID))
 685		hmap[h.ID] = h
 686	}
 687	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, ","))
 688	rows, err := db.Query(q)
 689	if err != nil {
 690		log.Printf("error querying donks: %s", err)
 691		return
 692	}
 693	defer rows.Close()
 694	for rows.Next() {
 695		var hid int64
 696		var d Donk
 697		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 698		if err != nil {
 699			log.Printf("error scanning donk: %s", err)
 700			continue
 701		}
 702		h := hmap[hid]
 703		h.Donks = append(h.Donks, &d)
 704	}
 705}
 706
 707func savebonk(w http.ResponseWriter, r *http.Request) {
 708	xid := r.FormValue("xid")
 709
 710	log.Printf("bonking %s", xid)
 711
 712	xonk := getxonk("", xid)
 713	if xonk == nil {
 714		return
 715	}
 716	if xonk.Honker == "" {
 717		xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
 718	}
 719	convoy := xonk.Convoy
 720
 721	userinfo := login.GetUserInfo(r)
 722
 723	dt := time.Now().UTC()
 724	bonk := Honk{
 725		UserID:   userinfo.UserID,
 726		Username: userinfo.Username,
 727		Honker:   xonk.Honker,
 728		What:     "bonk",
 729		XID:      xonk.XID,
 730		Date:     dt,
 731		Noise:    xonk.Noise,
 732		Convoy:   convoy,
 733		Donks:    xonk.Donks,
 734		Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
 735	}
 736
 737	user, _ := butwhatabout(userinfo.Username)
 738
 739	aud := strings.Join(bonk.Audience, " ")
 740	whofore := 0
 741	if strings.Contains(aud, user.URL) {
 742		whofore = 1
 743	}
 744	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
 745		dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
 746	if err != nil {
 747		log.Printf("error saving bonk: %s", err)
 748		return
 749	}
 750	bonk.ID, _ = res.LastInsertId()
 751	for _, d := range bonk.Donks {
 752		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 753		if err != nil {
 754			log.Printf("err saving donk: %s", err)
 755			return
 756		}
 757	}
 758
 759	go honkworldwide(user, &bonk)
 760
 761}
 762
 763func zonkit(w http.ResponseWriter, r *http.Request) {
 764	xid := r.FormValue("xid")
 765
 766	log.Printf("zonking %s", xid)
 767	userinfo := login.GetUserInfo(r)
 768	stmtZonkIt.Exec(userinfo.UserID, xid)
 769}
 770
 771func savehonk(w http.ResponseWriter, r *http.Request) {
 772	rid := r.FormValue("rid")
 773	noise := r.FormValue("noise")
 774
 775	userinfo := login.GetUserInfo(r)
 776
 777	dt := time.Now().UTC()
 778	xid := xfiltrate()
 779	what := "honk"
 780	if rid != "" {
 781		what = "tonk"
 782	}
 783	honk := Honk{
 784		UserID:   userinfo.UserID,
 785		Username: userinfo.Username,
 786		What:     "honk",
 787		XID:      xid,
 788		RID:      rid,
 789		Date:     dt,
 790	}
 791	if noise[0] == '@' {
 792		honk.Audience = append(grapevine(noise), thewholeworld)
 793	} else {
 794		honk.Audience = prepend(thewholeworld, grapevine(noise))
 795	}
 796	var convoy string
 797	if rid != "" {
 798		xonk := getxonk("", rid)
 799		if xonk != nil {
 800			honk.Audience = append(honk.Audience, xonk.Audience...)
 801			convoy = xonk.Convoy
 802		} else {
 803			xonkaud, c := whosthere(rid)
 804			honk.Audience = append(honk.Audience, xonkaud...)
 805			convoy = c
 806		}
 807	}
 808	if convoy == "" {
 809		convoy = "data:,electrichonkytonk-" + xfiltrate()
 810	}
 811	honk.Audience = oneofakind(honk.Audience)
 812	noise = obfusbreak(noise)
 813	honk.Noise = noise
 814	honk.Convoy = convoy
 815
 816	file, filehdr, err := r.FormFile("donk")
 817	if err == nil {
 818		var buf bytes.Buffer
 819		io.Copy(&buf, file)
 820		file.Close()
 821		data := buf.Bytes()
 822		xid := xfiltrate()
 823		var media, name string
 824		img, format, err := image.Decode(&buf)
 825		if err == nil {
 826			data, format, err = vacuumwrap(img, format)
 827			if err != nil {
 828				log.Printf("can't vacuum image: %s", err)
 829				return
 830			}
 831			media = "image/" + format
 832			if format == "jpeg" {
 833				format = "jpg"
 834			}
 835			name = xid + "." + format
 836			xid = name
 837		} else {
 838			maxsize := 100000
 839			if len(data) > maxsize {
 840				log.Printf("bad image: %s too much text: %d", err, len(data))
 841				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 842				return
 843			}
 844			for i := 0; i < len(data); i++ {
 845				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 846					log.Printf("bad image: %s not text: %d", err, data[i])
 847					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 848					return
 849				}
 850			}
 851			media = "text/plain"
 852			name = filehdr.Filename
 853			if name == "" {
 854				name = xid + ".txt"
 855			}
 856			xid += ".txt"
 857		}
 858		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 859		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 860		if err != nil {
 861			log.Printf("unable to save image: %s", err)
 862			return
 863		}
 864		var d Donk
 865		d.FileID, _ = res.LastInsertId()
 866		d.XID = name
 867		d.Name = name
 868		d.Media = media
 869		d.URL = url
 870		honk.Donks = append(honk.Donks, &d)
 871	}
 872	herd := herdofemus(honk.Noise)
 873	for _, e := range herd {
 874		donk := savedonk(e.ID, e.Name, "image/png")
 875		if donk != nil {
 876			donk.Name = e.Name
 877			honk.Donks = append(honk.Donks, donk)
 878		}
 879	}
 880
 881	user, _ := butwhatabout(userinfo.Username)
 882
 883	aud := strings.Join(honk.Audience, " ")
 884	whofore := 0
 885	if strings.Contains(aud, user.URL) {
 886		whofore = 1
 887	}
 888	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 889		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
 890	if err != nil {
 891		log.Printf("error saving honk: %s", err)
 892		return
 893	}
 894	honk.ID, _ = res.LastInsertId()
 895	for _, d := range honk.Donks {
 896		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 897		if err != nil {
 898			log.Printf("err saving donk: %s", err)
 899			return
 900		}
 901	}
 902
 903	go honkworldwide(user, &honk)
 904
 905	http.Redirect(w, r, "/", http.StatusSeeOther)
 906}
 907
 908func viewhonkers(w http.ResponseWriter, r *http.Request) {
 909	userinfo := login.GetUserInfo(r)
 910	templinfo := getInfo(r)
 911	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 912	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 913	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 914	if err != nil {
 915		log.Print(err)
 916	}
 917}
 918
 919var handfull = make(map[string]string)
 920var handlock sync.Mutex
 921
 922func gofish(name string) string {
 923	if name[0] == '@' {
 924		name = name[1:]
 925	}
 926	m := strings.Split(name, "@")
 927	if len(m) != 2 {
 928		log.Printf("bad fish name: %s", name)
 929		return ""
 930	}
 931	handlock.Lock()
 932	ref, ok := handfull[name]
 933	handlock.Unlock()
 934	if ok {
 935		return ref
 936	}
 937	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 938	handlock.Lock()
 939	defer handlock.Unlock()
 940	if err != nil {
 941		log.Printf("failed to go fish %s: %s", name, err)
 942		handfull[name] = ""
 943		return ""
 944	}
 945	links, _ := jsongetarray(j, "links")
 946	for _, l := range links {
 947		href, _ := jsongetstring(l, "href")
 948		rel, _ := jsongetstring(l, "rel")
 949		t, _ := jsongetstring(l, "type")
 950		if rel == "self" && friendorfoe(t) {
 951			handfull[name] = href
 952			return href
 953		}
 954	}
 955	handfull[name] = ""
 956	return ""
 957}
 958
 959func savehonker(w http.ResponseWriter, r *http.Request) {
 960	u := login.GetUserInfo(r)
 961	name := r.FormValue("name")
 962	url := r.FormValue("url")
 963	peep := r.FormValue("peep")
 964	combos := r.FormValue("combos")
 965	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 966
 967	if honkerid > 0 {
 968		combos = " " + strings.TrimSpace(combos) + " "
 969		_, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
 970		if err != nil {
 971			log.Printf("update honker err: %s", err)
 972			return
 973		}
 974		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 975	}
 976
 977	flavor := "presub"
 978	if peep == "peep" {
 979		flavor = "peep"
 980	}
 981	if url == "" {
 982		return
 983	}
 984	if url[0] == '@' {
 985		url = gofish(url)
 986	}
 987	if url == "" {
 988		return
 989	}
 990	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
 991	if err != nil {
 992		log.Print(err)
 993		return
 994	}
 995	if flavor == "presub" {
 996		user, _ := butwhatabout(u.Username)
 997		go subsub(user, url)
 998	}
 999	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1000}
1001
1002type Zonker struct {
1003	Name      string
1004	Wherefore string
1005}
1006
1007func killzone(w http.ResponseWriter, r *http.Request) {
1008	db := opendatabase()
1009	userinfo := login.GetUserInfo(r)
1010	rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1011	if err != nil {
1012		log.Printf("err: %s", err)
1013		return
1014	}
1015	var zonkers []Zonker
1016	for rows.Next() {
1017		var z Zonker
1018		rows.Scan(&z.Name, &z.Wherefore)
1019		zonkers = append(zonkers, z)
1020	}
1021	templinfo := getInfo(r)
1022	templinfo["Zonkers"] = zonkers
1023	templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1024	err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1025	if err != nil {
1026		log.Print(err)
1027	}
1028}
1029
1030func killitwithfire(w http.ResponseWriter, r *http.Request) {
1031	userinfo := login.GetUserInfo(r)
1032	wherefore := r.FormValue("wherefore")
1033	name := r.FormValue("name")
1034	if name == "" {
1035		return
1036	}
1037	switch wherefore {
1038	case "zonker":
1039	case "zurl":
1040	case "zonvoy":
1041	default:
1042		return
1043	}
1044	db := opendatabase()
1045	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1046		userinfo.UserID, name, wherefore)
1047
1048	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1049}
1050
1051func somedays() string {
1052	secs := 432000 + notrand.Int63n(432000)
1053	return fmt.Sprintf("%d", secs)
1054}
1055
1056func avatate(w http.ResponseWriter, r *http.Request) {
1057	n := r.FormValue("a")
1058	a := avatar(n)
1059	w.Header().Set("Cache-Control", "max-age="+somedays())
1060	w.Write(a)
1061}
1062
1063func servecss(w http.ResponseWriter, r *http.Request) {
1064	w.Header().Set("Cache-Control", "max-age=7776000")
1065	http.ServeFile(w, r, "views"+r.URL.Path)
1066}
1067func servehtml(w http.ResponseWriter, r *http.Request) {
1068	templinfo := getInfo(r)
1069	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1070	if err != nil {
1071		log.Print(err)
1072	}
1073}
1074func serveemu(w http.ResponseWriter, r *http.Request) {
1075	xid := mux.Vars(r)["xid"]
1076	w.Header().Set("Cache-Control", "max-age="+somedays())
1077	http.ServeFile(w, r, "emus/"+xid)
1078}
1079
1080func servefile(w http.ResponseWriter, r *http.Request) {
1081	xid := mux.Vars(r)["xid"]
1082	row := stmtFileData.QueryRow(xid)
1083	var media string
1084	var data []byte
1085	err := row.Scan(&media, &data)
1086	if err != nil {
1087		log.Printf("error loading file: %s", err)
1088		http.NotFound(w, r)
1089		return
1090	}
1091	w.Header().Set("Content-Type", media)
1092	w.Header().Set("X-Content-Type-Options", "nosniff")
1093	w.Header().Set("Cache-Control", "max-age="+somedays())
1094	w.Write(data)
1095}
1096
1097func serve() {
1098	db := opendatabase()
1099	login.Init(db)
1100
1101	listener, err := openListener()
1102	if err != nil {
1103		log.Fatal(err)
1104	}
1105	go redeliverator()
1106
1107	debug := false
1108	getconfig("debug", &debug)
1109	readviews = ParseTemplates(debug,
1110		"views/honkpage.html",
1111		"views/honkers.html",
1112		"views/zonkers.html",
1113		"views/honkform.html",
1114		"views/honk.html",
1115		"views/login.html",
1116		"views/header.html",
1117	)
1118	if !debug {
1119		s := "views/style.css"
1120		savedstyleparams[s] = getstyleparam(s)
1121		s = "views/local.css"
1122		savedstyleparams[s] = getstyleparam(s)
1123	}
1124
1125	mux := mux.NewRouter()
1126	mux.Use(login.Checker)
1127
1128	posters := mux.Methods("POST").Subrouter()
1129	getters := mux.Methods("GET").Subrouter()
1130
1131	getters.HandleFunc("/", homepage)
1132	getters.HandleFunc("/rss", showrss)
1133	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1134	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1135	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1136	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1137	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1138	getters.HandleFunc("/a", avatate)
1139	getters.HandleFunc("/t", viewconvoy)
1140	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1141	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1142	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1143
1144	getters.HandleFunc("/style.css", servecss)
1145	getters.HandleFunc("/local.css", servecss)
1146	getters.HandleFunc("/login", servehtml)
1147	posters.HandleFunc("/dologin", login.LoginFunc)
1148	getters.HandleFunc("/logout", login.LogoutFunc)
1149
1150	loggedin := mux.NewRoute().Subrouter()
1151	loggedin.Use(login.Required)
1152	loggedin.HandleFunc("/atme", homepage)
1153	loggedin.HandleFunc("/killzone", killzone)
1154	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1155	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1156	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1157	loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1158	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1159	loggedin.HandleFunc("/honkers", viewhonkers)
1160	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1161	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1162	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1163
1164	err = http.Serve(listener, mux)
1165	if err != nil {
1166		log.Fatal(err)
1167	}
1168}
1169
1170var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1171var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1172var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1173var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1174var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1175var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1176var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1177
1178func preparetodie(db *sql.DB, s string) *sql.Stmt {
1179	stmt, err := db.Prepare(s)
1180	if err != nil {
1181		log.Fatalf("error %s: %s", err, s)
1182	}
1183	return stmt
1184}
1185
1186func prepareStatements(db *sql.DB) {
1187	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1188	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1189	stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1190	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1191	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1192
1193	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 "
1194	limit := " order by honkid desc limit "
1195	stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1196	stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1197	stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1198	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")
1199	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")
1200	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1201	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1202	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1203
1204	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1205	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1206	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1207	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1208	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ? and userid = ?")
1209	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1210	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1211	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1212	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1213	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1214	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1215	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1216	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1217	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1218	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1219}
1220
1221func ElaborateUnitTests() {
1222}
1223
1224func finishusersetup() error {
1225	db := opendatabase()
1226	k, err := rsa.GenerateKey(rand.Reader, 2048)
1227	if err != nil {
1228		return err
1229	}
1230	pubkey, err := zem(&k.PublicKey)
1231	if err != nil {
1232		return err
1233	}
1234	seckey, err := zem(k)
1235	if err != nil {
1236		return err
1237	}
1238	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1239	if err != nil {
1240		return err
1241	}
1242	return nil
1243}
1244
1245func main() {
1246	cmd := "run"
1247	if len(os.Args) > 1 {
1248		cmd = os.Args[1]
1249	}
1250	switch cmd {
1251	case "init":
1252		initdb()
1253	case "upgrade":
1254		upgradedb()
1255	}
1256	db := opendatabase()
1257	dbversion := 0
1258	getconfig("dbversion", &dbversion)
1259	if dbversion != myVersion {
1260		log.Fatal("incorrect database version. run upgrade.")
1261	}
1262	getconfig("servername", &serverName)
1263	prepareStatements(db)
1264	switch cmd {
1265	case "ping":
1266		if len(os.Args) < 4 {
1267			fmt.Printf("usage: honk ping from to\n")
1268			return
1269		}
1270		name := os.Args[2]
1271		targ := os.Args[3]
1272		user, err := butwhatabout(name)
1273		if err != nil {
1274			log.Printf("unknown user")
1275			return
1276		}
1277		ping(user, targ)
1278	case "peep":
1279		peeppeep()
1280	case "run":
1281		serve()
1282	case "test":
1283		ElaborateUnitTests()
1284	default:
1285		log.Fatal("unknown command")
1286	}
1287}