remove since and simply check <link/> tags in rss feed. fixes time difference bugs
parent
1f38a42c77
commit
255fea17e0
|
@ -3,6 +3,7 @@
|
|||
|
||||
*.prof
|
||||
*.test
|
||||
*.db
|
||||
|
||||
config.json
|
||||
|
||||
|
|
10
README.md
10
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 `<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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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!")
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
33
db/repo.go
33
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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
714
mentions.db
714
mentions.db
|
@ -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
|
||||
ÿØÿà |