all repos — honk @ 7022da1a0a30c3f0f9b8d5590bfec0b19e1ba779

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