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