all repos — honk @ dc922e7d5b66206ef04e2e2f36a13d178d78bf10

my fork of honk

activity.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/rsa"
  21	"database/sql"
  22	"errors"
  23	"fmt"
  24	"html"
  25	"io"
  26	"log"
  27	notrand "math/rand"
  28	"net/http"
  29	"net/url"
  30	"os"
  31	"regexp"
  32	"strings"
  33	"time"
  34
  35	"humungus.tedunangst.com/r/webs/cache"
  36	"humungus.tedunangst.com/r/webs/gate"
  37	"humungus.tedunangst.com/r/webs/httpsig"
  38	"humungus.tedunangst.com/r/webs/junk"
  39)
  40
  41var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
  42var thefakename = `application/activity+json`
  43var falsenames = []string{
  44	`application/ld+json`,
  45	`application/activity+json`,
  46}
  47var itiswhatitis = "https://www.w3.org/ns/activitystreams"
  48var thewholeworld = "https://www.w3.org/ns/activitystreams#Public"
  49
  50func friendorfoe(ct string) bool {
  51	ct = strings.ToLower(ct)
  52	for _, at := range falsenames {
  53		if strings.HasPrefix(ct, at) {
  54			return true
  55		}
  56	}
  57	return false
  58}
  59
  60func PostJunk(keyname string, key *rsa.PrivateKey, url string, j junk.Junk) error {
  61	return PostMsg(keyname, key, url, j.ToBytes())
  62}
  63
  64func PostMsg(keyname string, key *rsa.PrivateKey, url string, msg []byte) error {
  65	client := http.DefaultClient
  66	req, err := http.NewRequest("POST", url, bytes.NewReader(msg))
  67	if err != nil {
  68		return err
  69	}
  70	req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
  71	req.Header.Set("Content-Type", theonetruename)
  72	httpsig.SignRequest(keyname, key, req, msg)
  73	resp, err := client.Do(req)
  74	if err != nil {
  75		return err
  76	}
  77	resp.Body.Close()
  78	switch resp.StatusCode {
  79	case 200:
  80	case 201:
  81	case 202:
  82	default:
  83		return fmt.Errorf("http post status: %d", resp.StatusCode)
  84	}
  85	log.Printf("successful post: %s %d", url, resp.StatusCode)
  86	return nil
  87}
  88
  89type JunkError struct {
  90	Junk junk.Junk
  91	Err  error
  92}
  93
  94func GetJunk(url string) (junk.Junk, error) {
  95	return GetJunkTimeout(url, 30*time.Second)
  96}
  97
  98func GetJunkFast(url string) (junk.Junk, error) {
  99	return GetJunkTimeout(url, 5*time.Second)
 100}
 101
 102func GetJunkHardMode(url string) (junk.Junk, error) {
 103	j, err := GetJunk(url)
 104	if err != nil {
 105		emsg := err.Error()
 106		if emsg == "http get status: 502" || strings.Contains(emsg, "timeout") {
 107			log.Printf("trying again after error: %s", emsg)
 108			time.Sleep(time.Duration(60+notrand.Int63n(60)) * time.Second)
 109			j, err = GetJunk(url)
 110			if err != nil {
 111				log.Printf("still couldn't get it")
 112			} else {
 113				log.Printf("retry success!")
 114			}
 115		}
 116	}
 117	return j, err
 118}
 119
 120var flightdeck = gate.NewSerializer()
 121
 122func GetJunkTimeout(url string, timeout time.Duration) (junk.Junk, error) {
 123
 124	fn := func() (interface{}, error) {
 125		at := thefakename
 126		if strings.Contains(url, ".well-known/webfinger?resource") {
 127			at = "application/jrd+json"
 128		}
 129		j, err := junk.Get(url, junk.GetArgs{
 130			Accept:  at,
 131			Agent:   "honksnonk/5.0; " + serverName,
 132			Timeout: timeout,
 133		})
 134		return j, err
 135	}
 136
 137	ji, err := flightdeck.Call(url, fn)
 138	if err != nil {
 139		return nil, err
 140	}
 141	j := ji.(junk.Junk)
 142	return j, nil
 143}
 144
 145func fetchsome(url string) ([]byte, error) {
 146	resp, err := http.Get(url)
 147	if err != nil {
 148		log.Printf("error fetching %s: %s", url, err)
 149		return nil, err
 150	}
 151	defer resp.Body.Close()
 152	if resp.StatusCode != 200 {
 153		return nil, errors.New("not 200")
 154	}
 155	var buf bytes.Buffer
 156	limiter := io.LimitReader(resp.Body, 10*1024*1024)
 157	io.Copy(&buf, limiter)
 158	return buf.Bytes(), nil
 159}
 160
 161func savedonk(url string, name, desc, media string, localize bool) *Donk {
 162	if url == "" {
 163		return nil
 164	}
 165	donk := finddonk(url)
 166	if donk != nil {
 167		return donk
 168	}
 169	donk = new(Donk)
 170	log.Printf("saving donk: %s", url)
 171	xid := xfiltrate()
 172	data := []byte{}
 173	if localize {
 174		fn := func() (interface{}, error) {
 175			return fetchsome(url)
 176		}
 177		ii, err := flightdeck.Call(url, fn)
 178		if err != nil {
 179			localize = false
 180			goto saveit
 181		}
 182		data = ii.([]byte)
 183
 184		if len(data) == 10*1024*1024 {
 185			log.Printf("truncation likely")
 186		}
 187		if strings.HasPrefix(media, "image") {
 188			img, err := shrinkit(data)
 189			if err != nil {
 190				log.Printf("unable to decode image: %s", err)
 191				localize = false
 192				data = []byte{}
 193				goto saveit
 194			}
 195			data = img.Data
 196			format := img.Format
 197			media = "image/" + format
 198			if format == "jpeg" {
 199				format = "jpg"
 200			}
 201			xid = xid + "." + format
 202		} else if media == "application/pdf" {
 203			if len(data) > 1000000 {
 204				log.Printf("not saving large pdf")
 205				localize = false
 206				data = []byte{}
 207			}
 208		} else if len(data) > 100000 {
 209			log.Printf("not saving large attachment")
 210			localize = false
 211			data = []byte{}
 212		}
 213	}
 214saveit:
 215	fileid, err := savefile(xid, name, desc, url, media, localize, data)
 216	if err != nil {
 217		log.Printf("error saving file %s: %s", url, err)
 218		return nil
 219	}
 220	donk.FileID = fileid
 221	donk.XID = xid
 222	return donk
 223}
 224
 225func iszonked(userid int64, xid string) bool {
 226	var id int64
 227	row := stmtFindZonk.QueryRow(userid, xid)
 228	err := row.Scan(&id)
 229	if err == nil {
 230		return true
 231	}
 232	if err != sql.ErrNoRows {
 233		log.Printf("error querying zonk: %s", err)
 234	}
 235	return false
 236}
 237
 238func needxonk(user *WhatAbout, x *Honk) bool {
 239	if rejectxonk(x) {
 240		return false
 241	}
 242	return needxonkid(user, x.XID)
 243}
 244func needbonkid(user *WhatAbout, xid string) bool {
 245	return needxonkidX(user, xid, true)
 246}
 247func needxonkid(user *WhatAbout, xid string) bool {
 248	return needxonkidX(user, xid, false)
 249}
 250func needxonkidX(user *WhatAbout, xid string, isannounce bool) bool {
 251	if !strings.HasPrefix(xid, "https://") {
 252		return false
 253	}
 254	if strings.HasPrefix(xid, user.URL+"/") {
 255		return false
 256	}
 257	if rejectorigin(user.ID, xid, isannounce) {
 258		return false
 259	}
 260	if iszonked(user.ID, xid) {
 261		log.Printf("already zonked: %s", xid)
 262		return false
 263	}
 264	var id int64
 265	row := stmtFindXonk.QueryRow(user.ID, xid)
 266	err := row.Scan(&id)
 267	if err == nil {
 268		return false
 269	}
 270	if err != sql.ErrNoRows {
 271		log.Printf("error querying xonk: %s", err)
 272	}
 273	return true
 274}
 275
 276func eradicatexonk(userid int64, xid string) {
 277	xonk := getxonk(userid, xid)
 278	if xonk != nil {
 279		deletehonk(xonk.ID)
 280	}
 281	_, err := stmtSaveZonker.Exec(userid, xid, "zonk")
 282	if err != nil {
 283		log.Printf("error eradicating: %s", err)
 284	}
 285}
 286
 287func savexonk(x *Honk) {
 288	log.Printf("saving xonk: %s", x.XID)
 289	go handles(x.Honker)
 290	go handles(x.Oonker)
 291	savehonk(x)
 292}
 293
 294type Box struct {
 295	In     string
 296	Out    string
 297	Shared string
 298}
 299
 300var boxofboxes = cache.New(cache.Options{Filler: func(ident string) (*Box, bool) {
 301	var info string
 302	row := stmtGetXonker.QueryRow(ident, "boxes")
 303	err := row.Scan(&info)
 304	if err != nil {
 305		log.Printf("need to get boxes for %s", ident)
 306		var j junk.Junk
 307		j, err = GetJunk(ident)
 308		if err != nil {
 309			log.Printf("error getting boxes: %s", err)
 310			return nil, false
 311		}
 312		allinjest(originate(ident), j)
 313		row = stmtGetXonker.QueryRow(ident, "boxes")
 314		err = row.Scan(&info)
 315	}
 316	if err == nil {
 317		m := strings.Split(info, " ")
 318		b := &Box{In: m[0], Out: m[1], Shared: m[2]}
 319		return b, true
 320	}
 321	return nil, false
 322}})
 323
 324func gimmexonks(user *WhatAbout, outbox string) {
 325	log.Printf("getting outbox: %s", outbox)
 326	j, err := GetJunk(outbox)
 327	if err != nil {
 328		log.Printf("error getting outbox: %s", err)
 329		return
 330	}
 331	t, _ := j.GetString("type")
 332	origin := originate(outbox)
 333	if t == "OrderedCollection" {
 334		items, _ := j.GetArray("orderedItems")
 335		if items == nil {
 336			items, _ = j.GetArray("items")
 337		}
 338		if items == nil {
 339			obj, ok := j.GetMap("first")
 340			if ok {
 341				items, _ = obj.GetArray("orderedItems")
 342			} else {
 343				page1, ok := j.GetString("first")
 344				if ok {
 345					j, err = GetJunk(page1)
 346					if err != nil {
 347						log.Printf("error gettings page1: %s", err)
 348						return
 349					}
 350					items, _ = j.GetArray("orderedItems")
 351				}
 352			}
 353		}
 354		if len(items) > 20 {
 355			items = items[0:20]
 356		}
 357		for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
 358			items[i], items[j] = items[j], items[i]
 359		}
 360		for _, item := range items {
 361			obj, ok := item.(junk.Junk)
 362			if ok {
 363				xonksaver(user, obj, origin)
 364				continue
 365			}
 366			xid, ok := item.(string)
 367			if ok {
 368				if !needxonkid(user, xid) {
 369					continue
 370				}
 371				obj, err = GetJunk(xid)
 372				if err != nil {
 373					log.Printf("error getting item: %s", err)
 374					continue
 375				}
 376				xonksaver(user, obj, originate(xid))
 377			}
 378		}
 379	}
 380}
 381
 382func whosthere(xid string) ([]string, string) {
 383	obj, err := GetJunk(xid)
 384	if err != nil {
 385		log.Printf("error getting remote xonk: %s", err)
 386		return nil, ""
 387	}
 388	convoy, _ := obj.GetString("context")
 389	if convoy == "" {
 390		convoy, _ = obj.GetString("conversation")
 391	}
 392	return newphone(nil, obj), convoy
 393}
 394
 395func newphone(a []string, obj junk.Junk) []string {
 396	for _, addr := range []string{"to", "cc", "attributedTo"} {
 397		who, _ := obj.GetString(addr)
 398		if who != "" {
 399			a = append(a, who)
 400		}
 401		whos, _ := obj.GetArray(addr)
 402		for _, w := range whos {
 403			who, _ := w.(string)
 404			if who != "" {
 405				a = append(a, who)
 406			}
 407		}
 408	}
 409	return a
 410}
 411
 412func extractattrto(obj junk.Junk) string {
 413	who, _ := obj.GetString("attributedTo")
 414	if who != "" {
 415		return who
 416	}
 417	o, ok := obj.GetMap("attributedTo")
 418	if ok {
 419		id, ok := o.GetString("id")
 420		if ok {
 421			return id
 422		}
 423	}
 424	arr, _ := obj.GetArray("attributedTo")
 425	for _, a := range arr {
 426		o, ok := a.(junk.Junk)
 427		if ok {
 428			t, _ := o.GetString("type")
 429			id, _ := o.GetString("id")
 430			if t == "Person" || t == "" {
 431				return id
 432			}
 433		}
 434		s, ok := a.(string)
 435		if ok {
 436			return s
 437		}
 438	}
 439	return ""
 440}
 441
 442func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk {
 443	depth := 0
 444	maxdepth := 10
 445	currenttid := ""
 446	goingup := 0
 447	var xonkxonkfn func(item junk.Junk, origin string) *Honk
 448
 449	saveonemore := func(xid string) {
 450		log.Printf("getting onemore: %s", xid)
 451		if depth >= maxdepth {
 452			log.Printf("in too deep")
 453			return
 454		}
 455		obj, err := GetJunkHardMode(xid)
 456		if err != nil {
 457			log.Printf("error getting onemore: %s: %s", xid, err)
 458			return
 459		}
 460		depth++
 461		xonkxonkfn(obj, originate(xid))
 462		depth--
 463	}
 464
 465	xonkxonkfn = func(item junk.Junk, origin string) *Honk {
 466		// id, _ := item.GetString( "id")
 467		what, _ := item.GetString("type")
 468		dt, _ := item.GetString("published")
 469
 470		var err error
 471		var xid, rid, url, content, precis, convoy string
 472		var replies []string
 473		var obj junk.Junk
 474		var ok bool
 475		isUpdate := false
 476		switch what {
 477		case "Delete":
 478			obj, ok = item.GetMap("object")
 479			if ok {
 480				xid, _ = obj.GetString("id")
 481			} else {
 482				xid, _ = item.GetString("object")
 483			}
 484			if xid == "" {
 485				return nil
 486			}
 487			if originate(xid) != origin {
 488				log.Printf("forged delete: %s", xid)
 489				return nil
 490			}
 491			log.Printf("eradicating %s", xid)
 492			eradicatexonk(user.ID, xid)
 493			return nil
 494		case "Tombstone":
 495			xid, _ = item.GetString("id")
 496			if xid == "" {
 497				return nil
 498			}
 499			if originate(xid) != origin {
 500				log.Printf("forged delete: %s", xid)
 501				return nil
 502			}
 503			log.Printf("eradicating %s", xid)
 504			eradicatexonk(user.ID, xid)
 505			return nil
 506		case "Announce":
 507			obj, ok = item.GetMap("object")
 508			if ok {
 509				xid, _ = obj.GetString("id")
 510			} else {
 511				xid, _ = item.GetString("object")
 512			}
 513			if !needbonkid(user, xid) {
 514				return nil
 515			}
 516			log.Printf("getting bonk: %s", xid)
 517			obj, err = GetJunkHardMode(xid)
 518			if err != nil {
 519				log.Printf("error getting bonk: %s: %s", xid, err)
 520			}
 521			origin = originate(xid)
 522			what = "bonk"
 523		case "Update":
 524			isUpdate = true
 525			fallthrough
 526		case "Create":
 527			obj, ok = item.GetMap("object")
 528			if !ok {
 529				xid, _ = item.GetString("object")
 530				log.Printf("getting created honk: %s", xid)
 531				obj, err = GetJunkHardMode(xid)
 532				if err != nil {
 533					log.Printf("error getting creation: %s", err)
 534				}
 535			}
 536			what = "honk"
 537			if obj != nil {
 538				t, _ := obj.GetString("type")
 539				switch t {
 540				case "Event":
 541					what = "event"
 542				}
 543			}
 544		case "Read":
 545			xid, ok = item.GetString("object")
 546			if ok {
 547				if !needxonkid(user, xid) {
 548					log.Printf("don't need read obj: %s", xid)
 549					return nil
 550				}
 551				obj, err = GetJunkHardMode(xid)
 552				if err != nil {
 553					log.Printf("error getting read: %s", err)
 554					return nil
 555				}
 556				return xonkxonkfn(obj, originate(xid))
 557			}
 558			return nil
 559		case "Add":
 560			xid, ok = item.GetString("object")
 561			if ok {
 562				// check target...
 563				if !needxonkid(user, xid) {
 564					log.Printf("don't need added obj: %s", xid)
 565					return nil
 566				}
 567				obj, err = GetJunkHardMode(xid)
 568				if err != nil {
 569					log.Printf("error getting add: %s", err)
 570					return nil
 571				}
 572				return xonkxonkfn(obj, originate(xid))
 573			}
 574			return nil
 575		case "Audio":
 576			fallthrough
 577		case "Video":
 578			fallthrough
 579		case "Question":
 580			fallthrough
 581		case "Note":
 582			fallthrough
 583		case "Article":
 584			fallthrough
 585		case "Page":
 586			obj = item
 587			what = "honk"
 588		case "Event":
 589			obj = item
 590			what = "event"
 591		default:
 592			log.Printf("unknown activity: %s", what)
 593			fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 594			item.Write(fd)
 595			io.WriteString(fd, "\n")
 596			fd.Close()
 597			return nil
 598		}
 599
 600		if obj != nil {
 601			_, ok := obj.GetString("diaspora:guid")
 602			if ok {
 603				// friendica does the silliest bonks
 604				c, ok := obj.GetString("source", "content")
 605				if ok {
 606					re_link := regexp.MustCompile(`link='([^']*)'`)
 607					m := re_link.FindStringSubmatch(c)
 608					if len(m) > 1 {
 609						xid := m[1]
 610						log.Printf("getting friendica flavored bonk: %s", xid)
 611						if !needxonkid(user, xid) {
 612							return nil
 613						}
 614						newobj, err := GetJunkHardMode(xid)
 615						if err != nil {
 616							log.Printf("error getting bonk: %s: %s", xid, err)
 617						} else {
 618							obj = newobj
 619							origin = originate(xid)
 620							what = "bonk"
 621						}
 622					}
 623				}
 624			}
 625		}
 626
 627		var xonk Honk
 628		// early init
 629		xonk.UserID = user.ID
 630		xonk.Honker, _ = item.GetString("actor")
 631		if xonk.Honker == "" {
 632			xonk.Honker, _ = item.GetString("attributedTo")
 633		}
 634		if obj != nil {
 635			if xonk.Honker == "" {
 636				xonk.Honker = extractattrto(obj)
 637			}
 638			xonk.Oonker = extractattrto(obj)
 639			if xonk.Oonker == xonk.Honker {
 640				xonk.Oonker = ""
 641			}
 642			xonk.Audience = newphone(nil, obj)
 643		}
 644		xonk.Audience = append(xonk.Audience, xonk.Honker)
 645		xonk.Audience = oneofakind(xonk.Audience)
 646
 647		var mentions []Mention
 648		if obj != nil {
 649			ot, _ := obj.GetString("type")
 650			url, _ = obj.GetString("url")
 651			dt2, ok := obj.GetString("published")
 652			if ok {
 653				dt = dt2
 654			}
 655			xid, _ = obj.GetString("id")
 656			precis, _ = obj.GetString("summary")
 657			if precis == "" {
 658				precis, _ = obj.GetString("name")
 659			}
 660			content, _ = obj.GetString("content")
 661			if !strings.HasPrefix(content, "<p>") {
 662				content = "<p>" + content
 663			}
 664			sens, _ := obj["sensitive"].(bool)
 665			if sens && precis == "" {
 666				precis = "unspecified horror"
 667			}
 668			rid, ok = obj.GetString("inReplyTo")
 669			if !ok {
 670				robj, ok := obj.GetMap("inReplyTo")
 671				if ok {
 672					rid, _ = robj.GetString("id")
 673				}
 674			}
 675			convoy, _ = obj.GetString("context")
 676			if convoy == "" {
 677				convoy, _ = obj.GetString("conversation")
 678			}
 679			if ot == "Question" {
 680				if what == "honk" {
 681					what = "qonk"
 682				}
 683				content += "<ul>"
 684				ans, _ := obj.GetArray("oneOf")
 685				for _, ai := range ans {
 686					a, ok := ai.(junk.Junk)
 687					if !ok {
 688						continue
 689					}
 690					as, _ := a.GetString("name")
 691					content += "<li>" + as
 692				}
 693				ans, _ = obj.GetArray("anyOf")
 694				for _, ai := range ans {
 695					a, ok := ai.(junk.Junk)
 696					if !ok {
 697						continue
 698					}
 699					as, _ := a.GetString("name")
 700					content += "<li>" + as
 701				}
 702				content += "</ul>"
 703			}
 704			if what == "honk" && rid != "" {
 705				what = "tonk"
 706			}
 707			atts, _ := obj.GetArray("attachment")
 708			for i, atti := range atts {
 709				att, ok := atti.(junk.Junk)
 710				if !ok {
 711					continue
 712				}
 713				at, _ := att.GetString("type")
 714				mt, _ := att.GetString("mediaType")
 715				u, _ := att.GetString("url")
 716				name, _ := att.GetString("name")
 717				desc, _ := att.GetString("summary")
 718				if desc == "" {
 719					desc = name
 720				}
 721				localize := false
 722				if i > 4 {
 723					log.Printf("excessive attachment: %s", at)
 724				} else if at == "Document" || at == "Image" {
 725					mt = strings.ToLower(mt)
 726					log.Printf("attachment: %s %s", mt, u)
 727					if mt == "text/plain" || mt == "application/pdf" ||
 728						strings.HasPrefix(mt, "image") {
 729						localize = true
 730					}
 731				} else {
 732					log.Printf("unknown attachment: %s", at)
 733				}
 734				if skipMedia(&xonk) {
 735					localize = false
 736				}
 737				donk := savedonk(u, name, desc, mt, localize)
 738				if donk != nil {
 739					xonk.Donks = append(xonk.Donks, donk)
 740				}
 741			}
 742			tags, _ := obj.GetArray("tag")
 743			for _, tagi := range tags {
 744				tag, ok := tagi.(junk.Junk)
 745				if !ok {
 746					continue
 747				}
 748				tt, _ := tag.GetString("type")
 749				name, _ := tag.GetString("name")
 750				desc, _ := tag.GetString("summary")
 751				if desc == "" {
 752					desc = name
 753				}
 754				if tt == "Emoji" {
 755					icon, _ := tag.GetMap("icon")
 756					mt, _ := icon.GetString("mediaType")
 757					if mt == "" {
 758						mt = "image/png"
 759					}
 760					u, _ := icon.GetString("url")
 761					donk := savedonk(u, name, desc, mt, true)
 762					if donk != nil {
 763						xonk.Donks = append(xonk.Donks, donk)
 764					}
 765				}
 766				if tt == "Hashtag" {
 767					if name == "" || name == "#" {
 768						// skip it
 769					} else {
 770						if name[0] != '#' {
 771							name = "#" + name
 772						}
 773						xonk.Onts = append(xonk.Onts, name)
 774					}
 775				}
 776				if tt == "Place" {
 777					p := new(Place)
 778					p.Name = name
 779					p.Latitude, _ = tag["latitude"].(float64)
 780					p.Longitude, _ = tag["longitude"].(float64)
 781					p.Url, _ = tag.GetString("url")
 782					xonk.Place = p
 783				}
 784				if tt == "Mention" {
 785					var m Mention
 786					m.Who, _ = tag.GetString("name")
 787					m.Where, _ = tag.GetString("href")
 788					mentions = append(mentions, m)
 789				}
 790			}
 791			starttime, ok := obj.GetString("startTime")
 792			if ok {
 793				start, err := time.Parse(time.RFC3339, starttime)
 794				if err == nil {
 795					t := new(Time)
 796					t.StartTime = start
 797					endtime, _ := obj.GetString("endTime")
 798					t.EndTime, _ = time.Parse(time.RFC3339, endtime)
 799					dura, _ := obj.GetString("duration")
 800					if strings.HasPrefix(dura, "PT") {
 801						dura = strings.ToLower(dura[2:])
 802						d, _ := time.ParseDuration(dura)
 803						t.Duration = Duration(d)
 804					}
 805					xonk.Time = t
 806				}
 807			}
 808			loca, ok := obj.GetMap("location")
 809			if ok {
 810				tt, _ := loca.GetString("type")
 811				name, _ := loca.GetString("name")
 812				if tt == "Place" {
 813					p := new(Place)
 814					p.Name = name
 815					p.Latitude, _ = loca["latitude"].(float64)
 816					p.Longitude, _ = loca["longitude"].(float64)
 817					p.Url, _ = loca.GetString("url")
 818					xonk.Place = p
 819				}
 820			}
 821
 822			xonk.Onts = oneofakind(xonk.Onts)
 823			replyobj, ok := obj.GetMap("replies")
 824			if ok {
 825				items, ok := replyobj.GetArray("items")
 826				if !ok {
 827					first, ok := replyobj.GetMap("first")
 828					if ok {
 829						items, _ = first.GetArray("items")
 830					}
 831				}
 832				for _, repl := range items {
 833					s, ok := repl.(string)
 834					if ok {
 835						replies = append(replies, s)
 836					}
 837				}
 838			}
 839
 840		}
 841		if originate(xid) != origin {
 842			log.Printf("original sin: %s <> %s", xid, origin)
 843			item.Write(os.Stdout)
 844			return nil
 845		}
 846
 847		if currenttid == "" {
 848			currenttid = convoy
 849		}
 850
 851		if len(content) > 90001 {
 852			log.Printf("content too long. truncating")
 853			content = content[:90001]
 854		}
 855
 856		// init xonk
 857		xonk.What = what
 858		xonk.XID = xid
 859		xonk.RID = rid
 860		xonk.Date, _ = time.Parse(time.RFC3339, dt)
 861		xonk.URL = url
 862		xonk.Noise = content
 863		xonk.Precis = precis
 864		xonk.Format = "html"
 865		xonk.Convoy = convoy
 866		xonk.Mentions = mentions
 867		for _, m := range mentions {
 868			if m.Where == user.URL {
 869				xonk.Whofore = 1
 870			}
 871		}
 872		imaginate(&xonk)
 873
 874		if isUpdate {
 875			log.Printf("something has changed! %s", xonk.XID)
 876			prev := getxonk(user.ID, xonk.XID)
 877			if prev == nil {
 878				log.Printf("didn't find old version for update: %s", xonk.XID)
 879				isUpdate = false
 880			} else {
 881				xonk.ID = prev.ID
 882				updatehonk(&xonk)
 883			}
 884		}
 885		if !isUpdate && needxonk(user, &xonk) {
 886			if strings.HasSuffix(convoy, "#context") {
 887				// friendica...
 888				if rid != "" {
 889					convoy = ""
 890				} else {
 891					convoy = url
 892				}
 893			}
 894			if rid != "" {
 895				if needxonkid(user, rid) {
 896					goingup++
 897					saveonemore(rid)
 898					goingup--
 899				}
 900				if convoy == "" {
 901					xx := getxonk(user.ID, rid)
 902					if xx != nil {
 903						convoy = xx.Convoy
 904					}
 905				}
 906			}
 907			if convoy == "" {
 908				convoy = currenttid
 909			}
 910			if convoy == "" {
 911				convoy = "data:,missing-" + xfiltrate()
 912				currenttid = convoy
 913			}
 914			xonk.Convoy = convoy
 915			savexonk(&xonk)
 916		}
 917		if goingup == 0 {
 918			for _, replid := range replies {
 919				if needxonkid(user, replid) {
 920					log.Printf("missing a reply: %s", replid)
 921					saveonemore(replid)
 922				}
 923			}
 924		}
 925		return &xonk
 926	}
 927
 928	return xonkxonkfn(item, origin)
 929}
 930
 931func rubadubdub(user *WhatAbout, req junk.Junk) {
 932	xid, _ := req.GetString("id")
 933	actor, _ := req.GetString("actor")
 934	j := junk.New()
 935	j["@context"] = itiswhatitis
 936	j["id"] = user.URL + "/dub/" + url.QueryEscape(xid)
 937	j["type"] = "Accept"
 938	j["actor"] = user.URL
 939	j["to"] = actor
 940	j["published"] = time.Now().UTC().Format(time.RFC3339)
 941	j["object"] = req
 942
 943	deliverate(0, user.ID, actor, j.ToBytes())
 944}
 945
 946func itakeitallback(user *WhatAbout, xid string) {
 947	j := junk.New()
 948	j["@context"] = itiswhatitis
 949	j["id"] = user.URL + "/unsub/" + url.QueryEscape(xid)
 950	j["type"] = "Undo"
 951	j["actor"] = user.URL
 952	j["to"] = xid
 953	f := junk.New()
 954	f["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 955	f["type"] = "Follow"
 956	f["actor"] = user.URL
 957	f["to"] = xid
 958	f["object"] = xid
 959	j["object"] = f
 960	j["published"] = time.Now().UTC().Format(time.RFC3339)
 961
 962	deliverate(0, user.ID, xid, j.ToBytes())
 963}
 964
 965func subsub(user *WhatAbout, xid string, owner string) {
 966	if xid == "" {
 967		log.Printf("can't subscribe to empty")
 968		return
 969	}
 970	j := junk.New()
 971	j["@context"] = itiswhatitis
 972	j["id"] = user.URL + "/sub/" + url.QueryEscape(xid)
 973	j["type"] = "Follow"
 974	j["actor"] = user.URL
 975	j["to"] = owner
 976	j["object"] = xid
 977	j["published"] = time.Now().UTC().Format(time.RFC3339)
 978
 979	deliverate(0, user.ID, owner, j.ToBytes())
 980}
 981
 982// returns activity, object
 983func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) {
 984	dt := h.Date.Format(time.RFC3339)
 985	var jo junk.Junk
 986	j := junk.New()
 987	j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID)
 988	j["actor"] = user.URL
 989	j["published"] = dt
 990	if h.Public {
 991		j["to"] = []string{h.Audience[0], user.URL + "/followers"}
 992	} else {
 993		j["to"] = h.Audience[0]
 994	}
 995	if len(h.Audience) > 1 {
 996		j["cc"] = h.Audience[1:]
 997	}
 998
 999	switch h.What {
1000	case "update":
1001		fallthrough
1002	case "tonk":
1003		fallthrough
1004	case "event":
1005		fallthrough
1006	case "honk":
1007		j["type"] = "Create"
1008		if h.What == "update" {
1009			j["type"] = "Update"
1010		}
1011
1012		jo = junk.New()
1013		jo["id"] = h.XID
1014		jo["type"] = "Note"
1015		if h.What == "event" {
1016			jo["type"] = "Event"
1017		}
1018		jo["published"] = dt
1019		jo["url"] = h.XID
1020		jo["attributedTo"] = user.URL
1021		if h.RID != "" {
1022			jo["inReplyTo"] = h.RID
1023		}
1024		if h.Convoy != "" {
1025			jo["context"] = h.Convoy
1026			jo["conversation"] = h.Convoy
1027		}
1028		jo["to"] = h.Audience[0]
1029		if len(h.Audience) > 1 {
1030			jo["cc"] = h.Audience[1:]
1031		}
1032		if !h.Public {
1033			jo["directMessage"] = true
1034		}
1035		mentions := bunchofgrapes(h.Noise)
1036		translate(h)
1037		redoimages(h)
1038		jo["summary"] = html.EscapeString(h.Precis)
1039		jo["content"] = h.Noise
1040		if h.Precis != "" {
1041			jo["sensitive"] = true
1042		}
1043
1044		var replies []string
1045		for _, reply := range h.Replies {
1046			replies = append(replies, reply.XID)
1047		}
1048		if len(replies) > 0 {
1049			jr := junk.New()
1050			jr["type"] = "Collection"
1051			jr["totalItems"] = len(replies)
1052			jr["items"] = replies
1053			jo["replies"] = jr
1054		}
1055
1056		var tags []junk.Junk
1057		for _, m := range mentions {
1058			t := junk.New()
1059			t["type"] = "Mention"
1060			t["name"] = m.Who
1061			t["href"] = m.Where
1062			tags = append(tags, t)
1063		}
1064		for _, o := range h.Onts {
1065			t := junk.New()
1066			t["type"] = "Hashtag"
1067			o = strings.ToLower(o)
1068			t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:])
1069			t["name"] = o
1070			tags = append(tags, t)
1071		}
1072		for _, e := range herdofemus(h.Noise) {
1073			t := junk.New()
1074			t["id"] = e.ID
1075			t["type"] = "Emoji"
1076			t["name"] = e.Name
1077			i := junk.New()
1078			i["type"] = "Image"
1079			i["mediaType"] = "image/png"
1080			i["url"] = e.ID
1081			t["icon"] = i
1082			tags = append(tags, t)
1083		}
1084		if len(tags) > 0 {
1085			jo["tag"] = tags
1086		}
1087		if p := h.Place; p != nil {
1088			t := junk.New()
1089			t["type"] = "Place"
1090			if p.Name != "" {
1091				t["name"] = p.Name
1092			}
1093			if p.Latitude != 0 {
1094				t["latitude"] = p.Latitude
1095			}
1096			if p.Longitude != 0 {
1097				t["longitude"] = p.Longitude
1098			}
1099			if p.Url != "" {
1100				t["url"] = p.Url
1101			}
1102			jo["location"] = t
1103		}
1104		if t := h.Time; t != nil {
1105			jo["startTime"] = t.StartTime.Format(time.RFC3339)
1106			if t.Duration != 0 {
1107				jo["duration"] = "PT" + strings.ToUpper(t.Duration.String())
1108			}
1109		}
1110		var atts []junk.Junk
1111		for _, d := range h.Donks {
1112			if re_emus.MatchString(d.Name) {
1113				continue
1114			}
1115			jd := junk.New()
1116			jd["mediaType"] = d.Media
1117			jd["name"] = d.Name
1118			jd["summary"] = html.EscapeString(d.Desc)
1119			jd["type"] = "Document"
1120			jd["url"] = d.URL
1121			atts = append(atts, jd)
1122		}
1123		if len(atts) > 0 {
1124			jo["attachment"] = atts
1125		}
1126		j["object"] = jo
1127	case "bonk":
1128		j["type"] = "Announce"
1129		if h.Convoy != "" {
1130			j["context"] = h.Convoy
1131		}
1132		j["object"] = h.XID
1133	case "unbonk":
1134		b := junk.New()
1135		b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID)
1136		b["type"] = "Announce"
1137		b["actor"] = user.URL
1138		if h.Convoy != "" {
1139			b["context"] = h.Convoy
1140		}
1141		b["object"] = h.XID
1142		j["type"] = "Undo"
1143		j["object"] = b
1144	case "zonk":
1145		j["type"] = "Delete"
1146		j["object"] = h.XID
1147	case "ack":
1148		j["type"] = "Read"
1149		j["object"] = h.XID
1150		if h.Convoy != "" {
1151			j["context"] = h.Convoy
1152		}
1153	case "deack":
1154		b := junk.New()
1155		b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID)
1156		b["type"] = "Read"
1157		b["actor"] = user.URL
1158		b["object"] = h.XID
1159		if h.Convoy != "" {
1160			b["context"] = h.Convoy
1161		}
1162		j["type"] = "Undo"
1163		j["object"] = b
1164	}
1165
1166	return j, jo
1167}
1168
1169var oldjonks = cache.New(cache.Options{Filler: func(xid string) ([]byte, bool) {
1170	row := stmtAnyXonk.QueryRow(xid)
1171	honk := scanhonk(row)
1172	if honk == nil || !honk.Public {
1173		return nil, true
1174	}
1175	user, _ := butwhatabout(honk.Username)
1176	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0)
1177	reversehonks(rawhonks)
1178	for _, h := range rawhonks {
1179		if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
1180			honk.Replies = append(honk.Replies, h)
1181		}
1182	}
1183	donksforhonks([]*Honk{honk})
1184	_, j := jonkjonk(user, honk)
1185	j["@context"] = itiswhatitis
1186
1187	return j.ToBytes(), true
1188}, Limit: 128})
1189
1190func gimmejonk(xid string) ([]byte, bool) {
1191	var j []byte
1192	ok := oldjonks.Get(xid, &j)
1193	return j, ok
1194}
1195
1196func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool {
1197	rcpts := make(map[string]bool)
1198	for _, a := range addresses {
1199		if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") {
1200			continue
1201		}
1202		if a[0] == '%' {
1203			rcpts[a] = true
1204			continue
1205		}
1206		var box *Box
1207		ok := boxofboxes.Get(a, &box)
1208		if ok && useshared && box.Shared != "" {
1209			rcpts["%"+box.Shared] = true
1210		} else {
1211			rcpts[a] = true
1212		}
1213	}
1214	return rcpts
1215}
1216
1217func honkworldwide(user *WhatAbout, honk *Honk) {
1218	jonk, _ := jonkjonk(user, honk)
1219	jonk["@context"] = itiswhatitis
1220	msg := jonk.ToBytes()
1221
1222	rcpts := boxuprcpts(user, honk.Audience, honk.Public)
1223
1224	if honk.Public {
1225		for _, h := range getdubs(user.ID) {
1226			if h.XID == user.URL {
1227				continue
1228			}
1229			var box *Box
1230			ok := boxofboxes.Get(h.XID, &box)
1231			if ok && box.Shared != "" {
1232				rcpts["%"+box.Shared] = true
1233			} else {
1234				rcpts[h.XID] = true
1235			}
1236		}
1237		for _, f := range getbacktracks(honk.XID) {
1238			rcpts[f] = true
1239		}
1240	}
1241	for a := range rcpts {
1242		go deliverate(0, user.ID, a, msg)
1243	}
1244	if honk.Public && len(honk.Onts) > 0 {
1245		collectiveaction(honk)
1246	}
1247}
1248
1249func collectiveaction(honk *Honk) {
1250	user := getserveruser()
1251	for _, ont := range honk.Onts {
1252		dubs := getnameddubs(serverUID, ont)
1253		if len(dubs) == 0 {
1254			continue
1255		}
1256		j := junk.New()
1257		j["@context"] = itiswhatitis
1258		j["type"] = "Add"
1259		j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID)
1260		j["actor"] = user.URL
1261		j["object"] = honk.XID
1262		j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:])
1263		rcpts := make(map[string]bool)
1264		for _, dub := range dubs {
1265			var box *Box
1266			ok := boxofboxes.Get(dub.XID, &box)
1267			if ok && box.Shared != "" {
1268				rcpts["%"+box.Shared] = true
1269			} else {
1270				rcpts[dub.XID] = true
1271			}
1272		}
1273		msg := j.ToBytes()
1274		for a := range rcpts {
1275			go deliverate(0, user.ID, a, msg)
1276		}
1277	}
1278}
1279
1280func junkuser(user *WhatAbout) []byte {
1281	about := markitzero(user.About)
1282
1283	j := junk.New()
1284	j["@context"] = itiswhatitis
1285	j["id"] = user.URL
1286	j["inbox"] = user.URL + "/inbox"
1287	j["outbox"] = user.URL + "/outbox"
1288	j["name"] = user.Display
1289	j["preferredUsername"] = user.Name
1290	j["summary"] = about
1291	if user.ID > 0 {
1292		j["type"] = "Person"
1293		j["url"] = user.URL
1294		j["followers"] = user.URL + "/followers"
1295		j["following"] = user.URL + "/following"
1296		a := junk.New()
1297		a["type"] = "Image"
1298		a["mediaType"] = "image/png"
1299		if ava := user.Options.Avatar; ava != "" {
1300			a["url"] = ava
1301		} else {
1302			a["url"] = fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL))
1303		}
1304		j["icon"] = a
1305	} else {
1306		j["type"] = "Service"
1307	}
1308	k := junk.New()
1309	k["id"] = user.URL + "#key"
1310	k["owner"] = user.URL
1311	k["publicKeyPem"] = user.Key
1312	j["publicKey"] = k
1313
1314	return j.ToBytes()
1315}
1316
1317var oldjonkers = cache.New(cache.Options{Filler: func(name string) ([]byte, bool) {
1318	user, err := butwhatabout(name)
1319	if err != nil {
1320		return nil, false
1321	}
1322	return junkuser(user), true
1323}, Duration: 1 * time.Minute})
1324
1325func asjonker(name string) ([]byte, bool) {
1326	var j []byte
1327	ok := oldjonkers.Get(name, &j)
1328	return j, ok
1329}
1330
1331var handfull = cache.New(cache.Options{Filler: func(name string) (string, bool) {
1332	m := strings.Split(name, "@")
1333	if len(m) != 2 {
1334		log.Printf("bad fish name: %s", name)
1335		return "", true
1336	}
1337	var href string
1338	row := stmtGetXonker.QueryRow(name, "fishname")
1339	err := row.Scan(&href)
1340	if err == nil {
1341		return href, true
1342	}
1343	log.Printf("fishing for %s", name)
1344	j, err := GetJunkFast(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1345	if err != nil {
1346		log.Printf("failed to go fish %s: %s", name, err)
1347		return "", true
1348	}
1349	links, _ := j.GetArray("links")
1350	for _, li := range links {
1351		l, ok := li.(junk.Junk)
1352		if !ok {
1353			continue
1354		}
1355		href, _ := l.GetString("href")
1356		rel, _ := l.GetString("rel")
1357		t, _ := l.GetString("type")
1358		if rel == "self" && friendorfoe(t) {
1359			when := time.Now().UTC().Format(dbtimeformat)
1360			_, err := stmtSaveXonker.Exec(name, href, "fishname", when)
1361			if err != nil {
1362				log.Printf("error saving fishname: %s", err)
1363			}
1364			return href, true
1365		}
1366	}
1367	return href, true
1368}})
1369
1370func gofish(name string) string {
1371	if name[0] == '@' {
1372		name = name[1:]
1373	}
1374	var href string
1375	handfull.Get(name, &href)
1376	return href
1377}
1378
1379func investigate(name string) (*SomeThing, error) {
1380	if name == "" {
1381		return nil, fmt.Errorf("no name")
1382	}
1383	if name[0] == '@' {
1384		name = gofish(name)
1385	}
1386	if name == "" {
1387		return nil, fmt.Errorf("no name")
1388	}
1389	obj, err := GetJunkFast(name)
1390	if err != nil {
1391		return nil, err
1392	}
1393	allinjest(originate(name), obj)
1394	return somethingabout(obj)
1395}
1396
1397func somethingabout(obj junk.Junk) (*SomeThing, error) {
1398	info := new(SomeThing)
1399	t, _ := obj.GetString("type")
1400	switch t {
1401	case "Person":
1402		fallthrough
1403	case "Organization":
1404		fallthrough
1405	case "Application":
1406		fallthrough
1407	case "Service":
1408		info.What = SomeActor
1409	case "OrderedCollection":
1410		fallthrough
1411	case "Collection":
1412		info.What = SomeCollection
1413	default:
1414		return nil, fmt.Errorf("unknown object type")
1415	}
1416	info.XID, _ = obj.GetString("id")
1417	info.Name, _ = obj.GetString("preferredUsername")
1418	if info.Name == "" {
1419		info.Name, _ = obj.GetString("name")
1420	}
1421	info.Owner, _ = obj.GetString("attributedTo")
1422	if info.Owner == "" {
1423		info.Owner = info.XID
1424	}
1425	return info, nil
1426}
1427
1428func allinjest(origin string, obj junk.Junk) {
1429	keyobj, ok := obj.GetMap("publicKey")
1430	if ok {
1431		ingestpubkey(origin, keyobj)
1432	}
1433	ingestboxes(origin, obj)
1434	ingesthandle(origin, obj)
1435}
1436
1437func ingestpubkey(origin string, obj junk.Junk) {
1438	keyobj, ok := obj.GetMap("publicKey")
1439	if ok {
1440		obj = keyobj
1441	}
1442	keyname, ok := obj.GetString("id")
1443	var data string
1444	row := stmtGetXonker.QueryRow(keyname, "pubkey")
1445	err := row.Scan(&data)
1446	if err == nil {
1447		return
1448	}
1449	if !ok || origin != originate(keyname) {
1450		log.Printf("bad key origin %s <> %s", origin, keyname)
1451		return
1452	}
1453	log.Printf("ingesting a needed pubkey: %s", keyname)
1454	owner, ok := obj.GetString("owner")
1455	if !ok {
1456		log.Printf("error finding %s pubkey owner", keyname)
1457		return
1458	}
1459	data, ok = obj.GetString("publicKeyPem")
1460	if !ok {
1461		log.Printf("error finding %s pubkey", keyname)
1462		return
1463	}
1464	if originate(owner) != origin {
1465		log.Printf("bad key owner: %s <> %s", owner, origin)
1466		return
1467	}
1468	_, _, err = httpsig.DecodeKey(data)
1469	if err != nil {
1470		log.Printf("error decoding %s pubkey: %s", keyname, err)
1471		return
1472	}
1473	when := time.Now().UTC().Format(dbtimeformat)
1474	_, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when)
1475	if err != nil {
1476		log.Printf("error saving key: %s", err)
1477	}
1478}
1479
1480func ingestboxes(origin string, obj junk.Junk) {
1481	ident, _ := obj.GetString("id")
1482	if ident == "" {
1483		return
1484	}
1485	if originate(ident) != origin {
1486		return
1487	}
1488	var info string
1489	row := stmtGetXonker.QueryRow(ident, "boxes")
1490	err := row.Scan(&info)
1491	if err == nil {
1492		return
1493	}
1494	log.Printf("ingesting boxes: %s", ident)
1495	inbox, _ := obj.GetString("inbox")
1496	outbox, _ := obj.GetString("outbox")
1497	sbox, _ := obj.GetString("endpoints", "sharedInbox")
1498	if inbox != "" {
1499		when := time.Now().UTC().Format(dbtimeformat)
1500		m := strings.Join([]string{inbox, outbox, sbox}, " ")
1501		_, err = stmtSaveXonker.Exec(ident, m, "boxes", when)
1502		if err != nil {
1503			log.Printf("error saving boxes: %s", err)
1504		}
1505	}
1506}
1507
1508func ingesthandle(origin string, obj junk.Junk) {
1509	xid, _ := obj.GetString("id")
1510	if xid == "" {
1511		return
1512	}
1513	if originate(xid) != origin {
1514		return
1515	}
1516	var handle string
1517	row := stmtGetXonker.QueryRow(xid, "handle")
1518	err := row.Scan(&handle)
1519	if err == nil {
1520		return
1521	}
1522	handle, _ = obj.GetString("preferredUsername")
1523	if handle != "" {
1524		when := time.Now().UTC().Format(dbtimeformat)
1525		_, err = stmtSaveXonker.Exec(xid, handle, "handle", when)
1526		if err != nil {
1527			log.Printf("error saving handle: %s", err)
1528		}
1529	}
1530}