From 24d35996907ffcdf393f9938589eedced84353e4 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 May 2025 20:29:43 +0200 Subject: [PATCH 1/5] Fix middle button mouse up on status header always opening status in a new tab (#34700) --- app/javascript/mastodon/components/status.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 21d596a58c..820b24cd6f 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -175,9 +175,8 @@ class Status extends ImmutablePureComponent { } }; - handleMouseUp = e => { + handleHeaderClick = e => { // Only handle clicks on the empty space above the content - if (e.target !== e.currentTarget && e.detail >= 1) { return; } @@ -547,7 +546,7 @@ class Status extends ImmutablePureComponent {
{(connectReply || connectUp || connectToRoot) &&
} -
+
{status.get('edited_at') && *} From c058c45a8e04f54b9ba634c577610003c0e902f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 10:07:14 +0200 Subject: [PATCH 2/5] New Crowdin Translations (automated) (#34701) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/bg.json | 1 - app/javascript/mastodon/locales/br.json | 1 - app/javascript/mastodon/locales/ca.json | 2 +- app/javascript/mastodon/locales/cs.json | 2 +- app/javascript/mastodon/locales/cy.json | 3 +++ app/javascript/mastodon/locales/da.json | 2 +- app/javascript/mastodon/locales/de.json | 2 +- app/javascript/mastodon/locales/es-AR.json | 2 +- app/javascript/mastodon/locales/es-MX.json | 2 +- app/javascript/mastodon/locales/es.json | 2 +- app/javascript/mastodon/locales/fi.json | 2 +- app/javascript/mastodon/locales/fo.json | 2 +- app/javascript/mastodon/locales/gl.json | 2 +- app/javascript/mastodon/locales/he.json | 2 +- app/javascript/mastodon/locales/hu.json | 1 - app/javascript/mastodon/locales/is.json | 2 +- app/javascript/mastodon/locales/it.json | 3 +++ app/javascript/mastodon/locales/nl.json | 2 +- app/javascript/mastodon/locales/pt-PT.json | 2 +- app/javascript/mastodon/locales/sq.json | 2 +- app/javascript/mastodon/locales/tr.json | 2 +- app/javascript/mastodon/locales/vi.json | 2 +- app/javascript/mastodon/locales/zh-TW.json | 2 +- config/locales/cy.yml | 4 ++++ config/locales/it.yml | 4 ++++ config/locales/simple_form.cy.yml | 9 +++++++++ config/locales/simple_form.it.yml | 5 +++++ 27 files changed, 46 insertions(+), 21 deletions(-) diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index f3c6f771f5..2a64f3e68d 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -28,7 +28,6 @@ "account.edit_profile": "Редактиране на профила", "account.enable_notifications": "Известяване при публикуване от @{name}", "account.endorse": "Представи в профила", - "account.familiar_followers_many": "Последвано от {name1}, {name2} и {othersCount, plural, one {# друг} other {# други}}", "account.familiar_followers_one": "Последвано от {name1}", "account.familiar_followers_two": "Последвано от {name1} и {name2}", "account.featured": "Препоръчано", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index d327f0965b..8f79d7a05d 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -25,7 +25,6 @@ "account.edit_profile": "Kemmañ ar profil", "account.enable_notifications": "Ma c'hemenn pa vez embannet traoù gant @{name}", "account.endorse": "Lakaat war-wel war ar profil", - "account.familiar_followers_many": "Heuilhet gant {name1}, {name2}, {othersCount, plural, one {hag # all} two {ha # all} few {ha # all} many {ha(g) # all} other {hag(g) # all}}", "account.familiar_followers_one": "Heuilhet gant {name1}", "account.familiar_followers_two": "Heuilhet gant {name1} ha {name2}", "account.featured_tags.last_status_at": "Toud diwezhañ : {date}", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 2ea3a36329..787fb4c6fc 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -28,7 +28,7 @@ "account.edit_profile": "Edita el perfil", "account.enable_notifications": "Notifica'm els tuts de @{name}", "account.endorse": "Recomana en el perfil", - "account.familiar_followers_many": "Seguit per {name1}, {name2} i {othersCount, plural, one {# altre compte} other {# altres comptes}}", + "account.familiar_followers_many": "Seguit per {name1}, {name2} i {othersCount, plural, one {# altre compte} other {# altres comptes}} que coneixeu", "account.familiar_followers_one": "Seguit per {name1}", "account.familiar_followers_two": "Seguit per {name1} i {name2}", "account.featured": "Destacat", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index a185c59d79..ce71e1cf5a 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -28,7 +28,7 @@ "account.edit_profile": "Upravit profil", "account.enable_notifications": "Oznamovat mi příspěvky @{name}", "account.endorse": "Zvýraznit na profilu", - "account.familiar_followers_many": "Sleduje je {name1}, {name2} a {othersCount, plural, one {# další} few {# další} many {# dalších} other {# dalších}}", + "account.familiar_followers_many": "Sleduje je {name1}, {name2} a {othersCount, plural, one {jeden další, které znáte} few {# další, které znáte} many {# dalších, které znáte} other {# dalších, které znáte}}", "account.familiar_followers_one": "Sleduje je {name1}", "account.familiar_followers_two": "Sleduje je {name1} a {name2}", "account.featured": "Zvýrazněné", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index cc717d4e23..6a86a4926c 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -28,6 +28,9 @@ "account.edit_profile": "Golygu'r proffil", "account.enable_notifications": "Rhowch wybod i fi pan fydd @{name} yn postio", "account.endorse": "Dangos ar fy mhroffil", + "account.familiar_followers_many": "Yn cael ei ddilyn gan {name1},{name2}, a {othersCount, plural, one {one other you know} other{# others you know}}", + "account.familiar_followers_one": "Wedi'i ddilyn gan {name1}", + "account.familiar_followers_two": "Wedi'i ddilyn gan {name1} a {name2}", "account.featured": "Nodwedd", "account.featured.accounts": "Proffilau", "account.featured.hashtags": "Hashnodau", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 31d15a3eba..db5e66fe94 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -28,7 +28,7 @@ "account.edit_profile": "Redigér profil", "account.enable_notifications": "Advisér mig, når @{name} poster", "account.endorse": "Fremhæv på profil", - "account.familiar_followers_many": "Følges af {name1}, {name2} og {othersCount, plural, one {# mere} other {# mere}}", + "account.familiar_followers_many": "Følges af {name1}, {name2} og {othersCount, plural, one {# mere, man kender} other {# mere, man kender}}", "account.familiar_followers_one": "Følges af {name1}", "account.familiar_followers_two": "Følges af {name1} og {name2}", "account.featured": "Fremhævet", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index dfe6c8c389..01b195e5d4 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -28,7 +28,7 @@ "account.edit_profile": "Profil bearbeiten", "account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet", "account.endorse": "Im Profil vorstellen", - "account.familiar_followers_many": "Gefolgt von {name1}, {name2} und {othersCount, plural, one {# Profil} other {# weiteren Profilen}}", + "account.familiar_followers_many": "Gefolgt von {name1}, {name2} und {othersCount, plural, one {einem weiteren Profil, das dir bekannt ist} other {# weiteren Profilen, die dir bekannt sind}}", "account.familiar_followers_one": "Gefolgt von {name1}", "account.familiar_followers_two": "Gefolgt von {name1} und {name2}", "account.featured": "Vorgestellt", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index f441d8fca8..73260068fe 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -28,7 +28,7 @@ "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificarme cuando @{name} envíe mensajes", "account.endorse": "Destacar en el perfil", - "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural, one {# cuenta más} other {# cuentas más}}", + "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural, one {# cuenta más que conocés} other {# cuentas más que conocés}}", "account.familiar_followers_one": "Seguido por {name1}", "account.familiar_followers_two": "Seguido por {name1} y {name2}", "account.featured": "Destacados", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index e0ca7223ba..ef99fea899 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -28,7 +28,7 @@ "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificarme cuando @{name} publique algo", "account.endorse": "Destacar en mi perfil", - "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural,one {# otro} other {# otros}}", + "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural,one {otro que conoces}other {# otros que conoces}}", "account.familiar_followers_one": "Seguido por {name1}", "account.familiar_followers_two": "Seguid por {name1} y {name2}", "account.featured": "Destacado", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 99a4610e20..c41b9de93e 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -28,7 +28,7 @@ "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificarme cuando @{name} publique algo", "account.endorse": "Destacar en el perfil", - "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural, one {# más} other {# más}}", + "account.familiar_followers_many": "Seguido por {name1}, {name2} y {othersCount, plural,one {otro que conoces}other {# otros que conoces}}", "account.familiar_followers_one": "Seguido por {name1}", "account.familiar_followers_two": "Seguido por {name1} y {name2}", "account.featured": "Destacado", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 3615185331..c65ed1288a 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -28,7 +28,7 @@ "account.edit_profile": "Muokkaa profiilia", "account.enable_notifications": "Ilmoita minulle, kun @{name} julkaisee", "account.endorse": "Suosittele profiilissa", - "account.familiar_followers_many": "Seuraajina {name1}, {name2} ja {othersCount, plural, one {# muu} other {# muuta}}", + "account.familiar_followers_many": "Seuraajina {name1}, {name2} ja {othersCount, plural, one {1 muu, jonka tunnet} other {# muuta, jotka tunnet}}", "account.familiar_followers_one": "Seuraajana {name1}", "account.familiar_followers_two": "Seuraajina {name1} ja {name2}", "account.featured": "Suositellut", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index c659f91eb6..1bb9bb29b0 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -28,7 +28,7 @@ "account.edit_profile": "Broyt vanga", "account.enable_notifications": "Boða mær frá, tá @{name} skrivar", "account.endorse": "Víst á vangamyndini", - "account.familiar_followers_many": "{name1}, {name2} og {othersCount, plural, one {# annar} other {# onnur}} fylgja", + "account.familiar_followers_many": "{name1}, {name2} og {othersCount, plural, one {ein annar/onnur tú kennir} other {# onnur tú kennir}} fylgja", "account.familiar_followers_one": "{name1} fylgir", "account.familiar_followers_two": "{name1} og {name2} fylgja", "account.featured": "Tikin fram", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index f88ab43be9..51275bfaee 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -28,7 +28,7 @@ "account.edit_profile": "Editar perfil", "account.enable_notifications": "Noficarme cando @{name} publique", "account.endorse": "Amosar no perfil", - "account.familiar_followers_many": "Seguida por {name1}, {name2}, e {othersCount, plural,one {# máis} other {# máis}}", + "account.familiar_followers_many": "Seguida por {name1}, {name2}, e {othersCount, plural, one {outra conta que coñeces} other {outras # contas que coñeces}}", "account.familiar_followers_one": "Seguida por {name1}", "account.familiar_followers_two": "Seguida por {name1} e {name2}", "account.featured": "Destacado", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index d70006669a..d3811a75c4 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -28,7 +28,7 @@ "account.edit_profile": "עריכת פרופיל", "account.enable_notifications": "שלח לי התראות כש@{name} מפרסם", "account.endorse": "קדם את החשבון בפרופיל", - "account.familiar_followers_many": "החשבון נעקב על ידי {name1}, {name2} ועוד {othersCount, plural,one {אחד נוסף}other {# נוספים}}", + "account.familiar_followers_many": "החשבון נעקב על ידי {name1}, {name2} ועוד {othersCount, plural,one {אחד נוסף שמוכר לך}other {# נוספים שמוכרים לך}}", "account.familiar_followers_one": "החשבון נעקב על ידי {name1}", "account.familiar_followers_two": "החשבון נעקב על ידי {name1} ו־{name2}", "account.featured": "מומלץ", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 0ff052d750..97f9b7d387 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -28,7 +28,6 @@ "account.edit_profile": "Profil szerkesztése", "account.enable_notifications": "Figyelmeztessen, ha @{name} bejegyzést tesz közzé", "account.endorse": "Kiemelés a profilodon", - "account.familiar_followers_many": "{name1}, {name2} és {othersCount, plural, one {# másik} other {# másik}} követi", "account.familiar_followers_one": "{name1} követi", "account.familiar_followers_two": "{name1} és {name2} követi", "account.featured": "Kiemelt", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index e9655f6af5..aff74649fe 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -28,7 +28,7 @@ "account.edit_profile": "Breyta notandasniði", "account.enable_notifications": "Láta mig vita þegar @{name} sendir inn", "account.endorse": "Birta á notandasniði", - "account.familiar_followers_many": "Fylgt af {name1}, {name2} og {othersCount, plural, one {# í viðbót} other {# í viðbót}}", + "account.familiar_followers_many": "Fylgt af {name1}, {name2} og {othersCount, plural, one {einum öðrum sem þú þekkir} other {# öðrum sem þú þekkir}}", "account.familiar_followers_one": "Fylgt af {name1}", "account.familiar_followers_two": "Fylgt af {name1} og {name2}", "account.featured": "Með aukið vægi", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 838d662f6b..606b9ded92 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -28,6 +28,9 @@ "account.edit_profile": "Modifica profilo", "account.enable_notifications": "Avvisami quando @{name} pubblica un post", "account.endorse": "In evidenza sul profilo", + "account.familiar_followers_many": "Seguito da {name1}, {name2}, e {othersCount, plural, one {un altro che conosci} other {# altri che conosci}}", + "account.familiar_followers_one": "Seguito da {name1}", + "account.familiar_followers_two": "Seguito da {name1} e {name2}", "account.featured": "In primo piano", "account.featured.accounts": "Profili", "account.featured.hashtags": "Hashtag", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 3dac5fd7dd..404b0e0395 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -28,7 +28,7 @@ "account.edit_profile": "Profiel bewerken", "account.enable_notifications": "Geef een melding wanneer @{name} een bericht plaatst", "account.endorse": "Op profiel weergeven", - "account.familiar_followers_many": "Gevolgd door {name1}, {name2} en {othersCount, plural, one {# ander account} other {# andere accounts}}", + "account.familiar_followers_many": "Gevolgd door {name1}, {name2} en {othersCount, plural, one {één ander bekend account} other {# andere bekende accounts}}", "account.familiar_followers_one": "Gevolgd door {name1}", "account.familiar_followers_two": "Gevolgd door {name1} en {name2}", "account.featured": "Uitgelicht", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 5facd93d64..88fd9bf427 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -28,7 +28,7 @@ "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificar-me das publicações de @{name}", "account.endorse": "Destacar no perfil", - "account.familiar_followers_many": "Seguido por {name1}, {name2} e {othersCount, plural,one {# outro}other {# outros}}", + "account.familiar_followers_many": "Seguido por {name1}, {name2} e {othersCount, plural,one {mais uma pessoa que conhece} other {# outras pessoas que conhece}}", "account.familiar_followers_one": "Seguido por {name1}", "account.familiar_followers_two": "Seguido por {name1} e {name2}", "account.featured": "Destaques", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 77d86db377..af3fd9761f 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -28,7 +28,7 @@ "account.edit_profile": "Përpunoni profilin", "account.enable_notifications": "Njoftomë, kur poston @{name}", "account.endorse": "Pasqyrojeni në profil", - "account.familiar_followers_many": "Ndjekur nga {name1}, {name2} dhe {othersCount, plural, one {# tjetër} other {# të tjerë}}", + "account.familiar_followers_many": "Ndjekur nga {name1}, {name2} dhe {othersCount, plural, one {një tjetër që njihni} other {# të tjerë që njihni}}", "account.familiar_followers_one": "Ndjekur nga {name1}", "account.familiar_followers_two": "Ndjekur nga {name1} dhe {name2}", "account.featured": "Të zgjedhur", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index f46f070817..566b7ddff0 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -28,7 +28,7 @@ "account.edit_profile": "Profili düzenle", "account.enable_notifications": "@{name} kişisinin gönderi bildirimlerini aç", "account.endorse": "Profilimde öne çıkar", - "account.familiar_followers_many": "{name1}, {name2}, {othersCount, plural, one {# diğer} other {# diğer}} kişi tarafından takip ediliyor", + "account.familiar_followers_many": "{name1}, {name2}, {othersCount, plural, one {# diğer} other {# diğer}} bildiğiniz kişi tarafından takip ediliyor", "account.familiar_followers_one": "{name1} tarafından takip ediliyor", "account.familiar_followers_two": "{name1} ve {name2} tarafından takip ediliyor", "account.featured": "Öne çıkan", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 7f7dcae7de..d592b0caa8 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -28,7 +28,7 @@ "account.edit_profile": "Sửa hồ sơ", "account.enable_notifications": "Nhận thông báo khi @{name} đăng tút", "account.endorse": "Tôn vinh người này", - "account.familiar_followers_many": "Theo dõi bởi {name1}, {name2} và {othersCount, plural, other {# người khác}}", + "account.familiar_followers_many": "Theo dõi bởi {name1}, {name2} và {othersCount, plural, other {# người khác mà bạn biết}}", "account.familiar_followers_one": "Theo dõi bởi {name1}", "account.familiar_followers_two": "Theo dõi bởi {name1} và {name2}", "account.featured": "Nêu bật", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index b47ba36975..14c64c92b8 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -28,7 +28,7 @@ "account.edit_profile": "編輯個人檔案", "account.enable_notifications": "當 @{name} 嘟文時通知我", "account.endorse": "於個人檔案推薦對方", - "account.familiar_followers_many": "被 {name1}、{name2}、及 {othersCount, plural, other {其他 # 人}} 跟隨", + "account.familiar_followers_many": "被 {name1}、{name2}、及 {othersCount, plural, other {其他您認識的 # 人}} 跟隨", "account.familiar_followers_one": "被 {name1} 跟隨", "account.familiar_followers_two": "被 {name1} 與 {name2} 跟隨", "account.featured": "精選內容", diff --git a/config/locales/cy.yml b/config/locales/cy.yml index c4d32c5fd7..59117fe1d1 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -2030,6 +2030,10 @@ cy: limit: Rydych chi eisoes wedi pinio uchafswm nifer y postiadau ownership: Nid oes modd pinio postiad rhywun arall reblog: Nid oes modd pinio hwb + quote_policies: + followers: Dilynwyr a defnyddwyr wedi'u crybwyll + nobody: Dim ond defnyddwyr wedi'u crybwyll + public: Pawb title: '%{name}: "%{quote}"' visibilities: direct: Uniongyrchol diff --git a/config/locales/it.yml b/config/locales/it.yml index 9894e99199..6ff9b33310 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1860,6 +1860,10 @@ it: limit: Hai già fissato in cima il massimo numero di post ownership: Non puoi fissare in cima un post di qualcun altro reblog: Un toot condiviso non può essere fissato in cima + quote_policies: + followers: Seguaci e utenti menzionati + nobody: Solo gli utenti menzionati + public: Tutti title: '%{name}: "%{quote}"' visibilities: direct: Diretto diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index 656cca8b37..c6a140b27b 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -56,6 +56,7 @@ cy: scopes: Pa APIs y bydd y rhaglen yn cael mynediad iddynt. Os dewiswch gwmpas lefel uchaf, nid oes angen i chi ddewis rhai unigol. setting_aggregate_reblogs: Peidiwch â dangos hybiau newydd ar bostiadau sydd wedi cael eu hybu'n ddiweddar (dim ond yn effeithio ar hybiau newydd ei dderbyn) setting_always_send_emails: Fel arfer ni fydd hysbysiadau e-bost yn cael eu hanfon pan fyddwch chi wrthi'n defnyddio Mastodon + setting_default_quote_policy: Mae defnyddwyr sy'n cael eu crybwyll yn cael dyfynnu bob amser. Dim ond ar gyfer postiadau a grëwyd gyda'r fersiwn nesaf o Mastodon y bydd y gosodiad hwn yn dod i rym, ond gallwch ddewis eich dewis wrth baratoi. setting_default_sensitive: Mae cyfryngau sensitif wedi'u cuddio yn rhagosodedig a gellir eu datgelu trwy glicio setting_display_media_default: Cuddio cyfryngau wedi eu marcio'n sensitif setting_display_media_hide_all: Cuddio cyfryngau bob tro @@ -148,6 +149,13 @@ cy: min_age: Ni ddylai fod yn is na'r isafswm oedran sy'n ofynnol gan gyfreithiau eich awdurdodaeth. user: chosen_languages: Wedi eu dewis, dim ond tŵtiau yn yr ieithoedd hyn bydd yn cael eu harddangos mewn ffrydiau cyhoeddus + date_of_birth: + few: Mae'n rhai i ni wneud yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. + many: Mae'n rhai i ni wneud yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. + one: Mae'n rhai i ni wneud yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. + other: Mae'n rhai i ni wneud yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. + two: Mae'n rhai i ni wneud yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. + zero: Gwnewch yn siŵr eich bod o leiaf yn %{count} i ddefnyddio Mastodon. Fyddwn ni ddim yn cadw hwn. role: Mae'r rôl yn rheoli pa ganiatâd sydd gan y defnyddiwr. user_role: color: Lliw i'w ddefnyddio ar gyfer y rôl drwy'r UI, fel RGB mewn fformat hecs @@ -228,6 +236,7 @@ cy: setting_boost_modal: Dangos deialog cadarnhau cyn rhoi hwb setting_default_language: Iaith postio setting_default_privacy: Preifatrwydd cyhoeddi + setting_default_quote_policy: Pwy sy'n gallu dyfynnu setting_default_sensitive: Marcio cyfryngau fel eu bod yn sensitif bob tro setting_delete_modal: Dangos deialog cadarnhau cyn dileu postiad setting_disable_hover_cards: Analluogi rhagolwg proffil ar lusgo diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index a3672696e5..556688337b 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -56,6 +56,7 @@ it: scopes: A quali API l'applicazione potrà avere accesso. Se selezionate un ambito di alto livello, non c'è bisogno di selezionare quelle singole. setting_aggregate_reblogs: Non mostrare nuove condivisioni per toot che sono stati condivisi di recente (ha effetto solo sulle nuove condivisioni) setting_always_send_emails: Normalmente le notifiche e-mail non vengono inviate quando si utilizza attivamente Mastodon + setting_default_quote_policy: Gli utenti menzionati sono sempre in grado di citare. Questa impostazione avrà effetto solo per i post che verranno creati con la prossima versione di Mastodon, ma puoi selezionare le tue preferenze in preparazione del rilascio della prossima versione setting_default_sensitive: Media con contenuti sensibili sono nascosti in modo predefinito e possono essere rivelati con un click setting_display_media_default: Nascondi media segnati come sensibili setting_display_media_hide_all: Nascondi sempre tutti i media @@ -148,6 +149,9 @@ it: min_age: Non si dovrebbe avere un'età inferiore a quella minima richiesta, dalle leggi della tua giurisdizione. user: chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate + date_of_birth: + one: Dobbiamo verificare che tu abbia almeno %{count} anno per usare Mastodon. Non archivieremo questa informazione. + other: Dobbiamo verificare che tu abbia almeno %{count} anni per usare Mastodon. Non archivieremo questa informazione. role: Il ruolo controlla quali permessi ha l'utente. user_role: color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale @@ -228,6 +232,7 @@ it: setting_boost_modal: Mostra dialogo di conferma prima del boost setting_default_language: Lingua dei post setting_default_privacy: Privacy dei post + setting_default_quote_policy: Chi può citare setting_default_sensitive: Segna sempre i media come sensibili setting_delete_modal: Mostra dialogo di conferma prima di eliminare un post setting_disable_hover_cards: Disabilita l'anteprima del profilo al passaggio del mouse From 3ea1f074abfae97fbd33c8953e4d3f40c17be856 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 May 2025 11:07:33 +0200 Subject: [PATCH 3/5] Fix sidekiq JSON serialization warning in `ActivityPub::FetchAllRepliesWorker` (#34702) --- app/workers/activitypub/fetch_all_replies_worker.rb | 2 +- spec/workers/activitypub/fetch_all_replies_worker_spec.rb | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index d4ac3e85b8..40b251cf14 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -80,7 +80,7 @@ class ActivityPub::FetchAllRepliesWorker root_status_body = fetch_resource(root_status_uri, true) return if root_status_body.nil? - FetchReplyWorker.perform_async(root_status_uri, { **options, prefetched_body: root_status_body }) + FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body }) get_replies(root_status_body, MAX_PAGES, options) end diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb index 682d538653..9a8bdac030 100644 --- a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb @@ -124,8 +124,6 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do before do stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_ENABLED', true) - allow(FetchReplyWorker).to receive(:push_bulk) - allow(FetchReplyWorker).to receive(:perform_async) all_items.each do |item| next if [top_note_uri, reply_note_uri].include? item @@ -150,7 +148,7 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do it 'fetches the top status only once' do _ = subject.perform(status.id, { request_id: 0 }) - expect(FetchReplyWorker).to have_received(:perform_async).with(top_note_uri, { prefetched_body: top_object.deep_stringify_keys, request_id: 0 }) + expect(FetchReplyWorker).to have_enqueued_sidekiq_job(top_note_uri, { 'prefetched_body' => top_object.deep_stringify_keys, 'request_id' => 0 }) expect(a_request(:get, top_note_uri)).to have_been_made.once end end From a5a2c6dc7ec0d8af53594cd53a90da7d6fbefd5a Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 16 May 2025 14:24:02 +0200 Subject: [PATCH 4/5] Add support for FASP data sharing (#34415) --- .../v0/backfill_requests_controller.rb | 26 ++++ .../v0/continuations_controller.rb | 10 ++ .../v0/event_subscriptions_controller.rb | 25 ++++ app/lib/fasp/request.rb | 1 + app/models/account.rb | 1 + app/models/concerns/account/fasp_concern.rb | 37 ++++++ app/models/concerns/favourite/fasp_concern.rb | 17 +++ app/models/concerns/status/fasp_concern.rb | 53 ++++++++ app/models/fasp.rb | 2 + app/models/fasp/backfill_request.rb | 67 ++++++++++ app/models/fasp/provider.rb | 2 + app/models/fasp/subscription.rb | 43 ++++++ app/models/favourite.rb | 1 + app/models/status.rb | 3 +- ...announce_account_lifecycle_event_worker.rb | 28 ++++ ...announce_content_lifecycle_event_worker.rb | 28 ++++ app/workers/fasp/announce_trend_worker.rb | 61 +++++++++ app/workers/fasp/backfill_worker.rb | 32 +++++ config/routes/fasp.rb | 10 ++ config/sidekiq.yml | 1 + ...0241213130230_create_fasp_subscriptions.rb | 18 +++ ...103131909_create_fasp_backfill_requests.rb | 15 +++ db/schema.rb | 27 ++++ spec/fabricators/account_fabricator.rb | 1 + .../fasp/backfill_request_fabricator.rb | 9 ++ .../fasp/subscription_fabricator.rb | 8 ++ .../concerns/account/fasp_concern_spec.rb | 83 ++++++++++++ .../concerns/favourite/fasp_concern_spec.rb | 11 ++ .../concerns/status/fasp_concern_spec.rb | 123 ++++++++++++++++++ spec/models/fasp/backfill_request_spec.rb | 93 +++++++++++++ spec/models/fasp/subscription_spec.rb | 33 +++++ .../data_sharing/v0/backfill_requests_spec.rb | 41 ++++++ .../data_sharing/v0/continuations_spec.rb | 22 ++++ .../v0/event_subscriptions_spec.rb | 57 ++++++++ ...nce_account_lifecycle_event_worker_spec.rb | 34 +++++ ...nce_content_lifecycle_event_worker_spec.rb | 34 +++++ .../fasp/announce_trend_worker_spec.rb | 52 ++++++++ spec/workers/fasp/backfill_worker_spec.rb | 32 +++++ 38 files changed, 1140 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb create mode 100644 app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb create mode 100644 app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb create mode 100644 app/models/concerns/account/fasp_concern.rb create mode 100644 app/models/concerns/favourite/fasp_concern.rb create mode 100644 app/models/concerns/status/fasp_concern.rb create mode 100644 app/models/fasp/backfill_request.rb create mode 100644 app/models/fasp/subscription.rb create mode 100644 app/workers/fasp/announce_account_lifecycle_event_worker.rb create mode 100644 app/workers/fasp/announce_content_lifecycle_event_worker.rb create mode 100644 app/workers/fasp/announce_trend_worker.rb create mode 100644 app/workers/fasp/backfill_worker.rb create mode 100644 db/migrate/20241213130230_create_fasp_subscriptions.rb create mode 100644 db/migrate/20250103131909_create_fasp_backfill_requests.rb create mode 100644 spec/fabricators/fasp/backfill_request_fabricator.rb create mode 100644 spec/fabricators/fasp/subscription_fabricator.rb create mode 100644 spec/models/concerns/account/fasp_concern_spec.rb create mode 100644 spec/models/concerns/favourite/fasp_concern_spec.rb create mode 100644 spec/models/concerns/status/fasp_concern_spec.rb create mode 100644 spec/models/fasp/backfill_request_spec.rb create mode 100644 spec/models/fasp/subscription_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb create mode 100644 spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb create mode 100644 spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb create mode 100644 spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb create mode 100644 spec/workers/fasp/announce_trend_worker_spec.rb create mode 100644 spec/workers/fasp/backfill_worker_spec.rb diff --git a/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb new file mode 100644 index 0000000000..c37a94f251 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/backfill_requests_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::BackfillRequestsController < Api::Fasp::BaseController + def create + backfill_request = current_provider.fasp_backfill_requests.new(backfill_request_params) + + respond_to do |format| + format.json do + if backfill_request.save + render json: { backfillRequest: { id: backfill_request.id } }, status: 201 + else + head 422 + end + end + end + end + + private + + def backfill_request_params + params + .permit(:category, :maxCount) + .to_unsafe_h + .transform_keys { |k| k.to_s.underscore } + end +end diff --git a/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb b/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb new file mode 100644 index 0000000000..eff2ac0e21 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/continuations_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::ContinuationsController < Api::Fasp::BaseController + def create + backfill_request = current_provider.fasp_backfill_requests.find(params[:backfill_request_id]) + Fasp::BackfillWorker.perform_async(backfill_request.id) + + head 204 + end +end diff --git a/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb b/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb new file mode 100644 index 0000000000..29e03d5836 --- /dev/null +++ b/app/controllers/api/fasp/data_sharing/v0/event_subscriptions_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::Fasp::DataSharing::V0::EventSubscriptionsController < Api::Fasp::BaseController + def create + subscription = current_provider.fasp_subscriptions.create!(subscription_params) + + render json: { subscription: { id: subscription.id } }, status: 201 + end + + def destroy + subscription = current_provider.fasp_subscriptions.find(params[:id]) + subscription.destroy + + head 204 + end + + private + + def subscription_params + params + .permit(:category, :subscriptionType, :maxBatchSize, threshold: {}) + .to_unsafe_h + .transform_keys { |k| k.to_s.underscore } + end +end diff --git a/app/lib/fasp/request.rb b/app/lib/fasp/request.rb index 2addbe8502..7d8c05d406 100644 --- a/app/lib/fasp/request.rb +++ b/app/lib/fasp/request.rb @@ -32,6 +32,7 @@ class Fasp::Request def request_headers(verb, url, body = '') result = { 'accept' => 'application/json', + 'content-type' => 'application/json', 'content-digest' => content_digest(body), } result.merge(signature_headers(verb, url, result)) diff --git a/app/models/account.rb b/app/models/account.rb index 53bf2407e8..22b8bce601 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -85,6 +85,7 @@ class Account < ApplicationRecord include Account::Associations include Account::Avatar include Account::Counters + include Account::FaspConcern include Account::FinderConcern include Account::Header include Account::Interactions diff --git a/app/models/concerns/account/fasp_concern.rb b/app/models/concerns/account/fasp_concern.rb new file mode 100644 index 0000000000..b18529a3e9 --- /dev/null +++ b/app/models/concerns/account/fasp_concern.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Account::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_new_account_to_subscribed_fasp, on: :create + after_commit :announce_updated_account_to_subscribed_fasp, on: :update + after_commit :announce_deleted_account_to_subscribed_fasp, on: :destroy + end + + private + + def announce_new_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless discoverable? + + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'new') + end + + def announce_updated_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless discoverable? || saved_change_to_discoverable? + + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'update') + end + + def announce_deleted_account_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless discoverable? + + uri = ActivityPub::TagManager.instance.uri_for(self) + Fasp::AnnounceAccountLifecycleEventWorker.perform_async(uri, 'delete') + end +end diff --git a/app/models/concerns/favourite/fasp_concern.rb b/app/models/concerns/favourite/fasp_concern.rb new file mode 100644 index 0000000000..c72e7c3792 --- /dev/null +++ b/app/models/concerns/favourite/fasp_concern.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Favourite::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_trends_to_subscribed_fasp, on: :create + end + + private + + def announce_trends_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + + Fasp::AnnounceTrendWorker.perform_async(status_id, 'favourite') + end +end diff --git a/app/models/concerns/status/fasp_concern.rb b/app/models/concerns/status/fasp_concern.rb new file mode 100644 index 0000000000..9a838c3a6a --- /dev/null +++ b/app/models/concerns/status/fasp_concern.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Status::FaspConcern + extend ActiveSupport::Concern + + included do + after_commit :announce_new_content_to_subscribed_fasp, on: :create + after_commit :announce_updated_content_to_subscribed_fasp, on: :update + after_commit :announce_deleted_content_to_subscribed_fasp, on: :destroy + after_commit :announce_trends_to_subscribed_fasp, on: :create + end + + private + + def announce_new_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless account_indexable? && public_visibility? + + # We need the uri here, but it is set in another `after_commit` + # callback. Hooks included from modules are run before the ones + # in the class itself and can neither be reordered nor is there + # a way to declare dependencies. + store_uri if uri.nil? + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'new') + end + + def announce_updated_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless account_indexable? && public_visibility? + + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'update') + end + + def announce_deleted_content_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless account_indexable? && public_visibility? + + Fasp::AnnounceContentLifecycleEventWorker.perform_async(uri, 'delete') + end + + def announce_trends_to_subscribed_fasp + return unless Mastodon::Feature.fasp_enabled? + return unless account_indexable? + + candidate_id, trend_source = + if reblog_of_id + [reblog_of_id, 'reblog'] + elsif in_reply_to_id + [in_reply_to_id, 'reply'] + end + Fasp::AnnounceTrendWorker.perform_async(candidate_id, trend_source) if candidate_id + end +end diff --git a/app/models/fasp.rb b/app/models/fasp.rb index cb33937715..e4e73a2312 100644 --- a/app/models/fasp.rb +++ b/app/models/fasp.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Fasp + DATA_CATEGORIES = %w(account content).freeze + def self.table_name_prefix 'fasp_' end diff --git a/app/models/fasp/backfill_request.rb b/app/models/fasp/backfill_request.rb new file mode 100644 index 0000000000..e1be611097 --- /dev/null +++ b/app/models/fasp/backfill_request.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fasp_backfill_requests +# +# id :bigint(8) not null, primary key +# category :string not null +# cursor :string +# fulfilled :boolean default(FALSE), not null +# max_count :integer default(100), not null +# created_at :datetime not null +# updated_at :datetime not null +# fasp_provider_id :bigint(8) not null +# +class Fasp::BackfillRequest < ApplicationRecord + belongs_to :fasp_provider, class_name: 'Fasp::Provider' + + validates :category, presence: true, inclusion: Fasp::DATA_CATEGORIES + validates :max_count, presence: true, + numericality: { only_integer: true } + + after_commit :queue_fulfillment_job, on: :create + + def next_objects + @next_objects ||= base_scope.to_a + end + + def next_uris + next_objects.map { |o| ActivityPub::TagManager.instance.uri_for(o) } + end + + def more_objects_available? + return false if next_objects.empty? + + base_scope.where(id: ...(next_objects.last.id)).any? + end + + def advance! + if more_objects_available? + update!(cursor: next_objects.last.id) + else + update!(fulfilled: true) + end + end + + private + + def base_scope + result = category_scope.limit(max_count).order(id: :desc) + result = result.where(id: ...cursor) if cursor.present? + result + end + + def category_scope + case category + when 'account' + Account.discoverable.without_instance_actor + when 'content' + Status.indexable + end + end + + def queue_fulfillment_job + Fasp::BackfillWorker.perform_async(id) + end +end diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb index cd1b3008c7..7926953e6c 100644 --- a/app/models/fasp/provider.rb +++ b/app/models/fasp/provider.rb @@ -22,7 +22,9 @@ class Fasp::Provider < ApplicationRecord include DebugConcern + has_many :fasp_backfill_requests, inverse_of: :fasp_provider, class_name: 'Fasp::BackfillRequest', dependent: :delete_all has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all + has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all validates :name, presence: true validates :base_url, presence: true, url: true diff --git a/app/models/fasp/subscription.rb b/app/models/fasp/subscription.rb new file mode 100644 index 0000000000..e2e554ed74 --- /dev/null +++ b/app/models/fasp/subscription.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: fasp_subscriptions +# +# id :bigint(8) not null, primary key +# category :string not null +# max_batch_size :integer not null +# subscription_type :string not null +# threshold_likes :integer +# threshold_replies :integer +# threshold_shares :integer +# threshold_timeframe :integer +# created_at :datetime not null +# updated_at :datetime not null +# fasp_provider_id :bigint(8) not null +# +class Fasp::Subscription < ApplicationRecord + TYPES = %w(lifecycle trends).freeze + + belongs_to :fasp_provider, class_name: 'Fasp::Provider' + + validates :category, presence: true, inclusion: Fasp::DATA_CATEGORIES + validates :subscription_type, presence: true, + inclusion: TYPES + + scope :category_content, -> { where(category: 'content') } + scope :category_account, -> { where(category: 'account') } + scope :lifecycle, -> { where(subscription_type: 'lifecycle') } + scope :trends, -> { where(subscription_type: 'trends') } + + def threshold=(threshold) + self.threshold_timeframe = threshold['timeframe'] || 15 + self.threshold_shares = threshold['shares'] || 3 + self.threshold_likes = threshold['likes'] || 3 + self.threshold_replies = threshold['replies'] || 3 + end + + def timeframe_start + threshold_timeframe.minutes.ago + end +end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 042f72beae..7bf793e2a1 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,6 +13,7 @@ class Favourite < ApplicationRecord include Paginable + include Favourite::FaspConcern update_index('statuses', :status) diff --git a/app/models/status.rb b/app/models/status.rb index 5e89fc3531..8287583bc3 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -36,6 +36,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable + include Status::FaspConcern include Status::FetchRepliesConcern include Status::SafeReblogInsert include Status::SearchConcern @@ -181,7 +182,7 @@ class Status < ApplicationRecord ], thread: :account - delegate :domain, to: :account, prefix: true + delegate :domain, :indexable?, to: :account, prefix: true REAL_TIME_WINDOW = 6.hours diff --git a/app/workers/fasp/announce_account_lifecycle_event_worker.rb b/app/workers/fasp/announce_account_lifecycle_event_worker.rb new file mode 100644 index 0000000000..ea8544c24d --- /dev/null +++ b/app/workers/fasp/announce_account_lifecycle_event_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Fasp::AnnounceAccountLifecycleEventWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(uri, event_type) + Fasp::Subscription.includes(:fasp_provider).category_account.lifecycle.each do |subscription| + announce(subscription, uri, event_type) + end + end + + private + + def announce(subscription, uri, event_type) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'account', + eventType: event_type, + objectUris: [uri], + }) + end +end diff --git a/app/workers/fasp/announce_content_lifecycle_event_worker.rb b/app/workers/fasp/announce_content_lifecycle_event_worker.rb new file mode 100644 index 0000000000..744528f2d3 --- /dev/null +++ b/app/workers/fasp/announce_content_lifecycle_event_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Fasp::AnnounceContentLifecycleEventWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(uri, event_type) + Fasp::Subscription.includes(:fasp_provider).category_content.lifecycle.each do |subscription| + announce(subscription, uri, event_type) + end + end + + private + + def announce(subscription, uri, event_type) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: event_type, + objectUris: [uri], + }) + end +end diff --git a/app/workers/fasp/announce_trend_worker.rb b/app/workers/fasp/announce_trend_worker.rb new file mode 100644 index 0000000000..ae93c3d9f6 --- /dev/null +++ b/app/workers/fasp/announce_trend_worker.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class Fasp::AnnounceTrendWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(status_id, trend_source) + status = ::Status.includes(:account).find(status_id) + return unless status.account.indexable? + + Fasp::Subscription.includes(:fasp_provider).category_content.trends.each do |subscription| + announce(subscription, status.uri) if trending?(subscription, status, trend_source) + end + rescue ActiveRecord::RecordNotFound + # status might not exist anymore, in which case there is nothing to do + end + + private + + def trending?(subscription, status, trend_source) + scope = scope_for(status, trend_source) + threshold = threshold_for(subscription, trend_source) + scope.where(created_at: subscription.timeframe_start..).count >= threshold + end + + def scope_for(status, trend_source) + case trend_source + when 'favourite' + status.favourites + when 'reblog' + status.reblogs + when 'reply' + status.replies + end + end + + def threshold_for(subscription, trend_source) + case trend_source + when 'favourite' + subscription.threshold_likes + when 'reblog' + subscription.threshold_shares + when 'reply' + subscription.threshold_replies + end + end + + def announce(subscription, uri) + Fasp::Request.new(subscription.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'trending', + objectUris: [uri], + }) + end +end diff --git a/app/workers/fasp/backfill_worker.rb b/app/workers/fasp/backfill_worker.rb new file mode 100644 index 0000000000..4e30b71a7d --- /dev/null +++ b/app/workers/fasp/backfill_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Fasp::BackfillWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp', retry: 5 + + def perform(backfill_request_id) + backfill_request = Fasp::BackfillRequest.find(backfill_request_id) + + announce(backfill_request) + + backfill_request.advance! + rescue ActiveRecord::RecordNotFound + # ignore missing backfill requests + end + + private + + def announce(backfill_request) + Fasp::Request.new(backfill_request.fasp_provider).post('/data_sharing/v0/announcements', body: { + source: { + backfillRequest: { + id: backfill_request.id.to_s, + }, + }, + category: backfill_request.category, + objectUris: backfill_request.next_uris, + moreObjectsAvailable: backfill_request.more_objects_available?, + }) + end +end diff --git a/config/routes/fasp.rb b/config/routes/fasp.rb index 9d052526de..bd2bb4b520 100644 --- a/config/routes/fasp.rb +++ b/config/routes/fasp.rb @@ -10,6 +10,16 @@ namespace :api, format: false do end end + namespace :data_sharing do + namespace :v0 do + resources :backfill_requests, only: [:create] do + resource :continuation, only: [:create] + end + + resources :event_subscriptions, only: [:create, :destroy] + end + end + resource :registration, only: [:create] end end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 488c2f2ab3..9bfc7e9984 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -7,6 +7,7 @@ - [mailers, 2] - [pull] - [scheduler] + - [fasp] :scheduler: :listened_queues_only: true diff --git a/db/migrate/20241213130230_create_fasp_subscriptions.rb b/db/migrate/20241213130230_create_fasp_subscriptions.rb new file mode 100644 index 0000000000..7037022303 --- /dev/null +++ b/db/migrate/20241213130230_create_fasp_subscriptions.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateFaspSubscriptions < ActiveRecord::Migration[7.2] + def change + create_table :fasp_subscriptions do |t| + t.string :category, null: false + t.string :subscription_type, null: false + t.integer :max_batch_size, null: false + t.integer :threshold_timeframe + t.integer :threshold_shares + t.integer :threshold_likes + t.integer :threshold_replies + t.references :fasp_provider, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20250103131909_create_fasp_backfill_requests.rb b/db/migrate/20250103131909_create_fasp_backfill_requests.rb new file mode 100644 index 0000000000..31dcaaa469 --- /dev/null +++ b/db/migrate/20250103131909_create_fasp_backfill_requests.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateFaspBackfillRequests < ActiveRecord::Migration[7.2] + def change + create_table :fasp_backfill_requests do |t| + t.string :category, null: false + t.integer :max_count, null: false, default: 100 + t.string :cursor + t.boolean :fulfilled, null: false, default: false + t.references :fasp_provider, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index db1687ba99..77b5b732d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -445,6 +445,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true end + create_table "fasp_backfill_requests", force: :cascade do |t| + t.string "category", null: false + t.integer "max_count", default: 100, null: false + t.string "cursor" + t.boolean "fulfilled", default: false, null: false + t.bigint "fasp_provider_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["fasp_provider_id"], name: "index_fasp_backfill_requests_on_fasp_provider_id" + end + create_table "fasp_debug_callbacks", force: :cascade do |t| t.bigint "fasp_provider_id", null: false t.string "ip", null: false @@ -471,6 +482,20 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true end + create_table "fasp_subscriptions", force: :cascade do |t| + t.string "category", null: false + t.string "subscription_type", null: false + t.integer "max_batch_size", null: false + t.integer "threshold_timeframe" + t.integer "threshold_shares" + t.integer "threshold_likes" + t.integer "threshold_replies" + t.bigint "fasp_provider_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["fasp_provider_id"], name: "index_fasp_subscriptions_on_fasp_provider_id" + end + create_table "favourites", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false @@ -1322,7 +1347,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do add_foreign_key "custom_filter_statuses", "statuses", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade + add_foreign_key "fasp_backfill_requests", "fasp_providers" add_foreign_key "fasp_debug_callbacks", "fasp_providers" + add_foreign_key "fasp_subscriptions", "fasp_providers" add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade add_foreign_key "featured_tags", "accounts", on_delete: :cascade diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index 534b8ae843..6ec89a1cb6 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -15,4 +15,5 @@ Fabricator(:account) do user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } uri { |attrs| attrs[:domain].nil? ? '' : "https://#{attrs[:domain]}/users/#{attrs[:username]}" } discoverable true + indexable true end diff --git a/spec/fabricators/fasp/backfill_request_fabricator.rb b/spec/fabricators/fasp/backfill_request_fabricator.rb new file mode 100644 index 0000000000..1dd58b0081 --- /dev/null +++ b/spec/fabricators/fasp/backfill_request_fabricator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Fabricator(:fasp_backfill_request, from: 'Fasp::BackfillRequest') do + category 'content' + max_count 10 + cursor nil + fulfilled false + fasp_provider +end diff --git a/spec/fabricators/fasp/subscription_fabricator.rb b/spec/fabricators/fasp/subscription_fabricator.rb new file mode 100644 index 0000000000..6b5fdaaefb --- /dev/null +++ b/spec/fabricators/fasp/subscription_fabricator.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Fabricator(:fasp_subscription, from: 'Fasp::Subscription') do + category 'content' + subscription_type 'lifecycle' + max_batch_size 10 + fasp_provider +end diff --git a/spec/models/concerns/account/fasp_concern_spec.rb b/spec/models/concerns/account/fasp_concern_spec.rb new file mode 100644 index 0000000000..0434689bff --- /dev/null +++ b/spec/models/concerns/account/fasp_concern_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Account::FaspConcern, feature: :fasp do + describe '#create' do + let(:discoverable_attributes) do + Fabricate.attributes_for(:account).except('user_id') + end + let(:undiscoverable_attributes) do + discoverable_attributes.merge('discoverable' => false) + end + + context 'when account is discoverable' do + it 'queues a job to notify provider' do + Account.create(discoverable_attributes) + + expect(Fasp::AnnounceAccountLifecycleEventWorker).to have_enqueued_sidekiq_job + end + end + + context 'when account is not discoverable' do + it 'does not queue a job' do + Account.create(undiscoverable_attributes) + + expect(Fasp::AnnounceAccountLifecycleEventWorker).to_not have_enqueued_sidekiq_job + end + end + end + + describe '#update' do + before do + # Create account and clear sidekiq queue so we only catch + # jobs queued as part of the update + account + Sidekiq::Worker.clear_all + end + + context 'when account is discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { account.touch }.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account was discoverable before' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect do + account.update(discoverable: false) + end.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account has not been discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com', discoverable: false) } + + it 'does not queue a job' do + expect { account.touch }.to_not enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + end + + describe '#destroy' do + context 'when account is discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { account.destroy }.to enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, domain: 'example.com', discoverable: false) } + + it 'does not queue a job' do + expect { account.destroy }.to_not enqueue_sidekiq_job(Fasp::AnnounceAccountLifecycleEventWorker) + end + end + end +end diff --git a/spec/models/concerns/favourite/fasp_concern_spec.rb b/spec/models/concerns/favourite/fasp_concern_spec.rb new file mode 100644 index 0000000000..a56618f1f2 --- /dev/null +++ b/spec/models/concerns/favourite/fasp_concern_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Favourite::FaspConcern, feature: :fasp do + describe '#create' do + it 'queues a job to notify provider' do + expect { Fabricate(:favourite) }.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end +end diff --git a/spec/models/concerns/status/fasp_concern_spec.rb b/spec/models/concerns/status/fasp_concern_spec.rb new file mode 100644 index 0000000000..717a2bbe1a --- /dev/null +++ b/spec/models/concerns/status/fasp_concern_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Status::FaspConcern, feature: :fasp do + describe '#create' do + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + context 'when status is public' do + it 'queues a job to notify provider of new status' do + expect do + Fabricate(:status, account:) + end.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status is not public' do + it 'does not queue a job' do + expect do + Fabricate(:status, account:, visibility: :unlisted) + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status is in reply to another' do + it 'queues a job to notify provider of possible trend' do + parent = Fabricate(:status) + expect do + Fabricate(:status, account:, thread: parent) + end.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end + + context 'when status is a reblog of another' do + it 'queues a job to notify provider of possible trend' do + original = Fabricate(:status, account:) + expect do + Fabricate(:status, account:, reblog: original) + end.to enqueue_sidekiq_job(Fasp::AnnounceTrendWorker) + end + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, indexable: false) } + + it 'does not queue a job' do + expect do + Fabricate(:status, account:) + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + describe '#update' do + before do + # Create status and clear sidekiq queues to only catch + # jobs queued due to the update + status + Sidekiq::Worker.clear_all + end + + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account:, visibility:) } + + context 'when status is public' do + let(:visibility) { :public } + + it 'queues a job to notify provider' do + expect { status.touch }.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when status has not been public' do + let(:visibility) { :unlisted } + + it 'does not queue a job' do + expect do + status.touch + end.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, domain: 'example.com', indexable: false) } + let(:status) { Fabricate(:status, account:) } + + it 'does not queue a job' do + expect { status.touch }.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end + + describe '#destroy' do + let(:status) { Fabricate(:status, account:) } + + before do + # Create status and clear sidekiq queues to only catch + # jobs queued due to the update + status + Sidekiq::Worker.clear_all + end + + context 'when account is indexable' do + let(:account) { Fabricate(:account, domain: 'example.com') } + + it 'queues a job to notify provider' do + expect { status.destroy }.to enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + + context 'when account is not indexable' do + let(:account) { Fabricate(:account, domain: 'example.com', indexable: false) } + + it 'does not queue a job' do + expect { status.destroy }.to_not enqueue_sidekiq_job(Fasp::AnnounceContentLifecycleEventWorker) + end + end + end +end diff --git a/spec/models/fasp/backfill_request_spec.rb b/spec/models/fasp/backfill_request_spec.rb new file mode 100644 index 0000000000..5ea820db1e --- /dev/null +++ b/spec/models/fasp/backfill_request_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::BackfillRequest do + describe '#next_objects' do + let(:account) { Fabricate(:account) } + let!(:statuses) { Fabricate.times(3, :status, account:).sort_by(&:id) } + + context 'with a new backfill request' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + it 'returns the newest two statuses' do + expect(subject.next_objects).to eq [statuses[2], statuses[1]] + end + end + + context 'with cursor set to second newest status' do + subject do + Fabricate(:fasp_backfill_request, max_count: 2, cursor: statuses[1].id) + end + + it 'returns the oldest status' do + expect(subject.next_objects).to eq [statuses[0]] + end + end + + context 'when all statuses are not `indexable`' do + subject { Fabricate(:fasp_backfill_request) } + + let(:account) { Fabricate(:account, indexable: false) } + + it 'returns no statuses' do + expect(subject.next_objects).to be_empty + end + end + end + + describe '#next_uris' do + subject { Fabricate(:fasp_backfill_request) } + + let(:statuses) { Fabricate.times(2, :status) } + + it 'returns uris of the next objects' do + uris = statuses.map(&:uri) + + expect(subject.next_uris).to match_array(uris) + end + end + + describe '#more_objects_available?' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + context 'when more objects are available' do + before { Fabricate.times(3, :status) } + + it 'returns `true`' do + expect(subject.more_objects_available?).to be true + end + end + + context 'when no more objects are available' do + before { Fabricate.times(2, :status) } + + it 'returns `false`' do + expect(subject.more_objects_available?).to be false + end + end + end + + describe '#advance!' do + subject { Fabricate(:fasp_backfill_request, max_count: 2) } + + context 'when more objects are available' do + before { Fabricate.times(3, :status) } + + it 'updates `cursor`' do + expect { subject.advance! }.to change(subject, :cursor) + expect(subject).to be_persisted + end + end + + context 'when no more objects are available' do + before { Fabricate.times(2, :status) } + + it 'sets `fulfilled` to `true`' do + expect { subject.advance! }.to change(subject, :fulfilled) + .from(false).to(true) + expect(subject).to be_persisted + end + end + end +end diff --git a/spec/models/fasp/subscription_spec.rb b/spec/models/fasp/subscription_spec.rb new file mode 100644 index 0000000000..d51759d48f --- /dev/null +++ b/spec/models/fasp/subscription_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::Subscription do + describe '#threshold=' do + subject { described_class.new } + + it 'allows setting all threshold values at once' do + subject.threshold = { + 'timeframe' => 30, + 'shares' => 5, + 'likes' => 8, + 'replies' => 7, + } + + expect(subject.threshold_timeframe).to eq 30 + expect(subject.threshold_shares).to eq 5 + expect(subject.threshold_likes).to eq 8 + expect(subject.threshold_replies).to eq 7 + end + end + + describe '#timeframe_start' do + subject { described_class.new(threshold_timeframe: 45) } + + it 'returns a Time representing the beginning of the timeframe' do + travel_to Time.zone.local(2025, 4, 7, 16, 40) do + expect(subject.timeframe_start).to eq Time.zone.local(2025, 4, 7, 15, 55) + end + end + end +end diff --git a/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb new file mode 100644 index 0000000000..2d1f1d6417 --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/backfill_requests_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::BackfillRequests', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/backfill_requests' do + let(:provider) { Fabricate(:fasp_provider) } + + context 'with valid parameters' do + it 'creates a new backfill request' do + params = { category: 'content', maxCount: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_requests_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json + end.to change(Fasp::BackfillRequest, :count).by(1) + expect(response).to have_http_status(201) + end + end + + context 'with invalid parameters' do + it 'does not create a backfill request' do + params = { category: 'unknown', maxCount: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_requests_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json + end.to_not change(Fasp::BackfillRequest, :count) + expect(response).to have_http_status(422) + end + end + end +end diff --git a/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb new file mode 100644 index 0000000000..59ab44d0c4 --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/continuations_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::Continuations', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/backfill_requests/:id/continuations' do + let(:backfill_request) { Fabricate(:fasp_backfill_request) } + let(:provider) { backfill_request.fasp_provider } + + it 'queues a job to continue the given backfill request' do + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_backfill_request_continuation_url(backfill_request), + method: :post) + + post api_fasp_data_sharing_v0_backfill_request_continuation_path(backfill_request), headers:, as: :json + expect(response).to have_http_status(204) + expect(Fasp::BackfillWorker).to have_enqueued_sidekiq_job(backfill_request.id) + end + end +end diff --git a/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb new file mode 100644 index 0000000000..beab9e326f --- /dev/null +++ b/spec/requests/api/fasp/data_sharing/v0/event_subscriptions_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::Fasp::DataSharing::V0::EventSubscriptions', feature: :fasp do + include ProviderRequestHelper + + describe 'POST /api/fasp/data_sharing/v0/event_subscriptions' do + let(:provider) { Fabricate(:fasp_provider) } + + context 'with valid parameters' do + it 'creates a new subscription' do + params = { category: 'content', subscriptionType: 'lifecycle', maxBatchSize: 10 } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscriptions_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json + end.to change(Fasp::Subscription, :count).by(1) + expect(response).to have_http_status(201) + end + end + + context 'with invalid parameters' do + it 'does not create a subscription' do + params = { category: 'unknown' } + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscriptions_url, + method: :post, + body: params) + + expect do + post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json + end.to_not change(Fasp::Subscription, :count) + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE /api/fasp/data_sharing/v0/event_subscriptions/:id' do + let(:subscription) { Fabricate(:fasp_subscription) } + let(:provider) { subscription.fasp_provider } + + it 'deletes the subscription' do + headers = request_authentication_headers(provider, + url: api_fasp_data_sharing_v0_event_subscription_url(subscription), + method: :delete) + + expect do + delete api_fasp_data_sharing_v0_event_subscription_path(subscription), headers:, as: :json + end.to change(Fasp::Subscription, :count).by(-1) + expect(response).to have_http_status(204) + end + end +end diff --git a/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb new file mode 100644 index 0000000000..0d4a870875 --- /dev/null +++ b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do + include ProviderRequestHelper + + let(:account_uri) { 'https://masto.example.com/accounts/1' } + let(:subscription) do + Fabricate(:fasp_subscription, category: 'account') + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'account', + eventType: 'new', + objectUris: [account_uri], + }) + end + + it 'sends the account uri to subscribed providers' do + described_class.new.perform(account_uri, 'new') + + expect(stubbed_request).to have_been_made + end +end diff --git a/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb new file mode 100644 index 0000000000..60618607c9 --- /dev/null +++ b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do + include ProviderRequestHelper + + let(:status_uri) { 'https://masto.example.com/status/1' } + let(:subscription) do + Fabricate(:fasp_subscription) + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'new', + objectUris: [status_uri], + }) + end + + it 'sends the status uri to subscribed providers' do + described_class.new.perform(status_uri, 'new') + + expect(stubbed_request).to have_been_made + end +end diff --git a/spec/workers/fasp/announce_trend_worker_spec.rb b/spec/workers/fasp/announce_trend_worker_spec.rb new file mode 100644 index 0000000000..799d8a8f48 --- /dev/null +++ b/spec/workers/fasp/announce_trend_worker_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::AnnounceTrendWorker do + include ProviderRequestHelper + + let(:status) { Fabricate(:status) } + let(:subscription) do + Fabricate(:fasp_subscription, + category: 'content', + subscription_type: 'trends', + threshold_timeframe: 15, + threshold_likes: 2) + end + let(:provider) { subscription.fasp_provider } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + subscription: { + id: subscription.id.to_s, + }, + }, + category: 'content', + eventType: 'trending', + objectUris: [status.uri], + }) + end + + context 'when the configured threshold is met' do + before do + Fabricate.times(2, :favourite, status:) + end + + it 'sends the account uri to subscribed providers' do + described_class.new.perform(status.id, 'favourite') + + expect(stubbed_request).to have_been_made + end + end + + context 'when the configured threshold is not met' do + it 'does not notify any provider' do + described_class.new.perform(status.id, 'favourite') + + expect(stubbed_request).to_not have_been_made + end + end +end diff --git a/spec/workers/fasp/backfill_worker_spec.rb b/spec/workers/fasp/backfill_worker_spec.rb new file mode 100644 index 0000000000..43734e02ba --- /dev/null +++ b/spec/workers/fasp/backfill_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Fasp::BackfillWorker do + include ProviderRequestHelper + + let(:backfill_request) { Fabricate(:fasp_backfill_request) } + let(:provider) { backfill_request.fasp_provider } + let(:status) { Fabricate(:status) } + let!(:stubbed_request) do + stub_provider_request(provider, + method: :post, + path: '/data_sharing/v0/announcements', + response_body: { + source: { + backfillRequest: { + id: backfill_request.id.to_s, + }, + }, + category: 'content', + objectUris: [status.uri], + moreObjectsAvailable: false, + }) + end + + it 'sends status uri to provider that requested backfill' do + described_class.new.perform(backfill_request.id) + + expect(stubbed_request).to have_been_made + end +end From c5ded39c0edfc6a3c1244543d5c6cfbf829692e8 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 May 2025 20:29:43 +0200 Subject: [PATCH 5/5] [Glitch] Fix middle button mouse up on status header always opening status in a new tab Port 24d35996907ffcdf393f9938589eedced84353e4 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/components/status.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 18545b79f7..fb62be9b87 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -280,9 +280,8 @@ class Status extends ImmutablePureComponent { } }; - handleMouseUp = e => { + handleHeaderClick = e => { // Only handle clicks on the empty space above the content - if (e.target !== e.currentTarget && e.detail >= 1) { return; } @@ -691,7 +690,7 @@ class Status extends ImmutablePureComponent { {(connectReply || connectUp || connectToRoot) &&
} {(!muted) && ( -
+
{statusAvatar}