From 255fea17e09aa1bb28c50929b99d17fd5628e19f Mon Sep 17 00:00:00 2001 From: wgroeneveld Date: Sun, 2 May 2021 09:41:13 +0200 Subject: [PATCH] remove since and simply check tags in rss feed. fixes time difference bugs --- .gitignore | 1 + README.md | 10 +- app/pictures/handler.go | 8 +- app/webmention/handler.go | 12 +- app/webmention/handler_test.go | 2 +- app/webmention/recv/receive.go | 8 +- app/webmention/send/rsslinkcollector.go | 14 +- app/webmention/send/rsslinkcollector_test.go | 19 +- app/webmention/send/send.go | 42 +- app/webmention/send/send_test.go | 51 +- db/migrate-db.go | 55 -- db/migrate-pictures.go | 69 -- db/migrate.go | 7 +- db/repo.go | 33 +- db/repo_test.go | 20 +- mentions.db | 714 ------------------- rest/utils.go | 4 + 17 files changed, 74 insertions(+), 995 deletions(-) delete mode 100644 db/migrate-db.go delete mode 100644 db/migrate-pictures.go delete mode 100644 mentions.db diff --git a/.gitignore b/.gitignore index 76e392c..1dcd31b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.prof *.test +*.db config.json diff --git a/README.md b/README.md index f6b3f33..e8dd05a 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ Sends out **both webmentions and pingbacks**, based on the domain's `index.xml` This does a couple of things: -1. Fetch RSS entries (since x, or everything) +1. Fetch RSS entries (since last sent link x, or everything) 2. Find outbound `href`s (starting with `http`) 3. Check if those domains have a `webmention` link endpoint installed, according to the w3.org rules. If not, check for a `pingback` endpoint. If not, bail out. 4. If webmention/pingback found: `POST` for each found href with `source` the own domain and `target` the outbound link found in the RSS feed, using either XML or form data according to the protocol. @@ -173,11 +173,13 @@ As with the `POST` call, will result in a `202 Accepted` and handles things asyn **Does this thing take updates into account**? -Yes and no. It checks the `` `` RSS tag by default. I decided against porting the more complicated `` HTML check as it would only spam possible receivers. So if you consider your article to be updated, you should also update the publication date! +Yes and no. It checks the `` tag to see if there's a new post since mentions were last sent. If a new link is discovered, it will send out those. -**Do I have to provide a ?since= parameter each time**? +This means if you made changes in-between, and they appear in the RSS feed as recent items, it will get resend. -No. The server will automatically store the latest push, and if it's called again, it will not send out anything if nothing more recent was found in your RSS feed based on the last since timestamp. Providing the parameter merely lets you override the behavior. +**Do I have to provide a ?source= parameter each time**? + +No. The server will automatically store the latest push, and if it's called again, it will not send out anything if nothing more recent was found in your RSS feed based on the last published link. Providing the parameter merely lets you override the behavior. ### 2. Pingbacks diff --git a/app/pictures/handler.go b/app/pictures/handler.go index 59644d3..bdc4242 100644 --- a/app/pictures/handler.go +++ b/app/pictures/handler.go @@ -2,7 +2,9 @@ package pictures import ( "brainbaking.com/go-jamming/app/mf" + "brainbaking.com/go-jamming/common" "brainbaking.com/go-jamming/db" + "brainbaking.com/go-jamming/rest" _ "embed" "github.com/gorilla/mux" "github.com/rs/zerolog/log" @@ -18,16 +20,12 @@ func init() { } } -const ( - bridgy = "brid.gy" -) - // Handle handles picture GET calls. // It does not validate the picture query as it's part of a composite key anyway. func Handle(repo db.MentionRepo) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { picDomain := mux.Vars(r)["picture"] - if picDomain == mf.Anonymous || picDomain == bridgy { + if picDomain == mf.Anonymous || common.Includes(rest.SiloDomains, picDomain) { servePicture(w, anonymous) return } diff --git a/app/webmention/handler.go b/app/webmention/handler.go index d5ee04a..ba46c87 100644 --- a/app/webmention/handler.go +++ b/app/webmention/handler.go @@ -25,7 +25,6 @@ func HandleGet(repo db.MentionRepo) http.HandlerFunc { func HandlePut(conf *common.Config, repo db.MentionRepo) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - since := sinceQueryParam(r) domain := mux.Vars(r)["domain"] source := sourceQueryParam(r) @@ -38,7 +37,7 @@ func HandlePut(conf *common.Config, repo db.MentionRepo) http.HandlerFunc { if source != "" { go snder.SendSingle(domain, source) } else { - go snder.Send(domain, since) + go snder.Send(domain) } rest.Accept(w) @@ -53,15 +52,6 @@ func sourceQueryParam(r *http.Request) string { return "" } -func sinceQueryParam(r *http.Request) string { - sinceParam := r.URL.Query()["since"] - since := "" - if len(sinceParam) > 0 { - since = sinceParam[0] - } - return since -} - func HandlePost(conf *common.Config, repo db.MentionRepo) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() diff --git a/app/webmention/handler_test.go b/app/webmention/handler_test.go index 350e8a2..bb2bc0c 100644 --- a/app/webmention/handler_test.go +++ b/app/webmention/handler_test.go @@ -47,7 +47,7 @@ func TestHandlePostWithTestServer_Parallel(t *testing.T) { defer ts.Close() var wg sync.WaitGroup - for i := 0; i < 5; i++ { + for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() diff --git a/app/webmention/recv/receive.go b/app/webmention/recv/receive.go index c0fb725..72c0de0 100644 --- a/app/webmention/recv/receive.go +++ b/app/webmention/recv/receive.go @@ -27,8 +27,6 @@ var ( errPicNoRealImage = errors.New("Downloaded author picture is not a real image") errPicUnableToSave = errors.New("Unable to save downloaded author picture") errWontDownloadBecauseOfPrivacy = errors.New("Will not save locally because it's form a silo domain") - - siloDomains = []string{"brid.gy", "twitter.com"} ) func (recv *Receiver) Receive(wm mf.Mention) { @@ -127,10 +125,8 @@ func (recv *Receiver) parseBodyAsNonIndiewebSite(body string, wm mf.Mention) *mf // This refuses to download from silo sources such as brid.gy because of privacy concerns. func (recv *Receiver) saveAuthorPictureLocally(indieweb *mf.IndiewebData) error { srcDomain := rest.Domain(indieweb.Source) - for _, siloDomain := range siloDomains { - if srcDomain == siloDomain { - return errWontDownloadBecauseOfPrivacy - } + if common.Includes(rest.SiloDomains, srcDomain) { + return errWontDownloadBecauseOfPrivacy } _, picData, err := recv.RestClient.GetBody(indieweb.Author.Picture) diff --git a/app/webmention/send/rsslinkcollector.go b/app/webmention/send/rsslinkcollector.go index 962d041..131de0f 100644 --- a/app/webmention/send/rsslinkcollector.go +++ b/app/webmention/send/rsslinkcollector.go @@ -5,7 +5,6 @@ import ( "brainbaking.com/go-jamming/common" "regexp" "strings" - "time" ) type RSSItem struct { @@ -38,19 +37,20 @@ type RSSItem struct { ' ' } **/ -func (snder *Sender) Collect(xml string, since time.Time) ([]RSSItem, error) { +func (snder *Sender) Collect(xml string, lastSentLink string) ([]RSSItem, error) { feed, err := rss.ParseFeed([]byte(xml)) if err != nil { return nil, err } var items []RSSItem for _, rssitem := range feed.ItemList { - if since.IsZero() || since.Before(rssitem.PubDateAsTime()) { - items = append(items, RSSItem{ - link: rssitem.Link, - hrefs: snder.collectUniqueHrefsFromHtml(rssitem.Description), - }) + if rssitem.Link == lastSentLink { + break } + items = append(items, RSSItem{ + link: rssitem.Link, + hrefs: snder.collectUniqueHrefsFromHtml(rssitem.Description), + }) } return items, nil } diff --git a/app/webmention/send/rsslinkcollector_test.go b/app/webmention/send/rsslinkcollector_test.go index ea301af..5cbc535 100644 --- a/app/webmention/send/rsslinkcollector_test.go +++ b/app/webmention/send/rsslinkcollector_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/suite" "io/ioutil" "testing" - "time" ) type CollectSuite struct { @@ -37,7 +36,7 @@ func (s *CollectSuite) TestCollectUniqueHrefsFromHtmlShouldNotContainInlineLinks } func (s *CollectSuite) TestCollectShouldNotContainHrefsFromBlockedDomains() { - items, err := s.snder.Collect(s.xml, common.IsoToTime("2021-03-10T00:00:00.000Z")) + items, err := s.snder.Collect(s.xml, "https://brainbaking.com/notes/2021/03/09h15m17s30/") assert.NoError(s.T(), err) last := items[len(items)-1] assert.Equal(s.T(), "https://brainbaking.com/notes/2021/03/10h16m24s22/", last.link) @@ -49,7 +48,7 @@ func (s *CollectSuite) TestCollectShouldNotContainHrefsFromBlockedDomains() { } func (s *CollectSuite) TestCollectShouldNotContainHrefsThatPointToImages() { - items, err := s.snder.Collect(s.xml, common.IsoToTime("2021-03-14T00:00:00.000Z")) + items, err := s.snder.Collect(s.xml, "https://brainbaking.com/notes/2021/03/13h12m44s29/") assert.NoError(s.T(), err) last := items[len(items)-1] // test case: @@ -59,14 +58,15 @@ func (s *CollectSuite) TestCollectShouldNotContainHrefsThatPointToImages() { }, last.hrefs) } -func (s *CollectSuite) TestCollectNothingIfDateInFutureAndSinceNothingNewInFeed() { - items, err := s.snder.Collect(s.xml, time.Now().Add(time.Duration(600)*time.Hour)) +func (s *CollectSuite) TestCollectNothingIfNothingNewInFeed() { + latestEntry := "https://brainbaking.com/notes/2021/03/16h17m07s14/" + items, err := s.snder.Collect(s.xml, latestEntry) assert.NoError(s.T(), err) assert.Equal(s.T(), 0, len(items)) } -func (s *CollectSuite) TestCollectLatestXLinksWhenASinceParameterIsProvided() { - items, err := s.snder.Collect(s.xml, common.IsoToTime("2021-03-15T00:00:00.000Z")) +func (s *CollectSuite) TestCollectLatestXLinksWhenARecentLinkParameterIsProvided() { + items, err := s.snder.Collect(s.xml, "https://brainbaking.com/notes/2021/03/14h17m41s53/") assert.NoError(s.T(), err) assert.Equal(s.T(), 3, len(items)) @@ -81,9 +81,8 @@ func (s *CollectSuite) TestCollectLatestXLinksWhenASinceParameterIsProvided() { } -func (s *CollectSuite) TestCollectEveryExternalLinkWithoutAValidSinceDate() { - // no valid since date = zero time passed. - items, err := s.snder.Collect(s.xml, time.Time{}) +func (s *CollectSuite) TestCollectEveryExternalLinkWithoutARecentLink() { + items, err := s.snder.Collect(s.xml, "") assert.NoError(s.T(), err) assert.Equal(s.T(), 141, len(items)) diff --git a/app/webmention/send/send.go b/app/webmention/send/send.go index 7ff4529..defef1a 100644 --- a/app/webmention/send/send.go +++ b/app/webmention/send/send.go @@ -10,7 +10,6 @@ import ( "github.com/rs/zerolog/log" "strings" "sync" - "time" ) type Sender struct { @@ -19,23 +18,6 @@ type Sender struct { Repo db.MentionRepo } -func (snder *Sender) sinceForDomain(domain string, since string) time.Time { - if since != "" { - return common.IsoToTime(since) - } - - sinceConf, err := snder.Repo.Since(domain) - if err != nil { - log.Warn().Str("domain", domain).Msg("No query param, and no config found. Reverting to beginning of time...") - return time.Time{} - } - return sinceConf -} - -func (snder *Sender) updateSinceForDomain(domain string) { - snder.Repo.UpdateSince(domain, common.Now()) -} - // SendSingle sends out webmentions serially for a single source. // It does validate the relative path against the domain, which is supposed to be served using https. func (snder *Sender) SendSingle(domain string, relSource string) { @@ -60,29 +42,34 @@ func (snder *Sender) SendSingle(domain string, relSource string) { // Send sends out multiple webmentions based on since and what's posted in the RSS feed. // It first GETs domain/index.xml and goes from there. -func (snder *Sender) Send(domain string, since string) { - timeSince := snder.sinceForDomain(domain, since) +func (snder *Sender) Send(domain string) { + lastSent := snder.Repo.LastSentMention(domain) feedUrl := "https://" + domain + "/index.xml" - log.Info().Str("domain", domain).Time("since", timeSince).Msg(` OK: someone wants to send mentions`) + log.Info().Str("domain", domain).Str("lastsent", lastSent).Msg(` OK: someone wants to send mentions`) _, feed, err := snder.RestClient.GetBody(feedUrl) if err != nil { log.Err(err).Str("url", feedUrl).Msg("Unable to retrieve RSS feed, send aborted") return } - if err = snder.parseRssFeed(feed, timeSince); err != nil { + lastSent, err = snder.parseRssFeed(feed, lastSent) + if err != nil { log.Err(err).Str("url", feedUrl).Msg("Unable to parse RSS feed, send aborted") return } - snder.updateSinceForDomain(domain) + snder.Repo.UpdateLastSentMention(domain, lastSent) + log.Info().Str("domain", domain).Str("lastsent", lastSent).Msg(` OK: send processed.`) } -func (snder *Sender) parseRssFeed(feed string, since time.Time) error { - items, err := snder.Collect(feed, since) +func (snder *Sender) parseRssFeed(feed string, lastSentLink string) (string, error) { + items, err := snder.Collect(feed, lastSentLink) if err != nil { - return err + return lastSentLink, err + } + if len(items) == 0 { + return lastSentLink, nil } var wg sync.WaitGroup @@ -108,7 +95,8 @@ func (snder *Sender) parseRssFeed(feed string, since time.Time) error { } } wg.Wait() - return nil + // first item is the most recent one! + return items[0].link, nil } var mentionFuncs = map[string]func(snder *Sender, mention mf.Mention, endpoint string){ diff --git a/app/webmention/send/send_test.go b/app/webmention/send/send_test.go index 0af8625..5336a54 100644 --- a/app/webmention/send/send_test.go +++ b/app/webmention/send/send_test.go @@ -12,7 +12,6 @@ import ( "net/url" "sync" "testing" - "time" ) var conf = &common.Config{ @@ -22,54 +21,6 @@ var conf = &common.Config{ }, } -func TestSinceForDomain(t *testing.T) { - cases := []struct { - label string - sinceInParam string - sinceInDb string - expected time.Time - }{ - { - "Is since parameter if provided", - "2021-03-09T15:51:43.732Z", - "", - time.Date(2021, time.March, 9, 15, 51, 43, 732, time.UTC), - }, - { - "Is file contents if since parameter is empty and file is not", - "", - "2021-03-09T15:51:43.732Z", - time.Date(2021, time.March, 9, 15, 51, 43, 732, time.UTC), - }, - { - "Is empty time if both parameter and file are not present", - "", - "", - time.Time{}, - }, - } - - for _, tc := range cases { - t.Run(tc.label, func(t *testing.T) { - snder := Sender{ - Conf: conf, - Repo: db.NewMentionRepo(conf), - } - if tc.sinceInDb != "" { - snder.Repo.UpdateSince("domain", common.IsoToTime(tc.sinceInDb)) - } - - actual := snder.sinceForDomain("domain", tc.sinceInParam) - assert.Equal(t, tc.expected.Year(), actual.Year()) - assert.Equal(t, tc.expected.Month(), actual.Month()) - assert.Equal(t, tc.expected.Day(), actual.Day()) - assert.Equal(t, tc.expected.Hour(), actual.Hour()) - assert.Equal(t, tc.expected.Minute(), actual.Minute()) - assert.Equal(t, tc.expected.Second(), actual.Second()) - }) - } -} - func TestSendSingleDoesNotSendIfRelPathNotFound(t *testing.T) { var postedSomething bool snder := Sender{ @@ -205,7 +156,7 @@ func TestSendIntegrationTestCanSendBothWebmentionsAndPingbacks(t *testing.T) { }, } - snder.Send("brainbaking.com", "2021-03-16T16:00:00.000Z") + snder.Send("brainbaking.com") assert.Equal(t, 3, len(posted)) wmPost1 := posted["http://aaronpk.example/webmention-endpoint-header"].(url.Values) diff --git a/db/migrate-db.go b/db/migrate-db.go deleted file mode 100644 index dfe3875..0000000 --- a/db/migrate-db.go +++ /dev/null @@ -1,55 +0,0 @@ -package db - -import ( - "brainbaking.com/go-jamming/app/mf" - "brainbaking.com/go-jamming/common" - "encoding/json" - "fmt" - "github.com/rs/zerolog/log" - "io/ioutil" - "os" -) - -const ( - dataPath = "data" // decoupled from config, change if needed -) - -// MigrateDataFiles migrates from data/[domain]/md5hash.json files to the new key/value db. -// This is only needed if you've run go-jamming before the db migration. -func MigrateDataFiles(cnf *common.Config, repo *MentionRepoBunt) { - for _, domain := range cnf.AllowedWebmentionSources { - log.Info().Str("domain", domain).Msg("MigrateDataFiles: processing") - entries, err := os.ReadDir(fmt.Sprintf("%s/%s", dataPath, domain)) - if err != nil { - log.Warn().Err(err).Msg("Error while reading import path - migration could be already done...") - continue - } - - for _, file := range entries { - filename := fmt.Sprintf("%s/%s/%s", dataPath, domain, file.Name()) - data, err := ioutil.ReadFile(filename) - if err != nil { - log.Fatal().Str("file", filename).Err(err).Msg("Error while reading file") - } - - var indiewebData mf.IndiewebData - json.Unmarshal(data, &indiewebData) - mention := indiewebData.AsMention() - - log.Info().Stringer("wm", mention).Str("file", filename).Msg("Re-saving entry") - repo.Save(mention, &indiewebData) - } - } - - log.Info().Str("dbconfig", cnf.ConString).Msg("Checking for since files...") - for _, domain := range cnf.AllowedWebmentionSources { - since, err := ioutil.ReadFile(fmt.Sprintf("%s/%s-since.txt", dataPath, domain)) - if err != nil { - log.Warn().Str("domain", domain).Msg("No since found, skipping") - continue - } - - log.Info().Str("domain", domain).Str("since", string(since)).Msg("Saving since") - repo.UpdateSince(domain, common.IsoToTime(string(since))) - } -} diff --git a/db/migrate-pictures.go b/db/migrate-pictures.go deleted file mode 100644 index b1faa38..0000000 --- a/db/migrate-pictures.go +++ /dev/null @@ -1,69 +0,0 @@ -package db - -import ( - "brainbaking.com/go-jamming/app/mf" - "brainbaking.com/go-jamming/common" - "brainbaking.com/go-jamming/rest" - "fmt" - "github.com/rs/zerolog/log" - "strings" -) - -// MigratePictures converts all indiewebdata already present in the database into local byte arrays (strings). -// This makes it possible to self-host author pictures. Run only after Migrate() in migrate-db.go. -func MigratePictures(cnf *common.Config, repo *MentionRepoBunt) { - for _, domain := range cnf.AllowedWebmentionSources { - all := repo.GetAll(domain) - log.Info().Str("domain", domain).Int("mentions", len(all.Data)).Msg("migrate pictures: processing") - for _, mention := range all.Data { - if mention.Author.Picture == "" { - log.Warn().Str("url", mention.Url).Msg("Mention without author picture, skipping") - continue - } - - savePicture(mention, repo, cnf) - } - } -} - -// ChangeBaseUrl changes all base urls of pictures in the database. -// e.g. "http://localhost:1337/" to "https://jam.brainbaking.com/" -func ChangeBaseUrl(old, new string) { - cnf := common.Configure() - repo := NewMentionRepo(cnf) - - for _, domain := range cnf.AllowedWebmentionSources { - for _, mention := range repo.GetAll(domain).Data { - if mention.Author.Picture == "" { - log.Warn().Str("url", mention.Url).Msg("Mention without author picture, skipping") - continue - } - mention.Author.Picture = strings.ReplaceAll(mention.Author.Picture, old, new) - repo.Save(mention.AsMention(), mention) - } - } -} - -func savePicture(indieweb *mf.IndiewebData, repo *MentionRepoBunt, cnf *common.Config) { - restClient := &rest.HttpClient{} - picUrl := indieweb.Author.Picture - log.Info().Str("oldurl", picUrl).Msg("About to cache picture") - _, picData, err := restClient.GetBody(picUrl) - if err != nil { - log.Warn().Err(err).Str("url", picUrl).Msg("Unable to download author picture. Ignoring.") - return - } - srcDomain := rest.Domain(indieweb.Source) - _, dberr := repo.SavePicture(picData, srcDomain) - if dberr != nil { - log.Warn().Err(err).Str("url", picUrl).Msg("Unable to save downloaded author picture. Ignoring.") - return - } - - indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", srcDomain) - _, serr := repo.Save(indieweb.AsMention(), indieweb) - if serr != nil { - log.Fatal().Err(serr).Msg("Unable to update wm?") - } - log.Info().Str("oldurl", picUrl).Str("newurl", indieweb.Author.Picture).Msg("Picture saved!") -} diff --git a/db/migrate.go b/db/migrate.go index f65b287..c68e0df 100644 --- a/db/migrate.go +++ b/db/migrate.go @@ -1,13 +1,14 @@ package db -import "brainbaking.com/go-jamming/common" +import ( + "brainbaking.com/go-jamming/common" +) // Migrate self-checks and executes necessary DB migrations, if any. func Migrate() { cnf := common.Configure() repo := NewMentionRepo(cnf) - MigrateDataFiles(cnf, repo) - MigratePictures(cnf, repo) + // no migrations needed anymore/yet repo.db.Shrink() } diff --git a/db/repo.go b/db/repo.go index 3d5ffb5..453600a 100644 --- a/db/repo.go +++ b/db/repo.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/rs/zerolog/log" "github.com/tidwall/buntdb" - "time" ) type MentionRepoBunt struct { @@ -20,41 +19,41 @@ type MentionRepo interface { Save(key mf.Mention, data *mf.IndiewebData) (string, error) SavePicture(bytes string, domain string) (string, error) Delete(key mf.Mention) - Since(domain string) (time.Time, error) - UpdateSince(domain string, since time.Time) + LastSentMention(domain string) string + UpdateLastSentMention(domain string, lastSent string) Get(key mf.Mention) *mf.IndiewebData GetPicture(domain string) []byte GetAll(domain string) mf.IndiewebDataResult } -// UpdateSince updates the since timestamp to now. Logs but ignores errors. -func (r *MentionRepoBunt) UpdateSince(domain string, since time.Time) { +// UpdateLastSentMention updates the last sent mention link. Logs but ignores errors. +func (r *MentionRepoBunt) UpdateLastSentMention(domain string, lastSentMentionLink string) { err := r.db.Update(func(tx *buntdb.Tx) error { - _, _, err := tx.Set(sinceKey(domain), common.TimeToIso(since), nil) + _, _, err := tx.Set(lastSentKey(domain), lastSentMentionLink, nil) return err }) if err != nil { - log.Error().Err(err).Msg("UpdateSince: unable to save") + log.Error().Err(err).Msg("UpdateLastSentMention: unable to save") } } -// Since fetches the last timestamp of the mf send. -// Returns converted found instance, or an error if none found. -func (r *MentionRepoBunt) Since(domain string) (time.Time, error) { - var since string +// LastSentMention fetches the last known RSS link where mentions were sent, or an empty string if an error occured. +func (r *MentionRepoBunt) LastSentMention(domain string) string { + var lastSent string err := r.db.View(func(tx *buntdb.Tx) error { - val, err := tx.Get(sinceKey(domain)) - since = val + val, err := tx.Get(lastSentKey(domain)) + lastSent = val return err }) if err != nil { - return time.Time{}, err + log.Error().Err(err).Msg("LastSentMention: unable to retrieve last sent, reverting to empty") + return "" } - return common.IsoToTime(since), nil + return lastSent } -func sinceKey(domain string) string { - return fmt.Sprintf("%s:since", domain) +func lastSentKey(domain string) string { + return fmt.Sprintf("%s:lastsent", domain) } // Delete removes a possibly present mention by key. Ignores possible errors. diff --git a/db/repo_test.go b/db/repo_test.go index e6095f7..157eb93 100644 --- a/db/repo_test.go +++ b/db/repo_test.go @@ -5,11 +5,9 @@ import ( "brainbaking.com/go-jamming/common" "fmt" "github.com/stretchr/testify/assert" - "github.com/tidwall/buntdb" "io/ioutil" "os" "testing" - "time" ) var ( @@ -48,23 +46,13 @@ func TestDelete(t *testing.T) { assert.Equal(t, 0, len(results.Data)) } -func TestUpdateSince(t *testing.T) { +func TestUpdateLastSentMention(t *testing.T) { db := NewMentionRepo(conf) - nowStamp := time.Date(2020, 10, 13, 14, 15, 0, 0, time.UTC) - db.UpdateSince("pussycat.com", nowStamp) - since, err := db.Since("pussycat.com") + db.UpdateLastSentMention("pussycat.com", "https://last.sent") + last := db.LastSentMention("pussycat.com") - assert.NoError(t, err) - assert.Equal(t, nowStamp, since) -} - -func TestSinceFirstTimeIsEmptytime(t *testing.T) { - db := NewMentionRepo(conf) - since, err := db.Since("pussycat.com") - - assert.Equal(t, buntdb.ErrNotFound, err) - assert.Equal(t, time.Time{}, since) + assert.Equal(t, "https://last.sent", last) } func TestGet(t *testing.T) { diff --git a/mentions.db b/mentions.db deleted file mode 100644 index 07db498..0000000 --- a/mentions.db +++ /dev/null @@ -1,714 +0,0 @@ -*3 -$3 -set -$48 -18acebf759df6d79f8a109faa8d18fc7:brainbaking.com -$625 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"I changed my mind on my toot syndication to brainb...","content":"I changed my mind on my toot syndication to brainbaking.com policy. From now on, only non-replies (in-reply-to) get pushed to https://brainbaking.com/notes/ - it was cluttering up the RSS feed and most replies are useless to non-followers anyway. Sor...","published":"2021-03-20T13:27:00","url":"https://brainbaking.com/notes/2021/03/20h13m27s36/","type":"mention","source":"https://brainbaking.com/notes/2021/03/20h13m27s36/","target":"http://brainbaking.com"} -*3 -$3 -set -$48 -1b2ba8c60768014afa147580d452ced5:brainbaking.com -$633 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"I changed my mind on my toot syndication to brainb...","content":"I changed my mind on my toot syndication to brainbaking.com policy. From now on, only non-replies (in-reply-to) get pushed to https://brainbaking.com/notes/ - it was cluttering up the RSS feed and most replies are useless to non-followers anyway. Sor...","published":"2021-03-20T13:27:00","url":"https://brainbaking.com/notes/2021/03/20h13m27s36/","type":"mention","source":"https://brainbaking.com/notes/2021/03/20h13m27s36/","target":"https://brainbaking.com/notes/"} -*3 -$3 -set -$48 -421c001c7efa6e36e6f331c005d31c27:brainbaking.com -$597 -{"author":{"name":"Henrique Dias","picture":"https://hacdias.com/me-256.jpg"},"name":"Site Ideas","content":"A bunch of ideas for my website that might never get implemented.Wanna know more about the website? Check out the meta page!Finish uses.https://cleanuptheweb.org/Create /meta/historical with pictures of old versions of my website.Remove .Params.socia...","published":"2021-04-17T06:46:33Z","url":"https://hacdias.com/notes/site-ideas/","type":"mention","source":"https://hacdias.com/notes/site-ideas","target":"https://brainbaking.com/post/2021/04/using-hugo-to-launch-a-gemini-capsule/"} -*3 -$3 -set -$48 -4b0d1434331f1a382493b2838dc4b1a2:brainbaking.com -$421 -{"author":{"name":"Jamie Tanna","picture":"https://www.jvt.me/img/profile.png"},"name":"","content":"Recommended read: The IndieWeb Mixed Bag - Thoughts about the (d)evolution of blog interactions","published":"2021-03-15T12:42:00+0000","url":"https://www.jvt.me/mf2/2021/03/1bkre/","type":"mention","source":"https://www.jvt.me/mf2/2021/03/1bkre/","target":"https://brainbaking.com/post/2021/03/the-indieweb-mixed-bag/"} -*3 -$3 -set -$48 -5854ce85363d8c7513cd14e52870d46b:brainbaking.com -$708 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"@StampedingLonghorn @256 Don't forget the cleverly hidden Roland MT-32, a majestic piece of p...","content":"@StampedingLonghorn @256 Don't forget the cleverly hidden Roland MT-32, a majestic piece of pre-MIDI standardized era synthesizer. What else would you use to run Sierra Online games, and monkey1? I really need one for my 486… https://brainbaking.com/...","published":"2021-03-02T17:13:00","url":"https://brainbaking.com/notes/2021/03/02h17m13s27/","type":"mention","source":"https://brainbaking.com/notes/2021/03/02h17m13s27/","target":"https://brainbaking.com/post/2021/02/my-retro-desktop-setup/"} -*3 -$3 -set -$48 -69c99dcf12c180b8acb3c1c218d1f388:brainbaking.com -$655 -{"author":{"name":"Jefklak","picture":"https://jefklakscodex.com/img/avatar.jpg"},"name":"Reviews from 2001 revived!","content":"Good news everyone! Futurama might not be back, but old PC gaming previews and reviews from UnionVault.NET, one of Jefklak’s Codex ancestors, have been revived. I happened to stumble upon a few readable HTML files wile looking for something else and ...","published":"2020-09-25","url":"https://jefklakscodex.com/articles/features/reviews-from-2001-revived/","type":"mention","source":"https://jefklakscodex.com/articles/features/reviews-from-2001-revived/","target":"https://brainbaking.com/post/2020/09/reviving-a-80486/"} -*3 -$3 -set -$48 -78e327d69a3f5fa151f660b5a25c06a9:brainbaking.com -$682 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Heads up RSS feed readers of brainbaking.com! Federated half-baked thoughts (https://brainbaking....","content":"Heads up RSS feed readers of brainbaking.com! Federated half-baked thoughts (https://brainbaking.com/notes/) are now integrated in /index.xml 🤓. Don't like that? Subscribe to /post/index.xml instead! Next up: webmentions, PESOS-ing of Goodreads revi...","published":"2021-03-03T16:00:00","url":"https://brainbaking.com/notes/2021/03/03h16m00s44/","type":"mention","source":"https://brainbaking.com/notes/2021/03/03h16m00s44/","target":"https://brainbaking.com/notes/"} -*3 -$3 -set -$48 -792ed44939d1396d5156f1589e95b786:brainbaking.com -$707 -{"author":{"name":"Jefklak","picture":"https://jefklakscodex.com/img/avatar.jpg"},"name":"Rainbow Six 3: Raven Shield - 17 Years Later","content":"It’s amazing that the second disk is still readable by my Retro WinXP machine. It has been heavily abused in 2003 and the years after that. Rainbow Six' third installment, Raven Shield (or simply RvS), is quite a departure from the crude looking Rogu...","published":"2020-11-01","url":"https://jefklakscodex.com/articles/retrospectives/raven-shield-17-years-later/","type":"mention","source":"https://jefklakscodex.com/articles/retrospectives/raven-shield-17-years-later/","target":"https://brainbaking.com/post/2020/10/building-a-core2duo-winxp-retro-pc/"} -*3 -$3 -set -$48 -83313bc67b0730459876ab7c3b9738f5:brainbaking.com -$674 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Heads up RSS feed readers of brainbaking.com! Federated half-baked thoughts (https://brainbaking....","content":"Heads up RSS feed readers of brainbaking.com! Federated half-baked thoughts (https://brainbaking.com/notes/) are now integrated in /index.xml 🤓. Don't like that? Subscribe to /post/index.xml instead! Next up: webmentions, PESOS-ing of Goodreads revi...","published":"2021-03-03T16:00:00","url":"https://brainbaking.com/notes/2021/03/03h16m00s44/","type":"mention","source":"https://brainbaking.com/notes/2021/03/03h16m00s44/","target":"http://brainbaking.com"} -*3 -$3 -set -$48 -9427d6a53258e670b0988bbd7f9a6ea8:brainbaking.com -$704 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"I've been fiddling with IndieWeb stuff the last week and all in all, I think it's a mixed...","content":"I've been fiddling with IndieWeb stuff the last week and all in all, I think it's a mixed bag: https://brainbaking.com/post/2021/03/the-indieweb-mixed-bag/@kev after I published it, I found out your \"removing support for indieweb\" post. Seems like we...","published":"2021-03-09T15:17:00","url":"https://brainbaking.com/notes/2021/03/09h15m17s30/","type":"mention","source":"https://brainbaking.com/notes/2021/03/09h15m17s30/","target":"https://brainbaking.com/post/2021/03/the-indieweb-mixed-bag/"} -*3 -$3 -set -$48 -d6a0a260f50553c15cd4f7833694c841:brainbaking.com -$727 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"I pulled the Google plug and installed LineageOS: https://brainbaking.com/post/2021/03/getting-ri...","content":"I pulled the Google plug and installed LineageOS: https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/ Very impressed so far! Also rely on my own CalDAV server to replace GCalendar. Any others here running #lineageos for priv...","published":"2021-03-01T20:03:00","url":"https://brainbaking.com/notes/2021/03/01h20m03s35/","type":"mention","source":"https://brainbaking.com/notes/2021/03/01h20m03s35/","target":"https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/"} -*3 -$3 -set -$48 -f101a8437084c39583a39acbcbd569c5:brainbaking.com -$581 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"@rsolva that's a lie indeed 😁 see https://brainba...","content":"@rsolva that’s a lie indeed 😁 see https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/ I use davx5 and it works flawlessly","published":"2021-03-13T09:58:00","url":"https://brainbaking.com/notes/2021/03/13h09m58s25/","type":"mention","source":"https://brainbaking.com/notes/2021/03/13h09m58s25/","target":"https://brainbaking.com/post/2021/03/getting-rid-of-tracking-using-lineageos/"} -*3 -$3 -set -$50 -0f35e81e7376278498b425df5eaf956e:jefklakscodex.com -$610 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"I much prefer Sonic Mania's Lock On to Belgium's t...","content":"I much prefer Sonic Mania’s Lock On to Belgium’s third Lock Down. Sigh. At least 16-bit 2D platformers make me smile: https://jefklakscodex.com/articles/reviews/sonic-mania/\n\n\n\n Enclosed Toot image","published":"2021-03-25T10:45:00","url":"https://brainbaking.com/notes/2021/03/25h10m45s09/","type":"mention","source":"https://brainbaking.com/notes/2021/03/25h10m45s09/","target":"https://jefklakscodex.com/articles/reviews/sonic-mania/"} -*3 -$3 -set -$50 -169d68f8372aca44b39a3074a80429d8:jefklakscodex.com -$487 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"The insanity of collecting retro games","content":"Is a physical collection really worth it?","published":"2021-02-21","url":"https://brainbaking.com/post/2021/02/the-insanity-of-retro-game-collecting/","type":"mention","source":"https://brainbaking.com/post/2021/02/the-insanity-of-retro-game-collecting/","target":"https://jefklakscodex.com/articles/features/super-mario-64-aged-badly/"} -*3 -$3 -set -$50 -3bee3dedf14a37425fc4d3ec2056b135:jefklakscodex.com -$447 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Building an Athlon Windows 98 Retro PC","content":"Gaming from Quake to Quake III: Arena!","published":"2020-10-17","url":"https://brainbaking.com/post/2020/10/building-an-athlon-win98-retro-pc/","type":"mention","source":"https://brainbaking.com/post/2020/10/building-an-athlon-win98-retro-pc/","target":"https://jefklakscodex.com/tags/wizardry8/"} -*3 -$3 -set -$50 -460e6c81d63550d9bfb79051bb86eb11:jefklakscodex.com -$489 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"The insanity of collecting retro games","content":"Is a physical collection really worth it?","published":"2021-02-21","url":"https://brainbaking.com/post/2021/02/the-insanity-of-retro-game-collecting/","type":"mention","source":"https://brainbaking.com/post/2021/02/the-insanity-of-retro-game-collecting/","target":"https://jefklakscodex.com/articles/features/gaming-setup-2007-flashback/"} -*3 -$3 -set -$50 -509cf066810eadacb400e2397de5ed86:jefklakscodex.com -$473 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Win98 Upgrade: GeForce 3 Ti200 vs Riva TNT2","content":"Get more out of that AGPx4 slot!","published":"2021-01-28","url":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","type":"mention","source":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","target":"https://jefklakscodex.com/articles/features/the-best-and-worst-retro-hack-and-slash-games/"} -*3 -$3 -set -$50 -6880bf0f22930290ace839175db2ea34:jefklakscodex.com -$424 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Win98 Upgrade: GeForce 3 Ti200 vs Riva TNT2","content":"Get more out of that AGPx4 slot!","published":"2021-01-28","url":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","type":"mention","source":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","target":"https://jefklakscodex.com/tags/wizardry8/"} -*3 -$3 -set -$50 -6c97b9d5bf75e6601d3560dc3ef43771:jefklakscodex.com -$482 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"The Internet Killed Secrets in Games","content":"What's the 'secret' of the secret cow level in Diablo II?","published":"2020-11-19","url":"https://brainbaking.com/post/2020/11/the-internet-killed-secrets-in-games/","type":"mention","source":"https://brainbaking.com/post/2020/11/the-internet-killed-secrets-in-games/","target":"https://jefklakscodex.com/articles/reviews/gobliins2/"} -*3 -$3 -set -$50 -7ea008404c0004e592dc84213253d942:jefklakscodex.com -$479 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"The Internet Killed Secrets in Games","content":"What's the 'secret' of the secret cow level in Diablo II?","published":"2020-11-19","url":"https://brainbaking.com/post/2020/11/the-internet-killed-secrets-in-games/","type":"mention","source":"https://brainbaking.com/post/2020/11/the-internet-killed-secrets-in-games/","target":"https://jefklakscodex.com/articles/reviews/sacred/"} -*3 -$3 -set -$50 -8fc122b91788ceccb834ae721031ad98:jefklakscodex.com -$440 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"Win98 Upgrade: GeForce 3 Ti200 vs Riva TNT2","content":"Get more out of that AGPx4 slot!","published":"2021-01-28","url":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","type":"mention","source":"https://brainbaking.com/post/2021/01/win98-upgrade-geforce3/","target":"https://jefklakscodex.com/articles/reviews/dungeon-siege/"} -*3 -$3 -set -$50 -e8cd7a857456a9de776e7a13c982516c:jefklakscodex.com -$502 -{"author":{"name":"Wouter Groeneveld","picture":"https://brainbaking.com/img/avatar.jpg"},"name":"A journey through the history of webdesign","content":"Using personal websites and the Internet Archive","published":"2020-10-04","url":"https://brainbaking.com/post/2020/10/a-personal-journey-through-the-history-of-webdesign/","type":"mention","source":"https://brainbaking.com/post/2020/10/a-personal-journey-through-the-history-of-webdesign/","target":"https://jefklakscodex.com/tags/baldurs-gate-2/"} -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T12:54:38.452Z -*3 -$3 -set -$23 -jefklakscodex.com:since -$24 -2021-04-16T14:40:36.133Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T13:36:36.230Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T13:37:44.188Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T13:38:26.658Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T13:49:17.147Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T14:47:47.351Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T15:51:26.080Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T17:01:58.204Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T17:55:20.524Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T18:53:36.453Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T19:46:21.245Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T20:47:47.622Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T21:49:08.986Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T22:51:16.375Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-18T23:50:29.318Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T01:07:35.145Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T02:08:12.030Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T02:56:24.545Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T03:52:37.159Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T04:53:16.053Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T05:50:13.478Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T06:53:12.051Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T07:48:50.675Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T08:50:56.390Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T09:50:59.391Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T10:50:27.567Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T11:47:46.936Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T12:55:55.440Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T13:50:11.048Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T14:48:35.217Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T15:51:50.183Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T17:02:07.277Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-19T17:55:47.794Z -*3 -$3 -set -$23 -brainbaking.com:picture -$4861 -JFIFC  - -  -  -    - C dd A!1AQa"q2BR#$c 3STr5CDb1!1QA"aс2q#B ?7js7d;zczğюüzʔÃa6c4}'.Ǽw=COwb]yV^NG~ssI/N_/HLsTAU`lm._ԘuJ\@Y'`{e{EMfۧ<^46/43۷q:r6LfD(XnGǠR !5H[^?,\}R4qЄ幱t߹WミE[F5en}qns?QOͳ^1`>}\NO -DTGͭCDS|seavԴnN z>A({FAEbYMf[67<#`DD]k)xߤF}H%uXQ]ntRm1"A?"\ ,}i.s_a&A-BF0KYWJ>\ gaAiz Chn (9tڙ~Dy 徳U>HCOeۈ}kؕ -@mu :y"EPm/Sihuj.´Æ\b)Pd-U.cWVu՝AX՚t'&}EMq9,|Uy6etePlm+$P!˝RRdRRL86*Z@Iv]FZTP&.'O\ziovʬD亣2f58"oxB_u - ;;Uu0(76 3*>$9͵ˢz40_+=t$zHA -Jnv1(F:D[[MKJRPt5dD=Z](xqlT Ru/1`}cĎ09u:bUES- j&Ц>@ǿG&eW?]*r;K,?ᷗ;,ks=ʎѲHa&S*D*o`}W/Y?YK3MΐjK.*-r6``vT763"WиAA WđJSmln iPI -m4!Yyi"J:vM"HIX@l\JFP -[dJf2`i1K56.V]i,)vmߩC-F.'*"`;w?ɕN%XŸ$wJr-h)G*AkT]t'Z%g_'N+5R*z.ʪuEm#?KXs[;\6N_$ 1t -J߬zx/[alUoUo510([G_O93 -nRVolMlSc/kF5CLi{Z-ƝݲMMk tƤURe -Ո\*yN6I#ف{ ;ΙT[(nkJ|>]u)]DG*>nr#hLց;tQ& Qȇ6jRS.: -W) q犕U]',ЉHjfbd:i.JrlʭI1]l?Wբ[1 $6J&KH{bi17&pOa_Z cZO)Y3ayivpaD>M0󓦚#8eYPQ-QI7z]ʵ?X?T4Ӿ-NY5Lj]9.Ř؆i) -`{d(m}t3iJ}#_x-L/2U[m- *\npjUYE 43>$Nޝr'Q8ƧJ:q'e!>e@}[K`5\AJqb:ٳÔ,i>ι5>TO_4t:8.,ARY%B;}M֗_Ra4O&RpqTjg֫SJMI.`MAjPe^B7J%#Dz\bKUj X9(^21z̳f@ֵSvF->Җ4VMo-v,$=: ,-/^ -qQͫSZiYt^ǯ3q]1ލ) B.=ŷs: !].ujY)3*BͅGk gQбr[vL_Y9t"=A2g)0xQkeIW%?HUpOG,૗o{)| l0t,j\L !'DFɱ C`c׶*3x7ȄQ_:lZw+YnIi`8Nف6Bmiߐ0Âc`mE7G$ykX ED8H6:8xI;dFzB$rd%-@6>V%%&t;ۧ(m)Xhxv殶<3UuɅLr3Xw2lOvnB-Itڏ/dGɄR3npwviՋ%j \)^$@/!@P%!&y/PlfM.ؖl[Ζ)wfWJbVS9s#$Dz/)VDsf)O9{BMDAt֖V8z(%\&Fk[ D/h\5|if/_RI:)dѢIMX -}[[h)t}q\KL3ń6ͿWURhn[K&r$JG? |C -0c*E?E61RK.yK9~?)6B%X~OeBǰx@<@ej臟:ƨ;Bqf \s{*)O7YXKېHJr/)%0b 42=!Vs M]?ӊ㣉@%&U !@s>&KO7d%.oH -ͯ=Yr #7G\R@c\f -*>m44$ wi7=~NߗiB(I0#QQBJ1wմn}} -8fDǓ(htqS~?egsQWBܽ<8k(ʷHgo'K?ț[d!5̋F"=ҼF C\o -hx -`߼P5gL6\>% ɳNƧ]B`*dAo"_$c yBe9{f}% "wCBK$c>%h) D Tx°mt€=ϼ>)h ]v1@NR"MW$qx{Jrk!=؉ e\U3m8D?/;%$\ Jѥ@ݑ5s w2 bVDB('bv9"dֺgo Bݼ=V˫;$ Ͽ'=&M4 -Sٛ`CAY_1y͟ޣ^ׯJMҊiW0I*=(΂M=lTPG|]k9hn_s5'~'0^HꄜE`7\mVr~gwU|u `4l#1‰@eDOb+Oo;:+_ H"x'ϊazjTd-$B , - #-ㇿ*I`?lC -N4Z(e?\̛ -܇Noy`|ҎA*f*i{LKg'+)( Nc_WHS($!GIjNrDJ &9RS|-jG$қ Zq">*FƬi_Eț_>%W6gqRJ\eo[r9^vnI A*3]"HIX"h<b 4(X.JHʻylPz{RkWhTػ{/}K -9="$[ <H -S Dq7Է(pNu@ 3µtxŽJx^#WU3i0 -xuKmyf\]_*W? -臀j mj 5ǟ|ƽx*a_Ds7)RJфPx,ƪ:cHzxW^fV`EdK -pJ$@b!=nӒBYxqʂ="Nx -ubO}uw-W@S̀N7,0tW~%pDspe-sE (_gɩ9ЋUPX.zN722_ yT]{iڸf=?m!@sO&_׊_8b׫6^=]|wwPxFx**bg]a')fTɷyxuGjI D/}i1@PڏƉ( -m᳘($&R _N]Ěc9%CaEО.dpsL*+xq -j>914"X$b&rw{Z=gE lk`lC>V9P<%*XL/#2KB/'hӘVu$ݟvNHPX5Z%t |z>78V䠰 -^'9ָd!3 Ӓp)c|oW #'o˧CA􋗑q{;pLv>H:oZf+ ?aG߼/oCHnOBBqo0@X=/}qKiIdls.'M"J"S5)4)Npl.@B? 0aWROQe\2'R~7oyʓ:{vZ~"+#4,NN_*T319'F)B4ȄQG;d;tDfIlGh?r0\A)!o\`{> cwvJz -=xD?us1 }Qf wO]CѱijnyLR+grRx>%jTX"E:shu7o{ۻGо_O B>}r AKhTlˈ &*?FeS_4?ȪM>2r;#b;g*{ɉy?uuFFcZDвMmVI5{CL $*Yb-7G]`a#|%ɿcϽ1E F-8Ƽ0L+f &_^W:Y*40 -?X8^<;INB柗m>s~!N08M J%-p% N$@ ^xW? _\6L?|v0FOگP4tJflَT#8 ~rY.HrC_T=6 pA/5V4!5Ks =nƯ=6<>>.˥~(YM;P@"`fAj˽wh2~V1$'s1TIki J(G _icH}L>Qx -N/2,=8D ^?\5t#1G[3o_91hy'"?NYlUz>r,goHLt!8,ѻ[*7RE_eW.H -%fbȟD/^X]Φ?õh#*| 31+Z Nr_JGI[ssrk:59:Y=IlL=X˿x9SY5t|1kJʙA@_v>PɀkedUd=5ĝcVpD2;5ewk'HV)/y -[Hf3'N#ѯؐA*`Wh5nWz* 'O+6>HJ'WᓮGp7|η6n|oE减 扠(uH7bKCjmg+xNMgAyԊݸ,i)wK|,TS>)-i%ls~c DJgd[3ú$ShaA4 -6 +- T}"$vu凯j [ʵ f4ЊK9M%A^dtu@AHl Cf*yehs4ڏOp-dr10o!Τ -px Tg+)5j& -0a~BLIzv{$fps/Y)IeGI?(-#eX^T!A%h &Iegvi30f>۾lwY\Es8̒{c/DH$rgPxe1ȉ==Du~j7,ΘfH~g"8 Bń8 -=jmIzG\fp)?4C'g-)rի! 臢bXGP!Ϳk!#/Un"c(=^6Z8,]@~4pGdPFKJl=|b]q73 -L2vnc݌#.[oax\>wnqTp'ꆐ&;ϲ -ź:gJͨu'jdadƄW1K|2#.*~}#ʢ,N#SKJK~lʊ -t̿( ]a' # Nŧ -+B`iȗl~ }$&?Q'.J'=$ 2 J?Ȕb oO#ԁ~{Gj͂v|Am9%]S'3 2<  )mU?$3IN {A~Pg 0^)-I+σrrr C$|6-Q 86QطUTo}kol?>;FP3ZqptMǩ&o(w׶hrNsR*C0cx??HzIxt΁&٦NaL`¢Fw gIaB `PFyXG76~K/3΄o &ҹW{?xG%/xM-yTa@j4Uq5Awvv$4 ͪxkOD/JE;gY G0H"=N2w <"kLr+ +8;O~(3 Z*.B)+x>҂.GĴҝc&OQ'֓l`/֛;~ (?|DZUq!Cw.r4 -0a^q4_|ݞcڅ eB$0qۉʱǠ*"CaGcwF(}x_@둺^foM!h0 >"ϿV_1a: /ȧ},6kxqyOA(|efZ)Kz:W$I_pYkz<\43 `>\#fckۉuI1#'z{xJmMuOOBgeg_So?쯿x;<]3r7^j# Ȃ1mѴ\ dQ<{*Qf$fBLgkHk(G,#\E0/Ym5W_0͠2I2V̋av~'|g}ּD&[& )^%}ʄ4+zrn XRfX02A ]vid1INf WriJS5=H& R@NA׎3π$XyۆbxN@6]qS(WE,鶛4A'iգI72΂d;RbJ\r`0{;@D5p>wfG&RAP)T+xYLDA ssO-\ū9Qsg`.Y&ƫx&PijQqrmu#p3=ѭ"DV?Ė\fIN"9*_B{5nE,y/T_͇?j9!h'w˧k=صL&^hYd W0(oee|=D#pEvЂ,AXC5L M/ \DG?i$X؊y0ɢ\i2D+j߉(RMhWJ^_?ޭυljljT%Iann|P~%/?mjn[€C-TϾZ cO OM3$O*PKp=>A1z1tPҫ+#( -qJ%gh b` -I|J?ڎK@\KЈPۜ;ۤs(W3#@ ~0G~32ͻxuy̅cVC~a G -`-ijt fx ʆ[14oU62RD$ ->W髰3;yw4$XV+fT5I$ԑvFL]`aQgV2~@Z̅ J`GJvy'w>ڧF?YJ PBxsx'T3ZuMdLbjpiW]n%{x'" D: @xVfV:ҋ8Y&j5ɏfD|g"57ܜOf JOd.^7`Oxz85  l!]!-uIx!76JV@Ow1M(;De3>5B$B:y% :  abc;ɐ˩ - kw8iH~$P)0ɻ;%<@0M>EW) -Q+Fv4O{) -Ln(Y^ nh zz_D'%VـI2SVbҵoH[ `L ()Y_9O$Pl:%x"j7(gXŮ(Rj*t/[)hEǘ ޞQqu> 鯾5_*FjvŭM-٣LuSuwHV],Kt`>e^z5W91 - IQHO\EN[(5VbFaOxJjvP"aQ~Nӧ:^6аca+/Jz˃ - M*nɲ7Sރ|qdX8 kCp>=|^Mq3PCpfovJӣ#aXlclgDdΣ[ed-oČt;̸4YyOȮKNtt]cu2T  O#},ҥ \rHn,A_ҋ1 d]YR )jS&Pd< CLCG,g*?hN!&N~&rH٤MKRAN>d<0QPlj&[]>u0 #[\@(oViၭxꩋu/n'oj@]v/F IR`L|D9Ub2,H)7ZOb6C fa$c߅[<-4rtǔ?ᏱE=ޏ@5ko"[D\\w:@Q'mG2OMtEnt>ѰrJU(>} U/9\.Ͽ4W*v0upkDwLa7aIÀXk%lbqeUuUu d}A4A#Ot8s4nLѐ: >_%q`&4Z,w?uy}ݧ|nI=QN'X1yR޲,#>깽 wE>B‡~}aM7/[|yn ۸Z}lXQBR?Pazcd._+=T-:jVc%A!9WvJ9^j - ~-nc$!RINJ -h:. . e*! DMW%o\okf2g 3i-9"sFi`6pyG_*Z=wܭ,q&(ӾV)ᗷ"*)'[ 4ԐXCM&Z:'q]XQ&Vp~5) e>kN?ԡ=X?37(z0Vj vCddh,"kރ.:HTӧ# pPO.%ap1e6-~W,:f3~=?]WHG@"r!1X^ L]g*Yx3Țہ|, vD|> Oj:h2QtfK/0ϟz>qjOJCY ,s4<焉`cDI%-^dC%ГV -1|IPH0(^ - bbΏ>Ә7t`I*cUxO~@h7rM!;х|f۾? l%= ?yrWx[]YV ->txǧ"!Wߋ!y(~zxOSV"ɻJPвK_ gDF|ţ rwrwb%%qe8v<-^ 3GD@L.4+PHRD~]ՉLYMV=3 й9JCßB~ן_>/QͿ/w/|>3<f1 -pԠخׅҬI@\[Gۄ4,2w\}穀'z0, B :'x2CE'#nHY -0p^LD1/|[w>D>[>wWo7?ќ@榦Ңp4`H ϽH⪷K^1Y^q_xF<%ߺ݂A0p=3<~/ĺkdbX iΧ644UW B]QeNS|-q0XXik^L s5E@B,}!G_~_Ƚ}'1&?bsԽ-~&(u62LZ}Wц_ǤF6wA%0$H_-300Q0Q.L~H;$4@~c!c)Bg?])"NcI -A (4[f74j:%D;P/%l8d 0K!ZVOLyȟ_>`ffi^'! r1XаbǓ}V= -uTfiFZpªC~~GDf0<ہ2,s~2LVT^Xo:вimw+ Z(i1Gz6?G6vTk[ 6m4;10-BKkDYgdեţ=ޏͿg:?x-_0 \أ\I@S֓oaZ IxcȆbY\_(Yk+Gs31ׇD?yunqjU|O}8}/ -a+!%V? -Bf!Wc/cKak_s '$Ȟ_Ryo- H9gxw덌~=_WW9ƼlzR&AS#)@h^1/Ǐ{u гTͪ1B~D#K`* p_Kq$9³=1a4 RL)?b?h0{U# 1Q727T8]]qqߡ=o-䠓.Ei޳؜hȼ{bRF{d'"7#ޠj Vjj%\;z / -ȁS0/ j2gq'E6E96=i1Ec;  75UarP@h_>zuxKe0H- -KD@JT8`wU(P1t{*NXU*^xqV9n ȏ$aQ0€Q1/rg:hxlZche쿞X | >><Ɠ ُx|;G<|T @?^˜XcG8wCDO~e.+RxY凅~#; fx -pT@!4bJzF) @ch~mS4m%SU>gܽ."j>ZHdegGrBeNz Ok]Z@5`4*º'^~*"ɩ)S|XT<KNtnEwz۶ -qe 5LQ?l s fڔӡ}e -Fà@JGR?ep*T@qMꨣ?NU/HT Ș!aaMz7I^Q>ÌALi03gHm$ SrȪ:ooD~wWwcO̸k]tvdzn_( ;yi?2[zևJ+Qz{Vʰ(2X -0_[c𵀈З y|Q?$7ll?ztC8[p\ ԩJ-QeXkky@L̚FP``DDtFݕ՗wO4!Lvt{4>x ÷.f(t?~zt| 7b96N0߯bE0vqclcji_c1>\P "_:\{Mɭ\8U=@J --7b[ZRfW}|GD?̳L^T*ipiƆb$A 8>zML=O`L mBUAf0;ݿ&-Geh=ZvO^/Jvo0F6Jת}*D3X+/pw38 ;Y@sE`}> -z.Sm<P\jUB -@}jUHb8^:h*QJ\?7 4Sx zPA3[|VVP B^*TYƹ=\0{\ 5(Dfg3 -և / GvW qǯ׃f>d#4U7JCM[BV/H1$Td؇p~?4e -p|Voנ<-̘JNZ/-^Ǐd W6?:t.Fm.t̿R%P8V -oWp/GQĦ\.L@ Ƞ }02B_A|)߅#&3#$M'N!)؁)z obMoy/-9 <ӋTn۱;iWU ,xa*@ r2x(C`3)Y4@ 2hR\OX5R48ԄU{3@_ke~/P'ȞĘnD18|$UpV_д{OFtiފd04aza.O2aͿolz| FWZ[۞|A;yWQ!:;,0@>mW\̿^.H肗*A \͎ i\SvA%.0 K6 hB -b8մ걻޸p_Pw zM̞ yH\kY(epW5\2#͝C+䠀;ay#ՠso~G|όÓx_>rp5 P߽}/VsS;~*[^aADz`pa|U<Sst`h T,8) $kDҷ\/<k*_q^"dԁwH/hI@\ছnl|󍸶^&S]\O/;bfS4 .uy0{XY-8S 7H@rÂ-W ƭD0&٢<=l5to3&x7s.g)R|[5Q"$D<8Y?XVu_+lV @ $HĄ)j|̿蜣0 zݸQ_8h~?֟A;bً*ww>z@Xw&}wV  -kd`UMVZ1Wskk⛁A"Q$ɒ\@'Azc뱊/ vUy8: -{X^ZhJ2L/Pse HlO`z -oD"/Es5jZ>nDBD(`[OwmΌK.3+lC>/W: &\zX^ XPO#3 Y@ϏE њw`Eċybec3W6=5?4sz)=ݜ!,.&-6 46.W"`]/)7w rsň#,[|l* -_RD u'a}41?tZĉɓ\"zH=ÍZ~PC,,M -+9]7.XΒDX:vW_Ja6 ܲΙ}=x)?n/vԏk&N(uܹc!/4͢bϸQ(@Ѻ-cZ p)#o -Z|¾1΂cje?s]l2I3}3|i.0`&{޳9(C-0r,H)R!I 'M8Z{8rWw08Mnj%GdNieAZ -UPd?i2a@SvJEhs3k g6̀{ܳm̲ |˻6AF!"LǍhL1;Ǭ {pa¥#|14̱߹#H4׻fOw7S# N#¸_Ԇ(dz,4ql~%<l -3([ 4^8BdmBv)#g;T=dن9BSQ?f&frZM Zp_ś$`J$Ys̃yx+#l |[qzlIEtUUciGaЄP@b M@Ig!p\-2lx7".$ ź |&aUƸԫsDp^0=;M|V -5ٍ.my#?ɠB[l h`J"``z EԴuWUKO -mwmk'9_KwXd@?~ 2a xA8i6vXoYy.*T "Iy#ڏ -prcT q6 -cq;,*}QO` +BSXos*k>w֏tZ4uX -5Tudq~9EV5٦chU>{ Wz됙 kԑO,(35ʟ):7-qZ35rcgA#WH๨п؊B<*ך;92x# 5S4,)}{zHkL@ ̷n9eR5ܧij+DWl9~Z ~4nh͉ъg); ~\-.hZ,(FlA_xSf[čkXiػsgC?KC(zux?5lciM:Dž XTO4Y(ͼwPy,TxЬ)K>d0;Yܰ0Oy*R\U"pZBNi)e#QQIԚ%B1A|W;m{NpEޒK2aFH1'hX啉UB_Wy/p$=g, yxr"}9DV0hƴ4쮈/R^b&2ckBg 0)Mu`N'tkƾ28yw" l|{:ɋ9wYy9;bnZ؏u`4Ji -OjZKxU~GV[q[8MԻMMx5V7o[|N 7ɲ{*1G&{CFq1x_2$ZȌ3{dU.f_Q0 Ӥ2QyU?2)d2I#%2x[~U5 ꜑n_ |I/2R?&%N3#\߁'3XRfq -PstX$gO)Kt:O~ t$fZ4䩈P4,ѿ{_0Ѽf9l%D1M48F}i -dWvśSRŸ os= -B--G[ oT&xG@Jr',}vsWM=+7_9 ȹFjAxV6HJК2۔!t i3 -36y㾺[.7{P{񔿯-H,\cۀc|dӰЃ$BS -QĐl<`>*xh@d2Þzv8 ԀRwν〳ϧ'mؐom4~)"IF -?V$^hyc=/\rh(JT3;Ea($M<~@6^3[6|̾Ã)gO -9w t\+,ܵ;Iз% ub$&͡hqz -UӲM(xdN2!GȎZB"2!Ʉj̮O^BBo_ at=z˦EBqkoBOi+Ӻ(>-_7#Tv|X:ټyeLs/!O9&4 ¨=<ЪI11#_%"+J;eSyOKѿ稫"L<C2NqZ޵e|pN9ώbCh) `$o*-$Kpwgܝ_h0 ~,^l&sÖz;NiD'N"¦ >y9 -! c,9U~J"<5C2Bo?-df{zʝgX-2q$L^,>ANo4?k,x NGkMKПIPWGy}gW, \|]oh 6)_aԄ?q0(=,-4 -.! -] MhC.,$tcXQeE,:8XnR>F~!Y -9FjF@VFLkC?@.B<Z#AݟgDjJúi}!5<!aY; } ϩ$*T`HB iŤBEU3 \~/i6IF0~:`Um=C=~J:#&L/0@τH -6A$VbN8ѳonxO݀?.C¿zvDž/˻뀳= R:4ޗkHHJ["?:ƐQlJ ۓ@!v@V0g='ׁvܡagr{FC񥷷RH?@y}U澀 j/?X?C7_oP{*^#m ] -JF -PHvC`gK",:8EĆ2ڭuK(LbI_a}N_3t iTҔq?uiA+7rM#BgբFji[ MO8X~S}5: ڴ -wu"qaA0`h}, pof+hBEgaV{enu{+'IZ0X -Yj13׽YО}rY:ښ<ގOBK'#Гtڻ6]A8J``Tc0 @,_~5 -73i$JzGb:YԵO>^zVewv-'>kyO}vGjcrZ1ev<a1 mvCRc(v)Ѹ,u`3o+WDD۾}3d(_\Q, S(R,_ػ(+=4с@B 0 %01v`skHMlx_/Ĺ>{7zĉ7Il-B\n[#Q[}3: 1O4z_{Ck]GD d;Z#n"ITo7߲A> ]:h7u2u Z.KޚWUHW* K 7޸e۶I@vcAE?(/H Շ b-u#l>Koa{6?OqziRi#N$$)XPHpC 6gn×y gٔ8Yfa57? iʅU,]}0\2 7P1wpH40[N>w$-idK# ( @$,vDc`_uewI5׏Xk}PXj>'?k?==.baZTӎ -A~CpH! "TrI=r=OTq)>ch!a O߭ ѺJ@+ŷ+&w_*{v,'wݦ_AJKV)^{8J`a~dW`t4U skV]*~_KM&8Ms`0Xzo^-.€!tt L$''3ghzmvBMAPEFf[h4$GÂ{2t=s)p?I( -[:LjsH5 NFK32a^AyA)_ \ ?{0eT!6D]Mcur낃Q3'9o[[+5~~N[V$iH.vNee Pa[uHr:PuUON`804-TOV+P$p w߂`pş"` 24QX }BBh;pׯp.r:`yW#_~~ {á@hw(G |cN_%I_^n*ΏE FF~.ksțD@60 -ׅt/@"ʀ0G P}9tl0OmU_?f7Ӗt~ "G_)׮f5^bY( ASUg"gS,i,ǽ/՘㗾5x]{ј^V=C*r, C [lh#a |W?AtW6k :K`Q33ݦQcΕQ^=AEAlKRaI wOW|Տ 8p-m  ?|{t_D-RNHQEư s5ǃox8g2vG͛u~!b11B8a8@k?Ũ X8?'q~2S"f_wu3Ž4,\,BajQ8 eM(}N϶nשׁ%CR5c~h~]M@!#nq&FAYȊ53Z^AHzݷc\ҏZfia +f+,t(޼.p"*AָdԽgg\ϱU+g.@ ]UVK[dx[/lG [!gñ8IE5,RmH{YV>➞̴m.N]0Jl^C 0L.K¢/~s %`$CÃCmI!gM 曪j.1 -B"luMHg q2!^L{~6FJФ柛/ŷ@]>S![>%Ic}W{O; v:QO r{AL[xG߷eןCU_XJԟ$A2a2|kn0\vmy+Ud=߲^s}ۻ48ܭXiϹ2I?@HumϮdzZ< goi hfmʭ>'1E_<<ؾ17?Q]S>Ehuht"\ * GGB%I -: 7.hoV>1gz max>Rmݜ ;)* n -U5S& -kF Qa= #MS,B䅤5 ;@vިjY/".+Dh5*}%]@&_? FTiz*%jײv<6`ڣз`nƒ_܎4PSi9+ʴchxS -AHq@gylF TJvO΍$8X3IvbK݄UOĪK:G"Ry,t!իf:i뷇,xQH[j 7|U^(wڗ~7tJ$qCU@7sDEJdI︒,-gk1u1J\?l!ScGc#R},ܙĂB3XD(a6º|`F]nٶV܏5c#6|m1 -`/ȧ+ja LT}buq1 B.39Y߰$B)6!=l'38HCs'(PyWs7 u%ThqWֲ110poҒ{wCB@fqP\rQ}fP} -ΣMF> jN)#^ U>dǺF Q tuE4'yk 0KvG5 icmnQ?yIƀV3J} ,ODoahh4WWh5%v2@z$i3Q4!|艂ӡ3'p]!a@ -D(!w]+ b6Emð\H60k;<6Of : ybWCPNڂ@ -q5 G?b;>]-鐢 -:#SH!^^┢B4rh"B .H Cg!-g.cObx$.ru\Jj 3͟S9sz"mAQU0bP#5]VOgACaJ/ -<y )Z()RZӒ2*c*%3CWC!^*g:IN S}%DU@EP # .@EPOSR!?VvZ*)zӺAt( :Bƣ?ibZQ1-=lO& RN\O[gV̛:#YY-8$ ^Abb|ݷh 6g_@bTCD}4'XX1x~D@gmᕌt?b'CfIjAaJ q\_=]A~`wE @CXADV-wK) &4$d84R[im-xI5s -`*L {h="&H+Ry9,~(G^yLfe!ҕ)XzɮiWcw柛㟛G< 2VOHA`RF"ipfd,B-m>,;5׾D/YJ>n.{|w ?>: OhxdƓ:D@0vAdES(M 0&T"* &$o@Ms0A`60q;IDxbDeʮ0Mth3Ȕ( 3^z+wHH^ yOHR OMQۂ[@@ZH}º>XzPKLE:nw2guٜ5h@8 IRv 1 $?>{Jwqt\@pՔBJqmk`D 5apmn-oג{wNWb3Mz:h |^G57P#| r+D~ͮZ=\e]@PӳlHO޴"?J˽L[:@@xrΓH]C3V(^kR&RS? Iu6}YRuzAnPFz+OJ@oc ꠜnfBzdZ- RR: b?6L*ǀͻq$4\m?(Qk<RH8ɏR 8L΋qi յPXX{{_]_IENDB` -*3 -$3 -set -$48 -2e0e5a9b6e609f71e02273e5de434e14:brainbaking.com -$622 -{"author":{"name":"Webmention Rocks!","picture":"/pictures/webmention.rocks"},"name":"Receiver Test #1","content":"This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\n If your endpoint retu...","published":"2021-04-25T03:49:45-07:00","url":"https://webmention.rocks/receive/1","type":"mention","source":"https://webmention.rocks/receive/1/0ae8c8642f0e3bbc75ff2b6147b3a06f","target":"https://brainbaking.com/post/2021/03/the-indieweb-mixed-bag/"} -*3 -$3 -set -$19 -zylstra.org:picture -$16040 -JFIF,,ExifII*} R(1 2(i8$CanonCanon EOS-1Ds Mark II-'-'Adobe Photoshop CS6 (Macintosh)2014:11:25 16:14:10Sonja de Ridder"'0230 - -  -124d -2014:10:24 14:55:322014:10:24 14:55:32`e@B@Bq2jLp23399282250.0 mmrz(HH Adobe_CMAdobed    -         " -? -  -  3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?LCk4A=nHnMH{x,X?xhIILK7Kl -bD"OtTo8IӲ-aҠkIk>˂fXX4w!4;AJOaIK W}Nu&{ BLܔߡ{+Hg6[c 7yW1Ë^%g:^%?Yss8h[F\pO`"6=JSGxq"yN2T)v\O`˄?#ʺe;D03G]ާߢeM>~fWg0\ (0sL%}K'1nꅄ4n@LE"2r׋ϸ@>{7" VAuڽ64J_A˺>].-V6j:eX ]9HsL~}hVSsO R@͎j;$m#<"=)9'-H> -$KJ -,C-is]@V٘t%?K70'~@!Z}msY=EL]mUU46u:"f8AMH)}Fe6;q͟[VCnnNq*9~LůCO kb߉qC+h䆍/C\(H W?d$:LQ;: - J~7@ndƪ7%bd&O߁kHdhwSkgG1ncv~z~6eghFWj`y*95u&*u֔w[1ۏ*$y(ӽۍ-3$aO l-8'cKyAn* XLIIL.d!'L'ok{ϒJYg縺܇s`iu<j^ P#BP)OLDB5)6ꂛ;vYnʀs.UҵWٹCv\hmwOW*똵Wuю%>1nS BukmuYU ;3۹]e]_\ۈAD^wFmsS*y6kcn9D[ZAE]p DZS׌ 2V<Ӓמ+,}]zٴ怜au_cn%$co?K.e~ʨȱtcn,]wȹٳ6Z{(ySF%4*"΋ՙ,ۋoCiϊx~Yqݒ,=T& 0 *n :!8QZvpa6C-Z5S2}GAh>)umK״e//n HADB<O⚕hTE~js&`hO s*Tuz/54}OhhG'%miGݝ fM-fMcdsoRȣeܜ][O=mJtUվ׏V@ЇXk LFk;,g>WHA[}mʹs6Ё*P5S'u)G;VX+h,0Ii.CyT-9bcm'SidjIB{h@k5JهV# wry\OHws2 -.3t7]>c3/v\" \òڬeOnǵWkpk 6+=@Aoqt<V2}l۰] -^~i>2$I oD.. :$q {q@(1/ 1N; -9:|xV}k1)s2ͱR_H?D,d|ܺ鱣XY:MoSm?<VV]aq&I'IR/G/VEY_]]{$t]R~XBǴx6۲և{u#Ғ;#!ޮedLlVF3ip%X_.z=p_U]im}7: \5mcEQms}2Z\oI ;Oqi ųKSIvNm/#;x]^W xzpq-h`"#:U `F+npMVVm%i=\^ӨR9F^`R%ۉ6ӺM`}*6Z?nQBVݤ;+*OΜ!\4BJ>0mT>M{`#w.cuF۝i^#y?p6 A>edDiwQdNwacIIL C<tpa%#'A+M_:%8=:.ln啐eNv=! cl82;7 h?kGcs!bďwի4d^E qZZuΔ D!yLO>l?u.'zAŌnN5_Yp8=)k;?:YY^|;j٬5Sx -n#*ɉ.mZ(b}gk'P;!OJL@ !2M= -J@lt4ZNWQĬ -"ZXLh\%o>β {}/{`wR0Nk\ƴ9eʕc9ֵCVAi5R%/qvtKkG\w1}d\V?Ŏvݻ(}Rܡ:E7DYPev0H`kw=c UYs^5y@շ< =Hk:}AmM -1`rCGхƒΛqwߗa:uLʾTY<FֺܹόirRR{-fi4Ρ'UVhd0KL*>f)k9L^HnI d蒙:p徏'#qc}v.~o|mcwSt>?XQuՎ@;g^BsdtS 6awFޒ`"9j(٥1:H?k IM΋ktK\A1z^["DϫX4fc).kZaov3 IJGܖԶXe6S 7*"CudY:KoY(_Y[NKEh (+wTu:3w` F4߿?576 $0ꬂOM8~qokniGցz3 -qfz%݅敏U iOsWG,OeǸz{Z+i;\܁|S"|˷2iЏ08Qq )*9vd \T/[20141024tSonja de RidderShttp://ns.adobe.com/xap/1.0/ C    -!'"#%%%),($+!$%$C   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$9!1AQ"a2q#bB$%4Rr"!1AQ"aq ?IQ}OƑ5 qVLV\^Fj¤\ڈk;J9ā ۀ_WFʹCvS@dB4o=HAeSiC8~y=r+1҆WX^(ȥhU iؠ%jO͚ G0|" Qm |* լ+)F -Nqh9 pFK`Tyк*hWid.{HW1&lCgaQ*7K'9wvoU$)'kp5wL-#GR3l&BɂjԁerzߧdT&'A9hI4iuC!-ob> -=޳T|wQv3O(s%:~T`|\y==^#wjPt`\ Qv4m}kvnVӖlaވO 9^nYd8h5ՍݸivX)w\;RŁDG+4hĠARQ>E%!1 -*xMW.uIzT&r=:_*^J[e$x𐑐{Pa=6Cg\|+dÒ#& jAꡩp(("AҢK+Bs[TD@u9vsMYs 2V hb5 -²v6{(V 6m@Դ yqYM4C~Vmmc7dc=is%ָoVs.N㹮]|%6%fe7N}q+lt yŪ,˵A$.=Ld]8.;I:52L"wXM+OUm' jԷ9xﴴneKK8e^l -X6zQΫ{pʶ<#xZL݋D^fm5K -:{{q]0#ᶆdʅ&&f-4bPb'\%1;Tan0x"tUM=Z=2` @{4QCBɪ\ R*_Kj&rP#;ۺ;[Mnv>չvxw -Mn+'&5\{3Rhvs1 ٻ?nͳ5{|@.V,KȒgEdkkج\dsY&]JyѰREGwkś@[#,܀Ip+N[jiju߀DcnjJ0B ]k;Tͤߦ0GpFF~ޥ:=N hJ͹S ~c.[oM- ĈU>X-nR qK?oL1'$n#޻I,ʾ{;}[pj bBEf CF\o^ME2H9l#(rvH6|`[:=]ʂ "Pj3̃Z81bSH ڈu&zHfn]V[ڲK0%Xֱy{oh;\} 6/6 ]mM`k_@Of8Eɚj́Iȣ;_>Z8Tq#ⷄntr.;y³VU/f.BI -6ތaڋRJgnyO&WwkG(vO/ӕ.!PZviXƻW5BHZPTڛ] -O[GҹӾFK`ePqiL,PX‚exy -,{v(.q9W-K7[bl9TMi|U*eA UDzcKj05&,ؒ5jYrjZni=ejӨ-e,5a2BF-tEUrq [{p?3j~!N p3[Up+^&ٖo.Y4vܶ*]{$heuS xE9 Vc![uhFPH#^|;t&LvդyZ4z3Q(iY6-|nVӮkmJ$ݵdbፚ̵v nnةb5.HmJ[?YIb>Qv]=tiofCy0$aɨ+mG1 -b!2RgK>ⳤXdsM/P=PzŞnY8'o5%1<*issި[NV03[ *7PvYPѬaGcV TtuZGS|үH]K{ejdxw ׿#Sn,=Otq~*Vi[ -Vg? -:YL*O5y-Sm5[~B||./a-aմ5HR8*G f⯖uU]g$Ԭn"%~lA-nJ*&Qu,[<ߥ~9]2Qimh0 rMX5k?GV2Di *+=HY5+Gi0[x^9$ϿvYG|qAy[\̲]"2gj_Ļ0 Au=k&p(s]o&rKptI'FvFH>;ֿqkzeȈlןmuFPn7p*laWlb -]g); >ۮlj2Pvac4+4rmSXu6y"%泗kQ1i #}(% ((H%rs惓IQ CN&IEyq+^66Ե٣K+#TgV*/TQ, ]k?VM1^Kc<}cG#׮݆8XҁM^t !1Ʊ;ǜqSC]s19i vyhSޠ_JA@&+ɠ:Gh-Ѳ - i; ,b#L ~\Ɓr6ns‘@ҍ:etƙ$#.첃ܓVegKމS"ۈPxR5]kص\p3N9O4ƽKߺĿaF6<} 7*;=QI+-1YCjXaѼǐNXTld(#;6! -&۟s@̃ԣhdX Tr(#lYiP -*3 -$3 -set -$48 -a789ec1fde13d44f0f03cdce05eaa353:brainbaking.com -$705 -{"author":{"name":"Ton Zijlstra","picture":"/pictures/zylstra.org"},"name":"Nabeschouwing: de eerste Nederlandstalige Obsidian meet-up","content":"De allereerste Nederlandstalige meet-up van Obsidian.md gebruikers was interessant en leuk! We waren met z’n vieren, Sebastiaan, Wouter, Frank en ik, en spraken bijna 2 uur met elkaar. Leuk om te vergelijken waarom en hoe we notities maken in Obsid...","published":"2021-04-25T11:24:48+02:00","url":"https://www.zylstra.org/blog/2021/04/nabeschouwing-de-eerste-nederlandstalige-obsidian-meet-up/","type":"mention","source":"https://www.zylstra.org/blog/2021/04/nabeschouwing-de-eerste-nederlandstalige-obsidian-meet-up/","target":"https://brainbaking.com"} -*3 -$3 -set -$19 -zylstra.org:picture -$16040 -JFIF,,ExifII*} R(1 2(i8$CanonCanon EOS-1Ds Mark II-'-'Adobe Photoshop CS6 (Macintosh)2014:11:25 16:14:10Sonja de Ridder"'0230 - -  -124d -2014:10:24 14:55:322014:10:24 14:55:32`e@B@Bq2jLp23399282250.0 mmrz(HH Adobe_CMAdobed    -         " -? -  -  3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?LCk4A=nHnMH{x,X?xhIILK7Kl -bD"OtTo8IӲ-aҠkIk>˂fXX4w!4;AJOaIK W}Nu&{ BLܔߡ{+Hg6[c 7yW1Ë^%g:^%?Yss8h[F\pO`"6=JSGxq"yN2T)v\O`˄?#ʺe;D03G]ާߢeM>~fWg0\ (0sL%}K'1nꅄ4n@LE"2r׋ϸ@>{7" VAuڽ64J_A˺>].-V6j:eX ]9HsL~}hVSsO R@͎j;$m#<"=)9'-H> -$KJ -,C-is]@V٘t%?K70'~@!Z}msY=EL]mUU46u:"f8AMH)}Fe6;q͟[VCnnNq*9~LůCO kb߉qC+h䆍/C\(H W?d$:LQ;: - J~7@ndƪ7%bd&O߁kHdhwSkgG1ncv~z~6eghFWj`y*95u&*u֔w[1ۏ*$y(ӽۍ-3$aO l-8'cKyAn* XLIIL.d!'L'ok{ϒJYg縺܇s`iu<j^ P#BP)OLDB5)6ꂛ;vYnʀs.UҵWٹCv\hmwOW*똵Wuю%>1nS BukmuYU ;3۹]e]_\ۈAD^wFmsS*y6kcn9D[ZAE]p DZS׌ 2V<Ӓמ+,}]zٴ怜au_cn%$co?K.e~ʨȱtcn,]wȹٳ6Z{(ySF%4*"΋ՙ,ۋoCiϊx~Yqݒ,=T& 0 *n :!8QZvpa6C-Z5S2}GAh>)umK״e//n HADB<O⚕hTE~js&`hO s*Tuz/54}OhhG'%miGݝ fM-fMcdsoRȣeܜ][O=mJtUվ׏V@ЇXk LFk;,g>WHA[}mʹs6Ё*P5S'u)G;VX+h,0Ii.CyT-9bcm'SidjIB{h@k5JهV# wry\OHws2 -.3t7]>c3/v\" \òڬeOnǵWkpk 6+=@Aoqt<V2}l۰] -^~i>2$I oD.. :$q {q@(1/ 1N; -9:|xV}k1)s2ͱR_H?D,d|ܺ鱣XY:MoSm?<VV]aq&I'IR/G/VEY_]]{$t]R~XBǴx6۲և{u#Ғ;#!ޮedLlVF3ip%X_.z=p_U]im}7: \5mcEQms}2Z\oI ;Oqi ųKSIvNm/#;x]^W xzpq-h`"#:U `F+npMVVm%i=\^ӨR9F^`R%ۉ6ӺM`}*6Z?nQBVݤ;+*OΜ!\4BJ>0mT>M{`#w.cuF۝i^#y?p6 A>edDiwQdNwacIIL C<tpa%#'A+M_:%8=:.ln啐eNv=! cl82;7 h?kGcs!bďwի4d^E qZZuΔ D!yLO>l?u.'zAŌnN5_Yp8=)k;?:YY^|;j٬5Sx -n#*ɉ.mZ(b}gk'P;!OJL@ !2M= -J@lt4ZNWQĬ -"ZXLh\%o>β {}/{`wR0Nk\ƴ9eʕc9ֵCVAi5R%/qvtKkG\w1}d\V?Ŏvݻ(}Rܡ:E7DYPev0H`kw=c UYs^5y@շ< =Hk:}AmM -1`rCGхƒΛqwߗa:uLʾTY<FֺܹόirRR{-fi4Ρ'UVhd0KL*>f)k9L^HnI d蒙:p徏'#qc}v.~o|mcwSt>?XQuՎ@;g^BsdtS 6awFޒ`"9j(٥1:H?k IM΋ktK\A1z^["DϫX4fc).kZaov3 IJGܖԶXe6S 7*"CudY:KoY(_Y[NKEh (+wTu:3w` F4߿?576 $0ꬂOM8~qokniGցz3 -qfz%݅敏U iOsWG,OeǸz{Z+i;\܁|S"|˷2iЏ08Qq )*9vd \T/[20141024tSonja de RidderShttp://ns.adobe.com/xap/1.0/ C    -!'"#%%%),($+!$%$C   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$9!1AQ"a2q#bB$%4Rr"!1AQ"aq ?IQ}OƑ5 qVLV\^Fj¤\ڈk;J9ā ۀ_WFʹCvS@dB4o=HAeSiC8~y=r+1҆WX^(ȥhU iؠ%jO͚ G0|" Qm |* լ+)F -Nqh9 pFK`Tyк*hWid.{HW1&lCgaQ*7K'9wvoU$)'kp5wL-#GR3l&BɂjԁerzߧdT&'A9hI4iuC!-ob> -=޳T|wQv3O(s%:~T`|\y==^#wjPt`\ Qv4m}kvnVӖlaވO 9^nYd8h5ՍݸivX)w\;RŁDG+4hĠARQ>E%!1 -*xMW.uIzT&r=:_*^J[e$x𐑐{Pa=6Cg\|+dÒ#& jAꡩp(("AҢK+Bs[TD@u9vsMYs 2V hb5 -²v6{(V 6m@Դ yqYM4C~Vmmc7dc=is%ָoVs.N㹮]|%6%fe7N}q+lt yŪ,˵A$.=Ld]8.;I:52L"wXM+OUm' jԷ9xﴴneKK8e^l -X6zQΫ{pʶ<#xZL݋D^fm5K -:{{q]0#ᶆdʅ&&f-4bPb'\%1;Tan0x"tUM=Z=2` @{4QCBɪ\ R*_Kj&rP#;ۺ;[Mnv>չvxw -Mn+'&5\{3Rhvs1 ٻ?nͳ5{|@.V,KȒgEdkkج\dsY&]JyѰREGwkś@[#,܀Ip+N[jiju߀DcnjJ0B ]k;Tͤߦ0GpFF~ޥ:=N hJ͹S ~c.[oM- ĈU>X-nR qK?oL1'$n#޻I,ʾ{;}[pj bBEf CF\o^ME2H9l#(rvH6|`[:=]ʂ "Pj3̃Z81bSH ڈu&zHfn]V[ڲK0%Xֱy{oh;\} 6/6 ]mM`k_@Of8Eɚj́Iȣ;_>Z8Tq#ⷄntr.;y³VU/f.BI -6ތaڋRJgnyO&WwkG(vO/ӕ.!PZviXƻW5BHZPTڛ] -O[GҹӾFK`ePqiL,PX‚exy -,{v(.q9W-K7[bl9TMi|U*eA UDzcKj05&,ؒ5jYrjZni=ejӨ-e,5a2BF-tEUrq [{p?3j~!N p3[Up+^&ٖo.Y4vܶ*]{$heuS xE9 Vc![uhFPH#^|;t&LvդyZ4z3Q(iY6-|nVӮkmJ$ݵdbፚ̵v nnةb5.HmJ[?YIb>Qv]=tiofCy0$aɨ+mG1 -b!2RgK>ⳤXdsM/P=PzŞnY8'o5%1<*issި[NV03[ *7PvYPѬaGcV TtuZGS|үH]K{ejdxw ׿#Sn,=Otq~*Vi[ -Vg? -:YL*O5y-Sm5[~B||./a-aմ5HR8*G f⯖uU]g$Ԭn"%~lA-nJ*&Qu,[<ߥ~9]2Qimh0 rMX5k?GV2Di *+=HY5+Gi0[x^9$ϿvYG|qAy[\̲]"2gj_Ļ0 Au=k&p(s]o&rKptI'FvFH>;ֿqkzeȈlןmuFPn7p*laWlb -]g); >ۮlj2Pvac4+4rmSXu6y"%泗kQ1i #}(% ((H%rs惓IQ CN&IEyq+^66Ե٣K+#TgV*/TQ, ]k?VM1^Kc<}cG#׮݆8XҁM^t !1Ʊ;ǜqSC]s19i vyhSޠ_JA@&+ɠ:Gh-Ѳ - i; ,b#L ~\Ɓr6ns‘@ҍ:etƙ$#.첃ܓVegKމS"ۈPxR5]kص\p3N9O4ƽKߺĿaF6<} 7*;=QI+-1YCjXaѼǐNXTld(#;6! -&۟s@̃ԣhdX Tr(#lYiP -*3 -$3 -set -$48 -31d75d84838a97f1c4de550ba91b6fff:brainbaking.com -$589 -{"author":{"name":"Ton Zijlstra","picture":"/pictures/zylstra.org"},"name":"","content":"Don’t worry, I mostly blog in English, sometimes in Dutch and seldomly in German. My notes reflect much the same thing, they’re a mix of those languages, plus source material in a few languages more. How to deal with multilingual blogging has bee...","published":"2021-04-25T20:31:11+02:00","url":"https://www.zylstra.org/blog/2021/04/16112/","type":"mention","source":"https://www.zylstra.org/blog/2021/04/16112/","target":"https://brainbaking.com/post/2021/04/the-first-dutch-obsidian-meetup/"} -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-26T08:47:41.873Z -*3 -$3 -set -$21 -brainbaking.com:since -$24 -2021-04-26T08:52:19.857Z diff --git a/rest/utils.go b/rest/utils.go index b667b6e..75d14a0 100644 --- a/rest/utils.go +++ b/rest/utils.go @@ -44,6 +44,10 @@ var ( tiffM = imageType{0x4D, 0x4D, 0x00, 0x2A} webp = imageType{0x52, 0x49, 0x46, 0x46} // RIFF 32 bits supportedImageTypes = []imageType{jpg, png, gif, bmp, webp, tiffI, tiffM} + + // SiloDomains are domains where mentions of multiple individuals may come from. + // These are privacy issues and will be anonymized as such. + SiloDomains = []string{"brid.gy", "twitter.com", "facebook.com"} ) // IsRealImage checks the first few bytes of the provided data to see if it's a real image.