remove since and simply check <link/> tags in rss feed. fixes time difference bugs

pull/4/head
Wouter Groeneveld 2021-05-02 09:41:13 +02:00
parent 1f38a42c77
commit 255fea17e0
17 changed files with 74 additions and 995 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
*.prof
*.test
*.db
config.json

View File

@ -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 `<pubDate/>` `<item/>` RSS tag by default. I decided against porting the more complicated `<timestamp/>` 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 `<link/>` 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

View File

@ -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
}

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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))

View File

@ -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){

View File

@ -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)

View File

@ -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)))
}
}

View File

@ -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!")
}

View File

@ -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()
}

View File

@ -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.

View File

@ -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) {

View File

@ -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 ðŸ˜<C5B8> see https://brainba...","content":"@rsolva that’s a lie indeed ðŸ˜<C5B8> 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
ÿØÿàJFIFÿÛC 


  
ÿÛC ÿÀddÿÄ ÿÄA!1AQa"q<>2BÁR¡#$c 3STr±5CDb¢ÑáÿÄÿÄ1!1QA"a±Ñ<C2B1>q#BÁáñðÿÚ ?7«ÊÎjÂs7“Ùõd¬;ÊÖzcñ—¼zÊÕ<18>ğюüzʔÃþa6øc°ï²¥4õö}'.Ǽw•­©=C­å€O¬w”­©bþú]ˆ•yÌàVÎ^±ÞûNG~»ÇssêI„¹/´„ûN_/HìL…sTAUýœ`lmï÷.æ_òÔ˜uJ\­@ÀY'`{e{ÇøE¤MfßÛ§íÀ<Ñ^ï§46õ“<C3B5>Í/Ç43Û·ö¨ûqù£ñÎ:•r6óçLf<hÍ—^uÅ¥O`_š1NðUç<1F>ý£Vý'—ì£RUW
žYSQ=È#Þso+n*¡Y1DÌ`濞%jÕÑP¦H§3•pÄM=
fÝ:<3A>Tm·ÞÉ•P5:Å–ˆ;,si¯þ'¥<'8(Õ(êUýÓxî<>¤Xþ8p±aˆûbå |oéêÓ ™šjF}D$Kg‰ø×éïmÆŽžDcò¨ˆÔÂuü°ƒ§j*UvžÅZ<C385>9‰°ä£<C3A4>§Ù_/"öšl?4åŸ4%7 Êj€J
b+Κ¬•Côã:Pß|­Kh~%0ox1È­ÖP»ý9Í”~ñésŠ«BM5õQP©I%ÅLpom”÷—ÈÊñ§i<C2A7> ä€76ÿ»¹€ñž)õƒ»ÇíËå+z{QÚ[ïÌ 6ÚJÖµ*Á)ä“ål»ÆÀGÄÇ1%MÒzn¢¨ºV#Å
-(…Ï)襫òy'¿Så„ÓXRS
.`¾ô<C2BE>ßsœ<73>O§€£Â-¾à|ú_ €¹‰ÌØMrsÃÕHG<48>QÕaL¶¢-+rn6=†gdå* »ƒÓÝ€-´#4Ÿ)´Í1¾6i«$<>t}÷·žù˜ùLÚ§‡M™ªr_KURãïRÐ\q<.‘ï{Hʪ D¶Æ¤ÚF<>Zä΢ršÌ‡Òu‡Ò°_6JUìIØ—–F¿”{[‰Îu.œhûi´Uç¦bÒ¤‰GíÅî ´J9KpV§+Û¾!Pê!¸˜û@Ì£þ¸BIœI“%²¤Ðä¤fN•¥ãŸ?·ñLÞRµéȃbãñˆùDoÅ;_¹yKTD ª†ä¤†¦ÒJÜA ÷A¾çË"}—B(ÇY<C387>Z¦žL(I.,$9Ã+ Þÿ£4)˜K ,<2C>åNŽæ¢ŒøŽÕ¸¡ºÈì)Q‡Xôý½aã¤à³,¥þÝH y[9çR_Y×ÒqÀZ)ô˜°f\hž€'§ÆØ¢*ìd¶àÉ18QH) PÛkoò1¹¨¨ÌTã?O<>ÒT‡RH"÷dÙpc¬¾JeOiõ!ôÅB}U¨¥ _˜<5F>×4/¤á™J±Sé^cÃÍ1ÒñR€ñ:ˆV/¬5@ã<E28093>7Q¹Â“Ö.ÚH%3ûEÛób3,)ŽÊî<C38A>ѾG<C2BE>í2ˆ3UÀo»]r®# ^7ÿ…дh<C2B4>$&]M!Ix%'¯ÛÓ&î!¸¹€f¹¬Ò—"%>D<44>€»(XnGÇ øž˜R <0B>!5H[^?¹,ű\ú}R³¨4Åq€·Ð„“å¹±ùtß¹ÇW®ˆªï¾<C3AF>¦óE¥Ö[F§¢5e<04>ôön}€qns?ÁQÚüOÀͳ^•1`Ãâ>ò}\â‰NO
DšTG˜Í­Cü¥D“òÂSˆö‡é|µseañŠv<C5A0>¨ÆÔ´ôÍnˆN zŒ>Aáï±({FAè†ÔE…b£YMfžò[õ67<#Ì`ÍD<C38D>D]k)xÇߤF}HúíƒÓ%•uœ†XQ]ÈïntRŒm1"Aá?"\ ,}Ìëi.ãsî_ôáÈa&A³¬-Bû¬äÉÖFÓ0ãKYWJ€Â>\ö˜œçªÓó g­´âÃA—ãiz³í C˜ëhn×÷ ½€(à9tÚ™Ò~ÛD­<44> Áš¼”å¾³¥U>—H„­COàe‰‰úêÛˆ<05>î“}‰µíkžØ•
ï@€†mu :y"åEã¹PÓm/SihŽËujã.·Â´ØÚÆÛ\bõ³ª)²´¥Pd-U.cÏWøVåu—Õ<E28094>´¬XÕšt'&Æ}EM­±Åq¿<71>9,|ÚÍUy6—ˆetÌe¤üPlmîŽ<C3AE>+É$P!Ë<>RRdRçRÛL8­6¤â€*Z‰Ù@ƒäI¶vù]F<>ÇZT×P&.'O®•®\ziov¿ôʬîDÒâê亣2f5í8•¨"o¨xB£_Òu
;ç;U½ÎÖõôšu˜â0(76 ¿Â3*>$9…͵ˢéz4Ý0õ¸_+”=tú$z·HA
Jn½®v1Ã(žF:D[©­òè¤[ŸõMâKJRéPtÿ5dD£=öèïÖZ‡à™„](x€ªà¤”qlT <00>òíR™uÓ/1¸×`}c»ÄŽŸ09u:bU°EööæS•ò-¢ø·¹¼ j€Ÿîÿ‡&Ц>@Ç¿ýG&e<>¦W?]*ƒî¶rüÕ;ÌK,óõâ¦?á·—ç©Þ;,ks=ÊŽ¹Ñ²éHa&S*D¸„*ʶo`}¢ãç‰W/Y?„ÒéYK‡”µ€è3»Mô¨<C3B4>Î<EFBFBD>ûjKÊ.*âîÄ-r6Á``ùØv<C398>ÍT7ä63Š"½WиÖ·žA÷ÖA …„W¨ÍÄJS<6D>l¶ãn ¥iP±IóÇ
Çm4Þ!Yyi"JÖ:vœŠM"¹HˆˆíIXÃ@l€íŠ\JF×P
·[õÃêd½Jf™õõõ<C3B5>®2Ó`iüø1³§è<15>ª5´Ü6ÔÅ.V]i¥,ûï)v¬‹m€ß©ÄC-F˜Üëï.'*ã"©·`;w?É•¯Né½%X¬ÂŸ$w§Jr§é„-h)G±·*A¿kœ€©T…]tÒ'Z<>%g¨¶×_Óî'Näç+5‡¥R*z.ˆóÕʪuEm#¡?²K…ÖXsƒ²”[;\6Nâù¥_$ÑÆ 1ätÀ½ç
ÔÅJ×þ߬zx“Ô/Ëåœö€””[alçUoUo51÷0(©[ŠG_Oñ9¦<39>¶3š
¢n¤äÎñ<C38E>¦RÿVä…ol£M†ól´Sc/kF—…Ó5C¦L¡§i{Z-ÖÜÆ<>ˆ¾Ý²MM”kÍ <09>÷tƤÓUêæÔRe´ô
¼Õˆþº‹\*yN6¤Žà¥I#Ù<>{ í;ΙTÖÇ[ˆôÐ(nk©Jì¤÷|Á¼>]uÞ)<29>]DGƒ*>ênrÔð¿#hâLöÖ<C3B6>ˆ­Ü;tÂQ¶& Qøȇô¬6RS.:
Wª°”¦÷á)¿ q犕U]²',ÔЉH »­ëàjfbÕd:êi.ÅJrl«ôßËÅ÷Ê­I1]˜l?ÔWÕ¢ãÑ[1 ¢$6Jн&KH¹ïÂ<C3AF>ÏÃ{bÕi17&p•Oa_ЃËZ‡¨¾ cµºœ‰ZÊóO¹)ã†<Cü¿Äáë :Í(`z ó9"c×u$:s%µ‘Æ ±jîTÚp™MÀÞ{§k­L€™aРIݰG¬SX<53>ä/yØÍdÈw„]IÇ¥j½Œ {À·œ•ˆµÞwê-³!\Pè¥6ÚP³ò ŒF»<46>!"zWA¤Ôð<C394>7o¬ƒ§J{HÖKn¡Iˆùõ·Èã h”(tŠ<¢'zN¶¤­©¾&ûI&†8¤iÊmi-Ìt:ܰ„¶eÕ!`×mŠRn&æ0üל2èâ%q™«Ö%]2 ù\á/Ua “H­Êˆôäm"=,»_«ÎS®Gml²·ˆ+Z•õ—òHøœ<1A>@”ã?rg"Œ»îmô%>Y3aÉyivî„«p<05>¿Ôa¸D>M0Æâ󓦚Þ#ž8e¾Y¾úPâÑÀ¢QÄ-‡õ¤QI€ï7z]ʵæ?ª¾ìX‡ù?‰ÀÞóTí4Ó¾õ-³æNYÞ5L•j]9.·ªãŘ§Ø†ûi²À)
·`{“‡d(m}t´à3iµJ¡}#_xŠðáÊ-Lç/õ2§U¡ð¦[‡œm…<6D>- *Ä\npj˜U˜YEä ¦43­>$¹NÞ<4E>rµ¥õ<C2A5>'Q8ãÆ§J:â»q'e!>e@}¹œô[®ÂÓK¦`Öê5<i žÃïÚÒêš…U™×õj¸§n6.TÀ¡<E2809A>äm=v<>F˜Aè-¹Tª”ãII[c<>»Ž»}_˜Úÿ 5¤jLh˜Õ-.µH¦¸µ47S èEûyü±U7Þ ÚEEó&<26>Xe,Ky¤#«n!B˜96ÔIêåjŸ"9Þ yÄðŽ\ïúrª•?3Uh¡z¦ÀÙ|jÊà@Ld´¤7`¯n@Ò\O%zÿ3<>ÕŸÔÞ:´†·Ÿ%hfL”¡QøKg¹#%‰O…ndê6Š
ÅX;FŸŒÊÌŠ<C38C>)ß’òÁôVø~mfȬ…çKÓ˜q6€Ýa_ÁB?ÈŽ$ÍVžiª<E280BA>Ñ·s×hgøþ |£Ñ2*zOMRejZýVaæ<61>Kp(ª§A*ZRvPHÜß7òJd¨Nžÿýq8Ú<38>Mtcy‰ÍeY¬ëMSSÕšŠ¦ìŠ<C3AC>Z[³%8`\qEF×ÞÂöødÐp[@<19>2)…®˜ër©ó—CJ
Kˆx¥À}ŠM·É<C2B7>Y†¥G¦y!±ï~Lx™•þªs`RVô*ºÀâiÔ<69>R¶ÊIµ¸úïpn0sºB<C2BA>ëc<C3AB>·iÛô_݃³v÷¿¤8ôåB%VŸlU¥m>Ò\AJ<41>qb:æºÙ³†³¡’ª¥Ã”¾,¡i>ι5Ä<1C>æô>Té½O_‡4tú¯:8Š.,€AR<41>»‹¥YÅ%ÜÌüºëB;û}M¡ôÖ—_ÑáRa4—Oìý&R•‰êp¼ŒqT£jgæÖ«SJ®Mý÷‰ö¹¦I.Æ`M‰®AÇÈÀjPe^B7èêJ%#<23>Dìz\bKUj ÈX´«ÅÚÂ9(€…^î21zâ̳³éféø@ÖµîSávý†F™->Ò4VMûœ“o-vƒ,Š<C5A0>¢·$­Û: ,-<üÞVÓq¤(ðHõ|Ǧ/úrq&3U5IR'ê©%~Þàd¤A"k2CEøª ßmø=¾Ñç4.|NÔ9k">ŽÕË/é×^ ‡
®ºqQúÃÍ«îSÚäŒÊÏéËZõièßYÔt^¼øÇ¯­3·q÷üƒ]‰1¦Þ<C2A6>) …¤‚.=Å·¾s:¡±ô<C2B1>ýà !É].¸ujYì)3*BÍ…Gk˜ûß gQб¿¦r[v¸ÀõüLâúÖ_ž¯…²¿Y9©t"µ=A™2g)„0xQººôßke¿I­W%«°?¤Á­HUp×ÐOG,¨à«—ço{)| ölœ0tª,¼j\þL !'D鉡Fýϸɱ C`×¶*ý3éxÛÞ7øÈ„Q¡ƒ_<C692>:lZw+YnIiµº„ð§`8NÙ<4E>Ôè®6B¢miµÓß<C393>0Ô™Ãc`”ÌÖm§ÚE7¡G$yãkÞXÚ ED8H6ß:<3A>8¥åºx‰±Iá;d¢FzB•ï$ûŒr§d%-µ•²µ¶@ûª6>ÂÇÚäVÒ%%°•©&àt;Û§Ï(m)Xƒh¶xvæ®¶£<º3UuÉ…Lr3X“wØ2ºlOºvòÌn§B™¤Õ-®¿IÑtÚ<E2809D>/dG»ø™É„¿R3np¥<IÉlè±ÉŽŽFàAœYÈ—á§ŠÇ×pPþ iáåÔW7[©¸¾Ûæ:Ú<>˜Cîÿ3W§nÐÕÂÔÈ6ÿ32Ÿ¬Ù;K4ûo<>Ëmå<6D>§ÿÙ
*3
$3
set
$50
0f35e81e7376278498b425df5eaf956e:jefklakscodex.com
$596
{"author":{"name":"Wouter Groeneveld","picture":"/pictures/brainbaking.com"},"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\nEnclosed 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
$24
webmention.rocks:picture
$30648
‰PNG

IHDRÓ?1tEXtSoftwareAdobe ImageReadyqÉe<wZIDATxÚì½|Õµ?>wÚviÕ%Ûj¶åî \)¦˜ö^˜<>¼$”@Ê/!@÷<C3B7>Pòÿ%!&y/¡¤PlfŒÁM.Ø»l«[¶ºÕËíÎìÌïÎΖ)wfWòîJŸÏbV³³³S¾çœï9÷Üs<C39C>§â#ì’$DþzÍ/‡Çÿ–þà–•)V£ðöDsªf¾¹é¹æÎ)ãòÖíO9Ú{BšMDA¾ñt<C3B1>¾ÿÖßÐVÓ8z(ø%\&Fºk[üèç… ÃDôü¯±/§ÞÙåhï\ˆÿ5£Ø|®i¾±f§Œ/ô_R€ÄIëÑ:)ô¡d¦Ñ¢ðŸèIMËX¾
¯Ã}ô¯[¥[h§)àt³ð}Îìâq÷\ÈKÐLœ¡<>î3‚¾Å„‡6ÆÍ¿×éWURhníòŠ[ÒK&Œ»çrÉ$JŽÕ ïG? |óC
0òþcß*E?EâÓòé61°ÁRKò.y€K á9~?)6B¹À%ÁåX“~O±eBŽ¡½Ïǰœøçx¤@—<@Âe˜臟:ÚÆ¨´¯;¿·B±qf<71>±µÓ¸œâ ãñÑ\ò<>s{*Ô«)Œý<14>ÚüOÎ7YŒXKÛ<4B>HêÒJr/)À%Ñ0Ÿb Œ4ƒˆ€2ø¨ö“ƒ=õÍ!óV“s M]ëãÅ?ÓŠóÆã£‰@<40>¼ðé%øÆ&üU˜ á@s¬<73>¿×á>øÜ&Èàú³¶KO7d%ƒÖ.o€ØH<>¾Œ
åͯ=ÛYÕr Ç#7ÿGë”\ÈR@c\Žýý¯Ëºèf
*ÛÚ>Úm<FÀ`Î7—´ôüë;/œy¿ü”G”U„ `i×$ŸÝµcÈÜ@<40>túÝÝŠ<C39D>ö$*Ëšº| `qÖ¬”qút"(€Áf*ºjÇ9´í±·¶ýò-Ï û ‡ËºëZBг™}eìœÙoßTo,š$”ptõ³ãÅWÚøLEE<45>ÿZñÍ™ËÿuÏ <0B>Õ—èÐpaX™¥H<@ú©ðfˆgÌ(Àù½í'êÄ‚ŸÖ-f¢([8ÕÖ žÐžã4ˆJrS`guë·ÿáØe—<65>-ÿA%@“íTàî§Hî?ÎOCðcÃwzî]?èeÚ[Zd†ÿö:8— mü2{1<12>þ¹û÷ïø“¿]¢CQEÀ¨PJr ò(Ô—ø±pæ§ÿµËqA™¾«Ðoþ»˜PT3~S@Ñ*@Þü¢Œ©²aŽú<C5BD>»î×͇ë/A\GÛz<C39B>iÍ”à00âÈÉFOmëXˆ}<7D>ÿãåV€šDÄ·uzÃ~`ÖìÔ/³Ëå·ÀÈøÝ{_ØýûÍ—€®É¡÷T ·#ª€0ž&<26>xcÇB¼ç·o„žtèEQBéÜæâú˜ÐöìËÇï3ŠV¦ß² )'%tOÚϳìÍÛ×´ö\;"!d´
·Ï$üA°¿>44Â$ w<>îi7î=Ù~²N¹ûß—˜iB(íèã¤û<C2A4>»I0#QQBÈÏJ1„¶wÕ´n¸}}å
¢ð8fÑÇ“ˆÈ(ˆhtƒ…qßçßSØ~Ì?ñeª±gØÖå•ésQÞWBæÜ½<8™˜k(Ê·HîÚÐg<C390>oüè'½Kó?È›àúÐÚ[dù!»5ÌF±"èô»»=êÒ¼‰ùFŠ èC§\Æo
hx
`°™¦ß¼P¼5å§ûgL6\>% “øÉ³»NýíÆ§×]B¿`þË*d·»A«o"<22>_„$c´œ€û¾ö ò£©y´øæBçe9©{°f¥}%Ê⯠¹ï½Ç箜“"w CïÝÿBÙúK±$<00>Ýc>%hé) Ï ¤D Tx°£mt€=ϼ<C38F>Ü>)ßh ]·v1@N<>R¿"óŠM˜W$¾ïqx<71>Ö{JrÉk—¦Ò!=؉ eï\ÿU3î®m¬8®D?ü/;%€$\ž Ñ¥@µŸô<>ûÊÝÿ5s²òá wö2¡ˆÐ bÜV<01>DB¡°('«º¹ÜvšÂÃ9"düÖºg<C2BA>oøŠŽ úBݼ=‰VíË«;¢$ž ã¾Ï¿'=¦û&M4
SáÙÃñýýÞ`CA¬Y©_1¸y<C2B8>ÍŸÙÞ£ƒð^¤ÙÀׯJM±ÒŠØiÏúÍïÝ÷üW0I*Ÿ=(΂îMª=lõ‰T<E280B0>üàéá²PŠÄGË|þ]Æé´ò9hþ¹n_s«ø¢5'í«¥~'°0ô^H„ꄜE`7\”ŸmVìÜr¤~ãºõgwU|uÐá¬ç þ`4Ÿl#1‰ ”@eDOb+¢ÛOÔÖo;¤:+áäýì_ H"x'ßÙÏŠazèõ•ó˜jTød-$B¼¨«çš ó,
 #ã-ÿíㇿ*IÒ`ú?løC
¹N±4Z(ƒãÑe?€÷º\‰ÞûÌ›
܇NÌoþy`Çù|ÓÙùҎëA€*€Áf*½i<C2BD>ô{<7B>ô²ËLKg'+)( »NýcíÓ_WàÏÿHÑÆS($!G¤„IŸj<FÂjB<6A>¿ö‰¿è<C2BF>W+ä$aÚ»pÂD6Åu²Î!ÞíbÖm\c#nR* …¡ô9¼ÇêÃ#%¹ÔÊ)4<>K  úäá¿í]¿ùKì
 å>¿÷¤”ö„Nr¸ö„¦DJÊ Ì&9RÑSß|üµ-jG$Ò›á Zqž"õŽ>Ÿ*FƬÙi_EÈ_œ>%Wü6g„Â÷qR±æJ¿¨\Á‰<C381>eo¯[ßräË9^vnÏIí A*3]®"†H ®ºIX"h¯<ñböÂÌ šÿÉ4×(X·.éÓûJÆ<00>HàîÊ»ylPz{R­àkW§¦ØhT˜Ø»ù{/ì}öKè
Îï9¥ ="° $[Ã ˆH
S éDá¶ãq7Ðö÷Ô·(pNuÉ@ úÉ3µtôxŽJÊÓÓx^#W€ÂU3iùÌÖ0
xØuKm…yf©«½\Á]_*Wàç?
臀•j ßm jªÈ ÷Ô5ÇŸü|¢Æ½x*aö_DsíÂÜç7äò)ÈRJÑ„¯®ÀP¸xÕ,ÅÆª:‡cHz<48>W^fšV`Ed°íKå
üpJð$ë³@b!<21>ÿ=nÇÅÓBYxžqºãÊö=óú¢"NŸðÛx
Éuù€bO€}uÊåw-Wš@·÷„SÍ€N7,ž“Œ0tW°ûí»~ÿ%pç÷œD¢Äàæp¤èeÊ-ýÏÅíñ‹ƒòs¶E Íû÷ý“(_g ð¡ßÉ©÷ï9ÐU€Œ©„PX.z†N722_ ÄÔyãŠTŠÀµ]Áó{ÿ°iüºÁöžÐ°Ú¸f¤†ù=°Ê?¦Âm£½!â@òsâõO&_׊ì_8¯b“ȸ׫6^ã=Š]|wèÙw­PxFx—**bg¯]a·')fTàØÉ·Êþy÷xu¢ùG³jI€ Dó/}i1¡î@PÚ<50>ׯ‰ü( 
m᳘˜(ü$&RÜË †_ðNýý¬Ò]Äšÿc9%ô‚ŽCaE‰Ðž.dpÃæëØòsL*ôƒ<C3B4>+xÿÁqé
j>9´1åï€4"à<>X$ãbŠ&¶rè…w{ZÐ=ŠÆg— lk`l§£—C>V9P<%<25>*XL/#2KB/ø'ܘV¡uœ$ÝŸæv÷ùNÒHPX5×<kšMŽ~Yzºwî^ß°{Ü w×¶ôjãÕn§$àá<C3A0>¼1(.aGfcxlÅët÷Ät<¸ýDmåæ]HèR4Ñ?íK(}#‰$k Ì|ïsúTN[xY.r* AiÔÔÕä„Ë<E2809E>q(¸*ZWWƒ­<>P(Ñ~ä”I„ĈZ27™Ô?ªF"10ØÖ³õÑ¿n}ôÕqá
Ôví|f<>®0âA7!¼ºëc õ:Ü;ŸxY úBÓ×íÃÈ#Û:z¢]þ£,—1 v<E2809A>È™NOY ÿ”®<E2809D>'Hª` T•±«B(<5W<35>OsºÙã5$úE)É!¯YžH€”†²SoÜúTÕG‡Æº|r }±¶'ÝN†¦Sûs r<0F>æýf“l¥Æ}'cu†{׿Á8‡”З¢I&Šò§e¡ùO¥|MLh/¡ Š ÷`Ä!nI#ó.§ ôÓ
¸£s¤°ó˜U1FÞŽª³Ž2/ؘfÿ~UŠ]>Zì÷%¼Ät í|zã>78V—ªýä °˜
ý¡^'9©¸Ö¸ôd‰!3 ñÓp)²Úc”ª|oWóþ“è‡<15>˜ #'Ñü<C391><C3BC>ë»o—˧Cœ†Aôq{Þ¾;¦ påL…ù玪v—Ý>H:oZf+ ö˜<C3B6>¡?«a­Gëß¼õéÃ/oáïÖCHô§nO¦ýBqo£0@ñX„=õÍå/¾«}qKiIdÿÀˆls.Üø¿£×'MÉ"ûËJ¢"ú™SèÒ5Ñ)‡4)NÑpl†ÂÓÖ.@ðB? 0þa<C3BE><61>ùWROQ®˜e\2'ÓR£ ~åÓ7oyªõèÊ“:Ú{”vZ~<7E>"¦Éø+°#î¹4,NNí_÷éÁ¤þÛù“ô…*T319'ÀF¨)B4<>þ÷ÚÈ„ÊQƒGÆ„;d;téµDfI´lGçh´±«˜?Š©ã¿œªr0\ý€²®ÈA)Î!o\–êï¥<18>É`{ï>¿óé c¤<63>ø©wvé <4A><C2BA>ÇÁzÚŽ
Â=x«D€?us1§·ó‰—ô }Qf•˜ ä£<>þwñ¡O]CœúѱøÛijnyî…¿””LÑR§±­+gÙrR<72>€—åö…†àöòx>%è€jïTX»"E:shœuõò7o{êäÛ»GýоÊÂ_ú¡ä¤ÊO ØB>}r˜ã ¥A<C2A5>K¨hTü°çlˈöýþ<C3BD> Š&‡*?<3F>žFåe<C3A5><65>îŸS<C5B8>ð_æü<C3A6>4ÝÙ?ȪM>2r;#b;g*«{üɉùy?úÁuuµFFÏcZDвÛMmîVI5{ÒCL¡‰ $*ˆ­Yb-È7G<eïÿïÍï|ë÷£Èˆdá/
ýv;E)ú_… $²á%S…DP¨#ƒ¸Ë™÷vŽàÜê·¬ÿü ôE|—˜gmÌ?|õ1ÒtgŸƒE%<25><54>‰ó…ÜÎ0ÙÎgÛwÞ{ß÷fL/ýõÓO„s'Õ5ãÏøsAËuìöGe³Øã¢˜"&*Ö$Ô„Á<E2809E>`ñœähmݵ-üà9ȈF¥©ò©wvë Jn&¥¸@iáC(½HŒŠå@F\~~ÉPHœö=û†vLÀ÷Ä<cf€ŽòÔT¡ÈwaH¬þ˜*cýü<15>÷ <0C>d,Æ“²¢?½<><C2BD>Á÷6pÝu7\{õêW_yIñi]m­V
uL+€-7U]aX@2m!öä9Í@SHµ)Î!®_ê¢q¾<71>©·5#zòð« ÍXBBýã3¤áù?ÅÅ& b
È(%ÀMa­È
“ÃLᯠ+î©oÞõäˆ_65£ Pø£pr °`= ýÌ…ÅKŸü?B€q$KEô÷̳óæÍû÷ÛnùôStÆòCÄx±Îi7-Ô íÂM¯ªs:=áÛÉõòì)¿Ì5b¨ n»Êž*žÓ@@¡œž#¯~ºáߟ:W Š£Ûª<C39B>~ŠÆ³Sp<04>¿Ü<C2BF>§,̘rhÌ_'?9ñúèÑ¿íÑ?…_¥á<0F>(-6ƒe\¢ùÇüùé1Y wK1Þ¼ô¦÷þ¶ö_ŸMûÎCú]<5D>Ù?ôÓG ÑÿÅÏ<1F>D_gO¨ÐE$&sX …~( Ç:ã’~àkñ1õ2—$çS³i¿i™uJE÷LùÛ{¶ýç«þð9­Þü16ÿÚè÷'@iÅGbwÁôý~ Xa¬“LJoBÀ l;8lôchÃ/LE>ø“°¢ù÷]ð`aþ#|%¤¢É¿c×î•Ͻ™1åØÎâE þû<C3BE>ÏF«-åˆ8˜Æ¼0ØL+fê µ¹ÝÝ&_^ÁWÉø:Y*—4¬0
“?øÀÃX8<58>^<;IN‡B柗ÒÀþmÇê>úás»~½!NƒÇ0Þ8ðçMúè %-†p% ©N¶Êô$@ ^xW?Î £_×ðŸ\6ÅL<4C>ŸÍ?|Ãv0ŠFO»ëÚ¯ïøP4ùtJflÙŽT#ãà8 ‡Å~½rY.Hr÷CÏ_Tà…=6Ä úpA/5ãV4…!Á5Kƒs =§Âè—nªùäÐÆ¯=µÿ6Å<>>õÎ.¯Ë¥<C38B>~(ùYŠ<10>M;<3B>†ŒâP@¨"`þfAŠâj¯Ë½wýšhÛ2Œ~”á—V1$'““sÈÏ1ãTžIüŒkiÔå ­<yÍÎcKû[ñÌaݟ϶ïŒíhÉñcG5€äØ>Jà©ø(¶Gô º_»éi¯cH¥á}æÍLš>Qûòx
N/2ð,ï=èâû8DÁ ^?\5t¶Ñîº#1ÿÕñïG[<5B>3o_9ëö1™ÁähïyûÎ'"¢?ÙN­YlUz¡™Èˆ>ár,gèoñÕòHL­­òˆtñŸ”õ!×,ºâÑ»