all repos — honk @ cbc2665446d677cb5c022e0f0905b69327b7349c

my fork of honk

database.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/sha512"
  21	"database/sql"
  22	"encoding/json"
  23	"fmt"
  24	"html/template"
  25	"sort"
  26	"strconv"
  27	"strings"
  28	"sync"
  29	"time"
  30
  31	"humungus.tedunangst.com/r/webs/cache"
  32	"humungus.tedunangst.com/r/webs/httpsig"
  33	"humungus.tedunangst.com/r/webs/login"
  34	"humungus.tedunangst.com/r/webs/mz"
  35)
  36
  37func userfromrow(row *sql.Row) (*WhatAbout, error) {
  38	user := new(WhatAbout)
  39	var seckey, options string
  40	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &seckey, &options)
  41	if err == nil {
  42		user.SecKey, _, err = httpsig.DecodeKey(seckey)
  43	}
  44	if err != nil {
  45		return nil, err
  46	}
  47	if user.ID > 0 {
  48		user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
  49		err = unjsonify(options, &user.Options)
  50		if err != nil {
  51			elog.Printf("error processing user options: %s", err)
  52		}
  53	} else {
  54		user.URL = fmt.Sprintf("https://%s/%s", serverName, user.Name)
  55	}
  56	if user.Options.Reaction == "" {
  57		user.Options.Reaction = "none"
  58	}
  59	var marker mz.Marker
  60	marker.HashLinker = ontoreplacer
  61	marker.AtLinker = attoreplacer
  62	user.HTAbout = template.HTML(marker.Mark(user.About))
  63	user.Onts = marker.HashTags
  64
  65	return user, nil
  66}
  67
  68var somenamedusers = cache.New(cache.Options{Filler: func(name string) (*WhatAbout, bool) {
  69	row := stmtUserByName.QueryRow(name)
  70	user, err := userfromrow(row)
  71	if err != nil {
  72		return nil, false
  73	}
  74	return user, true
  75}})
  76
  77var somenumberedusers = cache.New(cache.Options{Filler: func(userid int64) (*WhatAbout, bool) {
  78	row := stmtUserByNumber.QueryRow(userid)
  79	user, err := userfromrow(row)
  80	if err != nil {
  81		return nil, false
  82	}
  83	return user, true
  84}})
  85
  86func getserveruser() *WhatAbout {
  87	var user *WhatAbout
  88	ok := somenumberedusers.Get(serverUID, &user)
  89	if !ok {
  90		elog.Panicf("lost server user")
  91	}
  92	return user
  93}
  94
  95func butwhatabout(name string) (*WhatAbout, error) {
  96	var user *WhatAbout
  97	ok := somenamedusers.Get(name, &user)
  98	if !ok {
  99		return nil, fmt.Errorf("no user: %s", name)
 100	}
 101	return user, nil
 102}
 103
 104var honkerinvalidator cache.Invalidator
 105
 106func gethonkers(userid int64) []*Honker {
 107	rows, err := stmtHonkers.Query(userid)
 108	if err != nil {
 109		elog.Printf("error querying honkers: %s", err)
 110		return nil
 111	}
 112	defer rows.Close()
 113	var honkers []*Honker
 114	for rows.Next() {
 115		h := new(Honker)
 116		var combos, meta string
 117		err = rows.Scan(&h.ID, &h.UserID, &h.Name, &h.XID, &h.Flavor, &combos, &meta)
 118		if err == nil {
 119			err = unjsonify(meta, &h.Meta)
 120		}
 121		if err != nil {
 122			elog.Printf("error scanning honker: %s", err)
 123			continue
 124		}
 125		h.Combos = strings.Split(strings.TrimSpace(combos), " ")
 126		honkers = append(honkers, h)
 127	}
 128	return honkers
 129}
 130
 131func getdubs(userid int64) []*Honker {
 132	rows, err := stmtDubbers.Query(userid)
 133	return dubsfromrows(rows, err)
 134}
 135
 136func getnameddubs(userid int64, name string) []*Honker {
 137	rows, err := stmtNamedDubbers.Query(userid, name)
 138	return dubsfromrows(rows, err)
 139}
 140
 141func dubsfromrows(rows *sql.Rows, err error) []*Honker {
 142	if err != nil {
 143		elog.Printf("error querying dubs: %s", err)
 144		return nil
 145	}
 146	defer rows.Close()
 147	var honkers []*Honker
 148	for rows.Next() {
 149		h := new(Honker)
 150		err = rows.Scan(&h.ID, &h.UserID, &h.Name, &h.XID, &h.Flavor)
 151		if err != nil {
 152			elog.Printf("error scanning honker: %s", err)
 153			return nil
 154		}
 155		honkers = append(honkers, h)
 156	}
 157	return honkers
 158}
 159
 160func allusers() []login.UserInfo {
 161	var users []login.UserInfo
 162	rows, _ := opendatabase().Query("select userid, username from users where userid > 0")
 163	defer rows.Close()
 164	for rows.Next() {
 165		var u login.UserInfo
 166		rows.Scan(&u.UserID, &u.Username)
 167		users = append(users, u)
 168	}
 169	return users
 170}
 171
 172func getxonk(userid int64, xid string) *Honk {
 173	row := stmtOneXonk.QueryRow(userid, xid)
 174	return scanhonk(row)
 175}
 176
 177func getbonk(userid int64, xid string) *Honk {
 178	row := stmtOneBonk.QueryRow(userid, xid)
 179	return scanhonk(row)
 180}
 181
 182func getpublichonks() []*Honk {
 183	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 184	rows, err := stmtPublicHonks.Query(dt, 100)
 185	return getsomehonks(rows, err)
 186}
 187func geteventhonks(userid int64) []*Honk {
 188	rows, err := stmtEventHonks.Query(userid, 25)
 189	honks := getsomehonks(rows, err)
 190	sort.Slice(honks, func(i, j int) bool {
 191		var t1, t2 time.Time
 192		if honks[i].Time == nil {
 193			t1 = honks[i].Date
 194		} else {
 195			t1 = honks[i].Time.StartTime
 196		}
 197		if honks[j].Time == nil {
 198			t2 = honks[j].Date
 199		} else {
 200			t2 = honks[j].Time.StartTime
 201		}
 202		return t1.After(t2)
 203	})
 204	now := time.Now().Add(-24 * time.Hour)
 205	for i, h := range honks {
 206		t := h.Date
 207		if tm := h.Time; tm != nil {
 208			t = tm.StartTime
 209		}
 210		if t.Before(now) {
 211			honks = honks[:i]
 212			break
 213		}
 214	}
 215	reversehonks(honks)
 216	return honks
 217}
 218func gethonksbyuser(name string, includeprivate bool, wanted int64) []*Honk {
 219	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 220	limit := 50
 221	whofore := 2
 222	if includeprivate {
 223		whofore = 3
 224	}
 225	rows, err := stmtUserHonks.Query(wanted, whofore, name, dt, limit)
 226	return getsomehonks(rows, err)
 227}
 228func gethonksforuser(userid int64, wanted int64) []*Honk {
 229	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 230	rows, err := stmtHonksForUser.Query(wanted, userid, dt, userid, userid)
 231	return getsomehonks(rows, err)
 232}
 233func gethonksforuserfirstclass(userid int64, wanted int64) []*Honk {
 234	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 235	rows, err := stmtHonksForUserFirstClass.Query(wanted, userid, dt, userid, userid)
 236	return getsomehonks(rows, err)
 237}
 238
 239func gethonksforme(userid int64, wanted int64) []*Honk {
 240	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 241	rows, err := stmtHonksForMe.Query(wanted, userid, dt, userid)
 242	return getsomehonks(rows, err)
 243}
 244func gethonksfromlongago(userid int64, wanted int64) []*Honk {
 245	now := time.Now().UTC()
 246	now = time.Date(now.Year()-1, now.Month(), now.Day(), now.Hour(), now.Minute(),
 247		now.Second(), 0, now.Location())
 248	dt1 := now.Add(-36 * time.Hour).Format(dbtimeformat)
 249	dt2 := now.Add(12 * time.Hour).Format(dbtimeformat)
 250	rows, err := stmtHonksFromLongAgo.Query(wanted, userid, dt1, dt2, userid)
 251	return getsomehonks(rows, err)
 252}
 253func getsavedhonks(userid int64, wanted int64) []*Honk {
 254	rows, err := stmtHonksISaved.Query(wanted, userid)
 255	return getsomehonks(rows, err)
 256}
 257func gethonksbyhonker(userid int64, honker string, wanted int64) []*Honk {
 258	rows, err := stmtHonksByHonker.Query(wanted, userid, honker, userid)
 259	return getsomehonks(rows, err)
 260}
 261func gethonksbyxonker(userid int64, xonker string, wanted int64) []*Honk {
 262	rows, err := stmtHonksByXonker.Query(wanted, userid, xonker, xonker, userid)
 263	return getsomehonks(rows, err)
 264}
 265func gethonksbycombo(userid int64, combo string, wanted int64) []*Honk {
 266	combo = "% " + combo + " %"
 267	rows, err := stmtHonksByCombo.Query(wanted, userid, userid, combo, userid, wanted, userid, combo, userid)
 268	return getsomehonks(rows, err)
 269}
 270func gethonksbyconvoy(userid int64, convoy string, wanted int64) []*Honk {
 271	rows, err := stmtHonksByConvoy.Query(wanted, userid, userid, convoy)
 272	honks := getsomehonks(rows, err)
 273	return honks
 274}
 275func gethonksbysearch(userid int64, q string, wanted int64) []*Honk {
 276	var queries []string
 277	var params []interface{}
 278	queries = append(queries, "honks.honkid > ?")
 279	params = append(params, wanted)
 280	queries = append(queries, "honks.userid = ?")
 281	params = append(params, userid)
 282
 283	terms := strings.Split(q, " ")
 284	for _, t := range terms {
 285		if t == "" {
 286			continue
 287		}
 288		negate := " "
 289		if t[0] == '-' {
 290			t = t[1:]
 291			negate = " not "
 292		}
 293		if t == "" {
 294			continue
 295		}
 296		if strings.HasPrefix(t, "site:") {
 297			site := t[5:]
 298			site = "%" + site + "%"
 299			queries = append(queries, "xid"+negate+"like ?")
 300			params = append(params, site)
 301			continue
 302		}
 303		if strings.HasPrefix(t, "honker:") {
 304			honker := t[7:]
 305			xid := fullname(honker, userid)
 306			if xid != "" {
 307				honker = xid
 308			}
 309			queries = append(queries, negate+"(honks.honker = ? or honks.oonker = ?)")
 310			params = append(params, honker)
 311			params = append(params, honker)
 312			continue
 313		}
 314		t = "%" + t + "%"
 315		queries = append(queries, "noise"+negate+"like ?")
 316		params = append(params, t)
 317	}
 318
 319	selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, format, convoy, whofore, flags from honks join users on honks.userid = users.userid "
 320	where := "where " + strings.Join(queries, " and ")
 321	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
 322	limit := " order by honks.honkid desc limit 250"
 323	params = append(params, userid)
 324	rows, err := opendatabase().Query(selecthonks+where+butnotthose+limit, params...)
 325	honks := getsomehonks(rows, err)
 326	return honks
 327}
 328func gethonksbyontology(userid int64, name string, wanted int64) []*Honk {
 329	rows, err := stmtHonksByOntology.Query(wanted, name, userid, userid)
 330	honks := getsomehonks(rows, err)
 331	return honks
 332}
 333
 334func reversehonks(honks []*Honk) {
 335	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 336		honks[i], honks[j] = honks[j], honks[i]
 337	}
 338}
 339
 340func getsomehonks(rows *sql.Rows, err error) []*Honk {
 341	if err != nil {
 342		elog.Printf("error querying honks: %s", err)
 343		return nil
 344	}
 345	defer rows.Close()
 346	var honks []*Honk
 347	for rows.Next() {
 348		h := scanhonk(rows)
 349		if h != nil {
 350			honks = append(honks, h)
 351		}
 352	}
 353	rows.Close()
 354	donksforhonks(honks)
 355	return honks
 356}
 357
 358type RowLike interface {
 359	Scan(dest ...interface{}) error
 360}
 361
 362func scanhonk(row RowLike) *Honk {
 363	h := new(Honk)
 364	var dt, aud string
 365	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 366		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Format, &h.Convoy, &h.Whofore, &h.Flags)
 367	if err != nil {
 368		if err != sql.ErrNoRows {
 369			elog.Printf("error scanning honk: %s", err)
 370		}
 371		return nil
 372	}
 373	h.Date, _ = time.Parse(dbtimeformat, dt)
 374	h.Audience = strings.Split(aud, " ")
 375	h.Public = loudandproud(h.Audience)
 376	return h
 377}
 378
 379func donksforhonks(honks []*Honk) {
 380	db := opendatabase()
 381	var ids []string
 382	hmap := make(map[int64]*Honk)
 383	for _, h := range honks {
 384		ids = append(ids, fmt.Sprintf("%d", h.ID))
 385		hmap[h.ID] = h
 386	}
 387	idset := strings.Join(ids, ",")
 388	// grab donks
 389	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, description, url, media, local from donks join filemeta on donks.fileid = filemeta.fileid where honkid in (%s)", idset)
 390	rows, err := db.Query(q)
 391	if err != nil {
 392		elog.Printf("error querying donks: %s", err)
 393		return
 394	}
 395	defer rows.Close()
 396	for rows.Next() {
 397		var hid int64
 398		d := new(Donk)
 399		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local)
 400		if err != nil {
 401			elog.Printf("error scanning donk: %s", err)
 402			continue
 403		}
 404		d.External = !strings.HasPrefix(d.URL, serverPrefix)
 405		h := hmap[hid]
 406		h.Donks = append(h.Donks, d)
 407	}
 408	rows.Close()
 409
 410	// grab onts
 411	q = fmt.Sprintf("select honkid, ontology from onts where honkid in (%s)", idset)
 412	rows, err = db.Query(q)
 413	if err != nil {
 414		elog.Printf("error querying onts: %s", err)
 415		return
 416	}
 417	defer rows.Close()
 418	for rows.Next() {
 419		var hid int64
 420		var o string
 421		err = rows.Scan(&hid, &o)
 422		if err != nil {
 423			elog.Printf("error scanning donk: %s", err)
 424			continue
 425		}
 426		h := hmap[hid]
 427		h.Onts = append(h.Onts, o)
 428	}
 429	rows.Close()
 430
 431	// grab meta
 432	q = fmt.Sprintf("select honkid, genus, json from honkmeta where honkid in (%s)", idset)
 433	rows, err = db.Query(q)
 434	if err != nil {
 435		elog.Printf("error querying honkmeta: %s", err)
 436		return
 437	}
 438	defer rows.Close()
 439	for rows.Next() {
 440		var hid int64
 441		var genus, j string
 442		err = rows.Scan(&hid, &genus, &j)
 443		if err != nil {
 444			elog.Printf("error scanning honkmeta: %s", err)
 445			continue
 446		}
 447		h := hmap[hid]
 448		switch genus {
 449		case "place":
 450			p := new(Place)
 451			err = unjsonify(j, p)
 452			if err != nil {
 453				elog.Printf("error parsing place: %s", err)
 454				continue
 455			}
 456			h.Place = p
 457		case "time":
 458			t := new(Time)
 459			err = unjsonify(j, t)
 460			if err != nil {
 461				elog.Printf("error parsing time: %s", err)
 462				continue
 463			}
 464			h.Time = t
 465		case "mentions":
 466			err = unjsonify(j, &h.Mentions)
 467			if err != nil {
 468				elog.Printf("error parsing mentions: %s", err)
 469				continue
 470			}
 471		case "badonks":
 472			err = unjsonify(j, &h.Badonks)
 473			if err != nil {
 474				elog.Printf("error parsing badonks: %s", err)
 475				continue
 476			}
 477		case "oldrev":
 478		default:
 479			elog.Printf("unknown meta genus: %s", genus)
 480		}
 481	}
 482	rows.Close()
 483}
 484
 485func donksforchonks(chonks []*Chonk) {
 486	db := opendatabase()
 487	var ids []string
 488	chmap := make(map[int64]*Chonk)
 489	for _, ch := range chonks {
 490		ids = append(ids, fmt.Sprintf("%d", ch.ID))
 491		chmap[ch.ID] = ch
 492	}
 493	idset := strings.Join(ids, ",")
 494	// grab donks
 495	q := fmt.Sprintf("select chonkid, donks.fileid, xid, name, description, url, media, local from donks join filemeta on donks.fileid = filemeta.fileid where chonkid in (%s)", idset)
 496	rows, err := db.Query(q)
 497	if err != nil {
 498		elog.Printf("error querying donks: %s", err)
 499		return
 500	}
 501	defer rows.Close()
 502	for rows.Next() {
 503		var chid int64
 504		d := new(Donk)
 505		err = rows.Scan(&chid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local)
 506		if err != nil {
 507			elog.Printf("error scanning donk: %s", err)
 508			continue
 509		}
 510		ch := chmap[chid]
 511		ch.Donks = append(ch.Donks, d)
 512	}
 513}
 514
 515func savefile(name string, desc string, url string, media string, local bool, data []byte) (int64, error) {
 516	fileid, _, err := savefileandxid(name, desc, url, media, local, data)
 517	return fileid, err
 518}
 519
 520func hashfiledata(data []byte) string {
 521	h := sha512.New512_256()
 522	h.Write(data)
 523	return fmt.Sprintf("%x", h.Sum(nil))
 524}
 525
 526func savefileandxid(name string, desc string, url string, media string, local bool, data []byte) (int64, string, error) {
 527	var xid string
 528	if local {
 529		hash := hashfiledata(data)
 530		row := stmtCheckFileData.QueryRow(hash)
 531		err := row.Scan(&xid)
 532		if err == sql.ErrNoRows {
 533			xid = xfiltrate()
 534			switch media {
 535			case "image/png":
 536				xid += ".png"
 537			case "image/jpeg":
 538				xid += ".jpg"
 539			case "application/pdf":
 540				xid += ".pdf"
 541			case "text/plain":
 542				xid += ".txt"
 543			}
 544			_, err = stmtSaveFileData.Exec(xid, media, hash, data)
 545			if err != nil {
 546				return 0, "", err
 547			}
 548		} else if err != nil {
 549			elog.Printf("error checking file hash: %s", err)
 550			return 0, "", err
 551		}
 552		if url == "" {
 553			url = fmt.Sprintf("https://%s/d/%s", serverName, xid)
 554		}
 555	}
 556
 557	res, err := stmtSaveFile.Exec(xid, name, desc, url, media, local)
 558	if err != nil {
 559		return 0, "", err
 560	}
 561	fileid, _ := res.LastInsertId()
 562	return fileid, xid, nil
 563}
 564
 565func finddonk(url string) *Donk {
 566	donk := new(Donk)
 567	row := stmtFindFile.QueryRow(url)
 568	err := row.Scan(&donk.FileID, &donk.XID)
 569	if err == nil {
 570		return donk
 571	}
 572	if err != sql.ErrNoRows {
 573		elog.Printf("error finding file: %s", err)
 574	}
 575	return nil
 576}
 577
 578func savechonk(ch *Chonk) error {
 579	dt := ch.Date.UTC().Format(dbtimeformat)
 580	db := opendatabase()
 581	tx, err := db.Begin()
 582	if err != nil {
 583		elog.Printf("can't begin tx: %s", err)
 584		return err
 585	}
 586
 587	res, err := tx.Stmt(stmtSaveChonk).Exec(ch.UserID, ch.XID, ch.Who, ch.Target, dt, ch.Noise, ch.Format)
 588	if err == nil {
 589		ch.ID, _ = res.LastInsertId()
 590		for _, d := range ch.Donks {
 591			_, err := tx.Stmt(stmtSaveDonk).Exec(-1, ch.ID, d.FileID)
 592			if err != nil {
 593				elog.Printf("error saving donk: %s", err)
 594				break
 595			}
 596		}
 597		chatplusone(tx, ch.UserID)
 598		err = tx.Commit()
 599	} else {
 600		tx.Rollback()
 601	}
 602	return err
 603}
 604
 605func chatplusone(tx *sql.Tx, userid int64) {
 606	var user *WhatAbout
 607	ok := somenumberedusers.Get(userid, &user)
 608	if !ok {
 609		return
 610	}
 611	options := user.Options
 612	options.ChatCount += 1
 613	j, err := jsonify(options)
 614	if err == nil {
 615		_, err = tx.Exec("update users set options = ? where username = ?", j, user.Name)
 616	}
 617	if err != nil {
 618		elog.Printf("error plussing chat: %s", err)
 619	}
 620	somenamedusers.Clear(user.Name)
 621	somenumberedusers.Clear(user.ID)
 622}
 623
 624func chatnewnone(userid int64) {
 625	var user *WhatAbout
 626	ok := somenumberedusers.Get(userid, &user)
 627	if !ok || user.Options.ChatCount == 0 {
 628		return
 629	}
 630	options := user.Options
 631	options.ChatCount = 0
 632	j, err := jsonify(options)
 633	if err == nil {
 634		db := opendatabase()
 635		_, err = db.Exec("update users set options = ? where username = ?", j, user.Name)
 636	}
 637	if err != nil {
 638		elog.Printf("error noneing chat: %s", err)
 639	}
 640	somenamedusers.Clear(user.Name)
 641	somenumberedusers.Clear(user.ID)
 642}
 643
 644func meplusone(tx *sql.Tx, userid int64) {
 645	var user *WhatAbout
 646	ok := somenumberedusers.Get(userid, &user)
 647	if !ok {
 648		return
 649	}
 650	options := user.Options
 651	options.MeCount += 1
 652	j, err := jsonify(options)
 653	if err == nil {
 654		_, err = tx.Exec("update users set options = ? where username = ?", j, user.Name)
 655	}
 656	if err != nil {
 657		elog.Printf("error plussing me: %s", err)
 658	}
 659	somenamedusers.Clear(user.Name)
 660	somenumberedusers.Clear(user.ID)
 661}
 662
 663func menewnone(userid int64) {
 664	var user *WhatAbout
 665	ok := somenumberedusers.Get(userid, &user)
 666	if !ok || user.Options.MeCount == 0 {
 667		return
 668	}
 669	options := user.Options
 670	options.MeCount = 0
 671	j, err := jsonify(options)
 672	if err == nil {
 673		db := opendatabase()
 674		_, err = db.Exec("update users set options = ? where username = ?", j, user.Name)
 675	}
 676	if err != nil {
 677		elog.Printf("error noneing me: %s", err)
 678	}
 679	somenamedusers.Clear(user.Name)
 680	somenumberedusers.Clear(user.ID)
 681}
 682
 683func loadchatter(userid int64) []*Chatter {
 684	duedt := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat)
 685	rows, err := stmtLoadChonks.Query(userid, duedt)
 686	if err != nil {
 687		elog.Printf("error loading chonks: %s", err)
 688		return nil
 689	}
 690	defer rows.Close()
 691	chonks := make(map[string][]*Chonk)
 692	var allchonks []*Chonk
 693	for rows.Next() {
 694		ch := new(Chonk)
 695		var dt string
 696		err = rows.Scan(&ch.ID, &ch.UserID, &ch.XID, &ch.Who, &ch.Target, &dt, &ch.Noise, &ch.Format)
 697		if err != nil {
 698			elog.Printf("error scanning chonk: %s", err)
 699			continue
 700		}
 701		ch.Date, _ = time.Parse(dbtimeformat, dt)
 702		chonks[ch.Target] = append(chonks[ch.Target], ch)
 703		allchonks = append(allchonks, ch)
 704	}
 705	donksforchonks(allchonks)
 706	rows.Close()
 707	rows, err = stmtGetChatters.Query(userid)
 708	if err != nil {
 709		elog.Printf("error getting chatters: %s", err)
 710		return nil
 711	}
 712	for rows.Next() {
 713		var target string
 714		err = rows.Scan(&target)
 715		if err != nil {
 716			elog.Printf("error scanning chatter: %s", target)
 717			continue
 718		}
 719		if _, ok := chonks[target]; !ok {
 720			chonks[target] = []*Chonk{}
 721
 722		}
 723	}
 724	var chatter []*Chatter
 725	for target, chonks := range chonks {
 726		chatter = append(chatter, &Chatter{
 727			Target: target,
 728			Chonks: chonks,
 729		})
 730	}
 731	sort.Slice(chatter, func(i, j int) bool {
 732		a, b := chatter[i], chatter[j]
 733		if len(a.Chonks) == 0 || len(b.Chonks) == 0 {
 734			if len(a.Chonks) == len(b.Chonks) {
 735				return a.Target < b.Target
 736			}
 737			return len(a.Chonks) > len(b.Chonks)
 738		}
 739		return a.Chonks[len(a.Chonks)-1].Date.After(b.Chonks[len(b.Chonks)-1].Date)
 740	})
 741
 742	return chatter
 743}
 744
 745func savehonk(h *Honk) error {
 746	dt := h.Date.UTC().Format(dbtimeformat)
 747	aud := strings.Join(h.Audience, " ")
 748
 749	db := opendatabase()
 750	tx, err := db.Begin()
 751	if err != nil {
 752		elog.Printf("can't begin tx: %s", err)
 753		return err
 754	}
 755
 756	res, err := tx.Stmt(stmtSaveHonk).Exec(h.UserID, h.What, h.Honker, h.XID, h.RID, dt, h.URL,
 757		aud, h.Noise, h.Convoy, h.Whofore, h.Format, h.Precis,
 758		h.Oonker, h.Flags)
 759	if err == nil {
 760		h.ID, _ = res.LastInsertId()
 761		err = saveextras(tx, h)
 762	}
 763	if err == nil {
 764		if h.Whofore == 1 {
 765			meplusone(tx, h.UserID)
 766		}
 767		err = tx.Commit()
 768	} else {
 769		tx.Rollback()
 770	}
 771	if err != nil {
 772		elog.Printf("error saving honk: %s", err)
 773	}
 774	honkhonkline()
 775	return err
 776}
 777
 778func updatehonk(h *Honk) error {
 779	old := getxonk(h.UserID, h.XID)
 780	oldrev := OldRevision{Precis: old.Precis, Noise: old.Noise}
 781	dt := h.Date.UTC().Format(dbtimeformat)
 782
 783	db := opendatabase()
 784	tx, err := db.Begin()
 785	if err != nil {
 786		elog.Printf("can't begin tx: %s", err)
 787		return err
 788	}
 789
 790	err = deleteextras(tx, h.ID, false)
 791	if err == nil {
 792		_, err = tx.Stmt(stmtUpdateHonk).Exec(h.Precis, h.Noise, h.Format, h.Whofore, dt, h.ID)
 793	}
 794	if err == nil {
 795		err = saveextras(tx, h)
 796	}
 797	if err == nil {
 798		var j string
 799		j, err = jsonify(&oldrev)
 800		if err == nil {
 801			_, err = tx.Stmt(stmtSaveMeta).Exec(old.ID, "oldrev", j)
 802		}
 803		if err != nil {
 804			elog.Printf("error saving oldrev: %s", err)
 805		}
 806	}
 807	if err == nil {
 808		err = tx.Commit()
 809	} else {
 810		tx.Rollback()
 811	}
 812	if err != nil {
 813		elog.Printf("error updating honk %d: %s", h.ID, err)
 814	}
 815	return err
 816}
 817
 818func deletehonk(honkid int64) error {
 819	db := opendatabase()
 820	tx, err := db.Begin()
 821	if err != nil {
 822		elog.Printf("can't begin tx: %s", err)
 823		return err
 824	}
 825
 826	err = deleteextras(tx, honkid, true)
 827	if err == nil {
 828		_, err = tx.Stmt(stmtDeleteHonk).Exec(honkid)
 829	}
 830	if err == nil {
 831		err = tx.Commit()
 832	} else {
 833		tx.Rollback()
 834	}
 835	if err != nil {
 836		elog.Printf("error deleting honk %d: %s", honkid, err)
 837	}
 838	return err
 839}
 840
 841func saveextras(tx *sql.Tx, h *Honk) error {
 842	for _, d := range h.Donks {
 843		_, err := tx.Stmt(stmtSaveDonk).Exec(h.ID, -1, d.FileID)
 844		if err != nil {
 845			elog.Printf("error saving donk: %s", err)
 846			return err
 847		}
 848	}
 849	for _, o := range h.Onts {
 850		_, err := tx.Stmt(stmtSaveOnt).Exec(strings.ToLower(o), h.ID)
 851		if err != nil {
 852			elog.Printf("error saving ont: %s", err)
 853			return err
 854		}
 855	}
 856	if p := h.Place; p != nil {
 857		j, err := jsonify(p)
 858		if err == nil {
 859			_, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "place", j)
 860		}
 861		if err != nil {
 862			elog.Printf("error saving place: %s", err)
 863			return err
 864		}
 865	}
 866	if t := h.Time; t != nil {
 867		j, err := jsonify(t)
 868		if err == nil {
 869			_, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "time", j)
 870		}
 871		if err != nil {
 872			elog.Printf("error saving time: %s", err)
 873			return err
 874		}
 875	}
 876	if m := h.Mentions; len(m) > 0 {
 877		j, err := jsonify(m)
 878		if err == nil {
 879			_, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "mentions", j)
 880		}
 881		if err != nil {
 882			elog.Printf("error saving mentions: %s", err)
 883			return err
 884		}
 885	}
 886	return nil
 887}
 888
 889var baxonker sync.Mutex
 890
 891func addreaction(user *WhatAbout, xid string, who, react string) {
 892	baxonker.Lock()
 893	defer baxonker.Unlock()
 894	h := getxonk(user.ID, xid)
 895	if h == nil {
 896		return
 897	}
 898	h.Badonks = append(h.Badonks, Badonk{Who: who, What: react})
 899	j, _ := jsonify(h.Badonks)
 900	db := opendatabase()
 901	tx, _ := db.Begin()
 902	_, _ = tx.Stmt(stmtDeleteOneMeta).Exec(h.ID, "badonks")
 903	_, _ = tx.Stmt(stmtSaveMeta).Exec(h.ID, "badonks", j)
 904	tx.Commit()
 905}
 906
 907func deleteextras(tx *sql.Tx, honkid int64, everything bool) error {
 908	_, err := tx.Stmt(stmtDeleteDonks).Exec(honkid)
 909	if err != nil {
 910		return err
 911	}
 912	_, err = tx.Stmt(stmtDeleteOnts).Exec(honkid)
 913	if err != nil {
 914		return err
 915	}
 916	if everything {
 917		_, err = tx.Stmt(stmtDeleteAllMeta).Exec(honkid)
 918	} else {
 919		_, err = tx.Stmt(stmtDeleteSomeMeta).Exec(honkid)
 920	}
 921	if err != nil {
 922		return err
 923	}
 924	return nil
 925}
 926
 927func jsonify(what interface{}) (string, error) {
 928	var buf bytes.Buffer
 929	e := json.NewEncoder(&buf)
 930	e.SetEscapeHTML(false)
 931	e.SetIndent("", "")
 932	err := e.Encode(what)
 933	return buf.String(), err
 934}
 935
 936func unjsonify(s string, dest interface{}) error {
 937	d := json.NewDecoder(strings.NewReader(s))
 938	err := d.Decode(dest)
 939	return err
 940}
 941
 942func cleanupdb(arg string) {
 943	db := opendatabase()
 944	days, err := strconv.Atoi(arg)
 945	var sqlargs []interface{}
 946	var where string
 947	if err != nil {
 948		honker := arg
 949		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
 950		where = "dt < ? and honker = ?"
 951		sqlargs = append(sqlargs, expdate)
 952		sqlargs = append(sqlargs, honker)
 953	} else {
 954		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
 955		where = "dt < ? and convoy not in (select convoy from honks where flags & 4 or whofore = 2 or whofore = 3)"
 956		sqlargs = append(sqlargs, expdate)
 957	}
 958	doordie(db, "delete from honks where flags & 4 = 0 and whofore = 0 and "+where, sqlargs...)
 959	doordie(db, "delete from donks where honkid > 0 and honkid not in (select honkid from honks)")
 960	doordie(db, "delete from onts where honkid not in (select honkid from honks)")
 961	doordie(db, "delete from honkmeta where honkid not in (select honkid from honks)")
 962
 963	doordie(db, "delete from filemeta where fileid not in (select fileid from donks)")
 964	for _, u := range allusers() {
 965		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)
 966	}
 967
 968	filexids := make(map[string]bool)
 969	blobdb := openblobdb()
 970	rows, err := blobdb.Query("select xid from filedata")
 971	if err != nil {
 972		elog.Fatal(err)
 973	}
 974	for rows.Next() {
 975		var xid string
 976		err = rows.Scan(&xid)
 977		if err != nil {
 978			elog.Fatal(err)
 979		}
 980		filexids[xid] = true
 981	}
 982	rows.Close()
 983	rows, err = db.Query("select xid from filemeta")
 984	for rows.Next() {
 985		var xid string
 986		err = rows.Scan(&xid)
 987		if err != nil {
 988			elog.Fatal(err)
 989		}
 990		delete(filexids, xid)
 991	}
 992	rows.Close()
 993	tx, err := blobdb.Begin()
 994	if err != nil {
 995		elog.Fatal(err)
 996	}
 997	for xid, _ := range filexids {
 998		_, err = tx.Exec("delete from filedata where xid = ?", xid)
 999		if err != nil {
1000			elog.Fatal(err)
1001		}
1002	}
1003	err = tx.Commit()
1004	if err != nil {
1005		elog.Fatal(err)
1006	}
1007}
1008
1009var stmtHonkers, stmtDubbers, stmtNamedDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateHonker *sql.Stmt
1010var stmtDeleteHonker *sql.Stmt
1011var stmtAnyXonk, stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1012var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1013var stmtHonksFromLongAgo *sql.Stmt
1014var stmtHonksByHonker, stmtSaveHonk, stmtUserByName, stmtUserByNumber *sql.Stmt
1015var stmtEventHonks, stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk *sql.Stmt
1016var stmtFindFile, stmtGetFileData, stmtSaveFileData, stmtSaveFile *sql.Stmt
1017var stmtCheckFileData *sql.Stmt
1018var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover, stmtOneHonker *sql.Stmt
1019var stmtUntagged, stmtDeleteHonk, stmtDeleteDonks, stmtDeleteOnts, stmtSaveZonker *sql.Stmt
1020var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1021var stmtAllOnts, stmtSaveOnt, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1022var stmtHonksForUserFirstClass *sql.Stmt
1023var stmtSaveMeta, stmtDeleteAllMeta, stmtDeleteOneMeta, stmtDeleteSomeMeta, stmtUpdateHonk *sql.Stmt
1024var stmtHonksISaved, stmtGetFilters, stmtSaveFilter, stmtDeleteFilter *sql.Stmt
1025var stmtGetTracks *sql.Stmt
1026var stmtSaveChonk, stmtLoadChonks, stmtGetChatters *sql.Stmt
1027
1028func preparetodie(db *sql.DB, s string) *sql.Stmt {
1029	stmt, err := db.Prepare(s)
1030	if err != nil {
1031		elog.Fatalf("error %s: %s", err, s)
1032	}
1033	return stmt
1034}
1035
1036func prepareStatements(db *sql.DB) {
1037	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos, meta from honkers where userid = ? and (flavor = 'presub' or flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1038	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, ?, ?, ?, '')")
1039	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ?, folxid = ? where userid = ? and name = ? and xid = ? and flavor = ?")
1040	stmtUpdateHonker = preparetodie(db, "update honkers set name = ?, combos = ?, meta = ? where honkerid = ? and userid = ?")
1041	stmtDeleteHonker = preparetodie(db, "delete from honkers where honkerid = ?")
1042	stmtOneHonker = preparetodie(db, "select xid from honkers where name = ? and userid = ?")
1043	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1044	stmtNamedDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and name = ? and flavor = 'dub'")
1045
1046	selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, format, convoy, whofore, flags from honks join users on honks.userid = users.userid "
1047	limit := " order by honks.honkid desc limit 250"
1048	smalllimit := " order by honks.honkid desc limit ?"
1049	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1050	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1051	stmtAnyXonk = preparetodie(db, selecthonks+"where xid = ? order by honks.honkid asc")
1052	stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1053	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+smalllimit)
1054	stmtEventHonks = preparetodie(db, selecthonks+"where (whofore = 2 or honks.userid = ?) and what = 'event'"+smalllimit)
1055	stmtUserHonks = preparetodie(db, selecthonks+"where honks.honkid > ? and (whofore = 2 or whofore = ?) and username = ? and dt > ?"+smalllimit)
1056	myhonkers := " and honker in (select xid from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'presub') and combos not like '% - %')"
1057	stmtHonksForUser = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ?"+myhonkers+butnotthose+limit)
1058	stmtHonksForUserFirstClass = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and (what <> 'tonk')"+myhonkers+butnotthose+limit)
1059	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1060	stmtHonksFromLongAgo = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and dt < ? and whofore = 2"+butnotthose+limit)
1061	stmtHonksISaved = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and flags & 4 order by honks.honkid desc")
1062	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.honkid > ? and honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1063	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.honkid > ? and honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1064	stmtHonksByCombo = preparetodie(db, selecthonks+" where honks.honkid > ? and honks.userid = ? and honks.honker in (select xid from honkers where honkers.userid = ? and honkers.combos like ?) "+butnotthose+" union "+selecthonks+"join onts on honks.honkid = onts.honkid where honks.honkid > ? and honks.userid = ? and onts.ontology in (select xid from honkers where combos like ?)"+butnotthose+limit)
1065	stmtHonksByConvoy = preparetodie(db, selecthonks+"where honks.honkid > ? and (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1066	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where honks.honkid > ? and onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1067
1068	stmtSaveMeta = preparetodie(db, "insert into honkmeta (honkid, genus, json) values (?, ?, ?)")
1069	stmtDeleteAllMeta = preparetodie(db, "delete from honkmeta where honkid = ?")
1070	stmtDeleteSomeMeta = preparetodie(db, "delete from honkmeta where honkid = ? and genus not in ('oldrev')")
1071	stmtDeleteOneMeta = preparetodie(db, "delete from honkmeta where honkid = ? and genus = ?")
1072	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1073	stmtDeleteHonk = preparetodie(db, "delete from honks where honkid = ?")
1074	stmtUpdateHonk = preparetodie(db, "update honks set precis = ?, noise = ?, format = ?, whofore = ?, dt = ? where honkid = ?")
1075	stmtSaveOnt = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1076	stmtDeleteOnts = preparetodie(db, "delete from onts where honkid = ?")
1077	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, chonkid, fileid) values (?, ?, ?)")
1078	stmtDeleteDonks = preparetodie(db, "delete from donks where honkid = ?")
1079	stmtSaveFile = preparetodie(db, "insert into filemeta (xid, name, description, url, media, local) values (?, ?, ?, ?, ?, ?)")
1080	blobdb := openblobdb()
1081	stmtSaveFileData = preparetodie(blobdb, "insert into filedata (xid, media, hash, content) values (?, ?, ?, ?)")
1082	stmtCheckFileData = preparetodie(blobdb, "select xid from filedata where hash = ?")
1083	stmtGetFileData = preparetodie(blobdb, "select media, content from filedata where xid = ?")
1084	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1085	stmtFindFile = preparetodie(db, "select fileid, xid from filemeta where url = ? and local = 1")
1086	stmtUserByName = preparetodie(db, "select userid, username, displayname, about, pubkey, seckey, options from users where username = ? and userid > 0")
1087	stmtUserByNumber = preparetodie(db, "select userid, username, displayname, about, pubkey, seckey, options from users where userid = ?")
1088	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, '', '', '', ?)")
1089	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, userid, rcpt, msg) values (?, ?, ?, ?, ?)")
1090	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1091	stmtLoadDoover = preparetodie(db, "select tries, userid, rcpt, msg from doovers where dooverid = ?")
1092	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1093	stmtUntagged = preparetodie(db, "select xid, rid, flags from (select honkid, xid, rid, flags from honks where userid = ? order by honkid desc limit 10000) order by honkid asc")
1094	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1095	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1096	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1097	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1098	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor, dt) values (?, ?, ?, ?)")
1099	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ? and dt < ?")
1100	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")
1101	stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1102	stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1103	stmtAllOnts = preparetodie(db, "select ontology, count(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2) group by ontology")
1104	stmtGetFilters = preparetodie(db, "select hfcsid, json from hfcs where userid = ?")
1105	stmtSaveFilter = preparetodie(db, "insert into hfcs (userid, json) values (?, ?)")
1106	stmtDeleteFilter = preparetodie(db, "delete from hfcs where userid = ? and hfcsid = ?")
1107	stmtGetTracks = preparetodie(db, "select fetches from tracks where xid = ?")
1108	stmtSaveChonk = preparetodie(db, "insert into chonks (userid, xid, who, target, dt, noise, format) values (?, ?, ?, ?, ?, ?, ?)")
1109	stmtLoadChonks = preparetodie(db, "select chonkid, userid, xid, who, target, dt, noise, format from chonks where userid = ? and dt > ? order by chonkid asc")
1110	stmtGetChatters = preparetodie(db, "select distinct(target) from chonks where userid = ?")
1111}