mirror of
https://github.com/by-jp/www.byjp.me.git
synced 2025-08-09 22:16:07 +01:00
Import from webmentions.io
This commit is contained in:
parent
2566def16d
commit
25eb81de1f
29 changed files with 414 additions and 32 deletions
|
@ -519,11 +519,22 @@ details {
|
|||
}
|
||||
|
||||
svg {
|
||||
margin: 0 0.25em 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.named-reactions {
|
||||
list-style: none;
|
||||
margin-top: 1em;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ol.comments {
|
||||
list-style: none;
|
||||
|
||||
|
|
43
content/bookmarks/i-don-t-want-to-host-services-but-i-do-.md
Normal file
43
content/bookmarks/i-don-t-want-to-host-services-but-i-do-.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: I don't want to host services (but I do)
|
||||
date: "2024-03-28T16:17:01Z"
|
||||
publishDate: "2023-08-09T16:00:00Z"
|
||||
bookmarkOf: https://ergaster.org/posts/2023/08/09-i-dont-want-to-host-services-but-i-do/
|
||||
references:
|
||||
bookmark:
|
||||
url: https://ergaster.org/posts/2023/08/09-i-dont-want-to-host-services-but-i-do/
|
||||
type: entry
|
||||
name: I don't want to host services (but I do)
|
||||
summary: 'I don’t want to self-host, and even worse: I think most individuals
|
||||
shouldn’t host services for others. Yet, I am self-hosting services and I even
|
||||
teach people how to do it.'
|
||||
author: Thibault Martin
|
||||
tags:
|
||||
- homelab
|
||||
- tech
|
||||
---
|
||||
|
||||
A concise and smart article talking to those of us interested in offering hobby networked services to others.
|
||||
|
||||
I’m still in the mindset that a form of fully decentralised systems could overcome many of the issues mentioned here. My current bet would be on client-side web applications (or just websites) served from content addressed stores (like §IPFS), storing data primarily on local machines (think [fission.codes](https://fission.codes) and the §LocalFirst movement), is going to be a fruitful route.
|
||||
|
||||
I’ll be looking forward to the next article on the challenges with end-to-end encryption (E2EE)!
|
||||
|
||||
|
||||
### Highlights
|
||||
|
||||
> Good backups are:
|
||||
>
|
||||
> * done regularly
|
||||
> * documented
|
||||
> * tested regularly
|
||||
> * stored at a different location than the computer powering your services, also called [off-site backups](https://en.wikipedia.org/wiki/Backup#Off-site%5Fdata%5Fprotection)
|
||||
> * not accessible from a compromised machine in your network
|
||||
|
||||
---
|
||||
|
||||
> My recommendation to most people putting services online would be: either do it for yourself _only_, or do it as a team with proper structure and processes.
|
||||
|
||||
---
|
||||
|
||||
> we need to figure out how to do decentralise services and data storage for individuals _at scale_
|
1
data/interactions/.json
Normal file
1
data/interactions/.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1795016","emoji":"💬","url":"https://hachyderm.io/@byjp/112115874706330891","comment":"@geffrey @death.au ????☺️ well now I’m blushing! Thank you!\n\nThe code for the site \u0026 the automation is public \u0026 MIT licenced, so do take anything you find useful; and get in touch if you have any questions!","author":{"name":"JP","url":"https://hachyderm.io/@byjp"},"timestamp":"2024-03-18T09:01:22Z"}]}
|
1
data/interactions/calendar.json
Normal file
1
data/interactions/calendar.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1758794","emoji":"♥️","url":"https://hachyderm.io/@byjp/111645944873743257#favorited-by-109303710428386973","author":{"name":"Jamie Tanna","url":"https://fed.brid.gy/r/https://www.jvt.me"},"timestamp":"2023-12-27T05:38:33Z"},{"guid":"webmentions.io#1758795","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111645944873743257#favorited-by-109344010117626087","author":{"name":"sass","url":"https://mastodon.social/@sass"},"timestamp":"2023-12-27T05:38:33Z"},{"guid":"webmentions.io#1758521","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111645944873743257#favorited-by-109290756203712091","author":{"name":"Maria Neumayer","url":"https://androiddev.social/@marianeum"},"timestamp":"2023-12-26T10:19:17Z"}]}
|
1
data/interactions/notes/jgdx0.json
Normal file
1
data/interactions/notes/jgdx0.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1790528","emoji":"💬","url":"https://mastodon.social/@MonaApp/112059587175850353","comment":"@byjp This will be fixed in the next update.","author":{"name":"Mona app","url":"https://mastodon.social/@MonaApp"},"timestamp":"2024-03-08T10:26:43Z"}]}
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Stephen","url":"https://pixelfed.social/jaguar314"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Stray Cats Observer","url":"https://pixelfed.social/headofcatdept"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Dany","url":"https://mastodon.art/users/danyaero"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Deekshith Allamaneni","url":"https://pixelfed.social/dk"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Tired Sloth","url":"https://pixelfed.social/TiredSloth"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Bryan 🏳️🌈 Mastodon","url":"https://hugs.lgbt/users/riversidebryan"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Art Zemon ✅ 🟦","url":"https://babka.social/users/azemon"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"},"timestamp":"0001-01-01T00:00:00Z"}],"reactions":{"⭐️":7,"🔁":1}}
|
||||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Stephen","url":"https://pixelfed.social/jaguar314"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Stray Cats Observer","url":"https://pixelfed.social/headofcatdept"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Dany","url":"https://mastodon.art/users/danyaero"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Deekshith Allamaneni","url":"https://pixelfed.social/dk"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Tired Sloth","url":"https://pixelfed.social/TiredSloth"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Bryan 🏳️🌈 Mastodon","url":"https://hugs.lgbt/users/riversidebryan"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Art Zemon ✅ 🟦","url":"https://babka.social/users/azemon"}},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/634552262586596629","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"}}]}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Dany","url":"https://mastodon.art/users/danyaero"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Myth2844","url":"https://pixelfed.social/myth2844"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Douglas VB","url":"https://mastodon.social/users/douglasvb"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Bryan 🏳️🌈 Mastodon","url":"https://hugs.lgbt/users/riversidebryan"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"JP","url":"https://hachyderm.io/users/byjp"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Shura","url":"https://pixelfed.social/Shura"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Nathan Metzger","url":"https://pixelfed.social/Linuxfan724"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Narek","url":"https://pixelfed.social/Kerobian"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"katherine","url":"https://pixelfed.social/frecklefox"}},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Oldladymd","url":"https://med-mastodon.com/users/Oldladymd"}},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"}}],"reactions":{"⭐️":9,"🔁":2}}
|
||||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Dany","url":"https://mastodon.art/users/danyaero"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Myth2844","url":"https://pixelfed.social/myth2844"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Douglas VB","url":"https://mastodon.social/users/douglasvb"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Bryan 🏳️🌈 Mastodon","url":"https://hugs.lgbt/users/riversidebryan"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Shura","url":"https://pixelfed.social/Shura"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Nathan Metzger","url":"https://pixelfed.social/Linuxfan724"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Narek","url":"https://pixelfed.social/Kerobian"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"katherine","url":"https://pixelfed.social/frecklefox"}},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Oldladymd","url":"https://med-mastodon.com/users/Oldladymd"}},{"emoji":"🔁","url":"https://pixelfed.social/p/jphastings/631278994729871476","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"}}],"reactions":{"⭐️":9,"🔁":2}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"interactions": [
|
||||
{
|
||||
"emoji": "💬",
|
||||
"comment": "Con antenitas de vinil 🤪",
|
||||
"url": "https://www.instagram.com/p/CoscyoJvPtI9wJtAdE6fJJ1vY6AMB827jvAHUo0/c/17994947992681879/",
|
||||
"author": {
|
||||
|
@ -9,9 +10,5 @@
|
|||
},
|
||||
"timestamp": "2023-02-15T20:16:08.000Z"
|
||||
}
|
||||
],
|
||||
"reactions": {
|
||||
"❤️": 46,
|
||||
"💬": 1
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"JP","url":"https://hachyderm.io/users/byjp"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Ben!","url":"https://pixelfed.social/bencord0"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Thomas Zimmermann","url":"https://pixelfed.social/curlingtom"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Ivan Jurišić","url":"https://pixelfed.social/ijurisic"},"timestamp":"0001-01-01T00:00:00Z"}],"reactions":{"⭐️":4}}
|
||||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Ben!","url":"https://pixelfed.social/bencord0"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Thomas Zimmermann","url":"https://pixelfed.social/curlingtom"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/619856573800023125","author":{"name":"Ivan Jurišić","url":"https://pixelfed.social/ijurisic"}}]}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/632549785261132608","author":{"name":"alexanderniki","url":"https://pixelfed.social/alexanderniki"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/632549785261132608","author":{"name":"Software Pagan","url":"https://pixelfed.social/softwarepagan"},"timestamp":"0001-01-01T00:00:00Z"},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/632549785261132608","author":{"name":"JP","url":"https://hachyderm.io/users/byjp"},"timestamp":"0001-01-01T00:00:00Z"},{"url":"https://pixelfed.social/p/RiversideBryan/634556807537550283","comment":"Congratulations 🥰","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"},"timestamp":"2023-11-27T01:02:12Z"}],"reactions":{"⭐️":3,"💬":1}}
|
||||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/632549785261132608","author":{"name":"alexanderniki","url":"https://pixelfed.social/alexanderniki"}},{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/632549785261132608","author":{"name":"Software Pagan","url":"https://pixelfed.social/softwarepagan"}},{"emoji": "💬", "url":"https://pixelfed.social/p/RiversideBryan/634556807537550283","comment":"Congratulations 🥰","author":{"name":"Bryan 🏳️🌈 Pixelfed","url":"https://pixelfed.social/RiversideBryan"},"timestamp":"2023-11-27T01:02:12Z"}]}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/629220605979255535","author":{"name":"Rui Almeida","url":"https://pixelfed.social/RJCA_PT"}}],"reactions":{"⭐️":1}}
|
||||
{"interactions":[{"emoji":"⭐️","url":"https://pixelfed.social/p/jphastings/629220605979255535","author":{"name":"Rui","url":"https://pixelfed.social/RJCA_PT"}}]}
|
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1795406","emoji":"♥️","url":"https://www.jvt.me/mf2/2024/03/oaqea/","author":{"name":"Jamie Tanna","url":"https://www.jvt.me"},"timestamp":"2024-03-19T16:46:00+01:00"}]}
|
1
data/interactions/posts/bookmarks-with-omnivore.json
Normal file
1
data/interactions/posts/bookmarks-with-omnivore.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1770115","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111793793066838551#favorited-by-108771874996654815","author":{"name":"chrismcleod.dev","url":"https://mastodon.online/@mstrkapowski"},"timestamp":"2024-01-21T14:06:42Z"},{"guid":"webmentions.io#1770114","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111793793066838551#favorited-by-109344010117626087","author":{"name":"sass","url":"https://mastodon.social/@sass"},"timestamp":"2024-01-21T14:06:41Z"}]}
|
|
@ -1 +1 @@
|
|||
{"interactions":[{"comment":"Ha! Just read this and I have to say I’m not impressed at all. I like my cooking a bit too much to let ChatGPT getting anywhere near it! I don’t think my commitment to science would have been as good as yours, I draw the line at throwing almonds and chickpeas on salmon 😂","url":"https://bsky.app/profile/davidcondemarin.bsky.social/post/3ko2zz752p72q","author":{"name":"David","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:00.000Z"},{"emoji":"❤️","url":"https://bsky.app/profile/byjp.me/post/3knrhqsj6ac23/liked-by","author":{"name":"David","url":"https://bsky.app/profile/davidcondemarin.bsky.social"}},{"emoji":"⭐️","url":"https://hachyderm.io/@byjp/112102305952063134/favourites","author":{"name":"Maria Neumayer","url":"https://androiddev.social/@marianeum"}}],"reactions":{"❤️":1,"⭐️":1,"💬":1}}
|
||||
{"interactions":[{"guid":"webmentions.io#1795504","emoji":"💬","url":"https://bsky.app/profile/davidcondemarin.bsky.social/post/3ko2zz752p72q","comment":"Ha! Just read this and I have to say I’m not impressed at all. I like my cooking a bit too much to let ChatGPT getting anywhere near it! I don’t think my commitment to science would have been as good as yours, I draw the line at throwing almonds and chickpeas on salmon ????","author":{"name":"David","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:44Z"},{"guid":"webmentions.io#1795367","emoji":"♥️","url":"https://bsky.app/profile/byjp.me/post/3knrhqsj6ac23#liked_by_did:plc:yqsc5vd6x67yofv5fmu5qkax","author":{"name":"","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:14Z"},{"guid":"webmentions.io#1794075","emoji":"⭐️","url":"https://hachyderm.io/@byjp/112102305952063134#favorited-by-109290756203712091","author":{"name":"Maria Neumayer","url":"https://androiddev.social/@marianeum"},"timestamp":"2024-03-17T02:11:11Z"}]}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1782674","emoji":"🔁","url":"https://hachyderm.io/@byjp/111951750976061859#reblogged-by-111257712064575715","author":{"name":"Bill Seitz","url":"https://toolsforthought.social/@billseitz"},"timestamp":"2024-02-19T02:40:36Z"},{"guid":"webmentions.io#1782381","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111951750976061859#favorited-by-109168960959032263","author":{"name":"Olivier Forget","url":"https://social.tchncs.de/@teleclimber"},"timestamp":"2024-02-18T10:41:56Z"},{"guid":"webmentions.io#1782366","emoji":"⭐️","url":"https://hachyderm.io/@byjp/111951750976061859#favorited-by-108221131497108772","author":{"name":"Luci for dyeing","url":"https://merveilles.town/@zens"},"timestamp":"2024-02-18T10:07:14Z"}]}
|
1
data/interactions/search.json
Normal file
1
data/interactions/search.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"interactions":[{"guid":"webmentions.io#1790184","emoji":"🔁","url":"https://hachyderm.io/@byjp/112053398317306907#reblogged-by-108221131497108772","author":{"name":"Luci for dyeing","url":"https://merveilles.town/@zens"},"timestamp":"2024-03-07T09:11:23Z"},{"guid":"webmentions.io#1790182","emoji":"🔁","url":"https://hachyderm.io/@byjp/112053398317306907#reblogged-by-111626810820500784","author":{"name":"\"You do you\" is Eu-gen-ics.","url":"https://disabled.social/@beadsland"},"timestamp":"2024-03-07T09:11:21Z"},{"guid":"webmentions.io#1790183","emoji":"⭐️","url":"https://hachyderm.io/@byjp/112053398317306907#favorited-by-108221131497108772","author":{"name":"Luci for dyeing","url":"https://merveilles.town/@zens"},"timestamp":"2024-03-07T09:11:21Z"}]}
|
|
@ -1,4 +1,15 @@
|
|||
{{ $index := (split .Path "/") | after 1 -}}
|
||||
{{- define "partials/reactionsvg.html" -}}
|
||||
{{- if eq .emoji "♥️" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 27)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>{{ end -}}
|
||||
{{- if eq .emoji "⭐️" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(65% 71 81)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>{{ end -}}
|
||||
{{- if eq .emoji "👍" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 270)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>{{ end -}}
|
||||
{{- if eq .emoji "👏" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(45% 71 270)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather"><path d="M20.248 12.654a1.61 1.61 0 0 1 2.366 1.203 1.6 1.6 0 0 1-.796 1.597l-10.522 6.075a5.471 5.471 0 0 1-4.16.554 5.5 5.5 0 0 1-3.334-2.555l-1.565-2.71a6.42 6.42 0 0 1-.198-6.064l3.128-6.321a1.65 1.65 0 0 1 2.237-.732 1.65 1.65 0 0 1 .868 1.71l-.5 3.314 9.016-5.205a1.6 1.6 0 0 1 2.185.586 1.61 1.61 0 0 1-.58 2.194l1.42-.82a1.61 1.61 0 0 1 2.18.555 1.61 1.61 0 0 1-.59 2.2l-1.42.82a1.6 1.6 0 0 1 2.185.585 1.61 1.61 0 0 1-.56 2.229zM18.427 6.28 12.86 9.495M20.037 9.069l-5.568 3.215M16.074 15.064l4.174-2.41"/></svg>{{ end -}}
|
||||
{{- if eq .emoji "🔁" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 130)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>{{ end -}}
|
||||
{{- if eq .emoji "💬" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(65% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>{{ end -}}
|
||||
|
||||
{{- if .name }}: {{ end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $index := (split .Path "/") | after 1 -}}
|
||||
{{- $interactions := index .Site.Data.interactions $index -}}
|
||||
{{ with $interactions }}
|
||||
<div class="interactions">
|
||||
|
@ -9,28 +20,28 @@
|
|||
{{ $most = $count }}
|
||||
{{ $emoji = $thisEm }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||
|
||||
Reactions & comments
|
||||
</summary>
|
||||
{{- $interactionMap := dict -}}
|
||||
{{- range $in := .interactions -}}
|
||||
{{- $oldList := index $interactionMap $in.emoji }}
|
||||
{{- $newList := append $in $oldList -}}
|
||||
{{- $interactionMap = merge $interactionMap (dict $in.emoji $newList) -}}
|
||||
{{- end -}}
|
||||
|
||||
<ul class="reactions">
|
||||
{{ range $thisEm, $count := .reactions }}
|
||||
{{ if gt $count 0 }}
|
||||
{{- range $thisEm, $items := $interactionMap -}}
|
||||
{{- $count := len $items -}}
|
||||
{{- if gt $count 0 -}}
|
||||
<li>
|
||||
{{ if eq $thisEm "❤️" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 27)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>{{ end -}}
|
||||
{{ if eq $thisEm "⭐️" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(65% 71 81)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>{{ end -}}
|
||||
{{ if eq $thisEm "👍" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 270)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>{{ end -}}
|
||||
{{ if eq $thisEm "🔁" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(55% 71 130)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>{{ end -}}
|
||||
{{ if eq $thisEm "💬" }}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lch(65% 0 0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>{{ end -}}
|
||||
|
||||
{{- $count }}
|
||||
{{ partial "partials/reactionsvg.html" (dict "emoji" $thisEm) }}
|
||||
{{ $count }}
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
@ -47,6 +58,20 @@
|
|||
</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
|
||||
<ul class="named-reactions">
|
||||
{{- range $thisEm, $items := $interactionMap -}}
|
||||
{{- if ne $thisEm "💬" -}}
|
||||
<li>
|
||||
{{ partial "partials/reactionsvg.html" (dict "emoji" $thisEm "name" true) -}}
|
||||
{{- range $i, $in := $items -}}
|
||||
<a href="{{ $in.url }}" target="_blank">{{ $in.author.name }}</a>
|
||||
{{- if ne $i (sub (len $items) 1) }}, {{ end -}}
|
||||
{{- end -}}
|
||||
</li>
|
||||
{{- end -}}
|
||||
{{ end }}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
{{ else }}
|
||||
|
|
9
tools/go.work
Normal file
9
tools/go.work
Normal file
|
@ -0,0 +1,9 @@
|
|||
go 1.21.6
|
||||
|
||||
use (
|
||||
./archive/goodreads
|
||||
./archive/instagram
|
||||
./archive/twitter
|
||||
./import/omnivore
|
||||
./import/webmentionio
|
||||
)
|
4
tools/go.work.sum
Normal file
4
tools/go.work.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
@ -1,3 +0,0 @@
|
|||
go 1.21.6
|
||||
|
||||
use .
|
|
@ -1 +0,0 @@
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
@ -33,6 +33,21 @@ func main() {
|
|||
fmt.Fprint(os.Stderr, "OMNIVORE_API_KEY is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rootDir := "./"
|
||||
if len(os.Args) > 1 {
|
||||
rootDir = os.Args[1]
|
||||
}
|
||||
|
||||
outputDir := path.Join(rootDir, "content/bookmarks")
|
||||
if !isDir(outputDir) {
|
||||
fmt.Printf(
|
||||
"Usage: %s [directory]\n 'directory' should be the root of your hugo blog (default: ./)\n",
|
||||
path.Base(os.Args[0]),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Make the GraphQL request
|
||||
articles, err := omnivoreArticles(
|
||||
"in:archive has:highlights sort:updated-des",
|
||||
|
@ -43,8 +58,6 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
outputDir := "../../../content/bookmarks"
|
||||
|
||||
for _, article := range articles {
|
||||
if err := outputArticle(article, outputDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to output article: %v\n", err)
|
||||
|
@ -52,6 +65,15 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func isDir(pathStr string) bool {
|
||||
st, err := os.Stat(pathStr)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return st.IsDir()
|
||||
}
|
||||
|
||||
var hashtags = regexp.MustCompile(`#\w+`)
|
||||
|
||||
func outputArticle(article Article, outputDir string) error {
|
||||
|
|
1
tools/import/webmentionio/.gitignore
vendored
Normal file
1
tools/import/webmentionio/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
11
tools/import/webmentionio/go.mod
Normal file
11
tools/import/webmentionio/go.mod
Normal file
|
@ -0,0 +1,11 @@
|
|||
module github.com/by-jp/www.byjp.me/tools/import/webmentionio
|
||||
|
||||
go 1.21.6
|
||||
|
||||
require (
|
||||
github.com/by-jp/www.byjp.me/tools/syndicate v0.0.0-00010101000000-000000000000
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
)
|
||||
|
||||
replace github.com/by-jp/www.byjp.me/tools/syndicate => ../../syndicate
|
4
tools/import/webmentionio/go.sum
Normal file
4
tools/import/webmentionio/go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
9
tools/import/webmentionio/interactions.go
Normal file
9
tools/import/webmentionio/interactions.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
synd "github.com/by-jp/www.byjp.me/tools/syndicate/shared"
|
||||
)
|
||||
|
||||
type InteractionFile struct {
|
||||
Interactions []synd.Interaction `json:"interactions"`
|
||||
}
|
37
tools/import/webmentionio/jf2.go
Normal file
37
tools/import/webmentionio/jf2.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
type Mentions struct {
|
||||
Type string
|
||||
Name string
|
||||
Children []Mention
|
||||
}
|
||||
|
||||
type Mention struct {
|
||||
Type string
|
||||
Author Author
|
||||
URL string
|
||||
// Fallback on WebmentionReceived as fallback for Published
|
||||
Published string // (likes often don't have timestamps)
|
||||
WebmentionReceived string `json:"wm-received"`
|
||||
|
||||
WebmentionID uint `json:"wm-id"`
|
||||
WebmentionTarget string `json:"wm-target"`
|
||||
Content struct {
|
||||
Text string
|
||||
}
|
||||
Summary struct {
|
||||
ContentType string `json:"content-type"`
|
||||
Value string
|
||||
}
|
||||
|
||||
// The kind of action, eg. like-of, in-reply-to, repost-of
|
||||
WebmentionProperty string `json:"wm-property"`
|
||||
WebmentionPrivate bool `json:"wm-private"`
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Type string
|
||||
Name string
|
||||
Photo string
|
||||
URL string
|
||||
}
|
204
tools/import/webmentionio/main.go
Normal file
204
tools/import/webmentionio/main.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
|
||||
synd "github.com/by-jp/www.byjp.me/tools/syndicate/shared"
|
||||
)
|
||||
|
||||
const webmentions = "https://webmention.io/api/mentions.jf2?domain=%s&token=%s"
|
||||
const siteTarget = "https://www.byjp.me/"
|
||||
|
||||
type Config struct {
|
||||
Domain string
|
||||
Token string
|
||||
Root string
|
||||
}
|
||||
|
||||
func main() {
|
||||
check(godotenv.Load())
|
||||
var c Config
|
||||
check(envconfig.Process("webmentionio", &c))
|
||||
|
||||
mentions, err := retrieveMentions(c)
|
||||
check(err)
|
||||
|
||||
pathChecker := checkValidPath(path.Join(c.Root, "content"))
|
||||
for _, m := range mentions.Children {
|
||||
pathStr, interaction, err := parseJF2(m, pathChecker)
|
||||
switch err {
|
||||
case nil:
|
||||
// Proceed
|
||||
case ErrNoEntry, ErrIsPrivate, ErrIncorrectTarget:
|
||||
// Skip
|
||||
continue
|
||||
default:
|
||||
// Stop
|
||||
check(err)
|
||||
}
|
||||
|
||||
dst := path.Join(c.Root, "data/interactions", pathStr+".json")
|
||||
addErr := addInteraction(dst, interaction)
|
||||
if addErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't save interaction %s to %s: %v\n", interaction.GUID, dst, addErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkValidPath(contentDir string) func(string) bool {
|
||||
return func(pathStr string) bool {
|
||||
pathDir := path.Join(contentDir, pathStr)
|
||||
std, err := os.Stat(pathDir)
|
||||
if err == nil && std.IsDir() {
|
||||
return true
|
||||
}
|
||||
|
||||
pathFile := pathDir + ".md"
|
||||
stf, err := os.Stat(pathFile)
|
||||
if err == nil && stf.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func addInteraction(jsonPath string, newIn synd.Interaction) error {
|
||||
inf := InteractionFile{}
|
||||
|
||||
rf, err := os.Open(jsonPath)
|
||||
if err == nil {
|
||||
if err := json.NewDecoder(rf).Decode(&inf); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
rf.Close()
|
||||
|
||||
added := false
|
||||
for idx, in := range inf.Interactions {
|
||||
if in.GUID == newIn.GUID {
|
||||
inf.Interactions[idx] = newIn
|
||||
added = true
|
||||
}
|
||||
}
|
||||
if !added {
|
||||
inf.Interactions = append(inf.Interactions, newIn)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(jsonPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wf, err := os.OpenFile(jsonPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wf.Close()
|
||||
|
||||
return json.NewEncoder(wf).Encode(inf)
|
||||
}
|
||||
|
||||
var ErrNoEntry = errors.New("JF2 item isn't an entry")
|
||||
var ErrIsPrivate = errors.New("JF2 item is set to private")
|
||||
var ErrIncorrectTarget = errors.New("JF2 item describes a different target site")
|
||||
|
||||
func parseJF2(m Mention, isValidPath func(string) bool) (string, synd.Interaction, error) {
|
||||
if m.Type != "entry" {
|
||||
return "", synd.Interaction{}, ErrNoEntry
|
||||
}
|
||||
if m.WebmentionPrivate {
|
||||
return "", synd.Interaction{}, ErrIsPrivate
|
||||
}
|
||||
if !strings.HasPrefix(m.WebmentionTarget, siteTarget) {
|
||||
return "", synd.Interaction{}, ErrIncorrectTarget
|
||||
}
|
||||
if strings.HasPrefix(m.URL, siteTarget) {
|
||||
return "", synd.Interaction{}, ErrIncorrectTarget
|
||||
}
|
||||
|
||||
sitePath := strings.TrimRight(m.WebmentionTarget[len(siteTarget):], "/")
|
||||
if !isValidPath(sitePath) {
|
||||
return "", synd.Interaction{}, ErrIncorrectTarget
|
||||
}
|
||||
|
||||
i := synd.Interaction{
|
||||
GUID: fmt.Sprintf("webmentions.io#%d", m.WebmentionID),
|
||||
URL: m.URL,
|
||||
Comment: m.Content.Text,
|
||||
Author: synd.Author{
|
||||
// TODO: Handle Bluesky no name on likes
|
||||
Name: m.Author.Name,
|
||||
URL: m.Author.URL,
|
||||
// TODO: Photo?
|
||||
},
|
||||
}
|
||||
|
||||
p, err := time.Parse(time.RFC3339, m.Published)
|
||||
if err != nil || p.IsZero() {
|
||||
p, _ = time.Parse(time.RFC3339, m.WebmentionReceived)
|
||||
}
|
||||
i.Timestamp = p
|
||||
|
||||
// TODO: very long content?
|
||||
// TODO: If wm-source != URL then acting on a reference, not on a syndicated post
|
||||
|
||||
switch m.WebmentionProperty {
|
||||
case "like-of":
|
||||
if strings.Contains(m.Author.URL, "/@") {
|
||||
i.Emoji = "⭐️"
|
||||
} else {
|
||||
i.Emoji = "♥️"
|
||||
}
|
||||
|
||||
case "repost-of":
|
||||
i.Emoji = "🔁"
|
||||
case "in-reply-to":
|
||||
i.Emoji = "💬"
|
||||
case "mention-of":
|
||||
i.Comment = "_A wibble_"
|
||||
i.Emoji = "🗣️"
|
||||
}
|
||||
|
||||
return sitePath, i, nil
|
||||
}
|
||||
|
||||
func retrieveMentions(c Config) (Mentions, error) {
|
||||
url := webmentionsURL(c.Domain, c.Token)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return Mentions{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return Mentions{}, fmt.Errorf("Non 200 response code: %v\n%s", res.StatusCode, body)
|
||||
}
|
||||
|
||||
var m Mentions
|
||||
return m, json.NewDecoder(res.Body).Decode(&m)
|
||||
}
|
||||
|
||||
func webmentionsURL(domain, token string) string {
|
||||
return fmt.Sprintf(webmentions, domain, token)
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,9 @@ type Post struct {
|
|||
}
|
||||
|
||||
type Interaction struct {
|
||||
// eg. Repost is 🔁, Facebook is 👍, Instagram is ♥️, Mastodon is ⭐️, Medium is 👏
|
||||
// Some unique identifier that remains consistent for this interaction
|
||||
GUID string `json:"guid"`
|
||||
// eg. Repost is 🔁, Facebook is 👍, Instagram & Bluesky are ♥️, Mastodon is ⭐️, Medium is 👏
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
// The URL of the original interaction
|
||||
URL string `json:"url"`
|
||||
|
|
Loading…
Reference in a new issue