mirror of
https://github.com/glitch-soc/mastodon
synced 2025-04-25 01:54:50 +00:00
Merge commit 'b33f9ea60338a78dde2fb7fe3f083c2ffaafcf1f' into glitch-soc/merge-upstream
This commit is contained in:
commit
ace0a3d61f
52 changed files with 1796 additions and 44 deletions
|
@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
|
||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
||||||
# renovate: datasource=docker depName=docker.io/ruby
|
# renovate: datasource=docker depName=docker.io/ruby
|
||||||
ARG RUBY_VERSION="3.4.2"
|
ARG RUBY_VERSION="3.4.3"
|
||||||
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="22"
|
ARG NODE_MAJOR_VERSION="22"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
||||||
- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md)
|
- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md)
|
||||||
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
|
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
|
||||||
|
- [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md): partial support for incoming quote-posts
|
||||||
|
|
||||||
## ActivityPub in Mastodon
|
## ActivityPub in Mastodon
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ GEM
|
||||||
cocoon (1.2.15)
|
cocoon (1.2.15)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.0)
|
connection_pool (2.5.1)
|
||||||
cose (1.3.1)
|
cose (1.3.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 1.0)
|
openssl-signature_algorithm (~> 1.0)
|
||||||
|
@ -495,6 +495,8 @@ GEM
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-sdk (~> 1.2)
|
opentelemetry-sdk (~> 1.2)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
|
opentelemetry-helpers-sql (0.1.1)
|
||||||
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-helpers-sql-obfuscation (0.3.0)
|
opentelemetry-helpers-sql-obfuscation (0.3.0)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_mailer (0.4.0)
|
opentelemetry-instrumentation-action_mailer (0.4.0)
|
||||||
|
@ -548,8 +550,9 @@ GEM
|
||||||
opentelemetry-instrumentation-net_http (0.23.0)
|
opentelemetry-instrumentation-net_http (0.23.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-pg (0.30.0)
|
opentelemetry-instrumentation-pg (0.30.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
|
opentelemetry-helpers-sql
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-rack (0.26.0)
|
opentelemetry-instrumentation-rack (0.26.0)
|
||||||
|
|
|
@ -381,6 +381,8 @@
|
||||||
"generic.saved": "Wedi'i Gadw",
|
"generic.saved": "Wedi'i Gadw",
|
||||||
"getting_started.heading": "Dechrau",
|
"getting_started.heading": "Dechrau",
|
||||||
"hashtag.admin_moderation": "Agor rhyngwyneb cymedroli #{name}",
|
"hashtag.admin_moderation": "Agor rhyngwyneb cymedroli #{name}",
|
||||||
|
"hashtag.browse": "Pori postiadau yn #{hashtag}",
|
||||||
|
"hashtag.browse_from_account": "Pori postiadau gan @{name} yn #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "a {additional}",
|
"hashtag.column_header.tag_mode.all": "a {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "neu {additional}",
|
"hashtag.column_header.tag_mode.any": "neu {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "heb {additional}",
|
"hashtag.column_header.tag_mode.none": "heb {additional}",
|
||||||
|
@ -394,6 +396,7 @@
|
||||||
"hashtag.counter_by_uses": "{count, plural, one {postiad {counter}} other {postiad {counter}}}",
|
"hashtag.counter_by_uses": "{count, plural, one {postiad {counter}} other {postiad {counter}}}",
|
||||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} postiad} other {{counter} postiad}} heddiw",
|
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} postiad} other {{counter} postiad}} heddiw",
|
||||||
"hashtag.follow": "Dilyn hashnod",
|
"hashtag.follow": "Dilyn hashnod",
|
||||||
|
"hashtag.mute": "Tewi #{hashtag}",
|
||||||
"hashtag.unfollow": "Dad-ddilyn hashnod",
|
"hashtag.unfollow": "Dad-ddilyn hashnod",
|
||||||
"hashtags.and_other": "…a {count, plural, other {# arall}}",
|
"hashtags.and_other": "…a {count, plural, other {# arall}}",
|
||||||
"hints.profiles.followers_may_be_missing": "Mae'n bosibl bod dilynwyr y proffil hwn ar goll.",
|
"hints.profiles.followers_may_be_missing": "Mae'n bosibl bod dilynwyr y proffil hwn ar goll.",
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
"account.edit_profile": "Επεξεργασία προφίλ",
|
"account.edit_profile": "Επεξεργασία προφίλ",
|
||||||
"account.enable_notifications": "Ειδοποίησέ με όταν δημοσιεύει ο @{name}",
|
"account.enable_notifications": "Ειδοποίησέ με όταν δημοσιεύει ο @{name}",
|
||||||
"account.endorse": "Προβολή στο προφίλ",
|
"account.endorse": "Προβολή στο προφίλ",
|
||||||
|
"account.featured": "Προτεινόμενα",
|
||||||
|
"account.featured.hashtags": "Ετικέτες",
|
||||||
|
"account.featured.posts": "Αναρτήσεις",
|
||||||
"account.featured_tags.last_status_at": "Τελευταία ανάρτηση στις {date}",
|
"account.featured_tags.last_status_at": "Τελευταία ανάρτηση στις {date}",
|
||||||
"account.featured_tags.last_status_never": "Καμία ανάρτηση",
|
"account.featured_tags.last_status_never": "Καμία ανάρτηση",
|
||||||
"account.follow": "Ακολούθησε",
|
"account.follow": "Ακολούθησε",
|
||||||
|
@ -64,6 +67,7 @@
|
||||||
"account.statuses_counter": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
|
"account.statuses_counter": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
|
||||||
"account.unblock": "Άρση αποκλεισμού @{name}",
|
"account.unblock": "Άρση αποκλεισμού @{name}",
|
||||||
"account.unblock_domain": "Άρση αποκλεισμού του τομέα {domain}",
|
"account.unblock_domain": "Άρση αποκλεισμού του τομέα {domain}",
|
||||||
|
"account.unblock_domain_short": "Άρση αποκλ.",
|
||||||
"account.unblock_short": "Άρση αποκλεισμού",
|
"account.unblock_short": "Άρση αποκλεισμού",
|
||||||
"account.unendorse": "Να μην παρέχεται στο προφίλ",
|
"account.unendorse": "Να μην παρέχεται στο προφίλ",
|
||||||
"account.unfollow": "Άρση ακολούθησης",
|
"account.unfollow": "Άρση ακολούθησης",
|
||||||
|
@ -292,6 +296,7 @@
|
||||||
"emoji_button.search_results": "Αποτελέσματα αναζήτησης",
|
"emoji_button.search_results": "Αποτελέσματα αναζήτησης",
|
||||||
"emoji_button.symbols": "Σύμβολα",
|
"emoji_button.symbols": "Σύμβολα",
|
||||||
"emoji_button.travel": "Ταξίδια & Τοποθεσίες",
|
"emoji_button.travel": "Ταξίδια & Τοποθεσίες",
|
||||||
|
"empty_column.account_featured": "Αυτή η λίστα είναι κενή",
|
||||||
"empty_column.account_hides_collections": "Αυτός ο χρήστης έχει επιλέξει να μην καταστήσει αυτές τις πληροφορίες διαθέσιμες",
|
"empty_column.account_hides_collections": "Αυτός ο χρήστης έχει επιλέξει να μην καταστήσει αυτές τις πληροφορίες διαθέσιμες",
|
||||||
"empty_column.account_suspended": "Λογαριασμός σε αναστολή",
|
"empty_column.account_suspended": "Λογαριασμός σε αναστολή",
|
||||||
"empty_column.account_timeline": "Δεν έχει αναρτήσεις εδώ!",
|
"empty_column.account_timeline": "Δεν έχει αναρτήσεις εδώ!",
|
||||||
|
@ -376,6 +381,8 @@
|
||||||
"generic.saved": "Αποθηκεύτηκε",
|
"generic.saved": "Αποθηκεύτηκε",
|
||||||
"getting_started.heading": "Ας ξεκινήσουμε",
|
"getting_started.heading": "Ας ξεκινήσουμε",
|
||||||
"hashtag.admin_moderation": "Άνοιγμα διεπαφής συντονισμού για το #{name}",
|
"hashtag.admin_moderation": "Άνοιγμα διεπαφής συντονισμού για το #{name}",
|
||||||
|
"hashtag.browse": "Περιήγηση αναρτήσεων με #{hashtag}",
|
||||||
|
"hashtag.browse_from_account": "Περιήγηση αναρτήσεων από @{name} σε #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "και {additional}",
|
"hashtag.column_header.tag_mode.all": "και {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "ή {additional}",
|
"hashtag.column_header.tag_mode.any": "ή {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "χωρίς {additional}",
|
"hashtag.column_header.tag_mode.none": "χωρίς {additional}",
|
||||||
|
@ -389,6 +396,7 @@
|
||||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
|
"hashtag.counter_by_uses": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}}",
|
||||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}} σήμερα",
|
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} ανάρτηση} other {{counter} αναρτήσεις}} σήμερα",
|
||||||
"hashtag.follow": "Παρακολούθηση ετικέτας",
|
"hashtag.follow": "Παρακολούθηση ετικέτας",
|
||||||
|
"hashtag.mute": "Σίγαση #{hashtag}",
|
||||||
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
|
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
|
||||||
"hashtags.and_other": "…και {count, plural, one {}other {# ακόμη}}",
|
"hashtags.and_other": "…και {count, plural, one {}other {# ακόμη}}",
|
||||||
"hints.profiles.followers_may_be_missing": "Μπορεί να λείπουν ακόλουθοι για αυτό το προφίλ.",
|
"hints.profiles.followers_may_be_missing": "Μπορεί να λείπουν ακόλουθοι για αυτό το προφίλ.",
|
||||||
|
@ -871,7 +879,9 @@
|
||||||
"subscribed_languages.target": "Αλλαγή εγγεγραμμένων γλωσσών για {target}",
|
"subscribed_languages.target": "Αλλαγή εγγεγραμμένων γλωσσών για {target}",
|
||||||
"tabs_bar.home": "Αρχική",
|
"tabs_bar.home": "Αρχική",
|
||||||
"tabs_bar.notifications": "Ειδοποιήσεις",
|
"tabs_bar.notifications": "Ειδοποιήσεις",
|
||||||
|
"terms_of_service.effective_as_of": "Ενεργό από {date}",
|
||||||
"terms_of_service.title": "Όροι Παροχής Υπηρεσιών",
|
"terms_of_service.title": "Όροι Παροχής Υπηρεσιών",
|
||||||
|
"terms_of_service.upcoming_changes_on": "Επερχόμενες αλλαγές στις {date}",
|
||||||
"time_remaining.days": "απομένουν {number, plural, one {# ημέρα} other {# ημέρες}}",
|
"time_remaining.days": "απομένουν {number, plural, one {# ημέρα} other {# ημέρες}}",
|
||||||
"time_remaining.hours": "απομένουν {number, plural, one {# ώρα} other {# ώρες}}",
|
"time_remaining.hours": "απομένουν {number, plural, one {# ώρα} other {# ώρες}}",
|
||||||
"time_remaining.minutes": "απομένουν {number, plural, one {# λεπτό} other {# λεπτά}}",
|
"time_remaining.minutes": "απομένουν {number, plural, one {# λεπτό} other {# λεπτά}}",
|
||||||
|
@ -902,6 +912,12 @@
|
||||||
"video.expand": "Επέκταση βίντεο",
|
"video.expand": "Επέκταση βίντεο",
|
||||||
"video.fullscreen": "Πλήρης οθόνη",
|
"video.fullscreen": "Πλήρης οθόνη",
|
||||||
"video.hide": "Απόκρυψη βίντεο",
|
"video.hide": "Απόκρυψη βίντεο",
|
||||||
|
"video.mute": "Σίγαση",
|
||||||
"video.pause": "Παύση",
|
"video.pause": "Παύση",
|
||||||
"video.play": "Αναπαραγωγή"
|
"video.play": "Αναπαραγωγή",
|
||||||
|
"video.skip_backward": "Παράλειψη πίσω",
|
||||||
|
"video.skip_forward": "Παράλειψη εμπρός",
|
||||||
|
"video.unmute": "Άρση σίγασης",
|
||||||
|
"video.volume_down": "Μείωση έντασης",
|
||||||
|
"video.volume_up": "Αύξηση έντασης"
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,7 +382,7 @@
|
||||||
"getting_started.heading": "Primeros pasos",
|
"getting_started.heading": "Primeros pasos",
|
||||||
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
|
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
|
||||||
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
|
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
|
||||||
"hashtag.browse_from_account": "Explorar publicaciones desde @{name} en #{hashtag}",
|
"hashtag.browse_from_account": "Explorar publicaciones de @{name} en #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "y {additional}",
|
"hashtag.column_header.tag_mode.all": "y {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "sin {additional}",
|
"hashtag.column_header.tag_mode.none": "sin {additional}",
|
||||||
|
|
|
@ -382,7 +382,7 @@
|
||||||
"getting_started.heading": "Primeros pasos",
|
"getting_started.heading": "Primeros pasos",
|
||||||
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
|
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
|
||||||
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
|
"hashtag.browse": "Explorar publicaciones en #{hashtag}",
|
||||||
"hashtag.browse_from_account": "Explorar publicaciones desde @{name} en #{hashtag}",
|
"hashtag.browse_from_account": "Explorar publicaciones de @{name} en #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "y {additional}",
|
"hashtag.column_header.tag_mode.all": "y {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "sin {additional}",
|
"hashtag.column_header.tag_mode.none": "sin {additional}",
|
||||||
|
|
|
@ -381,6 +381,8 @@
|
||||||
"generic.saved": "Salvato",
|
"generic.saved": "Salvato",
|
||||||
"getting_started.heading": "Per iniziare",
|
"getting_started.heading": "Per iniziare",
|
||||||
"hashtag.admin_moderation": "Apri l'interfaccia di moderazione per #{name}",
|
"hashtag.admin_moderation": "Apri l'interfaccia di moderazione per #{name}",
|
||||||
|
"hashtag.browse": "Sfoglia i post con #{hashtag}",
|
||||||
|
"hashtag.browse_from_account": "Sfoglia i post da @{name} con #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "e {additional}",
|
"hashtag.column_header.tag_mode.all": "e {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "senza {additional}",
|
"hashtag.column_header.tag_mode.none": "senza {additional}",
|
||||||
|
@ -394,6 +396,7 @@
|
||||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} post}}",
|
"hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} post}}",
|
||||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} post}} oggi",
|
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} post}} oggi",
|
||||||
"hashtag.follow": "Segui l'hashtag",
|
"hashtag.follow": "Segui l'hashtag",
|
||||||
|
"hashtag.mute": "Silenzia #{hashtag}",
|
||||||
"hashtag.unfollow": "Smetti di seguire l'hashtag",
|
"hashtag.unfollow": "Smetti di seguire l'hashtag",
|
||||||
"hashtags.and_other": "…e {count, plural, other {# in più}}",
|
"hashtags.and_other": "…e {count, plural, other {# in più}}",
|
||||||
"hints.profiles.followers_may_be_missing": "I seguaci per questo profilo potrebbero essere mancanti.",
|
"hints.profiles.followers_may_be_missing": "I seguaci per questo profilo potrebbero essere mancanti.",
|
||||||
|
|
|
@ -889,6 +889,8 @@
|
||||||
"video.expand": "Išplėsti vaizdo įrašą",
|
"video.expand": "Išplėsti vaizdo įrašą",
|
||||||
"video.fullscreen": "Visas ekranas",
|
"video.fullscreen": "Visas ekranas",
|
||||||
"video.hide": "Slėpti vaizdo įrašą",
|
"video.hide": "Slėpti vaizdo įrašą",
|
||||||
|
"video.mute": "Išjungti garsą",
|
||||||
"video.pause": "Pristabdyti",
|
"video.pause": "Pristabdyti",
|
||||||
"video.play": "Leisti"
|
"video.play": "Leisti",
|
||||||
|
"video.skip_backward": "Praleisti atgal"
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"account.no_bio": "Geen beschrijving opgegeven.",
|
"account.no_bio": "Geen beschrijving opgegeven.",
|
||||||
"account.open_original_page": "Originele pagina openen",
|
"account.open_original_page": "Originele pagina openen",
|
||||||
"account.posts": "Berichten",
|
"account.posts": "Berichten",
|
||||||
"account.posts_with_replies": "Berichten en reacties",
|
"account.posts_with_replies": "Reacties",
|
||||||
"account.report": "@{name} rapporteren",
|
"account.report": "@{name} rapporteren",
|
||||||
"account.requested": "Wachten op goedkeuring. Klik om het volgverzoek te annuleren",
|
"account.requested": "Wachten op goedkeuring. Klik om het volgverzoek te annuleren",
|
||||||
"account.requested_follow": "{name} wil je graag volgen",
|
"account.requested_follow": "{name} wil je graag volgen",
|
||||||
|
|
|
@ -36,9 +36,9 @@
|
||||||
"account.follow_back": "Fylg tilbake",
|
"account.follow_back": "Fylg tilbake",
|
||||||
"account.followers": "Fylgjarar",
|
"account.followers": "Fylgjarar",
|
||||||
"account.followers.empty": "Ingen fylgjer denne brukaren enno.",
|
"account.followers.empty": "Ingen fylgjer denne brukaren enno.",
|
||||||
"account.followers_counter": "{count, plural, one {{counter} følgjar} other {{counter} følgjarar}}",
|
"account.followers_counter": "{count, plural, one {{counter} fylgjar} other {{counter} fylgjarar}}",
|
||||||
"account.following": "Fylgjer",
|
"account.following": "Fylgjer",
|
||||||
"account.following_counter": "{count, plural, one {{counter} følgjer} other {{counter} følgjer}}",
|
"account.following_counter": "{count, plural, one {{counter} fylgjar} other {{counter} fylgjarar}}",
|
||||||
"account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
|
"account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
|
||||||
"account.go_to_profile": "Gå til profil",
|
"account.go_to_profile": "Gå til profil",
|
||||||
"account.hide_reblogs": "Gøym framhevingar frå @{name}",
|
"account.hide_reblogs": "Gøym framhevingar frå @{name}",
|
||||||
|
@ -67,6 +67,7 @@
|
||||||
"account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
|
"account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
|
||||||
"account.unblock": "Stopp blokkering av @{name}",
|
"account.unblock": "Stopp blokkering av @{name}",
|
||||||
"account.unblock_domain": "Stopp blokkering av domenet {domain}",
|
"account.unblock_domain": "Stopp blokkering av domenet {domain}",
|
||||||
|
"account.unblock_domain_short": "Fjern blokkering",
|
||||||
"account.unblock_short": "Stopp blokkering",
|
"account.unblock_short": "Stopp blokkering",
|
||||||
"account.unendorse": "Ikkje vis på profil",
|
"account.unendorse": "Ikkje vis på profil",
|
||||||
"account.unfollow": "Slutt å fylgja",
|
"account.unfollow": "Slutt å fylgja",
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
"annual_report.summary.thanks": "Takk for at du er med i Mastodon!",
|
"annual_report.summary.thanks": "Takk for at du er med i Mastodon!",
|
||||||
"attachments_list.unprocessed": "(ubehandla)",
|
"attachments_list.unprocessed": "(ubehandla)",
|
||||||
"audio.hide": "Gøym lyd",
|
"audio.hide": "Gøym lyd",
|
||||||
"block_modal.remote_users_caveat": "Me vil be tenaren {domain} om å respektere di avgjerd. Me kan ikkje garantera at det vert gjort, sidan nokre tenarar kan handtera blokkering ulikt. Offentlege innlegg kan framleis vera synlege for ikkje-innlogga brukarar.",
|
"block_modal.remote_users_caveat": "Me vil be tenaren {domain} om å respektera di avgjerd. Me kan ikkje garantera at det vert gjort, sidan nokre tenarar kan handtera blokkering ulikt. Offentlege innlegg kan framleis vera synlege for ikkje-innlogga brukarar.",
|
||||||
"block_modal.show_less": "Vis mindre",
|
"block_modal.show_less": "Vis mindre",
|
||||||
"block_modal.show_more": "Vis meir",
|
"block_modal.show_more": "Vis meir",
|
||||||
"block_modal.they_cant_mention": "Dei kan ikkje nemna eller fylgja deg.",
|
"block_modal.they_cant_mention": "Dei kan ikkje nemna eller fylgja deg.",
|
||||||
|
@ -276,7 +277,7 @@
|
||||||
"domain_pill.who_they_are": "Sidan handtak seier kven nokon er og kvar dei er, kan du interagere med folk på tvers av det sosiale nettverket av <button>plattformar som støttar ActivityPub</button>.",
|
"domain_pill.who_they_are": "Sidan handtak seier kven nokon er og kvar dei er, kan du interagere med folk på tvers av det sosiale nettverket av <button>plattformar som støttar ActivityPub</button>.",
|
||||||
"domain_pill.who_you_are": "Sidan handtaket ditt seier kven du er og kvar du er, kan folk interagere med deg på tvers av det sosiale nettverket av <button>plattformar som støttar ActivityPub</button>.",
|
"domain_pill.who_you_are": "Sidan handtaket ditt seier kven du er og kvar du er, kan folk interagere med deg på tvers av det sosiale nettverket av <button>plattformar som støttar ActivityPub</button>.",
|
||||||
"domain_pill.your_handle": "Handtaket ditt:",
|
"domain_pill.your_handle": "Handtaket ditt:",
|
||||||
"domain_pill.your_server": "Din digitale heim, der alle innlegga dine bur i. Liker du ikkje dette? Byt til ein ny tenar når som helst og ta med fylgjarane dine òg.",
|
"domain_pill.your_server": "Din digitale heim, der alle innlegga dine bur. Liker du ikkje dette? Byt til ein ny tenar når som helst og ta med fylgjarane dine òg.",
|
||||||
"domain_pill.your_username": "Din unike identifikator på denne tenaren. Det er mogleg å finne brukarar med same brukarnamn på forskjellige tenarar.",
|
"domain_pill.your_username": "Din unike identifikator på denne tenaren. Det er mogleg å finne brukarar med same brukarnamn på forskjellige tenarar.",
|
||||||
"embed.instructions": "Bygg inn denne statusen på nettsida di ved å kopiera koden nedanfor.",
|
"embed.instructions": "Bygg inn denne statusen på nettsida di ved å kopiera koden nedanfor.",
|
||||||
"embed.preview": "Slik kjem det til å sjå ut:",
|
"embed.preview": "Slik kjem det til å sjå ut:",
|
||||||
|
@ -380,6 +381,8 @@
|
||||||
"generic.saved": "Lagra",
|
"generic.saved": "Lagra",
|
||||||
"getting_started.heading": "Kom i gang",
|
"getting_started.heading": "Kom i gang",
|
||||||
"hashtag.admin_moderation": "Opne moderasjonsgrensesnitt for #{name}",
|
"hashtag.admin_moderation": "Opne moderasjonsgrensesnitt for #{name}",
|
||||||
|
"hashtag.browse": "Bla gjennom innlegg i #{hashtag}",
|
||||||
|
"hashtag.browse_from_account": "Bla gjennom innlegg frå @{name} i #{hashtag}",
|
||||||
"hashtag.column_header.tag_mode.all": "og {additional}",
|
"hashtag.column_header.tag_mode.all": "og {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "eller {additional}",
|
"hashtag.column_header.tag_mode.any": "eller {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "utan {additional}",
|
"hashtag.column_header.tag_mode.none": "utan {additional}",
|
||||||
|
@ -393,6 +396,7 @@
|
||||||
"hashtag.counter_by_uses": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}}",
|
"hashtag.counter_by_uses": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}}",
|
||||||
"hashtag.counter_by_uses_today": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}} i dag",
|
"hashtag.counter_by_uses_today": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}} i dag",
|
||||||
"hashtag.follow": "Fylg emneknagg",
|
"hashtag.follow": "Fylg emneknagg",
|
||||||
|
"hashtag.mute": "Demp @#{hashtag}",
|
||||||
"hashtag.unfollow": "Slutt å fylgje emneknaggen",
|
"hashtag.unfollow": "Slutt å fylgje emneknaggen",
|
||||||
"hashtags.and_other": "…og {count, plural, one {}other {# fleire}}",
|
"hashtags.and_other": "…og {count, plural, one {}other {# fleire}}",
|
||||||
"hints.profiles.followers_may_be_missing": "Kven som fylgjer denne profilen manglar kanskje.",
|
"hints.profiles.followers_may_be_missing": "Kven som fylgjer denne profilen manglar kanskje.",
|
||||||
|
@ -805,11 +809,11 @@
|
||||||
"server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)",
|
"server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)",
|
||||||
"server_banner.active_users": "aktive brukarar",
|
"server_banner.active_users": "aktive brukarar",
|
||||||
"server_banner.administered_by": "Administrert av:",
|
"server_banner.administered_by": "Administrert av:",
|
||||||
"server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-serverane du kan bruke til å delta i Fødiverset.",
|
"server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-tenarane du kan bruka til å delta i Allheimen.",
|
||||||
"server_banner.server_stats": "Tenarstatistikk:",
|
"server_banner.server_stats": "Tenarstatistikk:",
|
||||||
"sign_in_banner.create_account": "Opprett konto",
|
"sign_in_banner.create_account": "Opprett konto",
|
||||||
"sign_in_banner.follow_anyone": "Følg kven som helst på tvers av Fødiverset og sjå alt i kronologisk rekkjefølgje. Ingen algoritmar, reklamar eller clickbait i sikte.",
|
"sign_in_banner.follow_anyone": "Fylg kven som helst på tvers av Allheimen og sjå alt i kronologisk rekkjefylgje. Ingen algoritmar, reklame eller klikkfeller.",
|
||||||
"sign_in_banner.mastodon_is": "Mastodon er den beste måten å følgje med på det som skjer.",
|
"sign_in_banner.mastodon_is": "Mastodon er den beste måten å fylgja med på det som skjer.",
|
||||||
"sign_in_banner.sign_in": "Logg inn",
|
"sign_in_banner.sign_in": "Logg inn",
|
||||||
"sign_in_banner.sso_redirect": "Logg inn eller registrer deg",
|
"sign_in_banner.sso_redirect": "Logg inn eller registrer deg",
|
||||||
"status.admin_account": "Opne moderasjonsgrensesnitt for @{name}",
|
"status.admin_account": "Opne moderasjonsgrensesnitt for @{name}",
|
||||||
|
@ -875,7 +879,9 @@
|
||||||
"subscribed_languages.target": "Endre abonnerte språk for {target}",
|
"subscribed_languages.target": "Endre abonnerte språk for {target}",
|
||||||
"tabs_bar.home": "Heim",
|
"tabs_bar.home": "Heim",
|
||||||
"tabs_bar.notifications": "Varsel",
|
"tabs_bar.notifications": "Varsel",
|
||||||
|
"terms_of_service.effective_as_of": "I kraft frå {date}",
|
||||||
"terms_of_service.title": "Bruksvilkår",
|
"terms_of_service.title": "Bruksvilkår",
|
||||||
|
"terms_of_service.upcoming_changes_on": "Komande endringar {date}",
|
||||||
"time_remaining.days": "{number, plural, one {# dag} other {# dagar}} igjen",
|
"time_remaining.days": "{number, plural, one {# dag} other {# dagar}} igjen",
|
||||||
"time_remaining.hours": "{number, plural, one {# time} other {# timar}} igjen",
|
"time_remaining.hours": "{number, plural, one {# time} other {# timar}} igjen",
|
||||||
"time_remaining.minutes": "{number, plural, one {# minutt} other {# minutt}} igjen",
|
"time_remaining.minutes": "{number, plural, one {# minutt} other {# minutt}} igjen",
|
||||||
|
@ -906,6 +912,12 @@
|
||||||
"video.expand": "Utvid video",
|
"video.expand": "Utvid video",
|
||||||
"video.fullscreen": "Fullskjerm",
|
"video.fullscreen": "Fullskjerm",
|
||||||
"video.hide": "Gøym video",
|
"video.hide": "Gøym video",
|
||||||
|
"video.mute": "Demp",
|
||||||
"video.pause": "Pause",
|
"video.pause": "Pause",
|
||||||
"video.play": "Spel av"
|
"video.play": "Spel av",
|
||||||
|
"video.skip_backward": "Hopp bakover",
|
||||||
|
"video.skip_forward": "Hopp framover",
|
||||||
|
"video.unmute": "Opphev demping",
|
||||||
|
"video.volume_down": "Volum ned",
|
||||||
|
"video.volume_up": "Volum opp"
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,9 +45,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
@unresolved_mentions = []
|
@unresolved_mentions = []
|
||||||
@silenced_account_ids = []
|
@silenced_account_ids = []
|
||||||
@params = {}
|
@params = {}
|
||||||
|
@quote = nil
|
||||||
|
@quote_uri = nil
|
||||||
|
|
||||||
process_status_params
|
process_status_params
|
||||||
process_tags
|
process_tags
|
||||||
|
process_quote
|
||||||
process_audience
|
process_audience
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
|
@ -55,6 +58,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
attach_tags(@status)
|
attach_tags(@status)
|
||||||
attach_mentions(@status)
|
attach_mentions(@status)
|
||||||
attach_counts(@status)
|
attach_counts(@status)
|
||||||
|
attach_quote(@status)
|
||||||
end
|
end
|
||||||
|
|
||||||
resolve_thread(@status)
|
resolve_thread(@status)
|
||||||
|
@ -189,6 +193,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_quote(status)
|
||||||
|
return if @quote.nil?
|
||||||
|
|
||||||
|
@quote.status = status
|
||||||
|
@quote.save
|
||||||
|
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, request_id: @options[:request_id])
|
||||||
|
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||||
|
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] })
|
||||||
|
end
|
||||||
|
|
||||||
def process_tags
|
def process_tags
|
||||||
return if @object['tag'].nil?
|
return if @object['tag'].nil?
|
||||||
|
|
||||||
|
@ -203,6 +217,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_quote
|
||||||
|
return unless Mastodon::Feature.inbound_quotes_enabled?
|
||||||
|
|
||||||
|
@quote_uri = @status_parser.quote_uri
|
||||||
|
return if @quote_uri.blank?
|
||||||
|
|
||||||
|
approval_uri = @status_parser.quote_approval_uri
|
||||||
|
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||||
|
@quote = Quote.new(account: @account, approval_uri: approval_uri)
|
||||||
|
end
|
||||||
|
|
||||||
def process_hashtag(tag)
|
def process_hashtag(tag)
|
||||||
return if tag['name'].blank?
|
return if tag['name'].blank?
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
if @account.uri == object_uri
|
if @account.uri == object_uri
|
||||||
delete_person
|
delete_person
|
||||||
else
|
else
|
||||||
delete_note
|
delete_object
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_note
|
def delete_object
|
||||||
return if object_uri.nil?
|
return if object_uri.nil?
|
||||||
|
|
||||||
with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
|
with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
|
||||||
|
@ -32,21 +32,38 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
case @object['type']
|
||||||
|
when 'QuoteAuthorization'
|
||||||
|
revoke_quote
|
||||||
|
when 'Note', 'Question'
|
||||||
|
delete_status
|
||||||
|
else
|
||||||
|
delete_status || revoke_quote
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_status
|
||||||
@status = Status.find_by(uri: object_uri, account: @account)
|
@status = Status.find_by(uri: object_uri, account: @account)
|
||||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||||
|
|
||||||
return if @status.nil?
|
return if @status.nil?
|
||||||
|
|
||||||
forwarder.forward! if forwarder.forwardable?
|
forwarder.forward! if forwarder.forwardable?
|
||||||
delete_now!
|
RemoveStatusService.new.call(@status, redraft: false)
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_quote
|
||||||
|
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
|
||||||
|
return if @quote.nil?
|
||||||
|
|
||||||
|
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||||
|
@quote.reject!
|
||||||
end
|
end
|
||||||
|
|
||||||
def forwarder
|
def forwarder
|
||||||
@forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status)
|
@forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_now!
|
|
||||||
RemoveStatusService.new.call(@status, redraft: false)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -103,6 +103,16 @@ class ActivityPub::Parser::StatusParser
|
||||||
@object.dig(:shares, :totalItems)
|
@object.dig(:shares, :totalItems)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote_uri
|
||||||
|
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
|
||||||
|
value_or_id(as_array(@object[key]).first)
|
||||||
|
end.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def quote_approval_uri
|
||||||
|
as_array(@object['quoteAuthorization']).first
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def raw_language_code
|
def raw_language_code
|
||||||
|
|
|
@ -71,6 +71,23 @@ class StatusCacheHydrator
|
||||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
|
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
|
||||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
||||||
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||||
|
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id) if payload[:quote]
|
||||||
|
end
|
||||||
|
|
||||||
|
def hydrate_quote_payload(empty_payload, quote, account_id)
|
||||||
|
# TODO: properly handle quotes, including visibility and access control
|
||||||
|
|
||||||
|
empty_payload.tap do |payload|
|
||||||
|
# Nothing to do if we're in the shallow (depth limit) case
|
||||||
|
next unless payload.key?(:quoted_status)
|
||||||
|
|
||||||
|
# TODO: handle hiding a rendered status or showing a non-rendered status according to visibility
|
||||||
|
if quote&.quoted_status.nil?
|
||||||
|
payload[:quoted_status] = nil
|
||||||
|
elsif payload[:quoted_status].present?
|
||||||
|
payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mapped_applied_custom_filter(account_id, status)
|
def mapped_applied_custom_filter(account_id, status)
|
||||||
|
|
|
@ -26,6 +26,7 @@ module Status::SnapshotConcern
|
||||||
account_id: account_id || self.account_id,
|
account_id: account_id || self.account_id,
|
||||||
content_type: content_type,
|
content_type: content_type,
|
||||||
created_at: at_time || edited_at,
|
created_at: at_time || edited_at,
|
||||||
|
quote_id: quote&.id,
|
||||||
rate_limit: rate_limit
|
rate_limit: rate_limit
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
67
app/models/quote.rb
Normal file
67
app/models/quote.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: quotes
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# activity_uri :string
|
||||||
|
# approval_uri :string
|
||||||
|
# state :integer default("pending"), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# quoted_account_id :bigint(8)
|
||||||
|
# quoted_status_id :bigint(8)
|
||||||
|
# status_id :bigint(8) not null
|
||||||
|
#
|
||||||
|
class Quote < ApplicationRecord
|
||||||
|
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
|
||||||
|
REFRESH_DEADLINE = 6.hours
|
||||||
|
|
||||||
|
enum :state,
|
||||||
|
{ pending: 0, accepted: 1, rejected: 2, revoked: 3 },
|
||||||
|
validate: true
|
||||||
|
|
||||||
|
belongs_to :status
|
||||||
|
belongs_to :quoted_status, class_name: 'Status', optional: true
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :quoted_account, class_name: 'Account', optional: true
|
||||||
|
|
||||||
|
before_validation :set_accounts
|
||||||
|
|
||||||
|
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
|
||||||
|
validate :validate_visibility
|
||||||
|
|
||||||
|
def accept!
|
||||||
|
update!(state: :accepted)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject!
|
||||||
|
if accepted?
|
||||||
|
update!(state: :revoked)
|
||||||
|
elsif !revoked?
|
||||||
|
update!(state: :rejected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_refresh_if_stale!
|
||||||
|
return unless quoted_status_id.present? && approval_uri.present? && updated_at <= BACKGROUND_REFRESH_INTERVAL.ago
|
||||||
|
|
||||||
|
ActivityPub::QuoteRefreshWorker.perform_in(rand(REFRESH_DEADLINE), id)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
self.account = status.account
|
||||||
|
self.quoted_account = quoted_status&.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_visibility
|
||||||
|
return if account_id == quoted_account_id || quoted_status.nil? || quoted_status.distributable?
|
||||||
|
|
||||||
|
errors.add(:quoted_status_id, :visibility_mismatch)
|
||||||
|
end
|
||||||
|
end
|
|
@ -95,6 +95,7 @@ class Status < ApplicationRecord
|
||||||
has_one :status_stat, inverse_of: :status, dependent: nil
|
has_one :status_stat, inverse_of: :status, dependent: nil
|
||||||
has_one :poll, inverse_of: :status, dependent: :destroy
|
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||||
has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil
|
has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil
|
||||||
|
has_one :quote, inverse_of: :status, dependent: :destroy
|
||||||
|
|
||||||
validates :uri, uniqueness: true, presence: true, unless: :local?
|
validates :uri, uniqueness: true, presence: true, unless: :local?
|
||||||
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
||||||
|
@ -161,16 +162,18 @@ class Status < ApplicationRecord
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:tags,
|
:tags,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
|
quote: { status: { account: [:account_stat, user: :role] } },
|
||||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: :account,
|
active_mentions: :account,
|
||||||
reblog: [
|
reblog: [
|
||||||
:application,
|
:application,
|
||||||
:tags,
|
|
||||||
:media_attachments,
|
:media_attachments,
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
|
:tags,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
|
quote: { status: { account: [:account_stat, user: :role] } },
|
||||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: :account,
|
active_mentions: :account,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
# media_descriptions :text is an Array
|
# media_descriptions :text is an Array
|
||||||
# poll_options :string is an Array
|
# poll_options :string is an Array
|
||||||
# sensitive :boolean
|
# sensitive :boolean
|
||||||
|
# quote_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusEdit < ApplicationRecord
|
class StatusEdit < ApplicationRecord
|
||||||
|
|
|
@ -16,11 +16,11 @@ class StatusRelationshipsPresenter
|
||||||
@filters_map = {}
|
@filters_map = {}
|
||||||
else
|
else
|
||||||
statuses = statuses.compact
|
statuses = statuses.compact
|
||||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id, s.proper.quote&.quoted_status_id] }.uniq.compact
|
||||||
conversation_ids = statuses.filter_map(&:conversation_id).uniq
|
conversation_ids = statuses.flat_map { |s| [s.proper.conversation_id, s.proper.quote&.quoted_status&.conversation_id] }.uniq.compact
|
||||||
pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) }
|
pinnable_status_ids = statuses.flat_map { |s| [s.proper, s.proper.quote&.quoted_status] }.compact.filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) }
|
||||||
|
|
||||||
@filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
|
@filters_map = build_filters_map(statuses.flat_map { |s| [s, s.proper.quote&.quoted_status] }.compact.uniq, current_account_id).merge(options[:filters_map] || {})
|
||||||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
||||||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
||||||
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
@bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {})
|
||||||
|
|
25
app/serializers/rest/base_quote_serializer.rb
Normal file
25
app/serializers/rest/base_quote_serializer.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::BaseQuoteSerializer < ActiveModel::Serializer
|
||||||
|
attributes :state
|
||||||
|
|
||||||
|
def state
|
||||||
|
return object.state unless object.accepted?
|
||||||
|
|
||||||
|
# Extra states when a status is unavailable
|
||||||
|
return 'deleted' if object.quoted_status.nil?
|
||||||
|
return 'unauthorized' if status_filter.filtered?
|
||||||
|
|
||||||
|
object.state
|
||||||
|
end
|
||||||
|
|
||||||
|
def quoted_status
|
||||||
|
object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def status_filter
|
||||||
|
@status_filter ||= StatusFilter.new(object.quoted_status, current_user&.account, instance_options[:relationships] || {})
|
||||||
|
end
|
||||||
|
end
|
5
app/serializers/rest/quote_serializer.rb
Normal file
5
app/serializers/rest/quote_serializer.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::QuoteSerializer < REST::BaseQuoteSerializer
|
||||||
|
has_one :quoted_status, serializer: REST::ShallowStatusSerializer
|
||||||
|
end
|
9
app/serializers/rest/shallow_quote_serializer.rb
Normal file
9
app/serializers/rest/shallow_quote_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::ShallowQuoteSerializer < REST::BaseQuoteSerializer
|
||||||
|
attribute :quoted_status_id
|
||||||
|
|
||||||
|
def quoted_status_id
|
||||||
|
quoted_status&.id&.to_s
|
||||||
|
end
|
||||||
|
end
|
9
app/serializers/rest/shallow_status_serializer.rb
Normal file
9
app/serializers/rest/shallow_status_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::ShallowStatusSerializer < REST::StatusSerializer
|
||||||
|
has_one :quote, key: :quote, serializer: REST::ShallowQuoteSerializer
|
||||||
|
|
||||||
|
# It looks like redefining one `has_one` requires redefining all inherited ones
|
||||||
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
|
end
|
|
@ -10,6 +10,8 @@ class REST::StatusEditSerializer < ActiveModel::Serializer
|
||||||
has_many :ordered_media_attachments, key: :media_attachments, serializer: REST::MediaAttachmentSerializer
|
has_many :ordered_media_attachments, key: :media_attachments, serializer: REST::MediaAttachmentSerializer
|
||||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
|
|
||||||
|
has_one :quote, serializer: REST::QuoteSerializer, if: -> { object.quote_id.present? }
|
||||||
|
|
||||||
attribute :poll, if: -> { object.poll_options.present? }
|
attribute :poll, if: -> { object.poll_options.present? }
|
||||||
|
|
||||||
def content
|
def content
|
||||||
|
@ -19,4 +21,8 @@ class REST::StatusEditSerializer < ActiveModel::Serializer
|
||||||
def poll
|
def poll
|
||||||
{ options: object.poll_options.map { |title| { title: title } } }
|
{ options: object.poll_options.map { |title| { title: title } } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def quote
|
||||||
|
object.quote_id == status.quote&.id ? status.quote : Quote.new(state: :pending)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
has_many :tags
|
has_many :tags
|
||||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
|
|
||||||
|
has_one :quote, key: :quote, serializer: REST::QuoteSerializer
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
@account = status.account
|
@account = status.account
|
||||||
@media_attachments_changed = false
|
@media_attachments_changed = false
|
||||||
@poll_changed = false
|
@poll_changed = false
|
||||||
|
@quote_changed = false
|
||||||
@request_id = request_id
|
@request_id = request_id
|
||||||
|
|
||||||
# Only native types can be updated at the moment
|
# Only native types can be updated at the moment
|
||||||
|
@ -158,7 +159,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
@status.sensitive = @account.sensitized? || @status_parser.sensitive || false
|
@status.sensitive = @account.sensitized? || @status_parser.sensitive || false
|
||||||
@status.language = @status_parser.language
|
@status.language = @status_parser.language
|
||||||
|
|
||||||
@significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed
|
@significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed || @quote_changed
|
||||||
|
|
||||||
@status.edited_at = @status_parser.edited_at if significant_changes?
|
@status.edited_at = @status_parser.edited_at if significant_changes?
|
||||||
|
|
||||||
|
@ -183,6 +184,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
update_tags!
|
update_tags!
|
||||||
update_mentions!
|
update_mentions!
|
||||||
update_emojis!
|
update_emojis!
|
||||||
|
update_quote!
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_tags!
|
def update_tags!
|
||||||
|
@ -262,6 +264,45 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_quote!
|
||||||
|
return unless Mastodon::Feature.inbound_quotes_enabled?
|
||||||
|
|
||||||
|
quote = nil
|
||||||
|
quote_uri = @status_parser.quote_uri
|
||||||
|
|
||||||
|
if quote_uri.present?
|
||||||
|
approval_uri = @status_parser.quote_approval_uri
|
||||||
|
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||||
|
|
||||||
|
if @status.quote.present?
|
||||||
|
# If the quoted post has changed, discard the old object and create a new one
|
||||||
|
if @status.quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status) != quote_uri
|
||||||
|
@status.quote.destroy
|
||||||
|
quote = Quote.create(status: @status, approval_uri: approval_uri)
|
||||||
|
@quote_changed = true
|
||||||
|
else
|
||||||
|
quote = @status.quote
|
||||||
|
quote.update(approval_uri: approval_uri, state: :pending) if quote.approval_uri != @status_parser.quote_approval_uri
|
||||||
|
end
|
||||||
|
else
|
||||||
|
quote = Quote.create(status: @status, approval_uri: approval_uri)
|
||||||
|
@quote_changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if quote.present?
|
||||||
|
begin
|
||||||
|
quote.save
|
||||||
|
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, request_id: @request_id)
|
||||||
|
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||||
|
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, quote.id, quote_uri, { 'request_id' => @request_id })
|
||||||
|
end
|
||||||
|
elsif @status.quote.present?
|
||||||
|
@status.quote.destroy!
|
||||||
|
@quote_changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_counts!
|
def update_counts!
|
||||||
likes = @status_parser.favourites_count
|
likes = @status_parser.favourites_count
|
||||||
shares = @status_parser.reblogs_count
|
shares = @status_parser.reblogs_count
|
||||||
|
|
112
app/services/activitypub/verify_quote_service.rb
Normal file
112
app/services/activitypub/verify_quote_service.rb
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::VerifyQuoteService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
# Optionally fetch quoted post, and verify the quote is authorized
|
||||||
|
def call(quote, fetchable_quoted_uri: nil, prefetched_body: nil, request_id: nil)
|
||||||
|
@request_id = request_id
|
||||||
|
@quote = quote
|
||||||
|
@fetching_error = nil
|
||||||
|
|
||||||
|
fetch_quoted_post_if_needed!(fetchable_quoted_uri)
|
||||||
|
return if fast_track_approval! || quote.approval_uri.blank?
|
||||||
|
|
||||||
|
@json = fetch_approval_object(quote.approval_uri, prefetched_body:)
|
||||||
|
return quote.reject! if @json.nil?
|
||||||
|
|
||||||
|
return if non_matching_uri_hosts?(quote.approval_uri, value_or_id(@json['attributedTo']))
|
||||||
|
return unless matching_type? && matching_quote_uri?
|
||||||
|
|
||||||
|
# Opportunistically import embedded posts if needed
|
||||||
|
return if import_quoted_post_if_needed!(fetchable_quoted_uri) && fast_track_approval!
|
||||||
|
|
||||||
|
# Raise an error if we failed to fetch the status
|
||||||
|
raise @fetching_error if @quote.status.nil? && @fetching_error
|
||||||
|
|
||||||
|
return unless matching_quoted_post? && matching_quoted_author?
|
||||||
|
|
||||||
|
quote.accept!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# FEP-044f defines rules that don't require the approval flow
|
||||||
|
def fast_track_approval!
|
||||||
|
return false if @quote.quoted_status_id.blank?
|
||||||
|
|
||||||
|
# Always allow someone to quote themselves
|
||||||
|
if @quote.account_id == @quote.quoted_account_id
|
||||||
|
@quote.accept!
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Always allow someone to quote posts in which they are mentioned
|
||||||
|
if @quote.quoted_status.active_mentions.exists?(mentions: { account_id: @quote.account_id })
|
||||||
|
@quote.accept!
|
||||||
|
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_approval_object(uri, prefetched_body: nil)
|
||||||
|
if prefetched_body.nil?
|
||||||
|
fetch_resource(uri, true, @quote.account.followers.local.first, raise_on_error: :temporary)
|
||||||
|
else
|
||||||
|
body_to_json(prefetched_body, compare_id: uri)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_type?
|
||||||
|
supported_context?(@json) && equals_or_includes?(@json['type'], 'QuoteAuthorization')
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_quote_uri?
|
||||||
|
ActivityPub::TagManager.instance.uri_for(@quote.status) == value_or_id(@json['interactingObject'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_quoted_post_if_needed!(uri)
|
||||||
|
return if uri.nil? || @quote.quoted_status.present?
|
||||||
|
|
||||||
|
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
||||||
|
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id)
|
||||||
|
|
||||||
|
@quote.update(quoted_status: status) if status.present?
|
||||||
|
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||||
|
@fetching_error = e
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_quoted_post_if_needed!(uri)
|
||||||
|
# No need to fetch if we already have a post
|
||||||
|
return if uri.nil? || @quote.quoted_status_id.present? || !@json['interactionTarget'].is_a?(Hash)
|
||||||
|
|
||||||
|
# NOTE: Replacing the object's context by that of the parent activity is
|
||||||
|
# not sound, but it's consistent with the rest of the codebase
|
||||||
|
object = @json['interactionTarget'].merge({ '@context' => @json['@context'] })
|
||||||
|
|
||||||
|
# It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations
|
||||||
|
return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id'])
|
||||||
|
|
||||||
|
status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id)
|
||||||
|
|
||||||
|
if status.present?
|
||||||
|
@quote.update(quoted_status: status)
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_quoted_post?
|
||||||
|
return false if @quote.quoted_status_id.blank?
|
||||||
|
|
||||||
|
ActivityPub::TagManager.instance.uri_for(@quote.quoted_status) == value_or_id(@json['interactionTarget'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_quoted_author?
|
||||||
|
ActivityPub::TagManager.instance.uri_for(@quote.quoted_account) == value_or_id(@json['attributedTo'])
|
||||||
|
end
|
||||||
|
end
|
15
app/workers/activitypub/quote_refresh_worker.rb
Normal file
15
app/workers/activitypub/quote_refresh_worker.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::QuoteRefreshWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull', retry: 3, dead: false, lock: :until_executed, lock_ttl: 1.day.to_i
|
||||||
|
|
||||||
|
def perform(quote_id)
|
||||||
|
quote = Quote.find_by(id: quote_id)
|
||||||
|
return if quote.nil? || quote.updated_at > Quote::BACKGROUND_REFRESH_INTERVAL.ago
|
||||||
|
|
||||||
|
quote.touch
|
||||||
|
ActivityPub::VerifyQuoteService.new.call(quote)
|
||||||
|
end
|
||||||
|
end
|
19
app/workers/activitypub/refetch_and_verify_quote_worker.rb
Normal file
19
app/workers/activitypub/refetch_and_verify_quote_worker.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::RefetchAndVerifyQuoteWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include ExponentialBackoff
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull', retry: 3
|
||||||
|
|
||||||
|
def perform(quote_id, quoted_uri, options = {})
|
||||||
|
quote = Quote.find(quote_id)
|
||||||
|
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
# Do nothing
|
||||||
|
true
|
||||||
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
raise e unless response_error_unsalvageable?(e.response)
|
||||||
|
end
|
||||||
|
end
|
|
@ -55,6 +55,8 @@ el:
|
||||||
too_soon: είναι πολύ σύντομα, πρέπει να είναι μετά από %{date}
|
too_soon: είναι πολύ σύντομα, πρέπει να είναι μετά από %{date}
|
||||||
user:
|
user:
|
||||||
attributes:
|
attributes:
|
||||||
|
date_of_birth:
|
||||||
|
below_limit: είναι κάτω από το όριο ηλικίας
|
||||||
email:
|
email:
|
||||||
blocked: χρησιμοποιεί μη επιτρεπόμενο πάροχο e-mail
|
blocked: χρησιμοποιεί μη επιτρεπόμενο πάροχο e-mail
|
||||||
unreachable: δεν φαίνεται να υπάρχει
|
unreachable: δεν φαίνεται να υπάρχει
|
||||||
|
|
|
@ -49,8 +49,14 @@ nn:
|
||||||
attributes:
|
attributes:
|
||||||
reblog:
|
reblog:
|
||||||
taken: av innlegg eksisterer allereie
|
taken: av innlegg eksisterer allereie
|
||||||
|
terms_of_service:
|
||||||
|
attributes:
|
||||||
|
effective_date:
|
||||||
|
too_soon: er for snart, må vera seinare enn %{date}
|
||||||
user:
|
user:
|
||||||
attributes:
|
attributes:
|
||||||
|
date_of_birth:
|
||||||
|
below_limit: er under aldersgrensa
|
||||||
email:
|
email:
|
||||||
blocked: bruker ein forboden epostleverandør
|
blocked: bruker ein forboden epostleverandør
|
||||||
unreachable: ser ikkje ut til å eksistere
|
unreachable: ser ikkje ut til å eksistere
|
||||||
|
|
|
@ -331,6 +331,7 @@ cy:
|
||||||
create: Creu cyhoeddiad
|
create: Creu cyhoeddiad
|
||||||
title: Cyhoeddiad newydd
|
title: Cyhoeddiad newydd
|
||||||
preview:
|
preview:
|
||||||
|
disclaimer: Gan nad oes modd i ddefnyddwyr eu hosgoi, dylai hysbysiadau e-bost gael eu cyfyngu i gyhoeddiadau pwysig fel tor-data personol neu hysbysiadau cau gweinydd.
|
||||||
explanation_html: 'Bydd yr e-bost yn cael ei anfon at <strong>%{display_count} defnyddiwr</strong> . Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:'
|
explanation_html: 'Bydd yr e-bost yn cael ei anfon at <strong>%{display_count} defnyddiwr</strong> . Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:'
|
||||||
title: Hysbysiad rhagolwg cyhoeddiad
|
title: Hysbysiad rhagolwg cyhoeddiad
|
||||||
publish: Cyhoeddi
|
publish: Cyhoeddi
|
||||||
|
|
|
@ -319,6 +319,7 @@ el:
|
||||||
create: Δημιουργία ανακοίνωσης
|
create: Δημιουργία ανακοίνωσης
|
||||||
title: Νέα ανακοίνωση
|
title: Νέα ανακοίνωση
|
||||||
preview:
|
preview:
|
||||||
|
disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω ηλεκτρονικού ταχυδρομείου θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή.
|
||||||
explanation_html: 'Το email θα αποσταλεί σε <strong>%{display_count} χρήστες</strong>. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:'
|
explanation_html: 'Το email θα αποσταλεί σε <strong>%{display_count} χρήστες</strong>. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:'
|
||||||
title: Προεπισκόπηση ειδοποίησης ανακοίνωσης
|
title: Προεπισκόπηση ειδοποίησης ανακοίνωσης
|
||||||
publish: Δημοσίευση
|
publish: Δημοσίευση
|
||||||
|
@ -507,6 +508,8 @@ el:
|
||||||
select_capabilities: Επέλεξε Δυνατότητες
|
select_capabilities: Επέλεξε Δυνατότητες
|
||||||
sign_in: Σύνδεση
|
sign_in: Σύνδεση
|
||||||
status: Κατάσταση
|
status: Κατάσταση
|
||||||
|
title: Πάροχοι Δευτερεύουσας Υπηρεσίας Fediverse
|
||||||
|
title: FASP
|
||||||
follow_recommendations:
|
follow_recommendations:
|
||||||
description_html: "<strong>Ακολουθώντας συστάσεις βοηθάει τους νέους χρήστες να βρουν γρήγορα ενδιαφέρον περιεχόμενο</strong>. Όταν ένας χρήστης δεν έχει αλληλεπιδράσει με άλλους αρκετά για να διαμορφώσει εξατομικευμένες συστάσεις, συνιστώνται αυτοί οι λογαριασμοί. Υπολογίζονται εκ νέου σε καθημερινή βάση από ένα σύνολο λογαριασμών με τις υψηλότερες πρόσφατες αλληλεπιδράσεις και μεγαλύτερο αριθμό τοπικών ακόλουθων για μια δεδομένη γλώσσα."
|
description_html: "<strong>Ακολουθώντας συστάσεις βοηθάει τους νέους χρήστες να βρουν γρήγορα ενδιαφέρον περιεχόμενο</strong>. Όταν ένας χρήστης δεν έχει αλληλεπιδράσει με άλλους αρκετά για να διαμορφώσει εξατομικευμένες συστάσεις, συνιστώνται αυτοί οι λογαριασμοί. Υπολογίζονται εκ νέου σε καθημερινή βάση από ένα σύνολο λογαριασμών με τις υψηλότερες πρόσφατες αλληλεπιδράσεις και μεγαλύτερο αριθμό τοπικών ακόλουθων για μια δεδομένη γλώσσα."
|
||||||
language: Για τη γλώσσα
|
language: Για τη γλώσσα
|
||||||
|
@ -971,6 +974,7 @@ el:
|
||||||
chance_to_review_html: "<strong>Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα.</strong> Θα έχεις την ευκαιρία να εξετάσεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις."
|
chance_to_review_html: "<strong>Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα.</strong> Θα έχεις την ευκαιρία να εξετάσεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις."
|
||||||
explanation_html: Το πρότυπο όρων υπηρεσίας που παρέχονται είναι μόνο για ενημερωτικούς σκοπούς και δε θα πρέπει να ερμηνεύονται ως νομικές συμβουλές για οποιοδήποτε θέμα. Παρακαλούμε συμβουλέψου τον νομικό σου σύμβουλο σχετικά με την περίπτωσή σου και τις συγκεκριμένες νομικές ερωτήσεις που έχεις.
|
explanation_html: Το πρότυπο όρων υπηρεσίας που παρέχονται είναι μόνο για ενημερωτικούς σκοπούς και δε θα πρέπει να ερμηνεύονται ως νομικές συμβουλές για οποιοδήποτε θέμα. Παρακαλούμε συμβουλέψου τον νομικό σου σύμβουλο σχετικά με την περίπτωσή σου και τις συγκεκριμένες νομικές ερωτήσεις που έχεις.
|
||||||
title: Ρύθμιση Όρων Παροχής Υπηρεσιών
|
title: Ρύθμιση Όρων Παροχής Υπηρεσιών
|
||||||
|
going_live_on_html: Ενεργό, σε ισχύ από %{date}
|
||||||
history: Ιστορικό
|
history: Ιστορικό
|
||||||
live: Ενεργό
|
live: Ενεργό
|
||||||
no_history: Δεν υπάρχουν ακόμα καταγεγραμμένες αλλαγές στους όρους παροχής υπηρεσιών.
|
no_history: Δεν υπάρχουν ακόμα καταγεγραμμένες αλλαγές στους όρους παροχής υπηρεσιών.
|
||||||
|
@ -1936,6 +1940,10 @@ el:
|
||||||
recovery_instructions_html: Αν ποτέ δεν έχεις πρόσβαση στο κινητό σου, μπορείς να χρησιμοποιήσεις έναν από τους παρακάτω κωδικούς ανάκτησης για να αποκτήσεις πρόσβαση στο λογαριασμό σου. <strong>Διαφύλαξε τους κωδικούς ανάκτησης</strong>. Για παράδειγμα, μπορείς να τους εκτυπώσεις και να τους φυλάξεις μαζί με άλλα σημαντικά σου έγγραφα.
|
recovery_instructions_html: Αν ποτέ δεν έχεις πρόσβαση στο κινητό σου, μπορείς να χρησιμοποιήσεις έναν από τους παρακάτω κωδικούς ανάκτησης για να αποκτήσεις πρόσβαση στο λογαριασμό σου. <strong>Διαφύλαξε τους κωδικούς ανάκτησης</strong>. Για παράδειγμα, μπορείς να τους εκτυπώσεις και να τους φυλάξεις μαζί με άλλα σημαντικά σου έγγραφα.
|
||||||
webauthn: Κλειδιά ασφαλείας
|
webauthn: Κλειδιά ασφαλείας
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
announcement_published:
|
||||||
|
description: 'Οι διαχειριστές του %{domain} κάνουν μια ανακοίνωση:'
|
||||||
|
subject: Ανακοίνωση διακομιστή
|
||||||
|
title: Ανακοίνωση διακομιστή %{domain}
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Ρυθμίσεις Λογαριασμού
|
action: Ρυθμίσεις Λογαριασμού
|
||||||
explanation: Η έφεση του παραπτώματος εναντίον του λογαριασμού σου στις %{strike_date}, που υπέβαλες στις %{appeal_date} έχει εγκριθεί. Ο λογαριασμός σου είναι και πάλι σε καλή κατάσταση.
|
explanation: Η έφεση του παραπτώματος εναντίον του λογαριασμού σου στις %{strike_date}, που υπέβαλες στις %{appeal_date} έχει εγκριθεί. Ο λογαριασμός σου είναι και πάλι σε καλή κατάσταση.
|
||||||
|
@ -1968,6 +1976,8 @@ el:
|
||||||
terms_of_service_changed:
|
terms_of_service_changed:
|
||||||
agreement: Συνεχίζοντας να χρησιμοποιείς το %{domain}, συμφωνείς με αυτούς τους όρους. Αν διαφωνείς με τους ενημερωμένους όρους, μπορείς να τερματίσεις τη συμφωνία σου με το %{domain} ανά πάσα στιγμή διαγράφοντας τον λογαριασμό σου.
|
agreement: Συνεχίζοντας να χρησιμοποιείς το %{domain}, συμφωνείς με αυτούς τους όρους. Αν διαφωνείς με τους ενημερωμένους όρους, μπορείς να τερματίσεις τη συμφωνία σου με το %{domain} ανά πάσα στιγμή διαγράφοντας τον λογαριασμό σου.
|
||||||
changelog: 'Με μια ματιά, αυτό σημαίνει αυτή η ενημέρωση για σένα:'
|
changelog: 'Με μια ματιά, αυτό σημαίνει αυτή η ενημέρωση για σένα:'
|
||||||
|
description: 'Λαμβάνεις αυτό το email επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Αυτές οι ενημερώσεις θα τεθούν σε ισχύ στις %{date}. Σε ενθαρρύνουμε να εξετάσεις πλήρως τους ενημερωμένους όρους εδώ:'
|
||||||
|
description_html: Λαμβάνεις αυτό το email επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Αυτές οι ενημερώσεις θα τεθούν σε ισχύ στις <strong>%{date}</strong>. Σε ενθαρρύνουμε να εξετάσεις πλήρως τους <a href="%{path}" target="_blank">ενημερωμένους όρους εδώ</a>.
|
||||||
sign_off: Η ομάδα του %{domain}
|
sign_off: Η ομάδα του %{domain}
|
||||||
subject: Ενημερώσεις στους όρους παροχής υπηρεσιών μας
|
subject: Ενημερώσεις στους όρους παροχής υπηρεσιών μας
|
||||||
subtitle: Οι όροι παροχής υπηρεσιών του %{domain} αλλάζουν
|
subtitle: Οι όροι παροχής υπηρεσιών του %{domain} αλλάζουν
|
||||||
|
|
|
@ -319,6 +319,7 @@ it:
|
||||||
create: Crea annuncio
|
create: Crea annuncio
|
||||||
title: Nuovo annuncio
|
title: Nuovo annuncio
|
||||||
preview:
|
preview:
|
||||||
|
disclaimer: Poiché gli utenti non possono disattivarle, le notifiche e-mail dovrebbero essere limitate ad annunci importanti, come notifiche di violazione dei dati personali o di chiusura del server.
|
||||||
explanation_html: 'L''e-mail verrà inviata a <strong>%{display_count} utenti</strong>. Il seguente testo sarà incluso nell''e-mail:'
|
explanation_html: 'L''e-mail verrà inviata a <strong>%{display_count} utenti</strong>. Il seguente testo sarà incluso nell''e-mail:'
|
||||||
title: Anteprima della notifica dell'annuncio
|
title: Anteprima della notifica dell'annuncio
|
||||||
publish: Pubblica
|
publish: Pubblica
|
||||||
|
|
|
@ -2056,7 +2056,10 @@ lv:
|
||||||
edit_profile_title: Pielāgo savu profilu
|
edit_profile_title: Pielāgo savu profilu
|
||||||
explanation: Šeit ir daži padomi, kā sākt darbu
|
explanation: Šeit ir daži padomi, kā sākt darbu
|
||||||
feature_action: Uzzināt vairāk
|
feature_action: Uzzināt vairāk
|
||||||
|
feature_audience_title: Veido savu sekotāju pulku ar pārliecību
|
||||||
|
feature_control_title: Turi savu laika joslu savā pārvaldībā
|
||||||
feature_creativity: Mastodon nodrošina skaņas, video un attēlu ierakstus, pieejamības aprakstus, aptaujas, satura brīdinājumus, animētus profila attēlus, pielāgotas emocijzīmes, sīktēlu apgriešanas vadīklas un vēl, lai palīdzētu Tev sevi izpaust tiešsaistē. Vai Tu izplati savu mākslu, mūziku vai aplādes, Mastodon ir šeit ar Tevi.
|
feature_creativity: Mastodon nodrošina skaņas, video un attēlu ierakstus, pieejamības aprakstus, aptaujas, satura brīdinājumus, animētus profila attēlus, pielāgotas emocijzīmes, sīktēlu apgriešanas vadīklas un vēl, lai palīdzētu Tev sevi izpaust tiešsaistē. Vai Tu izplati savu mākslu, mūziku vai aplādes, Mastodon ir šeit ar Tevi.
|
||||||
|
feature_creativity_title: Nepārspējams radošums
|
||||||
feature_moderation: Mastodon nodod lēmumu pieņemšanu atpakaļ Tavās rokās. Katrs serveris izveido savus noteikumus un nosacījumus, kas tiek nodrošināti vietēji, ne kā lieliem uzņēmumiem piederošos sabiedriskajos medijiem, padarot katru serveri par vispielāgojamāko un visatsaucīgāko dažādu cilvēku kopu vajadzībām. Pievienojies serverim, kura noteikumiem Tu piekrīti, vai izvieto savu!
|
feature_moderation: Mastodon nodod lēmumu pieņemšanu atpakaļ Tavās rokās. Katrs serveris izveido savus noteikumus un nosacījumus, kas tiek nodrošināti vietēji, ne kā lieliem uzņēmumiem piederošos sabiedriskajos medijiem, padarot katru serveri par vispielāgojamāko un visatsaucīgāko dažādu cilvēku kopu vajadzībām. Pievienojies serverim, kura noteikumiem Tu piekrīti, vai izvieto savu!
|
||||||
feature_moderation_title: Satura pārraudzība, kādai tai būtu jābūt
|
feature_moderation_title: Satura pārraudzība, kādai tai būtu jābūt
|
||||||
follow_action: Sekot
|
follow_action: Sekot
|
||||||
|
|
|
@ -21,7 +21,7 @@ nn:
|
||||||
one: Tut
|
one: Tut
|
||||||
other: Tut
|
other: Tut
|
||||||
posts_tab_heading: Tut
|
posts_tab_heading: Tut
|
||||||
self_follow_error: Det er ikkje tillate å følgje din eigen konto
|
self_follow_error: Du kan ikkje fylgja deg sjølv
|
||||||
admin:
|
admin:
|
||||||
account_actions:
|
account_actions:
|
||||||
action: Utfør
|
action: Utfør
|
||||||
|
@ -309,6 +309,7 @@ nn:
|
||||||
title: Revisionslogg
|
title: Revisionslogg
|
||||||
unavailable_instance: "(domenenamn er utilgjengeleg)"
|
unavailable_instance: "(domenenamn er utilgjengeleg)"
|
||||||
announcements:
|
announcements:
|
||||||
|
back: Tilbake til kunngjeringane
|
||||||
destroyed_msg: Kunngjøringen er slettet!
|
destroyed_msg: Kunngjøringen er slettet!
|
||||||
edit:
|
edit:
|
||||||
title: Rediger kunngjøring
|
title: Rediger kunngjøring
|
||||||
|
@ -317,6 +318,10 @@ nn:
|
||||||
new:
|
new:
|
||||||
create: Lag kunngjøring
|
create: Lag kunngjøring
|
||||||
title: Ny kunngjøring
|
title: Ny kunngjøring
|
||||||
|
preview:
|
||||||
|
disclaimer: Av di folk ikkje kan velja bort epostvarsel, bør du avgrensa dei til viktige kunngjeringar som datainnbrot eller varsel om at tenaren skal stengja.
|
||||||
|
explanation_html: 'Denne eposten blir send til <strong>%{display_count} folk</strong>. Denne teksten vil stå i eposten:'
|
||||||
|
title: Førehandsvis kunngjeringa
|
||||||
publish: Publiser
|
publish: Publiser
|
||||||
published_msg: Kunngjøring publisert!
|
published_msg: Kunngjøring publisert!
|
||||||
scheduled_for: Planlagt for %{time}
|
scheduled_for: Planlagt for %{time}
|
||||||
|
@ -475,11 +480,41 @@ nn:
|
||||||
new:
|
new:
|
||||||
title: Importer domeneblokkeringar
|
title: Importer domeneblokkeringar
|
||||||
no_file: Inga fil vald
|
no_file: Inga fil vald
|
||||||
|
fasp:
|
||||||
|
debug:
|
||||||
|
callbacks:
|
||||||
|
created_at: Oppretta
|
||||||
|
delete: Slett
|
||||||
|
ip: IP-adresse
|
||||||
|
request_body: Meldingskropp i førespurnaden
|
||||||
|
title: Avlusingstilbakekall
|
||||||
|
providers:
|
||||||
|
active: Aktiv
|
||||||
|
base_url: Basisadresse
|
||||||
|
callback: Tilbakekall
|
||||||
|
delete: Slett
|
||||||
|
edit: Rediger leverandør
|
||||||
|
finish_registration: Fullfør registrering
|
||||||
|
name: Namn
|
||||||
|
providers: Leverandørar
|
||||||
|
public_key_fingerprint: Offentleg nøkkelavtrykk
|
||||||
|
registration_requested: Nokon vil registrera seg
|
||||||
|
registrations:
|
||||||
|
confirm: Stadfest
|
||||||
|
description: Du har fått ei registrering frå ein tenesteleverandør. Avslå registreringa viss du ikkje sette i gang dette. Viss du sette i gang registreringa, må du samanlikna namnet og nøkkelavtrykket nøye før du stadfestar registreringa.
|
||||||
|
reject: Avslå
|
||||||
|
title: Stadfest registrering via tenestetilbydar
|
||||||
|
save: Lagre
|
||||||
|
select_capabilities: Vel eigenskapar
|
||||||
|
sign_in: Logg inn
|
||||||
|
status: Status
|
||||||
|
title: Tilleggstenesteleverandørar for allheimen
|
||||||
|
title: Tenestleverandør
|
||||||
follow_recommendations:
|
follow_recommendations:
|
||||||
description_html: "<strong>Fylgjeforslag hjelper nye brukarar å finna interessant innhald raskt</strong>. Om ein brukar ikkje har samhandla nok med andre til å få tilpassa fylgjeforslag, blir desse kontoane føreslått i staden. Dei blir rekna ut på nytt kvar dag ut frå ei blanding av kva kontoar som har mykje nyleg aktivitet og høgast tal på fylgjarar på eit bestemt språk."
|
description_html: "<strong>Fylgjeforslag hjelper nye brukarar å finna interessant innhald raskt</strong>. Om ein brukar ikkje har samhandla nok med andre til å få tilpassa fylgjeforslag, blir desse kontoane føreslått i staden. Dei blir rekna ut på nytt kvar dag ut frå ei blanding av kva kontoar som har mykje nyleg aktivitet og høgast tal på fylgjarar på eit bestemt språk."
|
||||||
language: For språk
|
language: For språk
|
||||||
status: Status
|
status: Status
|
||||||
suppress: Demp følgjeforslag
|
suppress: Demp fylgjeforslag
|
||||||
suppressed: Dempa
|
suppressed: Dempa
|
||||||
title: Fylgjeforslag
|
title: Fylgjeforslag
|
||||||
unsuppress: Nullstill fylgjeforslag
|
unsuppress: Nullstill fylgjeforslag
|
||||||
|
@ -939,6 +974,7 @@ nn:
|
||||||
chance_to_review_html: "<strong>Dei genererte bruksvilkåra blir ikkje lagde ut automatisk</strong> Du får høve til å sjå gjennom resultatet og fylla inn dei detaljane som trengst."
|
chance_to_review_html: "<strong>Dei genererte bruksvilkåra blir ikkje lagde ut automatisk</strong> Du får høve til å sjå gjennom resultatet og fylla inn dei detaljane som trengst."
|
||||||
explanation_html: Malen for bruksvilkår er berre til informasjon, og du bør ikkje gå ut frå han som juridiske råd. Viss du har spørsmål om lovverk, bør du spørja ein advokat.
|
explanation_html: Malen for bruksvilkår er berre til informasjon, og du bør ikkje gå ut frå han som juridiske råd. Viss du har spørsmål om lovverk, bør du spørja ein advokat.
|
||||||
title: Oppsett for bruksvilkår
|
title: Oppsett for bruksvilkår
|
||||||
|
going_live_on_html: I bruk frå %{date}
|
||||||
history: Historikk
|
history: Historikk
|
||||||
live: Direkte
|
live: Direkte
|
||||||
no_history: Det er ikkje registrert nokon endringar i bruksvilkåra enno.
|
no_history: Det er ikkje registrert nokon endringar i bruksvilkåra enno.
|
||||||
|
@ -1681,9 +1717,9 @@ nn:
|
||||||
confirm_remove_selected_follows: Er du sikker på at du ikkje vil fylgja desse?
|
confirm_remove_selected_follows: Er du sikker på at du ikkje vil fylgja desse?
|
||||||
dormant: I dvale
|
dormant: I dvale
|
||||||
follow_failure: Greidde ikkje fylgja alle kontoane du valde.
|
follow_failure: Greidde ikkje fylgja alle kontoane du valde.
|
||||||
follow_selected_followers: Følg valgte tilhengere
|
follow_selected_followers: Fylg desse som fylgjer deg
|
||||||
followers: Fylgjarar
|
followers: Fylgjarar
|
||||||
following: Følginger
|
following: Folk du fylgjer
|
||||||
invited: Innboden
|
invited: Innboden
|
||||||
last_active: Sist aktiv
|
last_active: Sist aktiv
|
||||||
most_recent: Sist
|
most_recent: Sist
|
||||||
|
@ -1904,6 +1940,10 @@ nn:
|
||||||
recovery_instructions_html: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. <strong>Oppbevar gjenopprettingskodene sikkert</strong>, for eksempel ved å skrive dem ut og gjemme dem på et lurt sted bare du vet om.
|
recovery_instructions_html: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. <strong>Oppbevar gjenopprettingskodene sikkert</strong>, for eksempel ved å skrive dem ut og gjemme dem på et lurt sted bare du vet om.
|
||||||
webauthn: Sikkerhetsnøkler
|
webauthn: Sikkerhetsnøkler
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
announcement_published:
|
||||||
|
description: 'Styrarane på %{domain} har ei kunngjering:'
|
||||||
|
subject: Kunngjering om tenesta
|
||||||
|
title: Kunngjering frå %{domain}
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Kontoinnstillingar
|
action: Kontoinnstillingar
|
||||||
explanation: Apellen på prikken mot din kontor på %{strike_date} som du la inn på %{appeal_date} har blitt godkjend. Din konto er nok ein gong i god stand.
|
explanation: Apellen på prikken mot din kontor på %{strike_date} som du la inn på %{appeal_date} har blitt godkjend. Din konto er nok ein gong i god stand.
|
||||||
|
@ -1936,6 +1976,8 @@ nn:
|
||||||
terms_of_service_changed:
|
terms_of_service_changed:
|
||||||
agreement: Viss du held fram å bruka %{domain}, seier du deg einig i vilkåra. Viss du er usamd i dei oppdaterte vilkåra, kan du slutta å bruka %{domain} når du vil ved å sletta brukarkontoen din.
|
agreement: Viss du held fram å bruka %{domain}, seier du deg einig i vilkåra. Viss du er usamd i dei oppdaterte vilkåra, kan du slutta å bruka %{domain} når du vil ved å sletta brukarkontoen din.
|
||||||
changelog: 'Denne oppdateringa, kort fortalt:'
|
changelog: 'Denne oppdateringa, kort fortalt:'
|
||||||
|
description: 'Du får denne eposten fordi me har endra tenestvilkåra på %{domain}. Desse endringane kjem i kraft %{date}. Me oppmodar deg til å sjå på dei oppdaterte vilkåra her:'
|
||||||
|
description_html: Du får denne eposten fordi me har endra tenestvilkåra på %{domain}. Desse endringane kjem i kraft <strong>%{date}</strong>. Me oppmodar deg til å sjå på <a href="%{path}" target="_blank">dei oppdaterte vilkåra her</a>.
|
||||||
sign_off: Folka på %{domain}
|
sign_off: Folka på %{domain}
|
||||||
subject: Endra bruksvilkår
|
subject: Endra bruksvilkår
|
||||||
subtitle: Bruksvilkåra på %{domain} er endra
|
subtitle: Bruksvilkåra på %{domain} er endra
|
||||||
|
@ -1945,7 +1987,7 @@ nn:
|
||||||
appeal_description: Om du meiner dette er ein feil, kan du sende inn ei klage til gjengen i %{instance}.
|
appeal_description: Om du meiner dette er ein feil, kan du sende inn ei klage til gjengen i %{instance}.
|
||||||
categories:
|
categories:
|
||||||
spam: Søppelpost
|
spam: Søppelpost
|
||||||
violation: Innhald bryter følgjande retningslinjer
|
violation: Innhaldet bryt med desse retningslinene
|
||||||
explanation:
|
explanation:
|
||||||
delete_statuses: Nokre av innlegga dine er bryt éin eller fleire retningslinjer, og har så blitt fjerna av moderatorene på %{instance}.
|
delete_statuses: Nokre av innlegga dine er bryt éin eller fleire retningslinjer, og har så blitt fjerna av moderatorene på %{instance}.
|
||||||
disable: Du kan ikkje lenger bruke kontoen, men profilen din og andre data er intakt. Du kan be om ein sikkerhetskopi av dine data, endre kontoinnstillingar eller slette din konto.
|
disable: Du kan ikkje lenger bruke kontoen, men profilen din og andre data er intakt. Du kan be om ein sikkerhetskopi av dine data, endre kontoinnstillingar eller slette din konto.
|
||||||
|
|
|
@ -75,6 +75,7 @@ el:
|
||||||
filters:
|
filters:
|
||||||
action: Επιλέξτε ποια ενέργεια θα εκτελεστεί όταν μια δημοσίευση ταιριάζει με το φίλτρο
|
action: Επιλέξτε ποια ενέργεια θα εκτελεστεί όταν μια δημοσίευση ταιριάζει με το φίλτρο
|
||||||
actions:
|
actions:
|
||||||
|
blur: Απόκρυψη πολυμέσων πίσω από μια προειδοποίηση, χωρίς να κρύβεται το ίδιο το κείμενο
|
||||||
hide: Πλήρης αποκρυψη του φιλτραρισμένου περιεχομένου, συμπεριφέρεται σαν να μην υπήρχε
|
hide: Πλήρης αποκρυψη του φιλτραρισμένου περιεχομένου, συμπεριφέρεται σαν να μην υπήρχε
|
||||||
warn: Απόκρυψη φιλτραρισμένου περιεχομένου πίσω από μια προειδοποίηση που αναφέρει τον τίτλο του φίλτρου
|
warn: Απόκρυψη φιλτραρισμένου περιεχομένου πίσω από μια προειδοποίηση που αναφέρει τον τίτλο του φίλτρου
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
@ -88,6 +89,7 @@ el:
|
||||||
favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο.
|
favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο.
|
||||||
mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού.
|
mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού.
|
||||||
media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει ε, αν το πηγαίο περιεχόμενο είναι ακόμα διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα.
|
media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει ε, αν το πηγαίο περιεχόμενο είναι ακόμα διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα.
|
||||||
|
min_age: Οι χρήστες θα κληθούν να επιβεβαιώσουν την ημερομηνία γέννησής τους κατά την εγγραφή
|
||||||
peers_api_enabled: Μια λίστα με ονόματα τομέα που συνάντησε αυτός ο διακομιστής στο fediverse. Δεν περιλαμβάνονται δεδομένα εδώ για το αν συναλλάσσετε με ένα συγκεκριμένο διακομιστή, μόνο ότι ο διακομιστής σας το ξέρει. Χρησιμοποιείται από υπηρεσίες που συλλέγουν στατιστικά στοιχεία για την συναλλαγή με γενική έννοια.
|
peers_api_enabled: Μια λίστα με ονόματα τομέα που συνάντησε αυτός ο διακομιστής στο fediverse. Δεν περιλαμβάνονται δεδομένα εδώ για το αν συναλλάσσετε με ένα συγκεκριμένο διακομιστή, μόνο ότι ο διακομιστής σας το ξέρει. Χρησιμοποιείται από υπηρεσίες που συλλέγουν στατιστικά στοιχεία για την συναλλαγή με γενική έννοια.
|
||||||
profile_directory: Ο κατάλογος προφίλ παραθέτει όλους τους χρήστες που έχουν επιλέξει να είναι ανακαλύψιμοι.
|
profile_directory: Ο κατάλογος προφίλ παραθέτει όλους τους χρήστες που έχουν επιλέξει να είναι ανακαλύψιμοι.
|
||||||
require_invite_text: 'Όταν η εγγραφή απαιτεί χειροκίνητη έγκριση, κάνε το πεδίο κειμένου: «Γιατί θέλετε να συμμετάσχετε;» υποχρεωτικό αντί για προαιρετικό'
|
require_invite_text: 'Όταν η εγγραφή απαιτεί χειροκίνητη έγκριση, κάνε το πεδίο κειμένου: «Γιατί θέλετε να συμμετάσχετε;» υποχρεωτικό αντί για προαιρετικό'
|
||||||
|
@ -132,14 +134,21 @@ el:
|
||||||
name: Μπορείς να αλλάξεις μόνο το πλαίσιο των χαρακτήρων, για παράδειγμα για να γίνει περισσότερο ευανάγνωστο
|
name: Μπορείς να αλλάξεις μόνο το πλαίσιο των χαρακτήρων, για παράδειγμα για να γίνει περισσότερο ευανάγνωστο
|
||||||
terms_of_service:
|
terms_of_service:
|
||||||
changelog: Μπορεί να δομηθεί με σύνταξη Markdown.
|
changelog: Μπορεί να δομηθεί με σύνταξη Markdown.
|
||||||
|
effective_date: Ένα λογικό χρονικό πλαίσιο μπορεί να κυμαίνεται οποτεδήποτε από 10 έως 30 ημέρες από την ημερομηνία που ενημερώνετε τους χρήστες σας.
|
||||||
text: Μπορεί να δομηθεί με σύνταξη Markdown.
|
text: Μπορεί να δομηθεί με σύνταξη Markdown.
|
||||||
terms_of_service_generator:
|
terms_of_service_generator:
|
||||||
admin_email: Οι νομικές ανακοινώσεις περιλαμβάνουν αντικρούσεις, δικαστικές αποφάσεις, αιτήματα που έχουν ληφθεί και αιτήματα επιβολής του νόμου.
|
admin_email: Οι νομικές ανακοινώσεις περιλαμβάνουν αντικρούσεις, δικαστικές αποφάσεις, αιτήματα που έχουν ληφθεί και αιτήματα επιβολής του νόμου.
|
||||||
|
arbitration_address: Μπορεί να είναι το ίδιο με τη φυσική διεύθυνση παραπάνω, ή “Μ/Δ” εάν χρησιμοποιείται email.
|
||||||
|
arbitration_website: Μπορεί να είναι μια φόρμα ιστού ή “Μ/Δ” εάν χρησιμοποιείται email.
|
||||||
|
choice_of_law: Η πόλη, η περιοχή, το έδαφος ή οι εσωτερικοί ουσιαστικοί νόμοι των οποίων διέπουν όλες τις αξιώσεις.
|
||||||
dmca_address: Για τους φορείς των ΗΠΑ, χρησιμοποιήστε τη διεύθυνση που έχει καταχωρηθεί στο DMCA Designated Agent Directory. A P.O. Η λίστα είναι διαθέσιμη κατόπιν απευθείας αιτήματος, Χρησιμοποιήστε το αίτημα απαλλαγής από την άδεια χρήσης του καθορισμένου από το DMCA Agent Post Office Box για να στείλετε email στο Γραφείο Πνευματικών Δικαιωμάτων και περιγράψτε ότι είστε συντονιστής περιεχομένου με βάση το σπίτι, ο οποίος φοβάται την εκδίκηση ή την απόδοση για τις ενέργειές σας και πρέπει να χρησιμοποιήσετε ένα P.. Box για να αφαιρέσετε τη διεύθυνση οικίας σας από τη δημόσια προβολή.
|
dmca_address: Για τους φορείς των ΗΠΑ, χρησιμοποιήστε τη διεύθυνση που έχει καταχωρηθεί στο DMCA Designated Agent Directory. A P.O. Η λίστα είναι διαθέσιμη κατόπιν απευθείας αιτήματος, Χρησιμοποιήστε το αίτημα απαλλαγής από την άδεια χρήσης του καθορισμένου από το DMCA Agent Post Office Box για να στείλετε email στο Γραφείο Πνευματικών Δικαιωμάτων και περιγράψτε ότι είστε συντονιστής περιεχομένου με βάση το σπίτι, ο οποίος φοβάται την εκδίκηση ή την απόδοση για τις ενέργειές σας και πρέπει να χρησιμοποιήσετε ένα P.. Box για να αφαιρέσετε τη διεύθυνση οικίας σας από τη δημόσια προβολή.
|
||||||
|
dmca_email: Μπορεί να είναι το ίδιο email που χρησιμοποιείται για “Διεύθυνση email για νομικές ανακοινώσεις” παραπάνω.
|
||||||
domain: Μοναδικό αναγνωριστικό της διαδικτυακής υπηρεσίας που παρέχεις.
|
domain: Μοναδικό αναγνωριστικό της διαδικτυακής υπηρεσίας που παρέχεις.
|
||||||
jurisdiction: Ανέφερε τη χώρα όπου ζει αυτός που πληρώνει τους λογαριασμούς. Εάν πρόκειται για εταιρεία ή άλλη οντότητα, ανέφερε τη χώρα όπου υφίσταται, και την πόλη, περιοχή, έδαφος ή πολιτεία ανάλογα με την περίπτωση.
|
jurisdiction: Ανέφερε τη χώρα όπου ζει αυτός που πληρώνει τους λογαριασμούς. Εάν πρόκειται για εταιρεία ή άλλη οντότητα, ανέφερε τη χώρα όπου υφίσταται, και την πόλη, περιοχή, έδαφος ή πολιτεία ανάλογα με την περίπτωση.
|
||||||
|
min_age: Δεν πρέπει να είναι κάτω από την ελάχιστη ηλικία που απαιτείται από τους νόμους της δικαιοδοσίας σας.
|
||||||
user:
|
user:
|
||||||
chosen_languages: Όταν ενεργοποιηθεί, στη δημόσια ροή θα εμφανίζονται τουτ μόνο από τις επιλεγμένες γλώσσες
|
chosen_languages: Όταν ενεργοποιηθεί, στη δημόσια ροή θα εμφανίζονται τουτ μόνο από τις επιλεγμένες γλώσσες
|
||||||
|
date_of_birth: Πρέπει να βεβαιωθείς ότι είσαι τουλάχιστον %{age} για να χρησιμοποιήσεις το Mastodon. Δεν θα το αποθηκεύσουμε.
|
||||||
role: Ο ρόλος ελέγχει ποια δικαιώματα έχει ο χρήστης.
|
role: Ο ρόλος ελέγχει ποια δικαιώματα έχει ο χρήστης.
|
||||||
user_role:
|
user_role:
|
||||||
color: Το χρώμα που θα χρησιμοποιηθεί για το ρόλο σε ολόκληρη τη διεπαφή, ως RGB σε δεκαεξαδική μορφή
|
color: Το χρώμα που θα χρησιμοποιηθεί για το ρόλο σε ολόκληρη τη διεπαφή, ως RGB σε δεκαεξαδική μορφή
|
||||||
|
@ -252,6 +261,7 @@ el:
|
||||||
name: Ετικέτα
|
name: Ετικέτα
|
||||||
filters:
|
filters:
|
||||||
actions:
|
actions:
|
||||||
|
blur: Απόκρυψη πολυμέσων με προειδοποίηση
|
||||||
hide: Πλήρης απόκρυψη
|
hide: Πλήρης απόκρυψη
|
||||||
warn: Απόκρυψη με προειδοποίηση
|
warn: Απόκρυψη με προειδοποίηση
|
||||||
form_admin_settings:
|
form_admin_settings:
|
||||||
|
@ -265,6 +275,7 @@ el:
|
||||||
favicon: Favicon
|
favicon: Favicon
|
||||||
mascot: Προσαρμοσμένη μασκότ (απαρχαιωμένο)
|
mascot: Προσαρμοσμένη μασκότ (απαρχαιωμένο)
|
||||||
media_cache_retention_period: Περίοδος διατήρησης προσωρινής μνήμης πολυμέσων
|
media_cache_retention_period: Περίοδος διατήρησης προσωρινής μνήμης πολυμέσων
|
||||||
|
min_age: Ελάχιστη απαιτούμενη ηλικία
|
||||||
peers_api_enabled: Δημοσίευση λίστας των εντοπισμένων διακομιστών στο API
|
peers_api_enabled: Δημοσίευση λίστας των εντοπισμένων διακομιστών στο API
|
||||||
profile_directory: Ενεργοποίηση καταλόγου προφίλ
|
profile_directory: Ενεργοποίηση καταλόγου προφίλ
|
||||||
registrations_mode: Ποιος μπορεί να εγγραφεί
|
registrations_mode: Ποιος μπορεί να εγγραφεί
|
||||||
|
@ -330,16 +341,22 @@ el:
|
||||||
usable: Να επιτρέπεται η τοπική χρήση αυτής της ετικέτας από αναρτήσεις
|
usable: Να επιτρέπεται η τοπική χρήση αυτής της ετικέτας από αναρτήσεις
|
||||||
terms_of_service:
|
terms_of_service:
|
||||||
changelog: Τι άλλαξε;
|
changelog: Τι άλλαξε;
|
||||||
|
effective_date: Ημερομηνία έναρξης ισχύος
|
||||||
text: Όροι Παροχής Υπηρεσιών
|
text: Όροι Παροχής Υπηρεσιών
|
||||||
terms_of_service_generator:
|
terms_of_service_generator:
|
||||||
admin_email: Διεύθυνση email για τις νομικές ανακοινώσεις
|
admin_email: Διεύθυνση email για τις νομικές ανακοινώσεις
|
||||||
arbitration_address: Φυσική διεύθυνση για τις ανακοινώσεις διαιτησίας
|
arbitration_address: Φυσική διεύθυνση για τις ανακοινώσεις διαιτησίας
|
||||||
arbitration_website: Ιστοσελίδα για την υποβολή ειδοποιήσεων διαιτησίας
|
arbitration_website: Ιστοσελίδα για την υποβολή ειδοποιήσεων διαιτησίας
|
||||||
|
choice_of_law: Επιλογή νόμου
|
||||||
dmca_address: Φυσική διεύθυνση για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων
|
dmca_address: Φυσική διεύθυνση για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων
|
||||||
dmca_email: Διεύθυνση email για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων
|
dmca_email: Διεύθυνση email για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων
|
||||||
domain: Τομέας
|
domain: Τομέας
|
||||||
jurisdiction: Νομική δικαιοδοσία
|
jurisdiction: Νομική δικαιοδοσία
|
||||||
|
min_age: Ελάχιστη ηλικία
|
||||||
user:
|
user:
|
||||||
|
date_of_birth_1i: Ημέρα
|
||||||
|
date_of_birth_2i: Μήνας
|
||||||
|
date_of_birth_3i: Έτος
|
||||||
role: Ρόλος
|
role: Ρόλος
|
||||||
time_zone: Ζώνη ώρας
|
time_zone: Ζώνη ώρας
|
||||||
user_role:
|
user_role:
|
||||||
|
|
|
@ -89,6 +89,7 @@ nn:
|
||||||
favicon: WEBP, PNG, GIF eller JPG. Overstyrer det standarde Mastodon-favikonet med eit eigendefinert ikon.
|
favicon: WEBP, PNG, GIF eller JPG. Overstyrer det standarde Mastodon-favikonet med eit eigendefinert ikon.
|
||||||
mascot: Overstyrer illustrasjonen i det avanserte webgrensesnittet.
|
mascot: Overstyrer illustrasjonen i det avanserte webgrensesnittet.
|
||||||
media_cache_retention_period: Mediafiler frå innlegg laga av eksterne brukarar blir bufra på serveren din. Når sett til ein positiv verdi, slettast media etter eit gitt antal dagar. Viss mediedata blir førespurt etter det er sletta, vil dei bli lasta ned på nytt viss kjelda sitt innhald framleis er tilgjengeleg. På grunn av restriksjonar på kor ofte lenkeførehandsvisningskort lastar tredjepart-nettstadar, rådast det til å setje denne verdien til minst 14 dagar, eller at førehandsvisningskort ikkje blir oppdatert på førespurnad før det tidspunktet.
|
media_cache_retention_period: Mediafiler frå innlegg laga av eksterne brukarar blir bufra på serveren din. Når sett til ein positiv verdi, slettast media etter eit gitt antal dagar. Viss mediedata blir førespurt etter det er sletta, vil dei bli lasta ned på nytt viss kjelda sitt innhald framleis er tilgjengeleg. På grunn av restriksjonar på kor ofte lenkeførehandsvisningskort lastar tredjepart-nettstadar, rådast det til å setje denne verdien til minst 14 dagar, eller at førehandsvisningskort ikkje blir oppdatert på førespurnad før det tidspunktet.
|
||||||
|
min_age: Brukarane vil bli bedne om å stadfesta fødselsdatoen sin når dei registrerer seg
|
||||||
peers_api_enabled: Ei liste over domenenamn denne tenaren har møtt på i allheimen. Det står ingenting om tenaren din samhandlar med ein annan tenar, berre om tenaren din veit om den andre. Dette blir brukt av tenester som samlar statistikk om føderering i det heile.
|
peers_api_enabled: Ei liste over domenenamn denne tenaren har møtt på i allheimen. Det står ingenting om tenaren din samhandlar med ein annan tenar, berre om tenaren din veit om den andre. Dette blir brukt av tenester som samlar statistikk om føderering i det heile.
|
||||||
profile_directory: Profilkatalogen viser alle brukarar som har valt å kunne bli oppdaga.
|
profile_directory: Profilkatalogen viser alle brukarar som har valt å kunne bli oppdaga.
|
||||||
require_invite_text: Når registrering krev manuell godkjenning, lyt du gjera tekstfeltet "Kvifor vil du bli med?" obligatorisk i staden for valfritt
|
require_invite_text: Når registrering krev manuell godkjenning, lyt du gjera tekstfeltet "Kvifor vil du bli med?" obligatorisk i staden for valfritt
|
||||||
|
@ -133,11 +134,13 @@ nn:
|
||||||
name: Du kan berre endra bruken av store/små bokstavar, t. d. for å gjera det meir leseleg
|
name: Du kan berre endra bruken av store/små bokstavar, t. d. for å gjera det meir leseleg
|
||||||
terms_of_service:
|
terms_of_service:
|
||||||
changelog: Du kan bruka Markdown-syntaks for struktur.
|
changelog: Du kan bruka Markdown-syntaks for struktur.
|
||||||
|
effective_date: Ei rimeleg ventetid kan variera frå 10 til 30 dagar frå den dagen du varsla folka som bruker denne tenaren.
|
||||||
text: Du kan bruka Markdown-syntaks for struktur.
|
text: Du kan bruka Markdown-syntaks for struktur.
|
||||||
terms_of_service_generator:
|
terms_of_service_generator:
|
||||||
admin_email: Juridiske merknader kan vera motsegner, rettsavgjerder, orskurdar eller førespurnader om sletting.
|
admin_email: Juridiske merknader kan vera motsegner, rettsavgjerder, orskurdar eller førespurnader om sletting.
|
||||||
arbitration_address: Kan vere lik den fysiske adressa over, eller "N/A" viss du bruker epost.
|
arbitration_address: Kan vere lik den fysiske adressa over, eller "N/A" viss du bruker epost.
|
||||||
arbitration_website: Kan vere eit nettskjema eller "N/A" viss du bruker e-post.
|
arbitration_website: Kan vere eit nettskjema eller "N/A" viss du bruker e-post.
|
||||||
|
choice_of_law: Jurisdiksjon
|
||||||
dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
|
dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
|
||||||
dmca_email: Kan vere same e-post som brukast i "E-postadresse for juridiske meldingar" ovanfor.
|
dmca_email: Kan vere same e-post som brukast i "E-postadresse for juridiske meldingar" ovanfor.
|
||||||
domain: Noko som identifiserer den nettenesta du tilbyr.
|
domain: Noko som identifiserer den nettenesta du tilbyr.
|
||||||
|
@ -145,6 +148,7 @@ nn:
|
||||||
min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din.
|
min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din.
|
||||||
user:
|
user:
|
||||||
chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
|
chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
|
||||||
|
date_of_birth: Me må syta for at du er minst %{age} for å bruka Masodon. Me lagrar ikkje dette.
|
||||||
role: Rolla kontrollerer kva løyve brukaren har.
|
role: Rolla kontrollerer kva løyve brukaren har.
|
||||||
user_role:
|
user_role:
|
||||||
color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format
|
color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format
|
||||||
|
@ -165,7 +169,7 @@ nn:
|
||||||
value: Innhald
|
value: Innhald
|
||||||
indexable: Ta med offentlege innlegg i søkjeresultat
|
indexable: Ta med offentlege innlegg i søkjeresultat
|
||||||
show_collections: Vis dei du fylgjer og dei som fylgjer deg på profilen din
|
show_collections: Vis dei du fylgjer og dei som fylgjer deg på profilen din
|
||||||
unlocked: Godta nye følgjare automatisk
|
unlocked: Godta nye fylgjarar automatisk
|
||||||
account_alias:
|
account_alias:
|
||||||
acct: Brukarnamnet på den gamle kontoen
|
acct: Brukarnamnet på den gamle kontoen
|
||||||
account_migration:
|
account_migration:
|
||||||
|
@ -271,6 +275,7 @@ nn:
|
||||||
favicon: Favorittikon
|
favicon: Favorittikon
|
||||||
mascot: Eigendefinert maskot (eldre funksjon)
|
mascot: Eigendefinert maskot (eldre funksjon)
|
||||||
media_cache_retention_period: Oppbevaringsperiode for mediebuffer
|
media_cache_retention_period: Oppbevaringsperiode for mediebuffer
|
||||||
|
min_age: Minste aldersgrense
|
||||||
peers_api_enabled: Legg ut ei liste over oppdaga tenarar i APIet
|
peers_api_enabled: Legg ut ei liste over oppdaga tenarar i APIet
|
||||||
profile_directory: Aktiver profilkatalog
|
profile_directory: Aktiver profilkatalog
|
||||||
registrations_mode: Kven kan registrera seg
|
registrations_mode: Kven kan registrera seg
|
||||||
|
@ -336,17 +341,22 @@ nn:
|
||||||
usable: Godta at innlegga kan bruka denne emneknaggen lokalt
|
usable: Godta at innlegga kan bruka denne emneknaggen lokalt
|
||||||
terms_of_service:
|
terms_of_service:
|
||||||
changelog: Kva er endra?
|
changelog: Kva er endra?
|
||||||
|
effective_date: I kraft frå
|
||||||
text: Bruksvilkår
|
text: Bruksvilkår
|
||||||
terms_of_service_generator:
|
terms_of_service_generator:
|
||||||
admin_email: Epostadresse for juridiske merknader
|
admin_email: Epostadresse for juridiske merknader
|
||||||
arbitration_address: Fysisk adresse for skilsdomsvarsel
|
arbitration_address: Fysisk adresse for skilsdomsvarsel
|
||||||
arbitration_website: Nettstad for å senda inn skilsdomsvarsel
|
arbitration_website: Nettstad for å senda inn skilsdomsvarsel
|
||||||
|
choice_of_law: Jurisdiksjon
|
||||||
dmca_address: Fysisk adresse for opphavsrettsvarsel
|
dmca_address: Fysisk adresse for opphavsrettsvarsel
|
||||||
dmca_email: Epostadresse for opphavsrettsvarsel
|
dmca_email: Epostadresse for opphavsrettsvarsel
|
||||||
domain: Domene
|
domain: Domene
|
||||||
jurisdiction: Rettskrins
|
jurisdiction: Rettskrins
|
||||||
min_age: Minstealder
|
min_age: Minstealder
|
||||||
user:
|
user:
|
||||||
|
date_of_birth_1i: Dag
|
||||||
|
date_of_birth_2i: Månad
|
||||||
|
date_of_birth_3i: År
|
||||||
role: Rolle
|
role: Rolle
|
||||||
time_zone: Tidssone
|
time_zone: Tidssone
|
||||||
user_role:
|
user_role:
|
||||||
|
|
20
db/migrate/20250411094808_create_quotes.rb
Normal file
20
db/migrate/20250411094808_create_quotes.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateQuotes < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :quotes do |t|
|
||||||
|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, index: false, null: false
|
||||||
|
t.belongs_to :status, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
|
||||||
|
t.belongs_to :quoted_status, foreign_key: { to_table: :statuses, on_delete: :nullify }, null: true
|
||||||
|
t.belongs_to :quoted_account, foreign_key: { to_table: :accounts, on_delete: :nullify }, null: true
|
||||||
|
t.integer :state, null: false, default: 0
|
||||||
|
t.string :approval_uri, index: { where: 'approval_uri IS NOT NULL' }
|
||||||
|
t.string :activity_uri, index: { unique: true, where: 'activity_uri IS NOT NULL' }
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
# Can be used in the future to e.g. bulk-reject quotes from blocked accounts
|
||||||
|
add_index :quotes, [:account_id, :quoted_account_id]
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20250411095859_add_quote_id_to_status_edit.rb
Normal file
7
db/migrate/20250411095859_add_quote_id_to_status_edit.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddQuoteIdToStatusEdit < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :status_edits, :quote_id, :bigint, null: true
|
||||||
|
end
|
||||||
|
end
|
25
db/schema.rb
25
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
ActiveRecord::Schema[8.0].define(version: 2025_04_11_095859) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -871,6 +871,24 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
||||||
t.string "url"
|
t.string "url"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "quotes", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.bigint "status_id", null: false
|
||||||
|
t.bigint "quoted_status_id"
|
||||||
|
t.bigint "quoted_account_id"
|
||||||
|
t.integer "state", default: 0, null: false
|
||||||
|
t.string "approval_uri"
|
||||||
|
t.string "activity_uri"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id", "quoted_account_id"], name: "index_quotes_on_account_id_and_quoted_account_id"
|
||||||
|
t.index ["activity_uri"], name: "index_quotes_on_activity_uri", unique: true, where: "(activity_uri IS NOT NULL)"
|
||||||
|
t.index ["approval_uri"], name: "index_quotes_on_approval_uri", where: "(approval_uri IS NOT NULL)"
|
||||||
|
t.index ["quoted_account_id"], name: "index_quotes_on_quoted_account_id"
|
||||||
|
t.index ["quoted_status_id"], name: "index_quotes_on_quoted_status_id"
|
||||||
|
t.index ["status_id"], name: "index_quotes_on_status_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "relationship_severance_events", force: :cascade do |t|
|
create_table "relationship_severance_events", force: :cascade do |t|
|
||||||
t.integer "type", null: false
|
t.integer "type", null: false
|
||||||
t.string "target_name", null: false
|
t.string "target_name", null: false
|
||||||
|
@ -1008,6 +1026,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
||||||
t.text "media_descriptions", array: true
|
t.text "media_descriptions", array: true
|
||||||
t.string "poll_options", array: true
|
t.string "poll_options", array: true
|
||||||
t.boolean "sensitive"
|
t.boolean "sensitive"
|
||||||
|
t.bigint "quote_id"
|
||||||
t.index ["account_id"], name: "index_status_edits_on_account_id"
|
t.index ["account_id"], name: "index_status_edits_on_account_id"
|
||||||
t.index ["status_id"], name: "index_status_edits_on_status_id"
|
t.index ["status_id"], name: "index_status_edits_on_status_id"
|
||||||
end
|
end
|
||||||
|
@ -1353,6 +1372,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
||||||
add_foreign_key "polls", "statuses", on_delete: :cascade
|
add_foreign_key "polls", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade
|
add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade
|
||||||
add_foreign_key "preview_cards", "accounts", column: "author_account_id", on_delete: :nullify
|
add_foreign_key "preview_cards", "accounts", column: "author_account_id", on_delete: :nullify
|
||||||
|
add_foreign_key "quotes", "accounts", column: "quoted_account_id", on_delete: :nullify
|
||||||
|
add_foreign_key "quotes", "accounts", on_delete: :cascade
|
||||||
|
add_foreign_key "quotes", "statuses", column: "quoted_status_id", on_delete: :nullify
|
||||||
|
add_foreign_key "quotes", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "report_notes", "accounts", on_delete: :cascade
|
add_foreign_key "report_notes", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "report_notes", "reports", on_delete: :cascade
|
add_foreign_key "report_notes", "reports", on_delete: :cascade
|
||||||
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
|
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
|
||||||
|
|
7
spec/fabricators/quote_fabricator.rb
Normal file
7
spec/fabricators/quote_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:quote) do
|
||||||
|
status { Fabricate.build(:status) }
|
||||||
|
quoted_status { Fabricate.build(:status) }
|
||||||
|
state :pending
|
||||||
|
end
|
|
@ -7,7 +7,15 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
quote: {
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
@ -929,6 +937,115 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an unverifiable quote of a known post', feature: :inbound_quotes do
|
||||||
|
let(:quoted_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
build_object(
|
||||||
|
type: 'Note',
|
||||||
|
content: 'woah what she said is amazing',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a status with an unverified quote' do
|
||||||
|
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||||
|
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote).to have_attributes(
|
||||||
|
state: 'pending',
|
||||||
|
approval_uri: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an unverifiable unknown post', feature: :inbound_quotes do
|
||||||
|
let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
build_object(
|
||||||
|
type: 'Note',
|
||||||
|
content: 'woah what she said is amazing',
|
||||||
|
quote: unknown_post_uri
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, unknown_post_uri).to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a status with an unverified quote' do
|
||||||
|
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||||
|
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote).to have_attributes(
|
||||||
|
state: 'pending',
|
||||||
|
approval_uri: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a verifiable quote of a known post', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/quote-approval' }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
build_object(
|
||||||
|
type: 'Note',
|
||||||
|
content: 'woah what she said is amazing',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
quoteAuthorization: approval_uri
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||||
|
interactingObject: object_json[:id],
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a status with a verified quote' do
|
||||||
|
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||||
|
|
||||||
|
status = sender.statuses.first
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.quote).to_not be_nil
|
||||||
|
expect(status.quote).to have_attributes(
|
||||||
|
state: 'accepted',
|
||||||
|
approval_uri: approval_uri
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a vote to a local poll' do
|
context 'when a vote to a local poll' do
|
||||||
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
||||||
let!(:local_status) { Fabricate(:status, poll: poll) }
|
let!(:local_status) { Fabricate(:status, poll: poll) }
|
||||||
|
|
|
@ -77,4 +77,61 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the deleted object is an account' do
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Delete',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
signature: 'foo',
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
|
let(:service) { instance_double(DeleteAccountService, call: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(DeleteAccountService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the account deletion service' do
|
||||||
|
subject.perform
|
||||||
|
|
||||||
|
expect(service)
|
||||||
|
.to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the deleted object is a quote authorization' do
|
||||||
|
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||||
|
let(:status) { Fabricate(:status, account: quoter) }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||||
|
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||||
|
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Delete',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
object: quote.approval_uri,
|
||||||
|
signature: 'foo',
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
|
it 'revokes the authorization' do
|
||||||
|
expect { subject.perform }
|
||||||
|
.to change { quote.reload.state }.to('revoked')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,10 +40,119 @@ RSpec.describe StatusCacheHydrator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when handling an unapproved quote' do
|
||||||
|
let(:quoted_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:quote, status: status, quoted_status: quoted_status, state: :pending)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when handling an approved quote' do
|
||||||
|
let(:quoted_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post has been favourited' do
|
||||||
|
before do
|
||||||
|
FavouriteService.new.call(account, quoted_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post has been reblogged' do
|
||||||
|
before do
|
||||||
|
ReblogService.new.call(account, quoted_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post matches account filters' do
|
||||||
|
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as a full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when handling a reblog' do
|
context 'when handling a reblog' do
|
||||||
let(:reblog) { Fabricate(:status) }
|
let(:reblog) { Fabricate(:status) }
|
||||||
let(:status) { Fabricate(:status, reblog: reblog) }
|
let(:status) { Fabricate(:status, reblog: reblog) }
|
||||||
|
|
||||||
|
context 'when the reblog has an approved quote' do
|
||||||
|
let(:quoted_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:quote, status: reblog, quoted_status: quoted_status, state: :accepted)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:reblog][:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post has been favourited' do
|
||||||
|
before do
|
||||||
|
FavouriteService.new.call(account, quoted_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:reblog][:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post has been reblogged' do
|
||||||
|
before do
|
||||||
|
ReblogService.new.call(account, quoted_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:reblog][:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the quoted post matches account filters' do
|
||||||
|
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the same attributes as a full render' do
|
||||||
|
expect(subject).to eql(compare_to_hash)
|
||||||
|
expect(subject[:reblog][:quote]).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when it has been favourited' do
|
context 'when it has been favourited' do
|
||||||
before do
|
before do
|
||||||
FavouriteService.new.call(account, reblog)
|
FavouriteService.new.call(account, reblog)
|
||||||
|
|
87
spec/serializers/rest/quote_serializer_spec.rb
Normal file
87
spec/serializers/rest/quote_serializer_spec.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::QuoteSerializer do
|
||||||
|
subject do
|
||||||
|
serialized_record_json(
|
||||||
|
quote,
|
||||||
|
described_class,
|
||||||
|
options: {
|
||||||
|
scope: current_user,
|
||||||
|
scope_name: :current_user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
let(:quote) { Fabricate(:quote) }
|
||||||
|
|
||||||
|
context 'with a pending quote' do
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status: nil,
|
||||||
|
state: 'pending'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status: be_a(Hash),
|
||||||
|
state: 'accepted'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote of a deleted post' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.quoted_status.destroy!
|
||||||
|
quote.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status: nil,
|
||||||
|
state: 'deleted'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote of a blocked user' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.quoted_account.block!(current_user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status: nil,
|
||||||
|
state: 'unauthorized'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a recursive accepted quote' do
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: status, state: :accepted) }
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status: be_a(Hash),
|
||||||
|
state: 'accepted'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
93
spec/serializers/rest/shallow_quote_serializer_spec.rb
Normal file
93
spec/serializers/rest/shallow_quote_serializer_spec.rb
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::ShallowQuoteSerializer do
|
||||||
|
subject do
|
||||||
|
serialized_record_json(
|
||||||
|
quote,
|
||||||
|
described_class,
|
||||||
|
options: {
|
||||||
|
scope: current_user,
|
||||||
|
scope_name: :current_user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
let(:quote) { Fabricate(:quote) }
|
||||||
|
|
||||||
|
context 'with a pending quote' do
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status_id: nil,
|
||||||
|
state: 'pending'
|
||||||
|
)
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to_not have_key(:quoted_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status_id: be_a(String),
|
||||||
|
state: 'accepted'
|
||||||
|
)
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to_not have_key(:quoted_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote of a deleted post' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.quoted_status.destroy!
|
||||||
|
quote.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status_id: nil,
|
||||||
|
state: 'deleted'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an accepted quote of a blocked user' do
|
||||||
|
let(:quote) { Fabricate(:quote, state: :accepted) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.quoted_account.block!(current_user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status_id: nil,
|
||||||
|
state: 'unauthorized'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a recursive accepted quote' do
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: status, state: :accepted) }
|
||||||
|
|
||||||
|
it 'returns expected values' do
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to include(
|
||||||
|
quoted_status_id: be_a(String),
|
||||||
|
state: 'accepted'
|
||||||
|
)
|
||||||
|
expect(subject.deep_symbolize_keys)
|
||||||
|
.to_not have_key(:quoted_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ require 'rails_helper'
|
||||||
RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
subject { described_class.new }
|
subject { described_class.new }
|
||||||
|
|
||||||
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
|
let!(:status) { Fabricate(:status, text: 'Hello world', uri: 'https://example.com/statuses/1234', account: Fabricate(:account, domain: 'example.com')) }
|
||||||
let(:bogus_mention) { 'https://example.com/users/erroringuser' }
|
let(:bogus_mention) { 'https://example.com/users/erroringuser' }
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
{
|
{
|
||||||
|
@ -435,6 +435,398 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the status has an existing unverified quote and adds an approval link', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: nil) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
quoteAuthorization: approval_uri,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||||
|
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the approval URI and verifies the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change(quote, :approval_uri).to(approval_uri)
|
||||||
|
.and change(quote, :state).to('accepted')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status has an existing verified quote and removes an approval link', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the approval URI and unverifies the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change(quote, :approval_uri).to(nil)
|
||||||
|
.and change(quote, :state).to('pending')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status adds a verifiable quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
quoteAuthorization: approval_uri,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||||
|
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the approval URI and verifies the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change(status, :quote).from(nil)
|
||||||
|
expect(status.quote.approval_uri).to eq approval_uri
|
||||||
|
expect(status.quote.state).to eq 'accepted'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status adds a unverifiable quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the approval URI but does not verify the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change(status, :quote).from(nil)
|
||||||
|
expect(status.quote.approval_uri).to be_nil
|
||||||
|
expect(status.quote.state).to eq 'pending'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status removes a verified quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change { status.reload.quote }.to(nil)
|
||||||
|
|
||||||
|
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status removes an unverified quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: nil, state: :pending) }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change { status.reload.quote }.to(nil)
|
||||||
|
|
||||||
|
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status swaps a verified quote with an unverifiable quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:second_quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(second_quoted_status),
|
||||||
|
quoteAuthorization: approval_uri,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||||
|
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the URI and unverifies the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change { status.quote.quoted_status }.from(quoted_status).to(second_quoted_status)
|
||||||
|
.and change { status.quote.state }.from('accepted')
|
||||||
|
|
||||||
|
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status swaps a verified quote with another verifiable quote', feature: :inbound_quotes do
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
|
let(:second_quoted_account) { Fabricate(:account, domain: 'second-quoted.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:second_quoted_status) { Fabricate(:status, account: second_quoted_account) }
|
||||||
|
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||||
|
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||||
|
let(:second_approval_uri) { 'https://second-quoted.example.com/approvals/2' }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quote',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
quote: ActivityPub::TagManager.instance.uri_for(second_quoted_status),
|
||||||
|
quoteAuthorization: second_approval_uri,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: second_approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(second_quoted_status.account),
|
||||||
|
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(second_quoted_status),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the URI and unverifies the quote' do
|
||||||
|
expect { subject.call(status, json, json) }
|
||||||
|
.to change { status.quote.quoted_status }.from(quoted_status).to(second_quoted_status)
|
||||||
|
.and change { status.quote.approval_uri }.from(approval_uri).to(second_approval_uri)
|
||||||
|
.and(not_change { status.quote.state })
|
||||||
|
|
||||||
|
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def poll_option_json(name, votes)
|
def poll_option_json(name, votes)
|
||||||
{ type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
|
{ type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
|
||||||
end
|
end
|
||||||
|
|
246
spec/services/activitypub/verify_quote_service_spec.rb
Normal file
246
spec/services/activitypub/verify_quote_service_spec.rb
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::VerifyQuoteService do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account, domain: 'a.example.com') }
|
||||||
|
let(:quoted_account) { Fabricate(:account, domain: 'b.example.com') }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
let(:status) { Fabricate(:status, account: account) }
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri) }
|
||||||
|
|
||||||
|
context 'with an unfetchable approval URI' do
|
||||||
|
let(:approval_uri) { 'https://b.example.com/approvals/1234' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri)
|
||||||
|
.to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an already-fetched post' do
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('rejected')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an already-verified quote' do
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||||
|
|
||||||
|
it 'rejects the quote' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('revoked')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an approval URI' do
|
||||||
|
let(:approval_uri) { 'https://b.example.com/approvals/1234' }
|
||||||
|
|
||||||
|
let(:approval_type) { 'QuoteAuthorization' }
|
||||||
|
let(:approval_id) { approval_uri }
|
||||||
|
let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(quoted_account) }
|
||||||
|
let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(status) }
|
||||||
|
let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(quoted_status) }
|
||||||
|
|
||||||
|
let(:json) do
|
||||||
|
{
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
{
|
||||||
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
|
QuoteAuthorization: 'toot:QuoteAuthorization',
|
||||||
|
gts: 'https://gotosocial.org/ns#',
|
||||||
|
interactionPolicy: {
|
||||||
|
'@id': 'gts:interactionPolicy',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactingObject: {
|
||||||
|
'@id': 'gts:interactingObject',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
interactionTarget: {
|
||||||
|
'@id': 'gts:interactionTarget',
|
||||||
|
'@type': '@id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: approval_type,
|
||||||
|
id: approval_id,
|
||||||
|
attributedTo: approval_attributed_to,
|
||||||
|
interactingObject: approval_interacting_object,
|
||||||
|
interactionTarget: approval_interaction_target,
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, approval_uri)
|
||||||
|
.to_return(status: 200, body: Oj.dump(json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid activity for already-fetched posts' do
|
||||||
|
it 'updates the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('accepted')
|
||||||
|
|
||||||
|
expect(a_request(:get, approval_uri))
|
||||||
|
.to have_been_made.once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid activity for a post that cannot be fetched but is inlined' do
|
||||||
|
let(:quoted_status) { nil }
|
||||||
|
|
||||||
|
let(:approval_interaction_target) do
|
||||||
|
{
|
||||||
|
type: 'Note',
|
||||||
|
id: 'https://b.example.com/unknown-quoted',
|
||||||
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account),
|
||||||
|
content: 'previously unknown post',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://b.example.com/unknown-quoted')
|
||||||
|
.to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the status' do
|
||||||
|
expect { subject.call(quote, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted') }
|
||||||
|
.to change(quote, :state).to('accepted')
|
||||||
|
|
||||||
|
expect(a_request(:get, approval_uri))
|
||||||
|
.to have_been_made.once
|
||||||
|
|
||||||
|
expect(quote.reload.quoted_status.content).to eq 'previously unknown post'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid activity for a post that cannot be fetched and is inlined from an untrusted source' do
|
||||||
|
let(:quoted_status) { nil }
|
||||||
|
|
||||||
|
let(:approval_interaction_target) do
|
||||||
|
{
|
||||||
|
type: 'Note',
|
||||||
|
id: 'https://example.com/unknown-quoted',
|
||||||
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(account),
|
||||||
|
content: 'previously unknown post',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/unknown-quoted')
|
||||||
|
.to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote, fetchable_quoted_uri: 'https://example.com/unknown-quoted') }
|
||||||
|
.to not_change(quote, :state)
|
||||||
|
.and not_change(quote, :quoted_status)
|
||||||
|
|
||||||
|
expect(a_request(:get, approval_uri))
|
||||||
|
.to have_been_made.once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do
|
||||||
|
it 'updates the status without fetching the activity' do
|
||||||
|
expect { subject.call(quote, prefetched_body: Oj.dump(json)) }
|
||||||
|
.to change(quote, :state).to('accepted')
|
||||||
|
|
||||||
|
expect(a_request(:get, approval_uri))
|
||||||
|
.to_not have_been_made
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an unverifiable approval' do
|
||||||
|
let(:approval_uri) { 'https://evil.com/approvals/1234' }
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an invalid approval document because of a mismatched ID' do
|
||||||
|
let(:approval_id) { 'https://evil.com/approvals/1234' }
|
||||||
|
|
||||||
|
it 'does not accept the quote' do
|
||||||
|
# NOTE: maybe we want to skip that instead of rejecting it?
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('rejected')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an approval from the wrong account' do
|
||||||
|
let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(Fabricate(:account, domain: 'b.example.com')) }
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an approval for the wrong quoted post' do
|
||||||
|
let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: quoted_account)) }
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an approval for the wrong quote post' do
|
||||||
|
let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: account)) }
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an approval of the wrong type' do
|
||||||
|
let(:approval_type) { 'ReplyAuthorization' }
|
||||||
|
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with fast-track authorizations' do
|
||||||
|
let(:approval_uri) { nil }
|
||||||
|
|
||||||
|
context 'without any fast-track condition' do
|
||||||
|
it 'does not update the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to_not change(quote, :state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account and the quoted account are the same' do
|
||||||
|
let(:quoted_account) { account }
|
||||||
|
|
||||||
|
it 'updates the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('accepted')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is mentioned by the quoted post' do
|
||||||
|
before do
|
||||||
|
quoted_status.mentions << Mention.new(account: account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the status' do
|
||||||
|
expect { subject.call(quote) }
|
||||||
|
.to change(quote, :state).to('accepted')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
44
spec/workers/activitypub/quote_refresh_worker_spec.rb
Normal file
44
spec/workers/activitypub/quote_refresh_worker_spec.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::QuoteRefreshWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ActivityPub::VerifyQuoteService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:status) { Fabricate(:status, account: account) }
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: nil, updated_at: updated_at) }
|
||||||
|
|
||||||
|
context 'when dealing with an old quote' do
|
||||||
|
let(:updated_at) { (Quote::BACKGROUND_REFRESH_INTERVAL * 2).ago }
|
||||||
|
|
||||||
|
it 'sends the status to the service and bumps the updated date' do
|
||||||
|
expect { worker.perform(quote.id) }
|
||||||
|
.to(change { quote.reload.updated_at })
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(quote)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when dealing with a recent quote' do
|
||||||
|
let(:updated_at) { Time.now.utc }
|
||||||
|
|
||||||
|
it 'does not call the service and does not touch the quote' do
|
||||||
|
expect { worker.perform(quote.id) }
|
||||||
|
.to_not(change { quote.reload.updated_at })
|
||||||
|
|
||||||
|
expect(service).to_not have_received(:call).with(quote)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ActivityPub::VerifyQuoteService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::RefetchAndVerifyQuoteWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ActivityPub::VerifyQuoteService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before { stub_service }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:status) { Fabricate(:status, account: account) }
|
||||||
|
let(:quote) { Fabricate(:quote, status: status, quoted_status: nil) }
|
||||||
|
let(:url) { 'https://example.com/quoted-status' }
|
||||||
|
|
||||||
|
it 'sends the status to the service' do
|
||||||
|
worker.perform(quote.id, url)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(quote, fetchable_quoted_uri: url, request_id: anything)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, url)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_service
|
||||||
|
allow(ActivityPub::VerifyQuoteService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_return(service)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue