From ede0be5dba6836606db772a007ad9be5d60c7b99 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 18 Jun 2019 22:58:18 +0200 Subject: New Crowdin translations (#11077) * New translations en.json (Persian) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations simple_form.en.yml (Dutch) [ci skip] * New translations activerecord.en.yml (Dutch) [ci skip] * New translations devise.en.yml (Dutch) [ci skip] * New translations doorkeeper.en.yml (Dutch) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Czech) [ci skip] * New translations en.json (Greek) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations en.json (Greek) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.json (Persian) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations activerecord.en.yml (Persian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations activerecord.en.yml (Welsh) [ci skip] * New translations devise.en.yml (Welsh) [ci skip] * New translations en.json (Welsh) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations devise.en.yml (Welsh) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations activerecord.en.yml (Thai) [ci skip] * New translations devise.en.yml (Thai) [ci skip] * New translations doorkeeper.en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Slovak) [ci skip] * New translations devise.en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Thai) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations activerecord.en.yml (Thai) [ci skip] * New translations devise.en.yml (Thai) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Arabic) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations devise.en.yml (Arabic) [ci skip] * New translations doorkeeper.en.yml (Arabic) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations doorkeeper.en.yml (German) [ci skip] * New translations doorkeeper.en.yml (German) [ci skip] * New translations doorkeeper.en.yml (German) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations activerecord.en.yml (Japanese) [ci skip] * New translations devise.en.yml (Japanese) [ci skip] * New translations doorkeeper.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations devise.en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations devise.en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Corsican) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations activerecord.en.yml (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations doorkeeper.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations doorkeeper.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.json (Arabic) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.json (Basque) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations activerecord.en.yml (Hungarian) [ci skip] * i18n-tasks normalize * yarn manage:translations --- config/locales/activerecord.cy.yml | 4 + config/locales/activerecord.fa.yml | 4 + config/locales/activerecord.hu.yml | 12 + config/locales/activerecord.sl.yml | 4 + config/locales/activerecord.th.yml | 4 + config/locales/ar.yml | 16 +- config/locales/cs.yml | 8 +- config/locales/cy.yml | 78 +++- config/locales/devise.ar.yml | 4 +- config/locales/devise.cy.yml | 3 + config/locales/devise.hu.yml | 52 +-- config/locales/devise.sk.yml | 5 + config/locales/devise.th.yml | 20 + config/locales/doorkeeper.ar.yml | 2 +- config/locales/doorkeeper.cy.yml | 3 +- config/locales/doorkeeper.de.yml | 14 +- config/locales/doorkeeper.hu.yml | 61 ++- config/locales/doorkeeper.sl.yml | 136 +++++++ config/locales/doorkeeper.th.yml | 114 +++++- config/locales/es.yml | 80 ++++ config/locales/eu.yml | 18 +- config/locales/fa.yml | 53 +++ config/locales/hu.yml | 725 ++++++++++++++++++++++++++++++----- config/locales/ja.yml | 2 +- config/locales/simple_form.cy.yml | 28 +- config/locales/simple_form.el.yml | 6 +- config/locales/simple_form.fa.yml | 9 + config/locales/simple_form.hu.yml | 115 +++++- config/locales/simple_form.sk.yml | 4 + config/locales/simple_form.th.yml | 19 +- config/locales/simple_form.zh-CN.yml | 2 +- config/locales/sk.yml | 79 +++- config/locales/th.yml | 83 +++- config/locales/zh-CN.yml | 298 +++++++++++++- 34 files changed, 1836 insertions(+), 229 deletions(-) (limited to 'config') diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml index 265530124..19547df98 100644 --- a/config/locales/activerecord.cy.yml +++ b/config/locales/activerecord.cy.yml @@ -1,6 +1,10 @@ --- cy: activerecord: + attributes: + poll: + expires_at: Terfyn + options: Dewisiadau errors: models: account: diff --git a/config/locales/activerecord.fa.yml b/config/locales/activerecord.fa.yml index 1cd50eec7..d3b01a158 100644 --- a/config/locales/activerecord.fa.yml +++ b/config/locales/activerecord.fa.yml @@ -1,6 +1,10 @@ --- fa: activerecord: + attributes: + poll: + expires_at: مهلت + options: گزینه‌ها errors: models: account: diff --git a/config/locales/activerecord.hu.yml b/config/locales/activerecord.hu.yml index 52314c50c..bbc18bc33 100644 --- a/config/locales/activerecord.hu.yml +++ b/config/locales/activerecord.hu.yml @@ -1 +1,13 @@ +--- hu: + activerecord: + attributes: + poll: + expires_at: Határidő + options: Lehetőségek + errors: + models: + account: + attributes: + username: + invalid: csak betűk, számok vagy alávonás diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml index d98e3b676..1df109579 100644 --- a/config/locales/activerecord.sl.yml +++ b/config/locales/activerecord.sl.yml @@ -1,6 +1,10 @@ --- sl: activerecord: + attributes: + poll: + expires_at: Rok + options: Izbire errors: models: account: diff --git a/config/locales/activerecord.th.yml b/config/locales/activerecord.th.yml index 7890ed80c..6719422a2 100644 --- a/config/locales/activerecord.th.yml +++ b/config/locales/activerecord.th.yml @@ -1,6 +1,10 @@ --- th: activerecord: + attributes: + poll: + expires_at: กำหนดเวลาสิ้นสุด + options: ทางเลือก errors: models: account: diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 8da2de5ec..eb6a5ef06 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -58,7 +58,7 @@ ar: last_active: آخر نشاط link_verified_on: تم التحقق مِن مالك هذا الرابط بتاريخ %{date} media: الوسائط - moved_html: "%{name} إنتقلَ إلى %{new_profile_link} :" + moved_html: "%{name} إنتقلَ إلى %{new_profile_link}:" network_hidden: إنّ المعطيات غير متوفرة nothing_here: لا يوجد أي شيء هنا! people_followed_by: الأشخاص الذين يتبعهم %{name} @@ -575,7 +575,7 @@ ar: x_months: "%{count} شه" x_seconds: "%{count}ث" deletes: - bad_password_msg: محاولة جيدة يا هاكرز ! كلمة السر خاطئة + bad_password_msg: محاولة جيدة يا هاكرز! كلمة السر خاطئة confirm_password: قم بإدخال كلمتك السرية الحالية للتحقق من هويتك proceed: حذف حساب success_msg: تم حذف حسابك بنجاح @@ -753,15 +753,15 @@ ar: follow_request: action: إدارة طلبات المتابَعة body: طلب %{name} متابعتك - subject: 'متابع مُعلّق : %{name}' + subject: 'متابع مُعلّق: %{name}' title: طلب متابَعة جديد mention: action: الرد - body: 'أشار إليك %{name} في :' + body: 'أشار إليك %{name} في:' subject: لقد قام %{name} بذِكرك title: إشارة جديدة reblog: - body: 'قام %{name} بترقية منشورك :' + body: 'قام %{name} بترقية منشورك:' subject: قام %{name} بترقية منشورك title: ترقية جديدة number: @@ -975,15 +975,15 @@ ar: explanation: ها هي بعض النصائح قبل بداية الاستخدام final_action: اشرَع في النشر final_step: |- - يمكنك الشروع في النشر في الحين! حتى و إن لم كنت لا تمتلك متابِعين بعدُ، يمكن للآخرين الإطلاع على منشوراتك الموجهة للجمهور على الخيط المحلي أو إن قمت باستخدام وسوم. + يمكنك الشروع في النشر في الحين! حتى و إن لم كنت لا تمتلك متابِعين بعدُ، يمكن للآخرين الإطلاع على منشوراتك الموجهة للجمهور على الخيط العام المحلي أو إن قمت باستخدام وسوم. ابدأ بتقديم نفسك باستعمال وسم #introductions. full_handle: عنوانك الكامل full_handle_hint: هذا هو ما يجب تقديمه لأصدقائك قصد أن يكون بإمكانهم متابَعتك أو مُراسَلتك حتى و إن كانت حساباتهم على خوادم أخرى. review_preferences_action: تعديل التفضيلات subject: أهلًا بك على ماستدون tip_federated_timeline: الخيط الزمني الفديرالي هو بمثابة شبه نظرة شاملة على شبكة ماستدون. غير أنه لا يشمل إلا على الأشخاص المتابَعين مِن طرف جيرانك و جاراتك، لذا فهذا الخيط لا يعكس كافة الشبكة برُمّتها. - tip_following: أنت تتبع تلقائيا مديري و مديرات الخادم. للعثور على أشخاص مميزين أو قد تهمك حساباتهم بإمكانك الإطلاع على الخيوط المحلية و كذا الفدرالية. - tip_local_timeline: الخيط الزمني المحلي هو بمثابة نظرة سريعة على الأشخاص المتواجدين على %{instance} يمكن اعتبارهم كجيرانك وجاراتك الأقرب إليك! + tip_following: أنت تتبع تلقائيا مديري و مديرات الخادم. للعثور على أشخاص مميزين أو قد تهمك حساباتهم بإمكانك الإطلاع على الخيوط العامة المحلية و كذا الفدرالية. + tip_local_timeline: الخيط العام المحلي هو بمثابة نظرة سريعة على الأشخاص المتواجدين على %{instance} يمكن اعتبارهم كجيرانك وجاراتك الأقرب إليك! tips: نصائح title: أهلاً بك، %{name}! users: diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 574754738..0735a8698 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -30,10 +30,10 @@ cs: server_stats: 'Statistika serveru:' source_code: Zdrojový kód status_count_after: - few: příspěvky - many: příspěvků - one: příspěvek - other: příspěvků + few: tooty + many: tootů + one: toot + other: tootů status_count_before: Kteří napsali tagline: Sledujte své přátele a objevujte nové terms: Podmínky používání diff --git a/config/locales/cy.yml b/config/locales/cy.yml index adf6bc1d0..080e89214 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -160,7 +160,7 @@ cy: promote: Hyrwyddo protocol: Protocol public: Cyhoeddus - push_subscription_expires: Tanysgrifiad PuSH yn dod i ben + push_subscription_expires: Tanysgrifiad gwthiadwy yn dod i ben redownload: Adnewyddu proffil reject: Gwrthod reject_all: Gwrthod pob un @@ -609,6 +609,13 @@ cy: explanation: Darganfod defnyddwyr yn seiliedig ar eu diddordebau explore_mastodon: Archwilio %{title} how_to_enable: Ar hyn o bryd nid ydych chi wedi dewis y cyfeiriadur. Gallwch ddewis i mewn isod. Defnyddiwch hashnodau yn eich bio-destun i'w restru dan hashnodau penodol! + people: + few: "%{count} o bobl" + many: "%{count} o bobl" + one: "%{count} berson" + other: "%{count} o bobl" + two: "%{count} o bobl" + zero: "%{count} person" errors: '403': Nid oes gennych ganiatad i weld y dudalen hon. '404': Nid yw'r dudalen yr oeddech yn chwilio amdani'n bodoli. @@ -842,36 +849,87 @@ cy: no_account_html: Heb gyfrif? Mae modd i chi gofrestru yma proceed: Ymlaen i ddilyn prompt: 'Yr ydych am ddilyn:' + reason_html: |- + Pam yw'r cam hyn yn angenrheidiol? + Efallai nid yw %{instance} yn gweinydd ble wnaethoch gofrestru, felly mae'n rhaid i ni ailarweinio chi at eich gweinydd catref yn gyntaf. + remote_interaction: + favourite: + proceed: Ymlaen i hoffi + prompt: 'Hoffech hoffi''r tŵt hon:' + reblog: + proceed: Ymlaen i fŵstio + prompt: 'Hoffech fŵstio''r tŵt hon:' + reply: + proceed: Ymlaen i ateb + prompt: 'Hoffech ateb y tŵt hon:' remote_unfollow: error: Gwall title: Teitl unfollowed: Dad-ddilynwyd + scheduled_statuses: + over_daily_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig ar y dydd hynny + over_total_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig + too_soon: Mae rhaid i'r dydd rhestredig fod yn y dyfodol sessions: activity: Gweithgaredd ddiwethaf browser: Porwr browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Porwr anhysbys + ie: Internet Explorer + micro_messenger: MicroMessenger nokia: Porwr Nokia S40 Ovi + opera: Opera + otter: Otter + phantom_js: PhantomJS qq: Porwr QQ + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Sesiwn cyfredol description: "%{browser} ar %{platform}" explanation: Dyma'r porwyr gwê sydd wedi mewngofnodi i'ch cyfrif Mastododon ar hyn o bryd. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: OS Chrome + firefox_os: OS Firefox + ios: iOS + linux: Linux + mac: Mac other: platfform anhysbys + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Ffôn Windows revoke: Diddymu revoke_success: Sesiwn wedi ei ddiddymu yn llwyddiannus title: Sesiynau settings: + account: Cyfrif + account_settings: Gosodiadau'r cyfrif + appearance: Arddangosiad authorized_apps: Apiau awdurdodedig back: Yn ôl i Mastodon delete: Dileu cyfrif development: Datblygu edit_profile: Golygu proffil export: Allforio data + featured_tags: Hashnodau Nodedig + identity_proofs: Profiadau Hunaniaeth import: Mewnforio + import_and_export: Mewnfori ac allfori migrate: Mudo cyfrif notifications: Hysbysiadau preferences: Dewisiadau + profile: Proffil + relationships: Dilynion a dilynwyr two_factor_authentication: Awdurdodi dau-gam statuses: attached: @@ -907,8 +965,18 @@ cy: ownership: Ni ellir pinio tŵt rhywun arall private: Ni ellir pinio tŵt nad yw'n gyhoeddus reblog: Ni ellir pinio bŵstiau + poll: + total_votes: + few: "%{count} o bleidleisiau" + many: "%{count} o bleidleisiau" + one: "%{count} bleidlais" + other: "%{count} o bleidleisiau" + two: "%{count} o bleidleisiau" + zero: "%{count} pleidlais" + vote: Pleidleisio show_more: Dangos mwy sign_in_to_participate: Mengofnodwch i gymryd rhan yn y sgwrs + title: '%{name}: "%{quote}"' visibilities: private: Dilynwyr yn unig private_long: Dangos i ddilynwyr yn unig @@ -1007,6 +1075,10 @@ cy: contrast: Mastodon (Cyferbyniad uchel) default: Mastodon (Tywyll) mastodon-light: Mastodon (golau) + time: + formats: + default: "%b %d, %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: Mewnbynwch y côd a grewyd gan eich ap dilysu i gadarnhau description_html: Os ydych yn galluogi awdurdodi dau-gam, bydd mewngofnodi yn gofyn i chi fod a'ch ffôn gerllaw er mwyn cynhyrchu tocyn i chi gael mewnbynnu. @@ -1031,6 +1103,8 @@ cy: warning: explanation: disable: Er bod eich cyfrif wedi'i rewi, mae eich data cyfrif yn parhau i fod yn gyfan, ond ni allwch chi berfformio unrhyw gamau nes ei ddatgloi. + silence: Pan mae eich cyfrif yn gyfyngiedig, dim ond pobl sydd yn barod yn eich dilyn yn gweld eich tŵtiau ar y gweinydd hon, a efallai byddwch yn cael eich tynnu o restrau cyhoeddus. Er hyn, gall eraill eich dilyn chi wrth law. + suspend: Mae eich cyfrif wedi cael ei wahardd, a mae gyd o'ch tŵtiau a'ch ffeiliau cyfrwng uwchlwythadwy wedi cael eu tynnu or gweinydd yn barhaol, ac o weinyddau ble yr oedd eich dilynwyr. review_server_policies: Adolygu polisïau'r gweinydd subject: disable: Mae'ch cyfrif %{acct} wedi'i rewi @@ -1056,7 +1130,7 @@ cy: tip_federated_timeline: Mae'r ffrwd ffederasiwn yn olwg firehose o'r rhwydwaith Mastodon. Ond mae ond yn cynnwys y bobl mae eich cymdogion wedi ymrestru iddynt, felly nid yw'n gyflawn. tip_following: Rydych yn dilyn goruwchwyliwr eich gweinydd yn ddiofyn. I ganfod pobl mwy diddorol, edrychwch ar y ffrydiau lleol a'r rhai wedi ei ffedereiddio. tip_local_timeline: Mae'r ffrwd leol yn olwg firehose o bobl ar %{instance}. Dyma eich cymdogion agosaf! - tip_mobile_webapp: Os yw eich porwr gwe yn cynnig i ch ychwanegu Mastodon i'ch sgrîn gartref, mae modd i chi dderbyn hysbysiadau push. Mewn sawl modd mae'n gweithio fel ap cynhenid! + tip_mobile_webapp: Os yw eich porwr gwe yn cynnig i chi ychwanegu Mastodon i'ch sgrîn gartref, mae modd i chi dderbyn hysbysiadau gwthiadwy. Mewn sawl modd mae'n gweithio fel ap cynhenid! tips: Awgrymiadau title: Croeso, %{name}! users: diff --git a/config/locales/devise.ar.yml b/config/locales/devise.ar.yml index 92e2135ba..366bd81b9 100644 --- a/config/locales/devise.ar.yml +++ b/config/locales/devise.ar.yml @@ -25,7 +25,7 @@ ar: subject: 'ماستدون: تعليمات التأكيد لمثيل الخادوم %{instance}' title: للتحقق من عنوان البريد الإلكتروني email_changed: - explanation: 'لقد تم تغيير عنوان البريد الإلكتروني الخاص بحسابك إلى :' + explanation: 'لقد تم تغيير عنوان البريد الإلكتروني الخاص بحسابك إلى:' extra: إن لم تقم شخصيًا بتعديل عنوان بريدك الإلكتروني ، ذلك يعني أنّ شخصا آخر قد نَفِذَ إلى حسابك. فالرجاء قم بتعديل كلمتك السرية في الحال أو قم بالإتصال بمدير مثيل الخادوم إن كنت غير قادر على استعمال حسابك. subject: 'ماستدون: تم استبدال عنوان بريدك الإلكتروني' title: عنوان البريد الإلكتروني الجديد @@ -84,5 +84,5 @@ ar: many: "%{count} أخطاء منعت هذا %{resource} من الحفظ:" one: 'خطأ واحد منع هذا %{resource} من الحفظ:' other: "%{count} أخطاء منعت هذا %{resource} من الحفظ:" - two: 'أخطاء منعت هذا %{resource} من الحفظ:' + two: "%{count} أخطاء منعت هذا %{resource} من الحفظ:" zero: "%{count} أخطاء منعت هذا %{resource} من الحفظ:" diff --git a/config/locales/devise.cy.yml b/config/locales/devise.cy.yml index 8ea7e1b4f..727c71464 100644 --- a/config/locales/devise.cy.yml +++ b/config/locales/devise.cy.yml @@ -12,6 +12,7 @@ cy: last_attempt: Mae gennych un cyfle arall cyn i'ch cyfrif gael ei gloi. locked: Mae eich cyfrif wedi ei gloi. not_found_in_database: "%{authentication_keys} neu gyfrinair annilys." + pending: Mae eich cyfrif dal o dan adolygiad. timeout: Mae eich sesiwn wedi dod i ben. Mewngofnodwch eto i barhau. unauthenticated: Mae angen i chi fewngofnodi neu gofrestru cyn parhau. unconfirmed: Mae rhaid i chi gadarnhau eich cyfeiriad e-bost cyn parhau. @@ -20,6 +21,7 @@ cy: action: Gwiriwch eich cyfeiriad e-bost action_with_app: Cadarnhau a dychwelyd i %{app} explanation: Yr ydych wedi creu cyfrif ar %{host} gyda'r cyfrif e-bost hwn. Dim ond un clic sydd angen i'w wneud yn weithredol. Os nad chi oedd hyn, anwybyddwch yr e-bost hwn os gwelwch yn dda. + explanation_when_pending: Rydych wedi gwneud cais am wahoddiad i %{host} gyda'r ebost hyn. Unwaith rydych wedi cadarnhau eich ebost, byddem yn adolygu eich cais. Ni fyddwch yn gallu mewngofnodi tan yr amser hono. Os caiff eich cais ei wrthod, felly nid oes angen unrhyw gweithred pellach o chi. Os nad oeddech wedi gwneud y cais hyn, anwybyddu'r ebost hon, os gwelwch yn dda. extra_html: Gwnewch yn siŵr i edrych ar reolau'r achos a ein telerau gwasanaeth. subject: 'Mastodon: Canllawiau cadarnhau i %{instance}' title: Gwirio cyfeiriad e-bost @@ -60,6 +62,7 @@ cy: signed_up: Croeso! Rydych wedi cofrestru'n llwyddiannus. signed_up_but_inactive: Yr ydych wedi cofrestru'n llwyddiannus. Fodd bynnag, ni allwn eich mewngofnodi achos nid yw eich cyfrif wedi ei actifadu eto. signed_up_but_locked: Yr ydych wedi cofrestru'n llwyddiannus. Fodd bynnag, ni allwn eich mewngofnodi achos fod eich cyfrif wedi ei gloi. + signed_up_but_pending: Mae neges a'ch cysylltiad cadarnhau wedi cael ei hanfon i'ch ebost. Ar ôl i chi ei glicio, byddem yn adolygu eich cais. Byddwch yn derbyn hysbysiad os caiff ei gymeradwyo. signed_up_but_unconfirmed: Mae neges gyda dolen cadarnhau wedi ei anfon i'ch cyfeiriad e-bost. Dilynwch y ddolen er mwyn actifadu eich cyfrif. Edrychwch yn eich ffolder sbam os na dderbynioch chi'r e-bost hwn os gwelwch yn dda. update_needs_confirmation: Rydych wedi diweddaru eich cyfrif yn llwyddiannus, ond mae angen i ni wirio'ch cyfeiriad e-bost newydd. Edrychwch ar eich e-byst a dilynwch y ddolen gadarnhau er mwyn cadarnhau eich cyfeiriad e-bost newydd. Edrychwch ar eich ffolder sbam os na dderbynioch chi yr e-bost hwn. updated: Mae eich cyfrif wedi ei ddiweddaru yn llwyddiannus. diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml index 67baca016..e495cb5f0 100644 --- a/config/locales/devise.hu.yml +++ b/config/locales/devise.hu.yml @@ -2,40 +2,43 @@ hu: devise: confirmations: - confirmed: Az e-mail címed sikeresen meg lett erősítve. + confirmed: Az e-mail címed sikeresen megerősítésre került. send_instructions: Pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről. send_paranoid_instructions: Ha az e-mail címed létezik az adatbázisunkban, pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről. failure: already_authenticated: Már bejelentkeztél. - inactive: Fiókod még nem lett aktiválva. + inactive: Fiókodat még nem aktiválták. invalid: Helytelen %{authentication_keys} vagy jelszó. - last_attempt: Már csak egy próbálkozásod maradt mielőtt a fiókod lezárásra kerül. + last_attempt: Már csak egy próbálkozásod maradt mielőtt a fiókodat lezárjuk. locked: Fiókod le van zárva. not_found_in_database: Helytelen %{authentication_keys} vagy jelszó. + pending: Fiókod még engedélyezés alatt áll. timeout: A munkamenet lejárt. Jelentkezz be újra a folytatáshoz. unauthenticated: A folytatás előtt be kell jelentkezned. unconfirmed: A folytatás előtt meg kell erősítened az e-mail címed. mailer: confirmation_instructions: action: Erősítsd meg az e-mail címedet - explanation: Ezzel az e-mail címmel kezdeményeztek regisztrációt a(z) %{host} oldalon. Csak egy kattintás, és a felhasználói fiókdat aktiváljuk. Ha a regisztrációt nem te kezdeményezted, kérjük tekintsd ezt az e-mailt tárgytalannak. - extra_html: Kérjük tekintsd át a az instancia szabályzatát és a felhasználási feltételeket. + action_with_app: Megerősítés majd vissza ide %{app} + explanation: Ezzel az e-mail címmel kezdeményeztek regisztrációt a(z) %{host} oldalon. Csak egy kattintás, és a felhasználói fiókodat aktiváljuk. Ha a regisztrációt nem te kezdeményezted, kérjük tekintsd ezt az e-mailt tárgytalannak. + explanation_when_pending: Ezzel az e-mail címmel meghívást kértél a(z) %{host} oldalon. Ahogy megerősíted az e-mail címed, átnézzük a jelentkezésedet. Ennek ideje alatt nem tudsz belépni. Ha a jelentkezésed elutasítjuk, az adataidat töröljük, más teendőd nincs. Ha a kérelmet nem te kezdeményezted, kérjük tekintsd ezt az e-mailt tárgytalannak. + extra_html: Kérjük tekintsd át a a szerver szabályzatát és a felhasználási feltételeket. subject: 'Mastodon: Megerősítési lépések %{instance}' title: E-mail cím megerősítése email_changed: explanation: 'A fiókodhoz tartozó e-mail címet az alábbira módosítod:' - extra: Ha nem te kezdeményezted a fiókodhoz tartozó e-mail cím módosítását, valaki hozzáférhetett a fiókodhoz. Legjobb, ha azonnal megváltoztatod a jelszavadat; ha nem férsz hozzá a fiókodhoz, vedd fel a kapcsolatot az instanciád adminisztrátorával. + extra: Ha nem te kezdeményezted a fiókodhoz tartozó e-mail cím módosítását, valaki hozzáférhetett a fiókodhoz. Legjobb, ha azonnal megváltoztatod a jelszavadat; ha nem férsz hozzá a fiókodhoz, vedd fel a kapcsolatot a szervered adminisztrátorával. subject: 'Mastodon: a fiókodhoz tartozó e-mail címet megváltoztattuk' title: Új e-mail cím password_change: explanation: A fiókodhoz tartozó jelszót megváltoztattuk. - extra: Ha nem te kezdeményezted a fiókodhoz tartozó jelszó módosítását, valaki hozzáférhetett a fiókodhoz. Legjobb, ha azonnal megváltoztatod a jelszavadat; ha nem férsz hozzá a fiókodhoz, vedd fel a kapcsolatot az instanciád adminisztrátorával. - subject: 'Mastodon: Jelszó megváltoztatva' - title: Sikeres jelszó-módosítás + extra: Ha nem te kezdeményezted a fiókodhoz tartozó jelszó módosítását, valaki hozzáférhetett a fiókodhoz. Legjobb, ha azonnal megváltoztatod a jelszavadat; ha nem férsz hozzá a fiókodhoz, vedd fel a kapcsolatot a szervered adminisztrátorával. + subject: 'Mastodon: Jelszavad megváltoztattuk' + title: Sikeres jelszómódosítás reconfirmation_instructions: explanation: Az e-mail cím megváltoztatásához meg kell erősítened az új címet. extra: Amennyiben nem te kezdeményezted a módosítást, kérjük tekintsd ezt az e-mailt tárgytalannak. A Mastodon fiókodhoz tartozó e-mail címed változatlan marad mindaddig, amíg rá nem kattintasz a fenti linkre. - subject: 'Mastodon: erősítsd meg a(z) %{instance} instanciához tartozó e-mail címed' + subject: 'Mastodon: erősítsd meg a(z) %{instance} szerverhez tartozó e-mail címed' title: E-mail cím megerősítése reset_password_instructions: action: Jelszó módosítása @@ -46,30 +49,31 @@ hu: unlock_instructions: subject: 'Mastodon: Feloldási lépések' omniauth_callbacks: - failure: "%{kind} nem hitelesíthető, mert %{reason}." + failure: Sikertelen hitelesítés %{kind} fiókról, mert "%{reason}". success: Sikeres hitelesítés %{kind} fiókról. passwords: - no_token: Nem férhetsz hozzá az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd. - send_instructions: Pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. - send_paranoid_instructions: Ha létezik az e-mail cím, pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. + no_token: Nem férhetsz hozzá ehhez az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd. + send_instructions: Pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. Kérlek ellenőrizd a levélszemét mappádat, ha nem kaptál ilyen e-mailt. + send_paranoid_instructions: Ha létezik az e-mail cím, pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat. Kérlek ellenőrizd a levélszemét mappádat, ha nem kaptál ilyen e-mailt. updated: Jelszavad sikeresen frissült. Bejelentkeztél. - updated_not_active: Jelszavad sikeresen meg lett változtatva. + updated_not_active: Jelszavad sikeresen megváltoztattuk. registrations: - destroyed: Viszlát! A fiókod sikeresen törölve. Reméljük hamarosan viszontláthatunk. + destroyed: Viszlát! A fiókodat sikeresen töröltük. Reméljük hamarosan viszontláthatunk. signed_up: Üdvözlünk! Sikeresen regisztráltál. - signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod még nem lett aktiválva. - signed_up_but_locked: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod le lett zárva. - signed_up_but_unconfirmed: Egy üzenet a megerősítési linkkel kiküldésre került az e-mail címedre. Kérjük használd a linket a fiókod aktiválásához. - update_needs_confirmation: Sikeresen frissítetted a fiókodat, de szükségünk van az e-mail címed megerősítésére. Kérlek ellenőrizd az e-mailedet és kövesd a levélben szereplő megerősítési linket az e-mail címed megerősítéséhez. + signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókodat még nem aktiválták. + signed_up_but_locked: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod le van zárva. + signed_up_but_pending: Egy üzenetet a megerősítési linkkel kiküldtünk az e-mail címedre. Ha kattintasz a linkre, átnézzük a kérelmedet. Értesítünk, ha jóváhagytuk. + signed_up_but_unconfirmed: Egy üzenetet a megerősítési linkkel kiküldtünk az e-mail címedre. Kérjük használd a linket a fiókod aktiválásához. + update_needs_confirmation: Sikeresen frissítetted a fiókodat, de szükségünk van az e-mail címed megerősítésére. Kérlek ellenőrizd az e-mailedet és kövesd a levélben szereplő megerősítési linket az e-mail címed megerősítéséhez. Ellenőrizd a levélszemét mappád, ha nem kaptál volna ilyen levelet. updated: Fiókod frissítése sikeres. sessions: - already_signed_out: Sikeres kijelenkezés. + already_signed_out: Sikeres kijelentkezés. signed_in: Sikeres bejelentkezés. signed_out: Sikeres kijelentkezés. unlocks: - send_instructions: Pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. - send_paranoid_instructions: Ha a fiókod létezik, pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. - unlocked: A fiókod sikeresen fel lett oldva. Jelentkezz be a folytatáshoz. + send_instructions: Pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. Ellenőrizd a levélszemét mappád, ha nem kaptál volna ilyen levelet. + send_paranoid_instructions: Ha a fiókod létezik, pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel. Ellenőrizd a levélszemét mappád, ha nem kaptál volna ilyen levelet. + unlocked: A fiókodat sikeresen feloldottuk. Jelentkezz be a folytatáshoz. errors: messages: already_confirmed: már meg lett erősítve, kérjük jelentkezz be diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index 8842abe61..85de603d3 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -80,3 +80,8 @@ sk: expired: vypŕšal, prosím, vyžiadaj si nový not_found: nenájdený not_locked: nebol zamknutý + not_saved: + few: "%{count} chýb zabránilo uloženiu tohto %{resource}:" + many: "%{count} chýb zabránilo uloženiu tohto %{resource}:" + one: '1 chyba zabránila uloženiu tohto %{resource}:' + other: "%{count} chyby zabránili uloženiu tohto %{resource}:" diff --git a/config/locales/devise.th.yml b/config/locales/devise.th.yml index c6c75b98f..8a9a65465 100644 --- a/config/locales/devise.th.yml +++ b/config/locales/devise.th.yml @@ -2,17 +2,37 @@ th: devise: confirmations: + confirmed: ยืนยันที่อยู่อีเมลของคุณสำเร็จ send_instructions: You will receive an email with instructions for how to confirm your email address in a few minutes. send_paranoid_instructions: If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes. + failure: + already_authenticated: คุณได้ลงชื่อเข้าอยู่แล้ว + inactive: ยังไม่ได้เปิดใช้งานบัญชีของคุณ + invalid: "%{authentication_keys} หรือรหัสผ่านไม่ถูกต้อง" + not_found_in_database: "%{authentication_keys} หรือรหัสผ่านไม่ถูกต้อง" + timeout: เซสชันของคุณหมดอายุแล้ว โปรดลงชื่อเข้าอีกครั้งเพื่อดำเนินการต่อ + mailer: + email_changed: + title: ที่อยู่อีเมลใหม่ + password_change: + subject: 'Mastodon: เปลี่ยนรหัสผ่านแล้ว' + title: เปลี่ยนรหัสผ่านแล้ว + reset_password_instructions: + action: เปลี่ยนรหัสผ่าน passwords: send_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. send_paranoid_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. registrations: signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account. update_needs_confirmation: You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address. + sessions: + already_signed_out: ลงชื่อออกสำเร็จ + signed_in: ลงชื่อเข้าสำเร็จ + signed_out: ลงชื่อออกสำเร็จ unlocks: send_instructions: You will receive an email with instructions for how to unlock your account in a few minutes. send_paranoid_instructions: If your account exists, you will receive an email with instructions for how to unlock it in a few minutes. errors: messages: not_found: ไม่พบ + not_locked: ไม่ได้ล็อคอยู่ diff --git a/config/locales/doorkeeper.ar.yml b/config/locales/doorkeeper.ar.yml index 75d66086f..6f9e38f8b 100644 --- a/config/locales/doorkeeper.ar.yml +++ b/config/locales/doorkeeper.ar.yml @@ -29,7 +29,7 @@ ar: edit: title: تعديل التطبيق form: - error: عفوا ! تحقق من خُلوّ الاستمارة من الأخطاء من فضلك + error: عفوا! تحقق من خُلوّ الاستمارة من الأخطاء من فضلك help: native_redirect_uri: إستخدم %{native_redirect_uri} للاختبار و التجريب محليا redirect_uri: إستخدم خطا واحدا لكل رابط diff --git a/config/locales/doorkeeper.cy.yml b/config/locales/doorkeeper.cy.yml index f51e1b464..19798c4d9 100644 --- a/config/locales/doorkeeper.cy.yml +++ b/config/locales/doorkeeper.cy.yml @@ -72,6 +72,7 @@ cy: index: application: Rhaglen created_at: Awdurdodedig + date_format: "%Y-%m-%d% %H:%M:%S" scopes: Rhinweddau title: Eich rhaglenni awdurdodedig errors: @@ -114,7 +115,7 @@ cy: title: Mae awdurdodiad OAuth yn ofynnol scopes: follow: addasu perthnasau cyfrif - push: derbyn eich hysbysiadau PUSH + push: derbyn eich hysbysiadau gwthiadwy read: darllen holl ddata eich cyfrif read:accounts: gweld gwybodaeth y cyfrif read:blocks: gweld eich blociau diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml index 771b89251..edf9c660c 100644 --- a/config/locales/doorkeeper.de.yml +++ b/config/locales/doorkeeper.de.yml @@ -23,7 +23,7 @@ de: cancel: Abbrechen destroy: Löschen edit: Bearbeiten - submit: Übertragen + submit: Speichern confirmations: destroy: Bist du sicher? edit: @@ -31,8 +31,8 @@ de: form: error: Hoppla! Bitte überprüfe das Formular auf mögliche Fehler help: - native_redirect_uri: "%{native_redirect_uri} für lokale Tests benutzen" - redirect_uri: Bitte benutze eine Zeile pro URI + native_redirect_uri: Benutze %{native_redirect_uri} für lokale Tests + redirect_uri: Benutze eine Zeile pro URI scopes: Bitte die Befugnisse mit Leerzeichen trennen. Zur Verwendung der Standardwerte freilassen. index: application: Anwendung @@ -77,19 +77,19 @@ de: title: Deine autorisierten Anwendungen errors: messages: - access_denied: Der »resource owner« oder der Autorisierungs-Server hat die Anfrage verweigert. - credential_flow_not_configured: Die Prozedur »Resource Owner Password Credentials« schlug fehl, da Doorkeeper.configure.resource_owner_from_credentials nicht konfiguriert ist. + access_denied: Die Anfrage wurde durch Benutzer_in oder Autorisierungs-Server verweigert. + credential_flow_not_configured: Das Konto konnte nicht gefunden werden, da Doorkeeper.configure.resource_owner_from_credentials nicht konfiguriert ist. invalid_client: 'Client-Authentifizierung ist fehlgeschlagen: Client unbekannt, keine Authentisierung mitgeliefert oder Authentisierungsmethode wird nicht unterstützt.' invalid_grant: Die beigefügte Autorisierung ist ungültig, abgelaufen, wurde widerrufen, einem anderen Client ausgestellt oder der Weiterleitungs-URI stimmt nicht mit der Autorisierungs-Anfrage überein. invalid_redirect_uri: Der beigefügte Weiterleitungs-URI ist ungültig. invalid_request: Die Anfrage enthält ein nicht-unterstütztes Argument, ein Parameter fehlt, oder sie ist anderweitig fehlerhaft. - invalid_resource_owner: Die angegebenen Zugangsdaten für den Ressourcenbesitzer sind ungültig oder der Ressourcenbesitzer kann nicht gefunden werden + invalid_resource_owner: Die angegebenen Zugangsdaten für das Konto sind ungültig oder das Konto kann nicht gefunden werden invalid_scope: Die angeforderte Befugnis ist ungültig, unbekannt oder fehlerhaft. invalid_token: expired: Der Zugriffs-Token ist abgelaufen revoked: Der Zugriffs-Token wurde widerrufen unknown: Der Zugriffs-Token ist ungültig - resource_owner_authenticator_not_configured: Die Prozedur »Resource Owner find« ist fehlgeschlagen, da Doorkeeper.configure.resource_owner_authenticator nicht konfiguriert ist. + resource_owner_authenticator_not_configured: Das Konto konnte nicht gefunden werden, da Doorkeeper.configure.resource_owner_authenticator nicht konfiguriert ist. server_error: Der Autorisierungs-Server hat ein unerwartetes Problem festgestellt und konnte die Anfrage nicht bearbeiten. temporarily_unavailable: Der Autorisierungs-Server ist aufgrund von zwischenzeitlicher Überlastung oder Wartungsarbeiten derzeit nicht in der Lage, die Anfrage zu bearbeiten. unauthorized_client: Der Client ist nicht dazu autorisiert, diese Anfrage mit dieser Methode auszuführen. diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml index da57aaf9b..122392864 100644 --- a/config/locales/doorkeeper.hu.yml +++ b/config/locales/doorkeeper.hu.yml @@ -3,8 +3,8 @@ hu: activerecord: attributes: doorkeeper/application: - name: Név - redirect_uri: Visszairányító URI + name: Alkalmazás neve + redirect_uri: Átirányító URI scopes: Hatáskör website: Az alkalmazás weboldala errors: @@ -33,22 +33,23 @@ hu: help: native_redirect_uri: Használj %{native_redirect_uri} a helyi tesztekhez redirect_uri: Egy sor URI-nként - scopes: A nézeteket szóközzel válaszd el. Hagyd üresen az alapértelmezett nézetekhez. + scopes: A hatásköröket szóközzel válaszd el. Hagyd üresen az alapértelmezett hatáskörökhöz. index: application: Alkalmazás + callback_url: Callback URL delete: Eltávolítás name: Név new: Új alkalmazás scopes: Hatáskör show: Mutat - title: Alkalmazásod + title: Alkalmazásaid new: title: Új alkalmazás show: actions: Műveletek application_id: Alkalmazás azonosító - callback_urls: Callback urlek - scopes: Nézetek + callback_urls: Callback URL-ek + scopes: Hatáskörök secret: Titok title: 'Alkalmazás: %{name}' authorizations: @@ -61,6 +62,8 @@ hu: able_to: Képes lesz prompt: "%{client_name} nevű alkalmazás engedélyt kér a fiókodhoz való hozzáféréshez." title: Engedély szükséges + show: + title: Másold le ezt az engedélyező kódot és írd be az alkalmazásba. authorized_applications: buttons: revoke: Visszavonás @@ -68,27 +71,28 @@ hu: revoke: Biztos vagy benne? index: application: Alkalmazás - created_at: Készítve + created_at: Felhatalmazva + date_format: "%Y-%m-%d %H:%M:%S" scopes: Hatáskör title: Engedélyezett alkalmazásaid errors: messages: - access_denied: Az erőforrás tulajdonosa vagy hitelesítő kiszolgálója megtakadta a kérést. + access_denied: Az erőforrás tulajdonosa vagy hitelesítő kiszolgálója megtagadta a kérést. credential_flow_not_configured: Az erőforrás tulajdonos jelszóadatainak átadása megszakadt, mert a Doorkeeper.configure.resource_owner_from_credentials beállítatlan. - invalid_client: A kliens hitelesítése megszakadt, mert a ismeretlen a kliens, kliens nem küldött hitelesítést, vagy ismeretlen a kliens - invalid_grant: A biztosított hitelesítés érvénytelen, lejárt, visszavont, vagy nem egyezik a hitelesítéi kérésben használt URIval, vagy más kliensnek lett címezve. - invalid_redirect_uri: A redirect uri nem valós. - invalid_request: A kérésből hiányzik egy szükséges paraméter, nem támogatott paramétert tartalmaz, vagy egyéb módon hibás. + invalid_client: A kliens hitelesítése megszakadt, mert ismeretlen a kliens, a kliens nem küldött hitelesítést, vagy a hitelesítés módja nem támogatott. + invalid_grant: A biztosított hitelesítés érvénytelen, lejárt, visszavont, vagy nem egyezik a hitelesítési kérésben használt URI-val, vagy más kliensnek címezték. + invalid_redirect_uri: Az átirányító URI nem valós. + invalid_request: A kérésből hiányzik egy szükséges paraméter, nem támogatott paramétert tartalmaz, vagy máshogy sérült. invalid_resource_owner: A biztosított erőforrás tulajdonosának hitelesítő adatai nem valósak, vagy az erőforrás tulajdonosa nem található. invalid_scope: A kért nézet érvénytelen, ismeretlen, vagy hibás. invalid_token: expired: Hozzáférési kulcs lejárt - revoked: Hozzáférési kulcs vissza lett vonva + revoked: Hozzáférési kulcsot visszavonták unknown: Hozzáférési kulcs érvénytelen resource_owner_authenticator_not_configured: Erőforrás tulajdonos keresés megszakadt, ugyanis a Doorkeeper.configure.resource_owner_authenticator beállítatlan. server_error: Hitelesítő szervert váratlan esemény érte, mely meggátolta a kérés teljesítését. - temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést egy átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt. - unauthorized_client: A kliens nincs feljogosítva a kérés teljesítésére. + temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt. + unauthorized_client: A kliens nincs feljogosítva erre a kérésre. unsupported_grant_type: A hitelesítés módja nem támogatott a hitelesítő kiszolgálón. unsupported_response_type: A hitelesítő kiszolgáló nem támogatja ezt a választ. flash: @@ -110,6 +114,29 @@ hu: application: title: OAuth engedély szükséges scopes: - follow: fiókok követése, blokkoláse, blokkolás feloldása és követés abbahagyása + follow: fiókok követése, letiltása, tiltás feloldása és követés abbahagyása + push: push értesítések fogadása read: fiókod adatainak olvasása - write: bejegyzés írása a nevedben + read:accounts: fiók adatainak megtekintése + read:blocks: letiltások megtekintése + read:favourites: kedvencek megtekintése + read:filters: szűrök megtekintése + read:follows: követések megtekintése + read:lists: listák megtekintése + read:mutes: némítások megtekintése + read:notifications: értesítések megtekintése + read:reports: bejelentések megtekintése + read:search: nevedben keresés + read:statuses: tülkök megtekintése + write: fiókod adatainak megváltoztatása + write:accounts: profilod megváltoztatása + write:blocks: fiókok és domainek letiltása + write:favourites: tülkök kedvencnek jelölése + write:filters: szűrők létrehozása + write:follows: mások követése + write:lists: listák létrehozása + write:media: média feltöltése + write:mutes: emberek és beszélgetések némítása + write:notifications: értesítések törlése + write:reports: mások bejelentése + write:statuses: tülkök közzététele diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml index c27457089..341ed2830 100644 --- a/config/locales/doorkeeper.sl.yml +++ b/config/locales/doorkeeper.sl.yml @@ -4,3 +4,139 @@ sl: attributes: doorkeeper/application: name: Ime programa + redirect_uri: Preusmeritev URI + scopes: Obsegi + website: Spletišče programa + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: ne more vsebovati fragmenta. + invalid_uri: mora biti veljaven URI. + relative_uri: mora biti absolutni URI. + secured_uri: mora biti HTTPS/SSL URI. + doorkeeper: + applications: + buttons: + authorize: Overi + cancel: Prekliči + destroy: Uniči + edit: Uredi + submit: Pošlji + confirmations: + destroy: Ali ste prepričani? + edit: + title: Uredi aplikacijo + form: + error: Ups! Preverite obrazec za morebitne napake + help: + native_redirect_uri: Uporabite %{native_redirect_uri} za krajevne preizkuse + redirect_uri: Uporabite eno vrstico na URI + scopes: Ločite obsege s presledki. Pustite prazno, da uporabite privzete obsege. + index: + application: Program + callback_url: Povratni URL + delete: Izbriši + name: Ime + new: Nov program + scopes: Obsegi + show: Pokaži + title: Vaši programi + new: + title: Nov program + show: + actions: Dejanja + application_id: Ključ odjemalca + callback_urls: Povratni URL-ji + scopes: Obsegi + secret: Skrivnost odjemalca + title: 'Program: %{name}' + authorizations: + buttons: + authorize: Overi + deny: Zavrni + error: + title: Prišlo je do napake + new: + able_to: To bo lahko + prompt: Program %{client_name} zahteva dostop do vašega računa + title: Potrebna je pooblastitev + show: + title: Kopirajte to pooblastilno kodo in jo prilepite v program. + authorized_applications: + buttons: + revoke: Prekliči + confirmations: + revoke: Ali ste prepričani? + index: + application: Program + created_at: Odobreno + date_format: "%Y-%m-%d %H:%M:%S" + scopes: Obsegi + title: Vaši odobreni programi + errors: + messages: + access_denied: Lastnik virov ali strežnik pooblastil je zavrnil zahtevo. + credential_flow_not_configured: Pretok geselskih pooblastil lastnika virov ni uspel, ker Doorkeeper.configure.resource_owner_from_credentials ni nastavljen. + invalid_client: Overitev odjemalca ni uspelo zaradi neznanega odjemalca, zaradi nevključitve overitve odjemalca ali zaradi nepodprte metode overitve. + invalid_grant: Predložena odobritev za pooblastilo je neveljavna, potekla, preklicana, se ne ujema z URI preusmeritvijo, ki je uporabljena v zahtevi za pooblastilo ali je bila izdana drugemu odjemalcu. + invalid_redirect_uri: URI za preusmeritev ni veljaven. + invalid_request: Zahtevku manjka zahtevan parameter, vključuje nepodprto vrednost parametra ali je nepravilno oblikovan. + invalid_resource_owner: Predložene poverilnice lastnika virov niso veljavne ali pa lastnika virov ni mogoče najti + invalid_scope: Zahtevani obseg je neveljaven, neznan ali nepravilen. + invalid_token: + expired: Žeton za dostop je pretekel + revoked: Žeton za dostop je bil preklican + unknown: Žeton za dostop je neveljaven + resource_owner_authenticator_not_configured: Iskanje lastnika virov ni uspelo, ker Doorkeeper.configure.resource_owner_authenticator ni nastavljen. + server_error: Strežnik pooblastil je naletel na nepričakovano stanje, ki je preprečilo, da bi izpolnil zahtevo. + temporarily_unavailable: Strežnik pooblastil, zaradi začasne preobremenitve ali vzdrževanja, trenutno ne more obdelati zahteve. + unauthorized_client: Odjemalec nima pooblastila za izvajanje te zahteve po tej metodi. + unsupported_grant_type: Strežnik pooblastil ne podpira vrste odobritve pooblastila. + unsupported_response_type: Strežnik pooblastil ne podpira te vrste odziva. + flash: + applications: + create: + notice: Program je ustvarjen. + destroy: + notice: Program je izbrisan. + update: + notice: Program je posodobljen. + authorized_applications: + destroy: + notice: Program je preklican. + layouts: + admin: + nav: + applications: Programi + oauth2_provider: Ponudnik OAuth2 + application: + title: Potrebna je OAuth pooblastitev + scopes: + follow: spremeni razmerja med računi + push: prejmi potisna obvestila + read: preberi vse podatke svojega računa + read:accounts: oglejte si podrobnosti računov + read:blocks: oglejte si svoje blokirane + read:favourites: oglejte si svoje priljubljene + read:filters: oglejte si svoje filtre + read:follows: oglejte si svoje sledilce + read:lists: oglejte si svoje sezname + read:mutes: oglejte si svoje utišane + read:notifications: oglejte si svoja obvestila + read:reports: oglejte si svoje prijave + read:search: iščite v svojem imenu + read:statuses: oglejte si vsa stanja + write: spremenite vse podatke svojega računa + write:accounts: spremenite svoj profil + write:blocks: blokirajte račune in domene + write:favourites: priljubljena stanja + write:filters: ustvari filtre + write:follows: sledi osebam + write:lists: ustvarite sezname + write:media: pošlji medije + write:mutes: utišaj osebe in pogovore + write:notifications: počisti svoja obvestila + write:reports: prijavi druge osebe + write:statuses: objavi stanja diff --git a/config/locales/doorkeeper.th.yml b/config/locales/doorkeeper.th.yml index 4b7c9383e..5866baa43 100644 --- a/config/locales/doorkeeper.th.yml +++ b/config/locales/doorkeeper.th.yml @@ -3,46 +3,120 @@ th: activerecord: attributes: doorkeeper/application: - name: ชื่อ - redirect_uri: เปลี่ยนเส้นทาง URI + name: ชื่อแอปพลิเคชัน + redirect_uri: URI การเปลี่ยนเส้นทาง + scopes: ขอบเขต + website: เว็บไซต์แอปพลิเคชัน errors: models: doorkeeper/application: attributes: redirect_uri: - invalid_uri: ต้องใช้ URI ที่ถูกต้อง. - relative_uri: ต้องเป็น absolute URI. - secured_uri: ต้องใช้ HTTPS/SSL URI. + fragment_present: ไม่สามารถมีส่วนย่อย + invalid_uri: ต้องเป็น URI ที่ถูกต้อง + relative_uri: ต้องเป็น URI แบบเต็ม + secured_uri: ต้องเป็น URI แบบ HTTPS/SSL doorkeeper: applications: buttons: + authorize: อนุญาต cancel: ยกเลิก destroy: ทำลาย edit: แก้ไข + submit: ส่ง confirmations: - destroy: แน่ใจนะ? + destroy: คุณแน่ใจหรือไม่? edit: - title: แก้ไข แอ๊ฟพลิเคชั่น + title: แก้ไขแอปพลิเคชัน help: - native_redirect_uri: ใช้ %{native_redirect_uri} สำหรับการทดสอบ - redirect_uri: ใช้บรรทัดละหนึ่ง URI + native_redirect_uri: ใช้ %{native_redirect_uri} สำหรับการทดสอบในเว็บ + redirect_uri: ใช้หนึ่งบรรทัดต่อ URI + scopes: แยกขอบเขตด้วยช่องว่าง เว้นว่างเพื่อใช้ขอบเขตเริ่มต้น index: + application: แอปพลิเคชัน + callback_url: URL เรียกกลับ + delete: ลบ name: ชื่อ - new: New Application + new: แอปพลิเคชันใหม่ + scopes: ขอบเขต + show: แสดง + title: แอปพลิเคชันของคุณ new: - title: New Application + title: แอปพลิเคชันใหม่ show: - application_id: Application Id - callback_urls: Callback urls - secret: Secret + actions: การกระทำ + application_id: กุญแจไคลเอ็นต์ + callback_urls: URL เรียกกลับ + scopes: ขอบเขต + secret: รหัสลับไคลเอ็นต์ + title: 'แอปพลิเคชัน: %{name}' authorizations: buttons: - authorize: อนุญาติ - deny: ไม่อนุญาติ + authorize: อนุญาต + deny: ปฏิเสธ + error: + title: เกิดข้อผิดพลาด + new: + able_to: แอปจะสามารถ + prompt: แอปพลิเคชัน %{client_name} ขอเข้าถึงบัญชีของคุณ + title: ต้องมีการอนุญาต + show: + title: คัดลอกรหัสการอนุญาตนี้แล้ววางลงในแอปพลิเคชัน authorized_applications: buttons: - revoke: ยกเลิกการอนุญาติ + revoke: เพิกถอน + confirmations: + revoke: คุณแน่ใจหรือไม่? + index: + application: แอปพลิเคชัน + created_at: อนุญาตเมื่อ + scopes: ขอบเขต + title: แอปพลิเคชันที่ได้รับอนุญาตของคุณ + errors: + messages: + access_denied: เจ้าของทรัพยากรหรือเซิร์ฟเวอร์การอนุญาตปฏิเสธคำขอ + flash: + applications: + create: + notice: สร้างแอปพลิเคชันแล้ว + destroy: + notice: ลบแอปพลิเคชันแล้ว + update: + notice: อัปเดตแอปพลิเคชันแล้ว + authorized_applications: + destroy: + notice: เพิกถอนแอปพลิเคชันแล้ว + layouts: + admin: + nav: + applications: แอปพลิเคชัน + oauth2_provider: ผู้ให้บริการ OAuth2 + application: + title: ต้องมีการอนุญาต OAuth scopes: - follow: follow, block, unblock and unfollow accounts - read: read your account's data - write: post on your behalf + follow: ปรับเปลี่ยนความสัมพันธ์ของบัญชี + push: รับการแจ้งเตือนแบบผลักของคุณ + read: อ่านข้อมูลบัญชีทั้งหมดของคุณ + read:accounts: ดูข้อมูลบัญชี + read:blocks: ดูการปิดกั้นของคุณ + read:favourites: ดูรายการโปรดของคุณ + read:filters: ดูตัวกรองของคุณ + read:follows: ดูการติดตามของคุณ + read:lists: ดูรายการของคุณ + read:mutes: ดูการปิดเสียงของคุณ + read:notifications: ดูการแจ้งเตือนของคุณ + read:reports: ดูรายงานของคุณ + read:search: ค้นหาในนามของคุณ + read:statuses: ดูสถานะทั้งหมด + write: ปรับเปลี่ยนข้อมูลบัญชีทั้งหมดของคุณ + write:accounts: ปรับเปลี่ยนโปรไฟล์ของคุณ + write:blocks: ปิดกั้นบัญชีและโดเมน + write:favourites: ชื่นชอบสถานะ + write:filters: สร้างตัวกรอง + write:follows: ติดตามผู้คน + write:lists: สร้างรายการ + write:media: อัปโหลดไฟล์สื่อ + write:mutes: ปิดเสียงผู้คนและการสนทนา + write:notifications: ล้างการแจ้งเตือนของคุณ + write:reports: รายงานผู้คนอื่น ๆ + write:statuses: เผยแพร่สถานะ diff --git a/config/locales/es.yml b/config/locales/es.yml index 4a919f52b..49765cd0a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -4,10 +4,13 @@ es: about_hashtag_html: Estos son toots públicos etiquetados con #%{hashtag}. Puedes interactuar con ellos si tienes una cuenta en cualquier parte del fediverso. about_mastodon_html: Mastodon es un servidor de red social libre y de código abierto. Una alternativa descentralizada a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la red social. about_this: Acerca de esta instancia + active_count_after: activo administered_by: 'Administrado por:' + api: API apps: Aplicaciones móviles contact: Contacto contact_missing: No especificado + contact_unavailable: N/A discover_users: Descubrir usuarios documentation: Documentación extended_description_html: | @@ -43,6 +46,7 @@ es: joined: Se unió el %{date} last_active: última conexión link_verified_on: La propiedad de este vínculo fue verificada el %{date} + media: Multimedia moved_html: "%{name} se ha trasladado a %{new_profile_link}:" network_hidden: Esta información no está disponible nothing_here: "¡No hay nada aquí!" @@ -58,6 +62,7 @@ es: reserved_username: El nombre de usuario está reservado roles: admin: Administrador + bot: Bot moderator: Moderador unavailable: Perfil no disponible unfollow: Dejar de seguir @@ -74,6 +79,7 @@ es: approve: Aprobar approve_all: Aprobar todos are_you_sure: "¿Estás seguro?" + avatar: Avatar by_domain: Dominio change_email: changed_msg: "¡El correo electrónico se ha actualizado correctamente!" @@ -104,8 +110,10 @@ es: header: Cabecera inbox_url: URL de la bandeja de entrada invited_by: Invitado por + ip: IP location: all: Todos + local: Local remote: Remoto title: Localización login_status: Estado del login @@ -114,6 +122,7 @@ es: moderation: active: Activo all: Todos + pending: Pendiente silenced: Silenciados suspended: Suspendidos title: Moderación @@ -135,6 +144,7 @@ es: reject: Rechazar reject_all: Rechazar todos remove_avatar: Eliminar el avatar + remove_header: Eliminar cabecera resend_confirmation: already_confirmed: Este usuario ya está confirmado send: Reenviar el correo electrónico de confirmación @@ -159,6 +169,7 @@ es: statuses: Estados subscribe: Suscribir suspended: Susependido + time_in_queue: Esperando en cola %{time} title: Cuentas unconfirmed_email: Correo electrónico sin confirmar undo_silenced: Des-silenciar @@ -211,6 +222,7 @@ es: destroyed_msg: "¡Emojo destruido con éxito!" disable: Deshabilitar disabled_msg: Se deshabilitó con éxito ese emoji + emoji: Emoji enable: Habilitar enabled_msg: Se habilitó con éxito ese emoji image_hint: PNG de hasta 50KB @@ -266,6 +278,10 @@ es: reject_media_hint: Remueve localmente archivos multimedia almacenados para descargar cualquiera en el futuro. Irrelevante para suspensiones reject_reports: Rechazar informes reject_reports_hint: Ignore todos los reportes de este dominio. Irrelevante para suspensiones + rejecting_media: rechazar archivos multimedia + severity: + silence: silenciado + suspend: susependido show: affected_accounts: one: Una cuenta en la base de datos afectada @@ -286,8 +302,19 @@ es: create: Añadir dominio title: Nueva entrada en la lista negra de correo title: Lista negra de correo + followers: + back_to_account: Volver a la cuenta instances: + by_domain: Dominio + moderation: + all: Todos + limited: Limitado + title: Moderación title: Instancias conocidas + total_blocked_by_us: Bloqueado por nosotros + total_followed_by_them: Seguidos por ellos + total_followed_by_us: Seguido por nosotros + total_storage: Archivos multimedia invites: deactivate_all: Desactivar todos filter: @@ -379,6 +406,10 @@ es: min_invite_role: disabled: Nadie title: Permitir invitaciones de + registrations_mode: + modes: + none: Nadie puede registrarse + open: Cualquiera puede registrarse show_known_fediverse_at_about_page: desc_html: Cuando esté activado, se mostrarán toots de todo el fediverso conocido en la vista previa. En otro caso, se mostrarán solamente toots locales. title: Mostrar fediverso conocido en la vista previa de la historia @@ -525,7 +556,9 @@ es: request: Solicitar tu archivo size: Tamaño blocks: Personas que has bloqueado + csv: CSV follows: Personas que sigues + lists: Listas mutes: Tienes en silencio storage: Almacenamiento featured_tags: @@ -553,20 +586,31 @@ es: more: Mas… resources: Recursos generic: + all: Todos changes_saved_msg: "¡Cambios guardados con éxito!" copy: Copiar + order_by: Ordenar por save_changes: Guardar cambios validation_errors: one: "¡Algo no está bien! Por favor, revisa el error" other: "¡Algo no está bien! Por favor, revise %{count} errores más abajo" identity_proofs: + active: Activo + authorize: Sí, autorizar authorize_connection_prompt: "¿Autorizar esta conexión criptográfica?" errors: failed: La conexión criptográfica falló. Por favor, inténtalo de nuevo desde %{provider}. keybase: invalid_token: Los tokens de Keybase son hashes de firmas y deben tener 66 caracteres hex verification_failed: Keybase no reconoce este token como una firma del usuario de Keybase %{kb_username}. Por favor, inténtelo de nuevo desde Keybase. + identity: Identidad + inactive: Inactivo + status: Estado de la verificación + view_proof: Ver prueba imports: + modes: + merge: Unir + overwrite: Sobrescribir preface: Puedes importar ciertos datos, como todas las personas que estás siguiendo o bloqueando en tu cuenta en esta instancia, desde archivos exportados de otra instancia. success: Sus datos se han cargado correctamente y serán procesados en brevedad types: @@ -652,6 +696,9 @@ es: prev: Anterior preferences: other: Otros + relationships: + last_active: Última actividad + most_recent: Más reciente remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta @@ -659,28 +706,61 @@ es: proceed: Proceder a seguir prompt: 'Vas a seguir a:' remote_unfollow: + error: Error title: Título unfollowed: Ha dejado de seguirse sessions: activity: Última actividad browser: Navegador browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Desconocido + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Navegador de Nokia S40 Ovi + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: Navegador QQ + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Sesión actual description: "%{browser} en %{platform}" explanation: Estos son los navegadores web conectados actualmente en tu cuenta de Mastodon. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: Desconocido + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: Revocar revoke_success: Sesión revocada exitosamente title: Sesiones settings: + account: Cuenta + account_settings: Ajustes de la cuenta + appearance: Apariencia authorized_apps: Aplicaciones autorizadas back: Volver al inicio delete: Borrar cuenta development: Desarrollo edit_profile: Editar perfil export: Exportar información + featured_tags: Hashtags destacados import: Importar migrate: Migración de cuenta notifications: Notificaciones diff --git a/config/locales/eu.yml b/config/locales/eu.yml index d83deb214..9b9c2c027 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -60,8 +60,8 @@ eu: following: Onetsi nahi duzun pertsona aurretik jarraitu behar duzu posts: one: Toot - other: Tootak - posts_tab_heading: Tootak + other: Toot + posts_tab_heading: Toot posts_with_replies: Toot eta erantzunak reserved_username: Erabiltzaile-izena erreserbatuta dago roles: @@ -932,10 +932,10 @@ eu:

Zer informazio biltzen dugu?


@@ -983,7 +983,7 @@ eu:

Informazioa kanpoko inorekin partekatzen dugu?

-

Ez dugu identifikatu zaitzakeen informazio pertsonala, saltzen, trukatzen edo kanpora bidaltzen. Salbuespena konfidatzako hiirugarrengoak dira, gunea martxan izaten laguntzen digutenak, negozioa aurrera eramateko aholkua ematen digutenak edo zuri zerbitzua ematen laguntzen digutenak, hauek informazioaren konfidentzialtasuna errespetatzea onartzen dutenean., Agian legearekin betetzeko beharrezkoa den informazioa ere eman genezake, gunearen politika indarrean jartzeko behar dena, edo gure eskubideak, jabetzak, edo segurtasuna babesteko beharrezkoa dena.

+

Ez dugu identifikatu zaitzakeen informazio pertsonala saltzen, trukatzen edo kanpora bidaltzen. Salbuespena konfiantzako hirugarrengoak dira, gunea martxan izaten laguntzen digutenak, negozioa aurrera eramateko aholkua ematen digutenak edo zuri zerbitzua ematen laguntzen digutenak, hauek informazioaren konfidentzialtasuna errespetatzea onartzen dutenean. Agian legearekin betetzeko beharrezkoa den informazioa ere eman genezake, gunearen politika indarrean jartzeko behar dena, edo gure eskubideak, jabetzak, edo segurtasuna babesteko beharrezkoa dena.

Zure eduki publikoak sareko beste zerbitzariek deskargatu dezakete. Zure mezu publikoak eta soilik jarraitzaileentzat diren mezuak zure jarraitzaileen zerbitzarietara bidaltzen dira, jarraitzaile edo hartzaile horiek beste zerbitzari batean badute kontua.

@@ -995,7 +995,7 @@ eu:

Zerbitzari hau Europar Batasunean edo Europako Ekonomia-Eremuan badago: Gure gunea, produktua eta zerbitzuak 16 urte edo gehiago dituztenei zuzenduta daude. 16 urte baino gazteagoa bazara, GDPR legearen arabera ezin duzu gune hau erabili (General Data Protection Regulation)

-

Zerbitzari hau Amerikako Estatu Batuetan badago: Gure gunea, produktua eta zerbitzuak 13 urte edo gehiago dituztenei zuzenduta daude. 13 urte baino gazteagoa bazara, COPPA legearen arabera ezin duzu gune hau erabili (Children's Online Privacy Protection Act).

+

Zerbitzari hau Amerikako Estatu Batuetan badago: Gure gunea, produktua eta zerbitzuak 13 urte edo gehiago dituztenei zuzenduta daude. 13 urte baino gazteagoa bazara, COPPA legearen arabera ezin duzu gune hau erabili (Children's Online Privacy Protection Act).

Zerbitzari hau beste eremu legal batean badago, legearen eskariak desberdinak izan daitezke.

@@ -1003,7 +1003,7 @@ eu:

Aldaketak gure pribatutasun politikan

-

Guire pribatutasun politika aldatzea erabakitzen badugu, aldaketak orri honetan argitaratuko ditugu.

+

Gure pribatutasun politika aldatzea erabakitzen badugu, aldaketak orri honetan argitaratuko ditugu.

Dokumentu honek CC-BY-SA lizentzia du. Eta azkenekoz 2019ko martxoak 7an eguneratu zen

diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 948347f3c..d37dbdeb4 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -68,6 +68,7 @@ fa: admin: مدیر bot: ربات moderator: ناظم + unavailable: نمایهٔ ناموجود unfollow: پایان پیگیری admin: account_actions: @@ -113,6 +114,7 @@ fa: header: زمینه inbox_url: نشانی صندوق ورودی invited_by: دعوت‌شده از طرف + ip: IP joined: عضویت از location: all: همه @@ -268,6 +270,7 @@ fa: created_msg: مسدودکردن دامین در حال انجام است destroyed_msg: مسدودکردن دامین واگردانده شد domain: دامین + existing_domain_block_html: شما پیش‌تر محدودیت‌های سخت‌تری روی %{name} اعمال کرده‌اید، و باید نخست مسدودسازی را لغو کنید. new: create: مسدودسازی hint: مسدودسازی دامین جلوی فهرست‌شدن حساب‌ها در پایگاه داده را نمی‌گیرد، بلکه به طور خودکار روش‌های مدیریتی را روی فعالیت‌های فعلی و گذشتهٔ آن حساب‌ها اعمال می‌کند. @@ -333,6 +336,8 @@ fa: expired: منقضی‌شده title: فیلتر title: دعوت‌ها + pending_accounts: + title: حساب‌های منتظر (%{count}) relays: add_new: افزودن رلهٔ تازه delete: حذف @@ -469,6 +474,7 @@ fa: confirmed: تأییدشده expires_in: مهلت انقضا last_delivery: آخرین ارسال + title: WebSub topic: موضوع tags: accounts: حساب‌ها @@ -529,6 +535,9 @@ fa: migrate_account: نقل مکان به یک حساب دیگر migrate_account_html: اگر می‌خواهید این حساب را به حساب دیگری منتقل کنید، این‌جا را کلیک کنید. or_log_in_with: یا ورود به وسیلهٔ + providers: + cas: CAS + saml: SAML register: عضو شوید registration_closed: سرور %{instance} عضو تازه‌ای نمی‌پذیرد resend_confirmation: راهنمایی برای تأیید را دوباره بفرست @@ -603,6 +612,7 @@ fa: request: درخواست بایگانی داده‌هایتان size: اندازه blocks: حساب‌های مسدودشده + csv: CSV domain_blocks: دامین‌های مسدودشده follows: حساب‌های پی‌گرفته lists: فهرست‌ها @@ -746,11 +756,22 @@ fa: body: "%{name} نوشتهٔ شما را بازبوقید:" subject: "%{name} نوشتهٔ شما را بازبوقید" title: بازبوق تازه + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T pagination: newer: تازه‌تر next: بعدی older: قدیمی‌تر prev: قبلی + truncate: "…" polls: errors: already_voted: شما قبلاً در این نظرسنجی رأی داده‌اید @@ -763,6 +784,8 @@ fa: too_many_options: نمی‌تواند بیشتر از %{max} گزینه داشته باشد preferences: other: سایر تنظیمات + posting_defaults: تنظیمات پیش‌فرض انتشار + public_timelines: فهرست عمومی نوشته‌ها relationships: activity: فعالیت حساب dormant: غیرفعال @@ -805,12 +828,40 @@ fa: activity: آخرین فعالیت browser: مرورگر browsers: + alipay: Alipay + blackberry: بلک‌بری + chrome: کروم + edge: مایکروسافت اج + electron: الکترون + firefox: فایرفاکس generic: مرورگر ناشناخته + ie: اینترنت اکسپلورر + micro_messenger: مایکرومسنجر + nokia: Nokia S40 Ovi Browser + opera: اپرا + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: سافاری + uc_browser: UCBrowser + weibo: Weibo current_session: نشست فعلی description: "%{browser} روی %{platform}" explanation: مرورگرهای زیر هم‌اینک به حساب شما وارد شده‌اند. + ip: IP platforms: + adobe_air: Adobe Air + android: اندروید + blackberry: بلک‌بری + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: لینوکس + mac: مک other: سیستم ناشناخته + windows: ویندوز + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: لغو کردن revoke_success: نشست با موفقیت لغو شد title: نشست‌ها @@ -863,6 +914,7 @@ fa: vote: رأی show_more: نمایش sign_in_to_participate: برای شرکت در گفتگو وارد حساب خود شوید + title: '%{name}: "%{quote}"' visibilities: private: خصوصی private_long: تنها پیگیران شما می‌بینند @@ -964,6 +1016,7 @@ fa: time: formats: default: "%d %b %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: برای تأیید، کدی را که برنامهٔ تأییدکننده ساخته است وارد کنید description_html: اگر ورود دومرحله‌ای را فعال کنید، برای ورود به سیستم به تلفن خود نیاز خواهید داشت تا برایتان یک کد موقتی بسازد. diff --git a/config/locales/hu.yml b/config/locales/hu.yml index cd6a1692f..d771b9683 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1,89 +1,158 @@ --- hu: about: - about_hashtag_html: Ezek a #%{hashtag} címkével ellátott publikus tülkök. Reagálhatsz rájuk, ha már van felhasználói fiókod valahol a föderációban. - about_mastodon_html: Mastodon egy szabad, nyílt forráskódú szociális hálózati kiszolgálo. Egy központosítatlan alternatíva a kereskedelmi platformokra, elkerüli a kommunikációd monopolizációját veszélyét. Bárki futtathatja a Mastodon-t és részt vehet a szociális hálózatban. + about_hashtag_html: Ezek a #%{hashtag} hashtag-gel ellátott publikus tülkök. Reagálhatsz rájuk, ha már van felhasználói fiókod valahol a föderációban. + about_mastodon_html: A Mastodon egy szabad webes protokollokat használó, nyílt forráskódú szociális háló. Decentralizált, akár az e-mail. about_this: Rólunk + active_count_after: aktív + active_footnote: Havonta aktív felhasználók + administered_by: 'Adminisztrátor:' + api: API + apps: Mobil appok + apps_platforms: Használd a Mastodont iOS-ről, Androidról vagy más platformról + browse_directory: Böngészd a profil adatbázist és szűrj érdeklődési kör szerint + browse_public_posts: Nézz bele a Mastodon élő adatfolyamába contact: Kapcsolat contact_missing: Nincs megadva + contact_unavailable: N/A + discover_users: Találj meg másokat + documentation: Dokumentáció extended_description_html: |

Ez itt a szabályzat helye

Még nem állítottál be bővebb leírást.

+ federation_hint_html: Egy %{instance} fiókkal bármely más Mastodon szerveren vagy a föderációban lévő felhasználót követni tudsz. generic_description: "%{domain} csak egy a számtalan szerver közül a föderációban" - hosted_on: "%{domain} Mastodon instancia" + get_apps: Próbálj ki egy mobil appot + hosted_on: "%{domain} Mastodon szerver" learn_more: Tudj meg többet + privacy_policy: Adatvédelmi szabályzat + see_whats_happening: Nézd, mi történik + server_stats: 'Szerver statisztika:' source_code: Forráskód + status_count_after: + one: tülköt küldött + other: tülköt küldött status_count_before: eddig + tagline: Kövess barátokat és találj újakat + terms: Felhasználási feltételek + user_count_after: + one: felhasználónk + other: felhasználónk user_count_before: Összesen what_is_mastodon: Mi a Mastodon? accounts: + choices_html: "%{name} választásai:" follow: Követés - following: Követed őket + followers: + one: Követő + other: Követő + following: Követett + joined: Csatlakozott %{date} + last_active: utoljára aktív + link_verified_on: A link tulajdonosát %{date} -n ellenőriztük media: Média moved_html: "%{name} ide költözött: %{new_profile_link}" + network_hidden: Ez az információ nem elérhető nothing_here: Nincs itt semmi! - people_followed_by: "%{name} követett személyei" + people_followed_by: "%{name} követettjei" people_who_follow: "%{name} követői" - posts_with_replies: Tülkök és válaszok - reserved_username: Ez egy már lefoglalt felhasználónév + pin_errors: + following: Ehhez szükséges, hogy kövesd már a felhasználót + posts: + one: Tülk + other: Tülk + posts_tab_heading: Tülkölés + posts_with_replies: Tülkölés válaszokkal + reserved_username: Ez már foglalt felhasználónév roles: admin: Adminisztrátor + bot: Bot moderator: Moderátor - unfollow: Követés abbahagyása + unavailable: Nincs ilyen profil + unfollow: Követés vége admin: + account_actions: + action: Művelet végrehajtása + title: "%{acct} moderálása" account_moderation_notes: - create: Új bejegyzés + create: Új moderációs bejegyzés created_msg: Moderációs bejegyzés létrehozva! delete: Törlés destroyed_msg: Moderációs bejegyzés törölve! accounts: + approve: Jóváhagyás + approve_all: Mindet jóváhagy are_you_sure: Biztos vagy benne? + avatar: Profilkép + by_domain: Domain + change_email: + changed_msg: A fiókhoz tartozó e-mailt megváltoztattuk! + current_email: Jelenlegi e-mail + label: E-mail megváltoztatása + new_email: Új e-mail + submit: E-mail megváltoztatása + title: "%{username} felhasználó e-mail változás" confirm: Megerősítés confirmed: Megerősítve - confirming: Megerősítve + confirming: Megerősítés alatt + deleted: Törölve demote: Lefokozás disable: Kikapcsolás disable_two_factor_authentication: Kétlépcsős azonosítás kikapcsolása disabled: Kikapcsolva display_name: Megjelenített név + domain: Domain edit: Szerkesztés email: E-mail email_status: E-mail állapot - enable: Engedélyezés - enabled: Engedélyezve + enable: Bekapcsolás + enabled: Bekapcsolva feed_url: Hírcsatorna URL - followers: Követők - followers_url: Követők URL - follows: Követettek + followers: Követő + followers_url: Követő URL + follows: Követett + header: Fejléc inbox_url: Beérkezett üzenetek URL + invited_by: Meghívta + ip: IP + joined: Csatlakozott location: all: Összes local: Helyi remote: Távoli title: Hely - login_status: Bejelentkezve + login_status: Bejelentkezési állapot media_attachments: Média-csatolmányok memorialize: Emlékállítás moderation: + active: Aktív all: Összes + pending: Függőben silenced: Némítva suspended: Felfüggesztve title: Moderáció moderation_notes: Moderációs bejegyzés most_recent_activity: Legutóbbi tevékenységek most_recent_ip: Legutóbbi IP-cím + no_account_selected: Nem változott meg egy fiók sem, mert semmi sem volt kiválasztva + no_limits_imposed: Nem állítottunk be határértéket not_subscribed: Nincs feliratkozás outbox_url: Kimenő üzenetek URL - perform_full_suspension: Teljes felfüggesztés + pending: Engedélyezés alatt + perform_full_suspension: Felfüggesztés profile_url: Profil URL promote: Előléptetés protocol: Protokoll public: Nyilvános push_subscription_expires: A PuSH feliratkozás elévül redownload: Profilkép frissítése + reject: Elutasítás + reject_all: Összes elutasítása + remove_avatar: Profilkép eltávolítása + remove_header: Fejléc törlése resend_confirmation: already_confirmed: Ezt a felhasználót már megerősítették - send: Küldd újra a megerősítő email-t + send: Küldd újra a megerősítő e-mailt success: A megerősítő e-mail sikeresen elküldve! reset: Visszaállítás reset_password: Jelszó visszaállítása @@ -94,84 +163,132 @@ hu: moderator: Moderátor staff: Stáb user: Felhasználó + salmon_url: Salmon URL search: Keresés - shared_inbox_url: Bejövő üzenetek URL keresése + shared_inbox_url: Megosztott bejövő üzenetek URL show: - created_reports: Ezen fiók által létrehozott jelentések - targeted_reports: Jelentések ezzel a fiókkal kapcsolatban + created_reports: Létrehozott jelentések + targeted_reports: Jelentések ezzel kapcsolatban silence: Némítás + silenced: Némított statuses: Tülkök subscribe: Feliratkozás + suspended: Felfüggesztett + time_in_queue: Várakozás a sorban %{time} title: Fiókok + unconfirmed_email: Nem megerősített e-mail undo_silenced: Némítás visszavonása undo_suspension: Felfüggesztés visszavonása unsubscribe: Leiratkozás username: Felhasználónév - web: Weboldal + warn: Figyelmeztetés + web: Web action_logs: actions: + assigned_to_self_report: "%{name} a %{target} bejelentést magához rendelte" + change_email_user: "%{name} megváltoztatta %{target} felhasználó e-mail címét" confirm_user: "%{name} megerősítette e-mail címét: %{target}" - create_custom_emoji: "%{name} új hangulatjelet töltött fel: %{target}" + create_account_warning: "%{name} figyelmeztetést küldött %{target} felhasználónak" + create_custom_emoji: "%{name} új emojit töltött fel: %{target}" create_domain_block: "%{name} letiltotta az alábbi domaint: %{target}" create_email_domain_block: "%{name} feketelistára tette az alábbi e-mail domaint: %{target}" demote_user: "%{name} lefokozta az alábbi felhasználót: %{target}" + destroy_custom_emoji: "%{name} törölte az emojit: %{target}" destroy_domain_block: "%{name} engedélyezte az alábbi domaint: %{target}" destroy_email_domain_block: "%{name} fehérlistára tette az alábbi e-mail domaint: %{target}" destroy_status: "%{name} eltávolította az alábbi felhasználó tülkjét: %{target}" disable_2fa_user: "%{name} kikapcsolta a kétlépcsős azonosítást %{target} felhasználó fiókján" - disable_custom_emoji: "%{name} letiltotta az alábbi hangulatjelet: %{target}" + disable_custom_emoji: "%{name} letiltotta az alábbi emojit: %{target}" disable_user: "%{name} letiltotta az alábbi felhasználó bejelentkezését: %{target}" - enable_custom_emoji: "%{name} engedélyezte az alábbi hangulatjelet: %{target}" + enable_custom_emoji: "%{name} engedélyezte az alábbi emojit: %{target}" enable_user: "%{name} engedélyezte az alábbi felhasználó bejelentkezését: %{target}" memorialize_account: "%{name} emléket állított az alábbi felhasználónak: %{target}" promote_user: "%{name} előléptette az alábbi felhasználót: %{target}" + remove_avatar_user: "%{name} törölte %{target} profilképét" + reopen_report: "%{name} újranyitotta a bejelentést: %{target}" reset_password_user: "%{name} visszaállította az alábbi felhasználó jelszavát: %{target}" - resolve_report: "%{name} mellőzte az alábbi jelentést: %{target}" + resolve_report: "%{name} megoldotta alábbi bejelentést: %{target}" silence_account: "%{name} lenémította %{target} felhasználói fiókját" suspend_account: "%{name} felfüggesztette %{target} felhasználói fiókját" + unassigned_report: "%{name} törölte a %{target} bejelentés hozzárendelését" unsilence_account: "%{name} feloldotta a némítást %{target} felhasználói fiókján" unsuspend_account: "%{name} feloldotta %{target} felhasználói fiókjának felfüggesztését" - update_custom_emoji: "%{name} frissítette az alábbi hangulatjelet: %{target}" + update_custom_emoji: "%{name} frissítette az alábbi emojit: %{target}" update_status: "%{name} frissítette %{target} felhasználó tülkjét" + deleted_status: "(törölt tülk)" title: Audit napló custom_emojis: - copied_msg: Sikeresen létrehoztuk a hangulatjel helyi másolatát + by_domain: Domain + copied_msg: Sikeresen létrehoztuk az emoji helyi másolatát copy: Másolás - copy_failed_msg: Hangulatjel helyi másolatának létrehozása sikertelen - created_msg: Hangulatjel létrehozva! + copy_failed_msg: Emoji helyi másolatának létrehozása sikertelen + created_msg: Emoji létrehozva! delete: Törlés - destroyed_msg: A hangulatjel törlése sikeres! + destroyed_msg: Emoji törlése sikeres! disable: Letiltás - disabled_msg: Hangulatjel letiltva - emoji: Hangulatjel + disabled_msg: Emoji letiltva + emoji: Emoji enable: Engedélyezés - enabled_msg: Hangulatjel engedélyezve + enabled_msg: Emoji engedélyezve image_hint: PNG (maximális méret 50KB) listed: Listázva new: - title: Új egyedi hangulatjel hozzáadása + title: Új egyedi emoji hozzáadása overwrite: Felülírás + shortcode: Rövidítés shortcode_hint: Legalább két karakter, csak betűk, számok és alsóvonás - title: Egyedi hangulatjelek + title: Egyedi emojik unlisted: Nincs listázva - update_failed_msg: Nem sikerült frissíteni a hangulatjelet - updated_msg: Hangulatjel sikeresen frissítve! + update_failed_msg: Nem sikerült frissíteni az emojit + updated_msg: Emoji sikeresen frissítve! upload: Feltöltés + dashboard: + backlog: hátralévő feladatok + config: Beállítások + feature_deletions: Fióktörlések + feature_invites: Meghívó linkek + feature_profile_directory: Profil adatbázis + feature_registrations: Regisztráció + feature_relay: Föderációs relé + feature_timeline_preview: Idővonal betekintő + features: Funkciók + hidden_service: Föderáció rejtett szolgáltatásokkal + open_reports: nyitott bejelentések + recent_users: Legutóbbi felhasználók + search: Keresés teljes szövegben + single_user_mode: Egyfelhasználós mód + software: Szoftver + space: Tárhely használat + title: Műszerfal + total_users: felhasználó összesen + trends: Trendek + week_interactions: interakció ezen a héten + week_users_active: aktív ezen a héten + week_users_new: felhasználó ezen a héten domain_blocks: - add_new: Új hozzáadása + add_new: Új tiltott domain hozzáadása created_msg: A domain-tiltás feldolgozása folyamatban destroyed_msg: A domain tiltása feloldva + domain: Domain + existing_domain_block_html: A %{name} domainen már szorosabb korlátokat állítottál be, először oldd fel a tiltást. new: create: Tiltás létrehozása - hint: A domain-tiltás nem gátolja meg az új fiókok hozzáadását az abatbázishoz, de visszamenőlegesen és automatikusan aktivál bizonyos moderációs szabályokat ezen fiókok esetében. + hint: A domain tiltása nem gátolja meg az új fiókok hozzáadását az abatbázishoz, de visszamenőlegesen és automatikusan aktivál bizonyos moderációs szabályokat ezen fiókok esetében. severity: desc_html: A Némítás elrejti az adott felhasználó tülkjeit mindenki elől, aki nem követi az adott felhasználót. A Felfüggesztés eltávolítja az adott felhasználó által létrehozott minden tartalmat, ide értve a médiafájlokat és a fiókadatokat is. Válaszd az Egyik sem opciót, ha csupán a médiafájlokat szeretnéd elutasítani. noop: Egyik sem silence: Némítás suspend: Felfüggesztés - title: Új domain-tiltás + title: Új domain tiltása reject_media: Médiafájlok elutasítása reject_media_hint: Eltávolítja a helyben tárolt médiafájlokat és a továbbiakban letiltja az új médiafájlok letöltését. Felfüggesztett fiókok esetében irreleváns opció + reject_reports: Bejelentések elutasítása + reject_reports_hint: Erről a domainről származó minden bejelentés elutasítása. Felfüggesztett fiókok esetén irreleváns opció + rejecting_media: médiafájlok elutasítása + rejecting_reports: bejelentések elutasítása + severity: + silence: némítva + suspend: felfüggesztve show: affected_accounts: one: Összesen egy fiók érintett az adatbázisban @@ -181,51 +298,122 @@ hu: suspend: Minden felhasználó felfüggesztésének feloldása ezen a domainen title: "%{domain} domain tiltásának feloldása" undo: Visszavonás - undo: Visszavonás + undo: Domain tiltásának visszavonása email_domain_blocks: add_new: Új hozzáadása created_msg: E-mail domain sikeresen hozzáadva a feketelistához delete: Törlés destroyed_msg: E-mail domain sikeresen eltávolítva a feketelistáról + domain: Domain new: create: Domain hozzáadása title: Új e-mail feketelista bejegyzés title: E-mail feketelista + followers: + back_to_account: Vissza a fiókhoz + title: "%{acct} követői" instances: - title: Nyilvántartott instanciák + by_domain: Domain + delivery_available: Kézbesítés elérhető + known_accounts: + one: "%{count} ismert fiók" + other: "%{count} ismert fiók" + moderation: + all: Mind + limited: Korlátozott + title: Moderáció + title: Föderáció + total_blocked_by_us: Általunk letiltott + total_followed_by_them: Általuk követett + total_followed_by_us: Általunk követett + total_reported: Bejelentés róluk + total_storage: Média csatolmány invites: + deactivate_all: Összes deaktiválása filter: all: Összes available: Elérhető expired: Elévült title: Szűrő title: Meghívások + pending_accounts: + title: Függőben lévő fiókok (%{count}) + relays: + add_new: Új relé hozzáadása + delete: Törlés + description_html: A föderációs relé egy olyan köztes szerver, mely nagy mennyiségű publikus tülköt cserél az erre feliratkozó vagy publikáló szerverek között. Ezzel segíthet kis és közepes szervereknek tartalmat megtalálni a föderációban, mely egyébként csak akkor válna lehetővé, ha a saját felhasználóink más szervereken lévő fiókokat követnének. + disable: Kikapcsolás + disabled: Kikapcsolva + enable: Bekapcsolás + enable_hint: Ha bekapcsolod, a szerver minden nyilvános tülkre feliratkozik ezen a relén, valamint az összes nyilvános tülköt elküldi ennek. + enabled: Bekapcsolva + inbox_url: Relé URL + pending: Várakozás a relé jóváhagyására + save_and_enable: Mentés és engedélyezés + setup: Relé kapcsolat felállítása + status: Állapot + title: Relék + report_notes: + created_msg: Bejelentési feljegyzés létrehozva! + destroyed_msg: Bejelentési feljegyzés törölve! reports: + account: + note: feljegyzés + report: bejelentés action_taken_by: 'Kezelte:' are_you_sure: Biztos vagy benne? + assign_to_self: Magamhoz rendelés + assigned: Hozzárendelt moderátor comment: none: Egyik sem + created_at: Jelentve mark_as_resolved: Megjelölés megoldottként + mark_as_unresolved: Megjelölés megoldatlanként + notes: + create: Feljegyzés hozzáadása + create_and_resolve: Megoldás feljegyzéssel + create_and_unresolve: Újranyitás feljegyzéssel + delete: Törlés + placeholder: Jegyezd le, mi tettünk az ügy érdekében, vagy bármilyen változást... + reopen: Bejelentés újranyitása report: "#%{id} számú jelentés" reported_account: Bejelentett fiók reported_by: 'Jelentette:' resolved: Megoldott + resolved_msg: A bejelentést sikeresen megoldottuk! status: Állapot - title: Jelentések + title: Bejelentések + unassign: Hozzárendelés törlése unresolved: Megoldatlan + updated_at: Frissítve settings: activity_api_enabled: desc_html: Helyi tülkök, aktív felhasználók és új regisztrációk száma heti bontásban title: Felhasználói aktivitás összesített statisztikájának publikussá tétele bootstrap_timeline_accounts: - desc_html: Az egyes felhasználónevek vesszővel elválasztva. Csak helyi és aktivált fiókok esetében működik. Üresen (alapértelmezettként) minden helyi adminisztrátorra érvényes. + desc_html: Az egyes felhasználóneveket vesszővel válaszd el! Csak helyi és aktivált fiókok esetében működik. Üresen (alapértelmezettként) minden helyi adminisztrátorra érvényes. title: Alapértelmezett követések új felhasználók esetében contact_information: email: Kapcsolattartói e-mail cím username: Kapcsolattartó felhasználóneve + custom_css: + desc_html: Változtasd meg a kinézetet ebben a CSS-ben, mely minden oldalon be fog töltődni + title: Egyedi CSS + hero: + desc_html: A kezdőoldalon látszik. Legalább 600x100px méret javasolt. Ha nincs beállítva, a szerver bélyegképet használjuk + title: Hősi kép + mascot: + desc_html: Több oldalon is látszik. Legalább 293×205px méret javasolt. Ha nincs beállítva, az alapértelmezett kabalát használjuk + title: Kabala kép peers_api_enabled: - desc_html: Domainek, amelyekkel ez az instancia kapcsolatban áll - title: Instanciák listájának közzététele, melyekkel ez a szerver kapcsolatban áll + desc_html: Domainek, amelyekkel ez a szerver kapcsolatban áll + title: Szerverek listájának közzététele, melyekkel ez a szerver kapcsolatban áll + preview_sensitive_media: + desc_html: Más weboldalakon linkelt tartalmaink előnézetében mindenképp benne lesz egy bélyegkép még akkor is, ha a médiát szenzitívnek jelölték meg + title: Szenzitív média mutatása OpenGraph előnézetben + profile_directory: + desc_html: Lehetővé teszi, hogy a felhasználóinkat megtalálják + title: Profil adatbázis engedélyezése registrations: closed_message: desc_html: Ez az üzenet jelenik meg a főoldalon, ha a regisztráció nem engedélyezett. HTML-tageket is használhatsz @@ -234,52 +422,92 @@ hu: desc_html: Engedélyezed a felhasználóknak, hogy töröljék fiókjukat title: Fiók törlésének engedélyezése min_invite_role: - disabled: Senkinek + disabled: Senki title: Meghívások engedélyezése + registrations_mode: + modes: + approved: A regisztráció engedélyhez kötött + none: Senki sem regisztrálhat + open: Bárki regisztrálhat + title: Regisztrációs mód + show_known_fediverse_at_about_page: + desc_html: Ha aktív, az előnézetben minden tülk megjelenik a velünk kapcsolatban álló szerverekről, egyébként csak helyi tülköket mutatunk. + title: Mutassuk az általunk ismert föderációt az idővonal előnézetben show_staff_badge: desc_html: Stáb-jelvény megjelenítése a felhasználó oldalán title: Stáb-jelvény megjelenítése site_description: - desc_html: 'Rövid bemutatkozás a főoldalon és a meta fejlécekben. Az alábbi HTML-tageket használhatod: <a> és <em>.' - title: Az instancia bemutatása + desc_html: Rövid bemutatkozás a főoldalon és a meta fejlécekben. Írd le, mi teszi ezt a szervert különlegessé! Használhatod a <a> és <em> HTML tageket. + title: A szerver bemutatása site_description_extended: - desc_html: Ide teheted például a közösségi és egyéb szabályzatot, útmutatókat és mindent, ami egyedivé teszi instanciádat. HTML-tageket is használhatsz + desc_html: Ide teheted például a közösségi és egyéb szabályzatot, útmutatókat és mindent, ami egyedivé teszi szerveredet. HTML-tageket is használhatsz title: További egyedi információ + site_short_description: + desc_html: Oldalsávban és meta tag-ekben jelenik meg. Írd le, mi teszi ezt a szervert különlegessé egyetlen bekezdésben. + title: Rövid leírás site_terms: desc_html: Megírhatod saját adatkezelési szabályzatodat, felhasználási feltételeidet vagy más hasonló jellegű dokumentumodat. HTML-tageket is használhatsz title: Egyedi felhasználási feltételek - site_title: Az instancia neve + site_title: A szerver neve thumbnail: - desc_html: Az OpenGraph és API előnézetekhez használjuk. Ajánlott mérete 1200x560 pixel - title: Az instancia bélyegképe + desc_html: OpenGraph-os és API-s előnézetekben használjuk. Ajánlott mérete 1200x630 pixel + title: A szerver bélyegképe timeline_preview: - desc_html: Publikus időfolyam megjelenítése a főoldalon - title: Időfolyam előnézete - title: Oldal beállításai + desc_html: Nyilvános idővonal megjelenítése a főoldalon + title: Idővonal előnézete + title: Webhely beállításai statuses: back_to_account: Vissza a fiók oldalára batch: delete: Törlés - nsfw_off: Szenzitív tartalom kikapcsolva - nsfw_on: Szenzitív tartalom bekapcsolva + nsfw_off: Szenzitív megjelölés törlése + nsfw_on: Megjelölés szenzitív tartalomként failed_to_execute: Végrehajtás sikertelen media: title: Média no_media: Nem található médiafájl + no_status_selected: Nem változtattunk meg semmit, mert semmi sem volt kiválasztva title: Felhasználó tülkjei with_media: Médiafájlokkal subscriptions: + callback_url: Callback URL confirmed: Megerősítve expires_in: Elévül last_delivery: Utolsó kézbesítés + title: WebSub topic: Téma + tags: + accounts: Fiókok + hidden: Rejtett + hide: Ne jelenjen meg a profilok adatbázisában + name: Hashtag + title: Hashtagek + unhide: Jelenjen meg a profilok adatbázisában + visible: Látható title: Karbantartás + warning_presets: + add_new: Új hozzáadása + delete: Törlés + edit: Szerkesztés + edit_preset: Figyelmeztetés szerkesztése + title: Figyelmeztetések admin_mailer: + new_pending_account: + body: Az új fiók részletesen alább látható. Ezt a jelentkezést engedélyezheted vagy elutasíthatod. + subject: Új fiók (%{username}) engedélyezésre vár a %{instance} szerveren new_report: body: "%{reporter} jelentette: %{target}" - subject: 'Új jelentés az alábbi instancián: %{instance} (#%{id})' + body_remote: Valaki a %{domain} domainről jelentette %{target} + subject: 'Új jelentés az alábbi szerveren: %{instance} (#%{id})' + appearance: + advanced_web_interface: Haladó webes felület + advanced_web_interface_hint: 'Ha szeretnéd, a teljes képernyőszélességet felhasználhatod. A haladó webes felülettel különböző oszlopokat állíthatsz be, hogy egyszerre annyi infót láthass, amennyit csak akarsz: Saját idővonal, értesítések, föderációs idővonal, bármennyi lista vagy hashtag.' + animations_and_accessibility: Animáció és akadálymentesítés + confirmation_dialogs: Megerősítő párbeszédablakok + sensitive_content: Szenzitív tartalom application_mailer: notification_preferences: E-mail beállítások módosítása + salutation: "%{name}!" settings: 'E-mail beállítások módosítása: %{link}' view: 'Megtekintés:' view_profile: Profil megtekintése @@ -293,6 +521,10 @@ hu: warning: Ez érzékeny adat. Soha ne oszd meg másokkal! your_token: Hozzáférési kulcsod auth: + apply_for_account: Meghívó kérése + change_password: Jelszó + checkbox_agreement_html: Egyetértek a szerver szabályaival és a felhasználási feltételekkel + confirm_email: E-mail megerősítése delete_account: Felhasználói fiók törlése delete_account_html: Felhasználói fiókod törléséhez kattints ide. A rendszer újbóli megerősítést fog kérni. didnt_get_confirmation: Nem kaptad meg a megerősítési lépéseket? @@ -302,12 +534,19 @@ hu: logout: Kijelentkezés migrate_account: Felhasználói fiók költöztetése migrate_account_html: Ha szeretnéd átirányítani ezt a fiókodat egy másikra, a beállításokat itt találod meg. + or_log_in_with: Vagy jelentkezz be ezzel + providers: + cas: CAS + saml: SAML register: Regisztráció + registration_closed: "%{instance} nem fogad új tagokat" resend_confirmation: Megerősítési lépések újraküldése reset_password: Jelszó visszaállítása security: Biztonság set_new_password: Új jelszó beállítása + trouble_logging_in: Problémád van a bejelentkezéssel? authorize_follow: + already_following: Már követed ezt a felhasználót error: Hiba történt a távoli felhasználó keresésekor follow: Követés follow_request: 'Engedélyt kértél az alábbi felhasználó követésére:' @@ -336,9 +575,19 @@ hu: confirm_password: Személyazonosságod megerősítéséhez írd be a jelenlegi jelszavad description_html: Ezzel véglegesen és visszafordíthatatlanul törlöd minden tartalmadat és deaktiválod a fiókodat. A felhasználónevedet megtartjuk, hogy megakadályozzuk a neveddel történő jövőbeni visszaélések lehetőségét. proceed: Felhasználói fiók törlése - success_msg: Felhasználói fiókod sikeresen törölve lett - warning_html: Csak azt tudjuk garantálni, hogy az általad létrehozott tartalmat erről az instanciáról töröljük. Ha egyes tartalmaidat sokan megosztották, valószínűleg marad nyomuk a megosztások miatt. Nam fogjuk tudni frissíteni azon instanciák adatbázisát, amelyek nem kapcsolódnak a föderációhoz vagy amelyek leiratkoztak a tülkjeidről. + success_msg: Felhasználói fiókod sikeresen töröltük + warning_html: Csak azt tudjuk garantálni, hogy az általad létrehozott tartalmat erről a szerverről töröljük. Ha egyes tartalmaidat sokan megosztották, valószínűleg marad nyomuk a megosztások miatt. Nem fogjuk tudni frissíteni azon szerverek adatbázisát, amelyek nem kapcsolódnak a föderációhoz vagy amelyek leiratkoztak a tülkjeidről. warning_title: Szórt tartalmak elérése + directories: + directory: Profilok + enabled: Szerepelsz a profil adatbázisban. + enabled_but_waiting: Engedélyezted, hogy szerepelj a profil adatbázisban, de még nincs elegendő követőd (%{min_followers}) ehhez. + explanation: Találj másokra érdeklődésük alapján + explore_mastodon: "%{title} felfedezése" + how_to_enable: Nem engedélyezted a profil adatbázisban való megjelenésed. Engedélyezheted alább. Használj hashtageket az életrajzodban, hogy az ezekhez tartozó listákba bekerülj! + people: + one: "%{count} ember" + other: "%{count} ember" errors: '403': Nincs jogosultságod az oldal megtekintéséhez. '404': Az általad keresett oldal nem található. @@ -346,30 +595,97 @@ hu: '422': content: Megerősítés sikertelen. Nem tiltottad le esetleg a sütiket? title: Megerősítés sikertelen - '429': Kampec + '429': Korlátozva '500': content: Sajnáljuk, valami hiba történt a mi oldalunkon. title: Az oldal nem megfelelő - noscript_html: A Mastodon webalkalmazás használatához engedélyezned kell a JavaScriptet. A másik megoldás, hogy kipróbálod az egyik, a platformodnak megfelelő alkalmazást. + noscript_html: A Mastodon webalkalmazás használatához engedélyezned kell a JavaScriptet. A másik megoldás, hogy kipróbálsz egy platformodnak megfelelő alkalmazást. + existing_username_validator: + not_found: ezzel a névvel nem találtunk helyi felhasználót + not_found_multiple: nem találtuk %{usernames} felhasználó(ka)t exports: + archive_takeout: + date: Dátum + download: Archív letöltése + hint_html: Itt kérhető egy archív az összes feltöltött tülködről és médiádról. Az exportált adatok ActivityPub formátumban lesznek, melyet bármilyen szabványos program tud olvasni. 7 naponként kérhetsz ilyen archívot. + in_progress: Archív összeállítása... + request: Archív kérése + size: Méret blocks: Tiltólistádon + csv: CSV + domain_blocks: Tiltott domainjeid follows: Követettjeid + lists: Listáid mutes: Némításaid storage: Médiatároló + featured_tags: + add_new: Új hozzáadása + errors: + limit: Már kiemelted a maximálisan engedélyezett számú hashtaget + filters: + contexts: + home: Saját idővonal + notifications: Értesítések + public: Nyilvános idővonalak + thread: Beszélgetések + edit: + title: Szűrő szerkesztése + errors: + invalid_context: A megadott kontextus hamis vagy hiányzik + invalid_irreversible: Visszafordíthatatlan szűrést csak saját idővonalon vagy értesítéseken lehet végezni + index: + delete: Törlés + title: Szűrők + new: + title: Új szűrő hozzáadása + footer: + developers: Fejlesztőknek + more: Többet… + resources: Segédanyagok generic: - changes_saved_msg: Változások sikeresen elmentve! + all: Mind + changes_saved_msg: A változásokat elmentettük! + copy: Másolás + order_by: Rendezés save_changes: Változások mentése validation_errors: one: Valami nincs rendjén! Kérlek tekintsd meg a hibát alant other: Valami nincs rendjén! Kérlek tekintsd meg a %{count} darab hibát alant + html_validator: + invalid_markup: 'hibás HTML leíró: %{error}' + identity_proofs: + active: Aktív + authorize: Igen, engedélyezés + authorize_connection_prompt: Engedélyezed ezt a kriptografikus kapcsolatot? + errors: + failed: A kriptografikus kapcsolat sikertelen. Próbáld inkább innen %{provider}. + keybase: + invalid_token: A Keybase tokenek hashelt aláírások és pont 66 hexa karakterből állnak + verification_failed: A Keybase ezt a tokent nem ismerte fel a %{kb_username} Keybase felhasználó aláírásaként. Próbáld újra Keybase-ből. + wrong_user: Nem tudjuk tanúsítani %{proving} felhasználót amíg %{current} felhasználóként vagy bejelentkezve. Jelentkezz be %{proving} felhasználóként és próbáld újra. + explanation_html: Itt más személyazonosságaiddal tudsz kriptografikus kapcsolatot létesíteni, pl. Keybase profillal. Így mások titkosított üzenetet küldhetnek neked, valamint megbízhatnak az általad küldött tartalomban is. + i_am_html: "%{username} vagyok %{service} szerveren." + identity: Személyazonosság + inactive: Inaktív + publicize_checkbox: 'És ezt tülköld ki:' + publicize_toot: 'Tanúsítva! %{username} vagyok %{service}: %{url} szerveren' + status: Ellenőrzés állapota + view_proof: Tanúsítás megtekintése imports: - preface: Itt importálhatod egy másik instanciáról lementett adataidat, például követettjeid és letiltott felhasználóid listáját. + modes: + merge: Összefésülés + merge_long: Megtartjuk a meglévő bejegyzéseket és hozzávesszük az újakat + overwrite: Felülírás + overwrite_long: Lecseréljük újakkal a jelenlegi bejegyzéseket + preface: Itt importálhatod egy másik szerverről lementett adataidat, például követettjeid és letiltott felhasználóid listáját. success: Adataidat sikeresen feltöltöttük és feldolgozásukat megkezdtük types: blocking: Letiltottak listája + domain_blocking: Letiltott domainek listája following: Követettjeid listája muting: Némított felhasználók listája upload: Feltöltés + in_memoriam_html: In Memoriam. invites: delete: Visszavonás expired: Lejárt @@ -378,15 +694,16 @@ hu: '21600': 6 óra '3600': 1 óra '43200': 12 óra - '604800': 1 week + '604800': 1 hét '86400': 1 nap expires_in_prompt: Soha generate: Generálás + invited_by: 'Téged meghívott:' max_uses: one: 1 felhasználás other: "%{count} felhasználás" max_uses_prompt: Nincs korlát - prompt: Az itt generált linkek megosztásával hívhatod meg ismerőseidet az instanciára + prompt: Az itt generált linkek megosztásával hívhatod meg ismerőseidet erre a szerverre table: expires_at: Lejárat uses: Használat @@ -402,7 +719,7 @@ hu: acct: Az új fiók felhasznalonev@domain formátumban currently_redirecting: 'A profilod az alábbi fiókra van átirányítva:' proceed: Mentés - updated_msg: Fiókod átirányítási beállítasait sikeresen mentettük! + updated_msg: Fiókod átirányítási beállításait sikeresen mentettük! moderation: title: Moderáció notification_mailer: @@ -418,125 +735,349 @@ hu: other: "%{count} új értesítésed érkezett legutóbbi látogatásod óta \U0001F418" title: Amíg távol voltál… favourite: - body: 'Az állapotodat kedvencnek jelölte %{name}:' - subject: "%{name} kedvencnek jelölte az állapotod" + body: 'A tülködet kedvencnek jelölte %{name}:' + subject: "%{name} kedvencnek jelölte a tülködet" title: Új kedvencnek jelölés follow: body: "%{name} mostantól követ téged!" subject: "%{name} mostantól követ téged" title: Új követő follow_request: - action: Követési kérések kezelése + action: Követési kérelmek kezelése body: "%{name} követni szeretne téged" subject: 'Jóváhagyásra vár: %{name}' - title: Új követési kérés + title: Új követési kérelem mention: action: Válasz body: "%{name} megemlített téged:" subject: "%{name} megemlített téged" title: Új említés reblog: - body: 'Az állapotod reblogolta %{name}:' - subject: "%{name} reblogolta az állapotod" - title: Új reblog + body: 'A tülködet %{name} megtolta:' + subject: "%{name} megtolta a tülködet" + title: Új megtolás + number: + human: + decimal_units: + format: "%n%u" + units: + billion: Mrd + million: M + quadrillion: Q + thousand: K + trillion: T pagination: + newer: Újabb next: Következő + older: Régebbi prev: Előző + truncate: "…" + polls: + errors: + already_voted: Ezen a szavazáson már voksoltál + duplicate_options: duplázott elemeket tartalmaz + duration_too_long: túl távoli időpont + duration_too_short: túl közeli időpont + expired: A szavazásnak már vége + over_character_limit: egyik sem lehet %{max} karakternél hosszabb + too_few_options: több, mint egy opciónak kell lennie + too_many_options: nem lehet több, mint %{max} opció preferences: other: Egyéb + posting_defaults: Tülkölés alapértelmezései + public_timelines: Nyilvános idővonalak + relationships: + activity: Fiók aktivitás + dormant: Elhagyott + last_active: Utoljára aktív + most_recent: Legutóbbi + moved: Átköltöztetve + mutual: Kölcsönös + primary: Elsődleges + relationship: Kapcsolat + remove_selected_domains: A választott domainekről minden követő eltávolítása + remove_selected_followers: Kiválasztott követők eltávolítása + remove_selected_follows: Kiválasztottak követésének abbahagyása + status: Fiók állapota remote_follow: acct: Írd be a felhasználódat, amelyről követni szeretnéd felhasznalonev@domain formátumban missing_resource: A fiókodnál nem található a szükséges átirányítási URL + no_account_html: Nincs fiókod? Regisztrálj itt proceed: Tovább a követéshez prompt: 'Őt tervezed követni:' + reason_html: "Miért van erre szükség? %{instance} nem feltétlenül az a szerver, ahol regisztrálva vagy, ezért először a saját szerveredre irányítunk." + remote_interaction: + favourite: + proceed: Jelöljük kedvencnek + prompt: 'Ezt a tülköt szeretnéd kedvencnek jelölni:' + reblog: + proceed: Megtolás + prompt: 'Ezt a tülköt szeretnéd megtolni:' + reply: + proceed: Válaszadás + prompt: 'Erre a tülkre szeretnél válaszolni:' + remote_unfollow: + error: Hiba + title: Cím + unfollowed: Nem követett + scheduled_statuses: + over_daily_limit: Túllépted az időzített tülkökre vonatkozó napi limitet (%{limit}) + over_total_limit: Túllépted az időzített tülkökre vonatkozó limitet (%{limit}) + too_soon: Az időzítéshez jövőbeni időpont kell sessions: activity: Legutóbbi tevékenység browser: Böngésző browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Ismeretlen böngésző + ie: Internet Explorer + micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Böngésző + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Jelenlegi munkamenet description: "%{browser} az alábbi platformon: %{platform}" explanation: Jelenleg az alábbi böngészőkkel vagy bejelentkezve a fiókodba. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: ismeretlen platform + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: Visszavonás revoke_success: Munkamenet sikeresen visszavonva title: Munkamenetek settings: + account: Fiók + account_settings: Fiók beállítások + appearance: Megjelenés authorized_apps: Jóváhagyott alkalmazások back: Vissza a Mastodonhoz delete: Fiók törlése development: Fejlesztőknek edit_profile: Profil szerkesztése export: Adatok exportálása + featured_tags: Kiemelt hashtagek + identity_proofs: Személyazonosság tanúsítások import: Importálás + import_and_export: Import és export migrate: Fiók átirányítása notifications: Értesítések - preferences: Általános beállítások + preferences: Beállítások + profile: Profil + relationships: Követések és követők two_factor_authentication: Kétlépcsős azonosítás statuses: + attached: + description: 'Csatolva: %{attached}' + image: + one: "%{count} kép" + other: "%{count} kép" + video: + one: "%{count} videó" + other: "%{count} videó" + boosted_from_html: Megtolva innen %{acct_link} + content_warning: 'Tartalom figyelmeztetés: %{warning}' + disallowed_hashtags: + one: 'tiltott hashtaget tartalmaz: %{tags}' + other: 'tiltott hashtageket tartalmaz: %{tags}' + language_detection: Nyelv automatikus felismerése open_in_web: Megnyitás a weben over_character_limit: Túllépted a maximális %{max} karakteres keretet pin_errors: limit: Elérted a kitűzhető tülkök maximális számát ownership: Nem tűzheted ki valaki más tülkjét - private: Csak publikus tülköt tűzhetsz ki - reblog: Reblogolt tülköt nem tudsz kitűzni + private: Csak nyilvános tülköt tűzhetsz ki + reblog: Megtolt tülköt nem tudsz kitűzni + poll: + total_votes: + one: "%{count} szavazat" + other: "%{count} szavazat" + vote: Szavazás show_more: Mutass többet + sign_in_to_participate: Jelentkezz be, hogy részt vehess a beszélgetésben + title: '%{name}: "%{quote}"' visibilities: private: Csak követőknek private_long: A tülk csak követőidnek jelenik meg public: Nyilvános public_long: Bárki láthatja a tülköt unlisted: Listázatlan - unlisted_long: Mindenki látja, de a nyilvános időfolyamokban nem jelenik meg + unlisted_long: Mindenki látja, de a nyilvános idővonalakon nem jelenik meg stream_entries: pinned: Kitűzött tülk - reblogged: reblogolt + reblogged: megtolt sensitive_content: Szenzitív tartalom terms: + body_html: | +

Adatvédelmi nyilatkozat

+

Milyen adatokat gyűjtünk?

+ + + +
+ +

Mire használjuk az adataidat?

+ +

Bármely tőled begyűjtött adatot a következő célokra használhatjuk:

+ + + +
+ +

Hogyan védjük az adataidat?

+ +

Üzemben tartunk néhány biztonsági rendszert, hogy megvédjük a személyes adataidat, amikor eléred vagy karbantartod ezeket. Többek között a böngésződ munkamenete, a szerver oldal, valamint a böngésző közötti teljes kommunikáció SSL-lel van titkosítva, a jelszavadat pedig erős, egyirányú algoritmussal hash-eljük. Kétlépcsős azonosítást is bekapcsolhatsz, hogy még biztonságosabbá tedd a fiókodhoz való hozzáférést.

+ +
+ +

Mik az adatmegőrzési szabályaink?

+ +

Jóhiszeműen járunk el, hogy:

+ + + +

Kérhetsz archívot minden tárolt adatodról, tülkjeidről, média fájljaidról, profil- és háttér képedről.

+ +

Bármikor visszaállíthatatlanul le is törölheted a fiókodat.

+ +
+ +

Használunk sütiket?

+ +

Igen. A sütik pici állományok, melyeket az oldalunk a böngésződön keresztül a háttértáradra rak, ha engedélyezed ezt. Ezek a sütik teszik lehetővé, hogy az oldalunk felismerje a böngésződet, és ha regisztráltál, hozzá tudjon kötni a fiókodhoz.

+ +

Arra is használjuk a sütiket, hogy elmenthessük a beállításaidat egy következő látogatás céljából.

+ +
+ +

Átadunk bármilyen adatot harmadik személynek?

+ +

Az azonosításodra alkalmazható adatokat nem adjuk el, nem kereskedünk vele, nem adjuk át külső szereplőnek. Ez nem foglalja magába azon harmadik személyeket, aki az üzemeltetésben, felhasználók kiszolgálásban és a tevékenységünkben segítenek, de csak addig, amíg ők is elfogadják, hogy ezeket az adatokat bizalmasan kezelik. Akkor is átadhatjuk ezeket az adatokat, ha erre hitünk szerint törvény kötelez minket, ha betartatjuk az oldalunk szabályzatát vagy megvédjük a saját vagy mások személyiségi jogait, tulajdonát, biztonságát.

+ +

A nyilvános tartalmaidat más hálózatban lévő szerverek letölthetik. A nyilvános és csak követőknek szánt tülkjeid olyan szerverekre is elküldődnek, melyeken követőid vannak. A közvetlen üzenetek is átkerülnek a címzettek szervereire, ha ők más szerveren regisztráltak.

+ +

Ha felhatalmazol egy alkalmazást, hogy használja a fiókodat, a jóváhagyott hatásköröktől függően ez elérheti a nyilvános profiladataidat, a követettjeid listáját, a követőidet, listáidat, tülkjeidet és kedvenceidet is. Ezek az alkalmazások ugyanakkor sosem érhetik el a jelszavadat és e-mail címedet.

+ +
+ +

Az oldal gyerekek általi használata

+ +

Ha ez a szerver az EU-ban vagy EEA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 16 éven felülieket céloznak. Ha 16 évnél fiatalabb vagy, a GDPR (General Data Protection Regulation) értelmében kérlek ne használd ezt az oldalt!

+ +

Ha ez a szerver az USA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 13 éven felülieket céloznak. Ha 13 évnél fiatalabb vagy, a COPPA (Children's Online Privacy Protection Act) értelmében kérlek ne használd ezt az oldalt!

+ +

A jogi előírások különbözhetnek ettől a világ egyéb tájain.

+ +
+ +

Adatvédelmi nyilatkozat változásai

+ +

Ha úgy döntünk, hogy megváltoztatjuk az adatvédelmi nyilatkozatot, ezt ezen az oldalon közzé fogjuk tenni.

+ +

Ez a dokumentum CC-BY-SA. Utoljára 2018.03.07 frissült.

+ +

Eredetileg innen adaptálva Discourse privacy policy.

title: "%{instance} Felhasználási feltételek és Adatkezelési nyilatkozat" themes: - default: Mastodon + contrast: Mastodon (Nagy kontrasztú) + default: Mastodon (Sötét) + mastodon-light: Mastodon (Világos) time: formats: default: "%Y %b %d, %H:%M" + month: "%Y %b" two_factor_authentication: code_hint: Megerősítéshez írd be az alkalmazás által generált kódot - description_html: He engedélyezed a kétlépcsős azonosítást, a bejelentkezéshez szükséged lesz a teefonodre és egy alkalmazásra, amely hozzáférési kódot generál számodra. + description_html: He engedélyezed a kétlépcsős azonosítást, a bejelentkezéshez szükséged lesz a telefonodra és egy alkalmazásra, amely hozzáférési kódot generál számodra. disable: Kikapcsolás enable: Engedélyezés enabled: Kétlépcsős azonosítás engedélyezve enabled_success: A kétlépcsős azonosítást sikeresen engedélyezted generate_recovery_codes: Visszaállítási kódok generálása - instructions_html: "Olvasd be ez a QR-kódot a telefonodon futó Google Authenticator (vagy egyéb TOTP) alkalmazással. A jövőben ez az alkalmazás fog számodra hozzáférési kódot generálni a belépéshez." + instructions_html: "Olvasd be ezt a QR-kódot a telefonodon futó Google Authenticator vagy egyéb TOTP alkalmazással. A jövőben ez az alkalmazás fog számodra hozzáférési kódot generálni a belépéshez." lost_recovery_codes: A visszaállítási kódok segítségével tudsz belépni, ha elveszítenéd a telefonod. Ha a visszaállítási kódjaidat hagytad el, itt generálhatsz újakat. A régi kódokat ebben az esetben érvénytelenítjük. manual_instructions: 'Ha nem sikerült a QR-kód beolvasása, itt a szöveges kulcs, amelyet manuálisan kell begépelned:' recovery_codes: Visszaállítási kódok biztonsági mentése - recovery_codes_regenerated: Visszaállítási kódok sikeresen újragenerálva + recovery_codes_regenerated: A visszaállítási kódokat sikeresen újrageneráltuk recovery_instructions_html: A visszaállítási kódok egyikének segítségével tudsz majd belépni, ha elveszítenéd a telefonod. Tartsd biztos helyen a visszaállítási kódjaid! Például nyomtasd ki őket és tárold a többi fontos iratoddal együtt. setup: Beállítás wrong_code: A beírt kód nem érvényes! A szerver órája és az eszközöd órája szinkronban jár? user_mailer: + backup_ready: + explanation: A Mastodon fiókod teljes mentését kérted. A mentés kész ás letölthető! + subject: Az adataidról készült archív letöltésre kész + title: Archiválás + warning: + explanation: + disable: A fiókod befagyasztott állapotban megtartja minden adatát, de feloldásig nem csinálhatsz vele semmit. + silence: A fiókod korlátozott állapotában csak a követőid láthatják a tülkjeidet, valamint nem kerülsz rá nyilvános idővonalakra. Ugyanakkor mások manuálisan még követhetnek. + suspend: A fiókodat felfüggesztették, így minden tülköd és feltöltött fájlod menthetetlenül elveszett erről a szerverről és minden olyanról is, ahol voltak követőid. + review_server_policies: Szerver szabályzat átnézése + subject: + disable: A fiókodat %{acct} befagyasztották + none: Figyelmeztetés a %{acct} fióknak + silence: A fiókodat %{acct} korlátozták + suspend: A fiókodat %{acct} felfüggesztették + title: + disable: Befagyasztott fiók + none: Figyelem + silence: Lekorlátozott fiók + suspend: Felfüggesztett fiók welcome: edit_profile_action: Készítsd el profilod edit_profile_step: 'Itt tudod egyedivé tenni a profilod: feltölthetsz profil- és borítóképet, megváltoztathatod a megjelenített neved és így tovább. Ha jóvá szeretnéd hagyni követőidet, mielőtt láthatják a tülkjeid, itt tudod a fiókodat zárttá tenni.' explanation: Néhány tipp a kezdeti lépésekhez final_action: Kezdj tülkölni - final_step: 'Kezdj tülkölni! Publikus üzeneteid még követők híján is megjelennek másoknak, például a helyi időfolyamban és a címkéknél. Kezdd például azzal, hogy bemutatkozol: használd a #bemutatkozas és az #introductions címkét a tülködben.' + final_step: 'Kezdj tülkölni! Publikus üzeneteid még követők híján is megjelennek másoknak, például a helyi idővonalon és a hashtageknél. Kezdd például azzal, hogy bemutatkozol: használd a #bemutatkozas vagy az #introductions hashtaget a tülködben.' full_handle: Teljes felhasználóneved - full_handle_hint: Ez az, amit megadhatsz másoknak, hogy üzenhessenek neked vagy követhessenek téged más instanciákról. + full_handle_hint: Ez az, amit megadhatsz másoknak, hogy üzenhessenek neked vagy követhessenek téged más szerverekről. review_preferences_action: Beállítások módosítása - review_preferences_step: Tekintsd át beállításaidat, például hogy milyen értesítéseket kérsz emailben vagy hogy alapértelmezettként mi legyen a tülkjeid adatvédelmi beállítása. Ha nem vagy szédülős alkat, azt is engedélyezheted, hogy automatikusan lejátsszuk a GIF-eket. + review_preferences_step: Tekintsd át beállításaidat, például hogy milyen értesítéseket kérsz e-mailben vagy hogy alapértelmezettként mi legyen a tülkjeid láthatósága. Ha nem vagy szédülős alkat, azt is engedélyezheted, hogy automatikusan lejátsszuk a GIF-eket. subject: Üdvözöl a Mastodon - tip_federated_timeline: A nyilvános időfolyam a Mastodon ütőere, ahol minden tülk összefolyik. Nem teljes ugyan, mert csak azokat az emberek fogod látni, akiket instanciád többi felhasználója követ. - tip_following: Alapértelmezettként instanciád adminisztrátorait követed. Látogasd meg a helyi és a nyilvános időfolyamot, hogy más érdekes emberekre is rátalálj. - tip_local_timeline: A helyi időfolyam a saját instanciád (%{instance}) ütőere. Ezek a kedves emberek itt mind a szomszédaid! - tip_mobile_webapp: Ha a böngésződ lehetővé teszi, hogy kezdőképernyődhöz add a Mastodont, még értesítéseket is fogsz kapni – akárcsak egy igazi alkalmazás esetében! + tip_federated_timeline: A nyilvános idővonal a Mastodon ütőere, ahol minden tülkölés összefolyik. Nem teljes ugyan, mert csak azokat az embereket fogod látni, akiket a szervered többi felhasználója közül valaki követ. + tip_following: Alapértelmezettként szervered adminisztrátorait követed. Látogasd meg a helyi és a nyilvános idővonalat, hogy más érdekes emberekre is rátalálj. + tip_local_timeline: A helyi idővonal a saját szervered (%{instance}) ütőere. Ezek a kedves emberek itt mind a szomszédaid! + tip_mobile_webapp: Ha a böngésződ lehetővé teszi, hogy a kezdőképernyődhöz add a Mastodont, még értesítéseket is fogsz kapni, akárcsak egy igazi alkalmazás esetében! tips: Tippek title: Üdv a fedélzeten, %{name}! users: + follow_limit_reached: Nem követhetsz több, mint %{limit} embert invalid_email: A megadott e-mail cím helytelen invalid_otp_token: Érvénytelen ellenőrző kód + otp_lost_help_html: Ha mindkettőt elvesztetted, kérhetsz segítséget itt %{email} + seamless_external_login: Külső szolgáltatáson keresztül jelentkeztél be, így a jelszó és e-mail beállítások nem elérhetőek. signed_in_as: Bejelentkezve mint + verification: + explanation_html: 'A profilodon hitelesítheted magad, mint az itt található linkek tulajdonosa. Ehhez a linkelt weboldalnak tartalmaznia kell egy linket vissza a Mastodon profilodra. Ennek tartalmaznia kell a rel="me" attribútumot. A link szövege bármi lehet. Itt egy példa:' + verification: Hitelesítés diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 10876f5d3..ca640d07d 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -835,7 +835,7 @@ ja: uc_browser: UCBrowser weibo: Weibo current_session: 現在のセッション - description: "%{browser} on %{platform}" + description: "%{platform} 上の %{browser}" explanation: あなたのMastodonアカウントに現在ログインしているウェブブラウザの一覧です。 ip: IP platforms: diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index b3c879b2f..023506f24 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -2,31 +2,46 @@ cy: simple_form: hints: + account_warning_preset: + text: Gallwch defnyddio cystrawen tŵt, fel URLs, hashnodau a sôniadau + admin_account_action: + send_email_notification: Bydd y defnyddiwr yn derbyn esboniad o beth digwyddodd gyda'i cyfrif + text_html: Yn ddewisol. Gallwch defnyddio cystrawen tŵt. Gallwch ychwanegu rhagosodiadau rhybydd i arbed amser + type_html: Dewis beth i wneud gyda %{acct} + warning_preset_id: Yn ddewisol. Gallwch dal ychwanegu testun addasiol I ddiwedd y rhagosodiad defaults: autofollow: Bydd pobl sy'n cofrestru drwy'r gwahoddiad yn eich dilyn yn awtomatig avatar: PNG, GIF neu JPG. %{size} ar y mwyaf. Caiff ei israddio i %{dimensions}px bot: Mae'r cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro context: Un neu fwy cyd-destun lle dylai'r hidlydd weithio digest: Ond yn cael eu hanfon ar ôl cyfnod hir o anweithgarwch ac ond os ydych wedi derbyn unrhyw negeseuon personol yn eich absenoldeb + discoverable_html: Mae'r cyfeiriadur yn gadael i bobl ddarganfod cyfrifau wedi'i seilio ar ddiddordebau a gweithgareddau.. Mae hyn angen o leiaf %{min_followers} o ddilynwyr email: Byddwch yn derbyn e-bost i gadarnhau fields: Mae modd i chi arddangos hyd at 4 eitem fel tabl ar eich proffil header: PNG, GIF neu JPG. %{size} ar y mwyaf. Ceith ei israddio i %{dimensions}px inbox_url: Copïwch yr URL o dudalen flaen y relái yr ydych am ei ddefnyddio irreversible: Bydd tŵtiau wedi eu hidlo yn diflannu am byth, hyd yn oed os ceith yr hidlydd ei ddileu'n hwyrach - locale: Iaith y rhyngwyneb, e-byst a hysbysiadau push + locale: Iaith y rhyngwyneb, e-byst a hysbysiadau gwthiadwy locked: Ei wneud yn ofynnol i chi i ganiatau dilynwyr a llaw password: Defnyddiwch oleiaf 8 nodyn phrase: Caiff ei gyfateb heb ystyriaeth o briflythrennu mewn testun neu rhybudd ynghylch cynnwys tŵt scopes: Pa APIau y bydd gan y rhaglen ganiatad i gael mynediad iddynt. Os dewiswch maes lefel uchaf, yna nid oes angen dewis rhai unigol. + setting_aggregate_reblogs: Paid dangos bŵstiau newydd ar gyfer tŵtiau sydd wedi'i fŵstio yn ddiweddar (dim ond yn effeithio bŵstiau newydd ei dderbyn) + setting_default_sensitive: Mae cyfryngau sensitif yn cael ei gyddio'n rhagosodiedig, a gall cael eu dangos â chlic setting_display_media_default: Cuddio cyfryngau wedi eu marcio'n sensitif setting_display_media_hide_all: Cuddio cyfryngau bob tro setting_display_media_show_all: Dangos cyfryngau wedi eu marcio'n sensitif bob tro setting_hide_network: Ni fydd y rheini yr ydych yn eu dilyn a phwy sy'n eich dilyn chi yn cael ei ddangos ar eich proffil setting_noindex: Mae hyn yn effeithio ar eich proffil cyhoeddus a'ch tudalennau statws + setting_show_application: Bydd y offer frydych yn defnyddio i dŵtio yn cael ei arddangos yn golwg manwl eich tŵtiau username: Bydd eich enw defnyddiwr yn unigryw ar %{domain} whole_word: Os yw'r allweddair neu'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw'n cyfateb a'r gair cyfan + featured_tag: + name: 'Efallai hoffech defnyddio un o''r rhain:' imports: data: Allforiwyd dogfen CSV o achos Mastodon arall + invite_request: + text: Bydd hyn yn helpu ni adolygu eich cais sessions: otp: 'Mewnbynnwch y cod dau gam a gynhyrchwyd gan eich ap ffôn neu defnyddiwch un o''ch codau adfer:' user: @@ -34,6 +49,7 @@ cy: labels: account: fields: + name: Label value: Cynnwys account_warning_preset: text: Testun rhagosodedig @@ -56,6 +72,7 @@ cy: confirm_password: Cadarnhau cyfrinair context: Hidlo cyd-destunau current_password: Cyfrinair presennol + data: Data discoverable: Rhestrwch y cyfrif hwn ar y cyfeiriadur display_name: Enw arddangos email: Cyfeiriad e-bost @@ -72,6 +89,7 @@ cy: otp_attempt: Côd dau gam password: Cyfrinair phrase: Allweddair neu ymadrodd + setting_advanced_layout: Alluogi rhyngwyneb wê uwch setting_aggregate_reblogs: Grŵp hybiau mewn ffrydiau setting_auto_play_gif: Chwarae GIFs wedi'u hanimeiddio yn awtomatig setting_boost_modal: Dangos deialog cadarnhad cyn bŵstio @@ -87,6 +105,7 @@ cy: setting_hide_network: Cuddio eich rhwydwaith setting_noindex: Dewis peidio mynegeio peiriant chwilota setting_reduce_motion: Lleihau mudiant mewn animeiddiadau + setting_show_application: Datguddio'r offer defnyddwyd i anfon tŵtiau setting_system_font_ui: Defnyddio ffont rhagosodedig y system setting_theme: Thema'r wefan setting_unfollow_modal: Dangos deialog cadarnhau cyn dad-ddilyn rhywun @@ -95,19 +114,26 @@ cy: username: Enw defnyddiwr username_or_email: Enw defnyddiwr neu e-bost whole_word: Gair cyfan + featured_tag: + name: Hashnod interactions: must_be_follower: Blocio hysbysiadau o bobl nad ydynt yn eich dilyn must_be_following: Blocio hysbysiadau o bobl nad ydych yn eu dilyn must_be_following_dm: Blocio negeseuon uniongyrchol o bobl nad ydych yn eu dilyn + invite_request: + text: Pam hoffech ymuno? notification_emails: digest: Anfonwch e-byst crynhoi favourite: Anfon e-bost pan mae rhywun yn ffefrynnu eich statws follow: Anfon e-bost pan mae rhywun yn eich dilyn chi follow_request: Anfon e-bost pan mae rhywun yn gofyn i chi i'w dilyn mention: Anfon e-bost pan mae rhywun yn eich crybwyll + pending_account: Anfon ebost pan mae cyfrif newydd angen adolygiad reblog: Anfon e-bost pan mae rhywun yn bŵstio eich statws report: Anfon e-bost pan y cyflwynir adroddiad newydd 'no': Na + recommended: Argymhellwyd required: + mark: "*" text: gofynnol 'yes': Ie diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index bd78881ae..67f3b64aa 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -67,7 +67,7 @@ el: autofollow: Προσκάλεσε για να ακολουθήσουν το λογαριασμό σου avatar: Αβατάρ bot: Αυτός είναι ένας αυτοματοποιημένος λογαριασμός (bot) - chosen_languages: Φίλτραρε γλώσσες + chosen_languages: Φιλτράρισμα γλωσσών confirm_new_password: Επιβεβαίωσε νέο συνθηματικό confirm_password: Επιβεβαίωσε συνθηματικό context: Πλαίσια φιλτραρίσματος @@ -95,7 +95,7 @@ el: setting_boost_modal: Εμφάνιση ερώτησης επιβεβαίωσης πριν την προώθηση setting_default_language: Γλώσσα δημοσιεύσεων setting_default_privacy: Ιδιωτικότητα δημοσιεύσεων - setting_default_sensitive: Σημείωνε πάντα τα πολυμέσα ως ευαίσθητου περιεχομένου + setting_default_sensitive: Σημείωση των πολυμέσων πάντα ως ευαίσθητου περιεχομένου setting_delete_modal: Εμφάνιση ερώτησης επιβεβαίωσης πριν διαγράψεις ένα τουτ setting_display_media: Εμφάνιση πολυμέσων setting_display_media_default: Προκαθορισμένο @@ -105,7 +105,7 @@ el: setting_hide_network: Κρύψε τις διασυνδέσεις σου setting_noindex: Επέλεξε να μην συμμετέχεις στα αποτελέσματα μηχανών αναζήτησης setting_reduce_motion: Μείωση κίνησης κινουμένων στοιχείων - setting_show_application: Να αποκαλύπτεται η εφαρμογή που χρησιμοποιήθηκε για την αποστολή των τουτ + setting_show_application: Αποκάλυψη εφαρμογής που χρησιμοποιήθηκε για την αποστολή των τουτ setting_system_font_ui: Χρησιμοποίησε την προεπιλεγμένη γραμματοσειρά του συστήματος setting_theme: Θέμα ιστότοπου setting_unfollow_modal: Εμφάνιση ερώτησης επιβεβαίωσης πριν διακόψεις την παρακολούθηση κάποιου diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml index cbe97095b..7d2dbd8a9 100644 --- a/config/locales/simple_form.fa.yml +++ b/config/locales/simple_form.fa.yml @@ -27,6 +27,7 @@ fa: phrase: مستقل از کوچکی و بزرگی حروف، با متن اصلی یا هشدار محتوای بوق‌ها مقایسه می‌شود scopes: واسط‌های برنامه‌نویسی که این برنامه به آن دسترسی دارد. اگر بالاترین سطح دسترسی را انتخاب کنید، دیگر نیازی به انتخاب سطح‌های پایینی ندارید. setting_aggregate_reblogs: برای بازبوق‌هایی که به تازگی برایتان نمایش داده شده‌اند، بازبوق‌های بیشتر را نشان نده (فقط روی بازبوق‌های اخیر تأثیر می‌گذارد) + setting_default_sensitive: تصاویر حساس به طور پیش‌فرض پنهان هستند و می‌توانند با یک کلیک آشکار شوند setting_display_media_default: تصویرهایی را که به عنوان حساس علامت زده شده‌اند پنهان کن setting_display_media_hide_all: همیشه همهٔ عکس‌ها و ویدیوها را پنهان کن setting_display_media_show_all: همیشه تصویرهایی را که به عنوان حساس علامت زده شده‌اند را نشان بده @@ -39,6 +40,8 @@ fa: name: 'شاید بخواهید چنین چیزهایی را به کار ببرید:' imports: data: پروندهٔ CSV که از سرور ماستدون دیگری برون‌سپاری شده + invite_request: + text: این برای بررسی درخواست شما به ما کمک خواهد کرد sessions: otp: 'کد تأیید دومرحله‌ای که اپ روی تلفن شما ساخته را وارد کنید یا یکی از کدهای بازیابی را به کار ببرید:' user: @@ -86,6 +89,7 @@ fa: otp_attempt: کد ورود دومرحله‌ای password: رمز phrase: کلیدواژه یا عبارت + setting_advanced_layout: فعال‌سازی رابط کاربری پیشرفته setting_aggregate_reblogs: بازبوق‌ها را متحد کن setting_auto_play_gif: پخش خودکار تصویرهای متحرک setting_boost_modal: نمایش پیغام تأیید پیش از بازبوقیدن @@ -116,15 +120,20 @@ fa: must_be_follower: مسدودکردن اعلان‌های همه به جز پیگیران must_be_following: مسدودکردن اعلان‌های کسانی که شما پی نمی‌گیرید must_be_following_dm: مسدودکردن پیغام‌های خصوصی کسانی که شما پی نمی‌گیرید + invite_request: + text: چرا می‌خواهید عضو شوید؟ notification_emails: digest: خلاصه‌کردن چند اعلان در یک ایمیل favourite: وقتی کسی نوشتهٔ شما را پسندید ایمیل بفرست follow: وقتی کسی پیگیر شما شد ایمیل بفرست follow_request: وقتی کسی درخواست پیگیری کرد ایمیل بفرست mention: وقتی کسی از شما نام برد ایمیل بفرست + pending_account: وقتی حساب تازه‌ای نیاز به بازبینی داشت ایمیل بفرست reblog: وقتی کسی نوشتهٔ شما را بازبوقید ایمیل بفرست report: وقتی گزارش تازه‌ای فرستاده شد ایمیل بفرست 'no': خیر + recommended: توصیه می‌شود required: + mark: "*" text: ضروری 'yes': بله diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml index c508a5906..7e5e4e79d 100644 --- a/config/locales/simple_form.hu.yml +++ b/config/locales/simple_form.hu.yml @@ -2,59 +2,138 @@ hu: simple_form: hints: + account_warning_preset: + text: Használhatod a tülkökben szokásos szintaxist, URL-eket, hashtageket, megemlítéseket + admin_account_action: + send_email_notification: A felhasználó magyarázatot kap arra, mi történt a fiókjával + text_html: Opcionális. A tülk szintaxis használható. Egyszerűsítés végett létre is hozhatsz figyelmeztetéseket + type_html: Megmondhatod, mi legyen vele %{acct} + warning_preset_id: Opcionális. A figyelmeztetés végére saját szöveget is írhatsz defaults: - avatar: PNG, GIF vagy JPG. Maximum %{size}. Át lesz méretezve %{dimensions} pixelre - digest: Csak hosszú távollét esetén küldve és csak ha személyes üzenetet kaptál távollétedben - header: PNG, GIF vagy JPG. Maximum %{size}. Át lesz méretezve %{dimensions} pixelre + autofollow: Akik meghívón keresztül regisztrálnak, automatikusan követni fognak téged + avatar: PNG, GIF vagy JPG. Maximum %{size}. Átméretezzük %{dimensions} pixelre + bot: Ez a fiók automatikus műveleteket végez és valószínűleg nem figyeljük + context: Kontextusok, ahol a szűrőnek működnie kell + digest: Csak hosszú távollét esetén küldődik és csak ha személyes üzenetet kaptál távollétedben + discoverable_html: A profilok adatbázisa lehetővé teszi, hogy embereket közös érdeklődés vagy tevékenység alapján találj meg. Legalább %{min_followers} követőre van szükség az adatbázisba való bekerüléshez + email: Kapsz egy megerősítő e-mailt + fields: A profilodon legfeljebb 4 bejegyzés szerepelhet táblázatos formában + header: PNG, GIF vagy JPG. Maximum %{size}. Átméretezzük %{dimensions} pixelre + inbox_url: Másold ki a használandó relé szerver kezdőoldalának URL-jét + irreversible: A kiszűrt tülkök visszafordíthatatlanul eltűnnek, a szűrő későbbi törlése esetén is + locale: A felhasználói felület, e-mailek, push üzenetek nyelve locked: Egyenként engedélyezned kell a követőidet - setting_noindex: A publikus profilodra és státusz oldalra vonatkozik + password: Legalább 8 karakter + phrase: Illeszkedni fog kis/nagybetű függetlenül, és tartalom-figyelmeztetések mögött is + scopes: Mely API-kat érheti el az alkalmazás. Ha felső szintű hatáskört választasz, nem kell egyesével kiválasztanod az alatta lévőeket. + setting_aggregate_reblogs: Ne mutassunk megtolásokat olyan tülkökhöz, melyeket nemrég toltak meg (csak új megtolásokra lép életbe) + setting_default_sensitive: A szenzitív médiát alapesetben elrejtjük, de egyetlen kattintással előhozható + setting_display_media_default: Szenzitív tartalomként jelölt média elrejtése + setting_display_media_hide_all: Mindig minden média elrejtése + setting_display_media_show_all: Mindig mutasd a szenzitív tartalomként jelölt médiát + setting_hide_network: Nem látszik majd a profilodon, kik követnek és te kiket követsz + setting_noindex: A nyilvános profilodra és a tülkjeidre vonatkozik + setting_show_application: A tülkök részletes nézetében látszani fog, milyen alkalmazást használtál a tülköléshez + username: A felhasználói neved egyedi lesz a %{domain} domainen + whole_word: Ha a kulcsszó alfanumerikus, csak akkor minősül majd találatnak, ha teljes szóra illeszkedik + featured_tag: + name: 'Ezeket esetleg használhatod:' imports: data: Egy másik Mastodon szerverről exportált CSV fájl + invite_request: + text: Ez segít nekünk átnézni a jelentkezésedet sessions: - otp: Add meg a Második-faktor kódodat a telefonodról vagy használd az egyik tartalék bejelentkező kódodat. + otp: 'Add meg a telefonodon generált kétlépcsős azonosító kódodat vagy használd az egyik tartalék bejelentkező kódot:' + user: + chosen_languages: Ha aktív, csak a kiválasztott nyelvű tülkök jelennek majd meg a nyilvános idővonalon labels: + account: + fields: + name: Címke + value: Tartalom + account_warning_preset: + text: Figyelmeztető szöveg + admin_account_action: + send_email_notification: Figyelmeztessük a felhasználót e-mailben + text: Egyedi figyelmeztetés + type: Művelet + types: + disable: Letiltás + none: Ne csinálj semmit + silence: Elnémítás + suspend: Fiók felfüggesztése, adatok törlése visszaállíthatatlanul + warning_preset_id: Figyelmeztetés használata defaults: + autofollow: Meghívás a fiókod követésére avatar: Profilkép + bot: Ez a fiók egy bot + chosen_languages: Nyelvek szűrése confirm_new_password: Új jelszó megerősítése confirm_password: Jelszó megerősítése + context: Szűrés kontextusai current_password: Jelenlegi jelszó data: Adatok + discoverable: Fiók listázása a profilok adatbázisában display_name: Megjelenített név email: E-mail cím expires_in: Elévül + fields: Profil metaadatok header: Fejléc - locale: Nyelv - locked: Zárt felhasználói fiók - max_uses: Felhasználhatóság + inbox_url: Relé inbox-hoz tartozó URL + irreversible: Eldobás elrejtés helyett + locale: Felhasználói felület nyelve + locked: Felhasználói fiók lezárása + max_uses: Hányszor használható new_password: Új jelszó - note: Önéletrajz - otp_attempt: Második-faktor kód + note: Bemutatkozás + otp_attempt: Kétlépcsős azonosító kód password: Jelszó + phrase: Kulcsszó vagy kifejezés + setting_advanced_layout: Haladó webes felület engedélyezése + setting_aggregate_reblogs: Megtolások csoportosítása az idővonalakon setting_auto_play_gif: GIF-ek automatikus lejátszása - setting_boost_modal: Megerősítés kérése reblogolás előtt - setting_default_privacy: Tülkök alapártelmezett adatvédelmi szintje + setting_boost_modal: Megerősítés kérése megtolás előtt + setting_default_language: Tülkölés nyelve + setting_default_privacy: Tülkök alapértelmezett láthatósága setting_default_sensitive: Minden médiafájl megjelölése szenzitívként setting_delete_modal: Megerősítés kérése tülk törlése előtt + setting_display_media: Média megjelenítése + setting_display_media_default: Alapértelmezés + setting_display_media_hide_all: Mindent elrejt + setting_display_media_show_all: Mindent mutat + setting_expand_spoilers: Tartalom figyelmeztetéssel ellátott tülkök automatikus kinyitása + setting_hide_network: Hálózatod elrejtése setting_noindex: Megtiltom a keresőmotoroknak, hogy indexeljék a tülkjeimet setting_reduce_motion: Animációk mozgásának csökkentése + setting_show_application: A tülkölésre használt alkalmazás feltüntetése setting_system_font_ui: Rendszer betűtípusának használata - setting_theme: Oldalsablon + setting_theme: Megjelenítési sablon setting_unfollow_modal: Megerősítés kérése mielőtt abbahagyod valaki követését severity: Súlyosság type: Importálás típusa username: Felhasználónév + username_or_email: Felhasználónév vagy E-mail + whole_word: Teljes szó + featured_tag: + name: Hashtag interactions: must_be_follower: Nem követőidtől érkező értesítések tiltása must_be_following: Nem követettjeidtől érkező értesítések tiltása must_be_following_dm: Nem követettjeidtől érkező üzenetek tiltása + invite_request: + text: Miért akarsz csatlakozni? notification_emails: digest: Összevont e-mailek küldése - favourite: E-mail küldése amikor valaki kedvencnek jelöli az állapotod - follow: E-mail küldése amikor valaki követni kezd téged - follow_request: E-mail küldése amikor valaki követni szeretne téged - mention: E-mail küldése amikor valaki megemlít téged - reblog: E-mail küldése amikor valaki reblogolja az állapotod + favourite: E-mail küldése, amikor valaki kedvencnek jelöli a tülködet + follow: E-mail küldése, amikor valaki követni kezd téged + follow_request: E-mail küldése, amikor valaki követni szeretne téged + mention: E-mail küldése, amikor valaki megemlít téged + pending_account: E-mail küldése, ha új fiókot kell engedélyezni + reblog: E-mail küldése, amikor valaki megtolja a tülködet + report: E-mail küldése, ha új bejelentés érkezett 'no': Nem + recommended: Ajánlott required: + mark: "*" text: kötelező 'yes': Igen diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 7e9c0f277..8470f7939 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -65,6 +65,7 @@ sk: warning_preset_id: Použi varovnú predlohu defaults: autofollow: Pozvi k následovaniu tvojho profilu + avatar: Maskot bot: Toto je automatizovaný bot účet chosen_languages: Filtruj jazyky confirm_new_password: Znovu tvoje nové heslo, pre potvrdenie @@ -119,6 +120,8 @@ sk: must_be_follower: Blokuj oboznámenia od užívateľov, ktorí ma nenásledujú must_be_following: Blokuj oboznámenia od ľudí, ktorých nesledujem must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesledujem + invite_request: + text: Prečo sa k nám chceš pridať? notification_emails: digest: Zasielať súhrnné emaily favourite: Zaslať email, ak si niekto obľúbi tvoj príspevok @@ -129,6 +132,7 @@ sk: reblog: Zaslať email, ak niekto re-tootne tvoj príspevok report: Zaslať email, ak niekto podá nové nahlásenie 'no': Nie + recommended: Odporúčané required: text: povinné 'yes': Áno diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml index a2d430558..9d6b75ed0 100644 --- a/config/locales/simple_form.th.yml +++ b/config/locales/simple_form.th.yml @@ -17,7 +17,7 @@ th: digest: ส่งเฉพาะหลังจากไม่มีการใช้งานเป็นเวลานานและในกรณีที่คุณได้รับข้อความส่วนบุคคลใด ๆ เมื่อคุณไม่อยู่เท่านั้น discoverable_html: ไดเรกทอรี ช่วยให้ผู้คนค้นหาบัญชีตามความสนใจและกิจกรรม ต้องการอย่างน้อย %{min_followers} ผู้ติดตาม email: คุณจะได้รับอีเมลยืนยัน - fields: คุณสามารถมีได้มากถึง 4 รายการแสดงผลเป็นตารางในโปรไฟล์ของคุณ + fields: คุณสามารถมีได้มากถึง 4 รายการแสดงเป็นตารางในโปรไฟล์ของคุณ header: PNG, GIF หรือ JPG สูงสุด %{size} จะถูกย่อขนาดเป็น %{dimensions}px inbox_url: คัดลอก URL จากหน้าแรกของรีเลย์ที่คุณต้องการใช้ irreversible: โพสต์ที่กรองจะหายไปอย่างถาวร แม้ว่าจะเอาตัวกรองออกในภายหลัง @@ -27,22 +27,25 @@ th: phrase: จะถูกจับคู่โดยไม่คำนึงถึงตัวอักษรใหญ่เล็กในข้อความหรือคำเตือนเนื้อหาของโพสต์ scopes: API ใดที่แอปพลิเคชันจะได้รับอนุญาตให้เข้าถึง หากคุณเลือกขอบเขตระดับบนสุด คุณไม่จำเป็นต้องเลือกแต่ละขอบเขต setting_aggregate_reblogs: ไม่แสดงการดันใหม่สำหรับโพสต์ที่เพิ่งดัน (มีผลต่อการดันที่ได้รับใหม่เท่านั้น) + setting_default_sensitive: ซ่อนสื่อที่ละเอียดอ่อนโดยค่าเริ่มต้นและสามารถเปิดเผยได้ด้วยการคลิก setting_display_media_default: ซ่อนสื่อที่ถูกทำเครื่องหมายว่าละเอียดอ่อน setting_display_media_hide_all: ซ่อนสื่อทั้งหมดเสมอ setting_display_media_show_all: แสดงสื่อที่ถูกทำเครื่องหมายว่าละเอียดอ่อนเสมอ setting_hide_network: จะไม่แสดงผู้ที่คุณติดตามและผู้ที่ติดตามคุณในโปรไฟล์ของคุณ setting_noindex: มีผลต่อโปรไฟล์สาธารณะและหน้าสถานะของคุณ - setting_show_application: จะแสดงผลแอปพลิเคชันที่คุณใช้เพื่อโพสต์ในมุมมองโดยละเอียดของโพสต์ของคุณ + setting_show_application: จะแสดงแอปพลิเคชันที่คุณใช้เพื่อโพสต์ในมุมมองโดยละเอียดของโพสต์ของคุณ username: ชื่อผู้ใช้ของคุณจะไม่ซ้ำกันบน %{domain} whole_word: เมื่อคำสำคัญหรือวลีมีแค่ตัวอักษรและตัวเลข จะถูกใช้หากตรงกันทั้งคำเท่านั้น featured_tag: name: 'คุณอาจต้องการใช้หนึ่งในนี้:' imports: data: ไฟล์ CSV ที่ส่งออกจากเซิร์ฟเวอร์ Mastodon อื่น + invite_request: + text: นี่จะช่วยให้เราตรวจทานใบสมัครของคุณ sessions: otp: 'ป้อนรหัสสองปัจจัยที่สร้างโดยแอปในโทรศัพท์ของคุณหรือใช้หนึ่งในรหัสกู้คืนของคุณ:' user: - chosen_languages: เมื่อกาเครื่องหมาย จะแสดงผลเฉพาะโพสต์ในภาษาที่เลือกในเส้นเวลาสาธารณะ + chosen_languages: เมื่อกาเครื่องหมาย จะแสดงเฉพาะโพสต์ในภาษาที่เลือกในเส้นเวลาสาธารณะ labels: account: fields: @@ -71,7 +74,7 @@ th: current_password: รหัสผ่านปัจจุบัน data: ข้อมูล discoverable: แสดงรายการบัญชีนี้ในไดเรกทอรี - display_name: ชื่อที่แสดงผล + display_name: ชื่อที่แสดง email: ที่อยู่อีเมล expires_in: หมดอายุหลังจาก fields: ข้อมูลเมตาโปรไฟล์ @@ -86,6 +89,7 @@ th: otp_attempt: รหัสสองปัจจัย password: รหัสผ่าน phrase: คำสำคัญหรือวลี + setting_advanced_layout: เปิดใช้งานส่วนติดต่อเว็บขั้นสูง setting_aggregate_reblogs: จัดกลุ่มการดันในเส้นเวลา setting_auto_play_gif: เล่น GIF แบบเคลื่อนไหวโดยอัตโนมัติ setting_boost_modal: แสดงกล่องโต้ตอบการยืนยันก่อนดัน @@ -93,7 +97,7 @@ th: setting_default_privacy: ความเป็นส่วนตัวของโพสต์ setting_default_sensitive: ทำเครื่องหมายสื่อว่าละเอียดอ่อนเสมอ setting_delete_modal: แสดงกล่องโต้ตอบการยืนยันก่อนลบโพสต์ - setting_display_media: การแสดงผลสื่อ + setting_display_media: การแสดงสื่อ setting_display_media_default: ค่าเริ่มต้น setting_display_media_hide_all: ซ่อนทั้งหมด setting_display_media_show_all: แสดงทั้งหมด @@ -116,15 +120,20 @@ th: must_be_follower: ปิดกั้นการแจ้งเตือนจากที่ไม่ใช่ผู้ติดตาม must_be_following: ปิดกั้นการแจ้งเตือนจากผู้คนที่คุณไม่ได้ติดตาม must_be_following_dm: ปิดกั้นข้อความโดยตรงจากผู้คนที่คุณไม่ได้ติดตาม + invite_request: + text: ทำไมคุณจึงต้องการเข้าร่วม? notification_emails: digest: ส่งอีเมลสรุป favourite: ส่งอีเมลเมื่อใครสักคนชื่นชอบสถานะของคุณ follow: ส่งอีเมลเมื่อใครสักคนติดตามคุณ follow_request: ส่งอีเมลเมื่อใครสักคนขอติดตามคุณ mention: ส่งอีเมลเมื่อใครสักคนกล่าวถึงคุณ + pending_account: ส่งอีเมลเมื่อบัญชีใหม่ต้องการการตรวจทาน reblog: ส่งอีเมลเมื่อใครสักคนดันสถานะของคุณ report: ส่งอีเมลเมื่อมีการส่งรายงานใหม่ 'no': ไม่ + recommended: แนะนำ required: + mark: "*" text: ต้องระบุ 'yes': ใช่ diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 2dbd7d66e..b781deb32 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -90,7 +90,7 @@ zh-CN: password: 密码 phrase: 关键词 setting_advanced_layout: 启用高级 web 界面 - setting_aggregate_reblogs: 在时间线中合并转嘟 + setting_aggregate_reblogs: 在时间轴中合并转嘟 setting_auto_play_gif: 自动播放 GIF 动画 setting_boost_modal: 在转嘟前询问我 setting_default_language: 发布语言 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 21ce53217..70574dc86 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -7,6 +7,7 @@ sk: active_count_after: aktívni active_footnote: Mesačne aktívnych užívateľov (MAU) administered_by: 'Správcom je:' + api: API apps: Aplikácie apps_platforms: Uživaj Mastodon z iOSu, Androidu a iných platforiem browse_directory: Prehľadávaj databázu profilov, filtruj podľa záujmov @@ -28,14 +29,29 @@ sk: see_whats_happening: Pozoruj, čo sa deje server_stats: 'Serverové štatistiky:' source_code: Zdrojový kód + status_count_after: + few: príspevkov + many: príspevkov + one: príspevok + other: príspevky status_count_before: Ktorí napísali tagline: Následuj kamarátov, a objavuj nových terms: Podmienky užívania + user_count_after: + few: užívateľov + many: užívateľov + one: užívateľ + other: uživatelia user_count_before: Domov pre what_is_mastodon: Čo je Mastodon? accounts: choices_html: "%{name}vé voľby:" follow: Následuj + followers: + few: Sledovateľov + many: Sledovateľov + one: Sledujúci + other: Sledovatelia following: Následujem joined: Pridal/a sa v %{date} last_active: naposledy aktívny @@ -48,11 +64,17 @@ sk: people_who_follow: Ľudia sledujúci %{name} pin_errors: following: Musíš už následovať toho človeka, ktorého si praješ zviditeľniť + posts: + few: Príspevkov + many: Príspevkov + one: Príspevok + other: Príspevky posts_tab_heading: Príspevky posts_with_replies: Príspevky s odpoveďami reserved_username: Prihlasovacie meno je vyhradené roles: admin: Správca + bot: Bot moderator: Moderátor unavailable: Profil nieje dostupný unfollow: Prestaň sledovať @@ -89,6 +111,7 @@ sk: display_name: Ukáž meno domain: Doména edit: Uprav + email: Email email_status: Stav emailu enable: Povoľ enabled: Povolený @@ -159,6 +182,7 @@ sk: statuses: Príspevky subscribe: Odoberaj suspended: Vylúčený/á + time_in_queue: Čakajúci %{time} v poradí title: Účty unconfirmed_email: Nepotvrdený email undo_silenced: Zruš stíšenie @@ -166,6 +190,7 @@ sk: unsubscribe: Prestaň odoberať username: Prezývka warn: Varuj + web: Web action_logs: actions: assigned_to_self_report: "%{name}pridelil/a hlásenie užívateľa %{target}sebe" @@ -273,6 +298,11 @@ sk: silence: stíšené suspend: vylúčené show: + affected_accounts: + few: "%{count} účtov v databázi ovplyvnených" + many: "%{count} účtov v databázi ovplyvnených" + one: "%{count} účet v databázi ovplyvnený" + other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény suspend: Zruš suspendáciu všetkých momentálne ovplyvnených účtov z tejto domény @@ -295,6 +325,11 @@ sk: instances: by_domain: Doména delivery_available: Je v dosahu doručovania + known_accounts: + few: "%{count} známych účtov" + many: "%{count} známych účtov" + one: "%{count} známy účet" + other: "%{count} známe účty" moderation: all: Všetky limited: Obmedzené @@ -451,6 +486,7 @@ sk: confirmed: Potvrdené expires_in: Vyprší do last_delivery: Posledné doručenie + title: WebSub topic: Téma tags: accounts: Účty @@ -470,6 +506,7 @@ sk: admin_mailer: new_pending_account: body: Podrobnosti o novom účte sú uvedené nižšie. Môžeš túto registračnú požiadavku buď prijať, alebo zamietnúť. + subject: Nový účet očakáva preverenie na %{instance} (%{username}) new_report: body: "%{reporter} nahlásil/a %{target}" body_remote: Niekto z %{domain} nahlásil/a %{target} @@ -554,6 +591,11 @@ sk: explanation: Pátraj po užívateľoch podľa ich záujmov explore_mastodon: Prebádaj %{title} how_to_enable: Momentálne niesi zaradený/á do verejnej profilovej databázy. Prihlásiť sa môžeš nižšie. Použi haštagy vo svojom biografickom popise na profile, ak chceš byť uvedený/á aj pod konkrétnými haštagmi! + people: + few: "%{count} ľudí" + many: "%{count} ľudí" + one: "%{count} človek" + other: "%{count} ľudia" errors: '403': Nemáš povolenie pre zobrazenie tejto stránky. '404': Stránka ktorú hľadáš nieje tu. @@ -566,6 +608,9 @@ sk: content: Ospravedlňujem sa. Niečo sa pokazilo na našom konci. title: Táto stránka nieje v poriadku noscript_html: Aby bolo možné používať Mastodon web aplikáciu, povoľ prosím JavaScript. Alebo skús jednu z aplikácii dostupných pre vašu platformu. + existing_username_validator: + not_found: nepodarilo sa nájsť miestného užívateľa s takouto prezývkou + not_found_multiple: nepodarilo sa nájsť %{usernames} exports: archive_takeout: date: Dátum @@ -575,6 +620,7 @@ sk: request: Vyžiadaj si tvoj archív size: Veľkosť blocks: Blokujete + csv: CSV domain_blocks: Blokované domény follows: Následujete lists: Zoznamy @@ -605,9 +651,23 @@ sk: more: Viac… resources: Podklady generic: + all: Všetko changes_saved_msg: Zmeny boli úspešne uložené! copy: Kopíruj + order_by: Zoraď podľa save_changes: Ulož zmeny + identity_proofs: + active: Aktívne + authorize: Áno, povoľ + authorize_connection_prompt: Povoliť toto kryptografické prepojenie? + errors: + failed: Kryptografické prepojenie sa nepodarilo. Prosím skús to znova z %{provider}. + i_am_html: Na %{service} som %{username}. + identity: Identita + inactive: Neaktívne + publicize_checkbox: 'A poslať toto:' + publicize_toot: 'Je to dokázané! Na %{service} som %{username}: %{url}' + status: Stav overenia imports: modes: merge: Spoj dohromady @@ -636,6 +696,11 @@ sk: expires_in_prompt: Nikdy generate: Vygeneruj invited_by: 'Bol/a si pozvaný/á užívateľom:' + max_uses: + few: "%{count} využití" + many: "%{count} využití" + one: 1 využitie + other: "%{count} využitia" max_uses_prompt: Bez obmedzení prompt: Vygeneruj a zdieľaj linky s ostatnými, aby mali umožnený prístup k tomuto serveru table: @@ -701,6 +766,17 @@ sk: too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: other: Ostatné + relationships: + activity: Aktivita účtu + dormant: Spiace + last_active: Naposledy aktívny + most_recent: Najnovšie + moved: Presunuli sa + mutual: Spoločné + relationship: Vzťah + remove_selected_followers: Odstráň vybraných následovatrľov + remove_selected_follows: Prestaň sledovať vybraných užívateľov + status: Stav účtu remote_follow: acct: Napíš svoju prezývku@doménu z ktorej chceš následovať missing_resource: Nemožno nájsť potrebnú presmerovaciu adresu k tvojmu účtu @@ -764,6 +840,7 @@ sk: edit_profile: Uprav profil export: Exportuj dáta featured_tags: Zvýraznené haštagy + identity_proofs: Overenia identity import: Importuj import_and_export: Import a export migrate: Presuň účet @@ -775,7 +852,7 @@ sk: statuses: attached: description: 'Priložené: %{attached}' - boosted_from_html: Povýšené od %{acct_link} + boosted_from_html: Vyzdvihnuté od %{acct_link} content_warning: 'Varovanie o obsahu: %{warning}' language_detection: Zisti automaticky open_in_web: Otvor v okne na webe diff --git a/config/locales/th.yml b/config/locales/th.yml index 9ef6bc3dd..7a16bc2f3 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -6,6 +6,7 @@ th: active_count_after: ที่ใช้งาน active_footnote: ผู้ใช้งานรายเดือน (MAU) administered_by: 'ดูแลโดย:' + api: API apps: แอปสำหรับมือถือ apps_platforms: ใช้ Mastodon จาก iOS, Android และแพลตฟอร์มอื่น ๆ browse_directory: เรียกดูไดเรกทอรีโปรไฟล์และกรองตามความสนใจ @@ -22,16 +23,19 @@ th: get_apps: ลองแอปสำหรับมือถือ learn_more: เรียนรู้เพิ่มเติม privacy_policy: นโยบายความเป็นส่วนตัว + see_whats_happening: ดูสิ่งที่กำลังเกิดขึ้น server_stats: 'สถิติเซิร์ฟเวอร์:' source_code: โค้ดต้นฉบับ status_count_after: other: สถานะ status_count_before: ผู้สร้าง + terms: เงื่อนไขการให้บริการ user_count_after: other: ผู้ใช้ user_count_before: บ้านของ what_is_mastodon: Mastodon คืออะไร? accounts: + choices_html: 'ทางเลือกของ %{name}:' follow: ติดตาม followers: other: ผู้ติดตาม @@ -52,6 +56,7 @@ th: admin: ผู้ดูแล bot: บอต moderator: ผู้ควบคุม + unavailable: ไม่มีโปรไฟล์ unfollow: เลิกติดตาม admin: account_moderation_notes: @@ -60,6 +65,7 @@ th: destroyed_msg: ทำลายหมายเหตุการควบคุมสำเร็จ! accounts: approve: อนุมัติ + approve_all: อนุมัติทั้งหมด are_you_sure: คุณแน่ใจหรือไม่? avatar: ภาพประจำตัว by_domain: โดเมน @@ -78,7 +84,7 @@ th: disable: ปิดใช้งาน disable_two_factor_authentication: ปิดใช้งาน 2FA disabled: ปิดใช้งานอยู่ - display_name: ชื่อที่แสดงผล + display_name: ชื่อที่แสดง domain: โดเมน edit: แก้ไข email: อีเมล @@ -88,10 +94,11 @@ th: feed_url: URL ฟีด followers: ผู้ติดตาม followers_url: URL ผู้ติดตาม - follows: ติดตาม + follows: การติดตาม header: ส่วนหัว inbox_url: URL กล่องขาเข้า invited_by: เชิญโดย + ip: IP joined: เข้าร่วมเมื่อ location: all: ทั้งหมด @@ -115,7 +122,9 @@ th: protocol: โปรโตคอล public: สาธารณะ push_subscription_expires: การบอกรับ PuSH หมดอายุเมื่อ + redownload: รีเฟรชโปรไฟล์ reject: ปฏิเสธ + reject_all: ปฏิเสธทั้งหมด remove_avatar: เอาภาพประจำตัวออก remove_header: เอาส่วนหัวออก resend_confirmation: @@ -124,6 +133,7 @@ th: success: ส่งอีเมลยืนยันสำเร็จ! reset_password: ตั้งรหัสผ่านใหม่ resubscribe: บอกรับใหม่ + role: สิทธิอนุญาต roles: admin: ผู้ดูแล moderator: ผู้ควบคุม @@ -153,9 +163,13 @@ th: copy: คัดลอก created_msg: สร้างอีโมจิสำเร็จ! delete: ลบ + destroyed_msg: ทำลายอีโมโจสำเร็จ! disable: ปิดใช้งาน + disabled_msg: ปิดใช้งานอีโมจินั้นสำเร็จ emoji: อีโมจิ enable: เปิดใช้งาน + enabled_msg: เปิดใช้งานอีโมจินั้นสำเร็จ + image_hint: PNG สูงสุด 50KB new: title: เพิ่มอีโมจิที่กำหนดเองใหม่ overwrite: เขียนทับ @@ -171,6 +185,7 @@ th: feature_invites: ลิงก์เชิญ feature_profile_directory: ไดเรกทอรีโปรไฟล์ feature_registrations: การลงทะเบียน + feature_timeline_preview: ตัวอย่างเส้นเวลา features: คุณลักษณะ open_reports: รายงานที่เปิด recent_users: ผู้ใช้ล่าสุด @@ -241,6 +256,7 @@ th: enable: เปิดใช้งาน enabled: เปิดใช้งานอยู่ inbox_url: URL รีเลย์ + save_and_enable: บันทึกแล้วเปิดใช้งาน status: สถานะ title: รีเลย์ report_notes: @@ -280,7 +296,7 @@ th: title: เปิดใช้งานไดเรกทอรีโปรไฟล์ registrations: closed_message: - desc_html: แสดงผลในหน้าแรกเมื่อปิดการลงทะเบียน คุณสามารถใช้แท็ก HTML + desc_html: แสดงในหน้าแรกเมื่อปิดการลงทะเบียน คุณสามารถใช้แท็ก HTML title: ข้อความการปิดการลงทะเบียน deletion: desc_html: อนุญาตให้ใครก็ตามลบบัญชีของเขา @@ -297,7 +313,7 @@ th: desc_html: แสดงป้ายพนักงานในหน้าผู้ใช้ title: แสดงป้ายพนักงาน site_description: - desc_html: ย่อหน้าเกริ่นนำในหน้าแรก อธิบายถึงสิ่งที่ทำให้เซิร์ฟเวอร์ Mastodon นี้พิเศษและสิ่งอื่นใดที่สำคัญ คุณสามารถใช้แท็ก HTML โดยเฉพาะอย่างยิ่ง <a> และ <em> + desc_html: ย่อหน้าเกริ่นนำใน API อธิบายถึงสิ่งที่ทำให้เซิร์ฟเวอร์ Mastodon นี้พิเศษและสิ่งอื่นใดที่สำคัญ คุณสามารถใช้แท็ก HTML โดยเฉพาะอย่างยิ่ง <a> และ <em> title: คำอธิบายเซิร์ฟเวอร์ site_description_extended: desc_html: สถานที่ที่ดีสำหรับแนวทางปฏิบัติ, กฎ, หลักเกณฑ์ และสิ่งอื่น ๆ ของคุณที่ทำให้เซิร์ฟเวอร์ของคุณแตกต่าง คุณสามารถใช้แท็ก HTML @@ -324,6 +340,7 @@ th: confirmed: ยืนยันแล้ว expires_in: หมดอายุภายใน last_delivery: ส่งล่าสุด + title: WebSub topic: หัวข้อ tags: accounts: บัญชี @@ -340,6 +357,10 @@ th: edit: แก้ไข edit_preset: แก้ไขคำเตือนที่ตั้งไว้ล่วงหน้า title: จัดการคำเตือนที่ตั้งไว้ล่วงหน้า + appearance: + advanced_web_interface: ส่วนติดต่อเว็บขั้นสูง + animations_and_accessibility: ภาพเคลื่อนไหวและการช่วยการเข้าถึง + sensitive_content: เนื้อหาที่ละเอียดอ่อน application_mailer: notification_preferences: เปลี่ยนการกำหนดลักษณะอีเมล settings: 'เปลี่ยนการกำหนดลักษณะอีเมล: %{link}' @@ -358,6 +379,9 @@ th: logout: ออกจากระบบ migrate_account: ย้ายไปยังบัญชีอื่น or_log_in_with: หรือเข้าสู่ระบบด้วย + providers: + cas: CAS + saml: SAML register: ลงทะเบียน resend_confirmation: ส่งขั้นตอนวิธีการยืนยันใหม่อีกครั้ง reset_password: ตั้งรหัสผ่านใหม่ @@ -367,6 +391,7 @@ th: authorize_follow: already_following: คุณกำลังติดตามบัญชีนี้อยู่แล้ว follow: ติดตาม + follow_request: 'คุณได้ส่งคำขอติดตามไปยัง:' following: 'สำเร็จ! คุณกำลังติดตาม:' post_follow: return: แสดงโปรไฟล์ของผู้ใช้ @@ -394,7 +419,7 @@ th: explanation: ค้นพบผู้ใช้ตามความสนใจของเขา explore_mastodon: สำรวจ %{title} errors: - '403': คุณไม่มีสิทธิดูหน้านี้ + '403': คุณไม่มีสิทธิอนุญาตเพื่อดูหน้านี้ '404': หน้าที่คุณกำลังมองหาไม่ได้อยู่ที่นี่ '410': หน้าที่คุณกำลังมองหาไม่มีอยู่ที่นี่อีกต่อไป '422': @@ -410,6 +435,7 @@ th: request: ขอการเก็บถาวรของคุณ size: ขนาด blocks: คุณปิดกั้น + csv: CSV domain_blocks: การปิดกั้นโดเมน follows: คุณติดตาม lists: รายการ @@ -439,6 +465,8 @@ th: changes_saved_msg: บันทึกการเปลี่ยนแปลงสำเร็จ! copy: คัดลอก save_changes: บันทึกการเปลี่ยนแปลง + identity_proofs: + authorize: ใช่ อนุญาต imports: modes: merge: ผสาน @@ -479,6 +507,7 @@ th: mention: "%{name} ได้กล่าวถึงคุณใน:" new_followers_summary: other: You have gotten %{count} new followers! Amazing! + title: เมื่อคุณไม่อยู่... favourite: body: 'สถานะของคุณได้รับการชื่นชอบโดย %{name}:' subject: "%{name} ได้ชื่นชอบสถานะของคุณ" @@ -506,8 +535,13 @@ th: next: ถัดไป older: เก่ากว่า prev: ก่อนหน้า + truncate: "…" + preferences: + posting_defaults: ค่าเริ่มต้นการโพสต์ + public_timelines: เส้นเวลาสาธารณะ relationships: activity: กิจกรรมบัญชี + last_active: ใช้งานล่าสุด relationship: ความสัมพันธ์ remove_selected_domains: เอาผู้ติดตามทั้งหมดออกจากโดเมนที่เลือก remove_selected_followers: เอาผู้ติดตามที่เลือกออก @@ -535,8 +569,44 @@ th: sessions: activity: กิจกรรมล่าสุด browser: เบราว์เซอร์ + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: เซสชันปัจจุบัน + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: เพิกถอน + revoke_success: เพิกถอนเซสชันสำเร็จ + title: เซสชัน settings: + account: บัญชี + account_settings: การตั้งค่าบัญชี + appearance: ลักษณะที่ปรากฏ authorized_apps: แอปที่ได้รับอนุญาต back: กลับไปที่ Mastodon delete: การลบบัญชี @@ -544,8 +614,10 @@ th: edit_profile: แก้ไขโปรไฟล์ export: การส่งออกข้อมูล import: นำเข้า + import_and_export: การนำเข้าและการส่งออก notifications: การแจ้งเตือน preferences: การกำหนดลักษณะ + profile: โปรไฟล์ relationships: การติดตามและผู้ติดตาม two_factor_authentication: การรับรองความถูกต้องด้วยสองปัจจัย statuses: @@ -564,6 +636,7 @@ th: other: "%{count} การลงคะแนน" show_more: แสดงเพิ่มเติม sign_in_to_participate: ลงชื่อเข้าเพื่อเข้าร่วมการสนทนา + title: '%{name}: "%{quote}"' visibilities: private: ผู้ติดตามเท่านั้น private_long: แสดงต่อผู้ติดตามเท่านั้น diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 538a035d2..0c9b291ad 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1,45 +1,60 @@ --- zh-CN: about: - about_hashtag_html: 这里展示的是带有话题标签 #%{hashtag} 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。 + about_hashtag_html: 这里展示的是带有话题标签 #%{hashtag} 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 站点或与其兼容的网站上拥有一个帐户。 about_mastodon_html: Mastodon(长毛象)是一个建立在开放式网络协议和自由、开源软件之上的社交网络,有着类似于电子邮件的分布式设计。 - about_this: 关于本实例 + about_this: 关于本站 active_count_after: 活跃 active_footnote: 每月活跃用户 - administered_by: 本实例的管理员: + administered_by: 本站管理员: + api: API apps: 移动应用 + apps_platforms: 在 iOS、Android 和其他平台上使用 Mastodon + browse_directory: 浏览用户资料目录并按兴趣筛选 + browse_public_posts: 浏览 Mastodon 上公共嘟文的实时信息流 contact: 联系方式 contact_missing: 未设定 contact_unavailable: 未公开 + discover_users: 发现用户 documentation: 文档 extended_description_html: |

这里可以写一些规定

本站尚未设置详细介绍。

+ federation_hint_html: 在%{instance} 上拥有账户后,你可以关注任何 Mastodon 服务器或其他服务器上的人。 generic_description: "%{domain} 是这个庞大网络中的一台服务器" + get_apps: 尝试移动应用 hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例 learn_more: 了解详情 privacy_policy: 隐私政策 + see_whats_happening: 看一看现在在发生什么 + server_stats: 服务器统计数据: source_code: 源代码 status_count_after: other: 条嘟文 status_count_before: 他们共嘟出了 + tagline: 关注朋友并发现新朋友 terms: 使用条款 user_count_after: other: 位用户 user_count_before: 这里共注册有 what_is_mastodon: Mastodon 是什么? accounts: + choices_html: "%{name} 的推荐:" follow: 关注 followers: other: 关注者 following: 正在关注 joined: 加入于 %{date} + last_active: 最近活动 + link_verified_on: 此链接的所有权已在 %{date} 检查 media: 媒体 moved_html: "%{name} 已经迁移到 %{new_profile_link}:" network_hidden: 此信息不可用 nothing_here: 这里神马都没有! people_followed_by: "%{name} 关注的人" people_who_follow: 关注 %{name} 的人 + pin_errors: + following: 您必须关注您要推荐的人 posts: other: 嘟文 posts_tab_heading: 嘟文 @@ -49,14 +64,20 @@ zh-CN: admin: 管理员 bot: 机器人 moderator: 监察员 + unavailable: 个人资料不可用 unfollow: 取消关注 admin: + account_actions: + action: 执行操作 + title: 在 %{acct} 上执行管理操作 account_moderation_notes: create: 新建 created_msg: 管理备忘建立成功! delete: 删除 destroyed_msg: 管理备忘删除成功! accounts: + approve: 批准 + approve_all: 批准全部 are_you_sure: 你确定吗? avatar: 头像 by_domain: 域名 @@ -70,6 +91,7 @@ zh-CN: confirm: 确认 confirmed: 已确认 confirming: 确认 + deleted: 已删除 demote: 降任 disable: 停用 disable_two_factor_authentication: 停用双重认证 @@ -85,8 +107,11 @@ zh-CN: followers: 关注者 followers_url: 关注者(Followers)URL follows: 正在关注 + header: 个人资料页横幅图片 inbox_url: 收件箱(Inbox)URL + invited_by: 邀请者为 ip: IP 地址 + joined: 加入于 location: all: 全部 local: 本地 @@ -96,15 +121,20 @@ zh-CN: media_attachments: 媒体文件 memorialize: 设置为追悼帐户 moderation: + active: 活跃 all: 全部 + pending: 待审核 silenced: 已隐藏 suspended: 已封禁 title: 帐户状态 moderation_notes: 管理备忘 most_recent_activity: 最后一次活跃的时间 most_recent_ip: 最后一次活跃的 IP 地址 + no_account_selected: 因为没有账户被选择,所以没有更改 + no_limits_imposed: 无限制 not_subscribed: 未订阅 outbox_url: 发件箱(Outbox)URL + pending: 待审核 perform_full_suspension: 封禁 profile_url: 个人资料页面 URL promote: 升任 @@ -112,7 +142,10 @@ zh-CN: public: 公开页面 push_subscription_expires: PuSH 订阅过期时间 redownload: 刷新个人资料 + reject: 拒绝 + reject_all: 拒绝全部 remove_avatar: 删除头像 + remove_header: 删除横幅图片 resend_confirmation: already_confirmed: 该用户已被确认 send: 重发确认邮件 @@ -133,24 +166,30 @@ zh-CN: created_reports: 这个帐户提交的举报 targeted_reports: 针对这个帐户的举报 silence: 隐藏 + silenced: 已隐藏 statuses: 嘟文 subscribe: 订阅 + suspended: 已封禁 + time_in_queue: 已经等待了 %{time} title: 用户 unconfirmed_email: 待验证的电子邮件地址 undo_silenced: 解除隐藏 undo_suspension: 解除封禁 unsubscribe: 取消订阅 username: 用户名 + warn: 警告 web: 站内页面 action_logs: actions: assigned_to_self_report: "%{name} 接管了举报 %{target}" change_email_user: "%{name} 更改了用户 %{target} 的电子邮件地址" confirm_user: "%{name} 确认了用户 %{target} 的电子邮件地址" + create_account_warning: "%{name} 向 %{target} 发送了警告" create_custom_emoji: "%{name} 添加了新的自定义表情 %{target}" create_domain_block: "%{name} 屏蔽了域名 %{target}" create_email_domain_block: "%{name} 屏蔽了电子邮件域名 %{target}" demote_user: "%{name} 对用户 %{target} 进行了降任操作" + destroy_custom_emoji: "%{name} 销毁了自定义表情 %{target}" destroy_domain_block: "%{name} 解除了对域名 %{target} 的屏蔽" destroy_email_domain_block: "%{name} 解除了对电子邮件域名 %{target} 的屏蔽" destroy_status: "%{name} 删除了 %{target} 的嘟文" @@ -204,8 +243,10 @@ zh-CN: config: 服务器配置 feature_deletions: 帐户删除 feature_invites: 邀请链接 + feature_profile_directory: 用户资料目录 feature_registrations: 公开注册 feature_relay: 同步中继 + feature_timeline_preview: 时间轴预览 features: 功能 hidden_service: 匿名服务连通性 open_reports: 待处理举报数 @@ -225,6 +266,7 @@ zh-CN: created_msg: 正在进行域名屏蔽 destroyed_msg: 域名屏蔽已撤销 domain: 域名 + existing_domain_block_html: 您已经对 %{name} 施加了更严格的限制,您需要先 解封。 new: create: 添加屏蔽 hint: 域名屏蔽不会阻止该域名下的帐户进入本站的数据库,但是会对来自这个域名的帐户自动进行预先设置的管理操作。 @@ -236,6 +278,13 @@ zh-CN: title: 添加域名屏蔽 reject_media: 拒绝接收媒体文件 reject_media_hint: 删除本地已缓存的媒体文件,并且不再接收来自该域名的任何媒体文件。此选项不影响封禁 + reject_reports: 拒绝接收举报 + reject_reports_hint: 忽略来自此域名的所有举报。这和封禁无关。 + rejecting_media: 拒绝接收媒体文件 + rejecting_reports: 拒绝接收举报 + severity: + silence: 已隐藏 + suspend: 已封禁 show: affected_accounts: other: 将会影响到数据库中的 %{count} 个帐户 @@ -255,8 +304,24 @@ zh-CN: create: 添加域名 title: 添加电子邮件域名屏蔽 title: 电子邮件域名屏蔽 + followers: + back_to_account: 返回帐户 + title: "%{acct} 的关注者" instances: + by_domain: 域名 + delivery_available: 无法投递 + known_accounts: + other: "%{count} 个已知帐户" + moderation: + all: 全部 + limited: 受限的 + title: 运营 title: 已知实例 + total_blocked_by_us: 被我方屏蔽的 + total_followed_by_them: 被对方关注的 + total_followed_by_us: 被我方关注的 + total_reported: 关于对方的举报 + total_storage: 媒体文件 invites: deactivate_all: 撤销所有邀请链接 filter: @@ -265,10 +330,17 @@ zh-CN: expired: 已失效 title: 筛选 title: 邀请用户 + pending_accounts: + title: 待处理的帐户 (%{count}) relays: add_new: 添加新的中继 + delete: 删除 description_html: "同步中继是一种中间服务器,各实例可以通过订阅中继和向中继推送信息的方式来大量交换公开嘟文。它可以帮助中小型实例发现网络中的内容,而无需本地用户手动关注其他远程实例上的用户。" + disable: 禁用 + disabled: 已禁用 + enable: 启用 enable_hint: 启用此功能后,你的实例会订阅此中继的所有公开嘟文,并同时向其推送本服务器的公开嘟文。 + enabled: 已启用 inbox_url: 中继 URL pending: 等待中继确认 save_and_enable: 保存并启用 @@ -324,12 +396,18 @@ zh-CN: hero: desc_html: 用于在首页展示。推荐分辨率 600×100px 以上。未指定的情况下将默认使用本站缩略图 title: 主题图片 + mascot: + desc_html: 用于在首页展示。推荐分辨率 293×205px 以上。未指定的情况下将使用默认吉祥物。 + title: 吉祥物图像 peers_api_enabled: desc_html: 截至目前本实例在网络中已发现的域名 title: 公开已知实例的列表 preview_sensitive_media: desc_html: 始终在站外链接预览中展示缩略图,无论媒体内容是否标记为敏感 title: 在 OpenGraph 预览中显示敏感媒体内容 + profile_directory: + desc_html: 允许用户可被发现 + title: 启用用户资料目录 registrations: closed_message: desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签 @@ -340,6 +418,12 @@ zh-CN: min_invite_role: disabled: 没有人 title: 允许发送邀请的用户组 + registrations_mode: + modes: + approved: 注册时需要批准 + none: 关闭注册 + open: 开放注册 + title: 注册模式 show_known_fediverse_at_about_page: desc_html: 启用此选项将会在预览中显示来自已知实例的嘟文,否则只会显示本站时间轴的内容. title: 在时间轴预览中显示已知实例 @@ -352,6 +436,9 @@ zh-CN: site_description_extended: desc_html: 可以填写行为守则、规定、指南或其他本站特有的内容。可以使用 HTML 标签 title: 本站详细介绍 + site_short_description: + desc_html: 会在在侧栏和元数据标签中显示。可以用一小段话描述 Mastodon 是什么,以及本服务器的特点。 + title: 服务器一句话介绍 site_terms: desc_html: 可以填写自己的隐私权政策、使用条款或其他法律文本。可以使用 HTML 标签 title: 自定义使用条款 @@ -373,6 +460,7 @@ zh-CN: media: title: 媒体文件 no_media: 不含媒体文件 + no_status_selected: 因为没有嘟文被选中,所以没有更改 title: 帐户嘟文 with_media: 含有媒体文件 subscriptions: @@ -380,14 +468,40 @@ zh-CN: confirmed: 已确认 expires_in: 失效时间 last_delivery: 最后一次接收数据的时间 + title: WebSub + topic: 话题 + tags: + accounts: 帐户 + hidden: 隐藏 + hide: 从目录隐藏 + name: 话题标签 + title: 话题标签 + unhide: 在目录中显示 + visible: 可见 title: 管理 + warning_presets: + add_new: 添加新条目 + delete: 删除 + edit: 编辑 + edit_preset: 编辑预置警告 + title: 管理预设警告 admin_mailer: + new_pending_account: + body: 新帐户的详细信息如下。您可以批准或拒绝此申请。 + subject: 在 %{instance} 上有新账户 ( %{username}) 需要审核 new_report: body: "%{reporter} 举报了用户 %{target}" body_remote: 来自 %{domain} 的用户举报了用户 %{target} subject: 来自 %{instance} 的用户举报(#%{id}) + appearance: + advanced_web_interface: 高级 web 界面 + advanced_web_interface_hint: 如果你想使用整个屏幕宽度,高级 web 界面允许您配置多个不同的栏目,可以同时看到更多的信息:主页、通知、跨站时间轴、任意数量的列表和话题标签。 + animations_and_accessibility: 动画和访问选项 + confirmation_dialogs: 确认对话框 + sensitive_content: 敏感内容 application_mailer: notification_preferences: 更改电子邮件首选项 + salutation: "%{name}:" settings: 使用此链接更改你的电子邮件首选项:%{link} view: 点此链接查看详情: view_profile: 查看个人资料页 @@ -401,7 +515,9 @@ zh-CN: warning: 一定小心,千万不要把它分享给任何人! your_token: 你的访问令牌 auth: + apply_for_account: 请求邀请 change_password: 密码 + checkbox_agreement_html: 我同意 服务器规则服务条款 confirm_email: 确认电子邮件地址 delete_account: 删除帐户 delete_account_html: 如果你想删除你的帐户,请点击这里继续。你需要确认你的操作。 @@ -413,11 +529,16 @@ zh-CN: migrate_account: 迁移到另一个帐户 migrate_account_html: 如果你希望引导他人关注另一个帐户,请点击这里进行设置。 or_log_in_with: 或通过其他方式登录 + providers: + cas: CAS + saml: SAML register: 注册 + registration_closed: "%{instance} 目前不接收新成员" resend_confirmation: 重新发送确认邮件 reset_password: 重置密码 security: 帐户安全 set_new_password: 设置新密码 + trouble_logging_in: 登录有问题? authorize_follow: already_following: 你已经在关注此用户了 error: 对不起,寻找这个跨站用户时出错 @@ -449,8 +570,17 @@ zh-CN: description_html: 继续操作将会永久地、不可撤销地删除帐户中的所有内容,然后冻结帐户。你的用户名将会被保留,以防有人冒用你的身份。 proceed: 删除帐户 success_msg: 你的帐户已经成功删除 - warning_html: 我们只能保证本实例上的内容将会被彻底删除。对于已经被广泛传播的内容,它们在本实例以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器所存储的数据亦无法删除。 + warning_html: 我们只能保证本服务器上的内容将会被彻底删除。对于已经被广泛传播的内容,它们在本服务器以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器所存储的数据亦无法删除。 warning_title: 关于已传播的内容的警告 + directories: + directory: 用户资料目录 + enabled: 您目前已被列入目录中。 + enabled_but_waiting: 您已选择列入目录,但是您没有达到关注者数量下限 (%{min_followers} 名) 。 + explanation: 根据兴趣发现用户 + explore_mastodon: 探索 %{title} + how_to_enable: 您目前没有选择选择列入到目录中。您可以在下面选择列入。可以在个人简介中加上话题标签,话题标签也会显示在用户资料目录里! + people: + other: "%{count} 人" errors: '403': 你没有访问这个页面的权限。 '404': 无法找到你所要访问的页面。 @@ -463,6 +593,9 @@ zh-CN: content: 抱歉,我们的后台出错了。 title: 这个页面有问题 noscript_html: 使用 Mastodon 网页版应用需要启用 JavaScript。你也可以选择适用于你的平台的 Mastodon 应用。 + existing_username_validator: + not_found: 在本站找不到此用户 + not_found_multiple: 找不到 %{usernames} exports: archive_takeout: date: 日期 @@ -472,9 +605,16 @@ zh-CN: request: 请求你的存档 size: 大小 blocks: 屏蔽的用户 + csv: CSV + domain_blocks: 域名屏蔽 follows: 关注的用户 + lists: 列表 mutes: 隐藏的用户 storage: 媒体文件存储 + featured_tags: + add_new: 添加新条目 + errors: + limit: 你所推荐的话题标签数已达上限 filters: contexts: home: 主页时间轴 @@ -484,22 +624,56 @@ zh-CN: edit: title: 编辑过滤器 errors: + invalid_context: 过滤器场景没有或无效 invalid_irreversible: 此功能只适用于主页时间轴或通知 index: delete: 删除 title: 过滤器 new: title: 添加新的过滤器 + footer: + developers: 开发者 + more: 更多… + resources: 资源 generic: + all: 全部 changes_saved_msg: 更改保存成功! + copy: 复制 + order_by: 排序方式 save_changes: 保存更改 validation_errors: other: 出错啦!检查一下下面 %{count} 处出错的地方吧 + html_validator: + invalid_markup: '包含无效的 HTML 标记: %{error}' + identity_proofs: + active: 有效 + authorize: 是,授权 + authorize_connection_prompt: 授权这一密码学连接? + errors: + failed: 密码关联失败。请在 %{provider} 上重新尝试。 + keybase: + invalid_token: Keybase 令牌是签名的哈希并且必须是 66 个十六进制字符 + verification_failed: Keybase 无法将此令牌识别为 Keybase 用户 %{kb_username} 的签名。请在 Keybase 再试一次。 + wrong_user: 无法以 %{current} 的身份创建 %{proving} 的证明。请以 %{proving} 的身份登录并再次尝试。 + explanation_html: 在这里,你可以和你的其他身份建立密码学关联,例如 Keybase 个人资料。这可以让别人向你发送加密信息,并信任你发给他们的内容。 + i_am_html: 我是 %{service} 上的 %{username} 。 + identity: 身份 + inactive: 失效 + publicize_checkbox: 并发一条这样的嘟文: + publicize_toot: '证明完毕!我是 %{service} 上的 %{username}: %{url}' + status: 验证状态 + view_proof: 查看证明 imports: + modes: + merge: 合并 + merge_long: 保留现有记录并添加新的记录 + overwrite: 覆盖 + overwrite_long: 将当前记录替换为新记录 preface: 你可以在此导入你在其他实例导出的数据,比如你所关注或屏蔽的用户列表。 success: 数据上传成功,正在处理中 types: blocking: 屏蔽列表 + domain_blocking: 域名屏蔽列表 following: 关注列表 muting: 隐藏列表 upload: 上传 @@ -520,7 +694,7 @@ zh-CN: max_uses: other: "%{count} 次" max_uses_prompt: 无限制 - prompt: 生成分享链接,邀请他人在本实例注册 + prompt: 生成分享链接,邀请他人在本服务器注册 table: expires_at: 失效时间 uses: 已使用次数 @@ -546,6 +720,8 @@ zh-CN: mention: "%{name} 在嘟文中提到了你:" new_followers_summary: other: 而且,你不在的时候,有 %{count} 个人关注了你!好棒! + subject: + other: "自从上次访问后,有 %{count} 条新通知 \U0001F418" title: 在你不在的这段时间…… favourite: body: 你的嘟文被 %{name} 收藏了: @@ -569,50 +745,132 @@ zh-CN: body: 你的嘟文被 %{name} 转嘟了: subject: "%{name} 转嘟了你的嘟文" title: 新的转嘟 + number: + human: + decimal_units: + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T pagination: newer: 更新 next: 下一页 older: 更早 prev: 上一页 + polls: + errors: + already_voted: 你已经在这里投过票了 + duplicate_options: 包含重复的项目 + duration_too_long: 持续时间过长 + duration_too_short: 持续时间过短 + expired: 投票已经结束 + over_character_limit: 每条不能超过 %{max} 个字符 + too_few_options: 至少需要两个选项 + too_many_options: 不能超过 %{max} 项 + preferences: + other: 其他 + posting_defaults: 发布默认值 + public_timelines: 公共时间轴 + relationships: + activity: 账户活动 + dormant: 休眠 + last_active: 最近活动 + most_recent: 最近的 + moved: 已迁移 + mutual: 互相关注 + primary: 主要 + relationship: 关系 + remove_selected_domains: 从选定的域名中删除所有关注者 + remove_selected_followers: 移除选中的关注者 + remove_selected_follows: 取消关注所选用户 + status: 帐户状态 remote_follow: acct: 请输入你的“用户名@实例域名” missing_resource: 无法确定你的帐户的跳转 URL no_account_html: 还没有帐号?你可以注册一个 proceed: 确认关注 prompt: 你正准备关注: + reason_html: "为什么需要这个步骤? %{instance} 可能不是您所注册的服务器,所以我们需要先重定向到您所在的服务器。" + remote_interaction: + favourite: + proceed: 确认收藏 + prompt: 您想要收藏此嘟文: + reblog: + proceed: 确认转嘟 + prompt: 您想要转嘟此条: + reply: + proceed: 确认回复 + prompt: 您想要回复此嘟文: remote_unfollow: error: 错误 title: 标题 unfollowed: 已取消关注 + scheduled_statuses: + over_daily_limit: 您已超出每日定时嘟文的上限(%{limit} 条) + over_total_limit: 您已超出定时嘟文的上限(%{limit} 条) + too_soon: 所定的时间必须在未来 sessions: activity: 最后一次活跃的时间 browser: 浏览器 browsers: alipay: 支付宝 + blackberry: 黑莓 + chrome: Chrome + edge: 微软 Edge + electron: Electron + firefox: 火狐 generic: 未知浏览器 + ie: IE 浏览器 micro_messenger: 微信 nokia: Nokia S40 Ovi 浏览器 + opera: 欧朋浏览器 + otter: Otter + phantom_js: PhantomJS qq: QQ浏览器 + safari: Safari uc_browser: UC浏览器 weibo: 新浪微博 current_session: 当前会话 description: "%{platform} 上的 %{browser}" explanation: 你的 Mastodon 帐户目前已在这些浏览器上登录。 ip: IP 地址 + platforms: + adobe_air: Adobe Air + android: Android + blackberry: 黑莓 + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: 未知平台 + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: 注销 revoke_success: 会话注销成功 title: 会话 settings: + account: 账户 + account_settings: 帐户设置 + appearance: 外观 authorized_apps: 已授权的应用 back: 返回 Mastodon delete: 删除帐户 development: 开发 edit_profile: 更改个人资料 export: 导出 + featured_tags: 推荐的话题标签 + identity_proofs: 身份证明 import: 导入 + import_and_export: 导入和导出 migrate: 帐户迁移 notifications: 通知 preferences: 首选项 + profile: 个人资料 + relationships: 正在关注以及关注者 two_factor_authentication: 双重认证 statuses: attached: @@ -633,7 +891,12 @@ zh-CN: ownership: 不能置顶他人的嘟文 private: 不能置顶非公开的嘟文 reblog: 不能置顶转嘟 + poll: + total_votes: + other: "%{count} 票" + vote: 投票 show_more: 显示更多 + sign_in_to_participate: 登录以加入对话 title: "%{name}:“%{quote}”" visibilities: private: 仅关注者 @@ -655,6 +918,7 @@ zh-CN: time: formats: default: "%Y年%-m月%d日 %H:%M" + month: "%Y 年 %B" two_factor_authentication: code_hint: 输入认证器生成的代码以确认操作 description_html: 启用双重认证后,你需要输入手机认证器生成的代码才能登录. @@ -676,6 +940,22 @@ zh-CN: explanation: 你请求了一份 Mastodon 帐户的完整备份。现在你可以下载了! subject: 你的存档已经准备完毕 title: 存档导出 + warning: + explanation: + disable: 虽然您的帐户被冻结,您的帐户数据仍然完整;但是您无法在解锁前执行任何操作。 + silence: 当您的帐户受限时,只有已经关注过你的人才会这台服务器上看到你的嘟文,并且您会被排除在各种公共列表之外。但是,其他人仍然可以手动关注你。 + suspend: 您的帐户已被封禁,所有的嘟文和您上传的媒体文件都已经从该服务器和您的关注者的服务器上删除并且不可恢复。 + review_server_policies: 审阅服务器条款 + subject: + disable: 您的帐户 %{acct} 已被冻结 + none: 对 %{acct} 的警告 + silence: 您的帐户 %{acct} 已经受限 + suspend: 您的帐户 %{acct} 已被封禁。 + title: + disable: 账户已冻结 + none: 警示 + silence: 帐户受限 + suspend: 账户被封禁 welcome: edit_profile_action: 设置个人资料 edit_profile_step: 你可以自定义你的个人资料,包括上传头像、横幅图片、更改昵称等等。如果你想在新的关注者关注你之前对他们进行审核,你也可以选择为你的帐户开启保护。 @@ -683,19 +963,23 @@ zh-CN: final_action: 开始嘟嘟 final_step: '开始嘟嘟吧!即便你现在没有关注者,其他人仍然能在本站时间轴或者话题标签等地方看到你的公开嘟文。试着用 #introductions 这个话题标签介绍一下自己吧。' full_handle: 你的完整用户地址 - full_handle_hint: 你需要把这个告诉你的朋友们,这样他们就能从另一个实例向你发送信息或者关注你。 + full_handle_hint: 你需要把这个告诉你的朋友们,这样他们就能从另一台服务器向你发送信息或者关注你。 review_preferences_action: 更改首选项 review_preferences_step: 记得调整你的偏好设置,比如你想接收什么类型的邮件,或者你想把你的嘟文可见范围默认设置为什么级别。如果你没有晕动病的话,考虑一下启用“自动播放 GIF 动画”这个选项吧。 subject: 欢迎来到 Mastodon tip_federated_timeline: 跨站公共时间轴可以让你一窥更广阔的 Mastodon 网络。不过,由于它只显示你的邻居们所订阅的内容,所以并不是全部。 - tip_following: 默认情况下,你会自动关注你所在实例的管理员。想结交更多有趣的人的话,记得多逛逛本站时间轴和跨站公共时间轴哦。 + tip_following: 默认情况下,你会自动关注你所在服务器的管理员。想结交更多有趣的人的话,记得多逛逛本站时间轴和跨站公共时间轴哦。 tip_local_timeline: 本站时间轴可以让你一窥 %{instance} 上的用户。他们就是离你最近的邻居! tip_mobile_webapp: 如果你的移动设备浏览器允许你将 Mastodon 添加到主屏幕,你就能够接收推送消息。它就像本地应用一样好使! tips: 小贴士 title: "%{name},欢迎你的加入!" users: + follow_limit_reached: 您不能关注超过 %{limit} 个人 invalid_email: 输入的电子邮件地址无效 invalid_otp_token: 输入的双重认证代码无效 otp_lost_help_html: 如果你不慎丢失了所有的代码,请联系 %{email} 寻求帮助 seamless_external_login: 因为你是通过外部服务登录的,所以密码和电子邮件地址设置都不可用。 signed_in_as: 当前登录的帐户: + verification: + explanation_html: 您可以 验证自己是个人资料元数据中的某个链接的所有者。 为此,被链接网站必须包含一个到您的 Mastodon 主页的链接。链接中 必须 包括 rel="me" 属性。链接的文本内容可以随意填写。例如: + verification: 验证 -- cgit From e9a11dca1977308bd2b367362cde512fe093b3f7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 19 Jun 2019 18:25:06 +0200 Subject: Fix inconsistent interpolation in sk.yml (#11124) --- config/locales/sk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 70574dc86..3e1255276 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -301,7 +301,7 @@ sk: affected_accounts: few: "%{count} účtov v databázi ovplyvnených" many: "%{count} účtov v databázi ovplyvnených" - one: "%{count} účet v databázi ovplyvnený" + one: Jeden účet v databázi ovplyvnený other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény -- cgit From 7696f77245c2302787d239da50248385b3292a5e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 20 Jun 2019 02:52:34 +0200 Subject: Add moderation API (#9387) Fix #8580 Fix #7143 --- app/controllers/admin/accounts_controller.rb | 1 + .../api/v1/admin/account_actions_controller.rb | 32 +++++ .../api/v1/admin/accounts_controller.rb | 128 ++++++++++++++++++ app/controllers/api/v1/admin/reports_controller.rb | 108 +++++++++++++++ app/models/account.rb | 2 + app/models/account_filter.rb | 2 + app/models/concerns/user_roles.rb | 14 ++ app/models/report.rb | 3 + app/models/report_filter.rb | 2 + app/models/user.rb | 1 + app/serializers/rest/admin/account_serializer.rb | 77 +++++++++++ app/serializers/rest/admin/report_serializer.rb | 16 +++ config/initializers/doorkeeper.rb | 8 +- config/locales/doorkeeper.en.yml | 6 + config/routes.rb | 23 ++++ .../v1/admin/account_actions_controller_spec.rb | 57 ++++++++ .../api/v1/admin/accounts_controller_spec.rb | 147 +++++++++++++++++++++ .../api/v1/admin/reports_controller_spec.rb | 109 +++++++++++++++ 18 files changed, 735 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/admin/account_actions_controller.rb create mode 100644 app/controllers/api/v1/admin/accounts_controller.rb create mode 100644 app/controllers/api/v1/admin/reports_controller.rb create mode 100644 app/serializers/rest/admin/account_serializer.rb create mode 100644 app/serializers/rest/admin/report_serializer.rb create mode 100644 spec/controllers/api/v1/admin/account_actions_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/accounts_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/reports_controller_spec.rb (limited to 'config') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index b0d45ce47..0c7760d77 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -127,6 +127,7 @@ module Admin :by_domain, :active, :pending, + :disabled, :silenced, :suspended, :username, diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb new file mode 100644 index 000000000..29c9b7107 --- /dev/null +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountActionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } + before_action :require_staff! + before_action :set_account + + def create + account_action = Admin::AccountAction.new(resource_params) + account_action.target_account = @account + account_action.current_account = current_account + account_action.save! + + render_empty + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def resource_params + params.permit( + :type, + :report_id, + :warning_preset_id, + :text, + :send_email_notification + ) + end +end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb new file mode 100644 index 000000000..c306180ca --- /dev/null +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] + before_action :require_staff! + before_action :set_accounts, only: :index + before_action :set_account, except: :index + before_action :require_local_account!, only: [:enable, :approve, :reject] + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + local + remote + by_domain + active + pending + disabled + silenced + suspended + username + display_name + email + ip + staff + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :account, :index? + render json: @accounts, each_serializer: REST::Admin::AccountSerializer + end + + def show + authorize @account, :show? + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + log_action :enable, @account.user + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def approve + authorize @account.user, :approve? + @account.user.approve! + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsilence + authorize @account, :unsilence? + @account.unsilence! + log_action :unsilence, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsuspend + authorize @account, :unsuspend? + @account.unsuspend! + log_action :unsuspend, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + private + + def set_accounts + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_account + @account = Account.find(params[:id]) + end + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def require_local_account! + forbidden unless @account.local? && @account.user.present? + end +end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb new file mode 100644 index 000000000..1d48d3160 --- /dev/null +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Api::V1::Admin::ReportsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] + before_action :require_staff! + before_action :set_reports, only: :index + before_action :set_report, except: :index + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + resolved + account_id + target_account_id + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :report, :index? + render json: @reports, each_serializer: REST::Admin::ReportSerializer + end + + def show + authorize @report, :show? + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def assign_to_self + authorize @report, :update? + @report.update!(assigned_account_id: current_account.id) + log_action :assigned_to_self, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def unassign + authorize @report, :update? + @report.update!(assigned_account_id: nil) + log_action :unassigned, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def reopen + authorize @report, :update? + @report.unresolve! + log_action :reopen, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def resolve + authorize @report, :update? + @report.resolve!(current_account) + log_action :resolve, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + private + + def set_reports + @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_report + @report = Report.find(params[:id]) + end + + def filtered_reports + ReportFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? + end + + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id + end + + def records_continue? + @reports.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index c977f887c..9276aa927 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -106,6 +106,8 @@ class Account < ApplicationRecord :confirmed?, :approved?, :pending?, + :disabled?, + :role, :admin?, :moderator?, :staff?, diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index d2503100c..c3b1fe08d 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -37,6 +37,8 @@ class AccountFilter Account.without_suspended when 'pending' accounts_with_users.merge User.pending + when 'disabled' + accounts_with_users.merge User.disabled when 'silenced' Account.silenced when 'suspended' diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb index 58dffdc46..a42b4a172 100644 --- a/app/models/concerns/user_roles.rb +++ b/app/models/concerns/user_roles.rb @@ -13,6 +13,20 @@ module UserRoles admin? || moderator? end + def role=(value) + case value + when 'admin' + self.admin = true + self.moderator = false + when 'moderator' + self.admin = false + self.moderator = true + else + self.admin = false + self.moderator = false + end + end + def role if admin? 'admin' diff --git a/app/models/report.rb b/app/models/report.rb index 86c303798..5192ceef7 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -17,6 +17,8 @@ # class Report < ApplicationRecord + include Paginable + belongs_to :account belongs_to :target_account, class_name: 'Account' belongs_to :action_taken_by_account, class_name: 'Account', optional: true @@ -26,6 +28,7 @@ class Report < ApplicationRecord scope :unresolved, -> { where(action_taken: false) } scope :resolved, -> { where(action_taken: true) } + scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].each_with_object({}) { |k, h| h[k] = { user: [:invite_request, :invite] } }) } validates :comment, length: { maximum: 1000 } diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index 56ab28df7..a392d60c3 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -9,9 +9,11 @@ class ReportFilter def results scope = Report.unresolved + params.each do |key, value| scope = scope.merge scope_for(key, value) end + scope end diff --git a/app/models/user.rb b/app/models/user.rb index 4abf124fc..50873dd01 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -87,6 +87,7 @@ class User < ApplicationRecord scope :approved, -> { where(approved: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } + scope :disabled, -> { where(disabled: true) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb new file mode 100644 index 000000000..f579d3302 --- /dev/null +++ b/app/serializers/rest/admin/account_serializer.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class REST::Admin::AccountSerializer < ActiveModel::Serializer + attributes :id, :username, :domain, :created_at, + :email, :ip, :role, :confirmed, :suspended, + :silenced, :disabled, :approved, :locale, + :invite_request + + attribute :created_by_application_id, if: :created_by_application? + attribute :invited_by_account_id, if: :invited? + + has_one :account, serializer: REST::AccountSerializer + + def id + object.id.to_s + end + + def email + object.user_email + end + + def ip + object.user_current_sign_in_ip.to_s.presence + end + + def role + object.user_role + end + + def suspended + object.suspended? + end + + def silenced + object.silenced? + end + + def confirmed + object.user_confirmed? + end + + def disabled + object.user_disabled? + end + + def approved + object.user_approved? + end + + def account + object + end + + def locale + object.user_locale + end + + def created_by_application_id + object.user&.created_by_application_id&.to_s&.presence + end + + def invite_request + object.user&.invite_request&.text + end + + def invited_by_account_id + object.user&.invite&.user&.account_id&.to_s&.presence + end + + def invited? + object.user&.invited? + end + + def created_by_application? + object.user&.created_by_application_id&.present? + end +end diff --git a/app/serializers/rest/admin/report_serializer.rb b/app/serializers/rest/admin/report_serializer.rb new file mode 100644 index 000000000..7a77132c0 --- /dev/null +++ b/app/serializers/rest/admin/report_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class REST::Admin::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken, :comment, :created_at, :updated_at + + has_one :account, serializer: REST::Admin::AccountSerializer + has_one :target_account, serializer: REST::Admin::AccountSerializer + has_one :assigned_account, serializer: REST::Admin::AccountSerializer + has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer + + has_many :statuses, serializer: REST::StatusSerializer + + def id + object.id.to_s + end +end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 367eead6a..914b3c001 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -80,7 +80,13 @@ Doorkeeper.configure do :'read:search', :'read:statuses', :follow, - :push + :push, + :'admin:read', + :'admin:read:accounts', + :'admin:read:reports', + :'admin:write', + :'admin:write:accounts', + :'admin:write:reports' # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index f1fe03716..d9b7c2c8e 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -114,6 +114,12 @@ en: application: title: OAuth authorization required scopes: + admin:read: read all data on the server + admin:read:accounts: read sensitive information of all accounts + admin:read:reports: read sensitive information of all reports and reported accounts + admin:write: modify all data on the server + admin:write:accounts: perform moderation actions on accounts + admin:write:reports: perform moderation actions on reports follow: modify account relationships push: receive your push notifications read: read all your account's data diff --git a/config/routes.rb b/config/routes.rb index 145079c69..764db8db2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -398,6 +398,29 @@ Rails.application.routes.draw do namespace :push do resource :subscription, only: [:create, :show, :update, :destroy] end + + namespace :admin do + resources :accounts, only: [:index, :show] do + member do + post :enable + post :unsilence + post :unsuspend + post :approve + post :reject + end + + resource :action, only: [:create], controller: 'account_actions' + end + + resources :reports, only: [:index, :show] do + member do + post :assign_to_self + post :unassign + post :reopen + post :resolve + end + end + end end namespace :v2 do diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb new file mode 100644 index 000000000..a5a8f4bb0 --- /dev/null +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'POST #create' do + before do + post :create, params: { account_id: account.id, type: 'disable' } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'performs action against account' do + expect(account.reload.user_disabled?).to be true + end + + it 'logs action' do + log_item = Admin::ActionLog.last + + expect(log_item).to_not be_nil + expect(log_item.action).to eq :disable + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end +end diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f3f9946ba --- /dev/null +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -0,0 +1,147 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #approve' do + before do + account.user.update(approved: false) + post :approve, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'approves user' do + expect(account.reload.user_approved?).to be true + end + end + + describe 'POST #reject' do + before do + account.user.update(approved: false) + post :reject, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes user' do + expect(User.where(id: account.user.id).count).to eq 0 + end + end + + describe 'POST #enable' do + before do + account.user.update(disabled: true) + post :enable, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'enables user' do + expect(account.reload.user_disabled?).to be false + end + end + + describe 'POST #unsuspend' do + before do + account.touch(:suspended_at) + post :unsuspend, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsuspends account' do + expect(account.reload.suspended?).to be false + end + end + + describe 'POST #unsilence' do + before do + account.touch(:silenced_at) + post :unsilence, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsilences account' do + expect(account.reload.silenced?).to be false + end + end +end diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb new file mode 100644 index 000000000..4ed3c5dc4 --- /dev/null +++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::ReportsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:report) { Fabricate(:report) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #resolve' do + before do + post :resolve, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #reopen' do + before do + post :reopen, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #assign_to_self' do + before do + post :assign_to_self, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #unassign' do + before do + post :unassign, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end -- cgit From 8f23726918fd8ded18ce9e55e6199df87abcc7cf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 20 Jun 2019 10:52:36 +0200 Subject: Fix converted media being saved with original extension and mime type (#11130) --- app/models/concerns/attachmentable.rb | 2 +- app/models/media_attachment.rb | 8 +++++--- config/application.rb | 1 + lib/paperclip/type_corrector.rb | 19 +++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 lib/paperclip/type_corrector.rb (limited to 'config') diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index de4cf8775..f4e37f1e6 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mime/types' +require 'mime/types/columnar' module Attachmentable extend ActiveSupport::Concern diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 5ca218411..ae94ce68a 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -70,12 +70,14 @@ class MediaAttachment < ApplicationRecord AUDIO_STYLES = { original: { format: 'ogg', + content_type: 'audio/ogg', convert_options: {}, }, }.freeze VIDEO_FORMAT = { format: 'mp4', + content_type: 'video/mp4', convert_options: { output: { 'loglevel' => 'fatal', @@ -189,11 +191,11 @@ class MediaAttachment < ApplicationRecord if f.file_content_type == 'image/gif' [:gif_transcoder, :blurhash_transcoder] elsif VIDEO_MIME_TYPES.include?(f.file_content_type) - [:video_transcoder, :blurhash_transcoder] + [:video_transcoder, :blurhash_transcoder, :type_corrector] elsif AUDIO_MIME_TYPES.include?(f.file_content_type) - [:transcoder] + [:transcoder, :type_corrector] else - [:lazy_thumbnail, :blurhash_transcoder] + [:lazy_thumbnail, :blurhash_transcoder, :type_corrector] end end end diff --git a/config/application.rb b/config/application.rb index 74006f5fa..4534ede49 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,6 +10,7 @@ require_relative '../app/lib/exceptions' require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/video_transcoder' +require_relative '../lib/paperclip/type_corrector' require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/version' require_relative '../lib/devise/ldap_authenticatable' diff --git a/lib/paperclip/type_corrector.rb b/lib/paperclip/type_corrector.rb new file mode 100644 index 000000000..0b0c10a56 --- /dev/null +++ b/lib/paperclip/type_corrector.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'mime/types/columnar' + +module Paperclip + class TypeCorrector < Paperclip::Processor + def make + target_extension = options[:format] + extension = File.extname(attachment.instance.file_file_name) + + return @file unless options[:style] == :original && target_extension && extension != target_extension + + attachment.instance.file_content_type = options[:content_type] || attachment.instance.file_content_type + attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.' + target_extension + + @file + end + end +end -- cgit From 6e7e714bd91da719076575d55c2aefb7c8fc8213 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 22 Jun 2019 00:57:48 +0200 Subject: New Crowdin translations (#11116) * New translations devise.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations devise.en.yml (Chinese Traditional) [ci skip] * New translations activerecord.en.yml (Thai) [ci skip] * New translations activerecord.en.yml (Slovak) [ci skip] * New translations activerecord.en.yml (French) [ci skip] * New translations activerecord.en.yml (Hungarian) [ci skip] * New translations activerecord.en.yml (Hebrew) [ci skip] * New translations activerecord.en.yml (Greek) [ci skip] * New translations activerecord.en.yml (German) [ci skip] * New translations activerecord.en.yml (Georgian) [ci skip] * New translations activerecord.en.yml (Galician) [ci skip] * New translations activerecord.en.yml (Esperanto) [ci skip] * New translations activerecord.en.yml (Danish) [ci skip] * New translations activerecord.en.yml (Czech) [ci skip] * New translations activerecord.en.yml (Corsican) [ci skip] * New translations activerecord.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations activerecord.en.yml (Indonesian) [ci skip] * New translations activerecord.en.yml (Japanese) [ci skip] * New translations activerecord.en.yml (Swedish) [ci skip] * New translations activerecord.en.yml (Portuguese, Brazilian) [ci skip] * New translations activerecord.en.yml (Spanish) [ci skip] * New translations activerecord.en.yml (Slovenian) [ci skip] * New translations devise.en.yml (Ido) [ci skip] * New translations activerecord.en.yml (Serbian (Latin)) [ci skip] * New translations activerecord.en.yml (Serbian (Cyrillic)) [ci skip] * New translations activerecord.en.yml (Russian) [ci skip] * New translations activerecord.en.yml (Portuguese) [ci skip] * New translations activerecord.en.yml (Kazakh) [ci skip] * New translations activerecord.en.yml (Polish) [ci skip] * New translations activerecord.en.yml (Persian) [ci skip] * New translations activerecord.en.yml (Occitan) [ci skip] * New translations activerecord.en.yml (Norwegian) [ci skip] * New translations activerecord.en.yml (Korean) [ci skip] * New translations devise.en.yml (Hungarian) [ci skip] * New translations devise.en.yml (Indonesian) [ci skip] * New translations doorkeeper.en.yml (Ido) [ci skip] * New translations doorkeeper.en.yml (Korean) [ci skip] * New translations doorkeeper.en.yml (Kazakh) [ci skip] * New translations doorkeeper.en.yml (Japanese) [ci skip] * New translations doorkeeper.en.yml (Indonesian) [ci skip] * New translations doorkeeper.en.yml (Hungarian) [ci skip] * New translations doorkeeper.en.yml (Norwegian) [ci skip] * New translations doorkeeper.en.yml (Hebrew) [ci skip] * New translations doorkeeper.en.yml (Greek) [ci skip] * New translations doorkeeper.en.yml (German) [ci skip] * New translations doorkeeper.en.yml (Georgian) [ci skip] * New translations doorkeeper.en.yml (Galician) [ci skip] * New translations doorkeeper.en.yml (French) [ci skip] * New translations doorkeeper.en.yml (Finnish) [ci skip] * New translations doorkeeper.en.yml (Occitan) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Ukrainian) [ci skip] * New translations doorkeeper.en.yml (Turkish) [ci skip] * New translations doorkeeper.en.yml (Thai) [ci skip] * New translations doorkeeper.en.yml (Swedish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Persian) [ci skip] * New translations doorkeeper.en.yml (Serbian (Latin)) [ci skip] * New translations doorkeeper.en.yml (Serbian (Cyrillic)) [ci skip] * New translations doorkeeper.en.yml (Russian) [ci skip] * New translations doorkeeper.en.yml (Portuguese, Brazilian) [ci skip] * New translations doorkeeper.en.yml (Portuguese) [ci skip] * New translations doorkeeper.en.yml (Polish) [ci skip] * New translations doorkeeper.en.yml (Esperanto) [ci skip] * New translations doorkeeper.en.yml (Danish) [ci skip] * New translations devise.en.yml (Persian) [ci skip] * New translations devise.en.yml (Serbian (Latin)) [ci skip] * New translations devise.en.yml (Serbian (Cyrillic)) [ci skip] * New translations devise.en.yml (Russian) [ci skip] * New translations devise.en.yml (Portuguese, Brazilian) [ci skip] * New translations devise.en.yml (Portuguese) [ci skip] * New translations devise.en.yml (Polish) [ci skip] * New translations devise.en.yml (Occitan) [ci skip] * New translations devise.en.yml (Slovenian) [ci skip] * New translations devise.en.yml (Norwegian) [ci skip] * New translations activerecord.en.yml (Chinese Simplified) [ci skip] * New translations devise.en.yml (Korean) [ci skip] * New translations devise.en.yml (Kazakh) [ci skip] * New translations devise.en.yml (Japanese) [ci skip] * New translations devise.en.yml (Slovak) [ci skip] * New translations devise.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Czech) [ci skip] * New translations doorkeeper.en.yml (Croatian) [ci skip] * New translations doorkeeper.en.yml (Corsican) [ci skip] * New translations doorkeeper.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations doorkeeper.en.yml (Chinese Traditional) [ci skip] * New translations doorkeeper.en.yml (Chinese Simplified) [ci skip] * New translations doorkeeper.en.yml (Catalan) [ci skip] * New translations doorkeeper.en.yml (Bulgarian) [ci skip] * New translations doorkeeper.en.yml (Basque) [ci skip] * New translations devise.en.yml (Swedish) [ci skip] * New translations doorkeeper.en.yml (Asturian) [ci skip] * New translations devise.en.yml (Welsh) [ci skip] * New translations devise.en.yml (Ukrainian) [ci skip] * New translations devise.en.yml (Turkish) [ci skip] * New translations devise.en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations activerecord.en.yml (Catalan) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.yml (Bulgarian) [ci skip] * New translations en.yml (Bengali) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.json (Ukrainian) [ci skip] * New translations en.json (Turkish) [ci skip] * New translations en.json (Telugu) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Tamil) [ci skip] * New translations en.json (Swedish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations en.json (Serbian (Latin)) [ci skip] * New translations en.json (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.json (Romanian) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (Indonesian) [ci skip] * New translations en.yml (Ido) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Hebrew) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Croatian) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Chinese Traditional, Hong Kong) [ci skip] * New translations en.json (Finnish) [ci skip] * New translations en.json (Danish) [ci skip] * New translations en.json (Croatian) [ci skip] * New translations en.json (Chinese Traditional) [ci skip] * New translations en.json (Catalan) [ci skip] * New translations en.json (Bulgarian) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations devise.en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Czech) [ci skip] * New translations en.json (Georgian) [ci skip] * New translations en.json (Portuguese) [ci skip] * New translations en.json (Occitan) [ci skip] * New translations en.json (Norwegian) [ci skip] * New translations en.json (Malay) [ci skip] * New translations en.json (Lithuanian) [ci skip] * New translations en.json (Latvian) [ci skip] * New translations en.json (Kazakh) [ci skip] * New translations en.json (Indonesian) [ci skip] * New translations en.json (Ido) [ci skip] * New translations en.json (Hebrew) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations simple_form.en.yml (Occitan) [ci skip] * New translations simple_form.en.yml (Norwegian) [ci skip] * New translations simple_form.en.yml (Korean) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Polish) [ci skip] * New translations simple_form.en.yml (Indonesian) [ci skip] * New translations simple_form.en.yml (Ido) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Hebrew) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations simple_form.en.yml (German) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Portuguese) [ci skip] * New translations simple_form.en.yml (Galician) [ci skip] * New translations activerecord.en.yml (Basque) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Ukrainian) [ci skip] * New translations simple_form.en.yml (Turkish) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Swedish) [ci skip] * New translations simple_form.en.yml (Portuguese, Brazilian) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Slovenian) [ci skip] * New translations simple_form.en.yml (Slovak) [ci skip] * New translations simple_form.en.yml (Serbian (Latin)) [ci skip] * New translations simple_form.en.yml (Serbian (Cyrillic)) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Romanian) [ci skip] * New translations simple_form.en.yml (Georgian) [ci skip] * New translations simple_form.en.yml (French) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Romanian) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Norwegian) [ci skip] * New translations en.yml (Malay) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Latvian) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Tamil) [ci skip] * New translations simple_form.en.yml (Finnish) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Esperanto) [ci skip] * New translations simple_form.en.yml (Danish) [ci skip] * New translations simple_form.en.yml (Croatian) [ci skip] * New translations simple_form.en.yml (Corsican) [ci skip] * New translations simple_form.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations simple_form.en.yml (Chinese Traditional) [ci skip] * New translations simple_form.en.yml (Catalan) [ci skip] * New translations en.yml (Telugu) [ci skip] * New translations simple_form.en.yml (Bulgarian) [ci skip] * New translations simple_form.en.yml (Basque) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Finnish) [ci skip] * New translations en.json (Finnish) [ci skip] * New translations en.json (Finnish) [ci skip] * New translations activerecord.en.yml (Finnish) [ci skip] * New translations en.json (Japanese) [ci skip] * New translations doorkeeper.en.yml (Japanese) [ci skip] * New translations doorkeeper.en.yml (Greek) [ci skip] * New translations doorkeeper.en.yml (Italian) [ci skip] * New translations doorkeeper.en.yml (Italian) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Polish) [ci skip] * New translations doorkeeper.en.yml (Czech) [ci skip] * New translations doorkeeper.en.yml (Corsican) [ci skip] * New translations doorkeeper.en.yml (Corsican) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.json (Italian) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations doorkeeper.en.yml (Slovenian) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations doorkeeper.en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.json (Dutch) [ci skip] * New translations en.json (German) [ci skip] * i18n-tasks normalize * yarn manage:translations --- app/javascript/mastodon/locales/de.json | 2 +- app/javascript/mastodon/locales/fi.json | 58 ++++---- app/javascript/mastodon/locales/it.json | 56 ++++---- app/javascript/mastodon/locales/ja.json | 2 +- app/javascript/mastodon/locales/nl.json | 12 +- app/javascript/mastodon/locales/sk.json | 2 +- app/javascript/mastodon/locales/zh-CN.json | 2 +- config/locales/activerecord.fi.yml | 12 ++ config/locales/activerecord.it.yml | 6 +- config/locales/devise.it.yml | 3 + config/locales/doorkeeper.co.yml | 6 + config/locales/doorkeeper.cs.yml | 6 + config/locales/doorkeeper.el.yml | 6 + config/locales/doorkeeper.it.yml | 8 ++ config/locales/doorkeeper.ja.yml | 6 + config/locales/doorkeeper.pl.yml | 6 + config/locales/doorkeeper.sl.yml | 6 + config/locales/doorkeeper.zh-CN.yml | 6 + config/locales/it.yml | 216 ++++++++++++++++++++++++++++- config/locales/pl.yml | 2 +- config/locales/simple_form.it.yml | 15 ++ config/locales/simple_form.ja.yml | 1 + config/locales/sk.yml | 4 + 23 files changed, 370 insertions(+), 73 deletions(-) (limited to 'config') diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 5c63af3b2..ac8bc9b9f 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -369,7 +369,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "upload_area.title": "Zum Hochladen hereinziehen", - "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Mediendatei hinzufügen ({formats})", "upload_error.limit": "Dateiupload-Limit erreicht.", "upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 4eca05ca5..342a15bfb 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -71,20 +71,20 @@ "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.", "compose_form.lock_disclaimer.lock": "lukittu", "compose_form.placeholder": "Mitä mietit?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Lisää valinta", + "compose_form.poll.duration": "Äänestyksen kesto", + "compose_form.poll.option_placeholder": "Valinta numero", + "compose_form.poll.remove_option": "Poista tämä valinta", "compose_form.publish": "Tuuttaa", - "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.publish_loud": "Julkista!", + "compose_form.sensitive.hide": "Valitse tämä arkaluontoisena", "compose_form.sensitive.marked": "Media on merkitty arkaluontoiseksi", "compose_form.sensitive.unmarked": "Mediaa ei ole merkitty arkaluontoiseksi", "compose_form.spoiler.marked": "Teksti on piilotettu varoituksen taakse", "compose_form.spoiler.unmarked": "Teksti ei ole piilotettu", "compose_form.spoiler_placeholder": "Sisältövaroitus", "confirmation_modal.cancel": "Peruuta", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Estä ja raportoi", "confirmations.block.confirm": "Estä", "confirmations.block.message": "Haluatko varmasti estää käyttäjän {name}?", "confirmations.delete.confirm": "Poista", @@ -118,7 +118,7 @@ "emoji_button.symbols": "Symbolit", "emoji_button.travel": "Matkailu", "empty_column.account_timeline": "Ei ole 'toots' täällä!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profiilia ei löydy", "empty_column.blocks": "Et ole vielä estänyt yhtään käyttäjää.", "empty_column.community": "Paikallinen aikajana on tyhjä. Homma lähtee käyntiin, kun kirjoitat jotain julkista!", "empty_column.direct": "Sinulla ei ole vielä yhtään viestiä yksittäiselle käyttäjälle. Kun lähetät tai vastaanotat sellaisen, se näkyy täällä.", @@ -138,7 +138,7 @@ "follow_request.reject": "Hylkää", "getting_started.developers": "Kehittäjille", "getting_started.directory": "Profiili hakemisto", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentaatio", "getting_started.heading": "Aloitus", "getting_started.invite": "Kutsu ihmisiä", "getting_started.open_source_notice": "Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHubissa: {github}.", @@ -147,8 +147,8 @@ "hashtag.column_header.tag_mode.all": "ja {additional}", "hashtag.column_header.tag_mode.any": "tai {additional}", "hashtag.column_header.tag_mode.none": "ilman {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Ehdostuta ei löydetty", + "hashtag.column_settings.select.placeholder": "Laita häshtägejä…", "hashtag.column_settings.tag_mode.all": "Kaikki", "hashtag.column_settings.tag_mode.any": "Kaikki", "hashtag.column_settings.tag_mode.none": "Ei mikään", @@ -156,25 +156,25 @@ "home.column_settings.basic": "Perusasetukset", "home.column_settings.show_reblogs": "Näytä buustaukset", "home.column_settings.show_replies": "Näytä vastaukset", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "Päivä päiviä", + "intervals.full.hours": "Tunti tunteja", + "intervals.full.minutes": "Minuuti minuuteja", "introduction.federation.action": "Seuraava", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", + "introduction.federation.federated.headline": "Federaatioitettu", + "introduction.federation.federated.text": "Julkisia viestejä muiden serverien that is not a word aikoo tulla federoituun aikajanaan.", + "introduction.federation.home.headline": "Koti", + "introduction.federation.home.text": "Viestit muilta pelaajilta jota seuraat aikovat tulla koti sivuusi. Voit seurata ketä vain missä vain serverillä!", + "introduction.federation.local.headline": "Paikallinen", + "introduction.federation.local.text": "Julkiset viestit muilta pelaajilta samalla serverillä tulevat sinun paikalliseen aikajanaan.", + "introduction.interactions.action": "Suorita harjoitus!", + "introduction.interactions.favourite.headline": "Lempi", + "introduction.interactions.favourite.text": "Toot is not a word.", + "introduction.interactions.reblog.headline": "Nopeutus", + "introduction.interactions.reblog.text": "Toot is not a word", + "introduction.interactions.reply.headline": "Vastaa", + "introduction.interactions.reply.text": "TOOT IS NOT A WORD", + "introduction.welcome.action": "Mennään!", + "introduction.welcome.headline": "Ensimmäiset askeleet", "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "keyboard_shortcuts.back": "liiku taaksepäin", "keyboard_shortcuts.blocked": "avaa lista estetyistä käyttäjistä", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index d5e88c4f3..f7e2e4353 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -40,7 +40,7 @@ "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", "bundle_column_error.body": "E' avvenuto un errore durante il caricamento di questo componente.", "bundle_column_error.retry": "Riprova", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Errore di rete", "bundle_modal_error.close": "Chiudi", "bundle_modal_error.message": "C'è stato un errore mentre questo componente veniva caricato.", "bundle_modal_error.retry": "Riprova", @@ -71,20 +71,20 @@ "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.", "compose_form.lock_disclaimer.lock": "bloccato", "compose_form.placeholder": "A cosa stai pensando?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Aggiungi una scelta", + "compose_form.poll.duration": "Durata del sondaggio", + "compose_form.poll.option_placeholder": "Scelta {number}", + "compose_form.poll.remove_option": "Rimuovi questa scelta", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Segna media come sensibile", "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile", "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile", "compose_form.spoiler.marked": "Il testo è nascosto dall'avviso", "compose_form.spoiler.unmarked": "Il testo non è nascosto", "compose_form.spoiler_placeholder": "Content warning", "confirmation_modal.cancel": "Annulla", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blocca & Segnala", "confirmations.block.confirm": "Blocca", "confirmations.block.message": "Sei sicuro di voler bloccare {name}?", "confirmations.delete.confirm": "Cancella", @@ -118,7 +118,7 @@ "emoji_button.symbols": "Simboli", "emoji_button.travel": "Viaggi e luoghi", "empty_column.account_timeline": "Non ci sono toot qui!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profilo non disponibile", "empty_column.blocks": "Non hai ancora bloccato nessun utente.", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.", @@ -156,15 +156,15 @@ "home.column_settings.basic": "Semplice", "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}", + "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}", "introduction.federation.action": "Avanti", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federato", "introduction.federation.federated.text": "I post pubblici provenienti da altri server del fediverse saranno mostrati nella timeline federata.", "introduction.federation.home.headline": "Home", "introduction.federation.home.text": "I post scritti da persone che segui saranno mostrati nella timeline home. Puoi seguire chiunque su qualunque server!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "Locale", "introduction.federation.local.text": "I post pubblici scritti da persone sul tuo stesso server saranno mostrati nella timeline locale.", "introduction.interactions.action": "Finisci il tutorial!", "introduction.interactions.favourite.headline": "Apprezza", @@ -204,17 +204,17 @@ "keyboard_shortcuts.search": "per spostare il focus sulla ricerca", "keyboard_shortcuts.start": "per aprire la colonna \"Come iniziare\"", "keyboard_shortcuts.toggle_hidden": "per mostrare/nascondere il testo dei CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere media", "keyboard_shortcuts.toot": "per iniziare a scrivere un toot completamente nuovo", "keyboard_shortcuts.unfocus": "per uscire dall'area di composizione o dalla ricerca", "keyboard_shortcuts.up": "per spostarsi in alto nella lista", "lightbox.close": "Chiudi", "lightbox.next": "Successivo", "lightbox.previous": "Precedente", - "lightbox.view_context": "View context", + "lightbox.view_context": "Mostra contesto", "lists.account.add": "Aggiungi alla lista", "lists.account.remove": "Togli dalla lista", - "lists.delete": "Delete list", + "lists.delete": "Elimina lista", "lists.edit": "Modifica lista", "lists.edit.submit": "Cambia titolo", "lists.new.create": "Aggiungi lista", @@ -243,16 +243,16 @@ "navigation_bar.lists": "Liste", "navigation_bar.logout": "Esci", "navigation_bar.mutes": "Utenti silenziati", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Personale", "navigation_bar.pins": "Toot fissati in cima", "navigation_bar.preferences": "Impostazioni", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Directory dei profili", "navigation_bar.public_timeline": "Timeline federata", "navigation_bar.security": "Sicurezza", "notification.favourite": "{name} ha apprezzato il tuo post", "notification.follow": "{name} ha iniziato a seguirti", "notification.mention": "{name} ti ha menzionato", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Un sondaggio in cui hai votato è terminato", "notification.reblog": "{name} ha condiviso il tuo post", "notifications.clear": "Cancella notifiche", "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?", @@ -263,7 +263,7 @@ "notifications.column_settings.filter_bar.show": "Mostra", "notifications.column_settings.follow": "Nuovi seguaci:", "notifications.column_settings.mention": "Menzioni:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Risultati del sondaggio:", "notifications.column_settings.push": "Notifiche push", "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", @@ -273,14 +273,14 @@ "notifications.filter.favourites": "Apprezzati", "notifications.filter.follows": "Seguaci", "notifications.filter.mentions": "Menzioni", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Risultati del sondaggio", "notifications.group": "{count} notifiche", "poll.closed": "Chiuso", "poll.refresh": "Aggiorna", "poll.total_votes": "{count, plural, one {# voto} other {# voti}}", "poll.vote": "Vota", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll_button.add_poll": "Aggiungi un sondaggio", + "poll_button.remove_poll": "Rimuovi sondaggio", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", "privacy.direct.short": "Diretto", @@ -292,8 +292,8 @@ "privacy.unlisted.short": "Non elencato", "regeneration_indicator.label": "Caricamento in corso…", "regeneration_indicator.sublabel": "Stiamo preparando il tuo home feed!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.days": "{number}g", + "relative_time.hours": "{number}o", "relative_time.just_now": "ora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", @@ -307,8 +307,8 @@ "search.placeholder": "Cerca", "search_popout.search_format": "Formato di ricerca avanzato", "search_popout.tips.full_text": "Testo semplice per trovare gli status che hai scritto, segnato come apprezzati, condiviso o in cui sei stato citato, e inoltre i nomi utente, nomi visualizzati e hashtag che lo contengono.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", + "search_popout.tips.hashtag": "etichetta", + "search_popout.tips.status": "stato", "search_popout.tips.text": "Testo semplice per trovare nomi visualizzati, nomi utente e hashtag che lo contengono", "search_popout.tips.user": "utente", "search_results.accounts": "Gente", @@ -371,7 +371,7 @@ "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", "upload_error.limit": "Limite al caricamento di file superato.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Caricamento file non consentito nei sondaggi.", "upload_form.description": "Descrizione per utenti con disabilità visive", "upload_form.focus": "Modifica anteprima", "upload_form.undo": "Cancella", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 6502ab02e..83a69e595 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -369,7 +369,7 @@ "trends.count_by_accounts": "{count}人がトゥート", "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", - "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "メディアを追加 ({formats})", "upload_error.limit": "アップロードできる上限を超えています。", "upload_error.poll": "アンケートではファイルをアップロードできません。", "upload_form.description": "視覚障害者のための説明", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4bcab5ba0..f6504f4bb 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -361,15 +361,15 @@ "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "tabs_bar.search": "Zoeken", - "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} left", - "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} left", - "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} left", + "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} te gaan", + "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} te gaan", + "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} te gaan", "time_remaining.moments": "Nog enkele ogenblikken resterend", - "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} left", + "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} te gaan", "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover", "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", - "upload_area.title": "Hierin slepen om te uploaden", - "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_area.title": "Hiernaar toe slepen om te uploaden", + "upload_button.label": "Media toevoegen ({formats})", "upload_error.limit": "Uploadlimiet van bestand overschreden.", "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index d624aa25c..18993af97 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Pridaj, alebo odstráň zo zoznamov", + "account.add_or_remove_from_list": "Pridaj do, alebo odober zo zoznamov", "account.badges.bot": "Bot", "account.block": "Blokuj @{name}", "account.block_domain": "Ukry všetko z {domain}", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 05c29f16e..865d3a514 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -320,7 +320,7 @@ "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", "status.cannot_reblog": "无法转嘟这条嘟文", - "status.copy": "复制链接到嘟文中", + "status.copy": "复制嘟文链接", "status.delete": "删除", "status.detailed_status": "对话详情", "status.direct": "发送私信给 @{name}", diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml index 23c538b19..2b2ffd121 100644 --- a/config/locales/activerecord.fi.yml +++ b/config/locales/activerecord.fi.yml @@ -1 +1,13 @@ +--- fi: + activerecord: + attributes: + poll: + expires_at: Määräaika + options: Vaihtoehdot + errors: + models: + account: + attributes: + username: + invalid: Vain kirjaimia, numeroita ja alleviivoja diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml index 4cec9fb63..70afdaef1 100644 --- a/config/locales/activerecord.it.yml +++ b/config/locales/activerecord.it.yml @@ -1,12 +1,16 @@ --- it: activerecord: + attributes: + poll: + expires_at: Scadenza + options: Scelte errors: models: account: attributes: username: - invalid: solo lettere, numeri e trattino basso + invalid: solo lettere, numeri e trattini bassi status: attributes: reblog: diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml index fc36fdbff..b603e12c6 100644 --- a/config/locales/devise.it.yml +++ b/config/locales/devise.it.yml @@ -12,6 +12,7 @@ it: last_attempt: Hai un altro tentativo prima che il tuo account venga bloccato. locked: Il tuo account è stato bloccato. not_found_in_database: "%{authentication_keys} o password invalida." + pending: Il tuo account è ancora in fase di approvazione. timeout: La tua sessione è terminata. Per favore, effettua l'accesso o registrati per continuare. unauthenticated: Devi effettuare l'accesso o registrarti per continuare. unconfirmed: Devi confermare il tuo indirizzo email per continuare. @@ -20,6 +21,7 @@ it: action: Verifica indirizzo email action_with_app: Conferma e torna a %{app} explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email. + explanation_when_pending: Hai richiesto un invito a %{host} con questo indirizzo email. Una volta confermato il tuo indirizzo e-mail, analizzeremo la tua richiesta. Non potrai eseguire l'accesso fino a quel momento. Se la tua richiesta sarà rifiutata, i tuoi dati saranno rimossi, quindi nessun'altra azione ti sarà richiesta. Se non fossi stato tu, per favore ignora questa email. extra_html: Per favore controllale regole del server e i nostri termini di servizio. subject: 'Mastodon: Istruzioni di conferma per %{instance}' title: Verifica indirizzo email @@ -60,6 +62,7 @@ it: signed_up: Benvenuto! Ti sei registrato con successo. signed_up_but_inactive: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché non hai ancora attivato il tuo account. signed_up_but_locked: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché il tuo account è bloccato. + signed_up_but_pending: Un messaggio con un collegamento per la conferma è stato inviato al tuo indirizzo email. Dopo aver cliccato il collegamento, esamineremo la tua richiesta. Ti sarà notificato se verrà approvata. signed_up_but_unconfirmed: Un messaggio con un link di conferma è stato inviato al tuo indirizzo email. Per favore, visita il link per attivare il tuo account. update_needs_confirmation: Hai aggiornato correttamente il tuo account, ma abbiamo bisogno di verificare il tuo nuovo indirizzo email. Per favore, controlla la posta in arrivo e visita il link di conferma per verificare il tuo indirizzo email. updated: Il tuo account è stato aggiornato con successo. diff --git a/config/locales/doorkeeper.co.yml b/config/locales/doorkeeper.co.yml index 542ad7c57..d45041a4e 100644 --- a/config/locales/doorkeeper.co.yml +++ b/config/locales/doorkeeper.co.yml @@ -114,6 +114,12 @@ co: application: title: Auturizazione OAuth riquestata scopes: + admin:read: leghje tutti i dati nant'à u servore + admin:read:accounts: leghje i cuntinuti sensibili di tutti i conti + admin:read:reports: leghje i cuntinuti sensibili di tutti i rapporti è conti signalati + admin:write: mudificà tutti i dati nant'à u servore + admin:write:accounts: realizà azzione di muderazione nant'à i conti + admin:write:reports: realizà azzione di muderazione nant'à i rapporti follow: Mudificà rilazione trà i conti push: Riceve e vostre nutificazione push read: leghje tutte l’infurmazioni di u vostru contu diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml index f523e125d..cb5cd147c 100644 --- a/config/locales/doorkeeper.cs.yml +++ b/config/locales/doorkeeper.cs.yml @@ -114,6 +114,12 @@ cs: application: title: Je požadována autorizace OAuth scopes: + admin:read: číst všechna data na serveru + admin:read:accounts: číst citlivé informace všech účtů + admin:read:reports: číst citlivé informace všech nahlášení a nahlášených účtů + admin:write: měnit všechna data na serveru + admin:write:accounts: provádět moderátorské akce s účty + admin:write:reports: provádět moderátorské akce s nahlášeními follow: upravovat vztahy mezi profily push: přijímat vaše push oznámení read: vidět všechna data vašeho účtu diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml index e820ff8a6..c63688ade 100644 --- a/config/locales/doorkeeper.el.yml +++ b/config/locales/doorkeeper.el.yml @@ -114,6 +114,12 @@ el: application: title: Απαιτείται έγκριση OAuth scopes: + admin:read: ανάγνωση δεδομένων στον διακομιστή + admin:read:accounts: ανάγνωση ευαίσθητων πληροφοριών όλων των λογαριασμών + admin:read:reports: ανάγνωση ευαίσθητων πληροφοριών όλων των καταγγελιών και των καταγγελλομένων λογαριασμών + admin:write: αλλαγή δεδομένων στον διακομιστή + admin:write:accounts: εκτέλεση διαχειριστικών ενεργειών σε λογαριασμούς + admin:write:reports: εκτέλεση διαχειριστικών ενεργειών σε καταγγελίες follow: να αλλάζει τις σχέσεις με λογαριασμούς push: να λαμβάνει τις ειδοποιήσεις σου read: να διαβάζει όλα τα στοιχεία του λογαριασμού σου diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml index f6bd8b4bc..361d0bd75 100644 --- a/config/locales/doorkeeper.it.yml +++ b/config/locales/doorkeeper.it.yml @@ -36,9 +36,11 @@ it: scopes: Dividi gli scopes con spazi. Lascia vuoto per utilizzare gli scopes di default. index: application: Applicazione + callback_url: URL di callback delete: Elimina name: Nome new: Nuova applicazione + scopes: Visibilità show: Mostra title: Le tue applicazioni new: @@ -112,6 +114,12 @@ it: application: title: Autorizzazione OAuth richiesta scopes: + admin:read: leggere tutti i dati dal server + admin:read:accounts: leggere dati sensibili di tutti gli account + admin:read:reports: leggere dati sensibili di tutte le segnalazioni e gli account segnalati + admin:write: modificare tutti i dati sul server + admin:write:accounts: eseguire azioni di moderazione sugli account + admin:write:reports: eseguire azioni di moderazione sulle segnalazioni follow: modificare relazioni tra account push: ricevere le tue notifiche push read: leggere tutte le informazioni del tuo account diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml index 9bc2d9a80..d80212f82 100644 --- a/config/locales/doorkeeper.ja.yml +++ b/config/locales/doorkeeper.ja.yml @@ -114,6 +114,12 @@ ja: application: title: OAuth認証 scopes: + admin:read: サーバーのすべてのデータの読み取り + admin:read:accounts: すべてのアカウントの機密情報の読み取り + admin:read:reports: すべての通報と通報されたアカウントの機密情報の読み取り + admin:write: サーバーのすべてのデータの変更 + admin:write:accounts: アカウントに対するアクションの実行 + admin:write:reports: 通報に対するアクションの実行 follow: アカウントのつながりを変更 push: プッシュ通知の受信 read: アカウントのすべてのデータの読み取り diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml index de724f6c9..2068eeef4 100644 --- a/config/locales/doorkeeper.pl.yml +++ b/config/locales/doorkeeper.pl.yml @@ -114,6 +114,12 @@ pl: application: title: Uwierzytelnienie OAuth jest wymagane scopes: + admin:read: odczytaj wszystkie dane na serwerze + admin:read:accounts: odczytaj wrażliwe informacje na wszystkich kontach + admin:read:reports: odczytaj wrażliwe informacje ze wszystkich zgłoszeń oraz zgłoszonych kont + admin:write: zmodyfikuj wszystkie dane na serwerze + admin:write:accounts: wykonaj działania moderacyjne na kontach + admin:write:reports: wykonaj działania moderacyjne na zgłoszeniach follow: możliwość śledzenia kont push: otrzymywanie powiadomień push dla Twojego konta read: możliwość odczytu wszystkich danych konta diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml index 341ed2830..26d92ddb5 100644 --- a/config/locales/doorkeeper.sl.yml +++ b/config/locales/doorkeeper.sl.yml @@ -114,6 +114,12 @@ sl: application: title: Potrebna je OAuth pooblastitev scopes: + admin:read: preberi vse podatke na strežniku + admin:read:accounts: preberi občutljive informacije vseh računov + admin:read:reports: preberi občutljive informacije vseh prijav in prijavljenih računov + admin:write: spremeni vse podatke na strežniku + admin:write:accounts: izvedi moderirana dejanja na računih + admin:write:reports: izvedi moderirana dejanja na prijavah follow: spremeni razmerja med računi push: prejmi potisna obvestila read: preberi vse podatke svojega računa diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index 1cce6adc2..dd9337904 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -113,6 +113,12 @@ zh-CN: application: title: 需要 OAuth 认证 scopes: + admin:read: 读取服务器上的所有数据 + admin:read:accounts: 读取所有账户的敏感信息 + admin:read:reports: 读取所有举报和被举报账户的敏感信息 + admin:write: 修改服务器上的所有数据 + admin:write:accounts: 对账户执行管理操作 + admin:write:reports: 对举报执行管理操作 follow: 关注或屏蔽用户 push: 接收你的帐户的推送通知 read: 读取你的帐户数据 diff --git a/config/locales/it.yml b/config/locales/it.yml index 1d1238056..6dfe212d1 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -7,23 +7,33 @@ it: active_count_after: attivo active_footnote: Utenti Attivi Mensili (MAU) administered_by: 'Amministrato da:' + api: API apps: Applicazioni Mobile apps_platforms: Usa Mastodon da iOS, Android e altre piattaforme + browse_directory: Sfoglia la directory dei profili e filtra per interessi + browse_public_posts: Sfoglia il flusso in tempo reale di post pubblici su Mastodon contact: Contatti contact_missing: Non impostato contact_unavailable: N/D + discover_users: Scopri utenti documentation: Documentazione extended_description_html: |

Un buon posto per le regole

La descrizione estesa non è ancora stata preparata.

+ federation_hint_html: Con un account su %{instance} sarai in grado di seguire persone su qualsiasi server Mastodon e oltre. generic_description: "%{domain} è un server nella rete" - get_apps: Prova l'app per smartphone + get_apps: Prova un'app per smartphone hosted_on: Mastodon ospitato su %{domain} learn_more: Scopri altro privacy_policy: Politica della privacy + see_whats_happening: Guarda cosa succede + server_stats: 'Statistiche del server:' source_code: Codice sorgente + status_count_after: + one: stato + other: stati status_count_before: Che hanno pubblicato - tagline: Segui vecchi amici e trovane nuovi + tagline: Segui amici e trovane di nuovi terms: Termini di Servizio user_count_after: one: utente @@ -40,6 +50,7 @@ it: joined: Dal %{date} last_active: ultima attività link_verified_on: La proprietà di questo link è stata controllata il %{date} + media: Media moved_html: "%{name} è stato spostato su %{new_profile_link}:" network_hidden: Questa informazione non e' disponibile nothing_here: Qui non c'è nulla! @@ -47,12 +58,17 @@ it: people_who_follow: Persone che seguono %{name} pin_errors: following: Devi gia seguire la persona che vuoi promuovere + posts: + one: Toot + other: Toot posts_tab_heading: Toot posts_with_replies: Toot e risposte reserved_username: Il nome utente è gia stato preso roles: admin: Amministratore + bot: Bot moderator: Moderatore + unavailable: Profilo non disponibile unfollow: Non seguire più admin: account_actions: @@ -64,7 +80,10 @@ it: delete: Elimina destroyed_msg: Nota di moderazione distrutta con successo! accounts: + approve: Approva + approve_all: Approva tutto are_you_sure: Sei sicuro? + avatar: Immagine di profilo by_domain: Dominio change_email: changed_msg: Account email cambiato con successo! @@ -84,6 +103,7 @@ it: display_name: Nome visualizzato domain: Dominio edit: Modifica + email: Email email_status: Stato email enable: Abilita enabled: Abilitato @@ -94,6 +114,7 @@ it: header: Intestazione inbox_url: URL inbox invited_by: Invitato da + ip: IP joined: Unito location: all: Tutto @@ -106,15 +127,18 @@ it: moderation: active: Attivo all: Tutto + pending: In attesa silenced: Silenziati suspended: Sospesi title: Moderazione moderation_notes: Note di moderazione most_recent_activity: Attività più recenti most_recent_ip: IP più recenti + no_account_selected: Nessun account è stato modificato visto che non ne è stato selezionato nessuno no_limits_imposed: Nessun limite imposto not_subscribed: Non sottoscritto outbox_url: URL outbox + pending: Revisioni in attesa perform_full_suspension: Sospendi profile_url: URL profilo promote: Promuovi @@ -122,6 +146,8 @@ it: public: Pubblico push_subscription_expires: Sottoscrizione PuSH scaduta redownload: Aggiorna avatar + reject: Rifiuta + reject_all: Rifiuta tutto remove_avatar: Rimuovi avatar remove_header: Rimuovi intestazione resend_confirmation: @@ -148,6 +174,7 @@ it: statuses: Stati subscribe: Sottoscrivi suspended: Sospeso + time_in_queue: Attesa in coda %{time} title: Account unconfirmed_email: Email non confermata undo_silenced: Rimuovi silenzia @@ -155,6 +182,7 @@ it: unsubscribe: Annulla l'iscrizione username: Nome utente warn: Avverti + web: Web action_logs: actions: assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso" @@ -188,6 +216,7 @@ it: update_custom_emoji: "%{name} ha aggiornato l'emoji %{target}" update_status: "%{name} stato aggiornato da %{target}" deleted_status: "(stato cancellato)" + title: Registro di controllo custom_emojis: by_domain: Dominio copied_msg: Creata con successo una copia locale dell'emoji @@ -198,6 +227,7 @@ it: destroyed_msg: Emoji distrutto con successo! disable: Disabilita disabled_msg: Questa emoji è stata disabilitata con successo + emoji: Emoji enable: Abilita enabled_msg: Questa emoji è stata abilitata con successo image_hint: PNG fino a 50 KB @@ -205,6 +235,7 @@ it: new: title: Aggiungi nuovo emoji personalizzato overwrite: Sovrascrivi + shortcode: Scorciatoia shortcode_hint: Almeno due caratteri, solo caratteri alfanumerici e trattino basso title: Emoji personalizzate unlisted: Non elencato @@ -212,19 +243,23 @@ it: updated_msg: Emoji aggiornata con successo! upload: Carica dashboard: + backlog: lavori arretrati config: Configurazione feature_deletions: Cancellazioni di account feature_invites: Link di invito feature_profile_directory: Directory dei profili feature_registrations: Registrazioni feature_relay: Ripetitore di federazione + feature_timeline_preview: Anteprima timeline features: Funzionalità hidden_service: Federazione con servizi nascosti open_reports: apri report recent_users: Utenti Recenti search: Ricerca testo intero single_user_mode: Modalita utente singolo + software: Software space: Utilizzo dello spazio + title: Cruscotto total_users: utenti totali trends: Tendenze week_interactions: interazioni per questa settimana @@ -235,6 +270,7 @@ it: created_msg: Il blocco del dominio sta venendo processato destroyed_msg: Il blocco del dominio è stato rimosso domain: Dominio + existing_domain_block_html: Hai già impostato limitazioni più stringenti su %{name}, dovresti sbloccare prima. new: create: Crea blocco hint: Il blocco dominio non previene la creazione di utenti nel database, ma applicherà automaticamente e retroattivamente metodi di moderazione specifici su quegli account. @@ -248,6 +284,8 @@ it: reject_media_hint: Rimuovi i file media salvati in locale e blocca i download futuri. Irrilevante per le sospensioni reject_reports: Respingi rapporti reject_reports_hint: Ignora tutti i rapporti provenienti da questo dominio. Irrilevante per sospensioni + rejecting_media: rigetta file media + rejecting_reports: rigetta segnalazioni severity: silence: silenziato suspend: sospeso @@ -276,16 +314,19 @@ it: title: Seguaci di %{acct} instances: by_domain: Dominio + delivery_available: Distribuzione disponibile known_accounts: one: "%{count} account noto" other: "%{count} account noti" moderation: + all: Tutto limited: Limitato title: Moderazione title: Istanze conosciute total_blocked_by_us: Bloccato da noi total_followed_by_them: Seguito da loro total_followed_by_us: Seguito da noi + total_reported: Segnalazioni su di loro total_storage: Media allegati invites: deactivate_all: Disattiva tutto @@ -295,6 +336,8 @@ it: expired: Scaduto title: Filtro title: Inviti + pending_accounts: + title: Account in attesa (%{count}) relays: add_new: Aggiungi ripetitore delete: Cancella @@ -308,12 +351,14 @@ it: pending: In attesa dell'approvazione del ripetitore save_and_enable: Salva e attiva setup: Crea una connessione con un ripetitore + status: Stato title: Ripetitori report_notes: created_msg: Nota rapporto creata! destroyed_msg: Nota rapporto cancellata! reports: account: + note: note report: rapporto action_taken_by: Azione intrapresa da are_you_sure: Sei sicuro? @@ -349,6 +394,7 @@ it: desc_html: Separa i nomi utente con virgola. Funziona solo con account locali e non bloccati. Quando vuoto, valido per tutti gli amministratori locali. title: Seguiti predefiniti per i nuovi utenti contact_information: + email: E-mail di lavoro username: Nome utente del contatto custom_css: desc_html: Modifica l'aspetto con il CSS caricato in ogni pagina @@ -378,10 +424,17 @@ it: min_invite_role: disabled: Nessuno title: Permetti inviti da + registrations_mode: + modes: + approved: Approvazione richiesta per le iscrizioni + none: Nessuno può iscriversi + open: Chiunque può iscriversi + title: Modalità di registrazione show_known_fediverse_at_about_page: desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali. title: Mostra la fediverse conosciuta nell'anteprima della timeline show_staff_badge: + desc_html: Mostra un distintivo dello staff sulla pagina dell'utente title: Mostra badge staff site_description: desc_html: Paragrafo introduttivo nella pagina iniziale. Descrive ciò che rende speciale questo server Mastodon e qualunque altra cosa sia importante dire. Potete usare marcatori HTML, in particolare <a> e <em>. @@ -410,6 +463,8 @@ it: nsfw_off: Segna come non sensibile nsfw_on: Segna come sensibile failed_to_execute: Impossibile eseguire + media: + title: Media no_media: Nessun media no_status_selected: Nessun status è stato modificato perché nessuno era stato selezionato title: Gli status dell'account @@ -418,11 +473,14 @@ it: callback_url: URL Callback confirmed: Confermato expires_in: Scade in + last_delivery: Ultima distribuzione + title: WebSub topic: Argomento tags: accounts: Account hidden: Nascosto - hide: Non mostrare nella directory + hide: Nascondi dalla directory + name: Etichetta title: Hashtag unhide: Mostra nella directory visible: Visibile @@ -433,8 +491,25 @@ it: edit: Modifica edit_preset: Modifica avviso predefinito title: Gestisci avvisi predefiniti + admin_mailer: + new_pending_account: + body: I dettagli del nuovo account sono qui sotto. Puoi approvare o rifiutare questa richiesta. + subject: Nuovo account pronto per la revisione su %{instance} (%{username}) + new_report: + body: "%{reporter} ha segnalato %{target}" + body_remote: Qualcuno da %{domain} ha segnalato %{target} + subject: Nuova segnalazione per %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfaccia web avanzata + advanced_web_interface_hint: |- + Se vuoi utilizzare l'intera larghezza dello schermo, l'interfaccia web avanzata ti consente di configurare varie colonne per mostrare più informazioni allo stesso tempo, secondo le tue preferenze: + Home, notifiche, timeline federata, qualsiasi numero di liste e etichette. + animations_and_accessibility: Animazioni e accessibiiltà + confirmation_dialogs: Dialoghi di conferma + sensitive_content: Contenuto sensibile application_mailer: notification_preferences: Cambia preferenze email + salutation: "%{name}," settings: 'Cambia le impostazioni per le email: %{link}' view: 'Guarda:' view_profile: Mostra profilo @@ -446,22 +521,32 @@ it: regenerate_token: Rigenera il token di accesso token_regenerated: Token di accesso rigenerato warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro! + your_token: Il tuo token di accesso auth: + apply_for_account: Richiedi un invito + change_password: Password + checkbox_agreement_html: Sono d'accordo con le regole del server ed i termini di servizio confirm_email: Conferma email delete_account: Elimina account delete_account_html: Se desideri cancellare il tuo account, puoi farlo qui. Ti sarà chiesta conferma. didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma? forgot_password: Hai dimenticato la tua password? + invalid_reset_password_token: Il token di reimpostazione della password non è valido o è scaduto. Per favore richiedine uno nuovo. login: Entra logout: Esci da Mastodon migrate_account: Sposta ad un account differente migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi configurarlo qui. or_log_in_with: Oppure accedi con + providers: + cas: CAS + saml: SAML register: Iscriviti + registration_closed: "%{instance} non accetta nuovi membri" resend_confirmation: Invia di nuovo le istruzioni di conferma reset_password: Resetta la password security: Credenziali set_new_password: Imposta una nuova password + trouble_logging_in: Problemi di accesso? authorize_follow: already_following: Stai già seguendo questo account error: Sfortunatamente c'è stato un errore nel consultare l'account remoto @@ -494,6 +579,7 @@ it: proceed: Cancella l'account success_msg: Il tuo account è stato cancellato warning_html: È garantita la cancellazione del contenuto solo da questo server. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + warning_title: Disponibilità di contenuto diffuso directories: directory: Directory dei profili enabled: Attualmente sei elencato nella directory. @@ -511,11 +597,14 @@ it: '422': content: Verifica di sicurezza non riuscita. Stai bloccando i cookies? title: Verifica di sicurezza non riuscita - '429': Throttled + '429': Limitato '500': content: Siamo spiacenti, ma qualcosa non ha funzionato dal nostro lato. title: Questa pagina non è corretta noscript_html: Per usare l'interfaccia web di Mastodon dovi abilitare JavaScript. In alternativa puoi provare una delle app native per Mastodon per la tua piattaforma. + existing_username_validator: + not_found: impossibile trovare un utente locale con quel nome utente + not_found_multiple: impossibile trovare %{usernames} exports: archive_takeout: date: Data @@ -525,6 +614,7 @@ it: request: Richiedi il tuo archivio size: Dimensioni blocks: Stai bloccando + csv: CSV domain_blocks: Blocchi di dominio follows: Stai seguendo lists: Liste @@ -544,6 +634,7 @@ it: title: Modifica filtro errors: invalid_context: Contesto mancante o non valido + invalid_irreversible: Il filtraggio irreversibile funziona solo nei contesti di home o notifiche index: delete: Cancella title: Filtri @@ -552,13 +643,36 @@ it: footer: developers: Sviluppatori more: Altro… + resources: Risorse generic: + all: Tutto changes_saved_msg: Modifiche effettuate con successo! copy: Copia + order_by: Ordina per save_changes: Salva modifiche validation_errors: one: Qualcosa ancora non va bene! Per favore, controlla l'errore qui sotto other: Qualcosa ancora non va bene! Per favore, controlla i %{count} errori qui sotto + html_validator: + invalid_markup: 'contiene markup HTML non valido: %{error}' + identity_proofs: + active: Attive + authorize: Si, autorizza + authorize_connection_prompt: Autorizzare questa connessione crittografata? + errors: + failed: La connessione crittografata non è riuscita. Per favore riprova da %{provider}. + keybase: + invalid_token: I toked di Keybase sono hash di firme e devono essere lunghi 66 caratteri esadecimali + verification_failed: Keybase non riconosce questo token come firma dell'utente Keybase %{kb_username}. Per favore riprova da Keybase. + wrong_user: Impossibile creare una prova per %{proving} mentre si è effettuato l'accesso come %{current}. Accedi come %{proving} e riprova. + explanation_html: Qui puoi connettere crittograficamente le tue altre identità, come il profilo Keybase. Questo consente ad altre persone di inviarti messaggi criptati e fidarsi dei contenuto che tu invii a loro. + i_am_html: Io sono %{username} su %{service}. + identity: Identità + inactive: Inattiva + publicize_checkbox: 'E posta questo:' + publicize_toot: 'É provato! Io sono %{username} su %{service}: %{url}' + status: Stato della verifica + view_proof: Vedi prova imports: modes: merge: Fondi @@ -573,6 +687,7 @@ it: following: Lista dei seguaci muting: Lista dei silenziati upload: Carica + in_memoriam_html: In Memoriam. invites: delete: Disattiva expired: Scaduto @@ -643,14 +758,26 @@ it: body: 'Il tuo status è stato condiviso da %{name}:' subject: "%{name} ha condiviso il tuo status" title: Nuova condivisione + number: + human: + decimal_units: + format: "%n%u" + units: + billion: G + million: M + quadrillion: P + thousand: k + trillion: T pagination: newer: Più recente next: Avanti older: Più vecchio prev: Indietro + truncate: "…" polls: errors: already_voted: Hai già votato in questo sondaggio + duplicate_options: contiene oggetti duplicati duration_too_long: è troppo lontano nel futuro duration_too_short: è troppo presto expired: Il sondaggio si è già concluso @@ -659,12 +786,28 @@ it: too_many_options: non può contenere più di %{max} elementi preferences: other: Altro + posting_defaults: Predefinite di pubblicazione + public_timelines: Timeline pubbliche + relationships: + activity: Attività dell'account + dormant: Dormiente + last_active: Ultima volta attivo + most_recent: Più recente + moved: Trasferito + mutual: Reciproco + primary: Principale + relationship: Relazione + remove_selected_domains: Rimuovi tutti i seguaci dai domini selezionati + remove_selected_followers: Rimuovi i seguaci selezionati + remove_selected_follows: Smetti di seguire gli utenti selezionati + status: Stato dell'account remote_follow: acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account no_account_html: Non hai un account? Puoi iscriverti qui proceed: Conferma prompt: 'Stai per seguire:' + reason_html: "Perchè questo passo è necessario? %{instance} potrebbe non essere il server nel quale tu sei registrato, quindi dobbiamo reindirizzarti prima al tuo server." remote_interaction: favourite: proceed: Continua per segnare come apprezzato @@ -685,15 +828,49 @@ it: too_soon: La data di pubblicazione deve essere nel futuro sessions: activity: Ultima attività + browser: Browser browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Browser sconosciuto + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Sessione corrente description: "%{browser} su %{platform}" explanation: Questi sono i browser da cui attualmente è avvenuto l'accesso al tuo account Mastodon. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: piattaforma sconosciuta + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + revoke: Revoca + revoke_success: Sessione revocata con successo title: Sessioni settings: + account: Account + account_settings: Impostazioni dell'account + appearance: Interfaccia authorized_apps: Applicazioni autorizzate back: Torna a Mastodon delete: Cancellazione account @@ -701,10 +878,14 @@ it: edit_profile: Modifica profilo export: Esporta impostazioni featured_tags: Hashtag in evidenza + identity_proofs: Prove di identità import: Importa + import_and_export: Importa ed esporta migrate: Migrazione dell'account notifications: Notifiche preferences: Preferenze + profile: Profilo + relationships: Follows and followers two_factor_authentication: Autenticazione a due fattori statuses: attached: @@ -712,7 +893,11 @@ it: image: one: "%{count} immagine" other: "%{count} immagini" + video: + one: "%{count} video" + other: "%{count} video" boosted_from_html: Condiviso da %{acct_link} + content_warning: 'Avviso di contenuto: %{warning}' disallowed_hashtags: one: 'contiene un hashtag non permesso: %{tags}' other: 'contiene gli hashtags non permessi: %{tags}' @@ -731,6 +916,7 @@ it: vote: Vota show_more: Mostra di più sign_in_to_participate: Accedi per partecipare alla conversazione + title: '%{name}: "%{quote}"' visibilities: private: Mostra solo ai tuoi seguaci private_long: Mostra solo ai seguaci @@ -748,6 +934,10 @@ it: contrast: Mastodon (contrasto elevato) default: Mastodon (scuro) mastodon-light: Mastodon (chiaro) + time: + formats: + default: "%b %d, %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: Inserisci il codice generato dalla tua app di autenticazione description_html: Se abiliti l'autorizzazione a due fattori, entrare nel tuo account ti richiederà di avere vicino il tuo telefono, il quale ti genererà un codice per eseguire l'accesso. @@ -769,7 +959,24 @@ it: explanation: Hai richiesto un backup completo del tuo account Mastodon. È pronto per essere scaricato! subject: Il tuo archivio è pronto per essere scaricato title: Esportazione archivio + warning: + explanation: + disable: Mentre il tuo account è congelato, i tuoi dati dell'account rimangono intatti, ma non potrai eseguire nessuna azione fintanto che non viene sbloccato. + silence: Mentre il tuo account è limitato, solo le persone che già ti seguono possono vedere i tuoi toot su questo server, e potresti essere escluso da vari elenchi pubblici. Comunque, altri possono manualmente seguirti. + suspend: Il tuo account è stato sospeso, e tutti i tuoi toot ed i tuoi file media caricati sono stati irreversibilmente rimossi da questo server, e dai server dove avevi dei seguaci. + review_server_policies: Rivedi regole del server + subject: + disable: Il tuo account %{acct} è stato congelato + none: Avviso per %{acct} + silence: Il tuo account %{acct} è stato limitato + suspend: Il tuo account %{acct} è stato sospeso + title: + disable: Account congelato + none: Avviso + silence: Account limitato + suspend: Account sospeso welcome: + edit_profile_action: Imposta profilo edit_profile_step: Puoi personalizzare il tuo profilo caricando un avatar, un'intestazione, modificando il tuo nome visualizzato e così via. Se vuoi controllare i tuoi nuovi seguaci prima di autorizzarli a seguirti, puoi bloccare il tuo account. explanation: Ecco alcuni suggerimenti per iniziare final_action: Inizia a postare @@ -789,6 +996,7 @@ it: follow_limit_reached: Non puoi seguire più di %{limit} persone invalid_email: L'indirizzo email inserito non è valido invalid_otp_token: Codice d'accesso non valido + otp_lost_help_html: Se perdessi l'accesso ad entrambi, puoi entrare in contatto con %{email} seamless_external_login: Ti sei collegato per mezzo di un servizio esterno, quindi le impostazioni di email e password non sono disponibili. signed_in_as: 'Hai effettuato l''accesso come:' verification: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index cc3a67126..78db4c672 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -35,7 +35,7 @@ pl: one: wpisu other: wpisów status_count_before: Są autorami - tagline: Śledź znajomych i poznawal nowych + tagline: Śledź znajomych i poznawaj nowych terms: Zasady użytkowania user_count_after: few: użytkowników diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index b832a8035..377a55293 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -27,6 +27,7 @@ it: phrase: Il confronto sarà eseguito ignorando minuscole/maiuscole e i content warning scopes: A quali API l'applicazione potrà avere accesso. Se selezionate un ambito di alto livello, non c'è bisogno di selezionare quelle singole. setting_aggregate_reblogs: Non mostrare nuove condivisioni per toot che sono stati condivisi di recente (ha effetto solo sulle nuove condivisioni) + setting_default_sensitive: Media con contenuti sensibili sono nascosti in modo predefinito e possono essere rivelati con un click setting_display_media_default: Nascondi media segnati come sensibili setting_display_media_hide_all: Nascondi sempre tutti i media setting_display_media_show_all: Nascondi sempre i media segnati come sensibili @@ -39,6 +40,8 @@ it: name: 'Eccone alcuni che potresti usare:' imports: data: File CSV esportato da un altro server Mastodon + invite_request: + text: Questo ci aiuterà ad esaminare la tua richiesta sessions: otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:' user: @@ -62,12 +65,14 @@ it: warning_preset_id: Usa un avviso preimpostato defaults: autofollow: Invita a seguire il tuo account + avatar: Immagine di profilo bot: Questo account è un bot chosen_languages: Filtra lingue confirm_new_password: Conferma nuova password confirm_password: Conferma password context: Contesti del filtro current_password: Password corrente + data: Data discoverable: Inserisci questo account nella directory display_name: Nome visualizzato email: Indirizzo email @@ -82,7 +87,9 @@ it: new_password: Nuova password note: Biografia otp_attempt: Codice due-fattori + password: Password phrase: Parola chiave o frase + setting_advanced_layout: Abilita interfaccia web avanzata setting_aggregate_reblogs: Raggruppa condivisioni in timeline setting_auto_play_gif: Play automatico GIF animate setting_boost_modal: Mostra dialogo di conferma prima del boost @@ -107,18 +114,26 @@ it: username: Nome utente username_or_email: Nome utente o email whole_word: Parola intera + featured_tag: + name: Etichetta interactions: must_be_follower: Blocca notifiche da chi non ti segue must_be_following: Blocca notifiche dalle persone che non segui must_be_following_dm: Blocca i messaggi diretti dalle persone che non segui + invite_request: + text: Perchè vuoi unirti? notification_emails: digest: Invia email riassuntive favourite: Invia email quando segna come preferito al tuo stato follow: Invia email quando qualcuno ti segue follow_request: Invia email quando qualcuno richiede di seguirti mention: Invia email quando qualcuno ti menziona + pending_account: Invia e-mail quando un nuovo account richiede l'approvazione reblog: Invia email quando qualcuno da un boost al tuo stato report: Manda una mail quando viene inviato un nuovo rapporto + 'no': 'No' + recommended: Consigliato required: + mark: "*" text: richiesto 'yes': Si diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index a466b12cb..ccc6c5ca1 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -134,5 +134,6 @@ ja: 'no': いいえ recommended: おすすめ required: + mark: "*" text: 必須 'yes': はい diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 3e1255276..75a43e322 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -546,6 +546,7 @@ sk: migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to nastaviť tu. or_log_in_with: Alebo prihlás s register: Zaregistruj sa + registration_closed: "%{instance} neprijíma nových členov" resend_confirmation: Zašli potvrdzujúce pokyny znovu reset_password: Obnov heslo security: Zabezpečenie @@ -668,6 +669,7 @@ sk: publicize_checkbox: 'A poslať toto:' publicize_toot: 'Je to dokázané! Na %{service} som %{username}: %{url}' status: Stav overenia + view_proof: Ukáž overenie imports: modes: merge: Spoj dohromady @@ -766,6 +768,7 @@ sk: too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: other: Ostatné + public_timelines: Verejné časové osi relationships: activity: Aktivita účtu dormant: Spiace @@ -773,6 +776,7 @@ sk: most_recent: Najnovšie moved: Presunuli sa mutual: Spoločné + primary: Hlavné relationship: Vzťah remove_selected_followers: Odstráň vybraných následovatrľov remove_selected_follows: Prestaň sledovať vybraných užívateľov -- cgit From 8fe7116cdf61152eb6d371d1e69b2948bf586e8b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 22 Jun 2019 17:26:09 +0200 Subject: New Crowdin translations (#11144) * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Catalan) [ci skip] * New translations doorkeeper.en.yml (Catalan) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations doorkeeper.en.yml (Galician) [ci skip] * New translations en.json (Arabic) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations doorkeeper.en.yml (German) [ci skip] * New translations en.json (Slovenian) [ci skip] * i18n-tasks normalize * yarn manage:translations --- app/javascript/mastodon/locales/ca.json | 4 +- .../mastodon/locales/defaultMessages.json | 2 +- app/javascript/mastodon/locales/sl.json | 346 ++++++++++----------- config/locales/doorkeeper.ca.yml | 6 + config/locales/doorkeeper.de.yml | 6 + config/locales/doorkeeper.gl.yml | 6 + 6 files changed, 194 insertions(+), 176 deletions(-) (limited to 'config') diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index acb9709d0..bb73b2a41 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -314,7 +314,7 @@ "search_results.accounts": "Gent", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_status": "Obre aquest toot a la interfície de moderació", "status.block": "Bloqueja @{name}", @@ -366,7 +366,7 @@ "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.moments": "Moments restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.", "upload_area.title": "Arrossega i deixa anar per a carregar", "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index f76caaf96..4df299d84 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1051,7 +1051,7 @@ { "descriptors": [ { - "defaultMessage": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "defaultMessage": "Add media ({formats})", "id": "upload_button.label" } ], diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index f0e813e90..51794a862 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -149,10 +149,10 @@ "hashtag.column_header.tag_mode.none": "brez {additional}", "hashtag.column_settings.select.no_options_message": "Ni najdenih predlogov", "hashtag.column_settings.select.placeholder": "Vpiši ključnik…", - "hashtag.column_settings.tag_mode.all": "Vse našteto", + "hashtag.column_settings.tag_mode.all": "Vse od naštetega", "hashtag.column_settings.tag_mode.any": "Karkoli od naštetega", "hashtag.column_settings.tag_mode.none": "Nič od naštetega", - "hashtag.column_settings.tag_toggle": "V ta stolpec vključite dodatne oznake", + "hashtag.column_settings.tag_toggle": "Za ta stolpec vključi dodatne oznake", "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži spodbude", "home.column_settings.show_replies": "Pokaži odgovore", @@ -161,187 +161,187 @@ "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", "introduction.federation.action": "Naprej", "introduction.federation.federated.headline": "Združeno", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "za krmarjenje nazaj", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "suniti", - "keyboard_shortcuts.column": "osredotočiti status v enega od stolpcev", - "keyboard_shortcuts.compose": "osredotočiti na sestavljanje besedila", + "introduction.federation.federated.text": "Javne objave iz drugih strežnikov fediverse-a bodo prikazane v združeni časovnici.", + "introduction.federation.home.headline": "Domov", + "introduction.federation.home.text": "Objave oseb, ki jim sledite, bodo prikazane v vaši domači časovnici. Lahko sledite vsakomur na katerem koli strežniku!", + "introduction.federation.local.headline": "Lokalno", + "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.", + "introduction.interactions.action": "Zaključi vadnico!", + "introduction.interactions.favourite.headline": "Priljubljeni", + "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.", + "introduction.interactions.reblog.headline": "Spodbudi", + "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.", + "introduction.interactions.reply.headline": "Odgovori", + "introduction.interactions.reply.text": "Lahko odgovarjate na tuje in vaše tute, kar bo odgovore povezalo v pogovor.", + "introduction.welcome.action": "Gremo!", + "introduction.welcome.headline": "Prvi koraki", + "introduction.welcome.text": "Dobrodošli v fediverse-u! Čez nekaj trenutkov boste lahko oddajali sporočila in se pogovarjali s prijatelji prek različnih strežnikov. Vendar je ta strežnik {domain} poseben - gosti vaš profil, zato si zapomnite njegovo ime.", + "keyboard_shortcuts.back": "pojdi nazaj", + "keyboard_shortcuts.blocked": "odpri seznam blokiranih uporabnikov", + "keyboard_shortcuts.boost": "spodbudi", + "keyboard_shortcuts.column": "fokusiraj na status v enemu od stolpcev", + "keyboard_shortcuts.compose": "fokusiraj na območje za sestavljanje besedila", "keyboard_shortcuts.description": "Opis", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "premakniti navzdol po seznamu", - "keyboard_shortcuts.enter": "odpreti status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.direct": "odpri stolpec za neposredna sporočila", + "keyboard_shortcuts.down": "premakni se navzdol po seznamu", + "keyboard_shortcuts.enter": "odpri status", + "keyboard_shortcuts.favourite": "vzljubi", + "keyboard_shortcuts.favourites": "odpri seznam priljubljenih", + "keyboard_shortcuts.federated": "odpri združeno časovnico", "keyboard_shortcuts.heading": "Tipkovne bližnjice", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.home": "odpri domačo časovnico", "keyboard_shortcuts.hotkey": "Hitra tipka", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "da začnete povsem nov tut", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", - "lightbox.close": "Close", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lightbox.view_context": "View context", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.edit.submit": "Change title", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", - "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", - "navigation_bar.edit_profile": "Edit profile", - "navigation_bar.favourites": "Favourites", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.follows_and_followers": "Follows and followers", - "navigation_bar.info": "O tem vozlišču", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Muted users", - "navigation_bar.personal": "Personal", + "keyboard_shortcuts.legend": "pokaži to legendo", + "keyboard_shortcuts.local": "odpri lokalno časovnico", + "keyboard_shortcuts.mention": "omeni avtorja", + "keyboard_shortcuts.muted": "odpri seznam utišanih uporabnikov", + "keyboard_shortcuts.my_profile": "odpri svoj profil", + "keyboard_shortcuts.notifications": "odpri stolpec z obvestili", + "keyboard_shortcuts.pinned": "odpri seznam pripetih tutov", + "keyboard_shortcuts.profile": "odpri avtorjev profil", + "keyboard_shortcuts.reply": "odgovori", + "keyboard_shortcuts.requests": "odpri seznam s prošnjami za sledenje", + "keyboard_shortcuts.search": "fokusiraj na iskanje", + "keyboard_shortcuts.start": "odpri stolpec \"začni\"", + "keyboard_shortcuts.toggle_hidden": "prikaži/skrij besedilo za CW", + "keyboard_shortcuts.toggle_sensitivity": "prikaži/skrij medije", + "keyboard_shortcuts.toot": "začni povsem nov tut", + "keyboard_shortcuts.unfocus": "odfokusiraj območje za sestavljanje besedila/iskanje", + "keyboard_shortcuts.up": "premakni se navzgor po seznamu", + "lightbox.close": "Zapri", + "lightbox.next": "Naslednji", + "lightbox.previous": "Prejšnji", + "lightbox.view_context": "Poglej kontekst", + "lists.account.add": "Dodaj na seznam", + "lists.account.remove": "Odstrani s seznama", + "lists.delete": "Izbriši seznam", + "lists.edit": "Uredi seznam", + "lists.edit.submit": "Spremeni naslov", + "lists.new.create": "Dodaj seznam", + "lists.new.title_placeholder": "Nov naslov seznama", + "lists.search": "Išči med ljudmi, katerim sledite", + "lists.subheading": "Vaši seznami", + "loading_indicator.label": "Nalaganje...", + "media_gallery.toggle_visible": "Preklopi vidljivost", + "missing_indicator.label": "Ni najdeno", + "missing_indicator.sublabel": "Tega vira ni bilo mogoče najti", + "mute_modal.hide_notifications": "Skrij obvestila tega uporabnika?", + "navigation_bar.apps": "Mobilne aplikacije", + "navigation_bar.blocks": "Blokirani uporabniki", + "navigation_bar.community_timeline": "Lokalna časovnica", + "navigation_bar.compose": "Sestavi nov tut", + "navigation_bar.direct": "Neposredna sporočila", + "navigation_bar.discover": "Odkrijte", + "navigation_bar.domain_blocks": "Skrite domene", + "navigation_bar.edit_profile": "Uredi profil", + "navigation_bar.favourites": "Priljubljeni", + "navigation_bar.filters": "Utišane besede", + "navigation_bar.follow_requests": "Prošnje za sledenje", + "navigation_bar.follows_and_followers": "Sledenja in sledilci", + "navigation_bar.info": "O tem strežniku", + "navigation_bar.keyboard_shortcuts": "Hitre tipke", + "navigation_bar.lists": "Seznami", + "navigation_bar.logout": "Odjava", + "navigation_bar.mutes": "Utišani uporabniki", + "navigation_bar.personal": "Osebno", "navigation_bar.pins": "Pripeti tuti", - "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Federated timeline", - "navigation_bar.security": "Security", - "notification.favourite": "{name} favourited your status", - "notification.follow": "{name} followed you", - "notification.mention": "{name} mentioned you", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} boosted your status", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", - "notifications.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "navigation_bar.preferences": "Nastavitve", + "navigation_bar.profile_directory": "Imenik profilov", + "navigation_bar.public_timeline": "Združena časovnica", + "navigation_bar.security": "Varnost", + "notification.favourite": "{name} je vzljubil/a vaš status", + "notification.follow": "{name} vam sledi", + "notification.mention": "{name} vas je omenil/a", + "notification.poll": "Glasovanje, v katerem ste sodelovali, se je končalo", + "notification.reblog": "{name} je spodbudil/a vaš status", + "notifications.clear": "Počisti obvestila", + "notifications.clear_confirmation": "Ali ste prepričani, da želite trajno izbrisati vsa vaša obvestila?", + "notifications.column_settings.alert": "Namizna obvestila", + "notifications.column_settings.favourite": "Priljubljeni:", + "notifications.column_settings.filter_bar.advanced": "Prikaži vse kategorije", + "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje", + "notifications.column_settings.filter_bar.show": "Pokaži", + "notifications.column_settings.follow": "Novi sledilci:", + "notifications.column_settings.mention": "Omembe:", + "notifications.column_settings.poll": "Rezultati glasovanja:", + "notifications.column_settings.push": "Potisna obvestila", + "notifications.column_settings.reblog": "Spodbude:", + "notifications.column_settings.show": "Prikaži v stolpcu", + "notifications.column_settings.sound": "Predvajaj zvok", + "notifications.filter.all": "Vse", + "notifications.filter.boosts": "Spodbude", + "notifications.filter.favourites": "Priljubljeni", + "notifications.filter.follows": "Sledi", + "notifications.filter.mentions": "Omembe", + "notifications.filter.polls": "Rezultati glasovanj", + "notifications.group": "{count} obvestil", + "poll.closed": "Zaprto", + "poll.refresh": "Osveži", + "poll.total_votes": "{count, plural,one {# glas} other {# glasov}}", + "poll.vote": "Glasuj", + "poll_button.add_poll": "Dodaj anketo", + "poll_button.remove_poll": "Odstrani anketo", + "privacy.change": "Prilagodi zasebnost statusa", + "privacy.direct.long": "Objavi samo omenjenim uporabnikom", + "privacy.direct.short": "Neposredno", + "privacy.private.long": "Objavi samo sledilcem", + "privacy.private.short": "Samo sledilci", + "privacy.public.long": "Objavi na javne časovnice", + "privacy.public.short": "Javno", + "privacy.unlisted.long": "Ne objavi na javne časovnice", + "privacy.unlisted.short": "Ni prikazano", + "regeneration_indicator.label": "Nalaganje…", + "regeneration_indicator.sublabel": "Vaš domači vir se pripravlja!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "zdaj", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Cancel", - "report.forward": "Forward to {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Report {target}", - "search.placeholder": "Search", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", + "reply_indicator.cancel": "Prekliči", + "report.forward": "Posreduj do {target}", + "report.forward_hint": "Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik?", + "report.hint": "Poročilo bo poslano moderatorjem vašega vozlišča. Spodaj lahko navedete, zakaj prijavljate ta račun:", + "report.placeholder": "Dodatni komentarji", + "report.submit": "Pošlji", + "report.target": "Prijavi {target}", + "search.placeholder": "Iskanje", + "search_popout.search_format": "Napredna oblika iskanja", + "search_popout.tips.full_text": "Enostavno besedilo vrne statuse, ki ste jih napisali, vzljubili, spodbudili ali ste bili v njih omenjeni, kot tudi ujemajoča se uporabniška imena, prikazna imena in ključnike.", + "search_popout.tips.hashtag": "ključnik", + "search_popout.tips.status": "stanje", + "search_popout.tips.text": "Enostavno besedilo vrne ujemajoča se prikazna imena, uporabniška imena in ključnike", + "search_popout.tips.user": "uporabnik", + "search_results.accounts": "Ljudje", + "search_results.hashtags": "Ključniki", "search_results.statuses": "Tuti", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", - "status.delete": "Delete", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", - "status.favourite": "Favourite", - "status.filtered": "Filtered", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", - "status.mention": "Mention @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", + "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}", + "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", + "status.admin_status": "Odpri status v vmesniku za moderiranje", + "status.block": "Blokiraj @{name}", + "status.cancel_reblog_private": "Prekini spodbudo", + "status.cannot_reblog": "Te objave ni mogoče spodbuditi", + "status.copy": "Kopiraj povezavo do statusa", + "status.delete": "Izbriši", + "status.detailed_status": "Podroben pogled pogovora", + "status.direct": "Neposredno sporočilo @{name}", + "status.embed": "Vgradi", + "status.favourite": "Priljubljen", + "status.filtered": "Filtrirano", + "status.load_more": "Naloži več", + "status.media_hidden": "Mediji so skriti", + "status.mention": "Omeni @{name}", + "status.more": "Več", + "status.mute": "Utišaj @{name}", + "status.mute_conversation": "Utišaj pogovor", + "status.open": "Razširi ta status", + "status.pin": "Pripni na profil", "status.pinned": "Pripeti tut", - "status.read_more": "Read more", - "status.reblog": "Suni", - "status.reblog_private": "Suni v prvotno občinstvo", - "status.reblogged_by": "{name} sunjen", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", + "status.read_more": "Preberi več", + "status.reblog": "Spodbudi", + "status.reblog_private": "Spodbudi izvirnemu občinstvu", + "status.reblogged_by": "{name} spodbujen", + "status.reblogs.empty": "Nihče še ni spodbudil tega tuta. Ko se bo to zgodilo, se bodo pojavili tukaj.", + "status.redraft": "Izbriši in preoblikuj", "status.reply": "Odgovori", "status.replyAll": "Odgovori na objavo", "status.report": "Prijavi @{name}", diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml index dfa46551f..dde70f47a 100644 --- a/config/locales/doorkeeper.ca.yml +++ b/config/locales/doorkeeper.ca.yml @@ -114,6 +114,12 @@ ca: application: title: OAuth autorització requerida scopes: + admin:read: llegir totes les dades en el servidor + admin:read:accounts: llegir l'informació sensible de tots els comptes + admin:read:reports: llegir l'informació sensible de tots els informes i comptes reportats + admin:write: modificar totes les dades en el servidor + admin:write:accounts: fer l'acció de moderació en els comptes + admin:write:reports: fer l'acció de moderació en els informes follow: seguir, blocar, desblocar i deixar de seguir comptes push: rebre notificacions push del teu compte read: llegir les dades del teu compte diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml index edf9c660c..c41a847b2 100644 --- a/config/locales/doorkeeper.de.yml +++ b/config/locales/doorkeeper.de.yml @@ -114,6 +114,12 @@ de: application: title: OAuth-Autorisierung nötig scopes: + admin:read: alle Daten auf dem Server lesen + admin:read:accounts: sensible Daten aller Konten lesen + admin:read:reports: sensible Daten aller Meldungen und gemeldeten Konten lesen + admin:write: alle Daten auf dem Server ändern + admin:write:accounts: Moderationsaktionen auf Konten ausführen + admin:write:reports: Moderationsaktionen auf Meldungen ausführen follow: Kontenbeziehungen verändern push: deine Push-Benachrichtigungen erhalten read: all deine Daten lesen diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml index 0dc45d5a3..90cbd9b38 100644 --- a/config/locales/doorkeeper.gl.yml +++ b/config/locales/doorkeeper.gl.yml @@ -114,6 +114,12 @@ gl: application: title: Precisa autorización OAuth scopes: + admin:read: ler todos os datos no servidor + admin:read:accounts: ler información sensible de todas as contas + admin:read:reports: ler información sensible de todos os informes e contas reportadas + admin:write: modificar todos os datos no servidor + admin:write:accounts: executar accións de moderación nas contas + admin:write:reports: executar accións de moderación nos informes follow: modificar as relacións da conta push: recibir notificacións push read: ler todos os datos da súa conta -- cgit From 3086c645fde2345d34e401bdf3e2f19f19da3294 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 26 Jun 2019 19:33:04 +0200 Subject: Add option to disable blurhash previews (#11188) * Add option to disable blurhash previews * Update option text * Change options order --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/components/media_gallery.js | 4 +++- app/javascript/mastodon/features/video/index.js | 4 +++- app/javascript/mastodon/initial_state.js | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/appearance/show.html.haml | 3 +++ config/locales/simple_form.en.yml | 2 ++ config/settings.yml | 1 + 10 files changed, 21 insertions(+), 3 deletions(-) (limited to 'config') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 110debd6e..0a5c14cca 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -54,6 +54,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_aggregate_reblogs, :setting_show_application, :setting_advanced_layout, + :setting_use_blurhash, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 77bac61ee..9cd71b7c9 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; -import { autoPlayGif, displayMedia } from '../initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -81,6 +81,8 @@ class Item extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.attachment.get('blurhash'); const pixels = decode(hash, 32, 32); diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index b0c408527..0acdd198d 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -5,7 +5,7 @@ import { fromJS, is } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; -import { displayMedia } from '../../initial_state'; +import { displayMedia, useBlurhash } from '../../initial_state'; import Icon from 'mastodon/components/icon'; import { decode } from 'blurhash'; @@ -298,6 +298,8 @@ class Video extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.blurhash; const pixels = decode(hash, 32, 32); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 125508c23..f0f131ff5 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -20,5 +20,6 @@ export const mascot = getMeta('mascot'); export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); +export const useBlurhash = getMeta('use_blurhash'); export default initialState; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index bf2e5a962..aaf95cc19 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -34,6 +34,7 @@ class UserSettingsDecorator user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') + user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') end def merged_notification_emails @@ -112,6 +113,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_advanced_layout' end + def use_blurhash_preference + boolean_cast_setting 'setting_use_blurhash' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 50873dd01..3a4b415dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, to: :settings, prefix: :setting, allow_nil: false + :advanced_layout, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 8daf36e0a..54f99d570 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -32,6 +32,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers store[:reduce_motion] = object.current_account.user.setting_reduce_motion store[:advanced_layout] = object.current_account.user.setting_advanced_layout + store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:is_staff] = object.current_account.user.staff? end diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 10f009264..c13882801 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -34,6 +34,9 @@ .fields-group = f.input :setting_display_media, collection: ['default', 'show_all', 'hide_all'],label_method: lambda { |item| t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label + .fields-group + = f.input :setting_use_blurhash, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_expand_spoilers, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4602f9cd9..75cbec9de 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -34,6 +34,7 @@ en: setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages setting_show_application: The application you use to toot will be displayed in the detailed view of your toots + setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word featured_tag: @@ -109,6 +110,7 @@ en: setting_system_font_ui: Use system's default font setting_theme: Site theme setting_unfollow_modal: Show confirmation dialog before unfollowing someone + setting_use_blurhash: Show colorful gradients for hidden media severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index 805624d3e..ad71b6008 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -32,6 +32,7 @@ defaults: &defaults theme: 'default' aggregate_reblogs: true advanced_layout: false + use_blurhash: true notification_emails: follow: false reblog: false -- cgit From 3bc0c4a884ef054e0f715be1f3fd8566288fa514 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 2 Jul 2019 11:34:39 +0200 Subject: Remove unused StatsD code and expose StatsD as a global variable (#11232) The instrumentation code was used for StatsD metrics collection prior to the switch to the nsa gem and should have been removed at that point as it no longer does anything at all --- config/initializers/instrumentation.rb | 18 ------------------ config/initializers/statsd.rb | 6 +++--- 2 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 config/initializers/instrumentation.rb (limited to 'config') diff --git a/config/initializers/instrumentation.rb b/config/initializers/instrumentation.rb deleted file mode 100644 index 8483f2be2..000000000 --- a/config/initializers/instrumentation.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -instrumentation_hostname = ENV.fetch('INSTRUMENTATION_HOSTNAME') { 'localhost' } - -ActiveSupport::Notifications.subscribe(/process_action.action_controller/) do |*args| - event = ActiveSupport::Notifications::Event.new(*args) - controller = event.payload[:controller] - action = event.payload[:action] - format = event.payload[:format] || 'all' - format = 'all' if format == '*/*' - status = event.payload[:status] - key = "#{controller}.#{action}.#{format}.#{instrumentation_hostname}" - - ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.total_duration", value: event.duration - ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.db_time", value: event.payload[:db_runtime] - ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.view_time", value: event.payload[:view_runtime] - ActiveSupport::Notifications.instrument :performance, measurement: "#{key}.status.#{status}" -end diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb index ce83fd9de..93ea1d1e4 100644 --- a/config/initializers/statsd.rb +++ b/config/initializers/statsd.rb @@ -3,10 +3,10 @@ if ENV['STATSD_ADDR'].present? host, port = ENV['STATSD_ADDR'].split(':') - statsd = ::Statsd.new(host, port) - statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } + $statsd = ::Statsd.new(host, port) + $statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } - ::NSA.inform_statsd(statsd) do |informant| + ::NSA.inform_statsd($statsd) do |informant| informant.collect(:action_controller, :web) informant.collect(:active_record, :db) informant.collect(:active_support_cache, :cache) -- cgit From 3fd6ab99e69e736e10c7022f07dfa34b60a4e54d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 5 Jul 2019 02:14:56 +0200 Subject: Remove deprecated REST API `GET /api/v1/timelines/direct` (#11212) --- .../api/v1/timelines/direct_controller.rb | 63 ---------------------- app/models/status.rb | 41 -------------- config/routes.rb | 1 - .../api/v1/timelines/direct_controller_spec.rb | 17 ------ spec/models/status_spec.rb | 50 ----------------- 5 files changed, 172 deletions(-) delete mode 100644 app/controllers/api/v1/timelines/direct_controller.rb delete mode 100644 spec/controllers/api/v1/timelines/direct_controller_spec.rb (limited to 'config') diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb deleted file mode 100644 index d8a76d153..000000000 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Timelines::DirectController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] - before_action :require_user!, only: [:show] - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - - respond_to :json - - def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) - end - - private - - def load_statuses - cached_direct_statuses - end - - def cached_direct_statuses - cache_collection direct_statuses, Status - end - - def direct_statuses - direct_timeline_statuses - end - - def direct_timeline_statuses - # this query requires built in pagination. - Status.as_direct_timeline( - current_account, - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id], - true # returns array of cache_ids object - ) - end - - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.permit(:local, :limit).merge(core_params) - end - - def next_path - api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id) - end - - def prev_path - api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id - end -end diff --git a/app/models/status.rb b/app/models/status.rb index fb9bbc9a9..2258e2d07 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -280,47 +280,6 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) - # direct timeline is mix of direct message from_me and to_me. - # 2 queries are executed with pagination. - # constant expression using arel_table is required for partial index - - # _from_me part does not require any timeline filters - query_from_me = where(account_id: account.id) - .where(Status.arel_table[:visibility].eq(3)) - .limit(limit) - .order('statuses.id DESC') - - # _to_me part requires mute and block filter. - # FIXME: may we check mutes.hide_notifications? - query_to_me = Status - .joins(:mentions) - .merge(Mention.where(account_id: account.id)) - .where(Status.arel_table[:visibility].eq(3)) - .limit(limit) - .order('mentions.status_id DESC') - .not_excluded_by_account(account) - - if max_id.present? - query_from_me = query_from_me.where('statuses.id < ?', max_id) - query_to_me = query_to_me.where('mentions.status_id < ?', max_id) - end - - if since_id.present? - query_from_me = query_from_me.where('statuses.id > ?', since_id) - query_to_me = query_to_me.where('mentions.status_id > ?', since_id) - end - - if cache_ids - # returns array of cache_ids object that have id and updated_at - (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) - else - # returns ActiveRecord.Relation - items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) - Status.where(id: items.map(&:id)) - end - end - def as_public_timeline(account = nil, local_only = false) query = timeline_scope(local_only).without_replies diff --git a/config/routes.rb b/config/routes.rb index 764db8db2..29db54a32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -299,7 +299,6 @@ Rails.application.routes.draw do end namespace :timelines do - resource :direct, only: :show, controller: :direct resource :home, only: :show, controller: :home resource :public, only: :show, controller: :public resources :tag, only: :show diff --git a/spec/controllers/api/v1/timelines/direct_controller_spec.rb b/spec/controllers/api/v1/timelines/direct_controller_spec.rb deleted file mode 100644 index a22c2cbea..000000000 --- a/spec/controllers/api/v1/timelines/direct_controller_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::V1::Timelines::DirectController, type: :controller do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } - - describe 'GET #show' do - it 'returns 200' do - allow(controller).to receive(:doorkeeper_token) { token } - get :show - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index e8cf18af9..a8c567414 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -339,56 +339,6 @@ RSpec.describe Status, type: :model do end end - describe '.as_direct_timeline' do - let(:account) { Fabricate(:account) } - let(:followed) { Fabricate(:account) } - let(:not_followed) { Fabricate(:account) } - - before do - Fabricate(:follow, account: account, target_account: followed) - - @self_public_status = Fabricate(:status, account: account, visibility: :public) - @self_direct_status = Fabricate(:status, account: account, visibility: :direct) - @followed_public_status = Fabricate(:status, account: followed, visibility: :public) - @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) - @not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct) - - @results = Status.as_direct_timeline(account) - end - - it 'does not include public statuses from self' do - expect(@results).to_not include(@self_public_status) - end - - it 'includes direct statuses from self' do - expect(@results).to include(@self_direct_status) - end - - it 'does not include public statuses from followed' do - expect(@results).to_not include(@followed_public_status) - end - - it 'does not include direct statuses not mentioning recipient from followed' do - expect(@results).to_not include(@followed_direct_status) - end - - it 'does not include direct statuses not mentioning recipient from non-followed' do - expect(@results).to_not include(@not_followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - results2 = Status.as_direct_timeline(account) - expect(results2).to include(@followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: @not_followed_direct_status) - results2 = Status.as_direct_timeline(account) - expect(results2).to include(@not_followed_direct_status) - end - end - describe '.as_public_timeline' do it 'only includes statuses with public visibility' do public_status = Fabricate(:status, visibility: :public) -- cgit From 30f5bcf3e749be9651ed39a07b893f70605f8a39 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 5 Jul 2019 02:15:13 +0200 Subject: Remove deprecated REST API `POST /api/v1/notifications/dismiss?id=:id` (#11214) --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'config') diff --git a/config/routes.rb b/config/routes.rb index 29db54a32..1f97dea4c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -351,7 +351,6 @@ Rails.application.routes.draw do resources :notifications, only: [:index, :show] do collection do post :clear - post :dismiss # Deprecated end member do -- cgit From 0c1b1069c96d3452d43d922a737c522d10e4747b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 5 Jul 2019 02:15:24 +0200 Subject: Remove deprecated REST API `GET /api/v1/statuses/:id/card` (#11213) --- app/controllers/api/v1/statuses_controller.rb | 14 ++------------ config/routes.rb | 1 - spec/controllers/api/v1/statuses_controller_spec.rb | 14 -------------- 3 files changed, 2 insertions(+), 27 deletions(-) (limited to 'config') diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b0e134554..71a505c26 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy] - before_action :require_user!, except: [:show, :context, :card] - before_action :set_status, only: [:show, :context, :card] + before_action :require_user!, except: [:show, :context] + before_action :set_status, only: [:show, :context] respond_to :json @@ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) end - def card - @card = @status.preview_cards.first - - if @card.nil? - render_empty - else - render json: @card, serializer: REST::PreviewCardSerializer - end - end - def create @status = PostStatusService.new.call(current_user.account, text: status_params[:status], diff --git a/config/routes.rb b/config/routes.rb index 1f97dea4c..9ab5ba7f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -294,7 +294,6 @@ Rails.application.routes.draw do member do get :context - get :card end end diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 8bc3b0c67..9ff5fcd3b 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -91,13 +91,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do expect(response).to have_http_status(404) end end - - describe 'GET #card' do - it 'returns http unautharized' do - get :card, params: { id: status.id } - expect(response).to have_http_status(404) - end - end end context 'with a public status' do @@ -120,13 +113,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do expect(response).to have_http_status(200) end end - - describe 'GET #card' do - it 'returns http success' do - get :card, params: { id: status.id } - expect(response).to have_http_status(200) - end - end end end end -- cgit From 23aeef52cc4540b4514e9f3b935b21f0530a3746 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 6 Jul 2019 23:26:16 +0200 Subject: Remove Salmon and PubSubHubbub (#11205) * Remove Salmon and PubSubHubbub endpoints * Add error when trying to follow OStatus accounts * Fix new accounts not being created in ResolveAccountService --- app/controllers/activitypub/inboxes_controller.rb | 1 - app/controllers/admin/accounts_controller.rb | 16 +- app/controllers/api/push_controller.rb | 73 ------ app/controllers/api/salmon_controller.rb | 37 --- app/controllers/api/subscriptions_controller.rb | 51 ----- app/controllers/api/v1/follows_controller.rb | 31 --- app/lib/ostatus/activity/base.rb | 71 ------ app/lib/ostatus/activity/creation.rb | 219 ------------------ app/lib/ostatus/activity/deletion.rb | 16 -- app/lib/ostatus/activity/general.rb | 20 -- app/lib/ostatus/activity/post.rb | 23 -- app/lib/ostatus/activity/remote.rb | 11 - app/lib/ostatus/activity/share.rb | 26 --- app/lib/ostatus/atom_serializer.rb | 2 - app/models/account.rb | 3 +- app/serializers/webfinger_serializer.rb | 1 - app/services/authorize_follow_service.rb | 12 +- app/services/batched_remove_status_service.rb | 34 +-- app/services/block_domain_service.rb | 1 - app/services/block_service.rb | 12 +- app/services/concerns/author_extractor.rb | 23 -- app/services/concerns/stream_entry_renderer.rb | 7 - app/services/favourite_service.rb | 6 - app/services/fetch_remote_account_service.rb | 28 --- app/services/fetch_remote_status_service.rb | 28 --- app/services/follow_service.rb | 24 +- app/services/post_status_service.rb | 1 - app/services/process_feed_service.rb | 31 --- app/services/process_interaction_service.rb | 151 ------------ app/services/process_mentions_service.rb | 7 - app/services/pubsubhubbub/subscribe_service.rb | 53 ----- app/services/pubsubhubbub/unsubscribe_service.rb | 31 --- app/services/reblog_service.rb | 4 - app/services/reject_follow_service.rb | 12 +- app/services/remove_status_service.rb | 13 -- app/services/resolve_account_service.rb | 131 +---------- app/services/send_interaction_service.rb | 39 ---- app/services/subscribe_service.rb | 58 ----- app/services/unblock_service.rb | 12 +- app/services/unfavourite_service.rb | 13 +- app/services/unfollow_service.rb | 16 +- app/services/unsubscribe_service.rb | 36 --- app/services/update_remote_profile_service.rb | 66 ------ app/services/verify_salmon_service.rb | 26 --- app/views/accounts/show.html.haml | 1 - .../admin/subscriptions/_subscription.html.haml | 18 -- app/views/admin/subscriptions/index.html.haml | 16 -- app/views/well_known/webfinger/show.xml.ruby | 5 - app/workers/after_remote_follow_request_worker.rb | 24 +- app/workers/after_remote_follow_worker.rb | 24 +- app/workers/notification_worker.rb | 4 +- app/workers/processing_worker.rb | 4 +- app/workers/pubsubhubbub/confirmation_worker.rb | 75 +----- app/workers/pubsubhubbub/delivery_worker.rb | 74 +----- app/workers/pubsubhubbub/distribution_worker.rb | 25 +- .../pubsubhubbub/raw_distribution_worker.rb | 15 +- app/workers/pubsubhubbub/subscribe_worker.rb | 27 +-- app/workers/pubsubhubbub/unsubscribe_worker.rb | 8 +- app/workers/remote_profile_update_worker.rb | 6 +- app/workers/salmon_worker.rb | 6 +- app/workers/scheduler/subscriptions_scheduler.rb | 10 +- config/locales/en.yml | 7 - config/navigation.rb | 1 - config/routes.rb | 14 -- config/sidekiq.yml | 3 - spec/controllers/admin/accounts_controller_spec.rb | 38 ---- .../admin/subscriptions_controller_spec.rb | 32 --- spec/controllers/api/push_controller_spec.rb | 59 ----- spec/controllers/api/salmon_controller_spec.rb | 65 ------ .../api/subscriptions_controller_spec.rb | 68 ------ spec/controllers/api/v1/follows_controller_spec.rb | 51 ----- spec/fixtures/requests/webfinger.txt | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 145 ------------ spec/services/authorize_follow_service_spec.rb | 7 - .../services/batched_remove_status_service_spec.rb | 13 -- spec/services/block_service_spec.rb | 7 - spec/services/favourite_service_spec.rb | 7 - spec/services/fetch_remote_account_service_spec.rb | 40 ---- spec/services/follow_service_spec.rb | 68 ------ spec/services/import_service_spec.rb | 30 ++- spec/services/post_status_service_spec.rb | 2 - spec/services/process_feed_service_spec.rb | 252 --------------------- spec/services/process_interaction_service_spec.rb | 151 ------------ spec/services/process_mentions_service_spec.rb | 4 - .../pubsubhubbub/subscribe_service_spec.rb | 71 ------ .../pubsubhubbub/unsubscribe_service_spec.rb | 46 ---- spec/services/reblog_service_spec.rb | 4 - spec/services/reject_follow_service_spec.rb | 7 - spec/services/remove_status_service_spec.rb | 13 -- spec/services/resolve_account_service_spec.rb | 88 +------ spec/services/send_interaction_service_spec.rb | 7 - spec/services/subscribe_service_spec.rb | 43 ---- spec/services/unblock_service_spec.rb | 7 - spec/services/unfollow_service_spec.rb | 7 - spec/services/unsubscribe_service_spec.rb | 37 --- .../services/update_remote_profile_service_spec.rb | 84 ------- .../after_remote_follow_request_worker_spec.rb | 59 ----- spec/workers/after_remote_follow_worker_spec.rb | 59 ----- .../pubsubhubbub/confirmation_worker_spec.rb | 88 ------- spec/workers/pubsubhubbub/delivery_worker_spec.rb | 68 ------ .../pubsubhubbub/distribution_worker_spec.rb | 46 ---- .../scheduler/subscriptions_scheduler_spec.rb | 19 -- 102 files changed, 69 insertions(+), 3568 deletions(-) delete mode 100644 app/controllers/api/push_controller.rb delete mode 100644 app/controllers/api/salmon_controller.rb delete mode 100644 app/controllers/api/subscriptions_controller.rb delete mode 100644 app/controllers/api/v1/follows_controller.rb delete mode 100644 app/lib/ostatus/activity/base.rb delete mode 100644 app/lib/ostatus/activity/creation.rb delete mode 100644 app/lib/ostatus/activity/deletion.rb delete mode 100644 app/lib/ostatus/activity/general.rb delete mode 100644 app/lib/ostatus/activity/post.rb delete mode 100644 app/lib/ostatus/activity/remote.rb delete mode 100644 app/lib/ostatus/activity/share.rb delete mode 100644 app/services/concerns/author_extractor.rb delete mode 100644 app/services/concerns/stream_entry_renderer.rb delete mode 100644 app/services/process_feed_service.rb delete mode 100644 app/services/process_interaction_service.rb delete mode 100644 app/services/pubsubhubbub/subscribe_service.rb delete mode 100644 app/services/pubsubhubbub/unsubscribe_service.rb delete mode 100644 app/services/send_interaction_service.rb delete mode 100644 app/services/subscribe_service.rb delete mode 100644 app/services/unsubscribe_service.rb delete mode 100644 app/services/update_remote_profile_service.rb delete mode 100644 app/services/verify_salmon_service.rb delete mode 100644 app/views/admin/subscriptions/_subscription.html.haml delete mode 100644 app/views/admin/subscriptions/index.html.haml delete mode 100644 spec/controllers/admin/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/push_controller_spec.rb delete mode 100644 spec/controllers/api/salmon_controller_spec.rb delete mode 100644 spec/controllers/api/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/follows_controller_spec.rb delete mode 100644 spec/services/process_feed_service_spec.rb delete mode 100644 spec/services/process_interaction_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/subscribe_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/unsubscribe_service_spec.rb delete mode 100644 spec/services/send_interaction_service_spec.rb delete mode 100644 spec/services/subscribe_service_spec.rb delete mode 100644 spec/services/unsubscribe_service_spec.rb delete mode 100644 spec/services/update_remote_profile_service_spec.rb delete mode 100644 spec/workers/after_remote_follow_request_worker_spec.rb delete mode 100644 spec/workers/after_remote_follow_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/confirmation_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/delivery_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/distribution_worker_spec.rb delete mode 100644 spec/workers/scheduler/subscriptions_scheduler_spec.rb (limited to 'config') diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index a0b7532c2..e2cd8eaed 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -44,7 +44,6 @@ class ActivityPub::InboxesController < Api::BaseController ResolveAccountWorker.perform_async(signed_request_account.acct) end - Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed? DeliveryFailureTracker.track_inverse_success!(signed_request_account) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0c7760d77..2fa1dfe5f 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,8 +2,8 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] - before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] + before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] + before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index @@ -19,18 +19,6 @@ module Admin @warnings = @account.targeted_account_warnings.latest.custom end - def subscribe - authorize @account, :subscribe? - Pubsubhubbub::SubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - - def unsubscribe - authorize @account, :unsubscribe? - Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - def memorialize authorize @account, :memorialize? @account.memorialize! diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb deleted file mode 100644 index e04d19125..000000000 --- a/app/controllers/api/push_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -class Api::PushController < Api::BaseController - include SignatureVerification - - def update - response, status = process_push_request - render plain: response, status: status - end - - private - - def process_push_request - case hub_mode - when 'subscribe' - Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain) - when 'unsubscribe' - Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback) - else - ["Unknown mode: #{hub_mode}", 422] - end - end - - def hub_mode - params['hub.mode'] - end - - def hub_topic - params['hub.topic'] - end - - def hub_callback - params['hub.callback'] - end - - def hub_lease_seconds - params['hub.lease_seconds'] - end - - def hub_secret - params['hub.secret'] - end - - def account_from_topic - if hub_topic.present? && local_domain? && account_feed_path? - Account.find_local(hub_topic_params[:username]) - end - end - - def hub_topic_params - @_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path) - end - - def hub_topic_uri - @_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize - end - - def local_domain? - TagManager.instance.web_domain?(hub_topic_domain) - end - - def verified_domain - return signed_request_account.domain if signed_request_account - end - - def hub_topic_domain - hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '') - end - - def account_feed_path? - hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom' - end -end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb deleted file mode 100644 index ac5f3268d..000000000 --- a/app/controllers/api/salmon_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Api::SalmonController < Api::BaseController - include SignatureVerification - - before_action :set_account - respond_to :txt - - def update - if verify_payload? - process_salmon - head 202 - elsif payload.present? - render plain: signature_verification_failure_reason, status: 401 - else - head 400 - end - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def payload - @_payload ||= request.body.read - end - - def verify_payload? - payload.present? && VerifySalmonService.new.call(payload) - end - - def process_salmon - SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8')) - end -end diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb deleted file mode 100644 index 89007f3d6..000000000 --- a/app/controllers/api/subscriptions_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class Api::SubscriptionsController < Api::BaseController - before_action :set_account - respond_to :txt - - def show - if subscription.valid?(params['hub.topic']) - @account.update(subscription_expires_at: future_expires) - render plain: encoded_challenge, status: 200 - else - head 404 - end - end - - def update - if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE']) - ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8')) - end - - head 200 - end - - private - - def subscription - @_subscription ||= @account.subscription( - api_subscription_url(@account.id) - ) - end - - def body - @_body ||= request.body.read - end - - def encoded_challenge - HTMLEntities.new.encode(params['hub.challenge']) - end - - def future_expires - Time.now.utc + lease_seconds_or_default - end - - def lease_seconds_or_default - (params['hub.lease_seconds'] || 1.day).to_i.seconds - end - - def set_account - @account = Account.find(params[:id]) - end -end diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb deleted file mode 100644 index 5420c0533..000000000 --- a/app/controllers/api/v1/follows_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::FollowsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow, :'write:follows' } - before_action :require_user! - - respond_to :json - - def create - raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? - - @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - - if @account.nil? - username, domain = target_uri.split('@') - @account = Account.find_remote!(username, domain) - end - - render json: @account, serializer: REST::AccountSerializer - end - - private - - def target_uri - follow_params[:uri].strip.gsub(/\A@/, '') - end - - def follow_params - params.permit(:uri) - end -end diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb deleted file mode 100644 index db70f1998..000000000 --- a/app/lib/ostatus/activity/base.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Base - include Redisable - - def initialize(xml, account = nil, **options) - @xml = xml - @account = account - @options = options - end - - def status? - [:activity, :note, :comment].include?(type) - end - - def verb - raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def type - raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::TYPES.key(raw) - rescue - :activity - end - - def id - @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def url - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } - link.nil? ? nil : link['href'] - end - - def activitypub_uri - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } - link.nil? ? nil : link['href'] - end - - def activitypub_uri? - activitypub_uri.present? - end - - private - - def find_status(uri) - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status') - return Status.find_by(id: local_id) - elsif ActivityPub::TagManager.instance.local_uri?(uri) - local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri) - return Status.find_by(id: local_id) - end - - Status.find_by(uri: uri) - end - - def find_activitypub_status(uri, href) - tag_matches = /tag:([^,:]+)[^:]*:objectId=([\d]+)/.match(uri) - href_matches = %r{/users/([^/]+)}.match(href) - - unless tag_matches.nil? || href_matches.nil? - uri = "https://#{tag_matches[1]}/users/#{href_matches[1]}/statuses/#{tag_matches[2]}" - Status.find_by(uri: uri) - end - end -end diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb deleted file mode 100644 index 60de712db..000000000 --- a/app/lib/ostatus/activity/creation.rb +++ /dev/null @@ -1,219 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Creation < OStatus::Activity::Base - def perform - if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") - Rails.logger.debug "Delete for status #{id} was queued, ignoring" - return [nil, false] - end - - return [nil, false] if @account.suspended? || invalid_origin? - - RedisLock.acquire(lock_options) do |lock| - if lock.acquired? - # Return early if status already exists in db - @status = find_status(id) - return [@status, false] unless @status.nil? - @status = process_status - else - raise Mastodon::RaceConditionError - end - end - - [@status, true] - end - - def process_status - Rails.logger.debug "Creating remote status #{id}" - cached_reblog = reblog - status = nil - - # Skip if the reblogged status is not public - return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?) - - media_attachments = save_media.take(4) - - ApplicationRecord.transaction do - status = Status.create!( - uri: id, - url: url, - account: @account, - reblog: cached_reblog, - text: content, - spoiler_text: content_warning, - created_at: published, - override_timestamps: @options[:override_timestamps], - reply: thread?, - language: content_language, - visibility: visibility_scope, - conversation: find_or_create_conversation, - thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id), - sensitive: sensitive? - ) - - save_mentions(status) - save_hashtags(status) - save_emojis(status) - end - - if thread? && status.thread.nil? && Request.valid_url?(thread.second) - Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}" - ThreadResolveWorker.perform_async(status.id, thread.second) - end - - Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" - - LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - - # Only continue if the status is supposed to have arrived in real-time. - # Note that if @options[:override_timestamps] isn't set, the status - # may have a lower snowflake id than other existing statuses, potentially - # "hiding" it from paginated API calls - return status unless @options[:override_timestamps] || status.within_realtime_window? - - DistributionWorker.perform_async(status.id) - - status - end - - def content - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content - end - - def content_language - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en' - end - - def content_warning - @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || '' - end - - def visibility_scope - @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public - end - - def published - @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content - end - - def thread? - !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil? - end - - def thread - thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS) - [thr['ref'], thr['href']] - end - - private - - def sensitive? - # OStatus-specific convention (not standard) - @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } - end - - def find_or_create_conversation - uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content - return if uri.nil? - - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') - return Conversation.find_by(id: local_id) - end - - Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) - end - - def save_mentions(parent) - processed_account_ids = [] - - @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type'] - - mentioned_account = account_from_href(link['href']) - - next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id) - - mentioned_account.mentions.where(status: parent).first_or_create(status: parent) - - # So we can skip duplicate mentions - processed_account_ids << mentioned_account.id - end - end - - def save_hashtags(parent) - tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) - ProcessHashtagsService.new.call(parent, tags) - end - - def save_media - do_not_download = DomainBlock.reject_media?(@account.domain) - media_attachments = [] - - @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] - - media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href']) - parsed_url = Addressable::URI.parse(link['href']).normalize - - next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? - - media.save - media_attachments << media - - next if do_not_download - - begin - media.file_remote_url = link['href'] - media.save! - rescue ActiveRecord::RecordInvalid - next - end - end - - media_attachments - end - - def save_emojis(parent) - do_not_download = DomainBlock.reject_media?(parent.account.domain) - - return if do_not_download - - @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end - - def account_from_href(href) - url = Addressable::URI.parse(href).normalize - - if TagManager.instance.web_domain?(url.host) - Account.find_local(url.path.gsub('/users/', '')) - else - Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) - end - end - - def invalid_origin? - return false unless id.start_with?('http') # Legacy IDs cannot be checked - - needle = Addressable::URI.parse(id).normalized_host - - !(needle.casecmp(@account.domain).zero? || - needle.casecmp(Addressable::URI.parse(@account.remote_url.presence || @account.uri).normalized_host).zero?) - end - - def lock_options - { redis: Redis.current, key: "create:#{id}" } - end -end diff --git a/app/lib/ostatus/activity/deletion.rb b/app/lib/ostatus/activity/deletion.rb deleted file mode 100644 index c98f5ee0a..000000000 --- a/app/lib/ostatus/activity/deletion.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Deletion < OStatus::Activity::Base - def perform - Rails.logger.debug "Deleting remote status #{id}" - - status = Status.find_by(uri: id, account: @account) - status ||= Status.find_by(uri: activitypub_uri, account: @account) if activitypub_uri? - - if status.nil? - redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) - else - RemoveStatusService.new.call(status) - end - end -end diff --git a/app/lib/ostatus/activity/general.rb b/app/lib/ostatus/activity/general.rb deleted file mode 100644 index 8a6aabc33..000000000 --- a/app/lib/ostatus/activity/general.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::General < OStatus::Activity::Base - def specialize - special_class&.new(@xml, @account, @options) - end - - private - - def special_class - case verb - when :post - OStatus::Activity::Post - when :share - OStatus::Activity::Share - when :delete - OStatus::Activity::Deletion - end - end -end diff --git a/app/lib/ostatus/activity/post.rb b/app/lib/ostatus/activity/post.rb deleted file mode 100644 index 755ed8656..000000000 --- a/app/lib/ostatus/activity/post.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Post < OStatus::Activity::Creation - def perform - status, just_created = super - - if just_created - status.mentions.includes(:account).each do |mention| - mentioned_account = mention.account - next unless mentioned_account.local? - NotifyService.new.call(mentioned_account, mention) - end - end - - status - end - - private - - def reblog - nil - end -end diff --git a/app/lib/ostatus/activity/remote.rb b/app/lib/ostatus/activity/remote.rb deleted file mode 100644 index 5b204b6d8..000000000 --- a/app/lib/ostatus/activity/remote.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Remote < OStatus::Activity::Base - def perform - if activitypub_uri? - find_status(activitypub_uri) || FetchRemoteStatusService.new.call(url) - else - find_status(id) || FetchRemoteStatusService.new.call(url) - end - end -end diff --git a/app/lib/ostatus/activity/share.rb b/app/lib/ostatus/activity/share.rb deleted file mode 100644 index 5ca601415..000000000 --- a/app/lib/ostatus/activity/share.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Share < OStatus::Activity::Creation - def perform - return if reblog.nil? - - status, just_created = super - NotifyService.new.call(reblog.account, status) if reblog.account.local? && just_created - status - end - - def object - @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS) - end - - private - - def reblog - return @reblog if defined? @reblog - - original_status = OStatus::Activity::Remote.new(object).perform - return if original_status.nil? - - @reblog = original_status.reblog? ? original_status.reblog : original_status - end -end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 9a05d96cf..f5c0e85ca 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -53,8 +53,6 @@ class OStatus::AtomSerializer append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - append_element(feed, 'link', nil, rel: :hub, href: api_push_url) - append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id)) stream_entries.each do |stream_entry| feed << entry(stream_entry) diff --git a/app/models/account.rb b/app/models/account.rb index c588451fc..d6772eb98 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -164,8 +164,7 @@ class Account < ApplicationRecord end def refresh! - return if local? - ResolveAccountService.new.call(acct) + ResolveAccountService.new.call(acct) unless local? end def silenced? diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 8c0b07702..4220f697e 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'salmon', href: api_salmon_url(object.id) }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 29b8700c7..49bef727e 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -11,25 +11,17 @@ class AuthorizeFollowService < BaseService follow_request.authorize! end - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? follow_request end private def create_notification(follow_request) - if follow_request.account.ostatus? - NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) - elsif follow_request.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) - end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index e328b1739..cb66debc8 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class BatchedRemoveStatusService < BaseService - include StreamEntryRenderer include Redisable # Delete given statuses and reblogs of them @@ -18,10 +17,7 @@ class BatchedRemoveStatusService < BaseService @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } - @stream_entry_batches = [] - @salmon_batches = [] - @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } - @activity_xml = {} + @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } # Ensure that rendered XML reflects destroyed state statuses.each do |status| @@ -39,28 +35,16 @@ class BatchedRemoveStatusService < BaseService unpush_from_home_timelines(account, account_statuses) unpush_from_list_timelines(account, account_statuses) - - batch_stream_entries(account, account_statuses) if account.local? end # Cannot be batched statuses.each do |status| unpush_from_public_timelines(status) - batch_salmon_slaps(status) if status.local? end - - Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch } - NotificationWorker.push_bulk(@salmon_batches) { |batch| batch } end private - def batch_stream_entries(account, statuses) - statuses.each do |status| - @stream_entry_batches << [build_xml(status.stream_entry), account.id] - end - end - def unpush_from_home_timelines(account, statuses) recipients = account.followers_for_local_distribution.to_a @@ -101,20 +85,4 @@ class BatchedRemoveStatusService < BaseService end end end - - def batch_salmon_slaps(status) - return if @mentions[status.id].empty? - - recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id) - - recipients.each do |recipient_id| - @salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id] - end - end - - def build_xml(stream_entry) - return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) - - @activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry) - end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index c6eef04d4..c5e5e5761 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -44,7 +44,6 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account| - UnsubscribeService.new.call(account) if account.subscribed? SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at) end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 9050a4858..da06361c2 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -13,25 +13,17 @@ class BlockService < BaseService block = account.block!(target_account) BlockWorker.perform_async(account.id, target_account.id) - create_notification(block) unless target_account.local? + create_notification(block) if !target_account.local? && target_account.activitypub? block end private def create_notification(block) - if block.target_account.ostatus? - NotificationWorker.perform_async(build_xml(block), block.account_id, block.target_account_id) - elsif block.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) end def build_json(block) Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block)) - end end diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb deleted file mode 100644 index c2419e9ec..000000000 --- a/app/services/concerns/author_extractor.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module AuthorExtractor - def author_from_xml(xml, update_profile = true) - return nil if xml.nil? - - # Try for acct - acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: OStatus::TagManager::XMLNS)&.content - - # Try + - if acct.blank? - username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: OStatus::TagManager::XMLNS)&.content - uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: OStatus::TagManager::XMLNS)&.content - - return nil if username.blank? || uri.blank? - - domain = Addressable::URI.parse(uri).normalized_host - acct = "#{username}@#{domain}" - end - - ResolveAccountService.new.call(acct, update_profile: update_profile) - end -end diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb deleted file mode 100644 index 9f6c8a082..000000000 --- a/app/services/concerns/stream_entry_renderer.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module StreamEntryRenderer - def stream_entry_to_xml(stream_entry) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(stream_entry, true)) - end -end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 128a24ad6..02b26458a 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -30,8 +30,6 @@ class FavouriteService < BaseService if status.account.local? NotifyService.new.call(status.account, favourite) - elsif status.account.ostatus? - NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) elsif status.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end @@ -46,8 +44,4 @@ class FavouriteService < BaseService def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite)) - end end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index cfc560022..a7f95603d 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteAccountService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteAccountService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false) - - UpdateRemoteProfileService.new.call(xml, account) if account.present? && trusted_domain?(url, account) - - account - rescue TypeError - Rails.logger.debug "Unparseable URL given: #{url}" - nil - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def trusted_domain?(url, account) - domain = Addressable::URI.parse(url).normalized_host - domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 9c3008035..aac39dfd5 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteStatusService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteStatusService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - Rails.logger.debug "Processing Atom for remote status at #{url}" - - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - domain = Addressable::URI.parse(url).normalized_host - - return nil unless !account.nil? && confirmed_domain?(domain, account) - - statuses = ProcessFeedService.new.call(prefetched_body, account) - statuses.first - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def confirmed_domain?(domain, account) - account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 0305e2d62..8e118f5d3 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -13,7 +13,7 @@ class FollowService < BaseService target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? + raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) if source_account.following?(target_account) # We're already following this account, but we'll call follow! again to @@ -32,7 +32,7 @@ class FollowService < BaseService if target_account.locked? || target_account.activitypub? request_follow(source_account, target_account, reblogs: reblogs) - else + elsif target_account.local? direct_follow(source_account, target_account, reblogs: reblogs) end end @@ -44,9 +44,6 @@ class FollowService < BaseService if target_account.local? LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name) - elsif target_account.ostatus? - NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id) - AfterRemoteFollowRequestWorker.perform_async(follow_request.id) elsif target_account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url) end @@ -57,27 +54,12 @@ class FollowService < BaseService def direct_follow(source_account, target_account, reblogs: true) follow = source_account.follow!(target_account, reblogs: reblogs) - if target_account.local? - LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) - else - Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed? - NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id) - AfterRemoteFollowWorker.perform_async(follow.id) - end - + LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) MergeWorker.perform_async(target_account.id, source_account.id) follow end - def build_follow_request_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) - end - - def build_follow_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow)) - end - def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 7830aee11..34ec6d504 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -88,7 +88,6 @@ class PostStatusService < BaseService def postprocess_status! LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text? DistributionWorker.perform_async(@status.id) - Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id) ActivityPub::DistributionWorker.perform_async(@status.id) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb deleted file mode 100644 index 30a9dd85e..000000000 --- a/app/services/process_feed_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class ProcessFeedService < BaseService - def call(body, account, **options) - @options = options - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - update_author(body, account) - process_entries(xml, account) - end - - private - - def update_author(body, account) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - end - - def process_entries(xml, account) - xml.xpath('//xmlns:entry', xmlns: OStatus::TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact - end - - def process_entry(xml, account) - activity = OStatus::Activity::General.new(xml, account, @options) - activity.specialize&.perform if activity.status? - rescue ActiveRecord::RecordInvalid => e - Rails.logger.debug "Nothing was saved for #{activity.id} because: #{e}" - nil - end -end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb deleted file mode 100644 index 1fca3832b..000000000 --- a/app/services/process_interaction_service.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -class ProcessInteractionService < BaseService - include AuthorExtractor - include Authorization - - # Record locally the remote interaction with our user - # @param [String] envelope Salmon envelope - # @param [Account] target_account Account the Salmon was addressed to - def call(envelope, target_account) - body = salmon.unpack(envelope) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - return if account.nil? || account.suspended? - - if salmon.verify(envelope, account.keypair) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - - case verb(xml) - when :follow - follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :request_friend - follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :authorize - authorize_follow_request!(account, target_account) - when :reject - reject_follow_request!(account, target_account) - when :unfollow - unfollow!(account, target_account) - when :favorite - favourite!(xml, account) - when :unfavorite - unfavourite!(xml, account) - when :post - add_post!(body, account) if mentions_account?(xml, target_account) - when :share - add_post!(body, account) unless status(xml).nil? - when :delete - delete_post!(xml, account) - when :block - reflect_block!(account, target_account) - when :unblock - reflect_unblock!(account, target_account) - end - end - rescue HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError - nil - end - - private - - def mentions_account?(xml, account) - xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each { |mention_link| return true if [OStatus::TagManager.instance.uri_for(account), OStatus::TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) } - false - end - - def verb(xml) - raw = xml.at_xpath('//activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def follow!(account, target_account) - follow = account.follow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - NotifyService.new.call(target_account, follow) - end - - def follow_request!(account, target_account) - return if account.requested?(target_account) - - follow_request = FollowRequest.create!(account: account, target_account: target_account) - NotifyService.new.call(target_account, follow_request) - end - - def authorize_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.authorize! - Pubsubhubbub::SubscribeWorker.perform_async(account.id) unless account.subscribed? - end - - def reject_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.reject! - end - - def unfollow!(account, target_account) - account.unfollow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - end - - def reflect_block!(account, target_account) - UnfollowService.new.call(target_account, account) if target_account.following?(account) - account.block!(target_account) - end - - def reflect_unblock!(account, target_account) - UnblockService.new.call(account, target_account) - end - - def delete_post!(xml, account) - status = Status.find(xml.at_xpath('//xmlns:id', xmlns: OStatus::TagManager::XMLNS).content) - - return if status.nil? - - authorize_with account, status, :destroy? - - RemovalWorker.perform_async(status.id) - end - - def favourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account) - NotifyService.new.call(current_status.account, favourite) - end - - def unfavourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first - favourite&.destroy - end - - def add_post!(body, account) - ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8')) - end - - def status(xml) - uri = activity_id(xml) - return nil unless OStatus::TagManager.instance.local_id?(uri) - Status.find(OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')) - end - - def activity_id(xml) - xml.at_xpath('//activity:object', activity: OStatus::TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index bc607dff3..da52bff6a 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ProcessMentionsService < BaseService - include StreamEntryRenderer include Payloadable # Scan status for mentions and fetch remote mentioned users, create @@ -49,17 +48,11 @@ class ProcessMentionsService < BaseService if mentioned_account.local? LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name) - elsif mentioned_account.ostatus? && !@status.stream_entry.hidden? - NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id) elsif mentioned_account.activitypub? ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) end end - def ostatus_xml - @ostatus_xml ||= stream_entry_to_xml(@status.stream_entry) - end - def activitypub_json return @activitypub_json if defined?(@activitypub_json) @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account)) diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb deleted file mode 100644 index 550da6328..000000000 --- a/app/services/pubsubhubbub/subscribe_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::SubscribeService < BaseService - URL_PATTERN = /\A#{URI.regexp(%w(http https))}\z/ - - attr_reader :account, :callback, :secret, - :lease_seconds, :domain - - def call(account, callback, secret, lease_seconds, verified_domain = nil) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - @secret = secret - @lease_seconds = lease_seconds - @domain = verified_domain - - process_subscribe - end - - private - - def process_subscribe - if account.nil? - ['Invalid topic URL', 422] - elsif !valid_callback? - ['Invalid callback URL', 422] - elsif blocked_domain? - ['Callback URL not allowed', 403] - else - confirm_subscription - ['', 202] - end - end - - def confirm_subscription - subscription = locate_subscription - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds) - end - - def valid_callback? - callback.present? && callback =~ URL_PATTERN - end - - def blocked_domain? - DomainBlock.blocked? Addressable::URI.parse(callback).host - end - - def locate_subscription - subscription = Subscription.find_or_initialize_by(account: account, callback_url: callback) - subscription.domain = domain - subscription.save! - subscription - end -end diff --git a/app/services/pubsubhubbub/unsubscribe_service.rb b/app/services/pubsubhubbub/unsubscribe_service.rb deleted file mode 100644 index 646150f7b..000000000 --- a/app/services/pubsubhubbub/unsubscribe_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::UnsubscribeService < BaseService - attr_reader :account, :callback - - def call(account, callback) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - - process_unsubscribe - end - - private - - def process_unsubscribe - if account.nil? - ['Invalid topic URL', 422] - else - confirm_unsubscribe unless subscription.nil? - ['', 202] - end - end - - def confirm_unsubscribe - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe') - end - - def subscription - @_subscription ||= Subscription.find_by(account: account, callback_url: callback) - end -end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 9cf4bc128..3bb460fca 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -2,7 +2,6 @@ class ReblogService < BaseService include Authorization - include StreamEntryRenderer include Payloadable # Reblog a status and notify its remote author @@ -24,7 +23,6 @@ class ReblogService < BaseService reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) DistributionWorker.perform_async(reblog.id) - Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) ActivityPub::DistributionWorker.perform_async(reblog.id) create_notification(reblog) @@ -40,8 +38,6 @@ class ReblogService < BaseService if reblogged_status.account.local? LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name) - elsif reblogged_status.account.ostatus? - NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), reblog.account_id, reblogged_status.account_id) elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account) ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index f87d0ba91..bc0000c8c 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -6,25 +6,17 @@ class RejectFollowService < BaseService def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.reject! - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? follow_request end private def create_notification(follow_request) - if follow_request.account.ostatus? - NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) - elsif follow_request.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) - end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 81adc5aae..a8c9100b3 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RemoveStatusService < BaseService - include StreamEntryRenderer include Redisable include Payloadable @@ -78,11 +77,6 @@ class RemoveStatusService < BaseService target_accounts << @status.reblog.account if @status.reblog? && !@status.reblog.account.local? target_accounts.uniq!(&:id) - # Ostatus - NotificationWorker.push_bulk(target_accounts.select(&:ostatus?).uniq(&:domain)) do |target_account| - [salmon_xml, @account.id, target_account.id] - end - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:preferred_inbox_url)) do |target_account| [signed_activity_json, @account.id, target_account.preferred_inbox_url] @@ -90,9 +84,6 @@ class RemoveStatusService < BaseService end def remove_from_remote_followers - # OStatus - Pubsubhubbub::RawDistributionWorker.perform_async(salmon_xml, @account.id) - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| [signed_activity_json, @account.id, inbox_url] @@ -111,10 +102,6 @@ class RemoveStatusService < BaseService end end - def salmon_xml - @salmon_xml ||= stream_entry_to_xml(@stream_entry) - end - def signed_activity_json @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account)) end diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index e557706da..0ea31a0d8 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +require_relative '../models/account' + class ResolveAccountService < BaseService - include OStatus2::MagicKey include JsonLdHelper - DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' - # Find or create a local account for a remote user. # When creating, look up the user's webfinger and fetch all # important information from their feed @@ -48,18 +47,16 @@ class ResolveAccountService < BaseService return end - return if links_missing? || auto_suspend? return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) + return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) - if activitypub_ready? || @account&.activitypub? - handle_activitypub - else - handle_ostatus - end + next unless @account.nil? || @account.activitypub? + + handle_activitypub else raise Mastodon::RaceConditionError end @@ -73,38 +70,12 @@ class ResolveAccountService < BaseService private - def links_missing? - !(activitypub_ready? || ostatus_ready?) - end - - def ostatus_ready? - !(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? || - @webfinger.link('salmon').nil? || - @webfinger.link('http://webfinger.net/rel/profile-page').nil? || - @webfinger.link('magic-public-key').nil? || - canonical_uri.nil? || - hub_url.nil?) - end - def webfinger_update_due? @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) end def activitypub_ready? - !@webfinger.link('self').nil? && - ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) && - !actor_json.nil? && - actor_json['inbox'].present? - end - - def handle_ostatus - create_account if @account.nil? - update_account - update_account_profile if update_profile? - end - - def update_profile? - @options[:update_profile] + !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) end def handle_activitypub @@ -115,89 +86,10 @@ class ResolveAccountService < BaseService nil end - def create_account - Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" - - @account = Account.new(username: @username, domain: @domain) - @account.suspended_at = domain_block.created_at if auto_suspend? - @account.silenced_at = domain_block.created_at if auto_silence? - @account.private_key = nil - end - - def update_account - @account.last_webfingered_at = Time.now.utc - @account.protocol = :ostatus - @account.remote_url = atom_url - @account.salmon_url = salmon_url - @account.url = url - @account.public_key = public_key - @account.uri = canonical_uri - @account.hub_url = hub_url - @account.save! - end - - def auto_suspend? - domain_block&.suspend? - end - - def auto_silence? - domain_block&.silence? - end - - def domain_block - return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.rule_for(@domain) - end - - def atom_url - @atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href - end - - def salmon_url - @salmon_url ||= @webfinger.link('salmon').href - end - def actor_url @actor_url ||= @webfinger.link('self').href end - def url - @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href - end - - def public_key - @public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href) - end - - def canonical_uri - return @canonical_uri if defined?(@canonical_uri) - - author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri') - - if author_uri.nil? - owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS) - author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil? - end - - @canonical_uri = author_uri.nil? ? nil : author_uri.content - end - - def hub_url - return @hub_url if defined?(@hub_url) - - hubs = atom.xpath('//xmlns:link[@rel="hub"]') - @hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href'] - end - - def atom_body - return @atom_body if defined?(@atom_body) - - @atom_body = Request.new(:get, atom_url).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response.code == 200 - response.body_with_limit - end - end - def actor_json return @actor_json if defined?(@actor_json) @@ -205,15 +97,6 @@ class ResolveAccountService < BaseService @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil end - def atom - return @atom if defined?(@atom) - @atom = Nokogiri::XML(atom_body) - end - - def update_account_profile - RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false) - end - def lock_options { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" } end diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb deleted file mode 100644 index 3419043e5..000000000 --- a/app/services/send_interaction_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class SendInteractionService < BaseService - # Send an Atom representation of an interaction to a remote Salmon endpoint - # @param [String] Entry XML - # @param [Account] source_account - # @param [Account] target_account - def call(xml, source_account, target_account) - @xml = xml - @source_account = source_account - @target_account = target_account - - return if !target_account.ostatus? || block_notification? - - build_request.perform do |delivery| - raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300 - end - end - - private - - def build_request - request = Request.new(:post, @target_account.salmon_url, body: envelope) - request.add_headers('Content-Type' => 'application/magic-envelope+xml') - request - end - - def envelope - salmon.pack(@xml, @source_account.keypair) - end - - def block_notification? - DomainBlock.blocked?(@target_account.domain) - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb deleted file mode 100644 index 83fd64396..000000000 --- a/app/services/subscribe_service.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -class SubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - @account.secret = SecureRandom.hex - - build_request.perform do |response| - if response_failed_permanently? response - # We're not allowed to subscribe. Fail and move on. - @account.secret = '' - @account.save! - elsif response_successful? response - # The subscription will be confirmed asynchronously. - @account.save! - else - # The response was either a 429 rate limit, or a 5xx error. - # We need to retry at a later time. Fail loudly! - raise Mastodon::UnexpectedResponseError, response - end - end - end - - private - - def build_request - request = Request.new(:post, @account.hub_url, form: subscription_params) - request.on_behalf_of(some_local_account) if some_local_account - request - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'subscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - 'hub.secret': @account.secret, - 'hub.lease_seconds': 7.days.seconds, - } - end - - def some_local_account - @some_local_account ||= Account.local.without_suspended.first - end - - # Any response in the 3xx or 4xx range, except for 429 (rate limit) - def response_failed_permanently?(response) - (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests? - end - - # Any response in the 2xx range - def response_successful?(response) - response.status.success? - end -end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index 95a858e9f..c263ac8af 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -7,25 +7,17 @@ class UnblockService < BaseService return unless account.blocking?(target_account) unblock = account.unblock!(target_account) - create_notification(unblock) unless target_account.local? + create_notification(unblock) if !target_account.local? && target_account.activitypub? unblock end private def create_notification(unblock) - if unblock.target_account.ostatus? - NotificationWorker.perform_async(build_xml(unblock), unblock.account_id, unblock.target_account_id) - elsif unblock.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) end def build_json(unblock) Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block)) - end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index dcc890b7d..37917a64f 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -6,7 +6,7 @@ class UnfavouriteService < BaseService def call(account, status) favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! - create_notification(favourite) unless status.local? + create_notification(favourite) if !status.account.local? && status.account.activitypub? favourite end @@ -14,19 +14,10 @@ class UnfavouriteService < BaseService def create_notification(favourite) status = favourite.status - - if status.account.ostatus? - NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) - elsif status.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite)) - end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 17dc29735..b7033d7eb 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -21,8 +21,8 @@ class UnfollowService < BaseService return unless follow follow.destroy! - create_notification(follow) unless @target_account.local? - create_reject_notification(follow) if @target_account.local? && !@source_account.local? + create_notification(follow) if !@target_account.local? && @target_account.activitypub? + create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub? UnmergeWorker.perform_async(@target_account.id, @source_account.id) follow end @@ -38,16 +38,10 @@ class UnfollowService < BaseService end def create_notification(follow) - if follow.target_account.ostatus? - NotificationWorker.perform_async(build_xml(follow), follow.account_id, follow.target_account_id) - elsif follow.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) end def create_reject_notification(follow) - # Rejecting an already-existing follow request - return unless follow.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url) end @@ -58,8 +52,4 @@ class UnfollowService < BaseService def build_reject_json(follow) Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow)) - end end diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb deleted file mode 100644 index 95c1fb4fc..000000000 --- a/app/services/unsubscribe_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class UnsubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - - begin - build_request.perform do |response| - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success? - end - rescue HTTP::Error, OpenSSL::SSL::SSLError => e - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}" - end - - @account.secret = '' - @account.subscription_expires_at = nil - @account.save! - end - - private - - def build_request - Request.new(:post, @account.hub_url, form: subscription_params) - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'unsubscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - } - end -end diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb deleted file mode 100644 index 403395a0d..000000000 --- a/app/services/update_remote_profile_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class UpdateRemoteProfileService < BaseService - attr_reader :account, :remote_profile - - def call(body, account, resubscribe = false) - @account = account - @remote_profile = RemoteProfile.new(body) - - return if remote_profile.root.nil? - - update_account unless remote_profile.author.nil? - - old_hub_url = account.hub_url - account.hub_url = remote_profile.hub_link if remote_profile.hub_link.present? && remote_profile.hub_link != old_hub_url - - account.save_with_optional_media! - - Pubsubhubbub::SubscribeWorker.perform_async(account.id) if resubscribe && account.hub_url != old_hub_url - end - - private - - def update_account - account.display_name = remote_profile.display_name || '' - account.note = remote_profile.note || '' - account.locked = remote_profile.locked? - - if !account.suspended? && !DomainBlock.reject_media?(account.domain) - if remote_profile.avatar.present? - account.avatar_remote_url = remote_profile.avatar - else - account.avatar_remote_url = '' - account.avatar.destroy - end - - if remote_profile.header.present? - account.header_remote_url = remote_profile.header - else - account.header_remote_url = '' - account.header.destroy - end - - save_emojis if remote_profile.emojis.present? - end - end - - def save_emojis - do_not_download = DomainBlock.reject_media?(account.domain) - - return if do_not_download - - remote_profile.emojis.each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end -end diff --git a/app/services/verify_salmon_service.rb b/app/services/verify_salmon_service.rb deleted file mode 100644 index 205b35d8b..000000000 --- a/app/services/verify_salmon_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class VerifySalmonService < BaseService - include AuthorExtractor - - def call(payload) - body = salmon.unpack(payload) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - if account.nil? - false - else - salmon.verify(payload, account.keypair) - end - end - - private - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 950e61847..de7d2a8ba 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,6 @@ - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex' }/ - %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml deleted file mode 100644 index 1dec8e396..000000000 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%tr - %td - %samp= subscription.account.acct - %td - %samp= subscription.callback_url - %td - - if subscription.confirmed? - %i.fa.fa-check - %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) - %td - - if subscription.last_successful_delivery_at? - %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } - = l subscription.last_successful_delivery_at - - else - %i.fa.fa-times diff --git a/app/views/admin/subscriptions/index.html.haml b/app/views/admin/subscriptions/index.html.haml deleted file mode 100644 index 83704c8ee..000000000 --- a/app/views/admin/subscriptions/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = t('admin.subscriptions.title') - -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.subscriptions.topic') - %th= t('admin.subscriptions.callback_url') - %th= t('admin.subscriptions.confirmed') - %th= t('admin.subscriptions.expires_in') - %th= t('admin.subscriptions.last_delivery') - %tbody - = render @subscriptions - -= paginate @subscriptions diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index 968c8c138..c82cdb7b3 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd| link['href'] = account_url(@account) end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'salmon' - link['href'] = api_salmon_url(@account.id) - end - xrd << Ox::Element.new('Link').tap do |link| link['rel'] = 'magic-public-key' link['href'] = "data:application/magic-public-key,#{@account.magic_key}" diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb index 84eb6ade2..ce9c65834 100644 --- a/app/workers/after_remote_follow_request_worker.rb +++ b/app/workers/after_remote_follow_request_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowRequestWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow_request - - def perform(follow_request_id) - @follow_request = FollowRequest.find(follow_request_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow_request.destroy - FollowService.new.call(follow_request.account, updated_account.acct) - end - - def processing_required? - !updated_account.nil? && !updated_account.locked? - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow_request.target_account.remote_url) - end + def perform(follow_request_id); end end diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb index edab83f85..d9719f2bf 100644 --- a/app/workers/after_remote_follow_worker.rb +++ b/app/workers/after_remote_follow_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow - - def perform(follow_id) - @follow = Follow.find(follow_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow.destroy - FollowService.new.call(follow.account, updated_account.acct) - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow.target_account.remote_url) - end - - def processing_required? - !updated_account.nil? && updated_account.locked? - end + def perform(follow_id); end end diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb index da1d6ab45..1c0f001cf 100644 --- a/app/workers/notification_worker.rb +++ b/app/workers/notification_worker.rb @@ -5,7 +5,5 @@ class NotificationWorker sidekiq_options queue: 'push', retry: 5 - def perform(xml, source_account_id, target_account_id) - SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) - end + def perform(xml, source_account_id, target_account_id); end end diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb index 978c3aba2..cf3bd8397 100644 --- a/app/workers/processing_worker.rb +++ b/app/workers/processing_worker.rb @@ -5,7 +5,5 @@ class ProcessingWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true) - end + def perform(account_id, body); end end diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb index c0e7b677e..783a8c95f 100644 --- a/app/workers/pubsubhubbub/confirmation_worker.rb +++ b/app/workers/pubsubhubbub/confirmation_worker.rb @@ -2,81 +2,8 @@ class Pubsubhubbub::ConfirmationWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: false - attr_reader :subscription, :mode, :secret, :lease_seconds - - def perform(subscription_id, mode, secret = nil, lease_seconds = nil) - @subscription = Subscription.find(subscription_id) - @mode = mode - @secret = secret - @lease_seconds = lease_seconds - process_confirmation - end - - private - - def process_confirmation - prepare_subscription - - callback_get_with_params - logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{@callback_response_body}" - - update_subscription - end - - def update_subscription - if successful_subscribe? - subscription.save! - elsif successful_unsubscribe? - subscription.destroy! - end - end - - def successful_subscribe? - subscribing? && response_matches_challenge? - end - - def successful_unsubscribe? - (unsubscribing? && response_matches_challenge?) || !subscription.confirmed? - end - - def response_matches_challenge? - @callback_response_body == challenge - end - - def subscribing? - mode == 'subscribe' - end - - def unsubscribing? - mode == 'unsubscribe' - end - - def callback_get_with_params - Request.new(:get, subscription.callback_url, params: callback_params).perform do |response| - @callback_response_body = response.body_with_limit - end - end - - def callback_params - { - 'hub.topic': account_url(subscription.account, format: :atom), - 'hub.mode': mode, - 'hub.challenge': challenge, - 'hub.lease_seconds': subscription.lease_seconds, - } - end - - def prepare_subscription - subscription.secret = secret - subscription.lease_seconds = lease_seconds - subscription.confirmed = true - end - - def challenge - @_challenge ||= SecureRandom.hex - end + def perform(subscription_id, mode, secret = nil, lease_seconds = nil); end end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 619bfa48a..1260060bd 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -2,80 +2,8 @@ class Pubsubhubbub::DeliveryWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: 3, dead: false - sidekiq_retry_in do |count| - 5 * (count + 1) - end - - attr_reader :subscription, :payload - - def perform(subscription_id, payload) - @subscription = Subscription.find(subscription_id) - @payload = payload - process_delivery unless blocked_domain? - rescue => e - raise e.class, "Delivery failed for #{subscription&.callback_url}: #{e.message}", e.backtrace[0] - end - - private - - def process_delivery - callback_post_payload do |payload_delivery| - raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful? payload_delivery - end - - subscription.touch(:last_successful_delivery_at) - end - - def callback_post_payload(&block) - request = Request.new(:post, subscription.callback_url, body: payload) - request.add_headers(headers) - request.perform(&block) - end - - def blocked_domain? - DomainBlock.blocked?(host) - end - - def host - Addressable::URI.parse(subscription.callback_url).normalized_host - end - - def headers - { - 'Content-Type' => 'application/atom+xml', - 'Link' => link_header, - }.merge(signature_headers.to_h) - end - - def link_header - LinkHeader.new([hub_link_header, self_link_header]).to_s - end - - def hub_link_header - [api_push_url, [%w(rel hub)]] - end - - def self_link_header - [account_url(subscription.account, format: :atom), [%w(rel self)]] - end - - def signature_headers - { 'X-Hub-Signature' => payload_signature } if subscription.secret? - end - - def payload_signature - "sha1=#{hmac_payload_digest}" - end - - def hmac_payload_digest - OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret, payload) - end - - def response_successful?(payload_delivery) - payload_delivery.code > 199 && payload_delivery.code < 300 - end + def perform(subscription_id, payload); end end diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index fed5e917d..75bac5d6f 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -5,28 +5,5 @@ class Pubsubhubbub::DistributionWorker sidekiq_options queue: 'push' - def perform(stream_entry_ids) - stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.hidden? } - - return if stream_entries.empty? - - @account = stream_entries.first.account - @subscriptions = active_subscriptions.to_a - - distribute_public!(stream_entries) - end - - private - - def distribute_public!(stream_entries) - @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries)) - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription_id| - [subscription_id, @payload] - end - end - - def active_subscriptions - Subscription.where(account: @account).active.pluck(:id) - end + def perform(stream_entry_ids); end end diff --git a/app/workers/pubsubhubbub/raw_distribution_worker.rb b/app/workers/pubsubhubbub/raw_distribution_worker.rb index 16962a623..ece9c80ac 100644 --- a/app/workers/pubsubhubbub/raw_distribution_worker.rb +++ b/app/workers/pubsubhubbub/raw_distribution_worker.rb @@ -5,18 +5,5 @@ class Pubsubhubbub::RawDistributionWorker sidekiq_options queue: 'push' - def perform(xml, source_account_id) - @account = Account.find(source_account_id) - @subscriptions = active_subscriptions.to_a - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription| - [subscription.id, xml] - end - end - - private - - def active_subscriptions - Subscription.where(account: @account).active.select('id, callback_url, domain') - end + def perform(xml, source_account_id); end end diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 2e176d1c1..b861b5e67 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -5,30 +5,5 @@ class Pubsubhubbub::SubscribeWorker sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false - sidekiq_retry_in do |count| - case count - when 0 - 30.minutes.seconds - when 1 - 2.hours.seconds - when 2 - 12.hours.seconds - else - 24.hours.seconds * (count - 2) - end - end - - sidekiq_retries_exhausted do |msg, _e| - account = Account.find(msg['args'].first) - Sidekiq.logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing" - ::UnsubscribeService.new.call(account) - end - - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH re-subscribing to #{account.acct}" - ::SubscribeService.new.call(account) - rescue => e - raise e.class, "Subscribe failed for #{account&.acct}: #{e.message}", e.backtrace[0] - end + def perform(account_id); end end diff --git a/app/workers/pubsubhubbub/unsubscribe_worker.rb b/app/workers/pubsubhubbub/unsubscribe_worker.rb index a271715b7..0c1c263f6 100644 --- a/app/workers/pubsubhubbub/unsubscribe_worker.rb +++ b/app/workers/pubsubhubbub/unsubscribe_worker.rb @@ -5,11 +5,5 @@ class Pubsubhubbub::UnsubscribeWorker sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH unsubscribing from #{account.acct}" - ::UnsubscribeService.new.call(account) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id); end end diff --git a/app/workers/remote_profile_update_worker.rb b/app/workers/remote_profile_update_worker.rb index 03585ad2d..01e8daf8f 100644 --- a/app/workers/remote_profile_update_worker.rb +++ b/app/workers/remote_profile_update_worker.rb @@ -5,9 +5,5 @@ class RemoteProfileUpdateWorker sidekiq_options queue: 'pull' - def perform(account_id, body, resubscribe) - UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id, body, resubscribe); end end diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb index d37d40432..10200b06c 100644 --- a/app/workers/salmon_worker.rb +++ b/app/workers/salmon_worker.rb @@ -5,9 +5,5 @@ class SalmonWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessInteractionService.new.call(body, Account.find(account_id)) - rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound - true - end + def perform(account_id, body); end end diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb index d5873bccb..6903cadc7 100644 --- a/app/workers/scheduler/subscriptions_scheduler.rb +++ b/app/workers/scheduler/subscriptions_scheduler.rb @@ -5,13 +5,5 @@ class Scheduler::SubscriptionsScheduler sidekiq_options unique: :until_executed, retry: 0 - def perform - Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id)) - end - - private - - def expiring_accounts - Account.expiring(1.day.from_now).partitioned - end + def perform; end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d4f1855aa..611f36fdd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -469,13 +469,6 @@ en: no_status_selected: No statuses were changed as none were selected title: Account statuses with_media: With media - subscriptions: - callback_url: Callback URL - confirmed: Confirmed - expires_in: Expires in - last_delivery: Last delivery - title: WebSub - topic: Topic tags: accounts: Accounts hidden: Hidden diff --git a/config/navigation.rb b/config/navigation.rb index df1024189..ef845d1fc 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation| s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} - s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 9ab5ba7f0..4b6d464c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,7 +154,6 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :subscriptions, only: [:index] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] @@ -191,8 +190,6 @@ Rails.application.routes.draw do resources :accounts, only: [:index, :show] do member do - post :subscribe - post :unsubscribe post :enable post :unsilence post :unsuspend @@ -257,16 +254,6 @@ Rails.application.routes.draw do get '/admin', to: redirect('/admin/dashboard', status: 302) namespace :api do - # PubSubHubbub outgoing subscriptions - resources :subscriptions, only: [:show] - post '/subscriptions/:id', to: 'subscriptions#update' - - # PubSubHubbub incoming subscriptions - post '/push', to: 'push#update', as: :push - - # Salmon - post '/salmon/:id', to: 'salmon#update', as: :salmon - # OEmbed get '/oembed', to: 'oembed#show', as: :oembed @@ -318,7 +305,6 @@ Rails.application.routes.draw do get '/search', to: 'search#index', as: :search - resources :follows, only: [:create] resources :media, only: [:create, :update] resources :blocks, only: [:index] resources :mutes, only: [:index] diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 0ec1742ab..a16dea967 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,9 +9,6 @@ scheduled_statuses_scheduler: every: '5m' class: Scheduler::ScheduledStatusesScheduler - subscriptions_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *' - class: Scheduler::SubscriptionsScheduler media_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::MediaCleanupScheduler diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index a348ab3d7..608606ff9 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -75,44 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do end end - describe 'POST #subscribe' do - subject { post :subscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - - describe 'POST #unsubscribe' do - subject { post :unsubscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - describe 'POST #memorialize' do subject { post :memorialize, params: { id: account.id } } diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb deleted file mode 100644 index 967152abe..000000000 --- a/spec/controllers/admin/subscriptions_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe Admin::SubscriptionsController, type: :controller do - render_views - - describe 'GET #index' do - around do |example| - default_per_page = Subscription.default_per_page - Subscription.paginates_per 1 - example.run - Subscription.paginates_per default_per_page - end - - before do - sign_in Fabricate(:user, admin: true), scope: :user - end - - it 'renders subscriptions' do - Fabricate(:subscription) - specified = Fabricate(:subscription) - - get :index - - subscriptions = assigns(:subscriptions) - expect(subscriptions.count).to eq 1 - expect(subscriptions[0]).to eq specified - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb deleted file mode 100644 index d769d8554..000000000 --- a/spec/controllers/api/push_controller_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::PushController, type: :controller do - describe 'POST #update' do - context 'with hub.mode=subscribe' do - it 'creates a subscription' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::SubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'subscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - 'hub.lease_seconds' => '3600', - 'hub.secret' => 'as1234df', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - 'as1234df', - '3600', - nil - ) - expect(response).to have_http_status(202) - end - end - - context 'with hub.mode=unsubscribe' do - it 'unsubscribes the account' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::UnsubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'unsubscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - ) - expect(response).to have_http_status(202) - end - end - - context 'with unknown mode' do - it 'returns an unknown mode error' do - post :update, params: { 'hub.mode' => 'fake' } - - expect(response).to have_http_status(422) - expect(response.body).to match(/Unknown mode/) - end - end - end -end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb deleted file mode 100644 index 235a29af0..000000000 --- a/spec/controllers/api/salmon_controller_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SalmonController, type: :controller do - render_views - - let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account } - - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - end - - describe 'POST #update' do - context 'with valid post data' do - before do - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'contains XML in the request body' do - expect(request.body.read).to be_a String - end - - it 'returns http success' do - expect(response).to have_http_status(202) - end - - it 'creates remote account' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates status' do - expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil - end - - it 'creates mention for target account' do - expect(account.mentions.count).to eq 1 - end - end - - context 'with empty post data' do - before do - post :update, params: { id: account.id }, body: '' - end - - it 'returns http client error' do - expect(response).to have_http_status(400) - end - end - - context 'with invalid post data' do - before do - service = double(call: false) - allow(VerifySalmonService).to receive(:new).and_return(service) - - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'returns http client error' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb deleted file mode 100644 index 7a4252fe6..000000000 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SubscriptionsController, type: :controller do - render_views - - let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') } - - describe 'GET #show' do - context 'with valid subscription' do - before do - get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'echoes back the challenge' do - expect(response.body).to match '456' - end - end - - context 'with invalid subscription' do - before do - expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false) - get :show, params: { :id => account.id } - end - - it 'returns http success' do - expect(response).to have_http_status(404) - end - end - end - - describe 'POST #update' do - let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - before do - stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404) - stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404) - stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404) - stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404) - stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404) - stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404) - stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404) - stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404) - stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404) - - request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}" - - post :update, params: { id: account.id }, body: feed - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates statuses for feed' do - expect(account.statuses.count).to_not eq 0 - end - end -end diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb deleted file mode 100644 index 089e0fe5e..000000000 --- a/spec/controllers/api/v1/follows_controller_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::V1::FollowsController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'POST #create' do - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {}) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {}) - - post :create, params: { uri: 'gargron@quitter.no' } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates account for remote user' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates a follow relation between user and remote user' do - expect(user.account.following?(Account.find_by(username: 'gargron', domain: 'quitter.no'))).to be true - end - - it 'sends a salmon slap to the remote user' do - expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made - end - - it 'subscribes to remote hub' do - expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made - end - - it 'returns http success if already following, too' do - post :create, params: { uri: 'gargron@quitter.no' } - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/fixtures/requests/webfinger.txt b/spec/fixtures/requests/webfinger.txt index edb8a2dbb..f337ecae6 100644 --- a/spec/fixtures/requests/webfinger.txt +++ b/spec/fixtures/requests/webfinger.txt @@ -8,4 +8,4 @@ Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; -{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} \ No newline at end of file +{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 891871c1c..74ab7576f 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -406,28 +406,6 @@ RSpec.describe OStatus::AtomSerializer do scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'public' end - - it 'returns element whose rendered view triggers creation when processed' do - remote_account = Account.create!(username: 'username') - remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true) - entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test - xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test') - - remote_status.destroy! - remote_account.destroy! - - account = Account.create!( - domain: 'remote.test', - username: 'username', - last_webfingered_at: Time.now.utc - ) - - ProcessFeedService.new.call(xml, account) - - expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status - end end context 'if status is not present' do @@ -683,24 +661,6 @@ RSpec.describe OStatus::AtomSerializer do end end - it 'appends link element for hub' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' } - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push' - end - - it 'appends link element for Salmon' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' } - expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/' - end - it 'appends stream entries' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) @@ -784,18 +744,6 @@ RSpec.describe OStatus::AtomSerializer do object = block_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - xml = OStatus::AtomSerializer.render(block_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - block.destroy! - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect(block.account.blocking?(block.target_account)).to be true - end end describe '#unblock_salmon' do @@ -871,17 +819,6 @@ RSpec.describe OStatus::AtomSerializer do object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - xml = OStatus::AtomSerializer.render(unblock_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect { block.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#favourite_salmon' do @@ -964,17 +901,6 @@ RSpec.describe OStatus::AtomSerializer do expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' end - - it 'returns element whose rendered view triggers favourite when processed' do - favourite = Fabricate(:favourite) - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(favourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - favourite.destroy! - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect(favourite.account.favourited?(favourite.status)).to be true - end end describe '#unfavourite_salmon' do @@ -1064,16 +990,6 @@ RSpec.describe OStatus::AtomSerializer do expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' end - - it 'returns element whose rendered view triggers unfavourite when processed' do - favourite = Fabricate(:favourite) - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(unfavourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#follow_salmon' do @@ -1143,18 +1059,6 @@ RSpec.describe OStatus::AtomSerializer do expect(follow_salmon.title.text).to eq 'account started following target_account@remote' expect(follow_salmon.content.text).to eq 'account started following target_account@remote' end - - it 'returns element whose rendered view triggers follow when processed' do - follow = Fabricate(:follow) - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - xml = OStatus::AtomSerializer.render(follow_salmon) - follow.destroy! - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be true - end end describe '#unfollow_salmon' do @@ -1251,19 +1155,6 @@ RSpec.describe OStatus::AtomSerializer do object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers unfollow when processed' do - follow = Fabricate(:follow) - follow.destroy! - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - xml = OStatus::AtomSerializer.render(unfollow_salmon) - follow.account.follow!(follow.target_account) - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be false - end end describe '#follow_request_salmon' do @@ -1294,18 +1185,6 @@ RSpec.describe OStatus::AtomSerializer do follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' end - - it 'returns element whose rendered view triggers follow request when processed' do - follow_request = Fabricate(:follow_request) - follow_request_salmon = serialize(follow_request) - xml = OStatus::AtomSerializer.render(follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair) - follow_request.destroy! - - ProcessInteractionService.new.call(envelope, follow_request.target_account) - - expect(follow_request.account.requested?(follow_request.target_account)).to eq true - end end end @@ -1364,18 +1243,6 @@ RSpec.describe OStatus::AtomSerializer do verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] end - - it 'returns element whose rendered view creates follow from follow request when processed' do - follow_request = Fabricate(:follow_request) - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(authorize_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq true - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#reject_follow_request_salmon' do @@ -1427,18 +1294,6 @@ RSpec.describe OStatus::AtomSerializer do verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] end - - it 'returns element whose rendered view deletes follow request when processed' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(reject_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq false - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#object' do diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 562ef0041..ce56d57a6 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe AuthorizeFollowService, type: :service do it 'creates follow relation' do expect(bob.following?(sender)).to be true end - - it 'sends a follow request authorization salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:authorize]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index e53623449..d52e7f484 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -49,19 +49,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - matches = req.body.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.at_least_once - end - - it 'sends Salmon slap to previously mentioned users' do - expect(a_request(:post, "http://example.com/salmon").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.once - end - it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 6584bb90e..de20dd026 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -28,13 +28,6 @@ RSpec.describe BlockService, type: :service do it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end - - it 'sends a block salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:block]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 0a20ccf6e..4c29ea77b 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe FavouriteService, type: :service do it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end - - it 'sends a salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:favorite]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 3cd86708b..37e9910d4 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -36,36 +36,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - context 'protocol is :ostatus' do - let(:prefetched_body) { xml } - let(:protocol) { :ostatus } - - before do - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - - it 'does not update account information if XML comes from an unverified domain' do - feed_xml = <<-XML.squish - - - - http://activitystrea.ms/schema/1.0/person - http://kickass.zone/users/localhost - localhost - localhost - Villain!!! - - - XML - - returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus) - expect(returned_account.display_name).to_not eq 'Villain!!!' - end - end - context 'when prefetched_body is nil' do context 'protocol is :activitypub' do before do @@ -75,15 +45,5 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - - context 'protocol is :ostatus' do - before do - stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' }) - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - end end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 3c4ec59be..86c85293e 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -96,74 +96,6 @@ RSpec.describe FollowService, type: :service do end end - context 'remote OStatus account' do - describe 'locked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } - - before do - stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) - subject.call(sender, bob.acct) - end - - it 'creates a follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil - end - - it 'sends a follow request salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:request_friend]) - }).to have_been_made.once - end - end - - describe 'unlocked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } - - before do - stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:post, "http://hub.example.com/").to_return(status: 202) - subject.call(sender, bob.acct) - end - - it 'creates a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'sends a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:follow]) - }).to have_been_made.once - end - - it 'subscribes to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once - end - end - - describe 'already followed account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } - - before do - sender.follow!(bob) - subject.call(sender, bob.acct) - end - - it 'keeps a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'does not send a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made - end - - it 'does not subscribe to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made - end - end - end - context 'remote ActivityPub account' do let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 5cf2dadf0..5355133f4 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe ImportService, type: :service do let!(:account) { Fabricate(:account, locked: false) } let!(:bob) { Fabricate(:account, username: 'bob', locked: false) } - let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) } + let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') } + + before do + stub_request(:post, "https://example.com/inbox").to_return(status: 200) + end context 'import old-style list of muted users' do subject { ImportService.new } @@ -95,7 +99,8 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -106,7 +111,8 @@ RSpec.describe ImportService, type: :service do it 'follows the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -117,7 +123,8 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -136,9 +143,10 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, respecting boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -148,9 +156,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -160,9 +169,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index facbe977f..bf06f50e9 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -144,7 +144,6 @@ RSpec.describe PostStatusService, type: :service do it 'gets distributed' do allow(DistributionWorker).to receive(:perform_async) - allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async) allow(ActivityPub::DistributionWorker).to receive(:perform_async) account = Fabricate(:account) @@ -152,7 +151,6 @@ RSpec.describe PostStatusService, type: :service do status = subject.call(account, text: "test status update") expect(DistributionWorker).to have_received(:perform_async).with(status.id) - expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id) expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) end diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb deleted file mode 100644 index 9d3465f3f..000000000 --- a/spec/services/process_feed_service_spec.rb +++ /dev/null @@ -1,252 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessFeedService, type: :service do - subject { ProcessFeedService.new } - - describe 'processing a feed' do - let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } - let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } - - before do - stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404) - stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404) - stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt')) - end - - context 'when domain does not reject media' do - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - expect(account).to have_attached_file(:avatar) - expect(account.avatar_file_name).not_to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'marks replies as replies' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.reply?).to be true - end - - it 'sets account being replied to when possible' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.in_reply_to_account_id).to eq status.account_id - end - - it 'ignores delete statuses unless they existed before' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil - end - - it 'does not create statuses for follows' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil - end - - it 'does not create statuses for favourites' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil - end - - it 'creates posts with media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first).to have_attached_file(:file) - expect(status.media_attachments.first.image?).to be true - expect(status.media_attachments.first.file_file_name).not_to be_nil - end - end - - context 'when domain is set to reject media' do - let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) } - - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - end - - it 'rejects remote user\'s avatar' do - account.reload - expect(account.display_name).to eq '::1' - expect(account.avatar_file_name).to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'creates posts with remote-only media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first.file_file_name).to be_nil - expect(status.media_attachments.first.unknown?).to be true - end - end - end - - it 'does not accept tampered reblogs' do - good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - real_body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' }) - - bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz') - - body = < - - tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://talon.xyz/users/sombra - http://activitystrea.ms/schema/1.0/person - https://talon.xyz/users/sombra - sombra - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch SUCKS AHAHA - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch SUCKS AHAHA - - - -XML - created_statuses = subject.call(body, bad_actor) - - expect(created_statuses.first.reblog?).to be true - expect(created_statuses.first.account_id).to eq bad_actor.id - expect(created_statuses.first.reblog.account_id).to eq good_actor.id - expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks' - end - - it 'ignores reblogs if it failed to retrieve reblogged statuses' do - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) - - actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch rocks - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - - -XML - - created_statuses = subject.call(body, actor) - - expect(created_statuses).to eq [] - end - - it 'ignores statuses with an out-of-order delete' do - sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - delete_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/delete - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - -XML - - status_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - subject.call(delete_body, sender) - created_statuses = subject.call(status_body, sender) - - expect(created_statuses).to be_empty - end -end diff --git a/spec/services/process_interaction_service_spec.rb b/spec/services/process_interaction_service_spec.rb deleted file mode 100644 index b858c19d0..000000000 --- a/spec/services/process_interaction_service_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessInteractionService, type: :service do - let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } - let(:sender) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } - let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') } - - subject { ProcessInteractionService.new } - - describe 'status delete slap' do - let(:remote_status) { Fabricate(:status, account: remote_sender) } - let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) } - let(:payload) { - <<~XML - - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - #{remote_status.id} - http://activitystrea.ms/schema/1.0/delete - - XML - } - - before do - receiver.update(locked: true) - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - end - - it 'deletes a record' do - expect(RemovalWorker).to receive(:perform_async).with(remote_status.id) - subject.call(envelope, receiver) - end - end - - describe 'follow request slap' do - before do - receiver.update(locked: true) - - payload = < - - bob - https://cb6e6126.ngrok.io/users/bob - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request slap from known remote user identified by email' do - before do - receiver.update(locked: true) - # Copy already-generated key - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - - payload = < - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request authorization slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/authorize - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'creates a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end - - describe 'follow request rejection slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/reject - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'does not create a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 8a6bb44ac..35a804f2b 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -18,10 +18,6 @@ RSpec.describe ProcessMentionsService, type: :service do it 'creates a mention' do expect(remote_user.mentions.where(status: status).count).to eq 1 end - - it 'posts to remote user\'s Salmon end point' do - expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once - end end context 'OStatus with private toot' do diff --git a/spec/services/pubsubhubbub/subscribe_service_spec.rb b/spec/services/pubsubhubbub/subscribe_service_spec.rb deleted file mode 100644 index 01c956230..000000000 --- a/spec/services/pubsubhubbub/subscribe_service_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::SubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - let(:user_account) { Fabricate(:account) } - - context 'with a nil account' do - it 'returns the invalid topic status results' do - result = service_call(account: nil) - - expect(result).to eq invalid_topic_status - end - end - - context 'with an invalid callback url' do - it 'returns invalid callback status when callback is blank' do - result = service_call(callback: '') - - expect(result).to eq invalid_callback_status - end - it 'returns invalid callback status when callback is not a URI' do - result = service_call(callback: 'invalid-hostname') - - expect(result).to eq invalid_callback_status - end - end - - context 'with a blocked domain in the callback' do - it 'returns callback not allowed' do - Fabricate(:domain_block, domain: 'test.host', severity: :suspend) - result = service_call(callback: 'https://test.host/api') - - expect(result).to eq not_allowed_callback_status - end - end - - context 'with a valid account and callback' do - it 'returns success status and confirms subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - subscription = Fabricate(:subscription, account: user_account) - - result = service_call(callback: subscription.callback_url) - expect(result).to eq success_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600) - end - end - end - - def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600) - subject.call(account, callback, secret, lease_seconds) - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def invalid_callback_status - ['Invalid callback URL', 422] - end - - def not_allowed_callback_status - ['Callback URL not allowed', 403] - end - - def success_status - ['', 202] - end -end diff --git a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb b/spec/services/pubsubhubbub/unsubscribe_service_spec.rb deleted file mode 100644 index 7ed9fc5af..000000000 --- a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::UnsubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - - context 'with a nil account' do - it 'returns an invalid topic status' do - result = subject.call(nil, 'callback.host') - - expect(result).to eq invalid_topic_status - end - end - - context 'with a valid account' do - let(:account) { Fabricate(:account) } - - it 'returns a valid topic status and does not run confirm when no subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async) - end - - it 'returns a valid topic status and does run confirm when there is a subscription' do - subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host') - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe') - end - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def valid_topic_status - ['', 202] - end - end -end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 9d84c41d5..58fb46f0f 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -46,10 +46,6 @@ RSpec.describe ReblogService, type: :service do it 'creates a reblog' do expect(status.reblogs.count).to eq 1 end - - it 'sends a Salmon slap for a remote reblog' do - expect(a_request(:post, 'http://salmon.example.com')).to have_been_made - end end context 'ActivityPub' do diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index e5ac37ed9..1aec060db 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe RejectFollowService, type: :service do it 'does not create follow relation' do expect(bob.following?(sender)).to be false end - - it 'sends a follow request rejection salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:reject]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 7bba83a60..48191d47c 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -32,23 +32,10 @@ RSpec.describe RemoveStatusService, type: :service do expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - req.body.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made - end - it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice end - it 'sends Salmon slap to previously mentioned users' do - expect(a_request(:post, "http://example.com/salmon").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.once - end - it 'sends delete activity to rebloggers' do expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 27a85af7c..7a64f4161 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -6,19 +6,13 @@ RSpec.describe ResolveAccountService, type: :service do before do stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404) - stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@redirected.com").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt')) - stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404) - stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt')) - stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt')) + stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) + stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) + stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) + stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) + stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) end it 'raises error if no such user can be resolved via webfinger' do @@ -29,74 +23,7 @@ RSpec.describe ResolveAccountService, type: :service do expect(subject.call('catsrgr8@example.com')).to be_nil end - it 'prevents hijacking existing accounts' do - account = subject.call('hacker1@redirected.com') - expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477' - end - - it 'prevents hijacking inexisting accounts' do - expect(subject.call('hacker2@redirected.com')).to be_nil - end - - context 'with an OStatus account' do - it 'returns an already existing remote account' do - old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no') - returned_account = subject.call('gargron@quitter.no') - - expect(old_account.id).to eq returned_account.id - end - - it 'returns a new remote account' do - account = subject.call('gargron@quitter.no') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - - it 'follows a legitimate account redirection' do - account = subject.call('gargron@redirected.com') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - - it 'returns a new remote account' do - account = subject.call('foo@localdomain.com') - - expect(account.username).to eq 'foo' - expect(account.domain).to eq 'localdomain.com' - expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' - end - end - context 'with an ActivityPub account' do - before do - stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) - stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) - stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) - end - - it 'fallback to OStatus if actor json could not be fetched' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - - it 'fallback to OStatus if actor json did not have inbox_url' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt')) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - it 'returns new remote account' do account = subject.call('foo@ap.example.com') @@ -124,13 +51,14 @@ RSpec.describe ResolveAccountService, type: :service do it 'processes one remote account at a time using locks' do wait_for_start = true fail_occurred = false - return_values = [] + return_values = Concurrent::Array.new threads = Array.new(5) do Thread.new do true while wait_for_start + begin - return_values << described_class.new.call('foo@localdomain.com') + return_values << described_class.new.call('foo@ap.example.com') rescue ActiveRecord::RecordNotUnique fail_occurred = true end diff --git a/spec/services/send_interaction_service_spec.rb b/spec/services/send_interaction_service_spec.rb deleted file mode 100644 index 710d8184c..000000000 --- a/spec/services/send_interaction_service_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'rails_helper' - -RSpec.describe SendInteractionService, type: :service do - subject { SendInteractionService.new } - - it 'sends an XML envelope to the Salmon end point of remote user' -end diff --git a/spec/services/subscribe_service_spec.rb b/spec/services/subscribe_service_spec.rb deleted file mode 100644 index 10bdb1ba8..000000000 --- a/spec/services/subscribe_service_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' - -RSpec.describe SubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { SubscribeService.new } - - it 'sends subscription request to PuSH hub' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - end - - it 'generates and keeps PuSH secret on successful call' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(account.secret).to_not be_blank - end - - it 'fails silently if PuSH hub forbids subscription' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 403) - subject.call(account) - end - - it 'fails silently if PuSH hub is not found' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - end - - it 'fails loudly if there is a network error' do - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - expect { subject.call(account) }.to raise_error HTTP::Error - end - - it 'fails loudly if PuSH hub is unavailable' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 503) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'fails loudly if rate limited' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 429) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end -end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 5835b912b..6350c6834 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnblockService, type: :service do it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end - - it 'sends an unblock salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unblock]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 8a2881ab1..84b5dafbc 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnfollowService, type: :service do it 'destroys the following relation' do expect(sender.following?(bob)).to be false end - - it 'sends an unfollow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unfollow]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unsubscribe_service_spec.rb b/spec/services/unsubscribe_service_spec.rb deleted file mode 100644 index 54d4b1b53..000000000 --- a/spec/services/unsubscribe_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'rails_helper' - -RSpec.describe UnsubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { UnsubscribeService.new } - - it 'removes the secret and resets expiration on account' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 204) - subject.call(account) - account.reload - - expect(account.secret).to be_blank - expect(account.subscription_expires_at).to be_blank - end - - it 'logs error on subscription failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - it 'logs error on connection failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - def stub_logger - double(debug: nil).tap do |logger| - allow(Rails).to receive(:logger).and_return(logger) - end - end -end diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb deleted file mode 100644 index f3ea70b80..000000000 --- a/spec/services/update_remote_profile_service_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'rails_helper' - -RSpec.describe UpdateRemoteProfileService, type: :service do - let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - subject { UpdateRemoteProfileService.new } - - before do - stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt')) - end - - context 'with updated details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - - before do - subject.call(xml, remote_account) - end - - it 'downloads new avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with unchanged details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } - - before do - subject.call(xml, remote_account) - end - - it 'does not re-download avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with updated details from a domain set to reject media' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) } - - before do - subject.call(xml, remote_account) - end - - it 'does not the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to be_nil - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - - it 'does not set store the avatar' do - expect(remote_account.reload.avatar_file_name).to be_nil - end - end -end diff --git a/spec/workers/after_remote_follow_request_worker_spec.rb b/spec/workers/after_remote_follow_request_worker_spec.rb deleted file mode 100644 index bd623cca5..000000000 --- a/spec/workers/after_remote_follow_request_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowRequestWorker do - subject { described_class.new } - let(:follow_request) { Fabricate(:follow_request) } - describe 'perform' do - context 'when the follow_request does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: true)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: false) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow_request.account, account.acct) - expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/after_remote_follow_worker_spec.rb b/spec/workers/after_remote_follow_worker_spec.rb deleted file mode 100644 index d93c469f9..000000000 --- a/spec/workers/after_remote_follow_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowWorker do - subject { described_class.new } - let(:follow) { Fabricate(:follow) } - describe 'perform' do - context 'when the follow does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is not locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: false)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: true) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow.account, account.acct) - expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb b/spec/workers/pubsubhubbub/confirmation_worker_spec.rb deleted file mode 100644 index 1eecdd2b5..000000000 --- a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::ConfirmationWorker do - include RoutingHelper - - subject { described_class.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) } - - describe 'perform' do - describe 'with subscribe mode' do - it 'confirms and updates subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to eq 'asdf' - expect(subscription.confirmed).to eq true - expect(subscription.expires_at).to be_within(5).of(10.days.from_now) - end - - it 'does not update subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to be_blank - expect(subscription.confirmed).to eq false - expect(subscription.expires_at).to be_within(5).of(3.days.from_now) - end - end - - describe 'with unsubscribe mode' do - it 'confirms and destroys subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not destroy subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.not_to raise_error - end - end - end - - def url_for_mode(mode) - "http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom" - end - - def stub_random_value - allow(SecureRandom).to receive(:hex).and_return(challenge_value) - end - - def challenge_value - '1a2s3d4f' - end - - def http_headers - { 'Connection' => 'close', 'Host' => 'example.com' } - end -end diff --git a/spec/workers/pubsubhubbub/delivery_worker_spec.rb b/spec/workers/pubsubhubbub/delivery_worker_spec.rb deleted file mode 100644 index c0e0d5186..000000000 --- a/spec/workers/pubsubhubbub/delivery_worker_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::DeliveryWorker do - include RoutingHelper - subject { described_class.new } - - let(:payload) { 'test' } - - describe 'perform' do - it 'raises when subscription does not exist' do - expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not attempt to deliver when domain blocked' do - _domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend) - subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago) - - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago) - end - - it 'raises when request fails' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 500) - expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'updates subscriptions when delivery succeeds' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - it 'updates subscription without a secret when delivery succeeds' do - subscription = Fabricate(:subscription, secret: nil) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - def stub_request_to_respond_with(subscription, code) - stub_request(:post, 'http://example.com/callback') - .with(body: payload, headers: expected_headers(subscription)) - .to_return(status: code, body: '', headers: {}) - end - - def expected_headers(subscription) - { - 'Connection' => 'close', - 'Content-Type' => 'application/atom+xml', - 'Host' => 'example.com', - 'Link' => "; rel=\"hub\", ; rel=\"self\"", - }.tap do |basic| - known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload) - basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret? - end - end - end -end diff --git a/spec/workers/pubsubhubbub/distribution_worker_spec.rb b/spec/workers/pubsubhubbub/distribution_worker_spec.rb deleted file mode 100644 index 584485079..000000000 --- a/spec/workers/pubsubhubbub/distribution_worker_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe Pubsubhubbub::DistributionWorker do - subject { Pubsubhubbub::DistributionWorker.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example2.com') } - let!(:anonymous_subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example1.com', confirmed: true, lease_seconds: 3600) } - let!(:subscription_with_follower) { Fabricate(:subscription, account: alice, callback_url: 'http://example2.com', confirmed: true, lease_seconds: 3600) } - - before do - bob.follow!(alice) - end - - describe 'with public status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :public) } - - it 'delivers payload to all subscriptions' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id]) - end - end - - context 'when OStatus privacy is not used' do - describe 'with private status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) } - - it 'does not deliver anything' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - - describe 'with direct status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) } - - it 'does not deliver payload' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - end -end diff --git a/spec/workers/scheduler/subscriptions_scheduler_spec.rb b/spec/workers/scheduler/subscriptions_scheduler_spec.rb deleted file mode 100644 index a7d1046de..000000000 --- a/spec/workers/scheduler/subscriptions_scheduler_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -describe Scheduler::SubscriptionsScheduler do - subject { Scheduler::SubscriptionsScheduler.new } - - let!(:expiring_account1) { Fabricate(:account, subscription_expires_at: 20.minutes.from_now, domain: 'example.com', followers_count: 1, hub_url: 'http://hub.example.com') } - let!(:expiring_account2) { Fabricate(:account, subscription_expires_at: 4.hours.from_now, domain: 'example.org', followers_count: 1, hub_url: 'http://hub.example.org') } - - before do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - stub_request(:post, 'http://hub.example.org/').to_return(status: 202) - end - - it 're-subscribes for all expiring accounts' do - subject.perform - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - expect(a_request(:post, 'http://hub.example.org/')).to have_been_made.once - end -end -- cgit From 5ff9970c8d4acfb69b4696a3b5ce8e2a9580e370 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 7 Jul 2019 14:12:41 +0200 Subject: Re-add route for direct timeline --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'config') diff --git a/config/routes.rb b/config/routes.rb index 62374d0df..1b88fe5e3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -303,6 +303,7 @@ Rails.application.routes.draw do end namespace :timelines do + resource :direct, only: :show, controller: :direct resource :home, only: :show, controller: :home resource :public, only: :show, controller: :public resources :tag, only: :show -- cgit From b8514561394767a10d3cf40132ada24d938c1680 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 16:16:51 +0200 Subject: Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` (#11247) --- app/controllers/accounts_controller.rb | 7 - app/controllers/statuses_controller.rb | 36 +- app/controllers/stream_entries_controller.rb | 64 - app/helpers/admin/action_logs_helper.rb | 2 +- app/helpers/home_helper.rb | 2 +- app/helpers/statuses_helper.rb | 222 +++ app/helpers/stream_entries_helper.rb | 220 --- app/javascript/styles/application.scss | 2 +- app/javascript/styles/mastodon/statuses.scss | 163 +++ app/javascript/styles/mastodon/stream_entries.scss | 163 --- app/lib/formatter.rb | 2 +- app/lib/ostatus/atom_serializer.rb | 376 ------ app/lib/status_finder.rb | 2 - app/lib/tag_manager.rb | 11 - app/mailers/admin_mailer.rb | 2 +- app/mailers/notification_mailer.rb | 2 +- app/models/concerns/account_associations.rb | 1 - app/models/concerns/streamable.rb | 43 - app/models/remote_profile.rb | 57 - app/models/status.rb | 4 - app/models/stream_entry.rb | 59 - app/serializers/rest/account_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 6 +- app/serializers/rss/account_serializer.rb | 6 +- app/serializers/rss/tag_serializer.rb | 4 +- app/services/batched_remove_status_service.rb | 2 +- app/services/fetch_link_card_service.rb | 2 +- app/services/process_mentions_service.rb | 2 +- app/services/remove_status_service.rb | 15 +- app/services/resolve_url_service.rb | 5 +- app/services/suspend_account_service.rb | 1 - app/views/accounts/_moved.html.haml | 4 +- app/views/accounts/show.html.haml | 4 +- app/views/admin/accounts/_account.html.haml | 2 +- app/views/admin/reports/_status.html.haml | 2 +- app/views/application/_card.html.haml | 2 +- .../_post_follow_actions.html.haml | 2 +- app/views/remote_interaction/new.html.haml | 2 +- app/views/remote_unfollows/_card.html.haml | 2 +- .../_post_follow_actions.html.haml | 2 +- app/views/statuses/_attachment_list.html.haml | 8 + app/views/statuses/_detailed_status.html.haml | 79 ++ app/views/statuses/_og_description.html.haml | 4 + app/views/statuses/_og_image.html.haml | 38 + app/views/statuses/_poll.html.haml | 27 + app/views/statuses/_simple_status.html.haml | 60 + app/views/statuses/_status.html.haml | 62 + app/views/statuses/embed.html.haml | 3 + app/views/statuses/show.html.haml | 24 + .../stream_entries/_attachment_list.html.haml | 8 - .../stream_entries/_detailed_status.html.haml | 79 -- app/views/stream_entries/_og_description.html.haml | 4 - app/views/stream_entries/_og_image.html.haml | 38 - app/views/stream_entries/_poll.html.haml | 27 - app/views/stream_entries/_simple_status.html.haml | 60 - app/views/stream_entries/_status.html.haml | 62 - app/views/stream_entries/embed.html.haml | 3 - app/views/stream_entries/show.html.haml | 25 - config/routes.rb | 6 - .../20190706233204_drop_stream_entries.rb | 13 + db/schema.rb | 14 +- spec/controllers/accounts_controller_spec.rb | 31 - spec/controllers/api/oembed_controller_spec.rb | 2 +- spec/controllers/application_controller_spec.rb | 4 - spec/controllers/statuses_controller_spec.rb | 16 +- spec/controllers/stream_entries_controller_spec.rb | 95 -- spec/fabricators/stream_entry_fabricator.rb | 5 - .../admin/account_moderation_notes_helper_spec.rb | 2 +- spec/helpers/statuses_helper_spec.rb | 224 ++++ spec/helpers/stream_entries_helper_spec.rb | 224 ---- spec/lib/activitypub/tag_manager_spec.rb | 6 - spec/lib/ostatus/atom_serializer_spec.rb | 1415 -------------------- spec/lib/status_finder_spec.rb | 9 - spec/lib/tag_manager_spec.rb | 42 - spec/models/concerns/streamable_spec.rb | 63 - spec/models/remote_profile_spec.rb | 143 -- spec/models/stream_entry_spec.rb | 192 --- spec/services/process_mentions_service_spec.rb | 4 +- spec/services/suspend_account_service_spec.rb | 6 +- spec/views/statuses/show.html.haml_spec.rb | 81 ++ spec/views/stream_entries/show.html.haml_spec.rb | 88 -- 81 files changed, 1071 insertions(+), 3732 deletions(-) delete mode 100644 app/controllers/stream_entries_controller.rb create mode 100644 app/helpers/statuses_helper.rb delete mode 100644 app/helpers/stream_entries_helper.rb create mode 100644 app/javascript/styles/mastodon/statuses.scss delete mode 100644 app/javascript/styles/mastodon/stream_entries.scss delete mode 100644 app/lib/ostatus/atom_serializer.rb delete mode 100644 app/models/concerns/streamable.rb delete mode 100644 app/models/remote_profile.rb delete mode 100644 app/models/stream_entry.rb create mode 100644 app/views/statuses/_attachment_list.html.haml create mode 100644 app/views/statuses/_detailed_status.html.haml create mode 100644 app/views/statuses/_og_description.html.haml create mode 100644 app/views/statuses/_og_image.html.haml create mode 100644 app/views/statuses/_poll.html.haml create mode 100644 app/views/statuses/_simple_status.html.haml create mode 100644 app/views/statuses/_status.html.haml create mode 100644 app/views/statuses/embed.html.haml create mode 100644 app/views/statuses/show.html.haml delete mode 100644 app/views/stream_entries/_attachment_list.html.haml delete mode 100644 app/views/stream_entries/_detailed_status.html.haml delete mode 100644 app/views/stream_entries/_og_description.html.haml delete mode 100644 app/views/stream_entries/_og_image.html.haml delete mode 100644 app/views/stream_entries/_poll.html.haml delete mode 100644 app/views/stream_entries/_simple_status.html.haml delete mode 100644 app/views/stream_entries/_status.html.haml delete mode 100644 app/views/stream_entries/embed.html.haml delete mode 100644 app/views/stream_entries/show.html.haml create mode 100644 db/post_migrate/20190706233204_drop_stream_entries.rb delete mode 100644 spec/controllers/stream_entries_controller_spec.rb delete mode 100644 spec/fabricators/stream_entry_fabricator.rb create mode 100644 spec/helpers/statuses_helper_spec.rb delete mode 100644 spec/helpers/stream_entries_helper_spec.rb delete mode 100644 spec/lib/ostatus/atom_serializer_spec.rb delete mode 100644 spec/models/concerns/streamable_spec.rb delete mode 100644 spec/models/remote_profile_spec.rb delete mode 100644 spec/models/stream_entry_spec.rb create mode 100644 spec/views/statuses/show.html.haml_spec.rb delete mode 100644 spec/views/stream_entries/show.html.haml_spec.rb (limited to 'config') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 73a4b1859..065707378 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -31,13 +31,6 @@ class AccountsController < ApplicationController end end - format.atom do - mark_cacheable! - - @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) - render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) - end - format.rss do mark_cacheable! diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index ef26691b2..776099ca8 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -33,12 +33,10 @@ class StatusesController < ApplicationController set_ancestors set_descendants - - render 'stream_entries/show' end format.json do - render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do + render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) end end @@ -46,7 +44,7 @@ class StatusesController < ApplicationController end def activity - render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do + render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) end end @@ -58,7 +56,7 @@ class StatusesController < ApplicationController response.headers['X-Frame-Options'] = 'ALLOWALL' @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) - render 'stream_entries/embed', layout: 'embedded' + render layout: 'embedded' end def replies @@ -92,11 +90,20 @@ class StatusesController < ApplicationController def create_descendant_thread(starting_depth, statuses) depth = starting_depth + statuses.size + if depth < DESCENDANTS_DEPTH_LIMIT - { statuses: statuses, starting_depth: starting_depth } + { + statuses: statuses, + starting_depth: starting_depth, + } else next_status = statuses.pop - { statuses: statuses, starting_depth: starting_depth, next_status: next_status } + + { + statuses: statuses, + starting_depth: starting_depth, + next_status: next_status, + } end end @@ -164,22 +171,13 @@ class StatusesController < ApplicationController end def set_link_headers - response.headers['Link'] = LinkHeader.new( - [ - [account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], - [ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]], - ] - ) + response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) end def set_status - @status = @account.statuses.find(params[:id]) - @stream_entry = @status.stream_entry - @type = @stream_entry.activity_type.downcase - + @status = @account.statuses.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 raise ActiveRecord::RecordNotFound end @@ -192,7 +190,7 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog? + redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end def set_referrer_policy_header diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb deleted file mode 100644 index 0f7e9e0f5..000000000 --- a/app/controllers/stream_entries_controller.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -class StreamEntriesController < ApplicationController - include Authorization - include SignatureVerification - - layout 'public' - - before_action :set_account - before_action :set_stream_entry - before_action :set_link_headers - before_action :check_account_suspension - before_action :set_cache_headers - - def show - respond_to do |format| - format.html do - expires_in 5.minutes, public: true unless @stream_entry.hidden? - - redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) - end - - format.atom do - expires_in 3.minutes, public: true unless @stream_entry.hidden? - - render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) - end - end - end - - def embed - redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301 - end - - private - - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def set_link_headers - response.headers['Link'] = LinkHeader.new( - [ - [account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], - [ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]], - ] - ) - end - - def set_stream_entry - @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id]) - @type = 'status' - - raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? - authorize @stream_entry.activity, :show? if @stream_entry.hidden? - rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 - raise ActiveRecord::RecordNotFound - end - - def check_account_suspension - gone if @account.suspended? - end -end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e5fbb1500..1daa60774 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -89,7 +89,7 @@ module Admin::ActionLogsHelper when 'DomainBlock', 'EmailDomainBlock' link_to record.domain, "https://#{record.domain}" when 'Status' - link_to record.account.acct, TagManager.instance.url_for(record) + link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) when 'AccountWarning' link_to record.target_account.acct, admin_account_path(record.target_account_id) end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index df60b7dd7..b66e827fe 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -21,7 +21,7 @@ module HomeHelper end end else - link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do + link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})") end + diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb new file mode 100644 index 000000000..e067380f6 --- /dev/null +++ b/app/helpers/statuses_helper.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +module StatusesHelper + EMBEDDED_CONTROLLER = 'statuses' + EMBEDDED_ACTION = 'embed' + + def display_name(account, **options) + if options[:custom_emojify] + Formatter.instance.format_display_name(account, options) + else + account.display_name.presence || account.username + end + end + + def account_action_button(account) + if user_signed_in? + if account.id == current_user.account_id + link_to settings_profile_url, class: 'button logo-button' do + safe_join([svg_logo, t('settings.edit_profile')]) + end + elsif current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do + safe_join([svg_logo, t('accounts.unfollow')]) + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do + safe_join([svg_logo, t('accounts.follow')]) + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do + safe_join([svg_logo, t('accounts.follow')]) + end + end + end + + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end + + def account_badge(account, all: false) + if account.bot? + content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif (Setting.show_staff_badge && account.user_staff?) || all + content_tag(:div, class: 'roles') do + if all && !account.user_staff? + content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') + elsif account.user_admin? + content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') + elsif account.user_moderator? + content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') + end + end + end + end + + def link_to_more(url) + link_to t('statuses.show_more'), url, class: 'load-more load-gap' + end + + def nothing_here(extra_classes = '') + content_tag(:div, class: "nothing-here #{extra_classes}") do + t('accounts.nothing_here') + end + end + + def account_description(account) + prepend_str = [ + [ + number_to_human(account.statuses_count, strip_insignificant_zeros: true), + I18n.t('accounts.posts', count: account.statuses_count), + ].join(' '), + + [ + number_to_human(account.following_count, strip_insignificant_zeros: true), + I18n.t('accounts.following', count: account.following_count), + ].join(' '), + + [ + number_to_human(account.followers_count, strip_insignificant_zeros: true), + I18n.t('accounts.followers', count: account.followers_count), + ].join(' '), + ].join(', ') + + [prepend_str, account.note].join(' · ') + end + + def media_summary(status) + attachments = { image: 0, video: 0 } + + status.media_attachments.each do |media| + if media.video? + attachments[:video] += 1 + else + attachments[:image] += 1 + end + end + + text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') + + return if text.blank? + + I18n.t('statuses.attached.description', attached: text) + end + + def status_text_summary(status) + return if status.spoiler_text.blank? + + I18n.t('statuses.content_warning', warning: status.spoiler_text) + end + + def poll_summary(status) + return unless status.preloadable_poll + + status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") + end + + def status_description(status) + components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + + if status.spoiler_text.blank? + components << status.text + components << poll_summary(status) + end + + components.reject(&:blank?).join("\n\n") + end + + def stream_link_target + embedded_view? ? '_blank' : nil + end + + def acct(account) + if account.local? + "@#{account.acct}@#{Rails.configuration.x.local_domain}" + else + "@#{account.acct}" + end + end + + def style_classes(status, is_predecessor, is_successor, include_threads) + classes = ['entry'] + classes << 'entry-predecessor' if is_predecessor + classes << 'entry-reblog' if status.reblog? + classes << 'entry-successor' if is_successor + classes << 'entry-center' if include_threads + classes.join(' ') + end + + def microformats_classes(status, is_direct_parent, is_direct_child) + classes = [] + classes << 'p-in-reply-to' if is_direct_parent + classes << 'p-repost-of' if status.reblog? && is_direct_parent + classes << 'p-comment' if is_direct_child + classes.join(' ') + end + + def microformats_h_class(status, is_predecessor, is_successor, include_threads) + if is_predecessor || status.reblog? || is_successor + 'h-cite' + elsif include_threads + '' + else + 'h-entry' + end + end + + def rtl_status?(status) + status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) + end + + def rtl?(text) + text = simplified_text(text) + rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) + + if rtl_words.present? + total_size = text.size.to_f + rtl_size(rtl_words) / total_size > 0.3 + else + false + end + end + + def fa_visibility_icon(status) + case status.visibility + when 'public' + fa_icon 'globe fw' + when 'unlisted' + fa_icon 'unlock fw' + when 'private' + fa_icon 'lock fw' + when 'direct' + fa_icon 'envelope fw' + end + end + + private + + def simplified_text(text) + text.dup.tap do |new_text| + URI.extract(new_text).each do |url| + new_text.gsub!(url, '') + end + + new_text.gsub!(Account::MENTION_RE, '') + new_text.gsub!(Tag::HASHTAG_RE, '') + new_text.gsub!(/\s+/, '') + end + end + + def rtl_size(words) + words.reduce(0) { |acc, elem| acc + elem.size }.to_f + end + + def embedded_view? + params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION + end +end diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb deleted file mode 100644 index 02a860a74..000000000 --- a/app/helpers/stream_entries_helper.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -module StreamEntriesHelper - EMBEDDED_CONTROLLER = 'statuses' - EMBEDDED_ACTION = 'embed' - - def display_name(account, **options) - if options[:custom_emojify] - Formatter.instance.format_display_name(account, options) - else - account.display_name.presence || account.username - end - end - - def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) - end - end - end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end - - def account_badge(account, all: false) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif (Setting.show_staff_badge && account.user_staff?) || all - content_tag(:div, class: 'roles') do - if all && !account.user_staff? - content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') - elsif account.user_admin? - content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') - elsif account.user_moderator? - content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') - end - end - end - end - - def link_to_more(url) - link_to t('statuses.show_more'), url, class: 'load-more load-gap' - end - - def nothing_here(extra_classes = '') - content_tag(:div, class: "nothing-here #{extra_classes}") do - t('accounts.nothing_here') - end - end - - def account_description(account) - prepend_str = [ - [ - number_to_human(account.statuses_count, strip_insignificant_zeros: true), - I18n.t('accounts.posts', count: account.statuses_count), - ].join(' '), - - [ - number_to_human(account.following_count, strip_insignificant_zeros: true), - I18n.t('accounts.following', count: account.following_count), - ].join(' '), - - [ - number_to_human(account.followers_count, strip_insignificant_zeros: true), - I18n.t('accounts.followers', count: account.followers_count), - ].join(' '), - ].join(', ') - - [prepend_str, account.note].join(' · ') - end - - def media_summary(status) - attachments = { image: 0, video: 0 } - - status.media_attachments.each do |media| - if media.video? - attachments[:video] += 1 - else - attachments[:image] += 1 - end - end - - text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') - - return if text.blank? - - I18n.t('statuses.attached.description', attached: text) - end - - def status_text_summary(status) - return if status.spoiler_text.blank? - I18n.t('statuses.content_warning', warning: status.spoiler_text) - end - - def poll_summary(status) - return unless status.preloadable_poll - status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") - end - - def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] - - if status.spoiler_text.blank? - components << status.text - components << poll_summary(status) - end - - components.reject(&:blank?).join("\n\n") - end - - def stream_link_target - embedded_view? ? '_blank' : nil - end - - def acct(account) - if account.local? - "@#{account.acct}@#{Rails.configuration.x.local_domain}" - else - "@#{account.acct}" - end - end - - def style_classes(status, is_predecessor, is_successor, include_threads) - classes = ['entry'] - classes << 'entry-predecessor' if is_predecessor - classes << 'entry-reblog' if status.reblog? - classes << 'entry-successor' if is_successor - classes << 'entry-center' if include_threads - classes.join(' ') - end - - def microformats_classes(status, is_direct_parent, is_direct_child) - classes = [] - classes << 'p-in-reply-to' if is_direct_parent - classes << 'p-repost-of' if status.reblog? && is_direct_parent - classes << 'p-comment' if is_direct_child - classes.join(' ') - end - - def microformats_h_class(status, is_predecessor, is_successor, include_threads) - if is_predecessor || status.reblog? || is_successor - 'h-cite' - elsif include_threads - '' - else - 'h-entry' - end - end - - def rtl_status?(status) - status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) - end - - def rtl?(text) - text = simplified_text(text) - rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) - - if rtl_words.present? - total_size = text.size.to_f - rtl_size(rtl_words) / total_size > 0.3 - else - false - end - end - - def fa_visibility_icon(status) - case status.visibility - when 'public' - fa_icon 'globe fw' - when 'unlisted' - fa_icon 'unlock fw' - when 'private' - fa_icon 'lock fw' - when 'direct' - fa_icon 'envelope fw' - end - end - - private - - def simplified_text(text) - text.dup.tap do |new_text| - URI.extract(new_text).each do |url| - new_text.gsub!(url, '') - end - - new_text.gsub!(Account::MENTION_RE, '') - new_text.gsub!(Tag::HASHTAG_RE, '') - new_text.gsub!(/\s+/, '') - end - end - - def rtl_size(words) - words.reduce(0) { |acc, elem| acc + elem.size }.to_f - end - - def embedded_view? - params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION - end -end diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 6db3bc3dc..8ebc45b62 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -13,7 +13,7 @@ @import 'mastodon/widgets'; @import 'mastodon/forms'; @import 'mastodon/accounts'; -@import 'mastodon/stream_entries'; +@import 'mastodon/statuses'; @import 'mastodon/boost'; @import 'mastodon/components'; @import 'mastodon/polls'; diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss new file mode 100644 index 000000000..19ce0ab8f --- /dev/null +++ b/app/javascript/styles/mastodon/statuses.scss @@ -0,0 +1,163 @@ +.activity-stream { + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; + + &--under-tabs { + border-radius: 0 0 4px 4px; + } + + @media screen and (max-width: $no-gap-breakpoint) { + margin-bottom: 0; + border-radius: 0; + box-shadow: none; + } + + &--headless { + border-radius: 0; + margin: 0; + box-shadow: none; + + .detailed-status, + .status { + border-radius: 0 !important; + } + } + + div[data-component] { + width: 100%; + } + + .entry { + background: $ui-base-color; + + .detailed-status, + .status, + .load-more { + animation: none; + } + + &:last-child { + .detailed-status, + .status, + .load-more { + border-bottom: 0; + border-radius: 0 0 4px 4px; + } + } + + &:first-child { + .detailed-status, + .status, + .load-more { + border-radius: 4px 4px 0 0; + } + + &:last-child { + .detailed-status, + .status, + .load-more { + border-radius: 4px; + } + } + } + + @media screen and (max-width: 740px) { + .detailed-status, + .status, + .load-more { + border-radius: 0 !important; + } + } + } + + &--highlighted .entry { + background: lighten($ui-base-color, 8%); + } +} + +.button.logo-button { + flex: 0 auto; + font-size: 14px; + background: $ui-highlight-color; + color: $primary-text-color; + text-transform: none; + line-height: 36px; + height: auto; + padding: 3px 15px; + border: 0; + + svg { + width: 20px; + height: auto; + vertical-align: middle; + margin-right: 5px; + fill: $primary-text-color; + } + + &:active, + &:focus, + &:hover { + background: lighten($ui-highlight-color, 10%); + } + + &:disabled, + &.disabled { + &:active, + &:focus, + &:hover { + background: $ui-primary-color; + } + } + + &.button--destructive { + &:active, + &:focus, + &:hover { + background: $error-red; + } + } + + @media screen and (max-width: $no-gap-breakpoint) { + svg { + display: none; + } + } +} + +.embed, +.public-layout { + .detailed-status { + padding: 15px; + } + + .status { + padding: 15px 15px 15px (48px + 15px * 2); + min-height: 48px + 2px; + + &__avatar { + left: 15px; + top: 17px; + } + + &__content { + padding-top: 5px; + } + + &__prepend { + margin-left: 48px + 15px * 2; + padding-top: 15px; + } + + &__prepend-icon-wrapper { + left: -32px; + } + + .media-gallery, + &__action-bar, + .video-player { + margin-top: 10px; + } + } +} diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss deleted file mode 100644 index 19ce0ab8f..000000000 --- a/app/javascript/styles/mastodon/stream_entries.scss +++ /dev/null @@ -1,163 +0,0 @@ -.activity-stream { - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 10px; - - &--under-tabs { - border-radius: 0 0 4px 4px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - border-radius: 0; - box-shadow: none; - } - - &--headless { - border-radius: 0; - margin: 0; - box-shadow: none; - - .detailed-status, - .status { - border-radius: 0 !important; - } - } - - div[data-component] { - width: 100%; - } - - .entry { - background: $ui-base-color; - - .detailed-status, - .status, - .load-more { - animation: none; - } - - &:last-child { - .detailed-status, - .status, - .load-more { - border-bottom: 0; - border-radius: 0 0 4px 4px; - } - } - - &:first-child { - .detailed-status, - .status, - .load-more { - border-radius: 4px 4px 0 0; - } - - &:last-child { - .detailed-status, - .status, - .load-more { - border-radius: 4px; - } - } - } - - @media screen and (max-width: 740px) { - .detailed-status, - .status, - .load-more { - border-radius: 0 !important; - } - } - } - - &--highlighted .entry { - background: lighten($ui-base-color, 8%); - } -} - -.button.logo-button { - flex: 0 auto; - font-size: 14px; - background: $ui-highlight-color; - color: $primary-text-color; - text-transform: none; - line-height: 36px; - height: auto; - padding: 3px 15px; - border: 0; - - svg { - width: 20px; - height: auto; - vertical-align: middle; - margin-right: 5px; - fill: $primary-text-color; - } - - &:active, - &:focus, - &:hover { - background: lighten($ui-highlight-color, 10%); - } - - &:disabled, - &.disabled { - &:active, - &:focus, - &:hover { - background: $ui-primary-color; - } - } - - &.button--destructive { - &:active, - &:focus, - &:hover { - background: $error-red; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - svg { - display: none; - } - } -} - -.embed, -.public-layout { - .detailed-status { - padding: 15px; - } - - .status { - padding: 15px 15px 15px (48px + 15px * 2); - min-height: 48px + 2px; - - &__avatar { - left: 15px; - top: 17px; - } - - &__content { - padding-top: 5px; - } - - &__prepend { - margin-left: 48px + 15px * 2; - padding-top: 15px; - } - - &__prepend-icon-wrapper { - left: -32px; - } - - .media-gallery, - &__action-bar, - .video-player { - margin-top: 10px; - } - } -} diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8a1aad41a..6c1239963 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -295,6 +295,6 @@ class Formatter end def mention_html(account) - "@#{encode(account.username)}" + "@#{encode(account.username)}" end end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb deleted file mode 100644 index f5c0e85ca..000000000 --- a/app/lib/ostatus/atom_serializer.rb +++ /dev/null @@ -1,376 +0,0 @@ -# frozen_string_literal: true - -class OStatus::AtomSerializer - include RoutingHelper - include ActionView::Helpers::SanitizeHelper - - class << self - def render(element) - document = Ox::Document.new(version: '1.0') - document << element - ('' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8') - end - end - - def author(account) - author = Ox::Element.new('author') - - uri = OStatus::TagManager.instance.uri_for(account) - - append_element(author, 'id', uri) - append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person]) - append_element(author, 'uri', uri) - append_element(author, 'name', account.username) - append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) - append_element(author, 'summary', Formatter.instance.simplified_format(account).to_str, type: :html) if account.note? - append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) - append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) if account.avatar? - append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header? - account.emojis.each do |emoji| - append_element(author, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) - end - append_element(author, 'poco:preferredUsername', account.username) - append_element(author, 'poco:displayName', account.display_name) if account.display_name? - append_element(author, 'poco:note', account.local? ? account.note : strip_tags(account.note)) if account.note? - append_element(author, 'mastodon:scope', account.locked? ? :private : :public) - - author - end - - def feed(account, stream_entries) - feed = Ox::Element.new('feed') - - add_namespaces(feed) - - append_element(feed, 'id', account_url(account, format: 'atom')) - append_element(feed, 'title', account.display_name.presence || account.username) - append_element(feed, 'subtitle', account.note) - append_element(feed, 'updated', account.updated_at.iso8601) - append_element(feed, 'logo', full_asset_url(account.avatar.url(:original))) - - feed << author(account) - - append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) - append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) - append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - - stream_entries.each do |stream_entry| - feed << entry(stream_entry) - end - - feed - end - - def entry(stream_entry, root = false) - entry = Ox::Element.new('entry') - - add_namespaces(entry) if root - - append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status)) - append_element(entry, 'published', stream_entry.created_at.iso8601) - append_element(entry, 'updated', stream_entry.updated_at.iso8601) - append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") - - entry << author(stream_entry.account) if root - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb]) - - entry << object(stream_entry.target) if stream_entry.targeted? - - if stream_entry.status.nil? - append_element(entry, 'content', 'Deleted status') - elsif stream_entry.status.destroyed? - append_element(entry, 'content', 'Deleted status') - append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(stream_entry.status)) if stream_entry.account.local? - else - serialize_status_attributes(entry, stream_entry.status) - end - - append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(stream_entry.status)) - append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: ::TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? - append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? - - entry - end - - def object(status) - object = Ox::Element.new('activity:object') - - append_element(object, 'id', OStatus::TagManager.instance.uri_for(status)) - append_element(object, 'published', status.created_at.iso8601) - append_element(object, 'updated', status.updated_at.iso8601) - append_element(object, 'title', status.title) - - object << author(status.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb]) - - serialize_status_attributes(object, status) - - append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(status)) - append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: ::TagManager.instance.url_for(status.thread)) unless status.thread.nil? - append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil? - - object - end - - def follow_salmon(follow) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{follow.account.acct} started following #{follow.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(follow.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow]) - - object = author(follow.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}") - - entry << author(follow_request.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - object = author(follow_request.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def authorize_follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}") - - entry << author(follow_request.target_account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize]) - - object = Ox::Element.new('activity:object') - object << author(follow_request.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - inner_object = author(follow_request.target_account) - inner_object.value = 'activity:object' - - object << inner_object - entry << object - entry - end - - def reject_follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}") - - entry << author(follow_request.target_account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject]) - - object = Ox::Element.new('activity:object') - object << author(follow_request.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - inner_object = author(follow_request.target_account) - inner_object.value = 'activity:object' - - object << inner_object - entry << object - entry - end - - def unfollow_salmon(follow) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(follow.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow]) - - object = author(follow.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def block_salmon(block) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) - append_element(entry, 'title', description) - - entry << author(block.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block]) - - object = author(block.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def unblock_salmon(block) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{block.account.acct} no longer blocks #{block.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) - append_element(entry, 'title', description) - - entry << author(block.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock]) - - object = author(block.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def favourite_salmon(favourite) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(favourite.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite]) - - entry << object(favourite.status) - - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status)) - - entry - end - - def unfavourite_salmon(favourite) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(favourite.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite]) - - entry << object(favourite.status) - - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status)) - - entry - end - - private - - def append_element(parent, name, content = nil, **attributes) - element = Ox::Element.new(name) - attributes.each { |k, v| element[k] = sanitize_str(v) } - element << sanitize_str(content) unless content.nil? - parent << element - end - - def sanitize_str(raw_str) - raw_str.to_s - end - - def conversation_uri(conversation) - return conversation.uri if conversation.uri? - OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation') - end - - def add_namespaces(parent) - parent['xmlns'] = OStatus::TagManager::XMLNS - parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS - parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS - parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS - parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS - parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS - parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS - end - - def serialize_status_attributes(entry, status) - append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local? - - append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? - append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language) - - status.active_mentions.sort_by(&:id).each do |mentioned| - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) - end - - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility? - - status.tags.each do |tag| - append_element(entry, 'category', nil, term: tag.name) - end - - status.media_attachments.each do |media| - append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) - end - - append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any? - append_element(entry, 'mastodon:scope', status.visibility) - - status.emojis.each do |emoji| - append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) - end - end -end diff --git a/app/lib/status_finder.rb b/app/lib/status_finder.rb index 4d1aed297..22ced8bf8 100644 --- a/app/lib/status_finder.rb +++ b/app/lib/status_finder.rb @@ -13,8 +13,6 @@ class StatusFinder raise ActiveRecord::RecordNotFound unless TagManager.instance.local_url?(url) case recognized_params[:controller] - when 'stream_entries' - StreamEntry.find(recognized_params[:id]).status when 'statuses' Status.find(recognized_params[:id]) else diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index fb364cb98..daf4f556b 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -33,15 +33,4 @@ class TagManager domain = uri.host + (uri.port ? ":#{uri.port}" : '') TagManager.instance.web_domain?(domain) end - - def url_for(target) - return target.url if target.respond_to?(:local?) && !target.local? - - case target.object_type - when :person - short_account_url(target) - when :note, :comment, :activity - short_account_status_url(target.account, target) - end - end end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index db154cad5..9ab3e2bbd 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -3,7 +3,7 @@ class AdminMailer < ApplicationMailer layout 'plain_mailer' - helper :stream_entries + helper :statuses def new_report(recipient, report) @report = report diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 66fa337c1..723d901fc 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class NotificationMailer < ApplicationMailer - helper :stream_entries + helper :statuses add_template_helper RoutingHelper diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 70855e054..0921e3252 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -11,7 +11,6 @@ module AccountAssociations has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account # Timelines - has_many :stream_entries, inverse_of: :account, dependent: :destroy has_many :statuses, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy has_many :mentions, inverse_of: :account, dependent: :destroy diff --git a/app/models/concerns/streamable.rb b/app/models/concerns/streamable.rb deleted file mode 100644 index 7c9edb8ef..000000000 --- a/app/models/concerns/streamable.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Streamable - extend ActiveSupport::Concern - - included do - has_one :stream_entry, as: :activity - - after_create do - account.stream_entries.create!(activity: self, hidden: hidden?) if needs_stream_entry? - end - end - - def title - super - end - - def content - title - end - - def target - super - end - - def object_type - :activity - end - - def thread - super - end - - def hidden? - false - end - - private - - def needs_stream_entry? - account.local? - end -end diff --git a/app/models/remote_profile.rb b/app/models/remote_profile.rb deleted file mode 100644 index 742d2b56f..000000000 --- a/app/models/remote_profile.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -class RemoteProfile - include ActiveModel::Model - - attr_reader :document - - def initialize(body) - @document = Nokogiri::XML.parse(body, nil, 'utf-8') - end - - def root - @root ||= document.at_xpath('/atom:feed|/atom:entry', atom: OStatus::TagManager::XMLNS) - end - - def author - @author ||= root.at_xpath('./atom:author|./dfrn:owner', atom: OStatus::TagManager::XMLNS, dfrn: OStatus::TagManager::DFRN_XMLNS) - end - - def hub_link - @hub_link ||= link_href_from_xml(root, 'hub') - end - - def display_name - @display_name ||= author.at_xpath('./poco:displayName', poco: OStatus::TagManager::POCO_XMLNS)&.content - end - - def note - @note ||= author.at_xpath('./atom:summary|./poco:note', atom: OStatus::TagManager::XMLNS, poco: OStatus::TagManager::POCO_XMLNS)&.content - end - - def scope - @scope ||= author.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content - end - - def avatar - @avatar ||= link_href_from_xml(author, 'avatar') - end - - def header - @header ||= link_href_from_xml(author, 'header') - end - - def emojis - @emojis ||= author.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS) - end - - def locked? - scope == 'private' - end - - private - - def link_href_from_xml(xml, type) - xml.at_xpath(%(./atom:link[@rel="#{type}"]/@href), atom: OStatus::TagManager::XMLNS)&.content - end -end diff --git a/app/models/status.rb b/app/models/status.rb index 2258e2d07..906756e85 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -28,7 +28,6 @@ class Status < ApplicationRecord before_destroy :unlink_from_conversations include Paginable - include Streamable include Cacheable include StatusThreadingConcern @@ -61,7 +60,6 @@ class Status < ApplicationRecord has_and_belongs_to_many :preview_cards has_one :notification, as: :activity, dependent: :destroy - has_one :stream_entry, as: :activity, inverse_of: :status has_one :status_stat, inverse_of: :status has_one :poll, inverse_of: :status, dependent: :destroy @@ -106,13 +104,11 @@ class Status < ApplicationRecord :status_stat, :tags, :preview_cards, - :stream_entry, :preloadable_poll, account: :account_stat, active_mentions: { account: :account_stat }, reblog: [ :application, - :stream_entry, :tags, :preview_cards, :media_attachments, diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb deleted file mode 100644 index 1a9afc5c7..000000000 --- a/app/models/stream_entry.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: stream_entries -# -# id :bigint(8) not null, primary key -# activity_id :bigint(8) -# activity_type :string -# created_at :datetime not null -# updated_at :datetime not null -# hidden :boolean default(FALSE), not null -# account_id :bigint(8) -# - -class StreamEntry < ApplicationRecord - include Paginable - - belongs_to :account, inverse_of: :stream_entries - belongs_to :activity, polymorphic: true - belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry - - validates :account, :activity, presence: true - - STATUS_INCLUDES = [:account, :stream_entry, :conversation, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :conversation, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze - - default_scope { where(activity_type: 'Status') } - scope :recent, -> { reorder(id: :desc) } - scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) } - - delegate :target, :title, :content, :thread, - to: :status, - allow_nil: true - - def object_type - orphaned? || targeted? ? :activity : status.object_type - end - - def verb - orphaned? ? :delete : status.verb - end - - def targeted? - [:follow, :request_friend, :authorize, :reject, :unfollow, :block, :unblock, :share, :favorite].include? verb - end - - def threaded? - (verb == :favorite || object_type == :comment) && !thread.nil? - end - - def mentions - orphaned? ? [] : status.active_mentions.map(&:account) - end - - private - - def orphaned? - status.nil? - end -end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 12adc971c..272e3eb9c 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -29,7 +29,7 @@ class REST::AccountSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object) + ActivityPub::TagManager.instance.url_for(object) end def avatar diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index c9b76cb16..2dc4a1b61 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -58,7 +58,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def uri - OStatus::TagManager.instance.uri_for(object) + ActivityPub::TagManager.instance.uri_for(object) end def content @@ -66,7 +66,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object) + ActivityPub::TagManager.instance.url_for(object) end def favourited @@ -132,7 +132,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object.account) + ActivityPub::TagManager.instance.url_for(object.account) end def acct diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index 88eca79ed..278affe13 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -2,7 +2,7 @@ class RSS::AccountSerializer include ActionView::Helpers::NumberHelper - include StreamEntriesHelper + include StatusesHelper include RoutingHelper def render(account, statuses) @@ -10,7 +10,7 @@ class RSS::AccountSerializer builder.title("#{display_name(account)} (@#{account.local_username_and_domain})") .description(account_description(account)) - .link(TagManager.instance.url_for(account)) + .link(ActivityPub::TagManager.instance.url_for(account)) .logo(full_pack_url('media/images/logo.svg')) .accent_color('2b90d9') @@ -20,7 +20,7 @@ class RSS::AccountSerializer statuses.each do |status| builder.item do |item| item.title(status.title) - .link(TagManager.instance.url_for(status)) + .link(ActivityPub::TagManager.instance.url_for(status)) .pub_date(status.created_at) .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb index 644380149..e8562ee87 100644 --- a/app/serializers/rss/tag_serializer.rb +++ b/app/serializers/rss/tag_serializer.rb @@ -3,7 +3,7 @@ class RSS::TagSerializer include ActionView::Helpers::NumberHelper include ActionView::Helpers::SanitizeHelper - include StreamEntriesHelper + include StatusesHelper include RoutingHelper def render(tag, statuses) @@ -18,7 +18,7 @@ class RSS::TagSerializer statuses.each do |status| builder.item do |item| item.title(status.title) - .link(TagManager.instance.url_for(status)) + .link(ActivityPub::TagManager.instance.url_for(status)) .pub_date(status.created_at) .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index cb66debc8..27dc460a6 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -12,7 +12,7 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects def call(statuses, **options) - statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a } + statuses = Status.where(id: statuses.map(&:id)).includes(:account).flat_map { |status| [status] + status.reblogs.includes(:account).to_a } @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 494aaed75..75fbd0e8c 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -84,7 +84,7 @@ class FetchLinkCardService < BaseService def mention_link?(a) @status.mentions.any? do |mention| - a['href'] == TagManager.instance.url_for(mention.account) + a['href'] == ActivityPub::TagManager.instance.url_for(mention.account) end end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index da52bff6a..90dca9740 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -40,7 +40,7 @@ class ProcessMentionsService < BaseService private def mention_undeliverable?(mentioned_account) - mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && @status.stream_entry.hidden?) + mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus?) end def create_notification(mention) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index a8c9100b3..6311971ff 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -5,14 +5,13 @@ class RemoveStatusService < BaseService include Payloadable def call(status, **options) - @payload = Oj.dump(event: :delete, payload: status.id.to_s) - @status = status - @account = status.account - @tags = status.tags.pluck(:name).to_a - @mentions = status.active_mentions.includes(:account).to_a - @reblogs = status.reblogs.includes(:account).to_a - @stream_entry = status.stream_entry - @options = options + @payload = Oj.dump(event: :delete, payload: status.id.to_s) + @status = status + @account = status.account + @tags = status.tags.pluck(:name).to_a + @mentions = status.active_mentions.includes(:account).to_a + @reblogs = status.reblogs.includes(:account).to_a + @options = options RedisLock.acquire(lock_options) do |lock| if lock.acquired? diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index bbdc0a595..f941b489a 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -73,10 +73,7 @@ class ResolveURLService < BaseService return unless recognized_params[:action] == 'show' - if recognized_params[:controller] == 'stream_entries' - status = StreamEntry.find_by(id: recognized_params[:id])&.status - check_local_status(status) - elsif recognized_params[:controller] == 'statuses' + if recognized_params[:controller] == 'statuses' status = Status.find_by(id: recognized_params[:id]) check_local_status(status) elsif recognized_params[:controller] == 'accounts' diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index a5ce3dbd9..0ebe0b562 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -24,7 +24,6 @@ class SuspendAccountService < BaseService report_notes scheduled_statuses status_pins - stream_entries subscriptions ).freeze diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml index 7a777bfea..02fd7bf42 100644 --- a/app/views/accounts/_moved.html.haml +++ b/app/views/accounts/_moved.html.haml @@ -3,10 +3,10 @@ .moved-account-widget .moved-account-widget__message = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) + = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention')) .moved-account-widget__card - = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do + = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do .detailed-status__display-avatar .account__avatar-overlay .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" } diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index de7d2a8ba..0dc984dcc 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -39,12 +39,12 @@ - else .activity-stream.activity-stream--under-tabs - if params[:page].to_i.zero? - = render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } + = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } - if @newer_url .entry= link_to_more @newer_url - = render partial: 'stream_entries/status', collection: @statuses, as: :status + = render partial: 'statuses/status', collection: @statuses, as: :status - if @older_url .entry= link_to_more @older_url diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index eba3ad804..b057d3e42 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -19,4 +19,4 @@ = table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user) - else = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") - = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account) + = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index b3c145120..9376db7ff 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -19,7 +19,7 @@ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } .detailed-status__meta - = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · - if status.reblog? diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index e6059b035..00254c40c 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -1,4 +1,4 @@ -- account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) +- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) .card.h-card = link_to account_url, target: '_blank', rel: 'noopener' do diff --git a/app/views/authorize_interactions/_post_follow_actions.html.haml b/app/views/authorize_interactions/_post_follow_actions.html.haml index 561c60137..dd71160e2 100644 --- a/app/views/authorize_interactions/_post_follow_actions.html.haml +++ b/app/views/authorize_interactions/_post_follow_actions.html.haml @@ -1,4 +1,4 @@ .post-follow-actions %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@resource.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@resource), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@resource), class: 'button button--block' %div= t('authorize_follow.post_follow.close') diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml index c8c08991f..2cc0fcb93 100644 --- a/app/views/remote_interaction/new.html.haml +++ b/app/views/remote_interaction/new.html.haml @@ -7,7 +7,7 @@ .public-layout .activity-stream.activity-stream--highlighted - = render 'stream_entries/status', status: @status + = render 'statuses/status', status: @status = simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f| = render 'shared/error_messages', object: @remote_follow diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index 9abcfd37e..80ad3bae2 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -4,7 +4,7 @@ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' %span.display-name - - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) + - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} diff --git a/app/views/remote_unfollows/_post_follow_actions.html.haml b/app/views/remote_unfollows/_post_follow_actions.html.haml index 2a9c062e9..328f7c833 100644 --- a/app/views/remote_unfollows/_post_follow_actions.html.haml +++ b/app/views/remote_unfollows/_post_follow_actions.html.haml @@ -1,4 +1,4 @@ .post-follow-actions %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@account), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@account), class: 'button button--block' %div= t('authorize_follow.post_follow.close') diff --git a/app/views/statuses/_attachment_list.html.haml b/app/views/statuses/_attachment_list.html.haml new file mode 100644 index 000000000..d9706f47b --- /dev/null +++ b/app/views/statuses/_attachment_list.html.haml @@ -0,0 +1,8 @@ +.attachment-list + .attachment-list__icon + = fa_icon 'link' + %ul.attachment-list__list + - attachments.each do |media| + %li + - url = media.remote_url.presence || media.file.url + = link_to File.basename(url), url, title: media.description diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml new file mode 100644 index 000000000..8686c2033 --- /dev/null +++ b/app/views/statuses/_detailed_status.html.haml @@ -0,0 +1,79 @@ +.detailed-status.detailed-status--flex + .p-author.h-card + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do + .detailed-status__display-avatar + - if current_account&.user&.setting_auto_play_gif || autoplay + = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' + - else + = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' + %span.display-name + %bdi + %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) + %span.display-name__account + = acct(status.account) + = fa_icon('lock') if status.account.locked? + + = account_action_button(status.account) + + .status__content.emojify< + - if status.spoiler_text? + %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  + %button.status__content__spoiler-link= t('statuses.show_more') + .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } + = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) + - if status.preloadable_poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } + + - if !status.media_attachments.empty? + - if status.media_attachments.first.audio_or_video? + - video = status.media_attachments.first + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + - else + = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + - elsif status.preview_card + = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json + + .detailed-status__meta + %data.dt-published{ value: status.created_at.to_time.iso8601 } + + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do + %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) + · + - if status.application && @account.user&.setting_show_application + - if status.application.website.blank? + %strong.detailed-status__application= status.application.name + - else + = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' + · + = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do + - if status.in_reply_to_id.nil? + = fa_icon('reply') + - else + = fa_icon('reply-all') + %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true + = " " + · + - if status.direct_visibility? + %span.detailed-status__link< + = fa_icon('envelope') + - elsif status.private_visibility? || status.limited_visibility? + %span.detailed-status__link< + = fa_icon('lock') + - else + = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do + = fa_icon('retweet') + %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true + = " " + · + = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do + = fa_icon('star') + %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true + = " " + + - if user_signed_in? + · + = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank' diff --git a/app/views/statuses/_og_description.html.haml b/app/views/statuses/_og_description.html.haml new file mode 100644 index 000000000..a7b18424d --- /dev/null +++ b/app/views/statuses/_og_description.html.haml @@ -0,0 +1,4 @@ +- description = status_description(activity) + +%meta{ name: 'description', content: description }/ += opengraph 'og:description', description diff --git a/app/views/statuses/_og_image.html.haml b/app/views/statuses/_og_image.html.haml new file mode 100644 index 000000000..67f9274b6 --- /dev/null +++ b/app/views/statuses/_og_image.html.haml @@ -0,0 +1,38 @@ +- if activity.is_a?(Status) && (activity.non_sensitive_with_media? || (activity.with_media? && Setting.preview_sensitive_media)) + - player_card = false + - activity.media_attachments.each do |media| + - if media.image? + = opengraph 'og:image', full_asset_url(media.file.url(:original)) + = opengraph 'og:image:type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta.dig('original', 'width') + = opengraph 'og:image:height', media.file.meta.dig('original', 'height') + - if media.description.present? + = opengraph 'og:image:alt', media.description + - elsif media.video? || media.gifv? + - player_card = true + = opengraph 'og:image', full_asset_url(media.file.url(:small)) + = opengraph 'og:image:type', 'image/png' + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta.dig('small', 'width') + = opengraph 'og:image:height', media.file.meta.dig('small', 'height') + = opengraph 'og:video', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:type', media.file_content_type + = opengraph 'twitter:player', medium_player_url(media) + = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) + = opengraph 'twitter:player:stream:content_type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:video:width', media.file.meta.dig('original', 'width') + = opengraph 'og:video:height', media.file.meta.dig('original', 'height') + = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') + = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') + - if player_card + = opengraph 'twitter:card', 'player' + - else + = opengraph 'twitter:card', 'summary_large_image' +- else + = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) + = opengraph 'og:image:width', '120' + = opengraph 'og:image:height','120' + = opengraph 'twitter:card', 'summary' diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml new file mode 100644 index 000000000..ba34890df --- /dev/null +++ b/app/views/statuses/_poll.html.haml @@ -0,0 +1,27 @@ +- show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? + +.poll + %ul + - poll.loaded_options.each do |option| + %li + - if show_results + - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 + %span.poll__chart{ style: "width: #{percent}%" } + + %label.poll__text>< + %span.poll__number= percent.round + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) + - else + %label.poll__text>< + %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) + .poll__footer + - unless show_results + %button.button.button-secondary{ disabled: true } + = t('statuses.poll.vote') + + %span= t('statuses.poll.total_votes', count: poll.votes_count) + + - unless poll.expires_at.nil? + · + %span= l poll.expires_at diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml new file mode 100644 index 000000000..11220dfcb --- /dev/null +++ b/app/views/statuses/_simple_status.html.haml @@ -0,0 +1,60 @@ +.status + .status__info + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do + %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) + %data.dt-published{ value: status.created_at.to_time.iso8601 } + + .p-author.h-card + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do + .status__avatar + %div + - if current_account&.user&.setting_auto_play_gif || autoplay + = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' + - else + = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' + %span.display-name + %bdi + %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) +   + %span.display-name__account + = acct(status.account) + = fa_icon('lock') if status.account.locked? + .status__content.emojify< + - if status.spoiler_text? + %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  + %button.status__content__spoiler-link= t('statuses.show_more') + .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } + = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) + - if status.preloadable_poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } + + - if !status.media_attachments.empty? + - if status.media_attachments.first.audio_or_video? + - video = status.media_attachments.first + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + - else + = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + - elsif status.preview_card + = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json + + .status__action-bar + .status__action-bar__counter + = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + - if status.in_reply_to_id.nil? + = fa_icon 'reply fw' + - else + = fa_icon 'reply-all fw' + .status__action-bar__counter__label= obscured_counter status.replies_count + = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + - if status.public_visibility? || status.unlisted_visibility? + = fa_icon 'retweet fw' + - elsif status.private_visibility? + = fa_icon 'lock fw' + - else + = fa_icon 'envelope fw' + = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + = fa_icon 'star fw' diff --git a/app/views/statuses/_status.html.haml b/app/views/statuses/_status.html.haml new file mode 100644 index 000000000..0e3652503 --- /dev/null +++ b/app/views/statuses/_status.html.haml @@ -0,0 +1,62 @@ +:ruby + pinned ||= false + include_threads ||= false + is_predecessor ||= false + is_successor ||= false + direct_reply_id ||= false + parent_id ||= false + autoplay ||= current_account&.user&.setting_auto_play_gif + is_direct_parent = direct_reply_id == status.id + is_direct_child = parent_id == status.in_reply_to_id + centered ||= include_threads && !is_predecessor && !is_successor + h_class = microformats_h_class(status, is_predecessor, is_successor, include_threads) + style_classes = style_classes(status, is_predecessor, is_successor, include_threads) + mf_classes = microformats_classes(status, is_direct_parent, is_direct_child) + entry_classes = h_class + ' ' + mf_classes + ' ' + style_classes + +- if status.reply? && include_threads + - if @next_ancestor + .entry{ class: entry_classes } + = link_to_more ActivityPub::TagManager.instance.url_for(@next_ancestor) + + = render partial: 'statuses/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }, autoplay: autoplay + +.entry{ class: entry_classes } + + - if status.reblog? + .status__prepend + .status__prepend-icon-wrapper + %i.status__prepend-icon.fa.fa-fw.fa-retweet + %span + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name muted' do + %bdi + %strong.emojify= display_name(status.account, custom_emojify: true) + = t('stream_entries.reblogged') + - elsif pinned + .status__prepend + .status__prepend-icon-wrapper + %i.status__prepend-icon.fa.fa-fw.fa-thumb-tack + %span + = t('stream_entries.pinned') + + = render (centered ? 'statuses/detailed_status' : 'statuses/simple_status'), status: status.proper, autoplay: autoplay + +- if include_threads + - if @since_descendant_thread_id + .entry{ class: entry_classes } + = link_to_more short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1) + - @descendant_threads.each do |thread| + = render partial: 'statuses/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }, autoplay: autoplay + + - if thread[:next_status] + .entry{ class: entry_classes } + = link_to_more ActivityPub::TagManager.instance.url_for(thread[:next_status]) + - if @next_descendant_thread + .entry{ class: entry_classes } + = link_to_more short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1) + +- if include_threads && !embedded_view? && !user_signed_in? + .entry{ class: entry_classes } + = link_to new_user_session_path, class: 'load-more load-gap' do + = fa_icon 'comments' + = t('statuses.sign_in_to_participate') diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml new file mode 100644 index 000000000..6f2ec646f --- /dev/null +++ b/app/views/statuses/embed.html.haml @@ -0,0 +1,3 @@ +- cache @status do + .activity-stream.activity-stream--headless + = render 'status', status: @status, centered: true, autoplay: @autoplay diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml new file mode 100644 index 000000000..704e37a3d --- /dev/null +++ b/app/views/statuses/show.html.haml @@ -0,0 +1,24 @@ +- content_for :page_title do + = t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false)) + +- content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + + %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: short_account_status_url(@account, @status), format: 'json') }/ + %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@status) }/ + + = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'article' + = opengraph 'og:title', "#{display_name(@account)} (@#{@account.local_username_and_domain})" + = opengraph 'og:url', short_account_status_url(@account, @status) + + = render 'og_description', activity: @status + = render 'og_image', activity: @status, account: @account + +.grid + .column-0 + .activity-stream.h-entry + = render partial: 'status', locals: { status: @status, include_threads: true } + .column-1 + = render 'application/sidebar' diff --git a/app/views/stream_entries/_attachment_list.html.haml b/app/views/stream_entries/_attachment_list.html.haml deleted file mode 100644 index d9706f47b..000000000 --- a/app/views/stream_entries/_attachment_list.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.attachment-list - .attachment-list__icon - = fa_icon 'link' - %ul.attachment-list__list - - attachments.each do |media| - %li - - url = media.remote_url.presence || media.file.url - = link_to File.basename(url), url, title: media.description diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml deleted file mode 100644 index 069d0053f..000000000 --- a/app/views/stream_entries/_detailed_status.html.haml +++ /dev/null @@ -1,79 +0,0 @@ -.detailed-status.detailed-status--flex - .p-author.h-card - = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do - .detailed-status__display-avatar - - if current_account&.user&.setting_auto_play_gif || autoplay - = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' - - else - = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' - %span.display-name - %bdi - %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) - %span.display-name__account - = acct(status.account) - = fa_icon('lock') if status.account.locked? - - = account_action_button(status.account) - - .status__content.emojify< - - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< - %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  - %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } - = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if status.preloadable_poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - - - if !status.media_attachments.empty? - - if status.media_attachments.first.audio_or_video? - - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } - - else - = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } - - elsif status.preview_card - = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json - - .detailed-status__meta - %data.dt-published{ value: status.created_at.to_time.iso8601 } - - = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do - %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - · - - if status.application && @account.user&.setting_show_application - - if status.application.website.blank? - %strong.detailed-status__application= status.application.name - - else - = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' - · - = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do - - if status.in_reply_to_id.nil? - = fa_icon('reply') - - else - = fa_icon('reply-all') - %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true - = " " - · - - if status.direct_visibility? - %span.detailed-status__link< - = fa_icon('envelope') - - elsif status.private_visibility? || status.limited_visibility? - %span.detailed-status__link< - = fa_icon('lock') - - else - = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do - = fa_icon('retweet') - %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true - = " " - · - = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do - = fa_icon('star') - %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true - = " " - - - if user_signed_in? - · - = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank' diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml deleted file mode 100644 index a7b18424d..000000000 --- a/app/views/stream_entries/_og_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- description = status_description(activity) - -%meta{ name: 'description', content: description }/ -= opengraph 'og:description', description diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml deleted file mode 100644 index 67f9274b6..000000000 --- a/app/views/stream_entries/_og_image.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -- if activity.is_a?(Status) && (activity.non_sensitive_with_media? || (activity.with_media? && Setting.preview_sensitive_media)) - - player_card = false - - activity.media_attachments.each do |media| - - if media.image? - = opengraph 'og:image', full_asset_url(media.file.url(:original)) - = opengraph 'og:image:type', media.file_content_type - - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta.dig('original', 'width') - = opengraph 'og:image:height', media.file.meta.dig('original', 'height') - - if media.description.present? - = opengraph 'og:image:alt', media.description - - elsif media.video? || media.gifv? - - player_card = true - = opengraph 'og:image', full_asset_url(media.file.url(:small)) - = opengraph 'og:image:type', 'image/png' - - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta.dig('small', 'width') - = opengraph 'og:image:height', media.file.meta.dig('small', 'height') - = opengraph 'og:video', full_asset_url(media.file.url(:original)) - = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original)) - = opengraph 'og:video:type', media.file_content_type - = opengraph 'twitter:player', medium_player_url(media) - = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) - = opengraph 'twitter:player:stream:content_type', media.file_content_type - - unless media.file.meta.nil? - = opengraph 'og:video:width', media.file.meta.dig('original', 'width') - = opengraph 'og:video:height', media.file.meta.dig('original', 'height') - = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') - = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') - - if player_card - = opengraph 'twitter:card', 'player' - - else - = opengraph 'twitter:card', 'summary_large_image' -- else - = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) - = opengraph 'og:image:width', '120' - = opengraph 'og:image:height','120' - = opengraph 'twitter:card', 'summary' diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml deleted file mode 100644 index ba34890df..000000000 --- a/app/views/stream_entries/_poll.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? - -.poll - %ul - - poll.loaded_options.each do |option| - %li - - if show_results - - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 - %span.poll__chart{ style: "width: #{percent}%" } - - %label.poll__text>< - %span.poll__number= percent.round - = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) - - else - %label.poll__text>< - %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< - = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) - .poll__footer - - unless show_results - %button.button.button-secondary{ disabled: true } - = t('statuses.poll.vote') - - %span= t('statuses.poll.total_votes', count: poll.votes_count) - - - unless poll.expires_at.nil? - · - %span= l poll.expires_at diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml deleted file mode 100644 index 0b924f72f..000000000 --- a/app/views/stream_entries/_simple_status.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -.status - .status__info - = link_to TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do - %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - %data.dt-published{ value: status.created_at.to_time.iso8601 } - - .p-author.h-card - = link_to TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do - .status__avatar - %div - - if current_account&.user&.setting_auto_play_gif || autoplay - = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' - - else - = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' - %span.display-name - %bdi - %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) -   - %span.display-name__account - = acct(status.account) - = fa_icon('lock') if status.account.locked? - .status__content.emojify< - - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< - %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  - %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } - = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if status.preloadable_poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - - - if !status.media_attachments.empty? - - if status.media_attachments.first.audio_or_video? - - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } - - else - = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } - - elsif status.preview_card - = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json - - .status__action-bar - .status__action-bar__counter - = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - - if status.in_reply_to_id.nil? - = fa_icon 'reply fw' - - else - = fa_icon 'reply-all fw' - .status__action-bar__counter__label= obscured_counter status.replies_count - = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - - if status.public_visibility? || status.unlisted_visibility? - = fa_icon 'retweet fw' - - elsif status.private_visibility? - = fa_icon 'lock fw' - - else - = fa_icon 'envelope fw' - = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - = fa_icon 'star fw' diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml deleted file mode 100644 index 83887cd87..000000000 --- a/app/views/stream_entries/_status.html.haml +++ /dev/null @@ -1,62 +0,0 @@ -:ruby - pinned ||= false - include_threads ||= false - is_predecessor ||= false - is_successor ||= false - direct_reply_id ||= false - parent_id ||= false - autoplay ||= current_account&.user&.setting_auto_play_gif - is_direct_parent = direct_reply_id == status.id - is_direct_child = parent_id == status.in_reply_to_id - centered ||= include_threads && !is_predecessor && !is_successor - h_class = microformats_h_class(status, is_predecessor, is_successor, include_threads) - style_classes = style_classes(status, is_predecessor, is_successor, include_threads) - mf_classes = microformats_classes(status, is_direct_parent, is_direct_child) - entry_classes = h_class + ' ' + mf_classes + ' ' + style_classes - -- if status.reply? && include_threads - - if @next_ancestor - .entry{ class: entry_classes } - = link_to_more TagManager.instance.url_for(@next_ancestor) - - = render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }, autoplay: autoplay - -.entry{ class: entry_classes } - - - if status.reblog? - .status__prepend - .status__prepend-icon-wrapper - %i.status__prepend-icon.fa.fa-fw.fa-retweet - %span - = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %bdi - %strong.emojify= display_name(status.account, custom_emojify: true) - = t('stream_entries.reblogged') - - elsif pinned - .status__prepend - .status__prepend-icon-wrapper - %i.status__prepend-icon.fa.fa-fw.fa-thumb-tack - %span - = t('stream_entries.pinned') - - = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper, autoplay: autoplay - -- if include_threads - - if @since_descendant_thread_id - .entry{ class: entry_classes } - = link_to_more short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1) - - @descendant_threads.each do |thread| - = render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }, autoplay: autoplay - - - if thread[:next_status] - .entry{ class: entry_classes } - = link_to_more TagManager.instance.url_for(thread[:next_status]) - - if @next_descendant_thread - .entry{ class: entry_classes } - = link_to_more short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1) - -- if include_threads && !embedded_view? && !user_signed_in? - .entry{ class: entry_classes } - = link_to new_user_session_path, class: 'load-more load-gap' do - = fa_icon 'comments' - = t('statuses.sign_in_to_participate') diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml deleted file mode 100644 index 4871c101e..000000000 --- a/app/views/stream_entries/embed.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- cache @stream_entry.activity do - .activity-stream.activity-stream--headless - = render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true, autoplay: @autoplay diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml deleted file mode 100644 index 0e81c4f68..000000000 --- a/app/views/stream_entries/show.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- content_for :page_title do - = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…', escape: false)) - -- content_for :header_tags do - - if @account.user&.setting_noindex - %meta{ name: 'robots', content: 'noindex' }/ - - %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/ - %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ - %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/ - - = opengraph 'og:site_name', site_title - = opengraph 'og:type', 'article' - = opengraph 'og:title', "#{display_name(@account)} (@#{@account.local_username_and_domain})" - = opengraph 'og:url', short_account_status_url(@account, @stream_entry.activity) - - = render 'stream_entries/og_description', activity: @stream_entry.activity - = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account - -.grid - .column-0 - .activity-stream.h-entry - = render partial: "stream_entries/#{@type}", locals: { @type.to_sym => @stream_entry.activity, include_threads: true } - .column-1 - = render 'application/sidebar' diff --git a/config/routes.rb b/config/routes.rb index 4b6d464c6..69b495a96 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,12 +45,6 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } resources :accounts, path: 'users', only: [:show], param: :username do - resources :stream_entries, path: 'updates', only: [:show] do - member do - get :embed - end - end - get :remote_follow, to: 'remote_follow#new' post :remote_follow, to: 'remote_follow#create' diff --git a/db/post_migrate/20190706233204_drop_stream_entries.rb b/db/post_migrate/20190706233204_drop_stream_entries.rb new file mode 100644 index 000000000..1fecece05 --- /dev/null +++ b/db/post_migrate/20190706233204_drop_stream_entries.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class DropStreamEntries < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + drop_table :stream_entries + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 09e6c9fae..2e38fb1f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_27_222826) do +ActiveRecord::Schema.define(version: 2019_07_06_233204) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -647,17 +647,6 @@ ActiveRecord::Schema.define(version: 2019_06_27_222826) do t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true end - create_table "stream_entries", force: :cascade do |t| - t.bigint "activity_id" - t.string "activity_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "hidden", default: false, null: false - t.bigint "account_id" - t.index ["account_id", "activity_type", "id"], name: "index_stream_entries_on_account_id_and_activity_type_and_id" - t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type" - end - create_table "subscriptions", force: :cascade do |t| t.string "callback_url", default: "", null: false t.string "secret" @@ -831,7 +820,6 @@ ActiveRecord::Schema.define(version: 2019_06_27_222826) do add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade add_foreign_key "statuses_tags", "statuses", on_delete: :cascade add_foreign_key "statuses_tags", "tags", name: "fk_3081861e21", on_delete: :cascade - add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "tombstones", "accounts", on_delete: :cascade add_foreign_key "user_invite_requests", "users", on_delete: :cascade diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index b728d719f..3d2a0665d 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -48,37 +48,6 @@ RSpec.describe AccountsController, type: :controller do end end - context 'atom' do - let(:format) { 'atom' } - let(:content_type) { 'application/atom+xml' } - - shared_examples 'responsed streams' do - it 'assigns @entries' do - entries = assigns(:entries).to_a - expect(entries.size).to eq expected_statuses.size - entries.each.zip(expected_statuses.each) do |entry, expected_status| - expect(entry.status).to eq expected_status - end - end - end - - include_examples 'responses' - - context 'without max_id nor since_id' do - let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] } - - include_examples 'responsed streams' - end - - context 'with max_id and since_id' do - let(:max_id) { status4.stream_entry.id } - let(:since_id) { status1.stream_entry.id } - let(:expected_statuses) { [status3, status2] } - - include_examples 'responsed streams' - end - end - context 'activitystreams2' do let(:format) { 'json' } let(:content_type) { 'application/activity+json' } diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb index 7fee15a35..b9082bde1 100644 --- a/spec/controllers/api/oembed_controller_spec.rb +++ b/spec/controllers/api/oembed_controller_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Api::OEmbedController, type: :controller do describe 'GET #show' do before do request.host = Rails.configuration.x.local_domain - get :show, params: { url: account_stream_entry_url(alice, status.stream_entry) }, format: :json + get :show, params: { url: short_account_status_url(alice, status) }, format: :json end it 'returns http success' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 33cc71087..27946b60f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -360,9 +360,5 @@ describe ApplicationController, type: :controller do context 'Status' do include_examples 'cacheable', :status, Status end - - context 'StreamEntry' do - include_examples 'receives :with_includes', :stream_entry, StreamEntry - end end end diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 1bb6636c6..95e5c363c 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -55,18 +55,6 @@ describe StatusesController do expect(assigns(:status)).to eq status end - it 'assigns @stream_entry' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:stream_entry)).to eq status.stream_entry - end - - it 'assigns @type' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:type)).to eq 'status' - end - it 'assigns @ancestors for ancestors of the status if it is a reply' do ancestor = Fabricate(:status) status = Fabricate(:status, in_reply_to_id: ancestor.id) @@ -135,10 +123,10 @@ describe StatusesController do expect(response).to have_http_status(200) end - it 'renders stream_entries/show' do + it 'renders statuses/show' do status = Fabricate(:status) get :show, params: { account_username: status.account.username, id: status.id } - expect(response).to render_template 'stream_entries/show' + expect(response).to render_template 'statuses/show' end end end diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb deleted file mode 100644 index eb7fdf9d7..000000000 --- a/spec/controllers/stream_entries_controller_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntriesController, type: :controller do - render_views - - shared_examples 'before_action' do |route| - context 'when account is not suspended and stream_entry is available' do - it 'assigns instance variables' do - status = Fabricate(:status) - - get route, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(assigns(:account)).to eq status.account - expect(assigns(:stream_entry)).to eq status.stream_entry - expect(assigns(:type)).to eq 'status' - end - - it 'sets Link headers' do - alice = Fabricate(:account, username: 'alice') - status = Fabricate(:status, account: alice) - - get route, params: { account_username: alice.username, id: status.stream_entry.id } - - expect(response.headers['Link'].to_s).to eq "; rel=\"alternate\"; type=\"application/atom+xml\", ; rel=\"alternate\"; type=\"application/activity+json\"" - end - end - - context 'when account is suspended' do - it 'returns http status 410' do - account = Fabricate(:account, suspended: true) - status = Fabricate(:status, account: account) - - get route, params: { account_username: account.username, id: status.stream_entry.id } - - expect(response).to have_http_status(410) - end - end - - context 'when activity is nil' do - it 'raises ActiveRecord::RecordNotFound' do - account = Fabricate(:account) - stream_entry = Fabricate.build(:stream_entry, account: account, activity: nil, activity_type: 'Status') - stream_entry.save!(validate: false) - - get route, params: { account_username: account.username, id: stream_entry.id } - - expect(response).to have_http_status(404) - end - end - - context 'when it is hidden and it is not permitted' do - it 'raises ActiveRecord::RecordNotFound' do - status = Fabricate(:status) - user = Fabricate(:user) - status.account.block!(user.account) - status.stream_entry.update!(hidden: true) - - sign_in(user) - get route, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to have_http_status(404) - end - end - end - - describe 'GET #show' do - include_examples 'before_action', :show - - it 'redirects to status page' do - status = Fabricate(:status) - - get :show, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to redirect_to(short_account_status_url(status.account, status)) - end - - it 'returns http success with Atom' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.stream_entry.id }, format: 'atom' - expect(response).to have_http_status(200) - end - end - - describe 'GET #embed' do - include_examples 'before_action', :embed - - it 'redirects to new embed page' do - status = Fabricate(:status) - - get :embed, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to redirect_to(embed_short_account_status_url(status.account, status)) - end - end -end diff --git a/spec/fabricators/stream_entry_fabricator.rb b/spec/fabricators/stream_entry_fabricator.rb deleted file mode 100644 index f33822c7c..000000000 --- a/spec/fabricators/stream_entry_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -Fabricator(:stream_entry) do - account - activity { Fabricate(:status) } - hidden { [true, false].sample } -end diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index c07f6c4b8..ddfe8b46f 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do - include StreamEntriesHelper + include StatusesHelper describe '#admin_account_link_to' do context 'account is nil' do diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb new file mode 100644 index 000000000..510955a2f --- /dev/null +++ b/spec/helpers/statuses_helper_spec.rb @@ -0,0 +1,224 @@ +require 'rails_helper' + +RSpec.describe StatusesHelper, type: :helper do + describe '#display_name' do + it 'uses the display name when it exists' do + account = Account.new(display_name: "Display", username: "Username") + + expect(helper.display_name(account)).to eq "Display" + end + + it 'uses the username when display name is nil' do + account = Account.new(display_name: nil, username: "Username") + + expect(helper.display_name(account)).to eq "Username" + end + end + + describe '#stream_link_target' do + it 'returns nil if it is not an embedded view' do + set_not_embedded_view + + expect(helper.stream_link_target).to be_nil + end + + it 'returns _blank if it is an embedded view' do + set_embedded_view + + expect(helper.stream_link_target).to eq '_blank' + end + end + + describe '#acct' do + it 'is fully qualified for embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + + it 'is fully qualified for embedded foreign accounts' do + set_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded foreign accounts' do + set_not_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_not_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + end + + def set_not_embedded_view + params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" + params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" + end + + def set_embedded_view + params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER + params[:action] = StatusesHelper::EMBEDDED_ACTION + end + + describe '#style_classes' do + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, false, false) + + expect(classes).to eq 'entry' + end + + it do + status = double(reblog?: true) + classes = helper.style_classes(status, false, false, false) + + expect(classes).to eq 'entry entry-reblog' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, true, false, false) + + expect(classes).to eq 'entry entry-predecessor' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, true, false) + + expect(classes).to eq 'entry entry-successor' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, false, true) + + expect(classes).to eq 'entry entry-center' + end + + it do + status = double(reblog?: true) + classes = helper.style_classes(status, true, true, true) + + expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' + end + end + + describe '#microformats_classes' do + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, false, false) + + expect(classes).to eq '' + end + + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, true, false) + + expect(classes).to eq 'p-in-reply-to' + end + + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, false, true) + + expect(classes).to eq 'p-comment' + end + + it do + status = double(reblog?: true) + classes = helper.microformats_classes(status, true, false) + + expect(classes).to eq 'p-in-reply-to p-repost-of' + end + + it do + status = double(reblog?: true) + classes = helper.microformats_classes(status, true, true) + + expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' + end + end + + describe '#microformats_h_class' do + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, false, false) + + expect(css_class).to eq 'h-entry' + end + + it do + status = double(reblog?: true) + css_class = helper.microformats_h_class(status, false, false, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, true, false, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, true, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, false, true) + + expect(css_class).to eq '' + end + + it do + status = double(reblog?: true) + css_class = helper.microformats_h_class(status, true, true, true) + + expect(css_class).to eq 'h-cite' + end + end + + describe '#rtl?' do + it 'is false if text is empty' do + expect(helper).not_to be_rtl '' + end + + it 'is false if there are no right to left characters' do + expect(helper).not_to be_rtl 'hello world' + end + + it 'is false if right to left characters are fewer than 1/3 of total text' do + expect(helper).not_to be_rtl 'hello ݟ world' + end + + it 'is true if right to left characters are greater than 1/3 of total text' do + expect(helper).to be_rtl 'aaݟaaݟ' + end + end +end diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb deleted file mode 100644 index 845b9974e..000000000 --- a/spec/helpers/stream_entries_helper_spec.rb +++ /dev/null @@ -1,224 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntriesHelper, type: :helper do - describe '#display_name' do - it 'uses the display name when it exists' do - account = Account.new(display_name: "Display", username: "Username") - - expect(helper.display_name(account)).to eq "Display" - end - - it 'uses the username when display name is nil' do - account = Account.new(display_name: nil, username: "Username") - - expect(helper.display_name(account)).to eq "Username" - end - end - - describe '#stream_link_target' do - it 'returns nil if it is not an embedded view' do - set_not_embedded_view - - expect(helper.stream_link_target).to be_nil - end - - it 'returns _blank if it is an embedded view' do - set_embedded_view - - expect(helper.stream_link_target).to eq '_blank' - end - end - - describe '#acct' do - it 'is fully qualified for embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - - it 'is fully qualified for embedded foreign accounts' do - set_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded foreign accounts' do - set_not_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_not_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - end - - def set_not_embedded_view - params[:controller] = "not_#{StreamEntriesHelper::EMBEDDED_CONTROLLER}" - params[:action] = "not_#{StreamEntriesHelper::EMBEDDED_ACTION}" - end - - def set_embedded_view - params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER - params[:action] = StreamEntriesHelper::EMBEDDED_ACTION - end - - describe '#style_classes' do - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, false, false) - - expect(classes).to eq 'entry' - end - - it do - status = double(reblog?: true) - classes = helper.style_classes(status, false, false, false) - - expect(classes).to eq 'entry entry-reblog' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, true, false, false) - - expect(classes).to eq 'entry entry-predecessor' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, true, false) - - expect(classes).to eq 'entry entry-successor' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, false, true) - - expect(classes).to eq 'entry entry-center' - end - - it do - status = double(reblog?: true) - classes = helper.style_classes(status, true, true, true) - - expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' - end - end - - describe '#microformats_classes' do - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, false, false) - - expect(classes).to eq '' - end - - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, true, false) - - expect(classes).to eq 'p-in-reply-to' - end - - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, false, true) - - expect(classes).to eq 'p-comment' - end - - it do - status = double(reblog?: true) - classes = helper.microformats_classes(status, true, false) - - expect(classes).to eq 'p-in-reply-to p-repost-of' - end - - it do - status = double(reblog?: true) - classes = helper.microformats_classes(status, true, true) - - expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' - end - end - - describe '#microformats_h_class' do - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, false, false) - - expect(css_class).to eq 'h-entry' - end - - it do - status = double(reblog?: true) - css_class = helper.microformats_h_class(status, false, false, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, true, false, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, true, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, false, true) - - expect(css_class).to eq '' - end - - it do - status = double(reblog?: true) - css_class = helper.microformats_h_class(status, true, true, true) - - expect(css_class).to eq 'h-cite' - end - end - - describe '#rtl?' do - it 'is false if text is empty' do - expect(helper).not_to be_rtl '' - end - - it 'is false if there are no right to left characters' do - expect(helper).not_to be_rtl 'hello world' - end - - it 'is false if right to left characters are fewer than 1/3 of total text' do - expect(helper).not_to be_rtl 'hello ݟ world' - end - - it 'is true if right to left characters are greater than 1/3 of total text' do - expect(helper).to be_rtl 'aaݟaaݟ' - end - end -end diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 6d246629e..1c5c6f0ed 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -143,12 +143,6 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status end - it 'returns the local status for OStatus StreamEntry URL' do - status = Fabricate(:status) - stream_entry_url = account_stream_entry_url(status.account, status.stream_entry) - expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status - end - it 'returns the remote status by matching URI without fragment part' do status = Fabricate(:status, uri: 'https://example.com/123') expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb deleted file mode 100644 index 74ab7576f..000000000 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ /dev/null @@ -1,1415 +0,0 @@ -require 'rails_helper' - -RSpec.describe OStatus::AtomSerializer do - shared_examples 'follow request salmon' do - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow_request = Fabricate(:follow_request, account: account) - - follow_request_salmon = serialize(follow_request) - - expect(follow_request_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - - follow_request_salmon = serialize(follow_request) - - object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with request_friend type' do - follow_request = Fabricate(:follow_request) - - follow_request_salmon = serialize(follow_request) - - verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:request_friend] - end - - it 'appends activity:object with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow_request = Fabricate(:follow_request, target_account: target_account) - - follow_request_salmon = serialize(follow_request) - - object = follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - shared_examples 'namespaces' do - it 'adds namespaces' do - element = serialize - - expect(element['xmlns']).to eq OStatus::TagManager::XMLNS - expect(element['xmlns:thr']).to eq OStatus::TagManager::THR_XMLNS - expect(element['xmlns:activity']).to eq OStatus::TagManager::AS_XMLNS - expect(element['xmlns:poco']).to eq OStatus::TagManager::POCO_XMLNS - expect(element['xmlns:media']).to eq OStatus::TagManager::MEDIA_XMLNS - expect(element['xmlns:ostatus']).to eq OStatus::TagManager::OS_XMLNS - expect(element['xmlns:mastodon']).to eq OStatus::TagManager::MTDN_XMLNS - end - end - - shared_examples 'no namespaces' do - it 'does not add namespaces' do - expect(serialize['xmlns']).to eq nil - end - end - - shared_examples 'status attributes' do - it 'appends summary element with spoiler text if present' do - status = Fabricate(:status, language: :ca, spoiler_text: 'spoiler text') - - element = serialize(status) - - summary = element.summary - expect(summary['xml:lang']).to eq 'ca' - expect(summary.text).to eq 'spoiler text' - end - - it 'does not append summary element with spoiler text if not present' do - status = Fabricate(:status, spoiler_text: '') - element = serialize(status) - element.nodes.each { |node| expect(node.name).not_to eq 'summary' } - end - - it 'appends content element with formatted status' do - status = Fabricate(:status, language: :ca, text: 'text') - - element = serialize(status) - - content = element.content - expect(content[:type]).to eq 'html' - expect(content['xml:lang']).to eq 'ca' - expect(content.text).to eq '

text

' - end - - it 'appends link elements for mentioned accounts' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status) - Fabricate(:mention, account: account, status: status) - - element = serialize(status) - - mentioned = element.nodes.find do |node| - node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:person] - end - - expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends link elements for emojis' do - Fabricate(:custom_emoji) - - status = Fabricate(:status, text: ':coolcat:') - element = serialize(status) - emoji = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' } - - expect(emoji[:name]).to eq 'coolcat' - expect(emoji[:href]).to_not be_blank - end - end - - describe 'render' do - it 'returns XML with emojis' do - element = Ox::Element.new('tag') - element << '💩' - xml = OStatus::AtomSerializer.render(element) - - expect(xml).to eq "\n💩\n" - end - - it 'returns XML, stripping invalid characters like \b and \v' do - element = Ox::Element.new('tag') - element << "im l33t\b haxo\b\vr" - xml = OStatus::AtomSerializer.render(element) - - expect(xml).to eq "\nim l33t haxor\n" - end - end - - describe '#author' do - context 'when note is present' do - it 'appends poco:note element with note for local account' do - account = Fabricate(:account, domain: nil, note: '

note

') - - author = OStatus::AtomSerializer.new.author(account) - - note = author.nodes.find { |node| node.name == 'poco:note' } - expect(note.text).to eq '

note

' - end - - it 'appends poco:note element with tags-stripped note for remote account' do - account = Fabricate(:account, domain: 'remote', note: '

note

') - - author = OStatus::AtomSerializer.new.author(account) - - note = author.nodes.find { |node| node.name == 'poco:note' } - expect(note.text).to eq 'note' - end - - it 'appends summary element with type attribute and simplified note if present' do - account = Fabricate(:account, note: 'note') - author = OStatus::AtomSerializer.new.author(account) - expect(author.summary.text).to eq '

note

' - expect(author.summary[:type]).to eq 'html' - end - end - - context 'when note is not present' do - it 'does not append poco:note element' do - account = Fabricate(:account, note: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'poco:note' } - end - - it 'does not append summary element' do - account = Fabricate(:account, note: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'summary' } - end - end - - it 'returns author element' do - account = Fabricate(:account) - author = OStatus::AtomSerializer.new.author(account) - expect(author.name).to eq 'author' - end - - it 'appends activity:object-type element with person type' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - object_type = author.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:person] - end - - it 'appends email element with username and domain for local account' do - account = Fabricate(:account, username: 'username') - author = OStatus::AtomSerializer.new.author(account) - expect(author.email.text).to eq 'username@cb6e6126.ngrok.io' - end - - it 'appends email element with username and domain for remote user' do - account = Fabricate(:account, domain: 'domain', username: 'username') - author = OStatus::AtomSerializer.new.author(account) - expect(author.email.text).to eq 'username@domain' - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:rel]).to eq 'alternate' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' - end - - it 'has link element for avatar if present' do - account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'avatar' } - expect(link[:type]).to eq 'image/gif' - expect(link['media:width']).to eq '120' - expect(link['media:height']).to eq '120' - expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ - end - - it 'does not have link element for avatar if not present' do - account = Fabricate(:account, avatar: nil) - - author = OStatus::AtomSerializer.new.author(account) - - author.nodes.each do |node| - expect(node[:rel]).not_to eq 'avatar' if node.name == 'link' - end - end - - it 'appends link element for header if present' do - account = Fabricate(:account, header: attachment_fixture('avatar.gif')) - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'header' } - expect(link[:type]).to eq 'image/gif' - expect(link['media:width']).to eq '700' - expect(link['media:height']).to eq '335' - expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/headers\/.+\/original\/avatar.gif/ - end - - it 'does not append link element for header if not present' do - account = Fabricate(:account, header: nil) - - author = OStatus::AtomSerializer.new.author(account) - - author.nodes.each do |node| - expect(node[:rel]).not_to eq 'header' if node.name == 'link' - end - end - - it 'appends poco:displayName element with display name if present' do - account = Fabricate(:account, display_name: 'display name') - - author = OStatus::AtomSerializer.new.author(account) - - display_name = author.nodes.find { |node| node.name == 'poco:displayName' } - expect(display_name.text).to eq 'display name' - end - - it 'does not append poco:displayName element with display name if not present' do - account = Fabricate(:account, display_name: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'poco:displayName' } - end - - it "appends mastodon:scope element with 'private' if locked" do - account = Fabricate(:account, locked: true) - - author = OStatus::AtomSerializer.new.author(account) - - scope = author.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'private' - end - - it "appends mastodon:scope element with 'public' if unlocked" do - account = Fabricate(:account, locked: false) - - author = OStatus::AtomSerializer.new.author(account) - - scope = author.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'public' - end - - it 'includes URI' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - expect(author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - expect(author.uri.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'includes username' do - account = Fabricate(:account, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - name = author.nodes.find { |node| node.name == 'name' } - username = author.nodes.find { |node| node.name == 'poco:preferredUsername' } - expect(name.text).to eq 'username' - expect(username.text).to eq 'username' - end - end - - describe '#entry' do - shared_examples 'not root' do - include_examples 'no namespaces' do - def serialize - subject - end - end - - it 'does not append author element' do - subject.nodes.each { |node| expect(node.name).not_to eq 'author' } - end - end - - context 'it is root' do - include_examples 'namespaces' do - def serialize - stream_entry = Fabricate(:stream_entry) - OStatus::AtomSerializer.new.entry(stream_entry, true) - end - end - - it 'appends author element' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry, true) - - expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - end - - context 'if status is present' do - include_examples 'status attributes' do - def serialize(status) - OStatus::AtomSerializer.new.entry(status.stream_entry, true) - end - end - - it 'appends link element for the public collection if status is publicly visible' do - status = Fabricate(:status, visibility: :public) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - mentioned_person = entry.nodes.find do |node| - node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] - end - expect(mentioned_person[:href]).to eq OStatus::TagManager::COLLECTIONS[:public] - end - - it 'does not append link element for the public collection if status is not publicly visible' do - status = Fabricate(:status, visibility: :private) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - entry.nodes.each do |node| - if node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] - expect(mentioned_collection[:href]).not_to eq OStatus::TagManager::COLLECTIONS[:public] - end - end - end - - it 'appends category elements for tags' do - tag = Fabricate(:tag, name: 'tag') - status = Fabricate(:status, tags: [ tag ]) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.category[:term]).to eq 'tag' - end - - it 'appends link elements for media attachments' do - file = attachment_fixture('attachment.jpg') - media_attachment = Fabricate(:media_attachment, file: file) - status = Fabricate(:status, media_attachments: [ media_attachment ]) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - enclosure = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'enclosure' } - expect(enclosure[:type]).to eq 'image/jpeg' - expect(enclosure[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/media_attachments\/files\/.+\/original\/attachment.jpg$/ - end - - it 'appends mastodon:scope element with visibility' do - status = Fabricate(:status, visibility: :public) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'public' - end - end - - context 'if status is not present' do - it 'appends content element saying status is deleted' do - status = Fabricate(:status) - status.destroy! - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.content.text).to eq 'Deleted status' - end - - it 'appends title element saying the status is deleted' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - status.destroy! - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.title.text).to eq 'username deleted status' - end - end - - context 'it is not root' do - let(:stream_entry) { Fabricate(:stream_entry) } - subject { OStatus::AtomSerializer.new.entry(stream_entry, false) } - include_examples 'not root' - end - - context 'without root parameter' do - let(:stream_entry) { Fabricate(:stream_entry) } - subject { OStatus::AtomSerializer.new.entry(stream_entry) } - include_examples 'not root' - end - - it 'returns entry element' do - stream_entry = Fabricate(:stream_entry) - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends published element with created date' do - stream_entry = Fabricate(:stream_entry, created_at: '2000-01-01T00:00:00Z') - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.published.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends updated element with updated date' do - stream_entry = Fabricate(:stream_entry, updated_at: '2000-01-01T00:00:00Z') - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends title element with status title' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account, reblog_of_id: nil) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - expect(entry.title.text).to eq 'New status by username' - end - - it 'appends activity:object-type element with object type' do - status = Fabricate(:status) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] - end - - it 'appends activity:verb element with object type' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] - end - - it 'appends activity:object element with target if present' do - reblogged = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - reblog = Fabricate(:status, reblog: reblogged) - - entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry) - - object = entry.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}" - end - - it 'does not append activity:object element if target is not present' do - status = Fabricate(:status, reblog_of_id: nil) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - entry.nodes.each { |node| expect(node.name).not_to eq 'activity:object' } - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'appends link element for itself' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}.atom" - end - - it 'appends thr:in-reply-to element if threaded' do - in_reply_to_status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reblog_of_id: nil) - reply_status = Fabricate(:status, in_reply_to_id: in_reply_to_status.id) - - entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry) - - in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}" - end - - it 'does not append thr:in-reply-to element if not threaded' do - status = Fabricate(:status) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } - end - - it 'appends ostatus:conversation if conversation id is present' do - status = Fabricate(:status) - status.conversation.update!(created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } - expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation_id}:objectType=Conversation" - end - - it 'does not append ostatus:conversation if conversation id is not present' do - status = Fabricate.build(:status, conversation_id: nil) - status.save!(validate: false) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } - end - end - - describe '#feed' do - include_examples 'namespaces' do - def serialize - account = Fabricate(:account) - OStatus::AtomSerializer.new.feed(account, []) - end - end - - it 'returns feed element' do - account = Fabricate(:account) - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.name).to eq 'feed' - end - - it 'appends id element with account Atom URL' do - account = Fabricate(:account, username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.id.text).to eq 'https://cb6e6126.ngrok.io/users/username.atom' - end - - it 'appends title element with account display name if present' do - account = Fabricate(:account, display_name: 'display name') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.title.text).to eq 'display name' - end - - it 'does not append title element with account username if account display name is not present' do - account = Fabricate(:account, display_name: '', username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.title.text).to eq 'username' - end - - it 'appends subtitle element with account note' do - account = Fabricate(:account, note: 'note') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.subtitle.text).to eq 'note' - end - - it 'appends updated element with date account got updated' do - account = Fabricate(:account, updated_at: '2000-01-01T00:00:00Z') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends logo element with full asset URL for original account avatar' do - account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.logo.text).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ - end - - it 'appends author element' do - account = Fabricate(:account, username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' - end - - it 'appends link element for itself' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/users/username.atom' - end - - it 'appends link element for the next if it has 20 stream entries' do - account = Fabricate(:account, username: 'username') - stream_entry = Fabricate(:stream_entry) - - feed = OStatus::AtomSerializer.new.feed(account, Array.new(20, stream_entry)) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'next' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username.atom?max_id=#{stream_entry.id}" - end - - it 'does not append link element for the next if it does not have 20 stream entries' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - feed.nodes.each do |node| - expect(node[:rel]).not_to eq 'next' if node.name == 'link' - end - end - - it 'appends stream entries' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - feed = OStatus::AtomSerializer.new.feed(account, [status.stream_entry]) - - expect(feed.entry.title.text).to eq 'New status by username' - end - end - - describe '#block_salmon' do - include_examples 'namespaces' do - def serialize - block = Fabricate(:block) - OStatus::AtomSerializer.new.block_salmon(block) - end - end - - it 'returns entry element' do - block = Fabricate(:block) - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - expect(block_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - block = Fabricate(:block) - - time_before = Time.zone.now - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - time_after = Time.zone.now - - expect(block_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - block = Fabricate(:block, account: account, target_account: target_account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - expect(block_salmon.title.text).to eq 'account no longer wishes to interact with target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'account') - block = Fabricate(:block, account: account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - expect(block_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' - end - - it 'appends activity:object-type element with activity type' do - block = Fabricate(:block) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with block' do - block = Fabricate(:block) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:block] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - block = Fabricate(:block, target_account: target_account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - object = block_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#unblock_salmon' do - include_examples 'namespaces' do - def serialize - block = Fabricate(:block) - OStatus::AtomSerializer.new.unblock_salmon(block) - end - end - - it 'returns entry element' do - block = Fabricate(:block) - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - expect(unblock_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - block = Fabricate(:block) - - time_before = Time.zone.now - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - time_after = Time.zone.now - - expect(unblock_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - block = Fabricate(:block, account: account, target_account: target_account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - expect(unblock_salmon.title.text).to eq 'account no longer blocks target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'account') - block = Fabricate(:block, account: account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - expect(unblock_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' - end - - it 'appends activity:object-type element with activity type' do - block = Fabricate(:block) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with block' do - block = Fabricate(:block) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unblock] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - block = Fabricate(:block, target_account: target_account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#favourite_salmon' do - include_examples 'namespaces' do - def serialize - favourite = Fabricate(:favourite) - OStatus::AtomSerializer.new.favourite_salmon(favourite) - end - end - - it 'returns entry element' do - favourite = Fabricate(:favourite) - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - expect(favourite_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - favourite = Fabricate(:favourite, created_at: '2000-01-01T00:00:00Z') - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - expect(favourite_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{favourite.id}:objectType=Favourite" - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - favourite = Fabricate(:favourite, account: account) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - expect(favourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - favourite = Fabricate(:favourite) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - object_type = favourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' - end - - it 'appends activity:verb element with favorite' do - favourite = Fabricate(:favourite) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:favorite] - end - - it 'appends activity:object element with status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends thr:in-reply-to element for status' do - status_account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - status_account = Fabricate(:account, domain: 'remote', username: 'status_account') - status = Fabricate(:status, account: status_account) - favourite = Fabricate(:favourite, account: account, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' - expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' - end - end - - describe '#unfavourite_salmon' do - include_examples 'namespaces' do - def serialize - favourite = Fabricate(:favourite) - OStatus::AtomSerializer.new.favourite_salmon(favourite) - end - end - - it 'returns entry element' do - favourite = Fabricate(:favourite) - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - expect(unfavourite_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - favourite = Fabricate(:favourite) - - time_before = Time.zone.now - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - time_after = Time.zone.now - - expect(unfavourite_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite'))) - ) - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - favourite = Fabricate(:favourite, account: account) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - expect(unfavourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - favourite = Fabricate(:favourite) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - object_type = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' - end - - it 'appends activity:verb element with favorite' do - favourite = Fabricate(:favourite) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unfavorite] - end - - it 'appends activity:object element with status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends thr:in-reply-to element for status' do - status_account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - status_account = Fabricate(:account, domain: 'remote', username: 'status_account') - status = Fabricate(:status, account: status_account) - favourite = Fabricate(:favourite, account: account, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' - expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' - end - end - - describe '#follow_salmon' do - include_examples 'namespaces' do - def serialize - follow = Fabricate(:follow) - OStatus::AtomSerializer.new.follow_salmon(follow) - end - end - - it 'returns entry element' do - follow = Fabricate(:follow) - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - expect(follow_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - follow = Fabricate(:follow, created_at: '2000-01-01T00:00:00Z') - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - expect(follow_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow.id}:objectType=Follow" - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow = Fabricate(:follow, account: account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - expect(follow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow = Fabricate(:follow) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with follow' do - follow = Fabricate(:follow) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:follow] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow = Fabricate(:follow, target_account: target_account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - object = follow_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - expect(follow_salmon.title.text).to eq 'account started following target_account@remote' - expect(follow_salmon.content.text).to eq 'account started following target_account@remote' - end - end - - describe '#unfollow_salmon' do - include_examples 'namespaces' do - def serialize - follow = Fabricate(:follow) - follow.destroy! - OStatus::AtomSerializer.new.unfollow_salmon(follow) - end - end - - it 'returns entry element' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - follow = Fabricate(:follow) - follow.destroy! - - time_before = Time.zone.now - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - time_after = Time.zone.now - - expect(unfollow_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.title.text).to eq 'account is no longer following target_account@remote' - end - - it 'appends content element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.content.text).to eq 'account is no longer following target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow = Fabricate(:follow, account: account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with follow' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unfollow] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow = Fabricate(:follow, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.follow_request_salmon(follow_request) - end - end - - context do - def serialize(follow_request) - OStatus::AtomSerializer.new.follow_request_salmon(follow_request) - end - - it_behaves_like 'follow request salmon' - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request, created_at: '2000-01-01T00:00:00Z') - follow_request_salmon = serialize(follow_request) - expect(follow_request_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow_request.id}:objectType=FollowRequest" - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - follow_request_salmon = serialize(follow_request) - expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' - end - end - end - - describe '#authorize_follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - end - end - - it_behaves_like 'follow request salmon' do - def serialize(follow_request) - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - end - end - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request) - - time_before = Time.zone.now - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - time_after = Time.zone.now - - expect(authorize_follow_request_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: 'remote', username: 'account') - target_account = Fabricate(:account, domain: nil, username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - expect(authorize_follow_request_salmon.title.text).to eq 'target_account authorizes follow request by account@remote' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with authorize' do - follow_request = Fabricate(:follow_request) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] - end - end - - describe '#reject_follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - end - end - - it_behaves_like 'follow request salmon' do - def serialize(follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - end - end - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request) - - time_before = Time.zone.now - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - time_after = Time.zone.now - - expect(reject_follow_request_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: 'remote', username: 'account') - target_account = Fabricate(:account, domain: nil, username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - expect(reject_follow_request_salmon.title.text).to eq 'target_account rejects follow request by account@remote' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with authorize' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] - end - end - - describe '#object' do - include_examples 'status attributes' do - def serialize(status) - OStatus::AtomSerializer.new.object(status) - end - end - - it 'returns activity:object element' do - status = Fabricate(:status) - object = OStatus::AtomSerializer.new.object(status) - expect(object.name).to eq 'activity:object' - end - - it 'appends id element with URL for status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - object = OStatus::AtomSerializer.new.object(status) - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends published element with created date' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - object = OStatus::AtomSerializer.new.object(status) - expect(object.published.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends updated element with updated date' do - status = Fabricate(:status) - status.updated_at = '2000-01-01T00:00:00Z' - object = OStatus::AtomSerializer.new.object(status) - expect(object.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends title element with title' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - object = OStatus::AtomSerializer.new.object(status) - - expect(object.title.text).to eq 'New status by username' - end - - it 'appends author element with account' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.object(status) - - expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with object type' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.object(status) - - object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] - end - - it 'appends activity:verb element with verb' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.object(status) - - object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.object(status) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'appends thr:in-reply-to element if it is a reply and thread is not nil' do - account = Fabricate(:account, username: 'username') - thread = Fabricate(:status, account: account, created_at: '2000-01-01T00:00:00Z') - reply = Fabricate(:status, thread: thread) - - entry = OStatus::AtomSerializer.new.object(reply) - - in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}" - end - - it 'does not append thr:in-reply-to element if thread is nil' do - status = Fabricate(:status, thread: nil) - entry = OStatus::AtomSerializer.new.object(status) - entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } - end - - it 'does not append ostatus:conversation element if conversation_id is nil' do - status = Fabricate.build(:status, conversation_id: nil) - status.save!(validate: false) - - entry = OStatus::AtomSerializer.new.object(status) - - entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } - end - - it 'appends ostatus:conversation element if conversation_id is not nil' do - status = Fabricate(:status) - status.conversation.update!(created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.object(status) - - conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } - expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation.id}:objectType=Conversation" - end - end -end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb index 6b4ee434f..61483f4bf 100644 --- a/spec/lib/status_finder_spec.rb +++ b/spec/lib/status_finder_spec.rb @@ -25,15 +25,6 @@ describe StatusFinder do end end - context 'with a stream entry url' do - let(:stream_entry) { Fabricate(:stream_entry) } - let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } - - it 'finds the stream entry' do - expect(subject.status).to eq(stream_entry.status) - end - end - context 'with a remote url even if id exists on local' do let(:status) { Fabricate(:status) } let(:url) { "https://example.com/users/test/statuses/#{status.id}" } diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 3a804ac0f..e9a7aa934 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -119,46 +119,4 @@ RSpec.describe TagManager do expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false end end - - describe '#url_for' do - let(:alice) { Fabricate(:account, username: 'alice') } - - subject { TagManager.instance.url_for(target) } - - context 'activity object' do - let(:target) { Fabricate(:status, account: alice, reblog: Fabricate(:status)).stream_entry } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :activity - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'comment object' do - let(:target) { Fabricate(:status, account: alice, reply: true) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :comment - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'note object' do - let(:target) { Fabricate(:status, account: alice, reply: false, thread: nil) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :note - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'person object' do - let(:target) { alice } - - it 'returns the URL for account' do - expect(target.object_type).to eq :person - is_expected.to eq 'https://cb6e6126.ngrok.io/@alice' - end - end - end end diff --git a/spec/models/concerns/streamable_spec.rb b/spec/models/concerns/streamable_spec.rb deleted file mode 100644 index b5f2d5192..000000000 --- a/spec/models/concerns/streamable_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Streamable do - class Parent - def title; end - - def target; end - - def thread; end - - def self.has_one(*); end - - def self.after_create; end - end - - class Child < Parent - include Streamable - end - - child = Child.new - - describe '#title' do - it 'calls Parent#title' do - expect_any_instance_of(Parent).to receive(:title) - child.title - end - end - - describe '#content' do - it 'calls #title' do - expect_any_instance_of(Parent).to receive(:title) - child.content - end - end - - describe '#target' do - it 'calls Parent#target' do - expect_any_instance_of(Parent).to receive(:target) - child.target - end - end - - describe '#object_type' do - it 'returns :activity' do - expect(child.object_type).to eq :activity - end - end - - describe '#thread' do - it 'calls Parent#thread' do - expect_any_instance_of(Parent).to receive(:thread) - child.thread - end - end - - describe '#hidden?' do - it 'returns false' do - expect(child.hidden?).to be false - end - end -end diff --git a/spec/models/remote_profile_spec.rb b/spec/models/remote_profile_spec.rb deleted file mode 100644 index da5048f0a..000000000 --- a/spec/models/remote_profile_spec.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoteProfile do - let(:remote_profile) { RemoteProfile.new(body) } - let(:body) do - <<-XML - - John - XML - end - - describe '.initialize' do - it 'calls Nokogiri::XML.parse' do - expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8') - RemoteProfile.new(body) - end - - it 'sets document' do - remote_profile = RemoteProfile.new(body) - expect(remote_profile).not_to be nil - end - end - - describe '#root' do - let(:document) { remote_profile.document } - - it 'callse document.at_xpath' do - expect(document).to receive(:at_xpath).with( - '/atom:feed|/atom:entry', - atom: OStatus::TagManager::XMLNS - ) - - remote_profile.root - end - end - - describe '#author' do - let(:root) { remote_profile.root } - - it 'calls root.at_xpath' do - expect(root).to receive(:at_xpath).with( - './atom:author|./dfrn:owner', - atom: OStatus::TagManager::XMLNS, - dfrn: OStatus::TagManager::DFRN_XMLNS - ) - - remote_profile.author - end - end - - describe '#hub_link' do - let(:root) { remote_profile.root } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub') - remote_profile.hub_link - end - end - - describe '#display_name' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './poco:displayName', - poco: OStatus::TagManager::POCO_XMLNS - ).with(no_args) - - remote_profile.display_name - end - end - - describe '#note' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './atom:summary|./poco:note', - atom: OStatus::TagManager::XMLNS, - poco: OStatus::TagManager::POCO_XMLNS - ).with(no_args) - - remote_profile.note - end - end - - describe '#scope' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './mastodon:scope', - mastodon: OStatus::TagManager::MTDN_XMLNS - ).with(no_args) - - remote_profile.scope - end - end - - describe '#avatar' do - let(:author) { remote_profile.author } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar') - remote_profile.avatar - end - end - - describe '#header' do - let(:author) { remote_profile.author } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header') - remote_profile.header - end - end - - describe '#locked?' do - before do - allow(remote_profile).to receive(:scope).and_return(scope) - end - - subject { remote_profile.locked? } - - context 'scope is private' do - let(:scope) { 'private' } - - it 'returns true' do - is_expected.to be true - end - end - - context 'scope is not private' do - let(:scope) { 'public' } - - it 'returns false' do - is_expected.to be false - end - end - end -end diff --git a/spec/models/stream_entry_spec.rb b/spec/models/stream_entry_spec.rb deleted file mode 100644 index 8f8bfbd58..000000000 --- a/spec/models/stream_entry_spec.rb +++ /dev/null @@ -1,192 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntry, type: :model do - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:status) { Fabricate(:status, account: alice) } - let(:reblog) { Fabricate(:status, account: bob, reblog: status) } - let(:reply) { Fabricate(:status, account: bob, thread: status) } - let(:stream_entry) { Fabricate(:stream_entry, activity: activity) } - let(:activity) { reblog } - - describe '#object_type' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - allow(stream_entry).to receive(:targeted?).and_return(targeted) - end - - subject { stream_entry.object_type } - - context 'orphaned? is true' do - let(:orphaned) { true } - let(:targeted) { false } - - it 'returns :activity' do - is_expected.to be :activity - end - end - - context 'targeted? is true' do - let(:orphaned) { false } - let(:targeted) { true } - - it 'returns :activity' do - is_expected.to be :activity - end - end - - context 'orphaned? and targeted? are false' do - let(:orphaned) { false } - let(:targeted) { false } - - context 'activity is reblog' do - let(:activity) { reblog } - - it 'returns :note' do - is_expected.to be :note - end - end - - context 'activity is reply' do - let(:activity) { reply } - - it 'returns :comment' do - is_expected.to be :comment - end - end - end - end - - describe '#verb' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - end - - subject { stream_entry.verb } - - context 'orphaned? is true' do - let(:orphaned) { true } - - it 'returns :delete' do - is_expected.to be :delete - end - end - - context 'orphaned? is false' do - let(:orphaned) { false } - - context 'activity is reblog' do - let(:activity) { reblog } - - it 'returns :share' do - is_expected.to be :share - end - end - - context 'activity is reply' do - let(:activity) { reply } - - it 'returns :post' do - is_expected.to be :post - end - end - end - end - - describe '#mentions' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - end - - subject { stream_entry.mentions } - - context 'orphaned? is true' do - let(:orphaned) { true } - - it 'returns []' do - is_expected.to eq [] - end - end - - context 'orphaned? is false' do - before do - reblog.mentions << Fabricate(:mention, account: alice) - reblog.mentions << Fabricate(:mention, account: bob) - end - - let(:orphaned) { false } - - it 'returns [Account] includes alice and bob' do - is_expected.to eq [alice, bob] - end - end - end - - describe '#targeted?' do - it 'returns true for a reblog' do - expect(reblog.stream_entry.targeted?).to be true - end - - it 'returns false otherwise' do - expect(status.stream_entry.targeted?).to be false - end - end - - describe '#threaded?' do - it 'returns true for a reply' do - expect(reply.stream_entry.threaded?).to be true - end - - it 'returns false otherwise' do - expect(status.stream_entry.threaded?).to be false - end - end - - describe 'delegated methods' do - context 'with a nil status' do - subject { described_class.new(status: nil) } - - it 'returns nil for target' do - expect(subject.target).to be_nil - end - - it 'returns nil for title' do - expect(subject.title).to be_nil - end - - it 'returns nil for content' do - expect(subject.content).to be_nil - end - - it 'returns nil for thread' do - expect(subject.thread).to be_nil - end - end - - context 'with a real status' do - let(:original) { Fabricate(:status, text: 'Test status') } - let(:status) { Fabricate(:status, reblog: original, thread: original) } - subject { described_class.new(status: status) } - - it 'delegates target' do - expect(status.target).not_to be_nil - expect(subject.target).to eq(status.target) - end - - it 'delegates title' do - expect(status.title).not_to be_nil - expect(subject.title).to eq(status.title) - end - - it 'delegates content' do - expect(status.content).not_to be_nil - expect(subject.content).to eq(status.content) - end - - it 'delegates thread' do - expect(status.thread).not_to be_nil - expect(subject.thread).to eq(status.thread) - end - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 35a804f2b..b1abd79b0 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -15,8 +15,8 @@ RSpec.describe ProcessMentionsService, type: :service do subject.call(status) end - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 + it 'does not create a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 0 end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 6f45762aa..896ac17a3 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -27,14 +27,13 @@ RSpec.describe SuspendAccountService, type: :service do [ account.statuses, account.media_attachments, - account.stream_entries, account.notifications, account.favourites, account.active_relationships, account.passive_relationships, account.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) end it 'sends a delete actor activity to all known inboxes' do @@ -70,14 +69,13 @@ RSpec.describe SuspendAccountService, type: :service do [ remote_bob.statuses, remote_bob.media_attachments, - remote_bob.stream_entries, remote_bob.notifications, remote_bob.favourites, remote_bob.active_relationships, remote_bob.passive_relationships, remote_bob.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) end it 'sends a reject follow to follwer inboxes' do diff --git a/spec/views/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb new file mode 100644 index 000000000..dbda3b665 --- /dev/null +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'statuses/show.html.haml', without_verify_partial_doubles: true do + before do + double(:api_oembed_url => '') + allow(view).to receive(:show_landing_strip?).and_return(true) + allow(view).to receive(:site_title).and_return('example site') + allow(view).to receive(:site_hostname).and_return('example.com') + allow(view).to receive(:full_asset_url).and_return('//asset.host/image.svg') + allow(view).to receive(:local_time) + allow(view).to receive(:local_time_ago) + allow(view).to receive(:current_account).and_return(nil) + assign(:instance_presenter, InstancePresenter.new) + end + + it 'has valid author h-card and basic data for a detailed_status' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + bob = Fabricate(:account, username: 'bob', display_name: 'Bob') + status = Fabricate(:status, account: alice, text: 'Hello World') + reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') + + assign(:status, status) + assign(:account, alice) + assign(:descendant_threads, []) + + render + + mf2 = Microformats.parse(rendered) + + expect(mf2.entry.url.to_s).not_to be_empty + expect(mf2.entry.author.name.to_s).to eq alice.display_name + expect(mf2.entry.author.url.to_s).not_to be_empty + end + + it 'has valid h-cites for p-in-reply-to and p-comment' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + bob = Fabricate(:account, username: 'bob', display_name: 'Bob') + carl = Fabricate(:account, username: 'carl', display_name: 'Carl') + status = Fabricate(:status, account: alice, text: 'Hello World') + reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') + comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') + + assign(:status, reply) + assign(:account, alice) + assign(:ancestors, reply.ancestors(1, bob)) + assign(:descendant_threads, [{ statuses: reply.descendants(1) }]) + + render + + mf2 = Microformats.parse(rendered) + + expect(mf2.entry.url.to_s).not_to be_empty + expect(mf2.entry.comment.url.to_s).not_to be_empty + expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name + expect(mf2.entry.comment.author.url.to_s).not_to be_empty + + expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty + expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name + expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty + end + + it 'has valid opengraph tags' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + status = Fabricate(:status, account: alice, text: 'Hello World') + + assign(:status, status) + assign(:account, alice) + assign(:descendant_threads, []) + + render + + header_tags = view.content_for(:header_tags) + + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + end +end diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb deleted file mode 100644 index 93f0adb99..000000000 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true do - before do - double(:api_oembed_url => '') - double(:account_stream_entry_url => '') - allow(view).to receive(:show_landing_strip?).and_return(true) - allow(view).to receive(:site_title).and_return('example site') - allow(view).to receive(:site_hostname).and_return('example.com') - allow(view).to receive(:full_asset_url).and_return('//asset.host/image.svg') - allow(view).to receive(:local_time) - allow(view).to receive(:local_time_ago) - allow(view).to receive(:current_account).and_return(nil) - assign(:instance_presenter, InstancePresenter.new) - end - - it 'has valid author h-card and basic data for a detailed_status' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - status = Fabricate(:status, account: alice, text: 'Hello World') - reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') - - assign(:status, status) - assign(:stream_entry, status.stream_entry) - assign(:account, alice) - assign(:type, status.stream_entry.activity_type.downcase) - assign(:descendant_threads, []) - - render - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.author.name.to_s).to eq alice.display_name - expect(mf2.entry.author.url.to_s).not_to be_empty - end - - it 'has valid h-cites for p-in-reply-to and p-comment' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - carl = Fabricate(:account, username: 'carl', display_name: 'Carl') - status = Fabricate(:status, account: alice, text: 'Hello World') - reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') - comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') - - assign(:status, reply) - assign(:stream_entry, reply.stream_entry) - assign(:account, alice) - assign(:type, reply.stream_entry.activity_type.downcase) - assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob)) - assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }]) - - render - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.comment.url.to_s).not_to be_empty - expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name - expect(mf2.entry.comment.author.url.to_s).not_to be_empty - - expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty - expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name - expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty - end - - it 'has valid opengraph tags' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - status = Fabricate(:status, account: alice, text: 'Hello World') - - assign(:status, status) - assign(:stream_entry, status.stream_entry) - assign(:account, alice) - assign(:type, status.stream_entry.activity_type.downcase) - assign(:descendant_threads, []) - - render - - header_tags = view.content_for(:header_tags) - - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - end -end -- cgit From 63c7fe8e4892b22e80c015bf0ecb04496318623b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 8 Jul 2019 12:03:45 +0200 Subject: Refactor controllers for statuses, accounts, and more (#11249) --- app/controllers/about_controller.rb | 16 +- app/controllers/accounts_controller.rb | 15 +- .../activitypub/collections_controller.rb | 16 +- app/controllers/activitypub/inboxes_controller.rb | 7 +- app/controllers/activitypub/outboxes_controller.rb | 6 +- app/controllers/activitypub/replies_controller.rb | 68 +++++++++ app/controllers/api/proofs_controller.rb | 17 +-- app/controllers/application_controller.rb | 4 - .../concerns/account_controller_concern.rb | 34 +---- app/controllers/concerns/account_owned_concern.rb | 33 +++++ .../concerns/status_controller_concern.rb | 87 +++++++++++ app/controllers/custom_css_controller.rb | 1 + app/controllers/emojis_controller.rb | 5 +- app/controllers/follower_accounts_controller.rb | 2 +- app/controllers/following_accounts_controller.rb | 2 +- app/controllers/home_controller.rb | 2 +- app/controllers/intents_controller.rb | 1 + app/controllers/manifests_controller.rb | 1 + app/controllers/media_controller.rb | 1 - app/controllers/public_timelines_controller.rb | 14 +- app/controllers/remote_follow_controller.rb | 12 +- app/controllers/statuses_controller.rb | 164 ++------------------- app/controllers/tags_controller.rb | 18 ++- app/controllers/well_known/host_meta_controller.rb | 2 +- app/controllers/well_known/webfinger_controller.rb | 9 +- app/lib/activitypub/activity/announce.rb | 2 +- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/activity/delete.rb | 2 +- app/lib/activitypub/tag_manager.rb | 2 +- app/models/status.rb | 9 +- app/serializers/activitypub/activity_serializer.rb | 3 + app/serializers/activitypub/actor_serializer.rb | 2 + .../activitypub/collection_serializer.rb | 2 + app/serializers/activitypub/emoji_serializer.rb | 2 + app/serializers/activitypub/note_serializer.rb | 2 + app/services/process_hashtags_service.rb | 2 +- app/views/statuses/_simple_status.html.haml | 4 +- config/routes.rb | 3 +- .../concerns/account_controller_concern_spec.rb | 2 +- spec/controllers/statuses_controller_spec.rb | 4 +- spec/requests/link_headers_spec.rb | 8 +- 41 files changed, 299 insertions(+), 289 deletions(-) create mode 100644 app/controllers/activitypub/replies_controller.rb create mode 100644 app/controllers/concerns/account_owned_concern.rb create mode 100644 app/controllers/concerns/status_controller_concern.rb (limited to 'config') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 52a51fd62..761c7f5cd 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,11 +3,11 @@ class AboutController < ApplicationController layout 'public' - before_action :set_instance_presenter, only: [:show, :more, :terms] + before_action :set_body_classes, only: :show + before_action :set_instance_presenter + before_action :set_expires_in - def show - @hide_navbar = true - end + def show; end def more; end @@ -27,4 +27,12 @@ class AboutController < ApplicationController def set_instance_presenter @instance_presenter = InstancePresenter.new end + + def set_body_classes + @hide_navbar = true + end + + def set_expires_in + expires_in 0, public: true + end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 065707378..3184a73cb 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -6,13 +6,13 @@ class AccountsController < ApplicationController include AccountControllerConcern before_action :set_cache_headers + before_action :set_body_classes def show respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? - @body_classes = 'with-modals' @pinned_statuses = [] @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @@ -32,22 +32,25 @@ class AccountsController < ApplicationController end format.rss do - mark_cacheable! + expires_in 0, public: true @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) render xml: RSS::AccountSerializer.render(@account, @statuses) end format.json do - render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: true + render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter end end end private + def set_body_classes + @body_classes = 'with-modals' + end + def show_pinned_statuses? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 012c3c538..dd2f111b0 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -2,29 +2,19 @@ class ActivityPub::CollectionsController < Api::BaseController include SignatureVerification + include AccountOwnedConcern - before_action :set_account before_action :set_size before_action :set_statuses before_action :set_cache_headers def show - render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new( - collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - skip_activities: true - ) - end + expires_in 3.minutes, public: true + render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true end private - def set_account - @account = Account.find_local!(params[:account_username]) - end - def set_statuses @statuses = scope_for_collection @statuses = cache_collection(@statuses, Status) diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index e2cd8eaed..9be0676e1 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -3,8 +3,7 @@ class ActivityPub::InboxesController < Api::BaseController include SignatureVerification include JsonLdHelper - - before_action :set_account + include AccountOwnedConcern def create if unknown_deleted_account? @@ -27,8 +26,8 @@ class ActivityPub::InboxesController < Api::BaseController false end - def set_account - @account = Account.find_local!(params[:account_username]) if params[:account_username] + def account_required? + params[:account_username].present? end def body diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 5147afbf7..4c0b769f0 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -4,8 +4,8 @@ class ActivityPub::OutboxesController < Api::BaseController LIMIT = 20 include SignatureVerification + include AccountOwnedConcern - before_action :set_account before_action :set_statuses before_action :set_cache_headers @@ -17,10 +17,6 @@ class ActivityPub::OutboxesController < Api::BaseController private - def set_account - @account = Account.find_local!(params[:account_username]) - end - def outbox_presenter if page_requested? ActivityPub::CollectionPresenter.new( diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb new file mode 100644 index 000000000..99b7b310f --- /dev/null +++ b/app/controllers/activitypub/replies_controller.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class ActivityPub::RepliesController < Api::BaseController + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + DESCENDANTS_LIMIT = 60 + + before_action :set_status + before_action :set_cache_headers + before_action :set_replies + + def index + render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true + end + + private + + def set_status + @status = @account.statuses.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + raise ActiveRecord::RecordNotFound + end + + def set_replies + @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) + end + + def replies_collection_presenter + page = ActivityPub::CollectionPresenter.new( + id: account_status_replies_url(@account, @status, page_params), + type: :unordered, + part_of: account_status_replies_url(@account, @status), + next: next_page, + items: @replies.map { |status| status.local ? status : status.id } + ) + + return page if page_requested? + + ActivityPub::CollectionPresenter.new( + id: account_status_replies_url(@account, @status), + type: :unordered, + first: page + ) + end + + def page_requested? + params[:page] == 'true' + end + + def next_page + account_status_replies_url( + @account, + @status, + page: true, + min_id: @replies&.last&.id, + other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) + ) + end + + def page_params + params_slice(:other_accounts, :min_id).merge(page: true) + end +end diff --git a/app/controllers/api/proofs_controller.rb b/app/controllers/api/proofs_controller.rb index a84ad2014..a98599eee 100644 --- a/app/controllers/api/proofs_controller.rb +++ b/app/controllers/api/proofs_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class Api::ProofsController < Api::BaseController - before_action :set_account + include AccountOwnedConcern + before_action :set_provider - before_action :check_account_approval - before_action :check_account_suspension def index render json: @account, serializer: @provider.serializer_class @@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) end - def set_account - @account = Account.find_local!(params[:username]) - end - - def check_account_approval - not_found if @account.user_pending? - end - - def check_account_suspension - gone if @account.suspended? + def username_param + params[:username] end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd8000db0..cc8b8e4da 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -154,8 +154,4 @@ class ApplicationController < ActionController::Base def set_cache_headers response.headers['Vary'] = 'Accept' end - - def mark_cacheable! - expires_in 0, public: true - end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 1c422096c..287a930da 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -3,24 +3,19 @@ module AccountControllerConcern extend ActiveSupport::Concern + include AccountOwnedConcern + FOLLOW_PER_PAGE = 12 included do layout 'public' - before_action :set_account - before_action :check_account_approval - before_action :check_account_suspension before_action :set_instance_presenter before_action :set_link_headers end private - def set_account - @account = Account.find_local!(username_param) - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end @@ -29,27 +24,15 @@ module AccountControllerConcern response.headers['Link'] = LinkHeader.new( [ webfinger_account_link, - atom_account_url_link, actor_url_link, ] ) end - def username_param - params[:account_username] - end - def webfinger_account_link [ webfinger_account_url, - [%w(rel lrdd), %w(type application/xrd+xml)], - ] - end - - def atom_account_url_link - [ - account_url(@account, format: 'atom'), - [%w(rel alternate), %w(type application/atom+xml)], + [%w(rel lrdd), %w(type application/jrd+json)], ] end @@ -63,15 +46,4 @@ module AccountControllerConcern def webfinger_account_url webfinger_url(resource: @account.to_webfinger_s) end - - def check_account_approval - not_found if @account.user_pending? - end - - def check_account_suspension - if @account.suspended? - expires_in(3.minutes, public: true) - gone - end - end end diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb new file mode 100644 index 000000000..99c240fe9 --- /dev/null +++ b/app/controllers/concerns/account_owned_concern.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module AccountOwnedConcern + extend ActiveSupport::Concern + + included do + before_action :set_account, if: :account_required? + before_action :check_account_approval, if: :account_required? + before_action :check_account_suspension, if: :account_required? + end + + private + + def account_required? + true + end + + def set_account + @account = Account.find_local!(username_param) + end + + def username_param + params[:account_username] + end + + def check_account_approval + not_found if @account.local? && @account.user_pending? + end + + def check_account_suspension + expires_in(3.minutes, public: true) && gone if @account.suspended? + end +end diff --git a/app/controllers/concerns/status_controller_concern.rb b/app/controllers/concerns/status_controller_concern.rb new file mode 100644 index 000000000..62a7cf508 --- /dev/null +++ b/app/controllers/concerns/status_controller_concern.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module StatusControllerConcern + extend ActiveSupport::Concern + + ANCESTORS_LIMIT = 40 + DESCENDANTS_LIMIT = 60 + DESCENDANTS_DEPTH_LIMIT = 20 + + def create_descendant_thread(starting_depth, statuses) + depth = starting_depth + statuses.size + + if depth < DESCENDANTS_DEPTH_LIMIT + { + statuses: statuses, + starting_depth: starting_depth, + } + else + next_status = statuses.pop + + { + statuses: statuses, + starting_depth: starting_depth, + next_status: next_status, + } + end + end + + def set_ancestors + @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] + @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift + end + + def set_descendants + @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i + @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i + + descendants = cache_collection( + @status.descendants( + DESCENDANTS_LIMIT, + current_account, + @max_descendant_thread_id, + @since_descendant_thread_id, + DESCENDANTS_DEPTH_LIMIT + ), + Status + ) + + @descendant_threads = [] + + if descendants.present? + statuses = [descendants.first] + starting_depth = 0 + + descendants.drop(1).each_with_index do |descendant, index| + if descendants[index].id == descendant.in_reply_to_id + statuses << descendant + else + @descendant_threads << create_descendant_thread(starting_depth, statuses) + + # The thread is broken, assume it's a reply to the root status + starting_depth = 0 + + # ... unless we can find its ancestor in one of the already-processed threads + @descendant_threads.reverse_each do |descendant_thread| + statuses = descendant_thread[:statuses] + + index = statuses.find_index do |thread_status| + thread_status.id == descendant.in_reply_to_id + end + + if index.present? + starting_depth = descendant_thread[:starting_depth] + index + 1 + break + end + end + + statuses = [descendant] + end + end + + @descendant_threads << create_descendant_thread(starting_depth, statuses) + end + + @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT + end +end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 6e80feaf8..7f4dcfcfe 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -6,6 +6,7 @@ class CustomCssController < ApplicationController before_action :set_cache_headers def show + expires 3.minutes, public: true render plain: Setting.custom_css || '', content_type: 'text/css' end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 3feb08132..fe4c19cad 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -7,9 +7,8 @@ class EmojisController < ApplicationController def show respond_to do |format| format.json do - render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: true + render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 415abe10c..8baa64490 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? next if @account.user_hides_network? diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 948725664..4d1ea4594 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? next if @account.user_hides_network? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 85622a7b5..d1c525134 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -21,7 +21,7 @@ class HomeController < ApplicationController when 'statuses' status = Status.find_by(id: matches[2]) - if status && (status.public_visibility? || status.unlisted_visibility?) + if status&.distributable? redirect_to(ActivityPub::TagManager.instance.url_for(status)) return end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index 9f41cf48a..ca89fc7fe 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -2,6 +2,7 @@ class IntentsController < ApplicationController before_action :check_uri + rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri def show diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 332d845d8..1e5db4393 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -4,6 +4,7 @@ class ManifestsController < ApplicationController skip_before_action :store_current_location def show + expires_in 3.minutes, public: true render json: InstancePresenter.new, serializer: ManifestSerializer end end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index d44b52d26..b3b7519a1 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -31,7 +31,6 @@ class MediaController < ApplicationController def verify_permitted_status! authorize @media_attachment.status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 53d4472d8..23506b990 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -8,20 +8,16 @@ class PublicTimelinesController < ApplicationController before_action :set_instance_presenter def show - respond_to do |format| - format.html do - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json - end - end + @initial_state_json = ActiveModelSerializers::SerializableResource.new( + InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), + serializer: InitialStateSerializer + ).to_json end private def check_enabled - raise ActiveRecord::RecordNotFound unless Setting.timeline_preview + not_found unless Setting.timeline_preview end def set_body_classes diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 8ba331cd1..0fb71d335 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class RemoteFollowController < ApplicationController + include AccountOwnedConcern + layout 'modal' - before_action :set_account - before_action :gone, if: :suspended_account? before_action :set_body_classes def new @@ -32,14 +32,6 @@ class RemoteFollowController < ApplicationController { acct: session[:remote_follow] } end - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def suspended_account? - @account.suspended? - end - def set_body_classes @body_classes = 'modal-layout' @hide_header = true diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 776099ca8..13ce5c691 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,24 +1,21 @@ # frozen_string_literal: true class StatusesController < ApplicationController + include StatusControllerConcern include SignatureAuthentication include Authorization - - ANCESTORS_LIMIT = 40 - DESCENDANTS_LIMIT = 60 - DESCENDANTS_DEPTH_LIMIT = 20 + include AccountOwnedConcern layout 'public' - before_action :set_account before_action :set_status before_action :set_instance_presenter before_action :set_link_headers - before_action :check_account_suspension before_action :redirect_to_original, only: [:show] before_action :set_referrer_policy_header, only: [:show] before_action :set_cache_headers - before_action :set_replies, only: [:replies] + before_action :set_body_classes + before_action :set_autoplay, only: :embed content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -28,25 +25,20 @@ class StatusesController < ApplicationController respond_to do |format| format.html do expires_in 10.seconds, public: true if current_account.nil? - - @body_classes = 'with-modals' - set_ancestors set_descendants end format.json do - render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do - ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: @status.distributable? + render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end end def activity - render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do - ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: @status.distributable? + render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end def embed @@ -54,120 +46,14 @@ class StatusesController < ApplicationController expires_in 180, public: true response.headers['X-Frame-Options'] = 'ALLOWALL' - @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) render layout: 'embedded' end - def replies - render json: replies_collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json', - skip_activities: true - end - private - def replies_collection_presenter - page = ActivityPub::CollectionPresenter.new( - id: replies_account_status_url(@account, @status, page_params), - type: :unordered, - part_of: replies_account_status_url(@account, @status), - next: next_page, - items: @replies.map { |status| status.local ? status : status.id } - ) - if page_requested? - page - else - ActivityPub::CollectionPresenter.new( - id: replies_account_status_url(@account, @status), - type: :unordered, - first: page - ) - end - end - - def create_descendant_thread(starting_depth, statuses) - depth = starting_depth + statuses.size - - if depth < DESCENDANTS_DEPTH_LIMIT - { - statuses: statuses, - starting_depth: starting_depth, - } - else - next_status = statuses.pop - - { - statuses: statuses, - starting_depth: starting_depth, - next_status: next_status, - } - end - end - - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def set_ancestors - @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] - @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift - end - - def set_descendants - @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i - @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i - - descendants = cache_collection( - @status.descendants( - DESCENDANTS_LIMIT, - current_account, - @max_descendant_thread_id, - @since_descendant_thread_id, - DESCENDANTS_DEPTH_LIMIT - ), - Status - ) - - @descendant_threads = [] - - if descendants.present? - statuses = [descendants.first] - starting_depth = 0 - - descendants.drop(1).each_with_index do |descendant, index| - if descendants[index].id == descendant.in_reply_to_id - statuses << descendant - else - @descendant_threads << create_descendant_thread(starting_depth, statuses) - - # The thread is broken, assume it's a reply to the root status - starting_depth = 0 - - # ... unless we can find its ancestor in one of the already-processed threads - @descendant_threads.reverse_each do |descendant_thread| - statuses = descendant_thread[:statuses] - - index = statuses.find_index do |thread_status| - thread_status.id == descendant.in_reply_to_id - end - - if index.present? - starting_depth = descendant_thread[:starting_depth] + index + 1 - break - end - end - - statuses = [descendant] - end - end - - @descendant_threads << create_descendant_thread(starting_depth, statuses) - end - - @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT + def set_body_classes + @body_classes = 'with-modals' end def set_link_headers @@ -185,39 +71,15 @@ class StatusesController < ApplicationController @instance_presenter = InstancePresenter.new end - def check_account_suspension - gone if @account.suspended? - end - def redirect_to_original redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end def set_referrer_policy_header - return if @status.public_visibility? || @status.unlisted_visibility? - response.headers['Referrer-Policy'] = 'origin' - end - - def page_requested? - params[:page] == 'true' - end - - def set_replies - @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses - @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) - @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) - end - - def next_page - last_reply = @replies.last - return if last_reply.nil? - same_account = last_reply.account_id == @account.id - return unless same_account || @replies.size == DESCENDANTS_LIMIT - same_account = false unless @replies.size == DESCENDANTS_LIMIT - replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) + response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? end - def page_params - { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact + def set_autoplay + @autoplay = truthy_param?(:autoplay) end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 66b184901..2ecce0ca2 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -5,14 +5,15 @@ class TagsController < ApplicationController layout 'public' + before_action :set_tag before_action :set_body_classes before_action :set_instance_presenter def show - @tag = Tag.find_normalized!(params[:id]) - respond_to do |format| format.html do + expires_in 0, public: true + @initial_state_json = ActiveModelSerializers::SerializableResource.new( InitialStatePresenter.new(settings: {}, token: current_session&.token), serializer: InitialStateSerializer @@ -20,6 +21,8 @@ class TagsController < ApplicationController end format.rss do + expires_in 0, public: true + @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE) @statuses = cache_collection(@statuses, Status) @@ -27,19 +30,22 @@ class TagsController < ApplicationController end format.json do + expires_in 3.minutes, public: true + @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) - render json: collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json' + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end private + def set_tag + @tag = Tag.find_normalized!(params[:id]) + end + def set_body_classes @body_classes = 'with-modals' end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 5fb70288a..2e9298c4a 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -13,7 +13,7 @@ module WellKnown format.xml { render content_type: 'application/xrd+xml' } end - expires_in(3.days, public: true) + expires_in 3.days, public: true end end end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 28654b61d..53f7f1e27 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -19,7 +19,7 @@ module WellKnown end end - expires_in(3.days, public: true) + expires_in 3.days, public: true rescue ActiveRecord::RecordNotFound head 404 end @@ -27,12 +27,9 @@ module WellKnown private def username_from_resource - resource_user = resource_param - + resource_user = resource_param username, domain = resource_user.split('@') - if Rails.configuration.x.alternate_domains.include?(domain) - resource_user = "#{username}@#{Rails.configuration.x.local_domain}" - end + resource_user = "#{username}@#{Rails.configuration.x.local_domain}" if Rails.configuration.x.alternate_domains.include?(domain) WebfingerResource.new(resource_user).username end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 1aa6ee9ec..34c646668 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -40,7 +40,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity end def announceable?(status) - status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? + status.account_id == @account.id || status.distributable? end def related_to_local_activity? diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 00f0dd42d..5849c20d7 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -42,7 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) fetch_replies(@status) distribute(@status) - forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? + forward_for_reply if @status.distributable? end def find_existing_status diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 0eb14b89c..1f2b40c15 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -31,7 +31,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity return if @status.nil? - if @status.public_visibility? || @status.unlisted_visibility? + if @status.distributable? forward_for_reply forward_for_reblogs end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 595291342..4d452f290 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -51,7 +51,7 @@ class ActivityPub::TagManager def replies_uri_for(target, page_params = nil) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - replies_account_status_url(target.account, target, page_params) + account_status_replies_url(target.account, target, page_params) end # Primary audience of a status diff --git a/app/models/status.rb b/app/models/status.rb index 906756e85..6f1e35e4a 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -193,7 +193,7 @@ class Status < ApplicationRecord end def hidden? - private_visibility? || direct_visibility? || limited_visibility? + !distributable? end def distributable? @@ -446,7 +446,8 @@ class Status < ApplicationRecord end def update_statistics - return unless public_visibility? || unlisted_visibility? + return unless distributable? + ActivityTracker.increment('activity:statuses:local') end @@ -455,7 +456,7 @@ class Status < ApplicationRecord account&.increment_count!(:statuses_count) reblog&.increment_count!(:reblogs_count) if reblog? - thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) + thread&.increment_count!(:replies_count) if in_reply_to_id.present? && distributable? end def decrement_counter_caches @@ -463,7 +464,7 @@ class Status < ApplicationRecord account&.decrement_count!(:statuses_count) reblog&.decrement_count!(:reblogs_count) if reblog? - thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) + thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && distributable? end def unlink_from_conversations diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index c06d5c87c..fdedbc9d1 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true class ActivityPub::ActivitySerializer < ActivityPub::Serializer + cache key: 'activity', expires_in: 3.minutes + attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? + attribute :proper_uri, key: :object, unless: :serialize_object? attribute :atom_uri, if: :announce? diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0644219fb..ab7be27f6 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -3,6 +3,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer include RoutingHelper + cache key: 'actor', expires_in: 3.minutes + context :security context_extensions :manually_approves_followers, :featured, :also_known_as, diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index da1ba735f..9dd8134d3 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -7,6 +7,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer super end + cache key: 'collection', expires_in: 3.minutes + attribute :id, if: -> { object.id.present? } attribute :type attribute :total_items, if: -> { object.size.present? } diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 4dc38f3ea..08df25d7d 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -3,6 +3,8 @@ class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper + cache key: 'emoji', expires_in: 3.minutes + context_extensions :emoji attributes :id, :type, :name, :updated diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 67f596e78..87acc5429 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::NoteSerializer < ActivityPub::Serializer + cache key: 'note', expires_in: 3.minutes + context_extensions :atom_uri, :conversation, :sensitive, :hashtag, :emoji, :focal_point, :blurhash diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index d5ec076a8..b6974e598 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -14,7 +14,7 @@ class ProcessHashtagsService < BaseService TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end - return unless status.public_visibility? || status.unlisted_visibility? + return unless status.distributable? status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| featured_tag.increment(status.created_at) diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 11220dfcb..38fde1be8 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -50,9 +50,9 @@ = fa_icon 'reply-all fw' .status__action-bar__counter__label= obscured_counter status.replies_count = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - - if status.public_visibility? || status.unlisted_visibility? + - if status.distributable? = fa_icon 'retweet fw' - - elsif status.private_visibility? + - elsif status.private_visibility? || status.limited_visibility? = fa_icon 'lock fw' - else = fa_icon 'envelope fw' diff --git a/config/routes.rb b/config/routes.rb index 69b495a96..115e7bb44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,8 +52,9 @@ Rails.application.routes.draw do member do get :activity get :embed - get :replies end + + resources :replies, only: [:index], module: :activitypub end resources :followers, only: [:index], controller: :follower_accounts diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb index ea2b4a2a1..7ea214a7d 100644 --- a/spec/controllers/concerns/account_controller_concern_spec.rb +++ b/spec/controllers/concerns/account_controller_concern_spec.rb @@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do it 'sets link headers' do account = Fabricate(:account, username: 'username', user: Fabricate(:user)) get 'success', params: { account_username: 'username' } - expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml", ; rel="alternate"; type="application/activity+json"' + expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/jrd+json", ; rel="alternate"; type="application/activity+json"' end it 'returns http success' do diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 95e5c363c..6905dae10 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -92,7 +92,7 @@ describe StatusesController do end it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do - stub_const 'StatusesController::DESCENDANTS_LIMIT', 1 + stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1 status = Fabricate(:status) child = Fabricate(:status, in_reply_to_id: status.id) @@ -103,7 +103,7 @@ describe StatusesController do end it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do - stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 2 + stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2 status = Fabricate(:status) child0 = Fabricate(:status, in_reply_to_id: status.id) child1 = Fabricate(:status, in_reply_to_id: child0.id) diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb index 3dc408d92..712ee262b 100644 --- a/spec/requests/link_headers_spec.rb +++ b/spec/requests/link_headers_spec.rb @@ -11,16 +11,16 @@ describe 'Link headers' do end it 'contains webfinger url in link header' do - link_header = link_header_with_type('application/xrd+xml') + link_header = link_header_with_type('application/jrd+json') expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' expect(link_header.attr_pairs.first).to eq %w(rel lrdd) end - it 'contains atom url in link header' do - link_header = link_header_with_type('application/atom+xml') + it 'contains activitypub url in link header' do + link_header = link_header_with_type('application/activity+json') - expect(link_header.href).to eq 'http://www.example.com/users/test.atom' + expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test' expect(link_header.attr_pairs.first).to eq %w(rel alternate) end -- cgit From ef1524639776fe7d7be2d5c414fc98dd2410a5f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 8 Jul 2019 12:04:06 +0200 Subject: Remove unused remote unfollow controller (#11250) --- app/controllers/remote_unfollows_controller.rb | 39 ---------------------- app/views/remote_unfollows/_card.html.haml | 13 -------- .../_post_follow_actions.html.haml | 4 --- app/views/remote_unfollows/error.html.haml | 3 -- app/views/remote_unfollows/success.html.haml | 10 ------ config/locales/en.yml | 4 --- config/routes.rb | 2 -- .../remote_unfollows_controller_spec.rb | 38 --------------------- 8 files changed, 113 deletions(-) delete mode 100644 app/controllers/remote_unfollows_controller.rb delete mode 100644 app/views/remote_unfollows/_card.html.haml delete mode 100644 app/views/remote_unfollows/_post_follow_actions.html.haml delete mode 100644 app/views/remote_unfollows/error.html.haml delete mode 100644 app/views/remote_unfollows/success.html.haml delete mode 100644 spec/controllers/remote_unfollows_controller_spec.rb (limited to 'config') diff --git a/app/controllers/remote_unfollows_controller.rb b/app/controllers/remote_unfollows_controller.rb deleted file mode 100644 index af5943363..000000000 --- a/app/controllers/remote_unfollows_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class RemoteUnfollowsController < ApplicationController - layout 'modal' - - before_action :authenticate_user! - before_action :set_body_classes - - def create - @account = unfollow_attempt.try(:target_account) - - if @account.nil? - render :error - else - render :success - end - rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError - render :error - end - - private - - def unfollow_attempt - username, domain = acct_without_prefix.split('@') - UnfollowService.new.call(current_account, Account.find_remote!(username, domain)) - end - - def acct_without_prefix - acct_params.gsub(/\Aacct:/, '') - end - - def acct_params - params.fetch(:acct, '') - end - - def set_body_classes - @body_classes = 'modal-layout' - end -end diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml deleted file mode 100644 index 80ad3bae2..000000000 --- a/app/views/remote_unfollows/_card.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.account-card - .detailed-status__display-name - %div - = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' - - %span.display-name - - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) - = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account, custom_emojify: true) - %span @#{account.acct} - - - if account.note? - .account__header__content.emojify= Formatter.instance.simplified_format(account) diff --git a/app/views/remote_unfollows/_post_follow_actions.html.haml b/app/views/remote_unfollows/_post_follow_actions.html.haml deleted file mode 100644 index 328f7c833..000000000 --- a/app/views/remote_unfollows/_post_follow_actions.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.post-follow-actions - %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@account), class: 'button button--block' - %div= t('authorize_follow.post_follow.close') diff --git a/app/views/remote_unfollows/error.html.haml b/app/views/remote_unfollows/error.html.haml deleted file mode 100644 index cb63f02be..000000000 --- a/app/views/remote_unfollows/error.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.form-container - .flash-message#error_explanation - = t('remote_unfollow.error') diff --git a/app/views/remote_unfollows/success.html.haml b/app/views/remote_unfollows/success.html.haml deleted file mode 100644 index b007eedc7..000000000 --- a/app/views/remote_unfollows/success.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- content_for :page_title do - = t('remote_unfollow.title', acct: @account.acct) - -.form-container - .follow-prompt - %h2= t('remote_unfollow.unfollowed') - - = render 'application/card', account: @account - - = render 'post_follow_actions' diff --git a/config/locales/en.yml b/config/locales/en.yml index 611f36fdd..00b7d1dbe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -810,10 +810,6 @@ en: reply: proceed: Proceed to reply prompt: 'You want to reply to this toot:' - remote_unfollow: - error: Error - title: Title - unfollowed: Unfollowed scheduled_statuses: over_daily_limit: You have exceeded the limit of %{limit} scheduled toots for that day over_total_limit: You have exceeded the limit of %{limit} scheduled toots diff --git a/config/routes.rb b/config/routes.rb index 115e7bb44..95f8a39ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -141,8 +141,6 @@ Rails.application.routes.draw do get '/public', to: 'public_timelines#show', as: :public_timeline get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy - # Remote follow - resource :remote_unfollow, only: [:create] resource :authorize_interaction, only: [:show, :create] resource :share, only: [:show, :create] diff --git a/spec/controllers/remote_unfollows_controller_spec.rb b/spec/controllers/remote_unfollows_controller_spec.rb deleted file mode 100644 index a1a55ede0..000000000 --- a/spec/controllers/remote_unfollows_controller_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RemoteUnfollowsController do - render_views - - describe '#create' do - subject { post :create, params: { acct: acct } } - - let(:current_user) { Fabricate(:user, account: current_account) } - let(:current_account) { Fabricate(:account) } - let(:remote_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } - before do - sign_in current_user - current_account.follow!(remote_account) - stub_request(:post, 'http://example.com/inbox') { { status: 200 } } - end - - context 'when successfully unfollow remote account' do - let(:acct) { "acct:#{remote_account.username}@#{remote_account.domain}" } - - it do - is_expected.to render_template :success - expect(current_account.following?(remote_account)).to be false - end - end - - context 'when fails to unfollow remote account' do - let(:acct) { "acct:#{remote_account.username + '_test'}@#{remote_account.domain}" } - - it do - is_expected.to render_template :error - expect(current_account.following?(remote_account)).to be true - end - end - end -end -- cgit From 6ff67be0f6e79ec403e08c69717ee8c89451c70e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 13 Jul 2019 16:45:50 +0200 Subject: Add a spam check (#11217) * Add a spam check * Use Nilsimsa to generate locality-sensitive hashes and compare using Levenshtein distance * Add more tests * Add exemption when the message is a reply to something that mentions the sender * Use Nilsimsa Compare Value instead of Levenshtein distance * Use MD5 for messages shorter than 10 characters * Add message to automated report, do not add non-public statuses to automated report, add trust level to accounts and make unsilencing raise the trust level to prevent repeated spam checks on that account * Expire spam check data after 3 months * Add support for local statuses, reduce expiration to 1 week, always create a report * Add content warnings to the spam check and exempt empty statuses * Change Nilsimsa threshold to 95 and make sure removed statuses are removed from the spam check * Add all matched statuses into automatic report --- Gemfile | 1 + Gemfile.lock | 8 + app/lib/activitypub/activity/create.rb | 13 ++ app/lib/spam_check.rb | 169 +++++++++++++++++++++ app/models/account.rb | 18 ++- app/services/remove_status_service.rb | 5 + config/locales/en.yml | 2 + .../20190701022101_add_trust_level_to_accounts.rb | 5 + db/schema.rb | 1 + spec/lib/spam_check_spec.rb | 160 +++++++++++++++++++ 10 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 app/lib/spam_check.rb create mode 100644 db/migrate/20190701022101_add_trust_level_to_accounts.rb create mode 100644 spec/lib/spam_check_spec.rb (limited to 'config') diff --git a/Gemfile b/Gemfile index 613515628..15334678b 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' +gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' diff --git a/Gemfile.lock b/Gemfile.lock index 340bbcdd8..c3198b7d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,13 @@ GIT specs: http_parser.rb (0.6.1) +GIT + remote: https://github.com/witgo/nilsimsa + revision: fd184883048b922b176939f851338d0a4971a532 + ref: fd184883048b922b176939f851338d0a4971a532 + specs: + nilsimsa (1.1.2) + GEM remote: https://rubygems.org/ specs: @@ -704,6 +711,7 @@ DEPENDENCIES microformats (~> 4.1) mime-types (~> 3.2) net-ldap (~> 0.10) + nilsimsa! nokogiri (~> 1.10) nsa (~> 0.2) oj (~> 3.7) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 5849c20d7..56c24680a 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -41,6 +41,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) fetch_replies(@status) + check_for_spam distribute(@status) forward_for_reply if @status.distributable? end @@ -406,6 +407,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity Account.local.where(username: local_usernames).exists? end + def check_for_spam + spam_check = SpamCheck.new(@status) + + return if spam_check.skip? + + if spam_check.spam? + spam_check.flag! + else + spam_check.remember! + end + end + def forward_for_reply return unless @json['signature'].present? && reply_to_local? ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb new file mode 100644 index 000000000..923d48a02 --- /dev/null +++ b/app/lib/spam_check.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +class SpamCheck + include Redisable + include ActionView::Helpers::TextHelper + + NILSIMSA_COMPARE_THRESHOLD = 95 + NILSIMSA_MIN_SIZE = 10 + EXPIRE_SET_AFTER = 1.week.seconds + + def initialize(status) + @account = status.account + @status = status + end + + def skip? + already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? + end + + def spam? + if insufficient_data? + false + elsif nilsimsa? + any_other_digest?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD } + else + any_other_digest?('md5') { |_, other_digest| other_digest == digest } + end + end + + def flag! + auto_silence_account! + auto_report_status! + end + + def remember! + # The scores in sorted sets don't actually have enough bits to hold an exact + # value of our snowflake IDs, so we use it only for its ordering property. To + # get the correct status ID back, we have to save it in the string value + + redis.zadd(redis_key, @status.id, digest_with_algorithm) + redis.zremrangebyrank(redis_key, '0', '-10') + redis.expire(redis_key, EXPIRE_SET_AFTER) + end + + def reset! + redis.del(redis_key) + end + + def hashable_text + return @hashable_text if defined?(@hashable_text) + + @hashable_text = @status.text + @hashable_text = remove_mentions(@hashable_text) + @hashable_text = strip_tags(@hashable_text) unless @status.local? + @hashable_text = normalize_unicode(@status.spoiler_text + ' ' + @hashable_text) + @hashable_text = remove_whitespace(@hashable_text) + end + + def insufficient_data? + hashable_text.blank? + end + + def digest + @digest ||= begin + if nilsimsa? + Nilsimsa.new(hashable_text).hexdigest + else + Digest::MD5.hexdigest(hashable_text) + end + end + end + + def digest_with_algorithm + if nilsimsa? + ['nilsimsa', digest, @status.id].join(':') + else + ['md5', digest, @status.id].join(':') + end + end + + private + + def remove_mentions(text) + return text.gsub(Account::MENTION_RE, '') if @status.local? + + Nokogiri::HTML.fragment(text).tap do |html| + mentions = @status.mentions.map { |mention| ActivityPub::TagManager.instance.url_for(mention.account) } + + html.traverse do |element| + element.unlink if element.name == 'a' && mentions.include?(element['href']) + end + end.to_s + end + + def normalize_unicode(text) + text.unicode_normalize(:nfkc).downcase + end + + def remove_whitespace(text) + text.gsub(/\s+/, ' ').strip + end + + def auto_silence_account! + @account.silence! + end + + def auto_report_status! + status_ids = Status.where(visibility: %i(public unlisted)).where(id: matching_status_ids).pluck(:id) + [@status.id] if @status.distributable? + ReportService.new.call(Account.representative, @account, status_ids: status_ids, comment: I18n.t('spam_check.spam_detected_and_silenced')) + end + + def already_flagged? + @account.silenced? + end + + def trusted? + @account.trust_level > Account::TRUST_LEVELS[:untrusted] + end + + def no_unsolicited_mentions? + @status.mentions.all? { |mention| mention.silent? || (!@account.local? && !mention.account.local?) || mention.account.following?(@account) } + end + + def solicited_reply? + !@status.thread.nil? && @status.thread.mentions.where(account: @account).exists? + end + + def nilsimsa_compare_value(first, second) + first = [first].pack('H*') + second = [second].pack('H*') + bits = 0 + + 0.upto(31) do |i| + bits += Nilsimsa::POPC[255 & (first[i].ord ^ second[i].ord)].ord + end + + 128 - bits # -128 <= Nilsimsa Compare Value <= 128 + end + + def nilsimsa? + hashable_text.size > NILSIMSA_MIN_SIZE + end + + def other_digests + redis.zrange(redis_key, 0, -1) + end + + def any_other_digest?(filter_algorithm) + other_digests.any? do |record| + algorithm, other_digest, status_id = record.split(':') + + next unless algorithm == filter_algorithm + + yield algorithm, other_digest, status_id + end + end + + def matching_status_ids + if nilsimsa? + other_digests.select { |record| record.start_with?('nilsimsa') && nilsimsa_compare_value(digest, record.split(':')[1]) >= NILSIMSA_COMPARE_THRESHOLD }.map { |record| record.split(':')[2] }.compact + else + other_digests.select { |record| record.start_with?('md5') && record.split(':')[1] == digest }.map { |record| record.split(':')[2] }.compact + end + end + + def redis_key + @redis_key ||= "spam_check:#{@account.id}" + end +end diff --git a/app/models/account.rb b/app/models/account.rb index d6772eb98..a22b7fd7c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # also_known_as :string is an Array # silenced_at :datetime # suspended_at :datetime +# trust_level :integer # class Account < ApplicationRecord @@ -62,6 +63,11 @@ class Account < ApplicationRecord include AccountCounters include DomainNormalizable + TRUST_LEVELS = { + untrusted: 0, + trusted: 1, + }.freeze + enum protocol: [:ostatus, :activitypub] validates :username, presence: true @@ -163,6 +169,10 @@ class Account < ApplicationRecord last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago end + def trust_level + self[:trust_level] || 0 + end + def refresh! ResolveAccountService.new.call(acct) unless local? end @@ -171,21 +181,19 @@ class Account < ApplicationRecord silenced_at.present? end - def silence!(date = nil) - date ||= Time.now.utc + def silence!(date = Time.now.utc) update!(silenced_at: date) end def unsilence! - update!(silenced_at: nil) + update!(silenced_at: nil, trust_level: trust_level == TRUST_LEVELS[:untrusted] ? TRUST_LEVELS[:trusted] : trust_level) end def suspended? suspended_at.present? end - def suspend!(date = nil) - date ||= Time.now.utc + def suspend!(date = Time.now.utc) transaction do user&.disable! if local? update!(suspended_at: date) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 6311971ff..a69fce8b8 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -23,6 +23,7 @@ class RemoveStatusService < BaseService remove_from_hashtags remove_from_public remove_from_media if status.media_attachments.any? + remove_from_spam_check @status.destroy! else @@ -142,6 +143,10 @@ class RemoveStatusService < BaseService redis.publish('timeline:public:local:media', @payload) if @status.local? end + def remove_from_spam_check + redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id) + end + def lock_options { redis: Redis.current, key: "distribute:#{@status.id}" } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 00b7d1dbe..89251ad40 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -875,6 +875,8 @@ en: profile: Profile relationships: Follows and followers two_factor_authentication: Two-factor Auth + spam_check: + spam_detected_and_silenced: This is an automated report. Spam has been detected and the sender has been silenced automatically. If this is a mistake, please unsilence the account. statuses: attached: description: 'Attached: %{attached}' diff --git a/db/migrate/20190701022101_add_trust_level_to_accounts.rb b/db/migrate/20190701022101_add_trust_level_to_accounts.rb new file mode 100644 index 000000000..917486d2e --- /dev/null +++ b/db/migrate/20190701022101_add_trust_level_to_accounts.rb @@ -0,0 +1,5 @@ +class AddTrustLevelToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :trust_level, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e38fb1f2..c7b6b9be6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -148,6 +148,7 @@ ActiveRecord::Schema.define(version: 2019_07_06_233204) do t.string "also_known_as", array: true t.datetime "silenced_at" t.datetime "suspended_at" + t.integer "trust_level" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb new file mode 100644 index 000000000..c722dc642 --- /dev/null +++ b/spec/lib/spam_check_spec.rb @@ -0,0 +1,160 @@ +require 'rails_helper' + +RSpec.describe SpamCheck do + let!(:sender) { Fabricate(:account) } + let!(:alice) { Fabricate(:account, username: 'alice') } + let!(:bob) { Fabricate(:account, username: 'bob') } + + def status_with_html(text, options = {}) + status = PostStatusService.new.call(sender, { text: text }.merge(options)) + status.update_columns(text: Formatter.instance.format(status), local: false) + status + end + + describe '#hashable_text' do + it 'removes mentions from HTML for remote statuses' do + status = status_with_html('@alice Hello') + expect(described_class.new(status).hashable_text).to eq 'hello' + end + + it 'removes mentions from text for local statuses' do + status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?") + expect(described_class.new(status).hashable_text).to eq 'hey , how are you?' + end + end + + describe '#insufficient_data?' do + it 'returns true when there is no text' do + status = status_with_html('@alice') + expect(described_class.new(status).insufficient_data?).to be true + end + + it 'returns false when there is text' do + status = status_with_html('@alice h') + expect(described_class.new(status).insufficient_data?).to be false + end + end + + describe '#digest' do + it 'returns a string' do + status = status_with_html('@alice Hello world') + expect(described_class.new(status).digest).to be_a String + end + end + + describe '#spam?' do + it 'returns false for a unique status' do + status = status_with_html('@alice Hello') + expect(described_class.new(status).spam?).to be false + end + + it 'returns false for different statuses to the same recipient' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@alice Are you available to talk?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for statuses with different content warnings' do + status1 = status_with_html('@alice Are you available to talk?') + described_class.new(status1).remember! + status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for different statuses to different recipients' do + status1 = status_with_html('@alice How is it going?') + described_class.new(status1).remember! + status2 = status_with_html('@bob Are you okay?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for very short different statuses to different recipients' do + status1 = status_with_html('@alice 🙄') + described_class.new(status1).remember! + status2 = status_with_html('@bob Huh?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for statuses with no text' do + status1 = status_with_html('@alice') + described_class.new(status1).remember! + status2 = status_with_html('@bob') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns true for duplicate statuses to the same recipient' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@alice Hello') + expect(described_class.new(status2).spam?).to be true + end + + it 'returns true for duplicate statuses to different recipients' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@bob Hello') + expect(described_class.new(status2).spam?).to be true + end + + it 'returns true for nearly identical statuses with random numbers' do + source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.' + status1 = status_with_html('@alice ' + source_text + ' 1234') + described_class.new(status1).remember! + status2 = status_with_html('@bob ' + source_text + ' 9568') + expect(described_class.new(status2).spam?).to be true + end + end + + describe '#skip?' do + it 'returns true when the sender is already silenced' do + status = status_with_html('@alice Hello') + sender.silence! + expect(described_class.new(status).skip?).to be true + end + + it 'returns true when the mentioned person follows the sender' do + status = status_with_html('@alice Hello') + alice.follow!(sender) + expect(described_class.new(status).skip?).to be true + end + + it 'returns false when even one mentioned person doesn\'t follow the sender' do + status = status_with_html('@alice @bob Hello') + alice.follow!(sender) + expect(described_class.new(status).skip?).to be false + end + + it 'returns true when the sender is replying to a status that mentions the sender' do + parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?") + status = status_with_html('@alice @bob Hello', thread: parent) + expect(described_class.new(status).skip?).to be true + end + end + + describe '#remember!' do + pending + end + + describe '#flag!' do + let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') } + let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') } + + before do + described_class.new(status1).remember! + described_class.new(status2).flag! + end + + it 'silences the account' do + expect(sender.silenced?).to be true + end + + it 'creates a report about the account' do + expect(sender.targeted_reports.unresolved.count).to eq 1 + end + + it 'attaches both matching statuses to the report' do + expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id) + end + end +end -- cgit From e7353c47dbc200536dedb7beea37d60c6ea6fa2f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 05:56:35 +0200 Subject: Change default interface of web and streaming from 0.0.0.0 to 127.0.0.1 (#11302) --- config/puma.rb | 4 ++-- docker-compose.yml | 2 +- streaming/index.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'config') diff --git a/config/puma.rb b/config/puma.rb index 1afdb1c6d..25a5534b2 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -2,9 +2,9 @@ threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count if ENV['SOCKET'] - bind 'unix://' + ENV['SOCKET'] + bind "unix://#{ENV['SOCKET']}" else - port ENV.fetch('PORT') { 3000 } + bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } diff --git a/docker-compose.yml b/docker-compose.yml index 93d47f1a0..f3fe6cfd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: yarn start + command: BIND=0.0.0.0 node ./streaming networks: - external_network - internal_network diff --git a/streaming/index.js b/streaming/index.js index 639867b28..0529804b1 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -672,7 +672,7 @@ const attachServerWithConfig = (server, onSuccess) => { } }); } else { - server.listen(+process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => { + server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => { if (onSuccess) { onSuccess(`${server.address().address}:${server.address().port}`); } -- cgit From cecd0c3cb1ed208b3291342d644128c8fe71f12d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 06:12:05 +0200 Subject: New Crowdin translations (#11153) * New translations activerecord.en.yml (Indonesian) [ci skip] * New translations activerecord.en.yml (Italian) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Norwegian) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Finnish) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Tamil) [ci skip] * New translations en.yml (Telugu) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Dutch) [ci skip] * New translations simple_form.en.yml (Esperanto) [ci skip] * New translations simple_form.en.yml (French) [ci skip] * New translations simple_form.en.yml (Galician) [ci skip] * New translations simple_form.en.yml (Georgian) [ci skip] * New translations simple_form.en.yml (German) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations simple_form.en.yml (Hebrew) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Ido) [ci skip] * New translations simple_form.en.yml (Indonesian) [ci skip] * New translations simple_form.en.yml (Italian) [ci skip] * New translations simple_form.en.yml (Korean) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Italian) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations simple_form.en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Basque) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations simple_form.en.yml (German) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations simple_form.en.yml (Polish) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations doorkeeper.en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations simple_form.en.yml (Slovak) [ci skip] * New translations simple_form.en.yml (Corsican) [ci skip] * New translations simple_form.en.yml (Corsican) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Galician) [ci skip] * New translations en.json (Galician) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations activerecord.en.yml (Welsh) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations activerecord.en.yml (Serbian (Latin)) [ci skip] * New translations doorkeeper.en.yml (Serbian (Latin)) [ci skip] * New translations devise.en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Hebrew) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Indonesian) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Norwegian) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations devise.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations simple_form.en.yml (Basque) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations doorkeeper.en.yml (Basque) [ci skip] * New translations en.json (Korean) [ci skip] * New translations doorkeeper.en.yml (Slovak) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Asturian) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations devise.en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Slovak) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations activerecord.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * i18n-tasks normalize * yarn manage:translations --- app/javascript/mastodon/locales/ar.json | 3 + app/javascript/mastodon/locales/ast.json | 5 +- app/javascript/mastodon/locales/bg.json | 5 +- app/javascript/mastodon/locales/bn.json | 73 +-- app/javascript/mastodon/locales/ca.json | 3 + app/javascript/mastodon/locales/co.json | 3 + app/javascript/mastodon/locales/cs.json | 3 + app/javascript/mastodon/locales/cy.json | 3 + app/javascript/mastodon/locales/da.json | 3 + app/javascript/mastodon/locales/de.json | 3 + .../mastodon/locales/defaultMessages.json | 27 +- app/javascript/mastodon/locales/el.json | 3 + app/javascript/mastodon/locales/en.json | 3 + app/javascript/mastodon/locales/eo.json | 3 + app/javascript/mastodon/locales/es.json | 109 +++-- app/javascript/mastodon/locales/eu.json | 3 + app/javascript/mastodon/locales/fa.json | 3 + app/javascript/mastodon/locales/fi.json | 3 + app/javascript/mastodon/locales/fr.json | 3 + app/javascript/mastodon/locales/gl.json | 5 +- app/javascript/mastodon/locales/he.json | 5 +- app/javascript/mastodon/locales/hi.json | 3 + app/javascript/mastodon/locales/hr.json | 5 +- app/javascript/mastodon/locales/hu.json | 3 + app/javascript/mastodon/locales/hy.json | 5 +- app/javascript/mastodon/locales/id.json | 93 ++-- app/javascript/mastodon/locales/io.json | 5 +- app/javascript/mastodon/locales/it.json | 11 +- app/javascript/mastodon/locales/ja.json | 3 + app/javascript/mastodon/locales/ka.json | 3 + app/javascript/mastodon/locales/kk.json | 3 + app/javascript/mastodon/locales/ko.json | 5 +- app/javascript/mastodon/locales/lt.json | 7 +- app/javascript/mastodon/locales/lv.json | 5 +- app/javascript/mastodon/locales/ms.json | 7 +- app/javascript/mastodon/locales/nl.json | 3 + app/javascript/mastodon/locales/no.json | 5 +- app/javascript/mastodon/locales/oc.json | 13 +- app/javascript/mastodon/locales/pl.json | 3 + app/javascript/mastodon/locales/pt-BR.json | 25 +- app/javascript/mastodon/locales/pt.json | 181 +++---- app/javascript/mastodon/locales/ro.json | 3 + app/javascript/mastodon/locales/ru.json | 49 +- app/javascript/mastodon/locales/sk.json | 5 +- app/javascript/mastodon/locales/sl.json | 35 +- app/javascript/mastodon/locales/sq.json | 3 + app/javascript/mastodon/locales/sr-Latn.json | 5 +- app/javascript/mastodon/locales/sr.json | 3 + app/javascript/mastodon/locales/sv.json | 3 + app/javascript/mastodon/locales/ta.json | 3 + app/javascript/mastodon/locales/te.json | 3 + app/javascript/mastodon/locales/th.json | 35 +- app/javascript/mastodon/locales/tr.json | 3 + app/javascript/mastodon/locales/uk.json | 3 + app/javascript/mastodon/locales/zh-CN.json | 73 +-- app/javascript/mastodon/locales/zh-HK.json | 3 + app/javascript/mastodon/locales/zh-TW.json | 3 + config/locales/activerecord.bn.yml | 16 + config/locales/activerecord.cy.yml | 2 +- config/locales/ar.yml | 10 - config/locales/ast.yml | 2 - config/locales/ca.yml | 11 - config/locales/co.yml | 11 - config/locales/cs.yml | 11 - config/locales/cy.yml | 11 - config/locales/da.yml | 11 - config/locales/de.yml | 11 - config/locales/devise.bn.yml | 39 ++ config/locales/devise.sk.yml | 2 +- config/locales/devise.sl.yml | 39 +- config/locales/devise.zh-CN.yml | 2 +- config/locales/doorkeeper.cy.yml | 6 + config/locales/doorkeeper.es.yml | 28 ++ config/locales/doorkeeper.eu.yml | 16 +- config/locales/doorkeeper.hu.yml | 6 + config/locales/doorkeeper.nl.yml | 6 + config/locales/doorkeeper.oc.yml | 6 + config/locales/doorkeeper.sk.yml | 35 +- config/locales/doorkeeper.zh-CN.yml | 1 + config/locales/el.yml | 11 - config/locales/eo.yml | 20 +- config/locales/es.yml | 233 ++++++++- config/locales/eu.yml | 19 +- config/locales/fa.yml | 11 - config/locales/fi.yml | 8 - config/locales/fr.yml | 11 - config/locales/gl.yml | 11 - config/locales/he.yml | 7 - config/locales/hu.yml | 39 +- config/locales/id.yml | 5 - config/locales/it.yml | 13 +- config/locales/ja.yml | 11 - config/locales/ka.yml | 11 - config/locales/kk.yml | 11 - config/locales/ko.yml | 11 - config/locales/lt.yml | 11 - config/locales/nl.yml | 11 - config/locales/no.yml | 6 - config/locales/oc.yml | 121 ++++- config/locales/pl.yml | 13 +- config/locales/pt-BR.yml | 10 - config/locales/pt.yml | 10 - config/locales/ru.yml | 15 +- config/locales/simple_form.co.yml | 2 + config/locales/simple_form.cs.yml | 2 + config/locales/simple_form.cy.yml | 2 + config/locales/simple_form.de.yml | 2 + config/locales/simple_form.el.yml | 2 + config/locales/simple_form.es.yml | 31 ++ config/locales/simple_form.eu.yml | 10 +- config/locales/simple_form.gl.yml | 2 + config/locales/simple_form.ja.yml | 2 + config/locales/simple_form.oc.yml | 8 + config/locales/simple_form.pl.yml | 2 + config/locales/simple_form.ru.yml | 6 +- config/locales/simple_form.sk.yml | 2 + config/locales/simple_form.th.yml | 2 + config/locales/simple_form.zh-CN.yml | 6 +- config/locales/sk.yml | 23 +- config/locales/sl.yml | 545 ++++++++++++++++++--- config/locales/sq.yml | 10 - config/locales/sr-Latn.yml | 4 - config/locales/sr.yml | 8 - config/locales/sv.yml | 10 - config/locales/th.yml | 13 +- config/locales/tr.yml | 6 - config/locales/uk.yml | 9 - config/locales/zh-CN.yml | 128 +++-- config/locales/zh-HK.yml | 11 - config/locales/zh-TW.yml | 11 - 130 files changed, 1737 insertions(+), 924 deletions(-) (limited to 'config') diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index d05c61f98..d62ee90c2 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "أساسية", "home.column_settings.show_reblogs": "عرض الترقيات", "home.column_settings.show_replies": "عرض الردود", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}", "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}", "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", "lists.subheading": "قوائمك", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "تحميل...", "media_gallery.toggle_visible": "عرض / إخفاء", "missing_indicator.label": "تعذر العثور عليه", @@ -314,6 +316,7 @@ "search_results.accounts": "أشخاص", "search_results.hashtags": "الوُسوم", "search_results.statuses": "التبويقات", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} و {results}}", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index b911848ee..3ae4e5e5e 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Desfixar", "column_subheading.settings": "Axustes", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "Esti toot namái va unviase a los usuarios mentaos.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Amosar toots compartíos", "home.column_settings.show_replies": "Amosar rempuestes", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Títulu nuevu de la llista", "lists.search": "Guetar ente la xente que sigues", "lists.subheading": "Les tos llistes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Nun s'alcontró", @@ -314,6 +316,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 783f9eb68..4c97fe1fc 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 5b7162ec1..358f994f3 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -1,13 +1,13 @@ { "account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন", "account.badges.bot": "রোবট", - "account.block": "@{name} বন্ধ করুন", + "account.block": "@{name} কে বন্ধ করুন", "account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন", "account.blocked": "বন্ধ করা হয়েছে", - "account.direct": "@{name}কে সরকারি লিখুন", + "account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে", "account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে", - "account.edit_profile": "নিজের পাতা সম্পাদনা করুন", - "account.endorse": "নিজের পাতায় দেখান", + "account.edit_profile": "নিজের পাতা সম্পাদনা করতে", + "account.endorse": "আপনার নিজের পাতায় দেখাতে", "account.follow": "অনুসরণ করুন", "account.followers": "অনুসরণকারক", "account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।", @@ -18,21 +18,21 @@ "account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে", "account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।", "account.media": "ছবি বা ভিডিও", - "account.mention": "@{name} কে উল্লেখ করুন", + "account.mention": "@{name} কে উল্লেখ করতে", "account.moved_to": "{name} চলে গেছে এখানে:", - "account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন", + "account.mute": "@{name} সব কার্যক্রম আপনার সময়রেখা থেকে সরিয়ে ফেলতে", "account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন", "account.muted": "সরানো আছে", "account.posts": "টুট", "account.posts_with_replies": "টুট এবং মতামত", - "account.report": "@{name}কে রিপোর্ট করে দিন", + "account.report": "@{name} কে রিপোর্ট করতে", "account.requested": "অনুমতির অপেক্ষায় আছে। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন", "account.share": "@{name}র পাতা অন্যদের দেখান", "account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন", "account.unblock": "@{name}র কার্যকলাপ আবার দেখুন", "account.unblock_domain": "{domain}থেকে আবার দেখুন", - "account.unendorse": "নিজের পাতায় এটা দেখতে চান না", - "account.unfollow": "অনুসরণ বন্ধ করুন", + "account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে", + "account.unfollow": "অনুসরণ না করতে", "account.unmute": "@{name}র কার্যকলাপ আবার দেখুন", "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন", "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।", @@ -42,7 +42,7 @@ "bundle_column_error.retry": "আবার চেষ্টা করুন", "bundle_column_error.title": "নেটওয়ার্কের সমস্যা হচ্ছে", "bundle_modal_error.close": "বন্ধ করুন", - "bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।", + "bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_modal_error.retry": "আবার চেষ্টা করুন", "column.blocks": "যাদের বন্ধ করে রাখা হয়েছে", "column.community": "স্থানীয় সময়সারি", @@ -77,12 +77,12 @@ "compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন", "compose_form.publish": "টুট", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে", "compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে", "compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি", "compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে", "compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই", - "compose_form.spoiler_placeholder": "আপনার সাবধানতা এখানে লিখুন", + "compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন", "confirmation_modal.cancel": "বাতিল করুন", "confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন", "confirmations.block.confirm": "বন্ধ করুন", @@ -99,7 +99,7 @@ "confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।", "confirmations.reply.confirm": "মতামত", "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?", - "confirmations.unfollow.confirm": "অনুসরণ বন্ধ করুন", + "confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে", "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?", "embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।", "embed.preview": "সেটা দেখতে এরকম হবে:", @@ -137,11 +137,11 @@ "follow_request.authorize": "অনুমতি দিন", "follow_request.reject": "প্রত্যাখ্যান করুন", "getting_started.developers": "তৈরিকারকদের জন্য", - "getting_started.directory": "নিজস্ব পাতার তালিকা", + "getting_started.directory": "নিজস্ব-পাতাগুলির তালিকা", "getting_started.documentation": "নথিপত্র", "getting_started.heading": "শুরু করা", "getting_started.invite": "অন্যদের আমন্ত্রণ করুন", - "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। আপনি তৈরিতে সাহায্য করতে পারেন অথবা সমস্যা রিপোর্ট করতে পারেন গিটহাবে {github}।", + "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। তৈরিতে সাহায্য করতে বা কোনো সমস্যা সম্পর্কে জানাতে আমাদের গিটহাবে যেতে পারেন {github}।", "getting_started.security": "নিরাপত্তা", "getting_started.terms": "ব্যবহারের নিয়মাবলী", "hashtag.column_header.tag_mode.all": "এবং {additional}", @@ -152,10 +152,11 @@ "hashtag.column_settings.tag_mode.all": "এগুলো সব", "hashtag.column_settings.tag_mode.any": "এর ভেতরে যেকোনোটা", "hashtag.column_settings.tag_mode.none": "এগুলোর একটাও না", - "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করুন", + "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করতে", "home.column_settings.basic": "সাধারণ", "home.column_settings.show_reblogs": "সমর্থনগুলো দেখান", "home.column_settings.show_replies": "মতামত দেখান", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -195,7 +196,7 @@ "keyboard_shortcuts.local": "স্থানীয় সময়রেখাতে যেতে", "keyboard_shortcuts.mention": "লেখককে উল্লেখ করতে", "keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে", - "keyboard_shortcuts.my_profile": "নিজের পাতা দেখতে", + "keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে", "keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে", "keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে", "keyboard_shortcuts.profile": "লেখকের পাতা দেখতে", @@ -204,14 +205,14 @@ "keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে", "keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে", "keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে", "keyboard_shortcuts.toot": "নতুন একটা টুট লেখা শুরু করতে", "keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে", "keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে", "lightbox.close": "বন্ধ", "lightbox.next": "পরবর্তী", "lightbox.previous": "পূর্ববর্তী", - "lightbox.view_context": "View context", + "lightbox.view_context": "প্রসঙ্গটি দেখতে", "lists.account.add": "তালিকাতে যুক্ত করতে", "lists.account.remove": "তালিকা থেকে বাদ দিতে", "lists.delete": "তালিকা মুছে ফেলতে", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে", "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন", "lists.subheading": "আপনার তালিকা", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "আসছে...", "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান", "missing_indicator.label": "খুঁজে পাওয়া যায়নি", @@ -230,14 +232,14 @@ "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী", "navigation_bar.community_timeline": "স্থানীয় সময়রেখা", "navigation_bar.compose": "নতুন টুট লিখুন", - "navigation_bar.direct": "সরাসরি লেখা", + "navigation_bar.direct": "সরাসরি লেখাগুলি", "navigation_bar.discover": "ঘুরে দেখুন", "navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট", - "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করুন", + "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে", "navigation_bar.favourites": "পছন্দের", "navigation_bar.filters": "বন্ধ করা শব্দ", "navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "যাদেরকে অনুসরণ করেন এবং যারা তাকে অনুসরণ করে", "navigation_bar.info": "এই সার্ভার সম্পর্কে", "navigation_bar.keyboard_shortcuts": "হটকীগুলি", "navigation_bar.lists": "তালিকাগুলো", @@ -246,7 +248,7 @@ "navigation_bar.personal": "নিজস্ব", "navigation_bar.pins": "পিন দেওয়া টুট", "navigation_bar.preferences": "পছন্দসমূহ", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "নিজস্ব পাতার তালিকা", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা", "navigation_bar.security": "নিরাপত্তা", "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন", @@ -256,18 +258,18 @@ "notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন", "notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে", "notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?", - "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপন", + "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি", "notifications.column_settings.favourite": "পছন্দের:", - "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখতে", - "notifications.column_settings.filter_bar.category": "দ্রুত ছাঁকনি বার", - "notifications.column_settings.filter_bar.show": "দেখতে", + "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো", + "notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ", + "notifications.column_settings.filter_bar.show": "দেখানো", "notifications.column_settings.follow": "নতুন অনুসরণকারীরা:", "notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:", "notifications.column_settings.poll": "নির্বাচনের ফলাফল:", - "notifications.column_settings.push": "পুশ প্রজ্ঞাপন", + "notifications.column_settings.push": "পুশ প্রজ্ঞাপনগুলি", "notifications.column_settings.reblog": "সমর্থনগুলো:", - "notifications.column_settings.show": "কলামে দেখান", - "notifications.column_settings.sound": "শব্দ বাজাতে", + "notifications.column_settings.show": "কলামে দেখানো", + "notifications.column_settings.sound": "শব্দ বাজানো", "notifications.filter.all": "সব", "notifications.filter.boosts": "সমর্থনগুলো", "notifications.filter.favourites": "পছন্দের গুলো", @@ -276,7 +278,7 @@ "notifications.filter.polls": "নির্বাচনের ফলাফল", "notifications.group": "{count} প্রজ্ঞাপন", "poll.closed": "বন্ধ", - "poll.refresh": "আবার সতেজ করতে", + "poll.refresh": "বদলেছে কিনা দেখতে", "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}", "poll.vote": "ভোট", "poll_button.add_poll": "একটা নির্বাচন যোগ করতে", @@ -314,6 +316,7 @@ "search_results.accounts": "মানুষ", "search_results.hashtags": "হ্যাশট্যাগগুলি", "search_results.statuses": "টুট", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন", @@ -323,7 +326,7 @@ "status.copy": "লেখাটির লিংক কপি করতে", "status.delete": "মুছে ফেলতে", "status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে", - "status.direct": "@{name} কে সরাসরি পাঠান", + "status.direct": "@{name} কে সরাসরি লেখা পাঠাতে", "status.embed": "এমবেড করতে", "status.favourite": "পছন্দের করতে", "status.filtered": "ছাঁকনিদিত", @@ -344,7 +347,7 @@ "status.redraft": "মুছে আবার নতুন করে লিখতে", "status.reply": "মতামত জানাতে", "status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে", - "status.report": "@{name}কে রিপোর্ট করতে", + "status.report": "@{name} কে রিপোর্ট করতে", "status.sensitive_warning": "সংবেদনশীল কিছু", "status.share": "অন্যদের জানান", "status.show_less": "কম দেখতে", @@ -354,7 +357,7 @@ "status.show_thread": "আলোচনা দেখতে", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে", "status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে", - "suggestions.dismiss": "সাহায্যের জন্য পরামর্শগুলো সরাতে", + "suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে", "suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…", "tabs_bar.federated_timeline": "যুক্তবিশ্ব", "tabs_bar.home": "বাড়ি", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।", "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে", - "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।", "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।", "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index bb73b2a41..09f8838e9 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bàsic", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostes", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dia} other {# dies}}", "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}", "intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Nova llista", "lists.search": "Cercar entre les persones que segueixes", "lists.subheading": "Les teves llistes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", @@ -314,6 +316,7 @@ "search_results.accounts": "Gent", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_status": "Obre aquest toot a la interfície de moderació", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index fb8ffdd51..7a1ff863b 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bàsichi", "home.column_settings.show_reblogs": "Vede e spartere", "home.column_settings.show_replies": "Vede e risposte", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}", "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titulu di a lista", "lists.search": "Circà indè i vostr'abbunamenti", "lists.subheading": "E vo liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Caricamentu...", "media_gallery.toggle_visible": "Cambià a visibilità", "missing_indicator.label": "Micca trovu", @@ -314,6 +316,7 @@ "search_results.accounts": "Ghjente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Statuti", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}", "status.admin_account": "Apre l'interfaccia di muderazione per @{name}", "status.admin_status": "Apre stu statutu in l'interfaccia di muderazione", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index f10a3f38b..020fd35b0 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Základní", "home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_replies": "Zobrazit odpovědi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}", "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}", "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Název nového seznamu", "lists.search": "Hledejte mezi lidmi, které sledujete", "lists.subheading": "Vaše seznamy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Načítám…", "media_gallery.toggle_visible": "Přepínat viditelnost", "missing_indicator.label": "Nenalezeno", @@ -314,6 +316,7 @@ "search_results.accounts": "Lidé", "search_results.hashtags": "Hashtagy", "search_results.statuses": "Tooty", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}", "status.admin_account": "Otevřít moderátorské rozhraní pro uživatele @{name}", "status.admin_status": "Otevřít tento toot v moderátorském rozhraní", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 4ce5d7ad9..9de3efda8 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Syml", "home.column_settings.show_reblogs": "Dangos bŵstiau", "home.column_settings.show_replies": "Dangos ymatebion", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}", "intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}", "intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Teitl rhestr newydd", "lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn", "lists.subheading": "Eich rhestrau", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Llwytho...", "media_gallery.toggle_visible": "Toglo gwelededd", "missing_indicator.label": "Heb ei ganfod", @@ -314,6 +316,7 @@ "search_results.accounts": "Pobl", "search_results.hashtags": "Hanshnodau", "search_results.statuses": "Tŵtiau", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Agor rhyngwyneb goruwchwylio ar gyfer @{name}", "status.admin_status": "Agor y tŵt yn y rhyngwyneb goruwchwylio", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index ba8ba7a28..17080c41e 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Grundlæggende", "home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_replies": "Vis svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny liste titel", "lists.search": "Søg iblandt folk du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Indlæser...", "media_gallery.toggle_visible": "Ændre synlighed", "missing_indicator.label": "Ikke fundet", @@ -314,6 +316,7 @@ "search_results.accounts": "Folk", "search_results.hashtags": "Emnetags", "search_results.statuses": "Trut", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, et {result} andre {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index ac8bc9b9f..4ae785270 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Einfach", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}", "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}", "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Neuer Titel der Liste", "lists.search": "Suche nach Leuten denen du folgst", "lists.subheading": "Deine Listen", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Wird geladen …", "media_gallery.toggle_visible": "Sichtbarkeit umschalten", "missing_indicator.label": "Nicht gefunden", @@ -314,6 +316,7 @@ "search_results.accounts": "Personen", "search_results.hashtags": "Hashtags", "search_results.statuses": "Beiträge", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", "status.admin_account": "Öffne Moderationsoberfläche für @{name}", "status.admin_status": "Öffne Beitrag in der Moderationsoberfläche", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 4df299d84..43ce28ec0 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -158,6 +158,15 @@ ], "path": "app/javascript/mastodon/components/load_more.json" }, + { + "descriptors": [ + { + "defaultMessage": "{count, plural, one {# new item} other {# new items}}", + "id": "load_pending" + } + ], + "path": "app/javascript/mastodon/components/load_pending.json" + }, { "descriptors": [ { @@ -735,7 +744,7 @@ { "descriptors": [ { - "defaultMessage": "Media Only", + "defaultMessage": "Media only", "id": "community.column_settings.media_only" } ], @@ -1004,6 +1013,10 @@ "defaultMessage": "Toots", "id": "search_results.statuses" }, + { + "defaultMessage": "Searching toots by their content is not enabled on this Mastodon server.", + "id": "search_results.statuses_fts_disabled" + }, { "defaultMessage": "Hashtags", "id": "search_results.hashtags" @@ -1412,10 +1425,6 @@ }, { "descriptors": [ - { - "defaultMessage": "Basic", - "id": "home.column_settings.basic" - }, { "defaultMessage": "Show boosts", "id": "home.column_settings.show_reblogs" @@ -1797,6 +1806,14 @@ "defaultMessage": "Push notifications", "id": "notifications.column_settings.push" }, + { + "defaultMessage": "Basic", + "id": "home.column_settings.basic" + }, + { + "defaultMessage": "Update in real-time", + "id": "home.column_settings.update_live" + }, { "defaultMessage": "Quick filter bar", "id": "notifications.column_settings.filter_bar.category" diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index e118e427b..df85c025f 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Βασικά", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}", "intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}", "intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Τίτλος νέας λίστα", "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς", "lists.subheading": "Οι λίστες σου", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Φορτώνει...", "media_gallery.toggle_visible": "Εναλλαγή ορατότητας", "missing_indicator.label": "Δε βρέθηκε", @@ -314,6 +316,7 @@ "search_results.accounts": "Άνθρωποι", "search_results.hashtags": "Ταμπέλες", "search_results.statuses": "Τουτ", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}", "status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}", "status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 67e1e01f8..7e69e27c6 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 897cb6353..ddc694252 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bazaj agordoj", "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}", "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titolo de la nova listo", "lists.search": "Serĉi inter la homoj, kiujn vi sekvas", "lists.subheading": "Viaj listoj", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ŝargado…", "media_gallery.toggle_visible": "Baskuligi videblecon", "missing_indicator.label": "Ne trovita", @@ -314,6 +316,7 @@ "search_results.accounts": "Homoj", "search_results.hashtags": "Kradvortoj", "search_results.statuses": "Mesaĝoj", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}", "status.admin_account": "Malfermi la kontrolan interfacon por @{name}", "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 8fe50ace5..dc42bc7ef 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -98,7 +98,7 @@ "confirmations.redraft.confirm": "Borrar y volver a borrador", "confirmations.redraft.message": "Estás seguro de que quieres borrar este estado y volverlo a borrador? Perderás todas las respuestas, impulsos y favoritos asociados a él, y las respuestas a la publicación original quedarán huérfanos.", "confirmations.reply.confirm": "Responder", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", @@ -149,33 +149,34 @@ "hashtag.column_header.tag_mode.none": "sin {additional}", "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias", "hashtag.column_settings.select.placeholder": "Introduzca hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.all": "Cualquiera de estos", "hashtag.column_settings.tag_mode.any": "Cualquiera de estos", "hashtag.column_settings.tag_mode.none": "Ninguno de estos", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_replies": "Mostrar respuestas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# día} other {# días}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Siguiente", "introduction.federation.federated.headline": "Federado", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.federated.text": "Los mensajes públicos de otros servidores del fediverso aparecerán en la cronología federada.", "introduction.federation.home.headline": "Inicio", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.home.text": "Los posts de personas que sigues aparecerán en tu cronología. ¡Puedes seguir a cualquiera en cualquier servidor!", "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.federation.local.text": "Los posts públicos de personas en el mismo servidor que aparecerán en la cronología local.", "introduction.interactions.action": "¡Terminar tutorial!", "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.favourite.text": "Puedes guardar un toot para más tarde, y hacer saber al autor que te gustó, dándole a favorito.", + "introduction.interactions.reblog.headline": "Retootear", + "introduction.interactions.reblog.text": "Puedes compartir los toots de otras personas con tus seguidores retooteando los mismos.", "introduction.interactions.reply.headline": "Responder", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.interactions.reply.text": "Puedes responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.", "introduction.welcome.action": "¡Vamos!", "introduction.welcome.headline": "Primeros pasos", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.welcome.text": "¡Bienvenido al fediverso! En unos momentos, podrás transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial, alberga tu perfil, así que recuerda su nombre.", "keyboard_shortcuts.back": "volver atrás", "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados", "keyboard_shortcuts.boost": "retootear", @@ -184,7 +185,7 @@ "keyboard_shortcuts.description": "Descripción", "keyboard_shortcuts.direct": "abrir la columna de mensajes directos", "keyboard_shortcuts.down": "mover hacia abajo en la lista", - "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.enter": "abrir estado", "keyboard_shortcuts.favourite": "añadir a favoritos", "keyboard_shortcuts.favourites": "abrir la lista de favoritos", "keyboard_shortcuts.federated": "abrir el timeline federado", @@ -204,7 +205,7 @@ "keyboard_shortcuts.search": "para poner el foco en la búsqueda", "keyboard_shortcuts.start": "abrir la columna \"comenzar\"", "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios", "keyboard_shortcuts.toot": "para comenzar un nuevo toot", "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda", "keyboard_shortcuts.up": "para ir hacia arriba en la lista", @@ -216,11 +217,12 @@ "lists.account.remove": "Quitar de lista", "lists.delete": "Borrar lista", "lists.edit": "Editar lista", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cambiar título", "lists.new.create": "Añadir lista", "lists.new.title_placeholder": "Título de la nueva lista", "lists.search": "Buscar entre la gente a la que sigues", "lists.subheading": "Tus listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando…", "media_gallery.toggle_visible": "Cambiar visibilidad", "missing_indicator.label": "No encontrado", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palabras silenciadas", "navigation_bar.follow_requests": "Solicitudes para seguirte", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Siguiendo y seguidores", "navigation_bar.info": "Información adicional", "navigation_bar.keyboard_shortcuts": "Atajos", "navigation_bar.lists": "Listas", @@ -246,41 +248,41 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Toots fijados", "navigation_bar.preferences": "Preferencias", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Directorio de perfiles", "navigation_bar.public_timeline": "Historia federada", "navigation_bar.security": "Seguridad", "notification.favourite": "{name} marcó tu estado como favorito", "notification.follow": "{name} te empezó a seguir", "notification.mention": "{name} te ha mencionado", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Una encuesta en la que has votado ha terminado", "notification.reblog": "{name} ha retooteado tu estado", "notifications.clear": "Limpiar notificaciones", "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?", "notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías", + "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", + "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", + "notifications.filter.all": "Todos", + "notifications.filter.boosts": "Retoots", + "notifications.filter.favourites": "Favoritos", + "notifications.filter.follows": "Seguidores", + "notifications.filter.mentions": "Menciones", + "notifications.filter.polls": "Resultados de la votación", "notifications.group": "{count} notificaciones", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.closed": "Cerrada", + "poll.refresh": "Actualizar", + "poll.total_votes": "{count, plural, one {# voto} other {# votos}}", + "poll.vote": "Votar", + "poll_button.add_poll": "Añadir una encuesta", + "poll_button.remove_poll": "Eliminar encuesta", "privacy.change": "Ajustar privacidad", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.short": "Directo", @@ -289,7 +291,7 @@ "privacy.public.long": "Mostrar en la historia federada", "privacy.public.short": "Público", "privacy.unlisted.long": "No mostrar en la historia federada", - "privacy.unlisted.short": "Sin federar", + "privacy.unlisted.short": "No listado", "regeneration_indicator.label": "Cargando…", "regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!", "relative_time.days": "{number}d", @@ -308,19 +310,20 @@ "search_popout.search_format": "Formato de búsqueda avanzada", "search_popout.tips.full_text": "Búsquedas de texto recuperan posts que has escrito, marcado como favoritos, retooteado o en los que has sido mencionado, así como usuarios, nombres y hashtags.", "search_popout.tips.hashtag": "etiqueta", - "search_popout.tips.status": "status", + "search_popout.tips.status": "estado", "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag", "search_popout.tips.user": "usuario", "search_results.accounts": "Gente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", + "status.admin_account": "Abrir interfaz de moderación para @{name}", + "status.admin_status": "Abrir este estado en la interfaz de moderación", + "status.block": "Bloquear a @{name}", "status.cancel_reblog_private": "Des-impulsar", "status.cannot_reblog": "Este toot no puede retootearse", - "status.copy": "Copy link to status", + "status.copy": "Copiar enlace al estado", "status.delete": "Borrar", "status.detailed_status": "Vista de conversación detallada", "status.direct": "Mensaje directo a @{name}", @@ -336,7 +339,7 @@ "status.open": "Expandir estado", "status.pin": "Fijar", "status.pinned": "Toot fijado", - "status.read_more": "Read more", + "status.read_more": "Leer más", "status.reblog": "Retootear", "status.reblog_private": "Implusar a la audiencia original", "status.reblogged_by": "Retooteado por {name}", @@ -351,27 +354,27 @@ "status.show_less_all": "Mostrar menos para todo", "status.show_more": "Mostrar más", "status.show_more_all": "Mostrar más para todo", - "status.show_thread": "Show thread", + "status.show_thread": "Ver hilo", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Descartar sugerencia", + "suggestions.header": "Es posible que te interese…", "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", "tabs_bar.search": "Buscar", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}", + "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}", + "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", + "time_remaining.moments": "Momentos restantes", + "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Límite de subida de archivos excedido.", + "upload_error.poll": "Subida de archivos no permitida con encuestas.", "upload_form.description": "Describir para los usuarios con dificultad visual", "upload_form.focus": "Recortar", "upload_form.undo": "Borrar", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 3e91012b3..0c078840a 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Oinarrizkoa", "home.column_settings.show_reblogs": "Erakutsi bultzadak", "home.column_settings.show_replies": "Erakutsi erantzunak", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {egun #} other {# egun}}", "intervals.full.hours": "{number, plural, one {ordu #} other {# ordu}}", "intervals.full.minutes": "{number, plural, one {minutu #} other {# minutu}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Zerrenda berriaren izena", "lists.search": "Bilatu jarraitzen dituzun pertsonen artean", "lists.subheading": "Zure zerrendak", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Kargatzen...", "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna", "missing_indicator.label": "Ez aurkitua", @@ -314,6 +316,7 @@ "search_results.accounts": "Jendea", "search_results.hashtags": "Traolak", "search_results.statuses": "Toot-ak", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitzak}}", "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", "status.admin_status": "Ireki mezu hau moderazio interfazean", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 68d231ce9..41143bcc8 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "اصلی", "home.column_settings.show_reblogs": "نمایش بازبوق‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# روز} other {# روز}}", "intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}", "intervals.full.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "نام فهرست تازه", "lists.search": "بین کسانی که پی می‌گیرید بگردید", "lists.subheading": "فهرست‌های شما", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", @@ -314,6 +316,7 @@ "search_results.accounts": "افراد", "search_results.hashtags": "هشتگ‌ها", "search_results.statuses": "بوق‌ها", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "status.admin_account": "محیط مدیریت مربوط به @{name} را باز کن", "status.admin_status": "این نوشته را در محیط مدیریت باز کن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 342a15bfb..05495d5d7 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Perusasetukset", "home.column_settings.show_reblogs": "Näytä buustaukset", "home.column_settings.show_replies": "Näytä vastaukset", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "Päivä päiviä", "intervals.full.hours": "Tunti tunteja", "intervals.full.minutes": "Minuuti minuuteja", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Uuden listan nimi", "lists.search": "Etsi seuraamistasi henkilöistä", "lists.subheading": "Omat listat", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ladataan...", "media_gallery.toggle_visible": "Säädä näkyvyyttä", "missing_indicator.label": "Ei löytynyt", @@ -314,6 +316,7 @@ "search_results.accounts": "Ihmiset", "search_results.hashtags": "Hashtagit", "search_results.statuses": "Tuuttaukset", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 06bb70e02..f4db2e7a1 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basique", "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# jour} other {# jours}}", "intervals.full.hours": "{number, plural, one {# heure} other {# heures}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titre de la nouvelle liste", "lists.search": "Rechercher parmi les gens que vous suivez", "lists.subheading": "Vos listes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", @@ -314,6 +316,7 @@ "search_results.accounts": "Comptes", "search_results.hashtags": "Hashtags", "search_results.statuses": "Pouets", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", "status.admin_account": "Ouvrir l'interface de modération pour @{name}", "status.admin_status": "Ouvrir ce statut dans l'interface de modération", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 9b19d6f11..2605f61f8 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar repeticións", "home.column_settings.show_replies": "Mostrar respostas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural,one {# día} other {# días}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre a xente que segues", "lists.subheading": "As túas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Ocultar", "missing_indicator.label": "Non atopado", @@ -314,6 +316,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count,plural,one {result} outros {results}}", "status.admin_account": "Abrir interface de moderación para @{name}", "status.admin_status": "Abrir este estado na interface de moderación", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando", "ui.beforeunload": "O borrador perderase se sae de Mastodon.", "upload_area.title": "Arrastre e solte para subir", - "upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Engadir medios ({formats})", "upload_error.limit": "Excedeu o límite de subida de ficheiros.", "upload_error.poll": "Non se poden subir ficheiros nas sondaxes.", "upload_form.description": "Describa para deficientes visuais", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 248be3c7b..99bb87a5f 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -64,7 +64,7 @@ "column_header.show_settings": "הצגת העדפות", "column_header.unpin": "שחרור קיבוע", "column_subheading.settings": "אפשרויות", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "למתחילים", "home.column_settings.show_reblogs": "הצגת הדהודים", "home.column_settings.show_replies": "הצגת תגובות", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index ac58514d4..d4d9e5f64 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 6f9b5343a..273b70d07 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži boostove", "home.column_settings.show_replies": "Pokaži odgovore", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 1c3b63d7d..38d30efe4 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Alapértelmezések", "home.column_settings.show_reblogs": "Megtolások mutatása", "home.column_settings.show_replies": "Válaszok mutatása", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# nap} other {# nap}}", "intervals.full.hours": "{number, plural, one {# óra} other {# óra}}", "intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Új lista címe", "lists.search": "Keresés a követett személyek között", "lists.subheading": "Listáid", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Betöltés...", "media_gallery.toggle_visible": "Láthatóság állítása", "missing_indicator.label": "Nincs találat", @@ -314,6 +316,7 @@ "search_results.accounts": "Emberek", "search_results.hashtags": "Hashtagek", "search_results.statuses": "Tülkök", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {találat} other {találat}}", "status.admin_account": "Moderáció megnyitása @{name} felhasználóhoz", "status.admin_status": "Tülk megnyitása moderációra", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index b2dc16a48..801d34380 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Ցուցադրել կարգավորումները", "column_header.unpin": "Հանել", "column_subheading.settings": "Կարգավորումներ", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Հիմնական", "home.column_settings.show_reblogs": "Ցուցադրել տարածածները", "home.column_settings.show_replies": "Ցուցադրել պատասխանները", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Նոր ցանկի վերնագիր", "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ", "lists.subheading": "Քո ցանկերը", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Բեռնվում է…", "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել", "missing_indicator.label": "Չգտնվեց", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 07ce0eb98..daa87f955 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Tambah atau Hapus dari daftar", "account.badges.bot": "Bot", "account.block": "Blokir @{name}", "account.block_domain": "Sembunyikan segalanya dari {domain}", @@ -7,23 +7,23 @@ "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain disembunyikan", "account.edit_profile": "Ubah profil", - "account.endorse": "Feature on profile", + "account.endorse": "Tampilkan di profil", "account.follow": "Ikuti", "account.followers": "Pengikut", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Tidak ada satupun yang mengkuti pengguna ini saat ini.", "account.follows": "Mengikuti", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Pengguna ini belum mengikuti siapapun.", "account.follows_you": "Mengikuti anda", "account.hide_reblogs": "Sembunyikan boosts dari @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.link_verified_on": "Kepemilikan tautan ini telah dicek pada {date}", + "account.locked_info": "Status privasi akun ini disetel untuk dikunci. Pemilik secara manual meninjau siapa yang dapat mengikuti mereka.", "account.media": "Media", "account.mention": "Balasan @{name}", "account.moved_to": "{name} telah pindah ke:", "account.mute": "Bisukan @{name}", "account.mute_notifications": "Sembunyikan notifikasi dari @{name}", "account.muted": "Dibisukan", - "account.posts": "Toots", + "account.posts": "Toot", "account.posts_with_replies": "Postingan dengan balasan", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan", @@ -31,23 +31,23 @@ "account.show_reblogs": "Tampilkan boost dari @{name}", "account.unblock": "Hapus blokir @{name}", "account.unblock_domain": "Tampilkan {domain}", - "account.unendorse": "Don't feature on profile", + "account.unendorse": "Jangan tampilkan di profil", "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", "account.unmute_notifications": "Munculkan notifikasi dari @{name}", - "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.message": "Terjadi kesalahan yang tidak terduga.", "alert.unexpected.title": "Oops!", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", "bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.", "bundle_column_error.retry": "Coba lagi", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Kesalahan jaringan", "bundle_modal_error.close": "Tutup", "bundle_modal_error.message": "Kesalahan terjadi saat memuat komponen ini.", "bundle_modal_error.retry": "Coba lagi", "column.blocks": "Pengguna diblokir", "column.community": "Linimasa Lokal", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", + "column.direct": "Pesan langsung", + "column.domain_blocks": "Topik tersembunyi", "column.favourites": "Favorit", "column.follow_requests": "Permintaan mengikuti", "column.home": "Beranda", @@ -64,41 +64,41 @@ "column_header.show_settings": "Tampilkan pengaturan", "column_header.unpin": "Lepaskan", "column_subheading.settings": "Pengaturan", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Hanya media", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.direct_message_warning_learn_more": "Pelajari selengkapnya", "compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.", "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", "compose_form.lock_disclaimer.lock": "terkunci", "compose_form.placeholder": "Apa yang ada di pikiran anda?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Tambahkan pilihan", + "compose_form.poll.duration": "Durasi jajak pendapat", + "compose_form.poll.option_placeholder": "Pilihan {number}", + "compose_form.poll.remove_option": "Hapus opsi ini", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Tandai sebagai media sensitif", "compose_form.sensitive.marked": "Sumber ini telah ditandai sebagai sumber sensitif.", "compose_form.sensitive.unmarked": "Sumber ini tidak ditandai sebagai sumber sensitif", "compose_form.spoiler.marked": "Teks disembunyikan dibalik peringatan", "compose_form.spoiler.unmarked": "Teks tidak tersembunyi", "compose_form.spoiler_placeholder": "Peringatan konten", "confirmation_modal.cancel": "Batal", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blokir & Laporkan", "confirmations.block.confirm": "Blokir", "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?", "confirmations.delete.confirm": "Hapus", "confirmations.delete.message": "Apa anda yakin untuk menghapus status ini?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Hapus", "confirmations.delete_list.message": "Apakah anda yakin untuk menghapus daftar ini secara permanen?", "confirmations.domain_block.confirm": "Sembunyikan keseluruhan domain", "confirmations.domain_block.message": "Apakah anda benar benar yakin untuk memblokir keseluruhan {domain}? Dalam kasus tertentu beberapa pemblokiran atau penyembunyian lebih baik.", "confirmations.mute.confirm": "Bisukan", "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", - "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.confirm": "Hapus dan konsep ulang", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.confirm": "Balas", + "confirmations.reply.message": "Membalas sekarang akan menimpa pesan yang sedang Anda buat. Anda yakin ingin melanjutkan?", "confirmations.unfollow.confirm": "Berhenti mengikuti", "confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?", "embed.instructions": "Sematkan status ini di website anda dengan menyalin kode di bawah ini.", @@ -117,38 +117,38 @@ "emoji_button.search_results": "Hasil pencarian", "emoji_button.symbols": "Simbol", "emoji_button.travel": "Tempat Wisata", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.account_timeline": "Tidak ada toot di sini!", + "empty_column.account_unavailable": "Profil tidak tersedia", + "empty_column.blocks": "Anda belum memblokir siapapun.", "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.direct": "Anda belum memiliki pesan langsung. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.domain_blocks": "Tidak ada topik tersembunyi.", + "empty_column.favourited_statuses": "Anda belum memiliki toot favorit. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.favourites": "Tidak ada seorangpun yang memfavoritkan toot ini. Ketika seseorang melakukannya, maka akan muncul disini.", + "empty_column.follow_requests": "Anda belum memiliki permintaan mengikuti. Ketika Anda menerimanya, maka akan muncul disini.", "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", "empty_column.home": "Linimasa anda kosong! Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", "empty_column.home.public_timeline": "linimasa publik", "empty_column.list": "Tidak ada postingan di list ini. Ketika anggota dari list ini memposting status baru, status tersebut akan tampil disini.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.lists": "Anda belum memiliki daftar. Ketika Anda membuatnya, maka akan muncul disini.", + "empty_column.mutes": "Anda belum membisukan siapapun.", "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisi ini", "follow_request.authorize": "Izinkan", "follow_request.reject": "Tolak", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "getting_started.developers": "Pengembang", + "getting_started.directory": "Direktori profil", + "getting_started.documentation": "Dokumentasi", "getting_started.heading": "Mulai", - "getting_started.invite": "Invite people", + "getting_started.invite": "Undang orang", "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "getting_started.security": "Keamanan", + "getting_started.terms": "Ketentuan layanan", + "hashtag.column_header.tag_mode.all": "dan {additional}", + "hashtag.column_header.tag_mode.any": "atau {additional}", + "hashtag.column_header.tag_mode.none": "tanpa {additional}", + "hashtag.column_settings.select.no_options_message": "Tidak ada saran yang ditemukan", + "hashtag.column_settings.select.placeholder": "Masukkan tagar…", "hashtag.column_settings.tag_mode.all": "All of these", "hashtag.column_settings.tag_mode.any": "Any of these", "hashtag.column_settings.tag_mode.none": "None of these", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Dasar", "home.column_settings.show_reblogs": "Tampilkan boost", "home.column_settings.show_replies": "Tampilkan balasan", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {hasil} other {hasil}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index c3f8707d1..864d49995 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Simpla", "home.column_settings.show_reblogs": "Montrar repeti", "home.column_settings.show_replies": "Montrar respondi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f7e2e4353..7925cef8c 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -4,7 +4,7 @@ "account.block": "Blocca @{name}", "account.block_domain": "Nascondi tutto da {domain}", "account.blocked": "Bloccato", - "account.direct": "Invia messaggio diretto a @{name}", + "account.direct": "Invia messaggio privato a @{name}", "account.domain_blocked": "Dominio nascosto", "account.edit_profile": "Modifica profilo", "account.endorse": "Metti in evidenza sul profilo", @@ -121,7 +121,7 @@ "empty_column.account_unavailable": "Profilo non disponibile", "empty_column.blocks": "Non hai ancora bloccato nessun utente.", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", - "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.", + "empty_column.direct": "Non hai ancora nessun messaggio privato. Quando ne manderai o riceverai qualcuno, apparirà qui.", "empty_column.domain_blocks": "Non vi sono domini nascosti.", "empty_column.favourited_statuses": "Non hai ancora segnato nessun toot come apprezzato. Quando lo farai, comparirà qui.", "empty_column.favourites": "Nessuno ha ancora segnato questo toot come apprezzato. Quando qualcuno lo farà, apparirà qui.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Semplice", "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}", "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titolo della nuova lista", "lists.search": "Cerca tra le persone che segui", "lists.subheading": "Le tue liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Caricamento...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", @@ -283,7 +285,7 @@ "poll_button.remove_poll": "Rimuovi sondaggio", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", - "privacy.direct.short": "Diretto", + "privacy.direct.short": "Diretto in privato", "privacy.private.long": "Invia solo ai seguaci", "privacy.private.short": "Privato", "privacy.public.long": "Invia alla timeline pubblica", @@ -314,6 +316,7 @@ "search_results.accounts": "Gente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Toot", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", "status.admin_account": "Apri interfaccia di moderazione per @{name}", "status.admin_status": "Apri questo status nell'interfaccia di moderazione", @@ -323,7 +326,7 @@ "status.copy": "Copia link allo status", "status.delete": "Elimina", "status.detailed_status": "Vista conversazione dettagliata", - "status.direct": "Messaggio diretto @{name}", + "status.direct": "Messaggio privato @{name}", "status.embed": "Incorpora", "status.favourite": "Apprezzato", "status.filtered": "Filtrato", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 83a69e595..7312ce5ac 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本設定", "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number}日", "intervals.full.hours": "{number}時間", "intervals.full.minutes": "{number}分", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新規リスト名", "lists.search": "フォローしている人の中から検索", "lists.subheading": "あなたのリスト", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", @@ -314,6 +316,7 @@ "search_results.accounts": "人々", "search_results.hashtags": "ハッシュタグ", "search_results.statuses": "トゥート", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number}件の結果", "status.admin_account": "@{name} のモデレーション画面を開く", "status.admin_status": "このトゥートをモデレーション画面で開く", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index ff7059aea..a78543476 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "ძირითადი", "home.column_settings.show_reblogs": "ბუსტების ჩვენება", "home.column_settings.show_replies": "პასუხების ჩვენება", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "ახალი სიის სათაური", "lists.search": "ძებნა ადამიანებს შორის რომელთაც მიჰყვებით", "lists.subheading": "თქვენი სიები", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "იტვირთება...", "media_gallery.toggle_visible": "ხილვადობის ჩართვა", "missing_indicator.label": "არაა ნაპოვნი", @@ -314,6 +316,7 @@ "search_results.accounts": "ხალხი", "search_results.hashtags": "ჰეშტეგები", "search_results.statuses": "ტუტები", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index b9bd7cac3..9514d68a9 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Негізгі", "home.column_settings.show_reblogs": "Бөлісулерді көрсету", "home.column_settings.show_replies": "Жауаптарды көрсету", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# күн} other {# күн}}", "intervals.full.hours": "{number, plural, one {# сағат} other {# сағат}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Жаңа тізім аты", "lists.search": "Сіз іздеген адамдар арасында іздеу", "lists.subheading": "Тізімдеріңіз", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Жүктеу...", "media_gallery.toggle_visible": "Көрінуді қосу", "missing_indicator.label": "Табылмады", @@ -314,6 +316,7 @@ "search_results.accounts": "Адамдар", "search_results.hashtags": "Хэштегтер", "search_results.statuses": "Жазбалар", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "@{name} үшін модерация интерфейсін аш", "status.admin_status": "Бұл жазбаны модерация интерфейсінде аш", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 656a36bce..e71631938 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "기본 설정", "home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_replies": "답글 표시", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number} 일", "intervals.full.hours": "{number} 시간", "intervals.full.minutes": "{number} 분", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "새 리스트의 이름", "lists.search": "팔로우 중인 사람들 중에서 찾기", "lists.subheading": "당신의 리스트", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", @@ -314,6 +316,7 @@ "search_results.accounts": "사람", "search_results.hashtags": "해시태그", "search_results.statuses": "툿", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number}건의 결과", "status.admin_account": "@{name}에 대한 모더레이션 인터페이스 열기", "status.admin_status": "모더레이션 인터페이스에서 이 게시물 열기", @@ -326,7 +329,7 @@ "status.direct": "@{name}에게 다이렉트 메시지", "status.embed": "공유하기", "status.favourite": "즐겨찾기", - "status.filtered": "필터링 됨", + "status.filtered": "필터로 걸러짐", "status.load_more": "더 보기", "status.media_hidden": "미디어 숨겨짐", "status.mention": "답장", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index ac58514d4..919129cc5 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 647e23a69..5328f15c5 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index d7c509963..ad72b3233 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index f6504f4bb..d7f428193 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Algemeen", "home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_replies": "Reacties tonen", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dag} other {# dagen}}", "intervals.full.hours": "{number, plural, one {# uur} other {# uur}}", "intervals.full.minutes": "{number, plural, one {# minuut} other {# minuten}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Naam nieuwe lijst", "lists.search": "Zoek naar mensen die je volgt", "lists.subheading": "Jouw lijsten", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", @@ -314,6 +316,7 @@ "search_results.accounts": "Gebruikers", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", "status.admin_account": "Moderatie-omgeving van @{name} openen", "status.admin_status": "Deze toot in de moderatie-omgeving openen", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 2ba8236e2..ea722a01e 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Vis innstillinger", "column_header.unpin": "Løsne", "column_subheading.settings": "Innstillinger", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "Denne tuten blir ikke listet under noen emneknagger da den er ulistet. Kun offentlige tuter kan søktes etter med emneknagg.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Enkel", "home.column_settings.show_reblogs": "Vis fremhevinger", "home.column_settings.show_replies": "Vis svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny listetittel", "lists.search": "Søk blant personer du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 3178f200d..34804da20 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -77,7 +77,7 @@ "compose_form.poll.remove_option": "Levar aquesta opcion", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish} !", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar coma sensible", "compose_form.sensitive.marked": "Lo mèdia es marcat coma sensible", "compose_form.sensitive.unmarked": "Lo mèdia es pas marcat coma sensible", "compose_form.spoiler.marked": "Lo tèxte es rescondut jos l’avertiment", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Mostrar los partatges", "home.column_settings.show_replies": "Mostrar las responsas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# jorn} other {# jorns}}", "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minutas}}", @@ -204,14 +205,14 @@ "keyboard_shortcuts.search": "anar a la recèrca", "keyboard_shortcuts.start": "dobrir la colomna « Per començar »", "keyboard_shortcuts.toggle_hidden": "mostrar/amagar lo tèxte dels avertiments", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "per mostrar/rescondre los mèdias", "keyboard_shortcuts.toot": "començar un estatut tot novèl", "keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca", "keyboard_shortcuts.up": "far montar dins la lista", "lightbox.close": "Tampar", "lightbox.next": "Seguent", "lightbox.previous": "Precedent", - "lightbox.view_context": "View context", + "lightbox.view_context": "Veire lo contèxt", "lists.account.add": "Ajustar a la lista", "lists.account.remove": "Levar de la lista", "lists.delete": "Suprimir la lista", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Títol de la nòva lista", "lists.search": "Cercar demest lo monde que seguètz", "lists.subheading": "Vòstras listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favorits", "navigation_bar.filters": "Mots ignorats", "navigation_bar.follow_requests": "Demandas d’abonament", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Abonament e seguidors", "navigation_bar.info": "Tocant aqueste servidor", "navigation_bar.keyboard_shortcuts": "Acorchis clavièr", "navigation_bar.lists": "Listas", @@ -246,7 +248,7 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Tuts penjats", "navigation_bar.preferences": "Preferéncias", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Annuari de perfils", "navigation_bar.public_timeline": "Flux public global", "navigation_bar.security": "Seguretat", "notification.favourite": "{name} a ajustat a sos favorits", @@ -314,6 +316,7 @@ "search_results.accounts": "Gents", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Tuts", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Dobrir l’interfàcia de moderacion per @{name}", "status.admin_status": "Dobrir aqueste estatut dins l’interfàcia de moderacion", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 44edf6e3d..fb44f6e10 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Podstawowe", "home.column_settings.show_reblogs": "Pokazuj podbicia", "home.column_settings.show_replies": "Pokazuj odpowiedzi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}", "intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}", "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Wprowadź tytuł listy", "lists.search": "Szukaj wśród osób które śledzisz", "lists.subheading": "Twoje listy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", @@ -314,6 +316,7 @@ "search_results.accounts": "Ludzie", "search_results.hashtags": "Hashtagi", "search_results.statuses": "Wpisy", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index dca087af9..1fb700874 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -36,7 +36,7 @@ "account.unmute": "Não silenciar @{name}", "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}", "alert.unexpected.message": "Um erro inesperado ocorreu.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.title": "Eita!", "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_column_error.retry": "Tente novamente", @@ -77,7 +77,7 @@ "compose_form.poll.remove_option": "Remover essa opção", "compose_form.publish": "Publicar", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar mídia como sensível", "compose_form.sensitive.marked": "Mídia está marcada como sensível", "compose_form.sensitive.unmarked": "Mídia não está marcada como sensível", "compose_form.spoiler.marked": "O texto está escondido por um aviso de conteúdo", @@ -89,7 +89,7 @@ "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?", "confirmations.delete.confirm": "Excluir", "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Excluir", "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?", "confirmations.domain_block.confirm": "Esconder o domínio inteiro", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado. Você não vai ver conteúdo desse domínio em nenhuma das timelines públicas ou nas suas notificações. Seus seguidores desse domínio serão removidos.", @@ -156,13 +156,14 @@ "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar compartilhamentos", "home.column_settings.show_replies": "Mostrar as respostas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Próximo", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Global", "introduction.federation.federated.text": "Posts públicos de outros servidores do fediverso vão aparecer na timeline global.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "Posts de pessoas que você segue vão aparecer na sua página inicial. Você pode seguir pessoas de qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Posts públicos de pessoas no mesmo servidor que você vão aparecer na timeline local.", @@ -204,23 +205,24 @@ "keyboard_shortcuts.search": "para focar a pesquisa", "keyboard_shortcuts.start": "para abrir a coluna \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "mostrar/esconder o texto com aviso de conteúdo", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/esconder mídia", "keyboard_shortcuts.toot": "para compor um novo toot", "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar a listas", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Excluir lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre as pessoas que você segue", "lists.subheading": "Suas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Mais informações", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", @@ -246,7 +248,7 @@ "navigation_bar.personal": "Pessoal", "navigation_bar.pins": "Postagens fixadas", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Diretório de perfis", "navigation_bar.public_timeline": "Global", "navigation_bar.security": "Segurança", "notification.favourite": "{name} adicionou a sua postagem aos favoritos", @@ -314,6 +316,7 @@ "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir interface de moderação para @{name}", "status.admin_status": "Abrir esse status na interface de moderação", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 157090c55..c6ea3f847 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -17,7 +17,7 @@ "account.hide_reblogs": "Esconder partilhas de @{name}", "account.link_verified_on": "A posse deste link foi verificada em {date}", "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.", - "account.media": "Media", + "account.media": "Média", "account.mention": "Mencionar @{name}", "account.moved_to": "{name} mudou a sua conta para:", "account.mute": "Silenciar @{name}", @@ -49,50 +49,50 @@ "column.direct": "Mensagens directas", "column.domain_blocks": "Domínios escondidos", "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores Pendentes", + "column.follow_requests": "Seguidores pendentes", "column.home": "Início", "column.lists": "Listas", "column.mutes": "Utilizadores silenciados", "column.notifications": "Notificações", "column.pins": "Publicações fixas", - "column.public": "Cronologia federativa", + "column.public": "Cronologia federada", "column_back_button.label": "Voltar", - "column_header.hide_settings": "Esconder preferências", + "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", "column_header.moveRight_settings": "Mover coluna para a direita", "column_header.pin": "Fixar", - "column_header.show_settings": "Mostrar preferências", + "column_header.show_settings": "Mostrar configurações", "column_header.unpin": "Desafixar", - "column_subheading.settings": "Preferências", - "community.column_settings.media_only": "Somente media", - "compose_form.direct_message_warning": "Esta publicação só será enviada para os utilizadores mencionados.", - "compose_form.direct_message_warning_learn_more": "Aprender mais", - "compose_form.hashtag_warning": "Esta pulbicacção não será listada em nenhuma hashtag por ser não listada. Somente publicações públicas podem ser pesquisadas por hashtag.", + "column_subheading.settings": "Configurações", + "community.column_settings.media_only": "Somente multimédia", + "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.", + "compose_form.direct_message_warning_learn_more": "Conhecer mais", + "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.", "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", - "compose_form.lock_disclaimer.lock": "fechada", + "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.publish": "Publicar", - "compose_form.publish_loud": "{publicar}!", - "compose_form.sensitive.hide": "Mark media as sensitive", - "compose_form.sensitive.marked": "Media marcado como sensível", - "compose_form.sensitive.unmarked": "Media não está marcado como sensível", + "compose_form.poll.add_option": "Adicionar uma opção", + "compose_form.poll.duration": "Duração da votação", + "compose_form.poll.option_placeholder": "Opção {number}", + "compose_form.poll.remove_option": "Eliminar esta opção", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Marcar multimédia como sensível", + "compose_form.sensitive.marked": "Média marcada como sensível", + "compose_form.sensitive.unmarked": "Média não está marcada como sensível", "compose_form.spoiler.marked": "Texto escondido atrás de aviso", "compose_form.spoiler.unmarked": "O texto não está escondido", "compose_form.spoiler_placeholder": "Escreve o teu aviso aqui", "confirmation_modal.cancel": "Cancelar", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Bloquear e denunciar", "confirmations.block.confirm": "Bloquear", "confirmations.block.message": "De certeza que queres bloquear {name}?", "confirmations.delete.confirm": "Eliminar", "confirmations.delete.message": "De certeza que queres eliminar esta publicação?", - "confirmations.delete_list.confirm": "Apagar", - "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?", + "confirmations.delete_list.confirm": "Eliminar", + "confirmations.delete_list.message": "Tens a certeza de que desejas eliminar permanentemente esta lista?", "confirmations.domain_block.confirm": "Esconder tudo deste domínio", - "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma, nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", + "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é suficiente e é o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", "confirmations.mute.confirm": "Silenciar", "confirmations.mute.message": "De certeza que queres silenciar {name}?", "confirmations.redraft.confirm": "Apagar & redigir", @@ -109,23 +109,23 @@ "emoji_button.food": "Comida & Bebida", "emoji_button.label": "Inserir Emoji", "emoji_button.nature": "Natureza", - "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Não tem emojis!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objectos", "emoji_button.people": "Pessoas", - "emoji_button.recent": "Regularmente utilizados", - "emoji_button.search": "Procurar...", + "emoji_button.recent": "Utilizados regularmente", + "emoji_button.search": "Pesquisar...", "emoji_button.search_results": "Resultados da pesquisa", "emoji_button.symbols": "Símbolos", "emoji_button.travel": "Viagens & Lugares", - "empty_column.account_timeline": "Sem publicações!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_timeline": "Sem toots por aqui!", + "empty_column.account_unavailable": "Perfil indisponível", "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.", - "empty_column.community": "Ainda não existe conteúdo local para mostrar!", + "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!", "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.", "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.", - "empty_column.favourited_statuses": "Ainda não tens quaisquer publicações favoritas. Quando tiveres alguma, ela irá aparecer aqui.", - "empty_column.favourites": "Ainda ninguém favorizou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", - "empty_column.follow_requests": "Ainda não tens pedido de seguimento algum. Quando receberes algum, ele irá aparecer aqui.", + "empty_column.favourited_statuses": "Ainda não tens quaisquer toots favoritos. Quando tiveres algum, ele irá aparecer aqui.", + "empty_column.favourites": "Ainda ninguém marcou este toot como favorito. Quando alguém o fizer, ele irá aparecer aqui.", + "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguimento. Quando receberes algum, ele irá aparecer aqui.", "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home.public_timeline": "Cronologia pública", @@ -138,10 +138,10 @@ "follow_request.reject": "Rejeitar", "getting_started.developers": "Responsáveis pelo desenvolvimento", "getting_started.directory": "Directório de perfil", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentação", "getting_started.heading": "Primeiros passos", "getting_started.invite": "Convidar pessoas", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", + "getting_started.open_source_notice": "Mastodon é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {github}.", "getting_started.security": "Segurança", "getting_started.terms": "Termos de serviço", "hashtag.column_header.tag_mode.all": "e {additional}", @@ -154,28 +154,29 @@ "hashtag.column_settings.tag_mode.none": "Nenhum destes", "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionais para esta coluna", "home.column_settings.basic": "Básico", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.show_reblogs": "Mostrar boosts", + "home.column_settings.show_replies": "Mostrar respostas", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Seguinte", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federada", "introduction.federation.federated.text": "Publicações públicas de outros servidores do fediverse aparecerão na cronologia federativa.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "As publicações das pessoas que tu segues aparecerão na tua coluna inicial. Tu podes seguir qualquer pessoa em qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Publicações públicas de pessoas que tu segues no teu servidor aparecerão na coluna local.", "introduction.interactions.action": "Terminar o tutorial!", "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "Tu podes guardar um toot para depois e deixar o autor saber que gostaste dele, favoritando-o.", - "introduction.interactions.reblog.headline": "Partilhar", + "introduction.interactions.favourite.text": "Podes guardar um toot para depois e deixar o autor saber que gostaste dele, marcando-o como favorito.", + "introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.text": "Podes partilhar os toots de outras pessoas com os teus seguidores partilhando-os.", "introduction.interactions.reply.headline": "Responder", "introduction.interactions.reply.text": "Tu podes responder a toots de outras pessoas e aos teus, o que os irá juntar numa conversa.", "introduction.welcome.action": "Vamos!", "introduction.welcome.headline": "Primeiros passos", - "introduction.welcome.text": "Bem-vindo ao fediverse! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", + "introduction.welcome.text": "Bem-vindo ao fediverso! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", "keyboard_shortcuts.back": "para voltar", "keyboard_shortcuts.blocked": "para abrir a lista de utilizadores bloqueados", "keyboard_shortcuts.boost": "para partilhar", @@ -184,10 +185,10 @@ "keyboard_shortcuts.description": "Descrição", "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas", "keyboard_shortcuts.down": "para mover para baixo na lista", - "keyboard_shortcuts.enter": "para expandir uma publicação", + "keyboard_shortcuts.enter": "para expandir um estado", "keyboard_shortcuts.favourite": "para adicionar aos favoritos", "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos", - "keyboard_shortcuts.federated": "para abrir a cronologia federativa", + "keyboard_shortcuts.federated": "para abrir a cronologia federada", "keyboard_shortcuts.heading": "Atalhos do teclado", "keyboard_shortcuts.home": "para abrir a cronologia inicial", "keyboard_shortcuts.hotkey": "Atalho", @@ -204,31 +205,32 @@ "keyboard_shortcuts.search": "para focar na pesquisa", "keyboard_shortcuts.start": "para abrir a coluna dos \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "para mostrar/esconder texto atrás de CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "para compor um novo post", - "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar média", + "keyboard_shortcuts.toot": "para compor um novo toot", + "keyboard_shortcuts.unfocus": "para remover o foco da área de texto/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar à lista", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Remover lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", - "lists.new.title_placeholder": "Novo título da lista", + "lists.new.title_placeholder": "Título da nova lista", "lists.search": "Pesquisa entre as pessoas que segues", "lists.subheading": "As tuas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "A carregar...", - "media_gallery.toggle_visible": "Esconder/Mostrar", + "media_gallery.toggle_visible": "Mostrar/ocultar", "missing_indicator.label": "Não encontrado", "missing_indicator.sublabel": "Este recurso não foi encontrado", "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", "navigation_bar.apps": "Aplicações móveis", "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Local", + "navigation_bar.community_timeline": "Cronologia local", "navigation_bar.compose": "Escrever novo toot", "navigation_bar.direct": "Mensagens directas", "navigation_bar.discover": "Descobrir", @@ -237,23 +239,23 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Sobre este servidor", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.personal": "Personal", - "navigation_bar.pins": "Posts fixos", + "navigation_bar.personal": "Pessoal", + "navigation_bar.pins": "Toots afixados", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Global", + "navigation_bar.profile_directory": "Directório de perfis", + "navigation_bar.public_timeline": "Cronologia federada", "navigation_bar.security": "Segurança", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", + "notification.favourite": "{name} adicionou o teu estado aos favoritos", + "notification.follow": "{name} começou a seguir-te", "notification.mention": "{name} mencionou-te", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} partilhou o teu post", + "notification.poll": "Uma votação em participaste chegou ao fim", + "notification.reblog": "{name} fez boost ao teu o teu estado", "notifications.clear": "Limpar notificações", "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", "notifications.column_settings.alert": "Notificações no computador", @@ -263,24 +265,24 @@ "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados da votação:", "notifications.column_settings.push": "Notificações Push", - "notifications.column_settings.reblog": "Partilhas:", - "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Mostrar na coluna", "notifications.column_settings.sound": "Reproduzir som", "notifications.filter.all": "Todas", - "notifications.filter.boosts": "Partilhas", - "notifications.filter.favourites": "Favoritas", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favoritos", "notifications.filter.follows": "Seguimento", "notifications.filter.mentions": "Referências", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Resultados da votação", "notifications.group": "{count} notificações", "poll.closed": "Fechado", "poll.refresh": "Recarregar", "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}", "poll.vote": "Votar", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll_button.add_poll": "Adicionar votação", + "poll_button.remove_poll": "Remover votação", "privacy.change": "Ajustar a privacidade da mensagem", "privacy.direct.long": "Apenas para utilizadores mencionados", "privacy.direct.short": "Directo", @@ -300,26 +302,27 @@ "reply_indicator.cancel": "Cancelar", "report.forward": "Reenviar para {target}", "report.forward_hint": "A conta é de outro servidor. Enviar uma cópia anónima do relatório para lá também?", - "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a relatar esta conta:", + "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a denunciar esta conta:", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_popout.search_format": "Formato avançado de pesquisa", - "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, favoritaste, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", + "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "estado", "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", "search_popout.tips.user": "utilizador", "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", - "search_results.statuses": "Publicações", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir a interface de moderação para @{name}", "status.admin_status": "Abrir esta publicação na interface de moderação", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Não partilhar", - "status.cannot_reblog": "Este post não pode ser partilhado", + "status.block": "Bloquear @{name}", + "status.cancel_reblog_private": "Remover boost", + "status.cannot_reblog": "Não é possível fazer boost a esta publicação", "status.copy": "Copiar o link para a publicação", "status.delete": "Eliminar", "status.detailed_status": "Vista de conversação detalhada", @@ -328,7 +331,7 @@ "status.favourite": "Adicionar aos favoritos", "status.filtered": "Filtrada", "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", + "status.media_hidden": "Média escondida", "status.mention": "Mencionar @{name}", "status.more": "Mais", "status.mute": "Silenciar @{name}", @@ -338,15 +341,15 @@ "status.pinned": "Publicação fixa", "status.read_more": "Ler mais", "status.reblog": "Partilhar", - "status.reblog_private": "Partilhar com a audiência original", - "status.reblogged_by": "{name} partilhou", - "status.reblogs.empty": "Ainda ninguém partilhou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", + "status.reblog_private": "Fazer boost com a audiência original", + "status.reblogged_by": "{name} fez boost", + "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.", "status.redraft": "Apagar & reescrever", "status.reply": "Responder", "status.replyAll": "Responder à conversa", "status.report": "Denunciar @{name}", "status.sensitive_warning": "Conteúdo sensível", - "status.share": "Compartilhar", + "status.share": "Partilhar", "status.show_less": "Mostrar menos", "status.show_less_all": "Mostrar menos para todas", "status.show_more": "Mostrar mais", @@ -356,22 +359,22 @@ "status.unpin": "Não fixar no perfil", "suggestions.dismiss": "Dispensar a sugestão", "suggestions.header": "Tu podes estar interessado em…", - "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", + "tabs_bar.federated_timeline": "Federada", + "tabs_bar.home": "Início", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "tabs_bar.search": "Pesquisar", "time_remaining.days": "{número, plural, um {# day} outro {# days}} faltam", "time_remaining.hours": "{número, plural, um {# hour} outro {# hours}} faltam", "time_remaining.minutes": "{número, plural, um {# minute} outro {# minutes}} faltam", - "time_remaining.moments": "Momentos em falta", + "time_remaining.moments": "Momentos restantes", "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam", "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar", - "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", + "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.", "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", "upload_form.focus": "Alterar previsualização", "upload_form.undo": "Apagar", @@ -379,7 +382,7 @@ "video.close": "Fechar vídeo", "video.exit_fullscreen": "Sair de full screen", "video.expand": "Expandir vídeo", - "video.fullscreen": "Full screen", + "video.fullscreen": "Ecrã completo", "video.hide": "Esconder vídeo", "video.mute": "Silenciar", "video.pause": "Pausar", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index dcb7a088d..ac10d4678 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "De bază", "home.column_settings.show_reblogs": "Arată redistribuirile", "home.column_settings.show_replies": "Arată răspunsurile", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titlu pentru noua listă", "lists.search": "Caută printre persoanale pe care le urmărești", "lists.subheading": "Listele tale", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Încărcare...", "media_gallery.toggle_visible": "Comutați vizibilitatea", "missing_indicator.label": "Nu a fost găsit", @@ -314,6 +316,7 @@ "search_results.accounts": "Oameni", "search_results.hashtags": "Hashtaguri", "search_results.statuses": "Postări", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index d720b6272..8a7a39a06 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -13,20 +13,20 @@ "account.followers.empty": "Никто не подписан на этого пользователя.", "account.follows": "Подписки", "account.follows.empty": "Этот пользователь ни на кого не подписан.", - "account.follows_you": "Подписан(а) на Вас", + "account.follows_you": "Подписан(а) на вас", "account.hide_reblogs": "Скрыть реблоги от @{name}", "account.link_verified_on": "Владение этой ссылкой было проверено {date}", "account.locked_info": "Это закрытый аккаунт. Его владелец вручную одобряет подписчиков.", "account.media": "Медиа", "account.mention": "Упомянуть", "account.moved_to": "Ищите {name} здесь:", - "account.mute": "Заглушить", + "account.mute": "Скрыть @{name}", "account.mute_notifications": "Скрыть уведомления от @{name}", - "account.muted": "Приглушён", + "account.muted": "Скрыт", "account.posts": "Посты", - "account.posts_with_replies": "Посты и ответы", + "account.posts_with_replies": "Посты с ответами", "account.report": "Пожаловаться", - "account.requested": "Ожидает подтверждения", + "account.requested": "Ожидает подтверждения. Нажмите для отмены", "account.share": "Поделиться профилем @{name}", "account.show_reblogs": "Показывать продвижения от @{name}", "account.unblock": "Разблокировать", @@ -52,7 +52,7 @@ "column.follow_requests": "Запросы на подписку", "column.home": "Главная", "column.lists": "Списки", - "column.mutes": "Список глушения", + "column.mutes": "Список скрытых пользователей", "column.notifications": "Уведомления", "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", @@ -70,12 +70,12 @@ "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.", "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer.lock": "закрыт", - "compose_form.placeholder": "О чем Вы думаете?", + "compose_form.placeholder": "О чем вы думаете?", "compose_form.poll.add_option": "Добавить", "compose_form.poll.duration": "Длительность опроса", "compose_form.poll.option_placeholder": "Вариант {number}", "compose_form.poll.remove_option": "Удалить этот вариант", - "compose_form.publish": "Трубить", + "compose_form.publish": "Запостить", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive.hide": "Пометить медиафайл как чувствительный", "compose_form.sensitive.marked": "Медиафайлы не отмечены как чувствительные", @@ -117,31 +117,31 @@ "emoji_button.search_results": "Результаты поиска", "emoji_button.symbols": "Символы", "emoji_button.travel": "Путешествия", - "empty_column.account_timeline": "Статусов нет!", + "empty_column.account_timeline": "Здесь нет постов!", "empty_column.account_unavailable": "Профиль недоступен", "empty_column.blocks": "Вы ещё никого не заблокировали.", "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", - "empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.", + "empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите одно, оно появится здесь.", "empty_column.domain_blocks": "Скрытых доменов пока нет.", - "empty_column.favourited_statuses": "Вы не добавили ни одного статуса в 'Избранное'. Как только Вы это сделаете, они появятся здесь.", - "empty_column.favourites": "Никто ещё не добавил этот статус в 'Избранное'. Как только кто-то это сделает, они появятся здесь.", + "empty_column.favourited_statuses": "Вы не добавили ни один пост в «Избранное». Как только вы это сделаете, он появится здесь.", + "empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.", "empty_column.follow_requests": "Вам ещё не приходили запросы на подписку. Все новые запросы будут показаны здесь.", "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", - "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", + "empty_column.home": "Пока вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", "empty_column.home.public_timeline": "публичные ленты", "empty_column.list": "В этом списке пока ничего нет.", - "empty_column.lists": "У Вас ещё нет списков. Все созданные Вами списки будут показаны здесь.", - "empty_column.mutes": "Вы ещё никого не заглушили.", - "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", + "empty_column.lists": "У вас ещё нет списков. Созданные вами списки будут показаны здесь.", + "empty_column.mutes": "Вы ещё никого не скрывали.", + "empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.", "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", "follow_request.authorize": "Авторизовать", "follow_request.reject": "Отказать", - "getting_started.developers": "Для разработчиков", + "getting_started.developers": "Разработчикам", "getting_started.directory": "Каталог профилей", "getting_started.documentation": "Документация", "getting_started.heading": "Добро пожаловать", "getting_started.invite": "Пригласить людей", - "getting_started.open_source_notice": "Mastodon - сервис с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}.", + "getting_started.open_source_notice": "Mastodon — сервис с открытым исходным кодом. Вы можете внести вклад или сообщить о проблемах на GitHub: {github}.", "getting_started.security": "Безопасность", "getting_started.terms": "Условия использования", "hashtag.column_header.tag_mode.all": "и {additional}", @@ -156,9 +156,10 @@ "home.column_settings.basic": "Основные", "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", - "intervals.full.days": "{number, plural, one {# день} few {# дня} many {# дней} other {# дней}}", - "intervals.full.hours": "{number, plural, one {# час} few {# часа} many {# часов} other {# часов}}", - "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} many {# минут} other {# минут}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}", + "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}", + "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}", "introduction.federation.action": "Далее", "introduction.federation.federated.headline": "Глобальная лента", "introduction.federation.federated.text": "Публичные статусы с других серверов федеративной сети расположатся в глобальной ленте.", @@ -167,7 +168,7 @@ "introduction.federation.local.headline": "Локальная лента", "introduction.federation.local.text": "Публичные статусы от людей с того же сервера, что и вы, будут отображены в локальной ленте.", "introduction.interactions.action": "Завершить обучение", - "introduction.interactions.favourite.headline": "Отметки \"нравится\"", + "introduction.interactions.favourite.headline": "Отметки «нравится»", "introduction.interactions.favourite.text": "Вы можете отметить статус, чтобы вернуться к нему позже и дать знать автору, что запись вам понравилась, поставив отметку \"нравится\".", "introduction.interactions.reblog.headline": "Продвижения", "introduction.interactions.reblog.text": "Вы можете делиться статусами других людей, продвигая их в своём аккаунте.", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Заголовок списка", "lists.search": "Искать из ваших подписок", "lists.subheading": "Ваши списки", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", @@ -242,7 +244,7 @@ "navigation_bar.keyboard_shortcuts": "Сочетания клавиш", "navigation_bar.lists": "Списки", "navigation_bar.logout": "Выйти", - "navigation_bar.mutes": "Список глушения", + "navigation_bar.mutes": "Список скрытых пользователей", "navigation_bar.personal": "Личное", "navigation_bar.pins": "Закреплённые посты", "navigation_bar.preferences": "Опции", @@ -314,6 +316,7 @@ "search_results.accounts": "Люди", "search_results.hashtags": "Хэштеги", "search_results.statuses": "Посты", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", "status.admin_account": "Открыть интерфейс модератора для @{name}", "status.admin_status": "Открыть этот статус в интерфейсе модератора", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 18993af97..3cc2cbaa7 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Základné", "home.column_settings.show_reblogs": "Zobraziť povýšené", "home.column_settings.show_replies": "Ukázať odpovede", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dni}}", "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}", "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Názov nového zoznamu", "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ", "lists.subheading": "Tvoje zoznamy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Načítam...", "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť", "missing_indicator.label": "Nenájdené", @@ -254,7 +256,7 @@ "notification.mention": "{name} ťa spomenul/a", "notification.poll": "Anketa v ktorej si hlasoval/a sa skončila", "notification.reblog": "{name} zdieľal/a tvoj príspevok", - "notifications.clear": "Vyčistiť zoznam oboznámení", + "notifications.clear": "Vyčisti oboznámenia", "notifications.clear_confirmation": "Naozaj chceš nenávratne prečistiť všetky tvoje oboznámenia?", "notifications.column_settings.alert": "Oboznámenia na ploche", "notifications.column_settings.favourite": "Obľúbené:", @@ -314,6 +316,7 @@ "search_results.accounts": "Ľudia", "search_results.hashtags": "Haštagy", "search_results.statuses": "Príspevky", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}", "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}", "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 51794a862..f79a7051a 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži spodbude", "home.column_settings.show_replies": "Pokaži odgovore", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}", "intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}", "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", @@ -167,7 +168,7 @@ "introduction.federation.local.headline": "Lokalno", "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.", "introduction.interactions.action": "Zaključi vadnico!", - "introduction.interactions.favourite.headline": "Priljubljeni", + "introduction.interactions.favourite.headline": "Vzljubi", "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.", "introduction.interactions.reblog.headline": "Spodbudi", "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Nov naslov seznama", "lists.search": "Išči med ljudmi, katerim sledite", "lists.subheading": "Vaši seznami", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Nalaganje...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Ni najdeno", @@ -314,6 +316,7 @@ "search_results.accounts": "Ljudje", "search_results.hashtags": "Ključniki", "search_results.statuses": "Tuti", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}", "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", "status.admin_status": "Odpri status v vmesniku za moderiranje", @@ -351,29 +354,29 @@ "status.show_less_all": "Prikaži manj za vse", "status.show_more": "Prikaži več", "status.show_more_all": "Prikaži več za vse", - "status.show_thread": "Show thread", + "status.show_thread": "Prikaži objavo", "status.unmute_conversation": "Odtišaj pogovor", "status.unpin": "Odpni iz profila", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Zavrni predlog", + "suggestions.header": "Morda bi vas zanimalo…", "tabs_bar.federated_timeline": "Združeno", "tabs_bar.home": "Domov", "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Obvestila", - "tabs_bar.search": "Poišči", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "tabs_bar.search": "Iskanje", + "time_remaining.days": "{number, plural, one {# dan} other {# dni}} je ostalo", + "time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo", + "time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo", + "time_remaining.moments": "Preostali trenutki", + "time_remaining.seconds": "{number, plural, one {# sekunda} other {# sekund}} je ostalo", + "trends.count_by_accounts": "{count} {rawCount, plural, one {oseba} other {ljudi}} govori", "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", - "upload_area.title": "Povlecite in spustite za pošiljanje", - "upload_button.label": "Dodaj medij", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_area.title": "Za pošiljanje povlecite in spustite", + "upload_button.label": "Dodaj medije ({formats})", + "upload_error.limit": "Omejitev prenosa datoteke je presežena.", + "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.", "upload_form.description": "Opišite za slabovidne", - "upload_form.focus": "Obreži", + "upload_form.focus": "Spremeni predogled", "upload_form.undo": "Izbriši", "upload_progress.label": "Pošiljanje...", "video.close": "Zapri video", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 13ce4e978..21d45f2e8 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bazë", "home.column_settings.show_reblogs": "Shfaq përforcime", "home.column_settings.show_replies": "Shfaq përgjigje", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titull liste të re", "lists.search": "Kërkoni mes personash që ndiqni", "lists.subheading": "Listat tuaja", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.toggle_visible": "Ndërroni dukshmërinë", "missing_indicator.label": "S’u gjet", @@ -314,6 +316,7 @@ "search_results.accounts": "Persona", "search_results.hashtags": "Hashtagë", "search_results.statuses": "Mesazhe", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, një {result} {results} të tjera}", "status.admin_account": "Hap ndërfaqe moderimi për @{name}", "status.admin_status": "Hape këtë gjendje te ndërfaqja e moderimit", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 8f8ca7c30..55bae4cdd 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Prikaži postavke", "column_header.unpin": "Otkači", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Prikaži i podržavanja", "home.column_settings.show_replies": "Prikaži odgovore", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Naslov nove liste", "lists.search": "Pretraži među ljudima koje pratite", "lists.subheading": "Vaše liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Uključi/isključi vidljivost", "missing_indicator.label": "Nije pronađeno", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 8ef18a774..a4ae9fcaa 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Основно", "home.column_settings.show_reblogs": "Прикажи и подржавања", "home.column_settings.show_replies": "Прикажи одговоре", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Наслов нове листе", "lists.search": "Претражи међу људима које пратите", "lists.subheading": "Ваше листе", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Учитавам...", "media_gallery.toggle_visible": "Укључи/искључи видљивост", "missing_indicator.label": "Није пронађено", @@ -314,6 +316,7 @@ "search_results.accounts": "Људи", "search_results.hashtags": "Тарабе", "search_results.statuses": "Трубе", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index ab12be885..fda5c4d57 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Grundläggande", "home.column_settings.show_reblogs": "Visa knuffar", "home.column_settings.show_replies": "Visa svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny listrubrik", "lists.search": "Sök bland personer du följer", "lists.subheading": "Dina listor", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laddar...", "media_gallery.toggle_visible": "Växla synlighet", "missing_indicator.label": "Hittades inte", @@ -314,6 +316,7 @@ "search_results.accounts": "Människor", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 637ca884a..87163e660 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "அடிப்படையான", "home.column_settings.show_reblogs": "காட்டு boosts", "home.column_settings.show_replies": "பதில்களைக் காண்பி", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} மற்ற {# days}}", "intervals.full.hours": "{number, plural, one {# hour} மற்ற {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} மற்ற {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "புதிய பட்டியல் தலைப்பு", "lists.search": "நீங்கள் பின்தொடரும் நபர்கள் மத்தியில் தேடுதல்", "lists.subheading": "உங்கள் பட்டியல்கள்", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "ஏற்றுதல்...", "media_gallery.toggle_visible": "நிலைமாற்று தெரியும்", "missing_indicator.label": "கிடைக்கவில்லை", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "ஹாஷ்டேக்குகளைச்", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} மற்ற {results}}", "status.admin_account": "மிதமான இடைமுகத்தை திறக்க @{name}", "status.admin_status": "மிதமான இடைமுகத்தில் இந்த நிலையை திறக்கவும்", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 269ea45c3..ccb608812 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "ప్రాథమిక", "home.column_settings.show_reblogs": "బూస్ట్ లను చూపించు", "home.column_settings.show_replies": "ప్రత్యుత్తరాలను చూపించు", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "కొత్త జాబితా శీర్షిక", "lists.search": "మీరు అనుసరించే వ్యక్తులలో శోధించండి", "lists.subheading": "మీ జాబితాలు", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "లోడ్ అవుతోంది...", "media_gallery.toggle_visible": "దృశ్యమానతను టోగుల్ చేయండి", "missing_indicator.label": "దొరకలేదు", @@ -314,6 +316,7 @@ "search_results.accounts": "వ్యక్తులు", "search_results.hashtags": "హాష్ ట్యాగ్లు", "search_results.statuses": "టూట్లు", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "@{name} కొరకు సమన్వయ వినిమయసీమను తెరువు", "status.admin_status": "సమన్వయ వినిమయసీమలో ఈ స్టేటస్ ను తెరవండి", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 3bcf389c7..e8d7a27ed 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -72,7 +72,7 @@ "compose_form.lock_disclaimer.lock": "ล็อคอยู่", "compose_form.placeholder": "คุณกำลังคิดอะไรอยู่?", "compose_form.poll.add_option": "เพิ่มทางเลือก", - "compose_form.poll.duration": "Poll duration", + "compose_form.poll.duration": "ระยะเวลาการหยั่งเสียง", "compose_form.poll.option_placeholder": "ทางเลือก {number}", "compose_form.poll.remove_option": "เอาทางเลือกนี้ออก", "compose_form.publish": "โพสต์", @@ -156,9 +156,10 @@ "home.column_settings.basic": "พื้นฐาน", "home.column_settings.show_reblogs": "แสดงการดัน", "home.column_settings.show_replies": "แสดงการตอบกลับ", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, other {# วัน}}", + "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}", + "intervals.full.minutes": "{number, plural, other {# นาที}}", "introduction.federation.action": "ถัดไป", "introduction.federation.federated.headline": "ที่ติดต่อกับภายนอก", "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของ Fediverse จะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "ชื่อเรื่องรายการใหม่", "lists.search": "ค้นหาในหมู่ผู้คนที่คุณติดตาม", "lists.subheading": "รายการของคุณ", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "กำลังโหลด...", "media_gallery.toggle_visible": "เปิด/ปิดการมองเห็น", "missing_indicator.label": "ไม่พบ", @@ -252,7 +254,7 @@ "notification.favourite": "{name} ได้ชื่นชอบสถานะของคุณ", "notification.follow": "{name} ได้ติดตามคุณ", "notification.mention": "{name} ได้กล่าวถึงคุณ", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "การหยั่งเสียงที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว", "notification.reblog": "{name} ได้ดันสถานะของคุณ", "notifications.clear": "ล้างการแจ้งเตือน", "notifications.clear_confirmation": "คุณแน่ใจหรือไม่ว่าต้องการล้างการแจ้งเตือนทั้งหมดของคุณอย่างถาวร?", @@ -263,7 +265,7 @@ "notifications.column_settings.filter_bar.show": "แสดง", "notifications.column_settings.follow": "ผู้ติดตามใหม่:", "notifications.column_settings.mention": "การกล่าวถึง:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "ผลลัพธ์การหยั่งเสียง:", "notifications.column_settings.push": "การแจ้งเตือนแบบผลัก", "notifications.column_settings.reblog": "การดัน:", "notifications.column_settings.show": "แสดงในคอลัมน์", @@ -273,14 +275,14 @@ "notifications.filter.favourites": "รายการโปรด", "notifications.filter.follows": "การติดตาม", "notifications.filter.mentions": "การกล่าวถึง", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "ผลลัพธ์การหยั่งเสียง", "notifications.group": "{count} การแจ้งเตือน", "poll.closed": "ปิดแล้ว", "poll.refresh": "รีเฟรช", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.total_votes": "{count, plural, other {# การลงคะแนน}}", + "poll.vote": "ลงคะแนน", + "poll_button.add_poll": "เพิ่มการหยั่งเสียง", + "poll_button.remove_poll": "เอาการหยั่งเสียงออก", "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ", "privacy.direct.long": "โพสต์ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น", "privacy.direct.short": "โดยตรง", @@ -314,7 +316,8 @@ "search_results.accounts": "ผู้คน", "search_results.hashtags": "แฮชแท็ก", "search_results.statuses": "โพสต์", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, other {ผลลัพธ์}}", "status.admin_account": "เปิดส่วนติดต่อการควบคุมสำหรับ @{name}", "status.admin_status": "เปิดสถานะนี้ในส่วนติดต่อการควบคุม", "status.block": "ปิดกั้น @{name}", @@ -361,11 +364,11 @@ "tabs_bar.local_timeline": "ในเว็บ", "tabs_bar.notifications": "การแจ้งเตือน", "tabs_bar.search": "ค้นหา", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.days": "เหลืออีก {number, plural, other {# วัน}}", + "time_remaining.hours": "เหลืออีก {number, plural, other {# ชั่วโมง}}", + "time_remaining.minutes": "เหลืออีก {number, plural, other {# นาที}}", "time_remaining.moments": "ช่วงเวลาที่เหลือ", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "time_remaining.seconds": "เหลืออีก {number, plural, other {# วินาที}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "แบบร่างของคุณจะหายไปหากคุณออกจาก Mastodon", "upload_area.title": "ลากแล้วปล่อยเพื่ออัปโหลด", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ec4657b9b..0ea015cc6 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Temel", "home.column_settings.show_reblogs": "Boost edilenleri göster", "home.column_settings.show_replies": "Cevapları göster", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Yeni liste başlığı", "lists.search": "Takip ettiğiniz kişiler arasından arayın", "lists.subheading": "Listeleriniz", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", @@ -314,6 +316,7 @@ "search_results.accounts": "İnsanlar", "search_results.hashtags": "Hashtagler", "search_results.statuses": "Gönderiler", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", "status.admin_account": "@{name} için denetim arayüzünü açın", "status.admin_status": "Denetim arayüzünde bu durumu açın", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 124b9fb07..17e8cb49f 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Основні", "home.column_settings.show_reblogs": "Показувати передмухи", "home.column_settings.show_replies": "Показувати відповіді", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Нова назва списку", "lists.search": "Шукати серед людей, на яких ви підписані", "lists.subheading": "Ваші списки", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 865d3a514..bb774f1aa 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -12,7 +12,7 @@ "account.followers": "关注者", "account.followers.empty": "目前无人关注此用户。", "account.follows": "正在关注", - "account.follows.empty": "此用户目前没有关注任何人。", + "account.follows.empty": "此用户目前尚未关注任何人。", "account.follows_you": "关注了你", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.link_verified_on": "此链接的所有权已在 {date} 检查", @@ -48,7 +48,7 @@ "column.community": "本站时间轴", "column.direct": "私信", "column.domain_blocks": "已屏蔽的网站", - "column.favourites": "收藏过的嘟文", + "column.favourites": "收藏", "column.follow_requests": "关注请求", "column.home": "主页", "column.lists": "列表", @@ -92,11 +92,11 @@ "confirmations.delete_list.confirm": "删除", "confirmations.delete_list.message": "你确定要永久删除这个列表吗?", "confirmations.domain_block.confirm": "隐藏整个网站的内容", - "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户应该就能满足你的需要了。来自该网站的内容将不再出现在你的公共时间轴或通知列表里。来自该网站的关注者将会被移除。", + "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就已经足够了。来自该网站的内容将不再出现在你的任何公共时间轴或通知列表里。来自该网站的关注者将会被移除。", "confirmations.mute.confirm": "隐藏", "confirmations.mute.message": "你确定要隐藏 {name} 吗?", "confirmations.redraft.confirm": "删除并重新编辑", - "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和收藏都会被清除,回复将会被孤立。", + "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和收藏都会被清除,回复将会失去关联。", "confirmations.reply.confirm": "回复", "confirmations.reply.message": "回复此消息将会覆盖当前正在编辑的信息。确定继续吗?", "confirmations.unfollow.confirm": "取消关注", @@ -120,28 +120,28 @@ "empty_column.account_timeline": "这里没有嘟文!", "empty_column.account_unavailable": "个人资料不可用", "empty_column.blocks": "你目前没有屏蔽任何用户。", - "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!", + "empty_column.community": "本站时间轴暂时没有内容,快写点什么让它动起来吧!", "empty_column.direct": "你还没有使用过私信。当你发出或者收到私信时,它会在这里显示。", "empty_column.domain_blocks": "目前没有被隐藏的站点。", "empty_column.favourited_statuses": "你还没有收藏过任何嘟文。收藏过的嘟文会显示在这里。", - "empty_column.favourites": "没人收藏过这条嘟文。假如有人收藏了,就会显示在这里。", + "empty_column.favourites": "没有人收藏过这条嘟文。如果有人收藏了,就会显示在这里。", "empty_column.follow_requests": "你没有收到新的关注请求。收到了之后就会显示在这里。", "empty_column.hashtag": "这个话题标签下暂时没有内容。", - "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他人问个好吧。", "empty_column.home.public_timeline": "公共时间轴", "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。", - "empty_column.lists": "你没有创建过列表。你创建的列表会在这里显示。", + "empty_column.lists": "你还没有创建过列表。你创建的列表会在这里显示。", "empty_column.mutes": "你没有隐藏任何用户。", - "empty_column.notifications": "你还没有收到过任何通知,快向其他用户搭讪吧。", + "empty_column.notifications": "你还没有收到过任何通知,快和其他用户互动吧。", "empty_column.public": "这里什么都没有!写一些公开的嘟文,或者关注其他服务器的用户后,这里就会有嘟文出现了", "follow_request.authorize": "同意", "follow_request.reject": "拒绝", "getting_started.developers": "开发", - "getting_started.directory": "用户资料目录", + "getting_started.directory": "用户目录", "getting_started.documentation": "文档", "getting_started.heading": "开始使用", "getting_started.invite": "邀请用户", - "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", + "getting_started.open_source_notice": "Mastodon 是开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", "getting_started.security": "帐户安全", "getting_started.terms": "使用条款", "hashtag.column_header.tag_mode.all": "以及 {additional}", @@ -156,14 +156,15 @@ "home.column_settings.basic": "基本设置", "home.column_settings.show_reblogs": "显示转嘟", "home.column_settings.show_replies": "显示回复", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number} 天", "intervals.full.hours": "{number} 小时", "intervals.full.minutes": "{number} 分钟", "introduction.federation.action": "下一步", "introduction.federation.federated.headline": "跨站", - "introduction.federation.federated.text": "其他跨站服务器的公共动态会显示在跨站时间线中。", + "introduction.federation.federated.text": "联邦宇宙中其他服务器的公开嘟文会显示在跨站时间轴中。", "introduction.federation.home.headline": "主页", - "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", + "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", "introduction.federation.local.headline": "本站", "introduction.federation.local.text": "你所关注的用户的动态会显示在主页里,你可以关注任何服务器上的任何人。", "introduction.interactions.action": "教程结束!", @@ -172,10 +173,10 @@ "introduction.interactions.reblog.headline": "转嘟", "introduction.interactions.reblog.text": "通过转嘟,你可以向你的关注者分享其他人的嘟文。", "introduction.interactions.reply.headline": "回复", - "introduction.interactions.reply.text": "你可以向其他人回复,这些回复会像对话一样串在一起。", + "introduction.interactions.reply.text": "你可以回复其他嘟文,这些回复会像对话一样关联在一起。", "introduction.welcome.action": "让我们开始吧!", "introduction.welcome.headline": "首先", - "introduction.welcome.text": "欢迎来到联邦!稍后,您将可以广播消息并和您的朋友交流,这些消息将穿越于联邦中的各式服务器。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", + "introduction.welcome.text": "欢迎来到联邦宇宙!很快,您就可以发布信息并和您的朋友交流,这些消息将发送到联邦中的各个服务器中。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", "keyboard_shortcuts.back": "返回上一页", "keyboard_shortcuts.blocked": "打开被屏蔽用户列表", "keyboard_shortcuts.boost": "转嘟", @@ -194,9 +195,9 @@ "keyboard_shortcuts.legend": "显示此列表", "keyboard_shortcuts.local": "打开本站时间轴", "keyboard_shortcuts.mention": "提及嘟文作者", - "keyboard_shortcuts.muted": "打开屏蔽用户列表", + "keyboard_shortcuts.muted": "打开隐藏用户列表", "keyboard_shortcuts.my_profile": "打开你的个人资料", - "keyboard_shortcuts.notifications": "打卡通知栏", + "keyboard_shortcuts.notifications": "打开通知栏", "keyboard_shortcuts.pinned": "打开置顶嘟文列表", "keyboard_shortcuts.profile": "打开作者的个人资料", "keyboard_shortcuts.reply": "回复嘟文", @@ -213,7 +214,7 @@ "lightbox.previous": "上一个", "lightbox.view_context": "查看上下文", "lists.account.add": "添加到列表", - "lists.account.remove": "从列表中删除", + "lists.account.remove": "从列表中移除", "lists.delete": "删除列表", "lists.edit": "编辑列表", "lists.edit.submit": "更改标题", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新列表的标题", "lists.search": "搜索你关注的人", "lists.subheading": "你的列表", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "切换显示/隐藏", "missing_indicator.label": "找不到内容", @@ -235,29 +237,29 @@ "navigation_bar.domain_blocks": "已屏蔽的网站", "navigation_bar.edit_profile": "修改个人资料", "navigation_bar.favourites": "收藏的内容", - "navigation_bar.filters": "被隐藏的词", + "navigation_bar.filters": "屏蔽关键词", "navigation_bar.follow_requests": "关注请求", - "navigation_bar.follows_and_followers": "正在关注以及关注者", + "navigation_bar.follows_and_followers": "关注管理", "navigation_bar.info": "关于本站", "navigation_bar.keyboard_shortcuts": "快捷键列表", "navigation_bar.lists": "列表", - "navigation_bar.logout": "注销", + "navigation_bar.logout": "登出", "navigation_bar.mutes": "已隐藏的用户", "navigation_bar.personal": "个人", "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", - "navigation_bar.profile_directory": "用户资料目录", + "navigation_bar.profile_directory": "用户目录", "navigation_bar.public_timeline": "跨站公共时间轴", "navigation_bar.security": "安全", "notification.favourite": "{name} 收藏了你的嘟文", "notification.follow": "{name} 开始关注你", - "notification.mention": "{name} 提及你", + "notification.mention": "{name} 提及了你", "notification.poll": "你参与的一个投票已经结束", - "notification.reblog": "{name} 转了你的嘟文", + "notification.reblog": "{name} 转嘟了你的嘟文", "notifications.clear": "清空通知列表", "notifications.clear_confirmation": "你确定要永久清空通知列表吗?", "notifications.column_settings.alert": "桌面通知", - "notifications.column_settings.favourite": "你的嘟文被收藏时:", + "notifications.column_settings.favourite": "当你的嘟文被收藏时:", "notifications.column_settings.filter_bar.advanced": "显示所有类别", "notifications.column_settings.filter_bar.category": "快速过滤栏", "notifications.column_settings.filter_bar.show": "显示", @@ -301,25 +303,26 @@ "report.forward": "发送举报至 {target}", "report.forward_hint": "这名用户来自另一个服务器。是否要向那个服务器发送一条匿名的举报?", "report.hint": "举报将会发送给你所在服务器的监察员。你可以在下面填写举报该用户的理由:", - "report.placeholder": "附言", + "report.placeholder": "备注", "report.submit": "提交", "report.target": "举报 {target}", "search.placeholder": "搜索", "search_popout.search_format": "高级搜索格式", - "search_popout.tips.full_text": "输入其他内容将会返回所有你撰写、收藏、转嘟过或提及到你的嘟文,同时也会在用户名、昵称和话题标签中进行搜索。", + "search_popout.tips.full_text": "输入关键词检索所有你发送、收藏、转嘟过或提及到你的嘟文,以及其他用户公开的用户名、昵称和话题标签。", "search_popout.tips.hashtag": "话题标签", "search_popout.tips.status": "嘟文", - "search_popout.tips.text": "输入其他内容将会返回昵称、用户名和话题标签", + "search_popout.tips.text": "输入关键词检索昵称、用户名和话题标签", "search_popout.tips.user": "用户", "search_results.accounts": "用户", "search_results.hashtags": "话题标签", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "共 {count, number} 个结果", "status.admin_account": "打开 @{name} 的管理界面", "status.admin_status": "打开这条嘟文的管理界面", "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", - "status.cannot_reblog": "无法转嘟这条嘟文", + "status.cannot_reblog": "这条嘟文不允许被转嘟", "status.copy": "复制嘟文链接", "status.delete": "删除", "status.detailed_status": "对话详情", @@ -338,9 +341,9 @@ "status.pinned": "置顶嘟文", "status.read_more": "阅读全文", "status.reblog": "转嘟", - "status.reblog_private": "转嘟给原有关注者", + "status.reblog_private": "转嘟(可见者不变)", "status.reblogged_by": "{name} 转嘟了", - "status.reblogs.empty": "无人转嘟此条。如果有人转嘟了,就会显示在这里。", + "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。", "status.redraft": "删除并重新编辑", "status.reply": "回复", "status.replyAll": "回复所有人", @@ -367,15 +370,15 @@ "time_remaining.moments": "即将结束", "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 人正在讨论", - "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", + "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会丢失。", "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件 (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "超过文件上传限制。", + "upload_error.limit": "文件大小超过限制。", "upload_error.poll": "投票中不允许上传文件。", "upload_form.description": "为视觉障碍人士添加文字说明", - "upload_form.focus": "剪裁", + "upload_form.focus": "设置缩略图", "upload_form.undo": "删除", - "upload_progress.label": "上传中…", + "upload_progress.label": "上传中……", "video.close": "关闭视频", "video.exit_fullscreen": "退出全屏", "video.expand": "展开视频", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 2cfc11703..b4c8b874a 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本", "home.column_settings.show_reblogs": "顯示被轉推的文章", "home.column_settings.show_replies": "顯示回應文章", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新列表標題", "lists.search": "從你關注的用戶中搜索", "lists.subheading": "列表", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", @@ -314,6 +316,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "標籤", "search_results.statuses": "文章", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} 項結果", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 5715ef01a..5f75b38d6 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本", "home.column_settings.show_reblogs": "顯示轉推", "home.column_settings.show_replies": "顯示回覆", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# 天} other {# 天}}", "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}", "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新名單標題", "lists.search": "搜尋您關注的使用者", "lists.subheading": "您的名單", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", @@ -314,6 +316,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "主題標籤", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} 項結果", "status.admin_account": "開啟 @{name} 的管理介面", "status.admin_status": "在管理介面開啟此嘟文", diff --git a/config/locales/activerecord.bn.yml b/config/locales/activerecord.bn.yml index 152c69829..e0e6ac90c 100644 --- a/config/locales/activerecord.bn.yml +++ b/config/locales/activerecord.bn.yml @@ -1 +1,17 @@ +--- bn: + activerecord: + attributes: + poll: + expires_at: শেষ হবে + options: বিকল্প + errors: + models: + account: + attributes: + username: + invalid: শুধুমাত্র অক্ষর, সংখ্যা এবং _ বেবহার করা যাবে + status: + attributes: + reblog: + taken: লেখাটি ইতিপূর্বে ছিল diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml index 19547df98..92fba043f 100644 --- a/config/locales/activerecord.cy.yml +++ b/config/locales/activerecord.cy.yml @@ -3,7 +3,7 @@ cy: activerecord: attributes: poll: - expires_at: Terfyn + expires_at: Terfyn amser options: Dewisiadau errors: models: diff --git a/config/locales/ar.yml b/config/locales/ar.yml index eb6a5ef06..e48ee89c5 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -478,12 +478,6 @@ ar: no_status_selected: لم يطرأ أي تغيير على أي منشور بما أنه لم يتم اختيار أي واحد title: منشورات الحساب with_media: تحتوي على وسائط - subscriptions: - callback_url: عاود الاتصال بالعنوان - confirmed: مؤكَّد - expires_in: تنتهي مدة صلاحيتها في - last_delivery: آخر إيداع - topic: الموضوع tags: accounts: الحسابات hidden: المخفية @@ -818,10 +812,6 @@ ar: reply: proceed: المواصلة إلى الرد prompt: 'ترغب في الرد على هذا التبويق:' - remote_unfollow: - error: خطأ - title: العنوان - unfollowed: غير متابَع sessions: activity: آخر نشاط browser: المتصفح diff --git a/config/locales/ast.yml b/config/locales/ast.yml index ec545ca57..30390c163 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -227,8 +227,6 @@ ast: no_account_html: "¿Nun tienes una cuenta? Pues rexistrate equí" proceed: Siguir prompt: 'Vas siguir a:' - remote_unfollow: - error: Fallu sessions: browser: Restolador browsers: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index a5d96cc1c..d05406ebb 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -469,13 +469,6 @@ ca: no_status_selected: No s’han canviat els estatus perquè cap no ha estat seleccionat title: Estats del compte with_media: Amb contingut multimèdia - subscriptions: - callback_url: URL de retorn - confirmed: Confirmat - expires_in: Expira en - last_delivery: Últim lliurament - title: WebSub - topic: Tema tags: accounts: Comptes hidden: Amagat @@ -816,10 +809,6 @@ ca: reply: proceed: Procedir a respondre prompt: 'Vols respondre a aquest toot:' - remote_unfollow: - error: Error - title: Títol - unfollowed: Sense seguir scheduled_statuses: over_daily_limit: Has superat el límit de %{limit} toots programats per a aquell dia over_total_limit: Has superat el limit de %{limit} toots programats diff --git a/config/locales/co.yml b/config/locales/co.yml index b3d14fdb5..4e2ceda22 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -469,13 +469,6 @@ co: no_status_selected: I statuti ùn sò micca stati mudificati perchè manc'unu era selezziunatu title: Statutu di u contu with_media: Cù media - subscriptions: - callback_url: URL di richjama - confirmed: Cunfirmatu - expires_in: Spira in - last_delivery: Ultima arricata - title: WebSub - topic: Sughjettu tags: accounts: Conti hidden: Piattatu @@ -816,10 +809,6 @@ co: reply: proceed: Cuntinuà per risponde prompt: 'Vulete risponde à stu statutu:' - remote_unfollow: - error: Errore - title: Titulu - unfollowed: Disabbunatu scheduled_statuses: over_daily_limit: Avete trapassatu a limita di %{limit} statuti planificati per stu ghjornu over_total_limit: Avete trapassatu a limita di %{limit} statuti planificati diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 0735a8698..3518b3b91 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -481,13 +481,6 @@ cs: no_status_selected: Nebyly změněny žádné tooty, neboť žádné nebyly vybrány title: Tooty účtu with_media: S médii - subscriptions: - callback_url: Zpáteční URL - confirmed: Potvrzeno - expires_in: Vyprší v - last_delivery: Poslední doručení - title: WebSub - topic: Téma tags: accounts: Účty hidden: Skryté @@ -838,10 +831,6 @@ cs: reply: proceed: Pokračovat k odpovězení prompt: 'Chcete odpovědět na tento toot:' - remote_unfollow: - error: Chyba - title: Nadpis - unfollowed: Už nesledujete scheduled_statuses: over_daily_limit: Překročil/a jste limit %{limit} plánovaných tootů pro tento den over_total_limit: Překročil/a jste limit %{limit} plánovaných tootů diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 080e89214..fbeaa22b1 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -493,13 +493,6 @@ cy: no_status_selected: Ni newidwyd dim statws achos ni ddewiswyd dim un title: Statysau cyfrif with_media: A chyfryngau - subscriptions: - callback_url: URL galw-nôl - confirmed: Wedi'i gadarnhau - expires_in: Dod i ben ymhen - last_delivery: Danfoniad diwethaf - title: WebSub - topic: Pwnc tags: accounts: Cyfrifon hidden: Cudd @@ -862,10 +855,6 @@ cy: reply: proceed: Ymlaen i ateb prompt: 'Hoffech ateb y tŵt hon:' - remote_unfollow: - error: Gwall - title: Teitl - unfollowed: Dad-ddilynwyd scheduled_statuses: over_daily_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig ar y dydd hynny over_total_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig diff --git a/config/locales/da.yml b/config/locales/da.yml index da6ab1054..b24c9475c 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -390,13 +390,6 @@ da: no_status_selected: Ingen statusser blev ændret eller ingen blev valgt title: Konto statusser with_media: Med multimedier - subscriptions: - callback_url: Callback-URL - confirmed: Bekræftet - expires_in: Udløber om - last_delivery: Sidste levering - title: Websub - topic: Emne tags: accounts: Kontoer hidden: Skjult @@ -616,10 +609,6 @@ da: no_account_html: Har du ikke en konto? Du kan oprette dig her proceed: Fortsæt for at følge prompt: 'Du er ved at følge:' - remote_unfollow: - error: Fejl - title: Titel - unfollowed: Følger ikke længere sessions: activity: Sidste aktivitet browsers: diff --git a/config/locales/de.yml b/config/locales/de.yml index cfdaacab0..b9b8c02df 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -469,13 +469,6 @@ de: no_status_selected: Keine Beiträge wurden geändert, weil keine ausgewählt wurden title: Beiträge des Kontos with_media: Mit Medien - subscriptions: - callback_url: Callback-URL - confirmed: Bestätigt - expires_in: Verfällt in - last_delivery: Letzte Zustellung - title: WebSub - topic: Thema tags: accounts: Konten hidden: Versteckt @@ -816,10 +809,6 @@ de: reply: proceed: Fortfahren zum Antworten prompt: 'Du möchtest auf diesen Beitrag antworten:' - remote_unfollow: - error: Fehler - title: Titel - unfollowed: Entfolgt scheduled_statuses: over_daily_limit: Du hast das Limit für geplante Beiträge, dass %{limit} beträgt, für heute erreicht over_total_limit: Du hast das Limit für geplante Beiträge, dass %{limit} beträgt, erreicht diff --git a/config/locales/devise.bn.yml b/config/locales/devise.bn.yml index 152c69829..cb7179da6 100644 --- a/config/locales/devise.bn.yml +++ b/config/locales/devise.bn.yml @@ -1 +1,40 @@ +--- bn: + devise: + confirmations: + confirmed: আপনার ইমেইলটি সঠিকভাবে নিশ্চিত করা হয়েছে। + send_instructions: আপনি একটি ইমেইল পাবেন যেটাতে কিভাবে আপনার ইমেইলটি নিশ্চিত করতে হবে সেটা পাঠানো হবে। যদি না পান, অনুগ্রহ করে আপনার স্প্যাম ফোল্ডারটি চেক করবেন। + send_paranoid_instructions: আমাদের ডাটাবেসে যদি আপনার ইমেইল থেকে থাকে, আপনার কাছে একটা ইমেইল পাঠানো হবে যেখানে কিভাবে আপনার ইমেইল নিশ্চিত করতে হবে লেখা থাকবে। যদি না পান, অনুগ্রহ করে আপনার স্প্যাম ফোল্ডারটি চেক করবেন। + failure: + already_authenticated: আপনি ইতিপূর্বে ভেতরে ঢুকেছেন (আবার লাগবে না)। + inactive: আনার নিবন্ধনটি এখনো চালু করা হয়নি। + invalid: ভুল %{authentication_keys} বা পাসওয়ার্ড । + last_attempt: আপনার আর একবার চেষ্টা করার সুযোক আছে, তারপর আপনার নিবন্ধনে ঢোকার ক্ষেত্রে তালা দেওয়া হবে। + locked: নিবন্ধনে ঢোকার ক্ষেত্রে তালা দেওয়া হয়েছে। + not_found_in_database: ভুল %{authentication_keys} বা পাসওয়ার্ড। + pending: আপনার নিবন্ধনটি এখনো পর্যালোচনার জন্য অপেক্ষায় আছে। + timeout: আপনার সেশনটির সময় শেষ হয়ে গেছে। অনুগ্রহ করে আবার নিবন্ধনে ঢুকে চালাতে থাকেন। + unauthenticated: এটা ব্যবহার করতে আপনার আগে আপনার নিবন্ধনে ঢুকতে হবে অথবা নিবন্ধন তৈরি করতে হবে। + unconfirmed: এটা ব্যবহার করতে আপনার আগে আপনার ইমেইলটি নিশ্চিত করতে হবে। + mailer: + confirmation_instructions: + action: ইমেইলটি নিশ্চিত করুন + action_with_app: নিশ্চিত করুন এবং %{app} তে ফিরে যান + explanation: "%{host} তে এই ইমেইল ব্যবহার করে নিবন্ধন করতে হবে। আর একটা ক্লিক করলেই এটা চালু হয়ে যাবে। যদি আপনি এটা না পাঠিয়ে থাকেন, তাহলে অনুগ্রহ করে এই ইমেইলটি উপেক্ষা করুন।" + password_change: + extra: আপনি নিজে যদি পাসওয়ার্ডটি না বদলে থাকেন, খুব সম্ভব অন্যকেও আপনার নিবন্ধনে প্রবেশ করে এটা করেছে। অনুগ্রহ করে যত দ্রুত সম্ভব আপনার পাসওয়ার্ডটি বদলান অথবা যদি আপনি আপনার নিবন্ধনে আর না ঢুকতে পারেন, এই সার্ভারের পরিচালককে জানান। + subject: 'মাস্টাডন: পাসওয়ার্ড বদলানো হয়েছে' + title: পাসওয়ার্ড বদলানো হয়েছে + reconfirmation_instructions: + explanation: নতুন ইমেইলটি নিশ্চিত করুন। + extra: আপনি যদি এটা না চেয়ে থাকেন, এই ইমেইলটি উপেক্ষা করুন। উপরের লিংকটিতে না গেলে আপনার নিবন্ধনের সাথে যুক্ত ইমেইল বদলাবে না। + subject: 'মাস্টাডন: ইমেইল নিশ্চিত করুন %{instance} জন্য' + title: আপনার ইমেইলটি নিশ্চিত করুন + reset_password_instructions: + action: পাসওয়ার্ড বদলান + explanation: আপনি আপনার নিবন্ধনের জন্য নতুন পাসওয়ার্ড চেয়েছেন। + extra: আপনি যদি এটা না চেয়ে থাকেন, এই ইমেইলটি উপেক্ষা করুন। উপরের লিংকটিতে না গেলে আপনার পাসওয়ার্ড বদলাবে না। + subject: 'মাস্টাডন: পাসওয়ার্ড বদলানোর নির্দেশনা' + title: পাসওয়ার্ড বদলানো + registrations: + signed_up: স্বাগতম! আপনার নিবন্ধনটি সঠিকভাবে হয়েছে। diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index 85de603d3..4837390db 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -38,7 +38,7 @@ sk: explanation: Potvrď novú emailovú adresu na ktorú chceš zmeniť svoj email. extra: Pokiaľ si túto akciu nevyžiadal/a, prosím ignoruj tento email. Emailová adresa pre tvoj Mastodon účet totiž nebude zmenená pokiaľ nepostúpiš na adresu uvedenú vyššie. subject: 'Mastodon: Potvrďenie emailu pre %{instance}' - title: Overiť emailovú adresu + title: Over emailovú adresu reset_password_instructions: action: Zmeň svoje heslo explanation: Vyžiadal/a si si nové heslo pre svoj účet. diff --git a/config/locales/devise.sl.yml b/config/locales/devise.sl.yml index 7d1e05fdf..dee1b9125 100644 --- a/config/locales/devise.sl.yml +++ b/config/locales/devise.sl.yml @@ -6,7 +6,7 @@ sl: send_instructions: V nekaj minutah boste prejeli e-poštno sporočilo z navodili za potrditev vašega e-poštnega naslova. Če niste prejeli e-poštnega sporočila, preverite mapo neželena pošta. send_paranoid_instructions: Če vaš e-poštni naslov obstaja v naši podatkovni bazi, boste v nekaj minutah prejeli e-poštno sporočilo z navodili za potrditev vašega e-poštnega naslova. Če niste prejeli e-poštnega sporočila, preverite mapo neželena pošta. failure: - already_authenticated: Prijavljeni ste že. + already_authenticated: Ste že prijavljeni. inactive: Vaš račun še ni aktiviran. invalid: Neveljavno %{authentication_keys} ali geslo. last_attempt: Pred zaklepom računa imate še en poskus. @@ -45,9 +45,44 @@ sl: explanation: Zahtevali ste novo geslo za svoj račun. extra: Če tega niste zahtevali, prezrite to e-poštno sporočilo. Vaše geslo se ne bo spremenilo, dokler ne kliknete na zgornjo povezavo in ustvarite novega. subject: 'Mastodon: Navodila za ponastavitev gesla' - title: Ponastavitev gesla + title: Ponastavi geslo unlock_instructions: subject: 'Mastodon: Odkleni navodila' omniauth_callbacks: failure: Overitev iz %{kind} ni možna zaradi "%{reason}". success: Overitev iz računa %{kind} je bila uspešna. + passwords: + no_token: Do te strani ne morete dostopati, ne da bi prišli iz e-poštne za ponastavitev gesla. Če prihajate iz e-poštne za ponastavitev gesla, se prepričajte, da ste uporabili celoten navedeni URL. + send_instructions: Če vaš e-poštni naslov obstaja v naši bazi podatkov, boste v nekaj minutah na vaš e-poštni naslov prejeli povezavo za obnovitev gesla. Če niste prejeli e-pošte, preverite mapo z neželeno pošto. + send_paranoid_instructions: Če vaš e-poštni naslov obstaja v naši bazi podatkov, boste v nekaj minutah na vaš e-poštni naslov prejeli povezavo za obnovitev gesla. Če niste prejeli e-pošte, preverite mapo z neželeno pošto. + updated: Vaše geslo je bilo uspešno spremenjeno. Zdaj ste prijavljeni. + updated_not_active: Vaše geslo je bilo uspešno spremenjeno. + registrations: + destroyed: Adijo! Vaš račun je bil uspešno preklican. Upamo, da vas bomo kmalu spet videli. + signed_up: Dobrodošli! Uspešno ste se vpisali. + signed_up_but_inactive: Uspešno ste se vpisali. Vendar vas nismo mogli prijaviti, ker vaš račun še ni aktiviran. + signed_up_but_locked: Uspešno ste se vpisali. Vendar vas nismo mogli prijaviti, ker je vaš račun zaklenjen. + signed_up_but_pending: Na vaš e-poštni naslov je bilo poslano sporočilo s povezavo za potrditev. Ko kliknete na povezavo, bomo pregledali vašo prijavo. Obveščeni boste, če bo odobren. + signed_up_but_unconfirmed: Na vaš e-poštni naslov je bilo poslano sporočilo s povezavo za potrditev. Sledite povezavi, da aktivirate svoj račun. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + update_needs_confirmation: Uspešno ste posodobili račun, vendar moramo potrditi vaš novi e-poštni naslov. Preverite svojo e-pošto in sledite povezavi za potrditev, da potrdite nov e-poštni naslov. Če niste prejeli te e-poše, preverite mapo z neželeno pošto. + updated: Vaš račun je bil uspešno posodobljen. + sessions: + already_signed_out: Uspešno ste se odjavili. + signed_in: Uspešno ste se prijavili. + signed_out: Uspešno ste se odjavili. + unlocks: + send_instructions: Prejeli boste e-pošto z navodili o tem, kako v nekaj minutah odklenete svoj račun. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + send_paranoid_instructions: Če vaš račun obstaja, boste prejeli e-pošto z navodili za njegovo odklepanje v nekaj minutah. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + unlocked: Vaš račun je bil uspešno odklenjen. Če želite nadaljevati, se prijavite. + errors: + messages: + already_confirmed: je bil potrjen, poskusite se prijaviti + confirmation_period_expired: mora biti potrjena v %{period}, zahtevajte novo + expired: je potekla, zahtevajte novo + not_found: ni najdeno + not_locked: ni bil zaklenjen + not_saved: + few: "%{count} napake so preprečile shranjevanje %{resource}:" + one: '1 napaka je preprečila shranjevanje %{resource}:' + other: "%{count} napak je preprečilo shranjevanje %{resource}:" + two: "%{count} napaki sta preprečili shranjevanje %{resource}:" diff --git a/config/locales/devise.zh-CN.yml b/config/locales/devise.zh-CN.yml index 22fa130f6..f9943238e 100644 --- a/config/locales/devise.zh-CN.yml +++ b/config/locales/devise.zh-CN.yml @@ -58,7 +58,7 @@ zh-CN: updated: 你的密码已修改成功,你现在已登录。 updated_not_active: 你的密码已修改成功。 registrations: - destroyed: 再见!你的帐户已成功注销。我们希望很快可以再见到你。 + destroyed: 再见!你的帐户已成功销毁。我们希望很快可以再见到你。 signed_up: 欢迎!你已注册成功。 signed_up_but_inactive: 你已注册,但尚未激活帐户。 signed_up_but_locked: 你已注册,但帐户被锁定了。 diff --git a/config/locales/doorkeeper.cy.yml b/config/locales/doorkeeper.cy.yml index 19798c4d9..e29043e86 100644 --- a/config/locales/doorkeeper.cy.yml +++ b/config/locales/doorkeeper.cy.yml @@ -114,6 +114,12 @@ cy: application: title: Mae awdurdodiad OAuth yn ofynnol scopes: + admin:read: darllenwch yr holl ddata ar y serfiwr + admin:read:accounts: darllen gwybodaeth sensitif o'r holl gyfrifon + admin:read:reports: darllen gwybodaeth sensitif am bob adroddiad a chyfrifon yr adroddir amdanynt + admin:write: addasu pob data ar y serfiwr + admin:write:accounts: cyflawni camau cymedroli ar gyfrifon + admin:write:reports: cyflawni camau cymedroli ar adroddiadau follow: addasu perthnasau cyfrif push: derbyn eich hysbysiadau gwthiadwy read: darllen holl ddata eich cyfrif diff --git a/config/locales/doorkeeper.es.yml b/config/locales/doorkeeper.es.yml index 752387d87..1b03e33f2 100644 --- a/config/locales/doorkeeper.es.yml +++ b/config/locales/doorkeeper.es.yml @@ -114,7 +114,35 @@ es: application: title: OAuth autorización requerida scopes: + admin:read: leer todos los datos en el servidor + admin:read:accounts: leer información sensible de todas las cuentas + admin:read:reports: leer información sensible de todos los informes y cuentas reportadas + admin:write: modificar todos los datos en el servidor + admin:write:accounts: realizar acciones de moderación en cuentas + admin:write:reports: realizar acciones de moderación en informes follow: seguir, bloquear, desbloquear y dejar de seguir cuentas + push: recibir tus notificaciones push read: leer los datos de tu cuenta + read:accounts: ver información de cuentas + read:blocks: ver a quién has bloqueado + read:favourites: ver tus favoritos + read:filters: ver tus filtros + read:follows: ver a quién sigues + read:lists: ver tus listas + read:mutes: ver a quién has silenciado + read:notifications: ver tus notificaciones + read:reports: ver tus informes + read:search: buscar en su nombre + read:statuses: ver todos los estados write: publicar en tu nombre + write:accounts: modifica tu perfil write:blocks: bloquear cuentas y dominios + write:favourites: toots favoritos + write:filters: crear filtros + write:follows: seguir usuarios + write:lists: crear listas + write:media: subir archivos multimedia + write:mutes: silenciar usuarios y conversaciones + write:notifications: limpia tus notificaciones + write:reports: reportar a otras personas + write:statuses: publicar estados diff --git a/config/locales/doorkeeper.eu.yml b/config/locales/doorkeeper.eu.yml index f98babae6..70e52e8ad 100644 --- a/config/locales/doorkeeper.eu.yml +++ b/config/locales/doorkeeper.eu.yml @@ -5,7 +5,7 @@ eu: doorkeeper/application: name: Aplikazioaren izena redirect_uri: Birbideratu URIa - scopes: Esparruak + scopes: Irismena website: Aplikazioaren webgunea errors: models: @@ -33,14 +33,14 @@ eu: help: native_redirect_uri: Erabili %{native_redirect_uri} proba lokaletarako redirect_uri: Erabili lerro bat URI bakoitzeko - scopes: Banandu esparruak espazioekin. Laga hutsik lehenetsitako esparruak erabiltzeko. + scopes: Banandu irismenak espazioekin. Laga hutsik lehenetsitako irismenak erabiltzeko. index: application: Aplikazioa callback_url: Itzulera URLa delete: Ezabatu name: Izena new: Aplikazio berria - scopes: Esparruak + scopes: Irismena show: Erakutsi title: Zure aplikazioak new: @@ -49,7 +49,7 @@ eu: actions: Ekintzak application_id: Bezeroaren gakoa callback_urls: Itzulera URL-ak - scopes: Esparruak + scopes: Irismena secret: Bezeroaren sekretua title: 'Aplikazioa: %{name}' authorizations: @@ -73,7 +73,7 @@ eu: application: Aplikazioa created_at: Baimenduta date_format: "%Y-%m-%d %H:%M:%S" - scopes: Esparruak + scopes: Irismena title: Zuk baimendutako aplikazioak errors: messages: @@ -114,6 +114,12 @@ eu: application: title: OAuth autorizazioa behar da scopes: + admin:read: zerbitzariko datu guztiak irakurri + admin:read:accounts: kontu guztien informazio sentsiblea irakurri + admin:read:reports: salaketa guztietako eta salatutako kontu guztietako informazio sentsiblea irakurri + admin:write: zerbitzariko datu guztiak aldatu + admin:write:accounts: kontuetan moderazio ekintzak burutu + admin:write:reports: salaketetan moderazio ekintzak burutu follow: aldatu kontuaren erlazioak push: jaso push jakinarazpenak read: irakurri zure kontuko datu guztiak diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml index 122392864..92b4e6839 100644 --- a/config/locales/doorkeeper.hu.yml +++ b/config/locales/doorkeeper.hu.yml @@ -114,6 +114,12 @@ hu: application: title: OAuth engedély szükséges scopes: + admin:read: szerver minden adatának olvasása + admin:read:accounts: minden érzékeny fiókadat olvasása + admin:read:reports: minden bejelentés és bejelentett fiók érzékeny adatainak olvasása + admin:write: szerver minden adatának változtatása + admin:write:accounts: moderációs műveletek végzése fiókokon + admin:write:reports: moderációs műveletek végzése bejelentéseken follow: fiókok követése, letiltása, tiltás feloldása és követés abbahagyása push: push értesítések fogadása read: fiókod adatainak olvasása diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml index aa37ea190..1fabfc123 100644 --- a/config/locales/doorkeeper.nl.yml +++ b/config/locales/doorkeeper.nl.yml @@ -114,6 +114,12 @@ nl: application: title: OAuth-autorisatie vereist scopes: + admin:read: lees alle gegevens op de server + admin:read:accounts: lees gevoelige informatie van alle accounts + admin:read:reports: lees gevoelige informatie van alle rapportages en gerapporteerde accounts + admin:write: wijzig alle gegevens op de server + admin:write:accounts: moderatieacties op accounts uitvoeren + admin:write:reports: moderatieacties op rapportages uitvoeren follow: relaties tussen accounts bewerken push: ontvang jouw pushmeldingen read: alle gegevens van jouw account lezen diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml index d97c2f600..e715cc7d5 100644 --- a/config/locales/doorkeeper.oc.yml +++ b/config/locales/doorkeeper.oc.yml @@ -114,6 +114,12 @@ oc: application: title: Cal una autorizacion OAuth scopes: + admin:read: lectura de totas las donadas del servidor + admin:read:accounts: lectura de las informacions sensiblas dels comptes + admin:read:reports: lectura de las informacions sensiblas dels senhalaments e dels comptes senhalats + admin:write: modificacion de las donadas del servidor + admin:write:accounts: realizacion d’accions de moderacion suls comptes + admin:write:reports: realizacion d’accions suls senhalaments follow: modificar las relacions del compte push: recebre vòstras notificacions push read: legir totas las donadas de vòstre compte diff --git a/config/locales/doorkeeper.sk.yml b/config/locales/doorkeeper.sk.yml index f54eb6d48..9eaef177f 100644 --- a/config/locales/doorkeeper.sk.yml +++ b/config/locales/doorkeeper.sk.yml @@ -19,34 +19,34 @@ sk: doorkeeper: applications: buttons: - authorize: Overiť - cancel: Zrušiť + authorize: Autorizuj + cancel: Zruš destroy: Zničiť - edit: Upraviť - submit: Poslať + edit: Uprav + submit: Pošli confirmations: destroy: Si si istý/á? edit: - title: Upraviť aplikáciu + title: Uprav aplikáciu form: - error: No teda! Pozrite formulár pre prípadné chyby + error: No teda! Skontroluj formulár pre prípadné chyby help: - native_redirect_uri: Použite %{native_redirect_uri} pre lokálne testy - redirect_uri: Iba jedna URI na riadok - scopes: Oprávnenia oddeľujte medzerami. Nechajte prázdne pre štandardné oprávnenia. + native_redirect_uri: Použi %{native_redirect_uri} pre lokálne testy + redirect_uri: Použi jeden riadok pre každú URI + scopes: Oprávnenia oddeľuj medzerami. Nechaj prázdne pre štandardné oprávnenia. index: application: Aplikácia callback_url: Návratová URL - delete: Zmazať + delete: Vymaž name: Názov new: Nová aplikácia scopes: Oprávnenia - show: Ukázať - title: Vaše aplikácie + show: Ukáž + title: Tvoje aplikácie new: title: Nová aplikácia show: - actions: Akcie + actions: Úkony application_id: Kľúč klienta callback_urls: Návratové URL adresy scopes: Oprávnenia @@ -54,7 +54,7 @@ sk: title: 'Aplikácia: %{name}' authorizations: buttons: - authorize: Overiť + authorize: Over deny: Zamietni error: title: Nastala chyba @@ -91,7 +91,7 @@ sk: resource_owner_authenticator_not_configured: Resource Owner zlyhal pretože Doorkeeper.configure.resource_owner_authenticator nebol nakonfigurovaný. server_error: Nastala neočakávaná chyba na autorizačnom serveri ktorá zabránila vykonať požiadavku. temporarily_unavailable: Autorizačný server ťa teraz nemôže obslúžiť, pretože prebieha údržba alebo je dočasne preťažený. - unauthorized_client: Klient nie je autorizovaný vykonať danú požiadavku takouto metódou. + unauthorized_client: Klient nie je autorizovaný vykonať danú požiadavku týmto spôsobom. unsupported_grant_type: Tento typ oprávnenia nie je podporovaný autorizačným serverom. unsupported_response_type: Autorizačný server nepodporuje typ tejto odpovede. flash: @@ -113,6 +113,11 @@ sk: application: title: Požadovaná OAuth autorizácia scopes: + admin:read: prezeraj všetky dáta na serveri + admin:read:accounts: prezeraj chúlostivé informácie na všetkých účtoch + admin:write: uprav všetky dáta na serveri + admin:write:accounts: urob moderovacie úkony na účtoch + admin:write:reports: urob moderovacie úkony voči hláseniam follow: uprav vzťahy svojho účtu push: dostávaj oboznámenia ohľadom tvojho účtu na obrazovku read: prezri si všetky dáta ohľadom svojho účetu diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index dd9337904..015d2c0ce 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -72,6 +72,7 @@ zh-CN: index: application: 应用 created_at: 授权时间 + date_format: "%H:%M:%S" scopes: 权限范围 title: 已授权的应用列表 errors: diff --git a/config/locales/el.yml b/config/locales/el.yml index a08ec7141..21b0da25c 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -469,13 +469,6 @@ el: no_status_selected: Καμία δημοσίευση δεν άλλαξε αφού καμία δεν ήταν επιλεγμένη title: Καταστάσεις λογαριασμού with_media: Με πολυμέσα - subscriptions: - callback_url: URL επιστροφής (Callback) - confirmed: Επιβεβαιωμένες - expires_in: Λήγει σε - last_delivery: Τελευταία παράδοση - title: Πρωτόκολλο WebSub - topic: Θέμα tags: accounts: Λογαριασμοί hidden: Κρυμμένες @@ -816,10 +809,6 @@ el: reply: proceed: Συνέχισε για να απαντήσεις prompt: 'Θέλεις να απαντήσεις σε αυτό το τουτ:' - remote_unfollow: - error: Σφάλμα - title: Τίτλος - unfollowed: Σταμάτησες να ακολουθείς scheduled_statuses: over_daily_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων τουτ για εκείνη τη μέρα over_total_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων τουτ diff --git a/config/locales/eo.yml b/config/locales/eo.yml index c71b42fdd..de28be010 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -174,6 +174,7 @@ eo: statuses: Mesaĝoj subscribe: Aboni suspended: Haltigita + time_in_queue: Atendado en atendovico %{time} title: Kontoj unconfirmed_email: Nekonfirmita retadreso undo_silenced: Malfari kaŝon @@ -334,6 +335,8 @@ eo: expired: Eksvalida title: Filtri title: Invitoj + pending_accounts: + title: Pritraktataj kontoj (%{count}) relays: add_new: Aldoni novan ripetilon delete: Forigi @@ -465,13 +468,6 @@ eo: no_status_selected: Neniu mesaĝo estis ŝanĝita ĉar neniu estis elektita title: Mesaĝoj de la konto with_media: Kun aŭdovidaĵoj - subscriptions: - callback_url: Revena URL - confirmed: Konfirmita - expires_in: Eksvalidiĝas je - last_delivery: Lasta livero - title: WebSub - topic: Temo tags: accounts: Kontoj hidden: Kaŝitaj @@ -780,7 +776,10 @@ eo: too_many_options: ne povas enhavi pli da %{max} proponoj preferences: other: Aliaj aferoj + posting_defaults: Afiŝadoj defaŭltoj + public_timelines: Publikaj templinioj relationships: + activity: Konto aktiveco dormant: Dormanta last_active: Lasta aktiva most_recent: Plej lasta @@ -788,6 +787,9 @@ eo: mutual: Reciproka primary: Primara relationship: Rilato + remove_selected_domains: Forigi ĉiuj sekvantojn el la selektitajn domajnojn + remove_selected_followers: Forigi selektitajn sekvantojn + remove_selected_follows: Malsekvi selektitajn uzantojn status: Statuso de la konto remote_follow: acct: Enmetu vian uzantnomo@domajno de kie vi volas agi @@ -806,10 +808,6 @@ eo: reply: proceed: Konfirmi la respondon prompt: 'Vi volas respondi al ĉi tiu mesaĝo:' - remote_unfollow: - error: Eraro - title: Titolo - unfollowed: Ne plu sekvita scheduled_statuses: over_daily_limit: Vi transpasis la limigon al %{limit} samtage planitaj mesaĝoj over_total_limit: Vi transpasis la limigon al %{limit} planitaj mesaĝoj diff --git a/config/locales/es.yml b/config/locales/es.yml index 49765cd0a..d6adf4062 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -5,9 +5,13 @@ es: about_mastodon_html: Mastodon es un servidor de red social libre y de código abierto. Una alternativa descentralizada a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la red social. about_this: Acerca de esta instancia active_count_after: activo + active_footnote: Usuarios Activos Mensuales (UAM) administered_by: 'Administrado por:' api: API apps: Aplicaciones móviles + apps_platforms: Utiliza Mastodon desde iOS, Android y otras plataformas + browse_directory: Navega por el directorio de perfiles y filtra por intereses + browse_public_posts: Navega por un transmisión en vivo de publicaciones públicas en Mastodon contact: Contacto contact_missing: No especificado contact_unavailable: N/A @@ -111,6 +115,7 @@ es: inbox_url: URL de la bandeja de entrada invited_by: Invitado por ip: IP + joined: Unido location: all: Todos local: Local @@ -245,6 +250,7 @@ es: feature_profile_directory: Directorio de perfil feature_registrations: Registros feature_relay: Relés de federación + feature_timeline_preview: Vista previa de la línea de tiempo features: Características hidden_service: Federación con servicios ocultos open_reports: informes abiertos @@ -279,6 +285,7 @@ es: reject_reports: Rechazar informes reject_reports_hint: Ignore todos los reportes de este dominio. Irrelevante para suspensiones rejecting_media: rechazar archivos multimedia + rejecting_reports: rechazando informes severity: silence: silenciado suspend: susependido @@ -304,8 +311,13 @@ es: title: Lista negra de correo followers: back_to_account: Volver a la cuenta + title: Seguidores de %{acct} instances: by_domain: Dominio + delivery_available: Entrega disponible + known_accounts: + one: "%{count} cuenta conocida" + other: "%{count} cuentas conocidas" moderation: all: Todos limited: Limitado @@ -314,6 +326,7 @@ es: total_blocked_by_us: Bloqueado por nosotros total_followed_by_them: Seguidos por ellos total_followed_by_us: Seguido por nosotros + total_reported: Informes sobre ellas total_storage: Archivos multimedia invites: deactivate_all: Desactivar todos @@ -323,6 +336,8 @@ es: expired: Expiradas title: Filtrar title: Invitaciones + pending_accounts: + title: Cuentas pendientes (%{count}) relays: add_new: Añadir un nuevo relés delete: Borrar @@ -396,6 +411,9 @@ es: preview_sensitive_media: desc_html: Los enlaces de vistas previas en otras web mostrarán una miniatura incluso si el medio está marcado como contenido sensible title: Mostrar contenido sensible en previews de OpenGraph + profile_directory: + desc_html: Permitir que los usuarios puedan ser descubiertos + title: Habilitar directorio de perfiles registrations: closed_message: desc_html: Se muestra en la portada cuando los registros están cerrados. Puedes usar tags HTML @@ -408,8 +426,10 @@ es: title: Permitir invitaciones de registrations_mode: modes: + approved: Se requiere aprobación para registrarse none: Nadie puede registrarse open: Cualquiera puede registrarse + title: Modo de registros show_known_fediverse_at_about_page: desc_html: Cuando esté activado, se mostrarán toots de todo el fediverso conocido en la vista previa. En otro caso, se mostrarán solamente toots locales. title: Mostrar fediverso conocido en la vista previa de la historia @@ -449,26 +469,38 @@ es: no_status_selected: No se cambió ningún estado al no seleccionar ninguno title: Estado de las cuentas with_media: Con multimedia - subscriptions: - callback_url: URL del callback - confirmed: Confirmado - expires_in: Expira en - last_delivery: Última entrega - topic: Tópico + tags: + accounts: Cuentas + hidden: Oculto + hide: Ocultar del directorio + name: Etiqueta + title: Etiquetas + unhide: Mostrar en el directorio + visible: Visible title: Administración warning_presets: add_new: Añadir nuevo delete: Borrar edit: Editar + edit_preset: Editar aviso predeterminado + title: Editar configuración predeterminada de avisos admin_mailer: new_pending_account: body: Los detalles de la nueva cuenta están abajos. Puedes aprobar o rechazar esta aplicación. + subject: Nueva cuenta para revisión en %{instance} (%{username}) new_report: body: "%{reporter} ha reportado a %{target}" body_remote: Alguien de %{domain} a reportado a %{target} subject: Nuevo reporte para la %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfaz web avanzada + advanced_web_interface_hint: 'Si desea utilizar todo el ancho de pantalla, la interfaz web avanzada le permite configurar varias columnas diferentes para ver tanta información al mismo tiempo como quiera: Inicio, notificaciones, línea de tiempo federada, cualquier número de listas y etiquetas.' + animations_and_accessibility: Animaciones y accesibilidad + confirmation_dialogs: Diálogos de confirmación + sensitive_content: Contenido sensible application_mailer: notification_preferences: Cambiar preferencias de correo electrónico + salutation: "%{name}," settings: 'Cambiar preferencias de correo: %{link}' view: 'Vista:' view_profile: Ver perfil @@ -482,6 +514,7 @@ es: warning: Ten mucho cuidado con estos datos. ¡No los compartas con nadie! your_token: Tu token de acceso auth: + apply_for_account: Solicitar una invitación change_password: Contraseña checkbox_agreement_html: Acepto las reglas del servidor y términos de servicio confirm_email: Confirmar email @@ -495,12 +528,16 @@ es: migrate_account: Mudarse a otra cuenta migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes configurarlo aquí. or_log_in_with: O inicia sesión con + providers: + cas: CAS + saml: SAML register: Registrarse registration_closed: "%{instance} no está aceptando nuevos miembros" resend_confirmation: Volver a enviar el correo de confirmación reset_password: Restablecer contraseña security: Cambiar contraseña set_new_password: Establecer nueva contraseña + trouble_logging_in: "¿Problemas para iniciar sesión?" authorize_follow: already_following: Ya estás siguiendo a esta cuenta error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota @@ -514,10 +551,18 @@ es: title: Seguir a %{acct} datetime: distance_in_words: + about_x_hours: "%{count}h" about_x_months: "%{count}m" + about_x_years: "%{count}a" + almost_x_years: "%{count}a" half_a_minute: Justo ahora + less_than_x_minutes: "%{count}m" less_than_x_seconds: Justo ahora + over_x_years: "%{count}a" + x_days: "%{count}d" + x_minutes: "%{count}m" x_months: "%{count}m" + x_seconds: "%{count}s" deletes: bad_password_msg: "¡Buen intento, hackers! Contraseña incorrecta" confirm_password: Ingresa tu contraseña actual para demostrar tu identidad @@ -527,6 +572,10 @@ es: warning_html: Se garantiza únicamente la eliminación del contenido de esta instancia. El contenido que se haya compartido extensamente dejará sus huellas. Los servidores fuera de línea y los que se hayan desuscrito de tus actualizaciones ya no actualizarán sus bases de datos. warning_title: Disponibilidad diseminada del contenido directories: + directory: Directorio de perfiles + enabled: Actualmente está listado en el directorio. + enabled_but_waiting: Ha optado por ser listado en el directorio, pero aún no cumple con el número mínimo de seguidores (%{min_followers}) para ser listado. + explanation: Descubre usuarios según sus intereses explore_mastodon: Explorar %{title} how_to_enable: Usted no está registrado por el directorio. Puede registrar por abajo. ¡Utilice hashtags en su bio para aparecer bajo hashtags específicos! people: @@ -557,6 +606,7 @@ es: size: Tamaño blocks: Personas que has bloqueado csv: CSV + domain_blocks: Bloqueos de dominios follows: Personas que sigues lists: Listas mutes: Tienes en silencio @@ -594,6 +644,8 @@ es: validation_errors: one: "¡Algo no está bien! Por favor, revisa el error" other: "¡Algo no está bien! Por favor, revise %{count} errores más abajo" + html_validator: + invalid_markup: 'contiene código HTML no válido: %{error}' identity_proofs: active: Activo authorize: Sí, autorizar @@ -603,18 +655,26 @@ es: keybase: invalid_token: Los tokens de Keybase son hashes de firmas y deben tener 66 caracteres hex verification_failed: Keybase no reconoce este token como una firma del usuario de Keybase %{kb_username}. Por favor, inténtelo de nuevo desde Keybase. + wrong_user: No se puede crear una prueba para %{proving} mientras se inicia sesión como %{current}. Inicia sesión como %{proving} e inténtalo de nuevo. + explanation_html: Aquí puedes conectar criptográficamente sus otras identidades, como un perfil de Keybase. Esto permite a otras personas enviarle mensajes encriptados y confiar en el contenido que les envías. + i_am_html: Soy %{username} en %{service}. identity: Identidad inactive: Inactivo + publicize_checkbox: 'Y tootee esto:' + publicize_toot: "¡Comprobado! Soy %{username} en %{service}: %{url}" status: Estado de la verificación view_proof: Ver prueba imports: modes: merge: Unir + merge_long: Mantener registros existentes y añadir nuevos overwrite: Sobrescribir + overwrite_long: Reemplazar registros actuales con los nuevos preface: Puedes importar ciertos datos, como todas las personas que estás siguiendo o bloqueando en tu cuenta en esta instancia, desde archivos exportados de otra instancia. success: Sus datos se han cargado correctamente y serán procesados en brevedad types: blocking: Lista de bloqueados + domain_blocking: Lista de dominios bloqueados following: Lista de seguidos muting: Lista de silenciados upload: Cargar @@ -689,26 +749,70 @@ es: body: "%{name} ha retooteado tu estado:" subject: "%{name} ha retooteado tu estado" title: Nueva difusión + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: m + trillion: T pagination: newer: Más nuevo next: Próximo older: Más antiguo prev: Anterior + truncate: "…" + polls: + errors: + already_voted: Ya has votado en esta encuesta + duplicate_options: contiene elementos duplicados + duration_too_long: está demasiado lejos en el futuro + duration_too_short: es demasiado pronto + expired: La encuesta ya ha terminado + over_character_limit: no puede exceder %{max} caracteres cada uno + too_few_options: debe tener más de un elemento + too_many_options: no puede contener más de %{max} elementos preferences: other: Otros + posting_defaults: Configuración por defecto de publicaciones + public_timelines: Líneas de tiempo públicas relationships: + activity: Actividad de la cuenta + dormant: Inactivo last_active: Última actividad most_recent: Más reciente + moved: Movido + mutual: Mutuo + primary: Principal + relationship: Relación + remove_selected_domains: Eliminar todos los seguidores de los dominios seleccionados + remove_selected_followers: Eliminar los seguidores seleccionados + remove_selected_follows: Dejar de seguir a los usuarios seleccionados + status: Estado de la cuenta remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta no_account_html: "¿No tienes una cuenta? Puedes registrarte aqui" proceed: Proceder a seguir prompt: 'Vas a seguir a:' - remote_unfollow: - error: Error - title: Título - unfollowed: Ha dejado de seguirse + reason_html: "¿¿Por qué es necesario este paso? %{instance} puede que no sea el servidor donde estás registrado, así que necesitamos redirigirte primero a tu servidor de origen." + remote_interaction: + favourite: + proceed: Proceder a marcar como favorito + prompt: 'Quieres marcar como favorito este toot:' + reblog: + proceed: Proceder a retootear + prompt: 'Quieres retootear este toot:' + reply: + proceed: Proceder a responder + prompt: 'Quieres responder a este toot:' + scheduled_statuses: + over_daily_limit: Ha superado el límite de %{limit} toots programados para ese día + over_total_limit: Ha superado el límite de %{limit} toots programados + too_soon: La fecha programada debe estar en el futuro sessions: activity: Última actividad browser: Navegador @@ -761,10 +865,14 @@ es: edit_profile: Editar perfil export: Exportar información featured_tags: Hashtags destacados + identity_proofs: Pruebas de identidad import: Importar + import_and_export: Importar y exportar migrate: Migración de cuenta notifications: Notificaciones preferences: Preferencias + profile: Perfil + relationships: Siguiendo y seguidores two_factor_authentication: Autenticación de dos factores statuses: attached: @@ -788,8 +896,14 @@ es: ownership: El toot de alguien más no puede fijarse private: Los toots no-públicos no pueden fijarse reblog: Un boost no puede fijarse + poll: + total_votes: + one: "%{count} voto" + other: "%{count} votos" + vote: Vota show_more: Mostrar más sign_in_to_participate: Regístrate para participar en la conversación + title: '%{name}: "%{quote}"' visibilities: private: Sólo mostrar a seguidores private_long: Solo mostrar a tus seguidores @@ -802,6 +916,87 @@ es: reblogged: retooteado sensitive_content: Contenido sensible terms: + body_html: | +

Política de Privacidad

+

¿Qué información recogemos?

+ +
    +
  • Información básica sobre su cuenta: Si se registra en este servidor, se le requerirá un nombre de usuario, una dirección de correo electrónico y una contraseña. Además puede incluir información adicional en el perfil como un nombre de perfil y una biografía, y subir una foto de perfil y una imagen de cabecera. El nombre de usuario, nombre de perfil, biografía, foto de perfil e imagen de cabecera siempre son visibles públicamente
  • +
  • Publicaciones, seguimiento y otra información pública: La lista de gente a la que sigue es mostrada públicamente, al igual que sus seguidores. Cuando publica un mensaje, la fecha y hora es almacenada, así como la aplicación desde la cual publicó el mensaje. Los mensajes pueden contener archivos adjuntos multimedia, como imágenes y vídeos. Las publicaciones públicas y no listadas están disponibles públicamente. Cuando destaca una entrada en su perfil, también es información disponible públicamente. Sus publicaciones son entregadas a sus seguidores, en algunos casos significa que son entregadas a diferentes servidores y las copias son almacenadas allí. Cuando elimina publicaciones, esto también se transfiere a sus seguidores. La acción de rebloguear o marcar como favorito otra publicación es siempre pública.
  • +
  • Publicaciones directas y sólo para seguidores: Todos los mensajes se almacenan y procesan en el servidor. Los mensajes sólo para seguidores se entregan a los seguidores y usuarios que se mencionan en ellos, y los mensajes directos se entregan sólo a los usuarios que se mencionan en ellos. En algunos casos significa que se entregan a diferentes servidores y que las copias se almacenan allí. Hacemos un esfuerzo de buena fe para limitar el acceso a esas publicaciones sólo a las personas autorizadas, pero otros servidores pueden no hacerlo. Por lo tanto, es importante revisar los servidores a los que pertenecen sus seguidores. Puede cambiar una opción para aprobar y rechazar nuevos seguidores manualmente en la configuración Por favor, tenga en cuenta que los operadores del servidor y de cualquier servidor receptor pueden ver dichos mensajes, y que los destinatarios pueden capturarlos, copiarlos o volver a compartirlos de alguna otra manera. No comparta ninguna información peligrosa en Mastodon.
  • +
  • Direcciones IP y otros metadatos: Al iniciar sesión, registramos la dirección IP desde la que se ha iniciado sesión, así como el nombre de la aplicación de su navegador. Todas las sesiones iniciadas están disponibles para su revisión y revocación en los ajustes. La última dirección IP utilizada se almacena hasta 12 meses. También podemos conservar los registros del servidor que incluyen la dirección IP de cada solicitud a nuestro servidor.
  • +
+ +
+ +

¿Para qué utilizamos su información?

+ +

Toda la información que obtenemos de usted puede ser utilizada de las siguientes maneras:

+ +
    +
  • Para proporcionar la funcionalidad principal de Mastodon. Sólo puedes interactuar con el contenido de otras personas y publicar tu propio contenido cuando estés conectado. Por ejemplo, puedes seguir a otras personas para ver sus mensajes combinados en tu propia línea de tiempo personalizada.
  • +
  • Para ayudar a la moderación de la comunidad, por ejemplo, comparando su dirección IP con otras conocidas para determinar la evasión de prohibiciones u otras violaciones.
  • +
  • La dirección de correo electrónico que nos proporcione podrá utilizarse para enviarle información, notificaciones sobre otras personas que interactúen con su contenido o para enviarle mensajes, así como para responder a consultas y/u otras solicitudes o preguntas.
  • +
+ +
+ +

¿Cómo protegemos su información?

+ +

Implementamos una variedad de medidas de seguridad para mantener la seguridad de su información personal cuando usted ingresa, envía o accede a su información personal. Entre otras cosas, la sesión de su navegador, así como el tráfico entre sus aplicaciones y la API, están protegidos con SSL, y su contraseña está protegida mediante un algoritmo unidireccional fuerte. Puede habilitar la autenticación de dos factores para un acceso más seguro a su cuenta.

+ +
+ +

¿Cuál es nuestra política de retención de datos?

+ +

Haremos un esfuerzo de buena fe para:

+ +
    +
  • Conservar los registros del servidor que contengan la dirección IP de todas las peticiones a este servidor, en la medida en que se mantengan dichos registros, no más de 90 días.
  • +
  • Conservar las direcciones IP asociadas a los usuarios registrados no más de 12 meses.
  • +
+ +

Puede solicitar y descargar un archivo de su contenido, incluidos sus mensajes, archivos adjuntos multimedia, foto de perfil e imagen de cabecera.

+ +

Usted puede borrar su cuenta de forma irreversible en cualquier momento.

+ +
+ +

¿Utilizamos cookies?

+ +

Sí. Las cookies son pequeños archivos que un sitio o su proveedor de servicios transfiere al disco duro de su ordenador a través de su navegador web (si usted lo permite). Estas cookies permiten al sitio reconocer su navegador y, si tiene una cuenta registrada, asociarla con su cuenta registrada.

+ +

Utilizamos cookies para entender y guardar sus preferencias para futuras visitas.

+ +
+ +

¿Revelamos alguna información a terceros?

+ +

No vendemos, comerciamos ni transferimos a terceros su información personal identificable. Esto no incluye a los terceros de confianza que nos asisten en la operación de nuestro sitio, en la realización de nuestros negocios o en la prestación de servicios, siempre y cuando dichas partes acuerden mantener la confidencialidad de esta información. También podemos divulgar su información cuando creamos que es apropiado para cumplir con la ley, hacer cumplir las políticas de nuestro sitio, o proteger nuestros u otros derechos, propiedad o seguridad.

+ +

Su contenido público puede ser descargado por otros servidores de la red. Tus mensajes públicos y sólo para seguidores se envían a los servidores donde residen tus seguidores, y los mensajes directos se envían a los servidores de los destinatarios, en la medida en que dichos seguidores o destinatarios residan en un servidor diferente.

+ +

Cuando usted autoriza a una aplicación a usar su cuenta, dependiendo del alcance de los permisos que usted apruebe, puede acceder a la información de su perfil público, su lista de seguimiento, sus seguidores, sus listas, todos sus mensajes y sus favoritos. Las aplicaciones nunca podrán acceder a su dirección de correo electrónico o contraseña.

+ +
+ +

Uso del sitio por parte de los niños

+ +

Si este servidor está en la UE o en el EEE: Nuestro sitio, productos y servicios están dirigidos a personas mayores de 16 años. Si es menor de 16 años, según los requisitos de la GDPR (General Data Protection Regulation) no utilice este sitio.

+ +

Si este servidor está en los EE.UU.: Nuestro sitio, productos y servicios están todos dirigidos a personas que tienen al menos 13 años de edad. Si usted es menor de 13 años, según los requisitos de COPPA (Children's Online Privacy Protection Act) no utilice este sitio.

+ +

Los requisitos legales pueden ser diferentes si este servidor está en otra jurisdicción.

+ +
+ +

Cambios en nuestra Política de Privacidad

+ +

Si decidimos cambiar nuestra política de privacidad, publicaremos esos cambios en esta página.

+ +

Este documento es CC-BY-SA. Fue actualizado por última vez el 7 de marzo de 2018.

+ +

Adaptado originalmente desde la política de privacidad de Discourse.

title: Términos del Servicio y Políticas de Privacidad de %{instance} themes: contrast: Alto contraste @@ -810,6 +1005,7 @@ es: time: formats: default: "%d de %b del %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: Ingresa el código generado por tu aplicación de autenticación para confirmar description_html: Si habilitas la autenticación de dos factores, se requerirá estar en posesión de su teléfono, lo que generará tokens para que usted pueda iniciar sesión. @@ -831,6 +1027,22 @@ es: explanation: Has solicitado una copia completa de tu cuenta de Mastodon. ¡Ya está preparada para descargar! subject: Tu archivo está preparado para descargar title: Descargar archivo + warning: + explanation: + disable: Mientras su cuenta esté congelada, la información de su cuenta permanecerá intacta, pero no puede realizar ninguna acción hasta que se desbloquee. + silence: Mientras su cuenta está limitada, sólo las personas que ya le están siguiendo verán sus toots en este servidor, y puede que se le excluya de varios listados públicos. Sin embargo, otros pueden seguirle manualmente. + suspend: Su cuenta ha sido suspendida, y todos tus toots y tus archivos multimedia subidos han sido irreversiblemente eliminados de este servidor, y de los servidores donde tenías seguidores. + review_server_policies: Revisar las políticas del servidor + subject: + disable: Su cuenta %{acct} ha sido congelada + none: Advertencia para %{acct} + silence: Su cuenta %{acct} ha sido limitada + suspend: Su cuenta %{acct} ha sido suspendida + title: + disable: Cuenta congelada + none: Advertencia + silence: Cuenta limitada + suspend: Cuenta suspendida welcome: edit_profile_action: Configurar el perfil edit_profile_step: Puedes personalizar tu perfil subiendo un avatar, una cabecera, cambiando tu nombre de usuario y más cosas. Si quieres revisar a tus nuevos seguidores antes de que se les permita seguirte, puedes bloquear tu cuenta. @@ -846,6 +1058,7 @@ es: tip_following: Sigues a tus administradores de servidor por defecto. Para encontrar más gente interesante, revisa las lineas de tiempo local y federada. tip_local_timeline: La linea de tiempo local is una vista de la gente en %{instance}. Estos son tus vecinos inmediatos! tip_mobile_webapp: Si el navegador de tu dispositivo móvil ofrece agregar Mastodon a tu página de inicio, puedes recibir notificaciones. Actúa como una aplicación nativa en muchas formas! + tips: Consejos title: Te damos la bienvenida a bordo, %{name}! users: follow_limit_reached: No puedes seguir a más de %{limit} personas diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 9b9c2c027..d3299d775 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -61,8 +61,8 @@ eu: posts: one: Toot other: Toot - posts_tab_heading: Toot - posts_with_replies: Toot eta erantzunak + posts_tab_heading: Toot-ak + posts_with_replies: Toot-ak eta erantzunak reserved_username: Erabiltzaile-izena erreserbatuta dago roles: admin: Administratzailea @@ -431,7 +431,7 @@ eu: open: Edonork eman dezake izena title: Erregistratzeko modua show_known_fediverse_at_about_page: - desc_html: Txandakatzean, fedibertsu ezagun osoko toot-ak bistaratuko ditu aurrebistan. Bestela, toot lokalak besterik ez ditu erakutsiko. + desc_html: Txandakatzean, fedibertso ezagun osoko toot-ak bistaratuko ditu aurrebistan. Bestela, toot lokalak besterik ez ditu erakutsiko. title: Erakutsi fedibertsu ezagun osoko denbora-lerroa aurrebistan show_staff_badge: desc_html: Erakutsi langile banda erabiltzailearen orrian @@ -469,13 +469,6 @@ eu: no_status_selected: Ez da mezurik aldatu ez delako mezurik aukeratu title: Kontuaren mezuak with_media: Multimediarekin - subscriptions: - callback_url: Itzulera URL-a - confirmed: Berretsita - expires_in: Iraungitzea - last_delivery: Azken bidalketa - title: WebSub - topic: Mintzagaia tags: accounts: Kontuak hidden: Ezkutatuta @@ -783,7 +776,7 @@ eu: too_few_options: elementu bat baino gehiago izan behar du too_many_options: ezin ditu %{max} elementu baino gehiago izan preferences: - other: Beste bat + other: Denetarik posting_defaults: Bidalketarako lehenetsitakoak public_timelines: Denbora-lerro publikoak relationships: @@ -816,10 +809,6 @@ eu: reply: proceed: Ekin erantzuteari prompt: 'Toot honi erantzun nahi diozu:' - remote_unfollow: - error: Errorea - title: Izenburua - unfollowed: Jarraitzeari utzita scheduled_statuses: over_daily_limit: Egun horretarako programatutako toot kopuruaren muga gainditu duzu (%{limit}) over_total_limit: Programatutako toot kopuruaren muga gainditu duzu (%{limit}) diff --git a/config/locales/fa.yml b/config/locales/fa.yml index d37dbdeb4..f6b6c8758 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -469,13 +469,6 @@ fa: no_status_selected: هیچ بوقی تغییری نکرد زیرا هیچ‌کدام از آن‌ها انتخاب نشده بودند title: نوشته‌های حساب with_media: دارای عکس یا ویدیو - subscriptions: - callback_url: نشانی Callback - confirmed: تأییدشده - expires_in: مهلت انقضا - last_delivery: آخرین ارسال - title: WebSub - topic: موضوع tags: accounts: حساب‌ها hidden: پنهان‌شده @@ -816,10 +809,6 @@ fa: reply: proceed: به سمت پاسخ‌دادن prompt: 'شما می‌خواهید به این بوق پاسخ دهید:' - remote_unfollow: - error: خطا - title: عنوان - unfollowed: پایان پیگیری scheduled_statuses: over_daily_limit: شما از حد مجاز %{limit} بوق زمان‌بندی‌شده در آن روز فراتر رفته‌اید over_total_limit: شما از حد مجاز %{limit} بوق زمان‌بندی‌شده فراتر رفته‌اید diff --git a/config/locales/fi.yml b/config/locales/fi.yml index e0dc0f756..07a8e367b 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -330,12 +330,6 @@ fi: no_media: Ei mediaa title: Tilin tilat with_media: Sisältää mediaa - subscriptions: - callback_url: Paluu-URL - confirmed: Vahvistettu - expires_in: Vanhenee - last_delivery: Viimeisin toimitus - topic: Aihe title: Ylläpito admin_mailer: new_report: @@ -536,8 +530,6 @@ fi: missing_resource: Vaadittavaa uudelleenohjaus-URL:ää tiliisi ei löytynyt proceed: Siirry seuraamaan prompt: 'Olet aikeissa seurata:' - remote_unfollow: - error: Virhe sessions: activity: Viimeisin toiminta browser: Selain diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5c15ab6a4..b3ee1d3bd 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -469,13 +469,6 @@ fr: no_status_selected: Aucun statut n’a été modifié car aucun n’a été sélectionné title: État du compte with_media: avec médias - subscriptions: - callback_url: URL de rappel - confirmed: Confirmé - expires_in: Expire dans - last_delivery: Dernière livraison - title: WebSub - topic: Sujet tags: accounts: Comptes hidden: Masqué @@ -816,10 +809,6 @@ fr: reply: proceed: Confirmer la réponse prompt: 'Vous souhaitez répondre à ce pouet :' - remote_unfollow: - error: Erreur - title: Titre - unfollowed: Non-suivi scheduled_statuses: over_daily_limit: Vous avez dépassé la limite de %{limit} pouets planifiés pour ce jour over_total_limit: Vous avez dépassé la limite de %{limit} pouets planifiés diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 79ef993e2..7ecb50e40 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -469,13 +469,6 @@ gl: no_status_selected: Non se cambiou ningún estado xa que ningún foi seleccionado title: Estados da conta with_media: con medios - subscriptions: - callback_url: URL de chamada - confirmed: Confirmado - expires_in: Caduca en - last_delivery: Última entrega - title: WebSub - topic: Asunto tags: accounts: Contas hidden: Ocultas @@ -816,10 +809,6 @@ gl: reply: proceed: Respostar prompt: 'Vostede quere respostar a este toot:' - remote_unfollow: - error: Fallo - title: Título - unfollowed: Deixou de seguir scheduled_statuses: over_daily_limit: Excedeu o límite de %{limit} toots programados para ese día over_total_limit: Excedeu o límite de %{limit} toots programados diff --git a/config/locales/he.yml b/config/locales/he.yml index 5e50f738d..12953c223 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -177,13 +177,6 @@ he: title: תיאור אתר מורחב site_title: כותרת האתר title: הגדרות אתר - subscriptions: - callback_url: קישורית Callback - confirmed: מאושר - expires_in: פג תוקף ב- - last_delivery: משלוח אחרון - title: מנוי WebSub - topic: נושא title: ניהול application_mailer: settings: 'שינוי הגדרות דוא"ל: %{link}' diff --git a/config/locales/hu.yml b/config/locales/hu.yml index d771b9683..948f0db13 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -10,7 +10,7 @@ hu: api: API apps: Mobil appok apps_platforms: Használd a Mastodont iOS-ről, Androidról vagy más platformról - browse_directory: Böngészd a profil adatbázist és szűrj érdeklődési kör szerint + browse_directory: Böngészd a profilokat és szűrj érdeklődési körre browse_public_posts: Nézz bele a Mastodon élő adatfolyamába contact: Kapcsolat contact_missing: Nincs megadva @@ -469,13 +469,6 @@ hu: no_status_selected: Nem változtattunk meg semmit, mert semmi sem volt kiválasztva title: Felhasználó tülkjei with_media: Médiafájlokkal - subscriptions: - callback_url: Callback URL - confirmed: Megerősítve - expires_in: Elévül - last_delivery: Utolsó kézbesítés - title: WebSub - topic: Téma tags: accounts: Fiókok hidden: Rejtett @@ -816,10 +809,6 @@ hu: reply: proceed: Válaszadás prompt: 'Erre a tülkre szeretnél válaszolni:' - remote_unfollow: - error: Hiba - title: Cím - unfollowed: Nem követett scheduled_statuses: over_daily_limit: Túllépted az időzített tülkökre vonatkozó napi limitet (%{limit}) over_total_limit: Túllépted az időzített tülkökre vonatkozó limitet (%{limit}) @@ -924,7 +913,7 @@ hu: unlisted_long: Mindenki látja, de a nyilvános idővonalakon nem jelenik meg stream_entries: pinned: Kitűzött tülk - reblogged: megtolt + reblogged: megtolta sensitive_content: Szenzitív tartalom terms: body_html: | @@ -932,17 +921,17 @@ hu:

Milyen adatokat gyűjtünk?

    -
  • Alapvető fiókadatok: Ha regisztrálsz ezen a szerveren, kérhetünk tőled felhasználói nevet, e-mail címet és jelszót is. Megadhatsz magadról egyéb profil információt, mint megjelenítendő név, bemutatkozás, feltölthetsz profilképet, háttérképet. A felhasználói neved, megjelenítendő neved, bemutatkozásod, profil képed és háttér képed mindig nyilvánosak mindenki számára.
  • -
  • Tülkök (posztok), követések, más nyilvános adatok: Az általad követett emberek listája nyilvános. Ugyanez igaz a te követőidre is. Ha küldesz egy üzenetet, ennek az idejét eltároljuk azzal az alkalmazással együtt, melyből az üzenetet küldted. Az üzenetek tartalmazhatnak média csatolmányt, képeket, videókat. A nyilvános tülkök (posztok) bárki számára elérhetőek. Ha egy tülköt kiemelsz a profilodon, az is nyilvánossá válik. Amikor a tülkjeidet a követőidnek továbbítjuk, a poszt más szerverekre is kerülhet, melyeken így másolatok képződhetnek. Ha törölsz tülköket, ez is továbbítódik a követőid felé. A megtolás (reblog) és kedvencnek jelölés művelete is mindig nyilvános.
  • -
  • Közvetlen üzenetek és csak követőknek szánt tülkök: Minden tülk a szerveren tárolódik. A csak követőknek szánt tülköket a követőidnek és az ezekben megemlítetteknek továbbítjuk, míg a közvetlen üzeneteket kizárólag az ebben megemlítettek kapják. Néhány esetben ez azt jelenti, hogy ezek más szerverekre is továbbítódnak, így ott másolatok keletkezhetnek. Jóhiszeműen feltételezzük, hogy más szerverek is hasonlóan járnak el, mikor ezeket az üzeneteket csak az arra jogosultaknak mutatják meg. Ugyanakkor ez nem feltétlenül igaz. Ezért érdemes megnézni azokat a szervereket, melyeken követőid vannak. Be tudod állítani, hogy minden követési kérelmet jóvá kelljen hagynod. Tartsd észben, hogy a szerver üzemeltetői láthatják az üzeneteket, illetve a fogadók képernyőképet, másolatot készíthetnek belőlük, vagy újraoszthatják őket.Ne ossz meg veszélyes információt a Mastodon hálózaton!
  • -
  • IP címek és egyéb metaadatok: Bejelentkezéskor letároljuk a használt böngésződet és IP címedet. Mindent rögzített munkamenet elérhető és visszavonható a beállítások között. A legutolsó IP címet maximum 12 hónapig tárolunk. Egyéb szerver logokat is megtarthatunk, melyek HTTP kérésenként is tárolhatják az IP címedet.
  • +
  • Alapvető fiókadatok: Ha regisztrálsz ezen a szerveren, kérhetünk tőled felhasználói nevet, e-mail címet és jelszót is. Megadhatsz magadról egyéb profil információt, megjelenítendő nevet, bemutatkozást, feltölthetsz profilképet, háttérképet. A felhasználói neved, megjelenítendő neved, bemutatkozásod, profil képed és háttér képed mindig nyilvánosak mindenki számára.
  • +
  • Tülkök (posztok), követések, más nyilvános adatok: Az általad követett emberek listája nyilvános. Ugyanez igaz a te követőidre is. Ha küldesz egy üzenetet, ennek az idejét eltároljuk azzal az alkalmazással együtt, melyből az üzenetet küldted. Az üzenetek tartalmazhatnak média csatolmányt, képeket, videókat. A nyilvános tülkök (posztok) bárki számára elérhetőek. Ha egy tülköt kiemelsz a profilodon, az is nyilvánossá válik. Amikor a tülkjeidet a követőidnek továbbítjuk, a poszt más szerverekre is átkerülhet, melyeken így másolatok képződhetnek. Ha törölsz tülköket, ez is továbbítódik a követőid felé. A megtolás (reblog) és kedvencnek jelölés művelete is mindig nyilvános.
  • +
  • Közvetlen üzenetek és csak követőknek szánt tülkök: Minden tülk a szerveren tárolódik. A csak követőknek szánt tülköket a követőidnek és az ezekben megemlítetteknek továbbítjuk, míg a közvetlen üzeneteket kizárólag az ebben megemlítettek kapják. Néhány esetben ez azt jelenti, hogy ezek más szerverekre is továbbítódnak, így ott másolatok keletkezhetnek. Jóhiszeműen feltételezzük, hogy más szerverek is hasonlóan járnak el, mikor ezeket az üzeneteket csak az arra jogosultaknak mutatják meg. Ugyanakkor ez nem feltétlenül igaz. Érdemes ezért megvizsgálni azokat a szervereket, melyeken követőid vannak. Be tudod állítani, hogy minden követési kérelmet jóvá kelljen hagynod. Tartsd észben, hogy a szerver üzemeltetői láthatják az üzeneteket, illetve a fogadók képernyőképet, másolatot készíthetnek belőlük, vagy újraoszthatják őket. Ne ossz meg veszélyes információt a Mastodon hálózaton!
  • +
  • IP címek és egyéb metaadatok: Bejelentkezéskor letároljuk a használt böngésződet és IP címedet. Minden rögzített munkamenet elérhető és visszavonható a beállítások között. Az utoljára rögzített IP címet maximum 12 hónapig tároljuk. Egyéb szerver logokat is megtarthatunk, melyek HTTP kérésenként is tárolhatják az IP címedet.

Mire használjuk az adataidat?

-

Bármely tőled begyűjtött adatot a következő célokra használhatjuk:

+

Bármely tőled begyűjtött adatot a következő célokra használhatjuk fel:

  • Mastodon alapfunkcióinak biztosítása: Csak akkor léphetsz kapcsolatba másokkal, ha be vagy jelentkezve. Pl. követhetsz másokat a saját, személyre szabott idővonaladon.
  • @@ -960,14 +949,14 @@ hu:

    Mik az adatmegőrzési szabályaink?

    -

    Jóhiszeműen járunk el, hogy:

    +

    Mindent megteszünk, hogy:

      -
    • A szerver logokat, melyek kérésenként tartalmazzák a felhasználó IP címét maximum 90 napig tartjuk meg.
    • -
    • A regisztrált felhasználók IP címeikkel összekötő adatokat maximum 12 hónapig tartjuk meg.
    • +
    • A szerver logokat, melyek kérésenként tartalmazzák a felhasználó IP címét maximum 90 napig tartsuk meg.
    • +
    • A regisztrált felhasználókat IP címeikkel összekötő adatokat maximum 12 hónapig tartsuk meg.
    -

    Kérhetsz archívot minden tárolt adatodról, tülkjeidről, média fájljaidról, profil- és háttér képedről.

    +

    Kérhetsz mentést minden tárolt adatodról, tülködről, média fájlodról, profil- és háttér képedről.

    Bármikor visszaállíthatatlanul le is törölheted a fiókodat.

    @@ -983,7 +972,7 @@ hu:

    Átadunk bármilyen adatot harmadik személynek?

    -

    Az azonosításodra alkalmazható adatokat nem adjuk el, nem kereskedünk vele, nem adjuk át külső szereplőnek. Ez nem foglalja magába azon harmadik személyeket, aki az üzemeltetésben, felhasználók kiszolgálásban és a tevékenységünkben segítenek, de csak addig, amíg ők is elfogadják, hogy ezeket az adatokat bizalmasan kezelik. Akkor is átadhatjuk ezeket az adatokat, ha erre hitünk szerint törvény kötelez minket, ha betartatjuk az oldalunk szabályzatát vagy megvédjük a saját vagy mások személyiségi jogait, tulajdonát, biztonságát.

    +

    Az azonosításodra alkalmazható adatokat nem adjuk el, nem kereskedünk vele, nem adjuk át külső szereplőnek. Ez nem foglalja magában azon harmadik személyeket, aki az üzemeltetésben, felhasználók kiszolgálásban és a tevékenységünkben segítenek, de csak addig, amíg ők is elfogadják, hogy ezeket az adatokat bizalmasan kezelik. Akkor is átadhatjuk ezeket az adatokat, ha erre hitünk szerint törvény kötelez minket, ha betartatjuk az oldalunk szabályzatát vagy megvédjük a saját vagy mások személyiségi jogait, tulajdonát, biztonságát.

    A nyilvános tartalmaidat más hálózatban lévő szerverek letölthetik. A nyilvános és csak követőknek szánt tülkjeid olyan szerverekre is elküldődnek, melyeken követőid vannak. A közvetlen üzenetek is átkerülnek a címzettek szervereire, ha ők más szerveren regisztráltak.

    @@ -993,9 +982,9 @@ hu:

    Az oldal gyerekek általi használata

    -

    Ha ez a szerver az EU-ban vagy EEA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 16 éven felülieket céloznak. Ha 16 évnél fiatalabb vagy, a GDPR (General Data Protection Regulation) értelmében kérlek ne használd ezt az oldalt!

    +

    Ha ez a szerver az EU-ban vagy EEA-ban található: Az oldalunk, szolgáltatásaink és termékeink mind 16 éven felülieket céloznak. Ha 16 évnél fiatalabb vagy, a GDPR (General Data Protection Regulation) értelmében kérlek ne használd ezt az oldalt!

    -

    Ha ez a szerver az USA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 13 éven felülieket céloznak. Ha 13 évnél fiatalabb vagy, a COPPA (Children's Online Privacy Protection Act) értelmében kérlek ne használd ezt az oldalt!

    +

    Ha ez a szerver az USA-ban található: Az oldalunk, szolgáltatásaink és termékeink mind 13 éven felülieket céloznak. Ha 13 évnél fiatalabb vagy, a COPPA (Children's Online Privacy Protection Act) értelmében kérlek ne használd ezt az oldalt!

    A jogi előírások különbözhetnek ettől a világ egyéb tájain.

    diff --git a/config/locales/id.yml b/config/locales/id.yml index 43721b19b..16098b189 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -191,11 +191,6 @@ id: title: Deskripsi situs tambahan site_title: Judul Situs title: Pengaturan situs - subscriptions: - confirmed: Dikonfirmasi - expires_in: Kadaluarsa dalam - last_delivery: Terakhir dikirim - topic: Topik title: Administrasi application_mailer: settings: 'Ubah pilihan email: %{link}' diff --git a/config/locales/it.yml b/config/locales/it.yml index 6dfe212d1..1df321752 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -469,13 +469,6 @@ it: no_status_selected: Nessun status è stato modificato perché nessuno era stato selezionato title: Gli status dell'account with_media: con media - subscriptions: - callback_url: URL Callback - confirmed: Confermato - expires_in: Scade in - last_delivery: Ultima distribuzione - title: WebSub - topic: Argomento tags: accounts: Account hidden: Nascosto @@ -818,10 +811,6 @@ it: reply: proceed: Continua per rispondere prompt: 'Vuoi rispondere a questo toot:' - remote_unfollow: - error: Errore - title: Titolo - unfollowed: Non più seguito scheduled_statuses: over_daily_limit: Hai superato il limite di %{limit} toot programmati per questo giorno over_total_limit: Hai superato il limite di %{limit} toot programmati @@ -885,7 +874,7 @@ it: notifications: Notifiche preferences: Preferenze profile: Profilo - relationships: Follows and followers + relationships: Follows e followers two_factor_authentication: Autenticazione a due fattori statuses: attached: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index ca640d07d..1ff5a2b63 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -463,13 +463,6 @@ ja: no_status_selected: 何も選択されていないため、変更されていません title: トゥート一覧 with_media: メディアあり - subscriptions: - callback_url: コールバックURL - confirmed: 確認済み - expires_in: 期限 - last_delivery: 最終配送 - title: WebSub - topic: トピック tags: accounts: アカウント hidden: 非表示 @@ -805,10 +798,6 @@ ja: reply: proceed: 返信する prompt: '返信しようとしています:' - remote_unfollow: - error: エラー - title: タイトル - unfollowed: フォロー解除しました scheduled_statuses: over_daily_limit: その日予約できる投稿数 %{limit} を超えています over_total_limit: 予約できる投稿数 %{limit} を超えています diff --git a/config/locales/ka.yml b/config/locales/ka.yml index 53057d860..57dd0f5c0 100644 --- a/config/locales/ka.yml +++ b/config/locales/ka.yml @@ -368,13 +368,6 @@ ka: no_status_selected: სატუსები არ შეცვლილა, რადგან არცერთი არ მონიშნულა title: ანგარიშის სტატუსები with_media: მედიით - subscriptions: - callback_url: ქოლბექ ურლ - confirmed: დამოწმდა - expires_in: ვადა გასდის - last_delivery: ბოლო მიღება - title: ვებ-საბი - topic: სათაური title: ადმინისტრაცია admin_mailer: new_report: @@ -601,10 +594,6 @@ ka: no_account_html: არ გაქვთ ანგარიში? შეგიძლიათ დარეგისტრირდეთ აქ proceed: გააგრძელეთ გასაყოლად prompt: 'თქვენ გაჰყვებით:' - remote_unfollow: - error: შეცდომა - title: სათაური - unfollowed: დადევნების შეწყვეტა sessions: activity: ბოლო აქტივობა browser: ბრაუზერი diff --git a/config/locales/kk.yml b/config/locales/kk.yml index c6212c378..a3651b1d3 100644 --- a/config/locales/kk.yml +++ b/config/locales/kk.yml @@ -436,13 +436,6 @@ kk: no_status_selected: Бірде-бір статус өзгерген жоқ, себебі ештеңе таңдалмады title: Аккаунт статустары with_media: Медиамен - subscriptions: - callback_url: Callbаck URL - confirmed: Confirmеd - expires_in: Expirеs in - last_delivery: Last dеlivery - title: WеbSub - topic: Tоpic tags: accounts: Accоunts hidden: Hiddеn @@ -726,10 +719,6 @@ kk: reply: proceed: Жауап жазу prompt: 'Сіз мына жазбаға жауап жазасыз:' - remote_unfollow: - error: Қате - title: Тақырыбы - unfollowed: Жазылудан бас тартылды scheduled_statuses: over_daily_limit: Сіз бір күндік %{limit} жазба лимитін тауыстыңыз over_total_limit: Сіз %{limit} жазба лимитін тауыстыңыз diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 3f14d5df6..ec64972ed 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -465,13 +465,6 @@ ko: no_status_selected: 아무 것도 선택 되지 않아 아무 것도 바뀌지 않았습니다 title: 계정 툿 with_media: 미디어 있음 - subscriptions: - callback_url: 콜백 URL - confirmed: 확인됨 - expires_in: 기한 - last_delivery: 최종 발송 - title: WebSub - topic: 토픽 tags: accounts: 계정들 hidden: 숨겨짐 @@ -807,10 +800,6 @@ ko: reply: proceed: 답장 진행 prompt: '이 툿에 답장을 하려 합니다:' - remote_unfollow: - error: 에러 - title: 타이틀 - unfollowed: 언팔로우됨 scheduled_statuses: over_daily_limit: 그 날짜에 대한 %{limit}개의 예약 툿 제한을 초과합니다 over_total_limit: 예약 툿 제한 %{limit}을 초과합니다 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 2cf0b7c42..087cbb582 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -419,13 +419,6 @@ lt: no_status_selected: Jokie statusai nebuvo pakeisti, nes niekas nepasirinkta title: Paskyros statusai with_media: Su medija - subscriptions: - callback_url: Atgalinė URL - confirmed: Patvirtinta - expires_in: Pasibaigia - last_delivery: Paskutinis pristatymas - title: WebSub protokolas - topic: Tema tags: accounts: Paskyros hidden: Paslėpti @@ -672,10 +665,6 @@ lt: reply: proceed: Atsakyti prompt: 'Jūs norite atsakyti šiam toot''ui:' - remote_unfollow: - error: Klaida - title: Pavadinimas - unfollowed: Nebesekama scheduled_statuses: over_daily_limit: Jūs pasieketė limitą (%{limit}) galimų toot'ų per dieną over_total_limit: Jūs pasieketė %{limit} limitą galimų toot'ų diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 78be7872d..52c23de8f 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -469,13 +469,6 @@ nl: no_status_selected: Er werden geen toots gewijzigd, omdat er geen enkele werd geselecteerd title: Toots van account with_media: Met media - subscriptions: - callback_url: Callback-URL - confirmed: Bevestigd - expires_in: Verloopt over - last_delivery: Laatste bezorging - title: WebSub - topic: Account tags: accounts: Accounts hidden: Verborgen @@ -816,10 +809,6 @@ nl: reply: proceed: Doorgaan met reageren prompt: 'Je wilt op de volgende toot reageren:' - remote_unfollow: - error: Fout - title: Titel - unfollowed: Ontvolgd scheduled_statuses: over_daily_limit: Je hebt de limiet van %{limit} in te plannen toots voor die dag overschreden over_total_limit: Je hebt de limiet van %{limit} in te plannen toots overschreden diff --git a/config/locales/no.yml b/config/locales/no.yml index d21dda6fb..fbf138393 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -267,12 +267,6 @@ no_media: Ingen media title: Kontostatuser with_media: Med media - subscriptions: - callback_url: Callback-URL - confirmed: Bekreftet - expires_in: Utløper om - last_delivery: Siste levering - topic: Emne title: Administrasjon admin_mailer: new_report: diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 785caa4ec..067c343b8 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -7,6 +7,7 @@ oc: active_count_after: actius active_footnote: Utilizaire actius per mes (UAM) administered_by: 'Administrat per :' + api: API apps: Aplicacions per mobil apps_platforms: Utilizatz Mastodon d‘iOS, Android o d’autras plataforma estant browse_directory: Navigatz per l’annuari de perfil e filtratz segon çò qu’aimatz @@ -19,6 +20,7 @@ oc: extended_description_html: |

    Una bona plaça per las règlas

    La descripcion longa es pas estada causida pel moment.

    + federation_hint_html: Amb un compte sus %{instance} poiretz sègre de personas de qualque siasque servidor Mastodon e encara mai. generic_description: "%{domain} es un dels servidors del malhum" get_apps: Ensajatz una aplicacion mobil hosted_on: Mastodon albergat sus %{domain} @@ -31,6 +33,7 @@ oc: one: estatut other: estatuts status_count_before: qu’an escrich + tagline: Seguètz d’amics e trobatz-ne de nòus terms: Condicions d’utilizacion user_count_after: one: utilizaire @@ -62,8 +65,10 @@ oc: posts_with_replies: Tuts e responsas reserved_username: Aqueste nom d’utilizaire es reservat roles: + admin: Admin bot: Robòt moderator: Moderador + unavailable: Perfil indisponible unfollow: Quitar de sègre admin: account_actions: @@ -76,7 +81,9 @@ oc: destroyed_msg: Nòta de moderacion ben suprimida ! accounts: approve: Aprovar + approve_all: O validar tot are_you_sure: Sètz segur ? + avatar: Avatar by_domain: Domeni change_email: changed_msg: Adreça corrèctament cambiada ! @@ -107,6 +114,7 @@ oc: header: Bandièra inbox_url: URL de recepcion invited_by: Convidat per + ip: IP joined: Venguèt location: all: Totes @@ -126,6 +134,7 @@ oc: moderation_notes: Nòtas de moderacion most_recent_activity: Activitat mai recenta most_recent_ip: IP mai recenta + no_account_selected: Cap de compte pas cambiat estant que cap èra pas seleccionat no_limits_imposed: Cap de limit impausat not_subscribed: Pas seguidor outbox_url: URL Outbox @@ -134,8 +143,11 @@ oc: profile_url: URL del perfil promote: Promòure protocol: Protocòl + public: Public push_subscription_expires: Fin de l’abonament PuSH redownload: Actualizar lo perfil + reject: Regetar + reject_all: O regetar tot remove_avatar: Supriir l’avatar remove_header: Levar la bandièra resend_confirmation: @@ -145,7 +157,9 @@ oc: reset: Reïnicializar reset_password: Reïnicializar lo senhal resubscribe: Se tornar abonar + role: Autorizacions roles: + admin: Administrator moderator: Moderador staff: Personnal user: Uitlizaire @@ -160,6 +174,7 @@ oc: statuses: Estatuts subscribe: S’abonar suspended: Suspendut + time_in_queue: En espèra a la fila %{time} title: Comptes unconfirmed_email: Adreça pas confirmada undo_silenced: Levar lo silenci @@ -167,6 +182,7 @@ oc: unsubscribe: Se desabonar username: Nom d’utilizaire warn: Avisar + web: Web action_logs: actions: assigned_to_self_report: "%{name} s’assignèt lo rapòrt %{target}" @@ -211,6 +227,7 @@ oc: destroyed_msg: Emoji ben suprimit ! disable: Desactivar disabled_msg: Aqueste emoji es ben desactivat + emoji: Emoji enable: Activar enabled_msg: Aqueste emoji es ben activat image_hint: PNG cap a 50Ko @@ -233,6 +250,7 @@ oc: feature_profile_directory: Annuari de perfils feature_registrations: Inscripcions feature_relay: Relai de federacion + feature_timeline_preview: Apercebut del flux d’actualitats features: Foncionalitats hidden_service: Federacion amb servicis amagats open_reports: Senhalaments dobèrts @@ -252,6 +270,7 @@ oc: created_msg: Domeni blocat es a èsser tractat destroyed_msg: Lo blocatge del domeni es estat levat domain: Domeni + existing_domain_block_html: Impausèretz ja de limitas mai estrictas per %{name}, vos cal lo desblocard’en primièr. new: create: Crear blocatge hint: Lo blocatge empacharà pas la creacion de compte dins la basa de donadas, mai aplicarà la moderacion sus aquestes comptes. @@ -317,6 +336,8 @@ oc: expired: Expirats title: Filtre title: Convits + pending_accounts: + title: Comptes en espèra (%{count}) relays: add_new: Ajustar un nòu relai delete: Suprimir @@ -405,7 +426,9 @@ oc: title: Autorizat amb invitacions registrations_mode: modes: + approved: Validacion necessària per s’inscriure none: Degun pòt pas se marcar + open: Tot lo monde se pòt marcar title: Mòdes d’inscripcion show_known_fediverse_at_about_page: desc_html: Un còp activat mostrarà los tuts de totes los fediverse dins l’apercebut. Autrament mostrarà pas que los tuts locals. @@ -446,12 +469,6 @@ oc: no_status_selected: Cap d’estatut pas cambiat estant que cap èra pas seleccionat title: Estatuts del compte with_media: Amb mèdia - subscriptions: - callback_url: URL de rapèl - confirmed: Confirmat - expires_in: S’acaba dins - last_delivery: Darrièra distribucion - topic: Subjècte tags: accounts: Comptes hidden: Amagat @@ -459,6 +476,7 @@ oc: name: Etiqueta title: Etiquetas unhide: Aparéisser dins l’annuari + visible: Visible title: Administracion warning_presets: add_new: N’ajustar un nòu @@ -467,12 +485,22 @@ oc: edit_preset: Modificar lo tèxt predefinit d’avertiment title: Gerir los tèxtes predefinits admin_mailer: + new_pending_account: + body: Los detalhs del nòu compte son çai-jos. Podètz validar o regetar aquesta demanda. + subject: Nòu compte per repassar sus %{instance} (%{username}) new_report: body: "%{reporter} a senhalat %{target}" body_remote: Qualqu’un de %{domain} senhalèt %{target} subject: Novèl senhalament per %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfàcia web avançada + advanced_web_interface_hint: 'Se volètz utilizar la nautor complèta de l’ecran, l’interfàcia web avançada vos permet de configurar diferentas colomnas per mostrar tan d’informacions que volètz : Acuèlh, notificacions, flux d’actualitat, e d’autras listas e etiquetas.' + animations_and_accessibility: Animacion e accessibilitat + confirmation_dialogs: Fenèstras de confirmacion + sensitive_content: Contengut sensible application_mailer: notification_preferences: Cambiar las preferéncias de corrièl + salutation: "%{name}," settings: 'Cambiar las preferéncias de corrièl : %{link}' view: 'Veire :' view_profile: Veire lo perfil @@ -500,6 +528,9 @@ oc: migrate_account: Mudar endacòm mai migrate_account_html: Se volètz mandar los visitors d’aqueste compte a un autre, podètz o configurar aquí. or_log_in_with: O autentificatz-vos amb + providers: + cas: CAS + saml: SAML register: Se marcar registration_closed: "%{instance} accepta pas de nòus membres" resend_confirmation: Tornar mandar las instruccions de confirmacion @@ -531,6 +562,7 @@ oc: x_days: "%{count} jorns" x_minutes: "%{count} min" x_months: "%{count} meses" + x_seconds: "%{count}s" deletes: bad_password_msg: Ben ensajat pirata ! Senhal incorrècte confirm_password: Picatz vòstre senhal actual per verificar vòstra identitat @@ -561,6 +593,9 @@ oc: content: Un quicomet a pas foncionat coma caliá. title: Aquesta pagina es pas corrècta noscript_html: Per utilizar l’aplicacion web de Mastodon, mercés d’activar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa. + existing_username_validator: + not_found: impossible de trobar un utilizaire local amb aqueste nom d’utilizaire + not_found_multiple: impossible de trobar %{usernames} exports: archive_takeout: date: Data @@ -570,6 +605,7 @@ oc: request: Demandar vòstre archiu size: Talha blocks: Personas que blocatz + csv: CSV domain_blocks: Blocatge de domenis follows: Personas que seguètz lists: Listas @@ -603,16 +639,31 @@ oc: all: Tot changes_saved_msg: Cambiaments ben realizats ! copy: Copiar + order_by: Triar per save_changes: Salvar los cambiaments validation_errors: one: I a quicòm que truca ! Mercés de corregir l’error çai-jos other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos + html_validator: + invalid_markup: 'conten un balisatge HTML invalid : %{error}' identity_proofs: + active: Actiu authorize: Òc, autorizar authorize_connection_prompt: Autorizar aquesta connexion criptografica ? + errors: + failed: La connexion criptografica a fracassat. Ensajatz tornamai de %{provider} estant. + keybase: + invalid_token: Los getons Keybase son de hashes de signaturas e devon èsser de caractèrs 66 hex + verification_failed: Keybase reconeis pas aqueste geton coma signatura de l’utilizaire Keybase %{kb_username}. Ensajatz tornamai de Keybase estant. + wrong_user: Creacion impossibla de la pròva per %{proving} en estant connectat coma %{current}. Connectatz-vos coma %{proving} e ensajatz tornamai. + explanation_html: Aquí podètz connectar d’un biais criptografic vòstras identitats, coma un perfil Keybase. Aquò permet al monde de vos enviar de messatges chifrats e fisar al contengut que lor enviatz. i_am_html: Soi %{username} a %{service}. identity: Identitat + inactive: Inactiu + publicize_checkbox: 'E enviatz lo tut seguent :' + publicize_toot: 'Es provat ! Soi %{username} de %{service} : %{url}' status: Estatut de verificacion + view_proof: Veire la pròva imports: modes: merge: Fondre @@ -698,11 +749,22 @@ oc: body: "%{name} a tornat partejar vòstre estatut :" subject: "%{name} a tornat partejar vòstre estatut" title: Novèl partatge + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T pagination: newer: Mai recents next: Seguent older: Mai ancians prev: Precedent + truncate: "…" polls: errors: already_voted: Avètz ja votat per aqueste sondatge @@ -715,13 +777,20 @@ oc: too_many_options: pòt pas contenir mai de %{max} opcions preferences: other: Autre + posting_defaults: Valors per defaut de las publicacions + public_timelines: Fluxes d’actualitats publics relationships: activity: Activitat del compte dormant: Inactiu + last_active: Darrièra activitat + most_recent: Mai recenta moved: Mudat mutual: Mutuala primary: Pirmària relationship: Relacion + remove_selected_domains: Levar totes los seguidors dels domenis seleccionats + remove_selected_followers: Levar los seguidors seleccionats + remove_selected_follows: Quitar de sègre las personas seleccionadas status: Estat del compte remote_follow: acct: Picatz vòstre utilizaire@domeni que que volètz utilizar per sègre aqueste utilizaire @@ -740,9 +809,6 @@ oc: reply: proceed: Contunhar per respondre prompt: 'Volètz respondre a aqueste tut :' - remote_unfollow: - title: Títol - unfollowed: Pas mai seguit scheduled_statuses: over_daily_limit: Avètz passat la limita de %{limit} tuts programats per aquel jorn over_total_limit: Avètz passat la limita de %{limit} tuts programats @@ -751,15 +817,47 @@ oc: activity: Darrièra activitat browser: Navigator browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Navigator desconegut + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Session en cors description: "%{browser} sus %{platform}" explanation: Aquí los navigators connectats a vòstre compte Mastodon. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: plataforma desconeguda + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: Revocar revoke_success: Session ben revocada + title: Sessions settings: + account: Compte + account_settings: Paramètres de compte + appearance: Aparéncia authorized_apps: Aplicacions autorizadas back: Tornar a Mastodon delete: Supression de compte @@ -767,10 +865,13 @@ oc: edit_profile: Modificar lo perfil export: Exportar de donadas featured_tags: Etiquetas en avant + identity_proofs: Pròvas d’identitat import: Importar de donadas + import_and_export: Import e export migrate: Migracion de compte notifications: Notificacions preferences: Preferéncias + profile: Perfil relationships: Abonaments e seguidors two_factor_authentication: Autentificacion en dos temps statuses: @@ -806,6 +907,7 @@ oc: visibilities: private: Seguidors solament private_long: Mostrar pas qu’als seguidors + public: Public public_long: Tot lo monde pòt veire unlisted: Pas listat unlisted_long: Tot lo monde pòt veire mai serà pas visible sul flux public @@ -905,6 +1007,7 @@ oc: time: formats: default: Lo %d %b de %Y a %Ho%M + month: "%b de %Y" two_factor_authentication: code_hint: Picatz lo còdi generat per vòstra aplicacion d’autentificacion per confirmar description_html: S’activatz l’autentificacion two-factor, vos caldrà vòstre mobil per vos connectar perque generarà un geton per vos daissar dintrar. diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 78db4c672..1d6f13090 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -52,7 +52,7 @@ pl: many: śledzących one: śledzący other: Śledzących - following: Śledzonych + following: śledzonych joined: Dołączył(a) %{date} last_active: ostatnio aktywny(-a) link_verified_on: Własność tego odnośnika została sprawdzona %{date} @@ -481,13 +481,6 @@ pl: no_status_selected: Żaden wpis nie został zmieniony, bo żaden nie został wybrany title: Wpisy konta with_media: Z zawartością multimedialną - subscriptions: - callback_url: URL zwrotny - confirmed: Potwierdzone - expires_in: Wygasa - last_delivery: Ostatnio doręczono - title: WebSub - topic: Temat tags: accounts: Konta hidden: Ukryte @@ -838,10 +831,6 @@ pl: reply: proceed: Przejdź do dodawania odpowiedzi prompt: 'Chcesz odpowiedzieć na ten wpis:' - remote_unfollow: - error: Błąd - title: Tytuł - unfollowed: Przestałeś(-aś) śledzić scheduled_statuses: over_daily_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów na ten dzień over_total_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index d75e91b8b..e29191871 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -451,12 +451,6 @@ pt-BR: no_status_selected: Nenhum status foi modificado porque nenhum estava selecionado title: Postagens da conta with_media: Com mídia - subscriptions: - callback_url: URL de Callback - confirmed: Confirmado - expires_in: Expira em - last_delivery: Última entrega - topic: Tópico tags: accounts: Contas hidden: Escondido @@ -770,10 +764,6 @@ pt-BR: reply: proceed: Proceder para responder prompt: 'Você quer responder à esse toot:' - remote_unfollow: - error: Erro - title: Título - unfollowed: Deixou de seguir scheduled_statuses: over_daily_limit: Você excedeu o limite de %{limit} toots planejados para esse dia over_total_limit: Você excedeu o limite de %{limit} toots planejados diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 9cd92f6bd..41c399b7d 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -431,12 +431,6 @@ pt: no_status_selected: Nenhum estado foi alterado porque nenhum foi selecionado title: Estado das contas with_media: Com media - subscriptions: - callback_url: URL de Callback - confirmed: Confirmado - expires_in: Expira em - last_delivery: Última entrega - topic: Tópico tags: accounts: Contas hidden: Escondidas @@ -707,10 +701,6 @@ pt: reply: proceed: Prosseguir com resposta prompt: 'Queres responder a esta publicação:' - remote_unfollow: - error: Erro - title: Título - unfollowed: Não seguido scheduled_statuses: over_daily_limit: Excedeste o limite de %{limit} publicações agendadas para esse dia over_total_limit: Tu excedeste o limite de %{limit} publicações agendadas diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 7e336be98..83eb3089f 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -478,13 +478,6 @@ ru: no_status_selected: Не выбран ни один статус, ничего не изменено title: Статусы аккаунта with_media: С медиаконтентом - subscriptions: - callback_url: Callback URL - confirmed: Подтверждено - expires_in: Истекает через - last_delivery: Последняя доставка - title: WebSub - topic: Тема tags: accounts: Аккаунты hidden: Скрыты @@ -714,8 +707,8 @@ ru: many: "%{count} исп." one: 1 исп other: "%{count} исп" - max_uses_prompt: Без лимита - prompt: Генерируйте и делитесь ссылками с другими, чтобы предоставить им доступ к этому узлу + max_uses_prompt: Без ограничения + prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу table: expires_at: Истекает uses: Исп. @@ -831,10 +824,6 @@ ru: reply: proceed: Ответить prompt: 'Вы собираетесь ответить на этот статус:' - remote_unfollow: - error: Ошибка - title: Заголовок - unfollowed: Отписаны scheduled_statuses: over_daily_limit: Вы превысили лимит в %{limit} запланированных постов на указанный день over_total_limit: Вы превысили лимит на %{limit} запланированных постов diff --git a/config/locales/simple_form.co.yml b/config/locales/simple_form.co.yml index 1f5dba43f..d58e77528 100644 --- a/config/locales/simple_form.co.yml +++ b/config/locales/simple_form.co.yml @@ -34,6 +34,7 @@ co: setting_hide_network: I vostri abbunati è abbunamenti ùn saranu micca mustrati nant’à u vostru prufile setting_noindex: Tocca à u vostru prufile pubblicu è i vostri statuti setting_show_application: L'applicazione chì voi utilizate per mandà statuti sarà affissata indè a vista ditagliata di quelli + setting_use_blurhash: I digradati blurhash sò basati nant'à i culori di u ritrattu piattatu ma senza i ditagli username: U vostru cugnome sarà unicu nant'à %{domain} whole_word: Quandu a parolla o a frasa sana hè alfanumerica, sarà applicata solu s'ella currisponde à a parolla sana featured_tag: @@ -109,6 +110,7 @@ co: setting_system_font_ui: Pulizza di caratteri di u sistemu setting_theme: Tema di u situ setting_unfollow_modal: Mustrà una cunfirmazione per siguità qualch’unu + setting_use_blurhash: Vede digradati di culori per i media piattati severity: Severità type: Tippu d’impurtazione username: Cugnome diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 3bf74e971..5191d77fe 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -34,6 +34,7 @@ cs: setting_hide_network: Koho sledujete a kdo sleduje vás nebude zobrazeno na vašem profilu setting_noindex: Ovlivňuje váš veřejný profil a stránky tootů setting_show_application: Aplikace, kterou používáte k psaní tootů, bude zobrazena v detailním zobrazení vašich tootů + setting_use_blurhash: Gradienty jsou založeny na barvách skryté grafiky, ale zakrývají jakékoliv detaily username: Vaše uživatelské jméno bude na %{domain} unikátní whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikována pouze, pokud se shoduje s celým slovem featured_tag: @@ -109,6 +110,7 @@ cs: setting_system_font_ui: Použít výchozí písmo systému setting_theme: Motiv stránky setting_unfollow_modal: Zobrazovat před zrušením sledování potvrzovací okno + setting_use_blurhash: Zobrazit pro skrytá média barevné gradienty severity: Přísnost type: Typ importu username: Uživatelské jméno diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index 023506f24..9f4ef0708 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -34,6 +34,7 @@ cy: setting_hide_network: Ni fydd y rheini yr ydych yn eu dilyn a phwy sy'n eich dilyn chi yn cael ei ddangos ar eich proffil setting_noindex: Mae hyn yn effeithio ar eich proffil cyhoeddus a'ch tudalennau statws setting_show_application: Bydd y offer frydych yn defnyddio i dŵtio yn cael ei arddangos yn golwg manwl eich tŵtiau + setting_use_blurhash: Mae graddiannau wedi'u seilio ar liwiau'r delweddau cudd ond maent yn cuddio unrhyw fanylion username: Bydd eich enw defnyddiwr yn unigryw ar %{domain} whole_word: Os yw'r allweddair neu'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw'n cyfateb a'r gair cyfan featured_tag: @@ -109,6 +110,7 @@ cy: setting_system_font_ui: Defnyddio ffont rhagosodedig y system setting_theme: Thema'r wefan setting_unfollow_modal: Dangos deialog cadarnhau cyn dad-ddilyn rhywun + setting_use_blurhash: Dangoswch raddiannau lliwgar ar gyfer cyfryngau cudd severity: Difrifoldeb type: Modd mewnforio username: Enw defnyddiwr diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 61e0f9740..d07079642 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -34,6 +34,7 @@ de: setting_hide_network: Wem du folgst und wer dir folgt, wird in deinem Profil nicht angezeigt setting_noindex: Betrifft dein öffentliches Profil und deine Beiträge setting_show_application: Die Anwendung die du nutzst wird in der detaillierten Ansicht deiner Beiträge angezeigt + setting_use_blurhash: Die Farbverläufe basieren auf den Farben der versteckten Medien, aber verstecken irgendwelche Details, die Reize auslösen könnten username: Dein Profilname wird auf %{domain} einzigartig sein whole_word: Wenn das Schlagwort nur aus Buchstaben und Zahlen besteht, wird es nur angewendet, wenn es dem ganzen Wort entspricht featured_tag: @@ -109,6 +110,7 @@ de: setting_system_font_ui: Standardschriftart des Systems verwenden setting_theme: Theme setting_unfollow_modal: Bestätigungsdialog anzeigen, bevor jemandem entfolgt wird + setting_use_blurhash: Farbverlauf für versteckte Medien anzeigen severity: Schweregrad type: Art des Imports username: Profilname diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index 67f3b64aa..099c900a5 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -34,6 +34,7 @@ el: setting_hide_network: Δε θα εμφανίζεται στο προφίλ σου ποιους ακολουθείς και ποιοι σε ακολουθούν setting_noindex: Επηρεάζει το δημόσιο προφίλ και τις δημοσιεύσεις σου setting_show_application: Η εφαρμογή που χρησιμοποιείς για να στέλνεις τα τουτ σου θα εμφανίζεται στις αναλυτικές λεπτομέρειες τους + setting_use_blurhash: Οι χρωματισμοί βασίζονται στα χρώματα του κρυμμένου πολυμέσου αλλά θολώνουν τις λεπτομέρειες username: Το όνομα χρήστη σου θα είναι μοναδικό στο %{domain} whole_word: Όταν η λέξη ή η φράση κλειδί είναι μόνο αλφαριθμητική, θα εφαρμοστεί μόνο αν ταιριάζει με ολόκληρη τη λέξη featured_tag: @@ -109,6 +110,7 @@ el: setting_system_font_ui: Χρησιμοποίησε την προεπιλεγμένη γραμματοσειρά του συστήματος setting_theme: Θέμα ιστότοπου setting_unfollow_modal: Εμφάνιση ερώτησης επιβεβαίωσης πριν διακόψεις την παρακολούθηση κάποιου + setting_use_blurhash: Εμφάνιση χρωματισμών για τα κρυμμένα πολυμέσα severity: Αυστηρότητα type: Τύπος εισαγωγής username: Όνομα χρηστη diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 7b871e8ba..2986cf091 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -6,12 +6,16 @@ es: text: Puede usar sintaxis de toots, como URLs, hashtags y menciones admin_account_action: send_email_notification: El usuario recibirá una explicación de lo que sucedió con respecto a su cuenta + text_html: Opcional. Puede usar sintaxis de toots. Puede añadir configuraciones predefinidas de advertencia para ahorrar tiempo + type_html: Elige qué hacer con %{acct} + warning_preset_id: Opcional. Aún puede añadir texto personalizado al final de la configuración predefinida defaults: autofollow: Los usuarios que se registren mediante la invitación te seguirán automáticamente avatar: PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px bot: Esta cuenta ejecuta principalmente acciones automatizadas y podría no ser monitorizada context: Uno o múltiples contextos en los que debe aplicarse el filtro digest: Solo enviado tras un largo periodo de inactividad y solo si has recibido mensajes personales durante tu ausencia + discoverable_html: El directorio permite a la gente encontrar cuentas basadas en intereses y actividad. Requiere al menos %{min_followers} seguidores email: Se le enviará un correo de confirmación fields: Puedes tener hasta 4 elementos mostrándose como una tabla en tu perfil header: PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px @@ -22,10 +26,19 @@ es: password: Utilice al menos 8 caracteres phrase: Se aplicará sin importar las mayúsculas o los avisos de contenido de un toot scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionas el alcance de nivel mas alto, no necesitas seleccionar las individuales. + setting_aggregate_reblogs: No mostrar nuevos retoots para los toots que han sido recientemente retooteados (sólo afecta a los retoots recibidos recientemente) + setting_default_sensitive: El contenido multimedia sensible está oculto por defecto y puede ser mostrado con un click + setting_display_media_default: Ocultar contenido multimedia marcado como sensible + setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia + setting_display_media_show_all: Mostrar siempre contenido multimedia marcado como sensible setting_hide_network: A quién sigues y quién te sigue no será mostrado en tu perfil setting_noindex: Afecta a tu perfil público y páginas de estado setting_show_application: La aplicación que utiliza usted para publicar toots se mostrará en la vista detallada de sus toots + setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles + username: Tu nombre de usuario será único en %{domain} whole_word: Cuando la palabra clave o frase es solo alfanumérica, solo será aplicado si concuerda con toda la palabra + featured_tag: + name: 'Puede que quieras usar uno de estos:' imports: data: Archivo CSV exportado desde otra instancia de Mastodon invite_request: @@ -39,15 +52,21 @@ es: fields: name: Etiqueta value: Contenido + account_warning_preset: + text: Texto predefinido admin_account_action: send_email_notification: Notificar al usuario por correo electrónico text: Aviso personalizado type: Acción types: disable: Deshabilitar + none: No hacer nada silence: Silenciar + suspend: Suspender y eliminar de forma irreversible la información de la cuenta + warning_preset_id: Usar un aviso predeterminado defaults: autofollow: Invitar a seguir tu cuenta + avatar: Avatar bot: Esta es una cuenta bot chosen_languages: Filtrar idiomas confirm_new_password: Confirmar nueva contraseña @@ -55,6 +74,7 @@ es: context: Filtrar contextos current_password: Contraseña actual data: Información + discoverable: Listar esta cuenta en el directorio display_name: Nombre para mostrar email: Dirección de correo electrónico expires_in: Expirar tras @@ -70,12 +90,19 @@ es: otp_attempt: Código de dos factores password: Contraseña phrase: Palabra clave o frase + setting_advanced_layout: Habilitar interfaz web avanzada + setting_aggregate_reblogs: Agrupar retoots en las líneas de tiempo setting_auto_play_gif: Reproducir automáticamente los GIFs animados setting_boost_modal: Mostrar ventana de confirmación antes de un Retoot setting_default_language: Idioma de publicación setting_default_privacy: Privacidad de publicaciones setting_default_sensitive: Marcar siempre imágenes como sensibles setting_delete_modal: Mostrar diálogo de confirmación antes de borrar un toot + setting_display_media: Visualización multimedia + setting_display_media_default: Por defecto + setting_display_media_hide_all: Ocultar todo + setting_display_media_show_all: Mostrar todo + setting_expand_spoilers: Siempre expandir los toots marcados con advertencias de contenido setting_hide_network: Ocultar tu red setting_noindex: Excluirse del indexado de motores de búsqueda setting_reduce_motion: Reducir el movimiento de las animaciones @@ -83,11 +110,14 @@ es: setting_system_font_ui: Utilizar la tipografía por defecto del sistema setting_theme: Tema del sitio setting_unfollow_modal: Mostrar diálogo de confirmación antes de dejar de seguir a alguien + setting_use_blurhash: Mostrar gradientes coloridos para contenido multimedia oculto severity: Severidad type: Importar tipo username: Nombre de usuario username_or_email: Usuario o Email whole_word: Toda la palabra + featured_tag: + name: Etiqueta interactions: must_be_follower: Bloquear notificaciones de personas que no te siguen must_be_following: Bloquear notificaciones de personas que no sigues @@ -106,5 +136,6 @@ es: 'no': 'No' recommended: Recomendado required: + mark: "*" text: necesario 'yes': Sí diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index acd5fd6d9..be3883fb0 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -14,7 +14,7 @@ eu: avatar: PNG, GIF edo JPG. Gehienez %{size}. %{dimensions}px neurrira eskalatuko da bot: Kontu honek nagusiki automatizatutako ekintzak burutzen ditu eta agian ez du inork monitorizatzen context: Iragazkia aplikatzeko testuinguru bat edo batzuk - digest: Soilik jarduerarik gabeko epe luze bat eta gero, eta soilik ez zeudela mezu pertsonalen bat jaso baduzu + digest: Jarduerarik gabeko epe luze bat eta gero mezu pertsonalen bat jaso baduzu, besterik ez discoverable_html: Direktorioak Jendea interesen eta jardueraren arabera aurkitzea ahalbidetzen du. Gutxienez %{min_followers} jarraitzaile behar dira bertan agertzeko email: Baieztapen e-mail bat bidaliko zaizu fields: 4 elementu bistaratu ditzakezu taula batean zure profilean @@ -26,7 +26,7 @@ eu: password: Erabili 8 karaktere gutxienez phrase: Bat egingo du Maiuskula/minuskula kontuan hartu gabe eta edukiaren abisua kontuan hartu gabe scopes: Zeintzuk API atzitu ditzakeen aplikazioak. Goi mailako arloa aukeratzen baduzu, ez dituzu azpikoak aukeratu behar. - setting_aggregate_reblogs: Ez erakutsi buktzada berriak berriki bultzada jaso duten tootentzat (berriki jasotako bultzadei eragiten die besterik ez) + setting_aggregate_reblogs: Ez erakutsi bultzada berriak berriki bultzada jaso duten toot-entzat (berriki jasotako bultzadei eragiten die bakarrik) setting_default_sensitive: Multimedia hunkigarria lehenetsita ezkutatzen da, eta sakatuz ikusi daiteke setting_display_media_default: Ezkutatu hunkigarri gisa markatutako multimedia setting_display_media_hide_all: Ezkutatu multimedia guztia beti @@ -34,6 +34,7 @@ eu: setting_hide_network: Nor jarraitzen duzun eta nork jarraitzen zaituen ez da bistaratuko zure profilean setting_noindex: Zure profil publiko eta Toot-en orrietan eragina du setting_show_application: Tootak bidaltzeko erabiltzen duzun aplikazioa zure tooten ikuspegi xehetsuan bistaratuko da + setting_use_blurhash: Gradienteak ezkutatutakoaren koloreetan oinarritzen dira, baina xehetasunak ezkutatzen dituzte username: Zure erabiltzaile-izena bakana izango da %{domain} domeinuan whole_word: Hitz eta esaldi gakoa alfanumerikoa denean, hitz osoarekin bat datorrenean besterik ez da aplikatuko featured_tag: @@ -109,6 +110,7 @@ eu: setting_system_font_ui: Erabili sistemako tipografia lehenetsia setting_theme: Gunearen gaia setting_unfollow_modal: Erakutsi baieztapen elkarrizketa-koadroa inor jarraitzeari utzi aurretik + setting_use_blurhash: Erakutsi gradiente koloretsuak ezkutatutako multimediaren ordez severity: Larritasuna type: Inportazio mota username: Erabiltzaile-izena @@ -118,8 +120,8 @@ eu: name: Traola interactions: must_be_follower: Blokeatu jarraitzaile ez direnen jakinarazpenak - must_be_following: Blokeatu zuk jarraitzen ez dituzunen jakinarazpenak - must_be_following_dm: Blokeatu zuk jarraitzen ez dituzunen mezu zuzenak + must_be_following: Blokeatu zuk jarraitzen ez dituzu horien jakinarazpenak + must_be_following_dm: Blokeatu zuk jarraitzen ez dituzun horien mezu zuzenak invite_request: text: Zergatik elkartu nahi duzu? notification_emails: diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 22389051f..122481dcf 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -34,6 +34,7 @@ gl: setting_hide_network: Non se mostrará no seu perfil quen a segue e quen a está a seguir setting_noindex: Afecta ao seu perfil público e páxinas de estado setting_show_application: A aplicación que está a utilizar para enviar toots mostrarase na vista detallada do toot + setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esborranchando todos os detalles username: O seu nome de usuaria será único en %{domain} whole_word: Se a chave ou frase de paso é só alfanumérica, só se aplicará se concorda a palabra completa featured_tag: @@ -109,6 +110,7 @@ gl: setting_system_font_ui: Utilizar a tipografía por defecto do sistema setting_theme: Decorado da instancia setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén + setting_use_blurhash: Mostrar gradientes coloridos para medios ocultos severity: Severidade type: Tipo de importación username: Nome de usuaria diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index ccc6c5ca1..e32eded5d 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -34,6 +34,7 @@ ja: setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_noindex: 公開プロフィールおよび各投稿ページに影響します setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります + setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています username: あなたのユーザー名は %{domain} の中で重複していない必要があります whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります featured_tag: @@ -109,6 +110,7 @@ ja: setting_system_font_ui: システムのデフォルトフォントを使う setting_theme: サイトテーマ setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する + setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する severity: 重大性 type: インポートする項目 username: ユーザー名 diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml index e0bfcfef9..9453375b1 100644 --- a/config/locales/simple_form.oc.yml +++ b/config/locales/simple_form.oc.yml @@ -27,12 +27,14 @@ oc: phrase: Serà pres en compte que siá en majuscula o minuscula o dins un avertiment de contengut sensible scopes: A quinas APIs poiràn accedir las aplicacions. Se seleccionatz un encastre de naut nivèl, fa pas mestièr de seleccionar los nivèls mai basses. setting_aggregate_reblogs: Mostrar pas los nòus partatges que son estats partejats recentament (afecta pas que los nòus partatges recebuts) + setting_default_sensitive: Los mèdias sensibles son resconduts per defaut e se revelhan amb un clic setting_display_media_default: Rescondre los mèdias marcats coma sensibles setting_display_media_hide_all: Totjorn rescondre los mèdias setting_display_media_show_all: Totjorn mostrar los mèdias marcats coma sensibles setting_hide_network: Vòstre perfil mostrarà pas los que vos sègon e lo monde que seguètz setting_noindex: Aquò es destinat a vòstre perfil public e vòstra pagina d’estatuts setting_show_application: Lo nom de l’aplicacion qu’utilizatz per publicar serà mostrat dins la vista detalhada de vòstres tuts + setting_use_blurhash: Los degradats venon de las colors de l’imatge rescondut en enfoscar los detalhs username: Vòstre nom d’utilizaire serà unic sus %{domain} whole_word: Quand lo mot-clau o frasa es solament alfranumeric, serà pas qu’aplicat se correspond al mot complèt featured_tag: @@ -64,6 +66,7 @@ oc: warning_preset_id: Utilizar un avertiment predefinit defaults: autofollow: Convidar a sègre vòstre compte + avatar: Avatar bot: Aquò es lo compte a un robòt chosen_languages: Filtrar las lengas confirm_new_password: Confirmacion del nòu senhal @@ -83,9 +86,11 @@ oc: locked: Far venir lo compte privat max_uses: Limit d’utilizacions new_password: Nòu senhal + note: Bio otp_attempt: Còdi Two-factor password: Senhal phrase: Senhal o frasa + setting_advanced_layout: Activar l’interfàcia web avançada setting_aggregate_reblogs: Agropar los partatges dins lo flux d’actualitat setting_auto_play_gif: Lectura automatica dels GIFS animats setting_boost_modal: Mostrar una fenèstra de confirmacion abans de partejar un estatut @@ -105,6 +110,7 @@ oc: setting_system_font_ui: Utilizar la polissa del sistèma setting_theme: Tèma del site setting_unfollow_modal: Mostrar una confirmacion abans de quitar de sègre qualqu’un + setting_use_blurhash: Mostrar los degradats colorats pels mèdias resconduts severity: Severitat type: Tipe d’impòrt username: Nom d’utilizaire @@ -128,6 +134,8 @@ oc: reblog: Enviar un corrièl quand qualqu’un tòrna partejar vòstre estatut report: Enviar un corrièl pels nòus senhalaments 'no': Non + recommended: Recomandat required: + mark: "*" text: requesit 'yes': Òc diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 2f9bf5329..553900855 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -34,6 +34,7 @@ pl: setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany + setting_use_blurhash: Gradienty są oparte na kolorach ukrywanej zawartości, ale uniewidaczniają wszystkie szczegóły username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain} whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień featured_tag: @@ -109,6 +110,7 @@ pl: setting_system_font_ui: Używaj domyślnej czcionki systemu setting_theme: Motyw strony setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia + setting_use_blurhash: Pokazuj kolorowe gradienty dla ukrytej zawartości multimedialnej severity: Priorytet type: Importowane dane username: Nazwa użytkownika diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index 26a73c3c6..fcc1c2827 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -10,7 +10,7 @@ ru: type_html: Выберите, что делать с аккаунтом %{acct} warning_preset_id: Необязательно. Вы можете добавить собственный текст в конце шаблона defaults: - autofollow: Люди, пришедшие по этому приглашению автоматически будут подписаны на Вас + autofollow: Люди, пришедшие по этому приглашению, автоматически будут подписаны на вас avatar: PNG, GIF или JPG. Максимально %{size}. Будет уменьшено до %{dimensions}px bot: Этот аккаунт обычно выполяет автоматизированные действия и может не просматриваться владельцем context: Один или несколько контекстов, к которым должны быть применены фильтры @@ -64,7 +64,7 @@ ru: suspend: Заблокировать и безвозвратно удалить все данные аккаунта warning_preset_id: Использовать шаблон defaults: - autofollow: Пригласите подписаться на Ваш аккаунт + autofollow: С подпиской на ваш аккаунт avatar: Аватар bot: Это аккаунт бота chosen_languages: Фильтр языков @@ -83,7 +83,7 @@ ru: irreversible: Удалять, а не скрывать locale: Язык интерфейса locked: Сделать аккаунт закрытым - max_uses: Макс. число использований + max_uses: Максимальное число использований new_password: Новый пароль note: О Вас otp_attempt: Двухфакторный код diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 8470f7939..4ee251b26 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -109,6 +109,7 @@ sk: setting_system_font_ui: Použi základné systémové písmo setting_theme: Vzhľad webu setting_unfollow_modal: Vyžaduj potvrdenie pred skončením sledovania iného užívateľa + setting_use_blurhash: Ukáž farebné prechody pre skryté médiá severity: Závažnosť type: Typ importu username: Prezývka @@ -134,5 +135,6 @@ sk: 'no': Nie recommended: Odporúčané required: + mark: "*" text: povinné 'yes': Áno diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml index 9d6b75ed0..33a3c5a3a 100644 --- a/config/locales/simple_form.th.yml +++ b/config/locales/simple_form.th.yml @@ -34,6 +34,7 @@ th: setting_hide_network: จะไม่แสดงผู้ที่คุณติดตามและผู้ที่ติดตามคุณในโปรไฟล์ของคุณ setting_noindex: มีผลต่อโปรไฟล์สาธารณะและหน้าสถานะของคุณ setting_show_application: จะแสดงแอปพลิเคชันที่คุณใช้เพื่อโพสต์ในมุมมองโดยละเอียดของโพสต์ของคุณ + setting_use_blurhash: การไล่ระดับสีอิงตามสีของภาพที่ซ่อนอยู่แต่ทำให้รายละเอียดใด ๆ คลุมเครือ username: ชื่อผู้ใช้ของคุณจะไม่ซ้ำกันบน %{domain} whole_word: เมื่อคำสำคัญหรือวลีมีแค่ตัวอักษรและตัวเลข จะถูกใช้หากตรงกันทั้งคำเท่านั้น featured_tag: @@ -109,6 +110,7 @@ th: setting_system_font_ui: ใช้แบบอักษรเริ่มต้นของระบบ setting_theme: ชุดรูปแบบไซต์ setting_unfollow_modal: แสดงกล่องโต้ตอบการยืนยันก่อนเลิกติดตามใครสักคน + setting_use_blurhash: แสดงการไล่ระดับสีที่มีสีสันสำหรับสื่อที่ซ่อนอยู่ severity: ความรุนแรง type: ชนิดการนำเข้า username: ชื่อผู้ใช้ diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index b781deb32..c62e57a51 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -15,7 +15,7 @@ zh-CN: bot: 来自这个帐户的绝大多数操作都是自动进行的,并且可能无人监控 context: 过滤器的应用场景 digest: 仅在你长时间未登录,且收到了私信时发送 - discoverable_html: 目录 让大家能根据兴趣和活动寻找用户。需要至少 %{min_followers} 位关注者 + discoverable_html: 用户目录 让大家能根据兴趣和活动寻找用户。需要至少 %{min_followers} 位关注者 email: 我们会向你发送一封确认邮件 fields: 这将会在个人资料页上以表格的形式展示,最多 4 个项目 header: 文件大小限制 %{size},只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 %{dimensions}px @@ -34,6 +34,7 @@ zh-CN: setting_hide_network: 你关注的人和关注你的人将不会在你的个人资料页上展示 setting_noindex: 此设置会影响到你的公开个人资料以及嘟文页面 setting_show_application: 你用来发表嘟文的应用程序将会在你嘟文的详细内容中显示 + setting_use_blurhash: 渐变是基于模糊后的隐藏内容生成的 username: 你的用户名在 %{domain} 上是独特的 whole_word: 如果关键词只包含字母和数字,就只会在整个词被匹配时才会套用 featured_tag: @@ -73,7 +74,7 @@ zh-CN: context: 过滤器场景 current_password: 当前密码 data: 数据文件 - discoverable: 在本站用户资料目录中列出此账户 + discoverable: 在本站用户目录中收录此账户 display_name: 昵称 email: 电子邮件地址 expires_in: 失效时间 @@ -109,6 +110,7 @@ zh-CN: setting_system_font_ui: 使用系统默认字体 setting_theme: 站点主题 setting_unfollow_modal: 在取消关注前询问我 + setting_use_blurhash: 将隐藏媒体显示为彩色渐变 severity: 级别 type: 导入数据类型 username: 用户名 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 75a43e322..5577f9394 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -301,7 +301,7 @@ sk: affected_accounts: few: "%{count} účtov v databázi ovplyvnených" many: "%{count} účtov v databázi ovplyvnených" - one: Jeden účet v databázi ovplyvnený + one: "%{count} účet v databázi ovplyvnený" other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény @@ -481,13 +481,6 @@ sk: no_status_selected: Žiadne príspevky neboli zmenené, keďže si žiadne nemal/a zvolené title: Príspevky na účte with_media: S médiami - subscriptions: - callback_url: Zdrojová adresa URL - confirmed: Potvrdené - expires_in: Vyprší do - last_delivery: Posledné doručenie - title: WebSub - topic: Téma tags: accounts: Účty hidden: Skryté @@ -513,6 +506,7 @@ sk: subject: Nové hlásenie pre %{instance} (#%{id}) appearance: advanced_web_interface: Pokročilé webové rozhranie + advanced_web_interface_hint: 'Ak chceš využiť celkovú šírku tvojej obrazovky, pokročilé webové rozhranie ti umožňuje nastaviť mnoho rôznych stĺpcov, aby si videl/a toľko informácií naraz, koľko chceš: Domov, oboznámenia, federovanú časovú os, a ľubovolný počet zoznamov, či haštagov.' animations_and_accessibility: Animácie a prístupnosť confirmation_dialogs: Potvrdzovacie dialógy sensitive_content: Chúlostivý obsah @@ -657,12 +651,19 @@ sk: copy: Kopíruj order_by: Zoraď podľa save_changes: Ulož zmeny + validation_errors: + few: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chýb uvedených nižšie + many: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chýb uvedených nižšie + one: Niečo ešte nieje celkom v poriadku! Prosím skontroluj chybu uvedenú nižšie + other: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chyby uvedené nižšie identity_proofs: active: Aktívne authorize: Áno, povoľ authorize_connection_prompt: Povoliť toto kryptografické prepojenie? errors: failed: Kryptografické prepojenie sa nepodarilo. Prosím skús to znova z %{provider}. + keybase: + verification_failed: Keybase nerozpoznáva tento token ako podpis od Keybase užívateľa menom %{kb_username}. Prosím skús to znova cez Keybase. i_am_html: Na %{service} som %{username}. identity: Identita inactive: Neaktívne @@ -768,6 +769,7 @@ sk: too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: other: Ostatné + posting_defaults: Východiskové nastavenia príspevkov public_timelines: Verejné časové osi relationships: activity: Aktivita účtu @@ -778,6 +780,7 @@ sk: mutual: Spoločné primary: Hlavné relationship: Vzťah + remove_selected_domains: Vymaž všetkých následovateľov z vybraných domén remove_selected_followers: Odstráň vybraných následovatrľov remove_selected_follows: Prestaň sledovať vybraných užívateľov status: Stav účtu @@ -798,10 +801,6 @@ sk: reply: proceed: Pokračuj odpovedaním prompt: 'Chceš odpovedať na tento príspevok:' - remote_unfollow: - error: Chyba - title: Názov - unfollowed: Už nesleduješ scheduled_statuses: over_daily_limit: Prekročil/a si denný limit %{limit} predplánovaných príspevkov over_total_limit: Prekročil/a si limit %{limit} predplánovaných príspevkov diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 85e167ca9..ff6c5851a 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -1,22 +1,33 @@ --- sl: about: - about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediversu. + about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediverse-u. about_mastodon_html: Mastodon je socialno omrežje, ki temelji na odprtih spletnih protokolih in prosti ter odprtokodni programski opremi. Je decentraliziran, kot e-pošta. about_this: O Mastodonu + active_count_after: dejaven + active_footnote: Aktivni mesečni uporabniki (AMU) administered_by: 'Upravlja:' + api: API apps: Mobilne aplikacije + apps_platforms: Uporabljajte Mastodon iz iOS, Android ali iz drugih platform + browse_directory: Brskajte po imeniku profilov in filtriranje po interesih + browse_public_posts: Brskajte javnih objav v živo na Mastodonu contact: Kontakt contact_missing: Ni nastavljeno contact_unavailable: Ni na voljo + discover_users: Odkrijte uporabnike documentation: Dokumentacija extended_description_html: |

    Dober prostor za pravila

    Razširjen opis še ni bil nastavljen.

    + federation_hint_html: Z računom na %{instance} boste lahko spremljali ljudi na kateremkoli Mastodon strežniku. generic_description: "%{domain} je en strežnik v omrežju" + get_apps: Poskusite mobilno aplikacijo hosted_on: Mastodon gostuje na %{domain} - learn_more: Spoznaj več - privacy_policy: Politika zasebnosti + learn_more: Nauči se več + privacy_policy: Pravilnik o zasebnosti + see_whats_happening: Poglejte, kaj se dogaja + server_stats: 'Statistika strežnika:' source_code: Izvorna koda status_count_after: few: stanja @@ -24,12 +35,13 @@ sl: other: stanj two: stanja status_count_before: Ki so avtorji + tagline: Sledite prijateljem in odkrijte nove terms: Pogoji storitve user_count_after: few: uporabniki one: uporabnik other: uporabnikov - two: uporabniki + two: uporabnika user_count_before: Dom za what_is_mastodon: Kaj je Mastodon? accounts: @@ -38,56 +50,61 @@ sl: followers: few: Sledilci one: Sledilec - other: Sledilci - two: Sledilci + other: Sledilcev + two: Sledilca following: Sledim joined: Se je pridružil na %{date} - last_active: zadnji aktivni + last_active: zadnja dejavnost link_verified_on: Lastništvo te povezave je bilo preverjeno na %{date} - media: Medij + media: Mediji moved_html: "%{name} se je prestavil na %{new_profile_link}:" - network_hidden: Te informacije niso na voljo - nothing_here: Nič ni tukaj! + network_hidden: Ta informacija ni na voljo + nothing_here: Tukaj ni ničesar! people_followed_by: Ljudje, ki jim sledi %{name} people_who_follow: Ljudje, ki sledijo %{name} pin_errors: following: Verjetno že sledite osebi, ki jo želite potrditi posts: - few: Trob - one: Trob - other: Trob - two: Trob - posts_tab_heading: Trobi + few: Tuti + one: Tut + other: Tutov + two: Tuta + posts_tab_heading: Tuti posts_with_replies: Tuti in odgovori reserved_username: Uporabniško ime je zasedeno roles: admin: Skrbnik bot: Robot + moderator: Mod + unavailable: Profil ni na voljo unfollow: Prenehaj slediti admin: account_actions: action: Izvedi dejanje - title: Izvedi moderirano dejanje %{acct} + title: Izvedi moderirano dejanje za %{acct} account_moderation_notes: create: Pusti opombo - created_msg: Uspešno ustvarjena opomba moderiranja! + created_msg: Moderirana opomba je uspešno ustvarjena! delete: Izbriši destroyed_msg: Moderirana opomba je uspešno uničena! accounts: - are_you_sure: Ali si prepričan? + approve: Odobri + approve_all: Odobri vse + are_you_sure: Ali ste prepričani? + avatar: Podoba by_domain: Domena change_email: changed_msg: E-pošta računa je uspešno spremenjena! - current_email: Trenutna E-pošta - label: Spremeni E-pošto - new_email: Nova E-pošta - submit: Spremeni E-pošto - title: Spremeni E-pošto za %{username} + current_email: Trenutna e-pošta + label: Spremeni e-pošto + new_email: Nova e-pošta + submit: Spremeni e-pošto + title: Spremeni e-pošto za %{username} confirm: Potrdi confirmed: Potrjeno confirming: Potrjujem deleted: Izbrisano - demote: Ponižaj + demote: Degradiraj disable: Onemogoči disable_two_factor_authentication: Onemogoči 2FA disabled: Onemogočeno @@ -95,44 +112,50 @@ sl: domain: Domena edit: Uredi email: E-pošta - email_status: Stanje E-pošte + email_status: Stanje e-pošte enable: Omogoči enabled: Omogočeno - feed_url: URL vir + feed_url: URL vira followers: Sledilci - followers_url: URL sledilci + followers_url: URL sledilcev follows: Sledi header: Glava - inbox_url: URl v mapi "Prejeto" + inbox_url: URL mape "Prejeto" invited_by: Povabljen od + ip: IP joined: Pridružil location: all: Vse - local: Lokalno + local: Lokalni remote: Oddaljeni title: Lokacija login_status: Stanje prijave - media_attachments: Medijske priloge + media_attachments: Predstavnostne priloge memorialize: Spremenite v spomin moderation: active: Dejaven all: Vse + pending: Na čakanju silenced: Utišan suspended: Suspendiran title: Moderiranje moderation_notes: Opombe moderiranja - most_recent_activity: Zadnja aktivnost + most_recent_activity: Zadnja dejavnost most_recent_ip: Zadnji IP + no_account_selected: Noben račun ni bil spremenjen, ker ni bil izbran noben no_limits_imposed: Brez omejitev - not_subscribed: Ni naročeno - outbox_url: URl za pošiljanje - perform_full_suspension: Začasno ustavi + not_subscribed: Ni naročen + outbox_url: URL za pošiljanje + pending: Čakanje na pregled + perform_full_suspension: Suspendiraj profile_url: URL profila - promote: Spodbujanje + promote: Promoviraj protocol: Protokol public: Javen push_subscription_expires: Naročnina PuSH preteče redownload: Osveži profil + reject: Zavrni + reject_all: Zavrni vse remove_avatar: Odstrani podobo remove_header: Odstrani glavo resend_confirmation: @@ -145,9 +168,11 @@ sl: role: Dovoljenja roles: admin: Skrbnik + moderator: Moderator staff: Osebje user: Uporabnik - search: Poišči + salmon_url: URL lososa + search: Iskanje shared_inbox_url: URL mape "Prejeto v skupni rabi" show: created_reports: Narejene prijave @@ -157,6 +182,7 @@ sl: statuses: Stanja subscribe: Naroči suspended: Suspendiran + time_in_queue: Čakanje v vrsti %{time} title: Računi unconfirmed_email: Nepotrjena e-pošta undo_silenced: Razveljavi utišanje @@ -171,27 +197,27 @@ sl: change_email_user: "%{name} je spremenil naslov e-pošte uporabnika %{target}" confirm_user: "%{name} je potrdil naslov e-pošte uporabnika %{target}" create_account_warning: "%{name} je poslal opozorilo %{target}" - create_custom_emoji: "%{name} je poslal nove emotikone %{target}" + create_custom_emoji: "%{name} je posodobil emotikone %{target}" create_domain_block: "%{name} je blokiral domeno %{target}" create_email_domain_block: "%{name} je dal na črni seznam e-pošto domene %{target}" demote_user: "%{name} je degradiral uporabnika %{target}" - destroy_custom_emoji: "%{name} je uničil emotikon %{target}" + destroy_custom_emoji: "%{name} je uničil emotikone %{target}" destroy_domain_block: "%{name} je odblokiral domeno %{target}" destroy_email_domain_block: "%{name} je dal na beli seznam e-pošto domene %{target}" destroy_status: "%{name} je odstranil stanje od %{target}" disable_2fa_user: "%{name} je onemogočil dvofaktorsko zahtevo za uporabnika %{target}" - disable_custom_emoji: "%{name} je onemogočil emotikon %{target}" + disable_custom_emoji: "%{name} je onemogočil emotikone %{target}" disable_user: "%{name} je onemogočil prijavo za uporabnika %{target}" - enable_custom_emoji: "%{name} je omogočil emotikon %{target}" + enable_custom_emoji: "%{name} je omogočil emotikone %{target}" enable_user: "%{name} je omogočil prijavo za uporabnika %{target}" memorialize_account: "%{name} je spremenil račun od %{target} v stran spominov" - promote_user: "%{name} je spodbudil uporabnika %{target}" + promote_user: "%{name} je promoviral uporabnika %{target}" remove_avatar_user: "%{name} je odstranil podobo od %{target}" reopen_report: "%{name} je ponovno odprl prijavo %{target}" reset_password_user: "%{name} je ponastavil geslo od uporabnika %{target}" resolve_report: "%{name} je razrešil prijavo %{target}" silence_account: "%{name} je utišal račun od %{target}" - suspend_account: "%{name} je začasno ustavil račun od %{target}" + suspend_account: "%{name} je suspendiral račun od %{target}" unassigned_report: "%{name} je nedodeljeno prijavil %{target}" unsilence_account: "%{name} je preklical utišanje računa od %{target}" unsuspend_account: "%{name} je aktiviral račun od %{target}" @@ -201,9 +227,9 @@ sl: title: Dnevnik revizije custom_emojis: by_domain: Domena - copied_msg: Lokalna kopija emotikona je bila uspešno ustvarjena + copied_msg: Lokalna kopija emotikonov je bila uspešno ustvarjena copy: Kopiraj - copy_failed_msg: Lokalne kopije emotikona ni bilo mogoče ustvariti + copy_failed_msg: Lokalne kopije emotikonov ni bilo mogoče ustvariti created_msg: Emotikon je uspešno ustvarjen! delete: Izbriši destroyed_msg: Emotikon je uspešno uničen! @@ -225,13 +251,14 @@ sl: updated_msg: Emotikon je uspešno posodobljen! upload: Pošlji dashboard: - backlog: Zaostala opravila + backlog: zaostala opravila config: Nastavitve feature_deletions: Brisanje računov - feature_invites: Poveza povabil - feature_profile_directory: Mapa profila + feature_invites: Povezave povabil + feature_profile_directory: Imenik profilov feature_registrations: Registracije feature_relay: Rele federacije + feature_timeline_preview: Predogled časovnice features: Zmožnosti hidden_service: Federacija s skritimi storitvami open_reports: odprte prijave @@ -241,7 +268,7 @@ sl: software: Programska oprema space: Uporaba prostora title: Nadzorna plošča - total_users: Skupaj uporabnikov + total_users: skupaj uporabnikov trends: Trendi week_interactions: interakcije ta teden week_users_active: aktivni ta teden @@ -251,12 +278,13 @@ sl: created_msg: Domenski blok se sedaj obdeluje destroyed_msg: Domenski blok je bil razveljavljen domain: Domena + existing_domain_block_html: Uvedli ste strožje omejitve za %{name}, sedaj ga morate najprej odblokirati. new: create: Ustvari blok hint: Domenski blok ne bo preprečil ustvarjanja vnosov računov v zbirko podatkov, ampak bo retroaktivno in samodejno uporabil posebne metode moderiranja na teh računih. severity: desc_html: "Utišaj bo vse objave računa naredil nevidne vsem, ki jih ne sledijo. Suspendiraj bo odstranil vso vsebino, medije in podatke profila računa. Uporabi nič, če želite le zavrniti predstavnostne datoteke." - noop: Nič + noop: Brez silence: Utišaj suspend: Suspendiraj title: Nov domenski blok @@ -271,13 +299,13 @@ sl: suspend: suspendirani show: affected_accounts: - few: "%{count} računov v bazi podatkov so prizadeti" + few: "%{count} računi v bazi podatkov so prizadeti" one: En račun v bazi podatkov je prizadet - other: "%{count} računov v bazi podatkov so prizadeti" - two: "%{count} računov v bazi podatkov so prizadeti" + other: "%{count} računov v bazi podatkov je prizadetih" + two: "%{count} računa v bazi podatkov so prizadeta" retroactive: silence: Prekliči utišanje za vse obstoječe račune iz te domene - suspend: Odsuspendiraj vse obstoječe račune iz te domene + suspend: Aktiviraj vse obstoječe račune iz te domene title: Razveljavi domenski blok za %{domain} undo: Razveljavi undo: Razveljavi domenski blok @@ -290,17 +318,18 @@ sl: new: create: Dodaj domeno title: Nov vnos e-pošte na črni seznam - title: Črni seznam e-pošte + title: Črni seznam e-pošt followers: back_to_account: Nazaj na račun title: Sledilci od %{acct} instances: + by_domain: Domena delivery_available: Na voljo je dostava known_accounts: - few: "%{count} znanih računov" + few: "%{count} znani računi" one: "%{count} znan račun" other: "%{count} znanih računov" - two: "%{count} znanih računov" + two: "%{count} znana računa" moderation: all: Vse limited: Omejeno @@ -317,15 +346,18 @@ sl: all: Vse available: Razpoložljivo expired: Potekel + title: Filter title: Povabila + pending_accounts: + title: "(%{count}) računov na čakanju" relays: add_new: Dodaj nov rele delete: Izbriši - description_html: "Rele federacije je posredniški strežnik, ki si izmenjuje velike količine javnih trobov med strežniki, ki so se naročili in objavili na njem. Majhnim in srednjim strežnikom lahko pomaga pri odkrivanju vsebine iz sistema fediverse, kar bi sicer zahtevalo, da lokalni uporabniki ročno sledijo druge osebe na oddaljenih strežnikih." + description_html: "Rele federacije je posredniški strežnik, ki si izmenjuje velike količine javnih tutov med strežniki, ki so se naročili in objavili na njem. Majhnim in srednjim strežnikom lahko pomaga pri odkrivanju vsebine iz sistema fediverse, kar bi sicer zahtevalo, da lokalni uporabniki ročno sledijo druge osebe na oddaljenih strežnikih." disable: Onemogoči disabled: Onemogočeno enable: Omogoči - enable_hint: Ko je omogočen, se bo vaš strežnik naročil na vse javne trobe iz tega releja in začel pošiljati javne trobe tega strežnika. + enable_hint: Ko je omogočen, se bo vaš strežnik naročil na vse javne tute iz tega releja in začel pošiljati javne tute tega strežnika. enabled: Omogočeno inbox_url: URL releja pending: Čakanje na odobritev releja @@ -345,7 +377,7 @@ sl: assign_to_self: Dodeli meni assigned: Dodeljen moderator comment: - none: Nič + none: Brez created_at: Prijavljeno mark_as_resolved: Označi kot rešeno mark_as_unresolved: Označi kot nerešeno @@ -359,19 +391,19 @@ sl: report: 'Prijavi #%{id}' reported_account: Prijavljeni račun reported_by: Prijavljen od - resolved: Razrešeno + resolved: Razrešeni resolved_msg: Prijava je uspešno razrešena! status: Stanje title: Prijave - unassign: Odstopi - unresolved: Nerešeno - updated_at: Posodobljen + unassign: Odstopljeni + unresolved: Nerešeni + updated_at: Posodobljeni settings: activity_api_enabled: desc_html: Številke lokalno objavljenih stanj, aktivnih uporabnikov in novih registracij na tedenskih seznamih title: Objavi združeno statistiko o dejavnosti uporabnikov bootstrap_timeline_accounts: - desc_html: Več uporabniških imen ločite z vejico. Deluje samo na lokalnih in odklenjenih računih. Privzeto, ko je prazno, pri vseh lokalnih skrbnikih. + desc_html: Več uporabniških imen ločite z vejico. Deluje samo na lokalnih in odklenjenih računih. Privzeto, ko je prazno, je pri vseh lokalnih skrbnikih. title: Privzeta sledenja za nove uporabnike contact_information: email: Poslovna e-pošta @@ -379,21 +411,382 @@ sl: custom_css: desc_html: Spremeni videz z naloženim CSS na vsaki strani title: CSS po meri + hero: + desc_html: Prikazano na sprednji strani. Priporoča se vsaj 600x100px. Ko ni nastavljen, se vrne na sličico strežnika + title: Slika junaka + mascot: + desc_html: Prikazano na več straneh. Priporočena je najmanj 293 × 205 px. Ko ni nastavljen, se vrne na privzeto maskoto + title: Slika maskote + peers_api_enabled: + desc_html: Domene, na katere je ta strežnik naletel na fediverse-u + title: Objavi seznam odkritih strežnikov + preview_sensitive_media: + desc_html: Predogledi povezav na drugih spletiščih bodo prikazali sličico, tudi če je medij označen kot občutljiv + title: Prikaži občutljive medije v predogledih OpenGraph + profile_directory: + desc_html: Dovoli uporabnikom, da jih lahko odkrijejo + title: Omogoči imenik profilov + registrations: + closed_message: + desc_html: Prikazano na prvi strani, ko so registracije zaprte. Lahko uporabite oznake HTML + title: Sporočilo o zaprti registraciji + deletion: + desc_html: Dovoli vsakomur, da izbriše svoj račun + title: Odpri brisanje računa + min_invite_role: + disabled: Nihče + title: Dovoli vabila od + registrations_mode: + modes: + approved: Potrebna je odobritev za prijavo + none: Nihče se ne more prijaviti + open: Vsakdo se lahko prijavi + title: Način registracije + show_known_fediverse_at_about_page: + desc_html: Ko preklopite, bo prikazal tute vseh znanih fediverse-ov v predogledu. V nasprotnem primeru bodo prikazani samo lokalni tuti. + title: Pokaži znane fediverse-e v predogledu časovnice + show_staff_badge: + desc_html: Prikaži značko osebja na uporabniški strani + title: Prikaži značko osebja + site_description: + desc_html: Uvodni odstavek na API-ju. Opišite, zakaj je ta Mastodon strežnik poseben in karkoli pomembnega. Lahko uporabite HTML oznake, zlasti <a> in <em>. + title: Opis strežnika + site_description_extended: + desc_html: Dober kraj za vaš kodeks ravnanja, pravila, smernice in druge stvari, ki ločujejo vaš strežnik. Lahko uporabite oznake HTML + title: Razširjene informacije po meri + site_short_description: + desc_html: Prikazano v stranski vrstici in metaoznakah. V enem odstavku opišite, kaj je Mastodon in kaj naredi ta strežnik poseben. + title: Kratek opis strežnika + site_terms: + desc_html: Lahko napišete svojo pravilnik o zasebnosti, pogoje storitve ali druge pravne dokumente. Lahko uporabite oznake HTML + title: Pogoji storitve po meri + site_title: Ime strežnika + thumbnail: + desc_html: Uporablja se za predogled prek OpenGrapha in API-ja. Priporočamo 1200x630px + title: Sličica strežnika + timeline_preview: + desc_html: Prikaži javno časovnico na ciljni strani + title: Predogled časovnice + title: Nastavitve strani + statuses: + back_to_account: Nazaj na stran računa + batch: + delete: Izbriši + nsfw_off: Označi, da ni občutljivo + nsfw_on: Označi, kot občutljivo + failed_to_execute: Ni bilo mogoče izvesti + media: + title: Mediji + no_media: Ni medijev + no_status_selected: Nobeno stanje ni bilo spremenjeno, ker ni bilo izbrano nobeno + title: Stanja računa + with_media: Z mediji + tags: + accounts: Računi + hidden: Skriti + hide: Skrij iz imenika + name: Ključnik + title: Ključniki + unhide: Prikaži v imeniku + visible: Vidni + title: Upravljanje + warning_presets: + add_new: Dodaj novo + delete: Izbriši + edit: Uredi + edit_preset: Uredi prednastavitev opozoril + title: Upravljaj prednastavitev opozoril + admin_mailer: + new_pending_account: + body: Podrobnosti o novem računu so navedene spodaj. To aplikacijo lahko odobrite ali zavrnete. + subject: Nov račun za pregled na %{instance} (%{username}) + new_report: + body: "%{reporter} je prijavil %{target}" + body_remote: Nekdo iz %{domain} je prijavil %{target} + subject: Nove prijave za %{instance} (#%{id}) + appearance: + advanced_web_interface: Napredni spletni vmesnik + advanced_web_interface_hint: 'Če želite uporabiti celotno širino zaslona, vam napredni spletni vmesnik omogoča, da si nastavite več različnih stolpcev in da si hkrati ogledate toliko informacij, kot želite: domačo stran, obvestila, združeno časovnico, poljubno število seznamov in ključnikov.' + animations_and_accessibility: Animacije in dostopnost + confirmation_dialogs: Potrditvena okna + sensitive_content: Občutljiva vsebina + application_mailer: + notification_preferences: Spremenite e-poštne nastavitve + salutation: "%{name}," + settings: 'Spremenite e-poštne nastavitve: %{link}' + view: 'Pogled:' + view_profile: Ogled profila + view_status: Ogled stanja + applications: + created: Aplikacija je bila uspešno ustvarjena + destroyed: Aplikacija je bila uspešno izbrisana + invalid_url: Navedeni URL je neveljaven + regenerate_token: Obnovite dostopni žeton + token_regenerated: Dostopni žeton je bil uspešno regeneriran + warning: Bodite zelo previdni s temi podatki. Nikoli jih ne delite z nikomer! + your_token: Vaš dostopni žeton + auth: + apply_for_account: Zahtevajte povabilo + change_password: Geslo + checkbox_agreement_html: Strinjam se s pravili strežnika in pogoji storitve + confirm_email: Potrdi e-pošto + delete_account: Izbriši račun + delete_account_html: Če želite izbrisati svoj račun, lahko nadaljujete tukaj. Prosili vas bomo za potrditev. + didnt_get_confirmation: Niste prejeli navodil za potrditev? + forgot_password: Ste pozabili svoje geslo? + invalid_reset_password_token: Žeton za ponastavitev gesla je neveljaven ali je potekel. Zahtevajte novo. + login: Prijava + logout: Odjava + migrate_account: Premakni se na drug račun + migrate_account_html: Če želite ta račun preusmeriti na drugega, ga lahko nastavite tukaj. + or_log_in_with: Ali se prijavite z + providers: + cas: CAS + saml: SAML + register: Vpis + registration_closed: "%{instance} ne sprejema novih članov" + resend_confirmation: Ponovno pošlji navodila za potrditev + reset_password: Ponastavi geslo + security: Varnost + set_new_password: Nastavi novo geslo + trouble_logging_in: Težave pri prijavi? + authorize_follow: + already_following: Temu računu že sledite + error: Na žalost je prišlo do napake pri iskanju oddaljenega računa + follow: Sledi + follow_request: 'Prošnjo za sledenje se poslali:' + following: 'Uspeh! Zdaj sledite:' + post_follow: + close: Lahko pa tudi zaprete to okno. + return: Prikaži uporabnikov profil + web: Pojdi na splet + title: Sledi %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}h" + about_x_months: "%{count}mo" + about_x_years: "%{count}y" + almost_x_years: "%{count}y" + half_a_minute: Pravkar + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Pravkar + over_x_years: "%{count}y" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mo" + x_seconds: "%{count}s" + deletes: + bad_password_msg: Lep poskus, hekerji! napačno geslo + confirm_password: Vnesite svoje trenutno geslo, da potrdite svojo identiteto + description_html: S tem boste trajno, nepovratno odstranili vsebino iz vašega računa in jo deaktivirali. Vaše uporabniško ime bo ostalo rezervirano za preprečevanje prihodnjih lažnih predstav. + proceed: Izbriši račun + success_msg: Vaš račun je bil uspešno izbrisan + warning_html: Zagotovljeno je samo brisanje vsebine iz tega strežnika. Vsebina, ki je široko razširjena, bo verjetno pustila sledi. Strežniki brez povezave in strežniki, ki so se odjavili od vaših posodobitev, ne bodo posodabljali svojih podatkovnih baz. + warning_title: Razširjena razpoložljivost vsebine + directories: + directory: Imenik profilov + enabled: Trenutno ste navedeni v imeniku. + enabled_but_waiting: Vključili ste, da ste navedeni v imeniku, vendar še nimate najmanjšega števila sledilcev (%{min_followers}), da bi vas prikazalo. + explanation: Odkrijte uporabnike glede na njihove interese + explore_mastodon: Razišči %{title} + how_to_enable: Trenutno niste vključeni v imenik. Spodaj se lahko vključite. Uporabite ključnike v vaši biografiji, da boste navedeni pod specifične ključnike! + people: + few: "%{count} osebe" + one: "%{count} oseba" + other: "%{count} oseb" + two: "%{count} osebi" errors: - '403': You don't have permission to view this page. - '404': The page you are looking for isn't here. - '410': The page you were looking for doesn't exist here anymore. - '422': - '429': Throttled - '500': + '403': Nimate dovoljenja za ogled te strani. + '404': Iskana stran ne obstaja. + '410': Iskana stran ne obstaja več. + '422': + content: Varnostno preverjanje ni uspelo. Ali blokirate piškotke? + title: Varnostno preverjanje je spodletelo + '429': Omejeno + '500': + content: Žal nam je, toda na našem koncu je prišlo do napake. + title: Ta stran ni pravilna + noscript_html: Če želite uporabljati spletno aplikacijo Mastodon, omogočite JavaScript. Druga možnost je, da za svojo platformo poskusite eno od lastnih aplikacij za Mastodon. + existing_username_validator: + not_found: s tem uporabniškim imenom ni bilo mogoče najti lokalnega uporabnika + not_found_multiple: ni bilo mogoče najti %{usernames} + exports: + archive_takeout: + date: Datum + download: Prenesi svoj arhiv + hint_html: Zahtevate lahko arhiv vaših tutov in naloženih medijev. Izvoženi podatki bodo v formatu ActivityPub, ki ga bo mogoče brati s katerokoli skladno programsko opremo. Arhiv lahko zahtevate vsakih 7 dni. + in_progress: Prevajanje arhiva... + request: Zahtevajte svoj arhiv + size: Velikost + blocks: Blokirate + csv: CSV + domain_blocks: Bloki domene + follows: Sledite + lists: Seznami + mutes: Utišate + storage: Shranjeni mediji + featured_tags: + add_new: Dodaj novo + errors: + limit: Ste že dodali največje število ključnikov + filters: + contexts: + home: Domača časovnica + notifications: Obvestila + public: Javne časovnice + thread: Pogovori + edit: + title: Uredite filter + errors: + invalid_context: Ne vsebuje nobenega ali vsebuje neveljaven kontekst + invalid_irreversible: Nepovratno filtriranje deluje le v kontekstu doma ali obvestil + index: + delete: Izbriši + title: Filtri + new: + title: Dodaj nov filter + footer: + developers: Razvijalci + more: Več… + resources: Viri + generic: + all: Vse + changes_saved_msg: Spremembe so uspešno shranjene! + copy: Kopiraj + order_by: Razvrsti po + save_changes: Shrani spremembe + validation_errors: + few: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napake + one: Nekaj še ni čisto v redu! Spodaj si oglejte napako + other: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napak + two: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napaki + html_validator: + invalid_markup: 'vsebuje neveljavno oznako HTML: %{error}' + identity_proofs: + active: Dejaven + authorize: Da, odobri + authorize_connection_prompt: Odobrite to kriptografsko povezavo? + errors: + failed: Kriptografska povezava ni uspela. Poskusite znova od %{provider}. + keybase: + invalid_token: Žetoni Keybase so algoritem podpisov in morajo biti sestavljeni iz 66 heksadecimalnih znakov + verification_failed: Keybase ne prepozna tega žetona kot podpis uporabnika %{kb_username}. Poskusite znova s Keybase-om. + wrong_user: Dokler se prijavite kot %{current}, ni mogoče ustvariti dokazila za %{proving}. Prijavite se kot %{proving} in poskusite znova. + explanation_html: Tukaj lahko kriptografsko povežete druge identitete, na primer profil Keybase. To omogoča drugim, da vam pošljejo šifrirana sporočila in zaupate vsebino, ki ste jo poslali. + i_am_html: Jaz sem %{username} na %{service}. + identity: Identiteta + inactive: Neaktiven + publicize_checkbox: 'In to tutnite:' + publicize_toot: 'Dokazano je! Jaz sem %{username} na %{service}: %{url}' + status: Stanje preverjanja + view_proof: Oglejte si dokaz + imports: + modes: + merge: Združi + merge_long: Ohrani obstoječe zapise in dodaj nove + overwrite: Prepiši + overwrite_long: Zamenjaj trenutne zapise z novimi + preface: Podatke, ki ste jih izvozili iz drugega strežnika, lahko uvozite. Na primer seznam oseb, ki jih spremljate ali blokirate. + success: Vaši podatki so bili uspešno naloženi in bodo zdaj pravočasno obdelani + types: + blocking: Seznam blokiranih + domain_blocking: Seznam blokiranih domen + following: Seznam uporabnikov, katerim sledite + muting: Seznam utišanih + upload: Pošlji + in_memoriam_html: V spomin. invites: + delete: Onemogoči + expired: Poteklo expires_in: - '1800': 30 minutes - '21600': 6 hours - '3600': 1 hour - '43200': 12 hours - '604800': 1 week - '86400': 1 day + '1800': 30 minut + '21600': 6 ur + '3600': 1 ura + '43200': 12 ur + '604800': 1 teden + '86400': 1 dan + expires_in_prompt: Nikoli + generate: Ustvari + invited_by: 'Povabil/a vas je:' + max_uses: + few: "%{count} uporabe" + one: 1 uporaba + other: "%{count} uporab" + two: "%{count} uporabi" + max_uses_prompt: Brez omejitve + prompt: Ustvarite in delite povezave z drugimi, da omogočite dostop do tega strežnika + table: + expires_at: Poteče + uses: Uporabe + title: Povabite ljudi + lists: + errors: + limit: Dosegli ste največje število seznamov + media_attachments: + validations: + images_and_video: Videoposnetka ni mogoče priložiti stanju, ki že vsebuje slike + too_many: Ni možno priložiti več kot 4 datoteke + migrations: + acct: username@domain novega računa + currently_redirecting: 'Vaš profil je preusmerjen na:' + proceed: Shrani + updated_msg: Nastavitev selitve računa je bila uspešno posodobljena! + moderation: + title: Moderiranje + notification_mailer: + digest: + action: Prikaži vsa obvestila + body: Tukaj je kratek povzetek sporočil, ki ste jih zamudili od vašega zadnjega obiska v %{since} + mention: "%{name} vas je omenil/a v:" + new_followers_summary: + few: Prav tako ste pridobili %{count} nove sledilce, ko ste bili odsotni! Juhu! + one: Prav tako ste pridobili enega novega sledilca, ko ste bili odsotni! Juhu! + other: Prav tako ste pridobili %{count} novih sledilcev, ko ste bili odsotni! Juhu! + two: Prav tako ste pridobili %{count} nova sledilca, ko ste bili odsotni! Juhu! + subject: + few: "%{count} nova obvestila od vašega zadnjega obiska \U0001F418" + one: "1 novo obvestilo od vašega zadnjega obiska \U0001F418" + other: "%{count} novih obvestil od vašega zadnjega obiska \U0001F418" + two: "%{count} novi obvestili od vašega zadnjega obiska \U0001F418" + title: V vaši odsotnosti... + favourite: + body: "%{name} je vzljubil/a vaše stanje:" + subject: "%{name} je vzljubil/a vaše stanje" + title: Novo priljubljeno + follow: + body: "%{name} vam sedaj sledi!" + subject: "%{name} vam sedaj sledi" + title: Novi sledilec + follow_request: + action: Upravljajte s prošnjami za sledenje + body: "%{name} vas je prosil/a za sledenje" + subject: 'Čakajoči sledilec/ka: %{name}' + title: Nova prošnja za sledenje + mention: + action: Odgovori + body: "%{name} vas je omenil/a v:" + subject: "%{name} vas je omenil/a" + title: Nova omemba + reblog: + body: "%{name} je spodbudil/a vaše stanje:" + subject: "%{name} je spodbudil/a vaše stanje" + title: Nova spodbuda + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T + pagination: + newer: Novejše + next: Naprej + older: Starejše + prev: Nazaj + truncate: "…" statuses: pin_errors: ownership: Trob nekoga drugega ne more biti pripet diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 6cab03332..cbe225646 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -423,12 +423,6 @@ sq: no_status_selected: S’u ndryshua ndonjë gjendje, ngaqë s’u përzgjodh ndonjë e tillë title: Gjendje llogarish with_media: Me media - subscriptions: - callback_url: URL Callback-u - confirmed: U ripohua - expires_in: Skadon më - last_delivery: Dorëzimi e fundit - topic: Temë tags: accounts: Llogari hidden: Fshehur @@ -686,10 +680,6 @@ sq: reply: proceed: Ripohoni përgjigjen prompt: 'Doni t’i përgjigjeni këtij mesazhi:' - remote_unfollow: - error: Gabim - title: Titull - unfollowed: U hoq ndjekja scheduled_statuses: over_daily_limit: Keni tejkaluar kufirin e %{limit} mesazheve të planifikuara për atë ditë over_total_limit: Keni tejkaluar kufirin prej %{limit} mesazhesh të planifikuara diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 3310716e0..6530d4c76 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -263,10 +263,6 @@ sr-Latn: no_media: Bez multimedije title: Statusi naloga with_media: Sa multimedijom - subscriptions: - confirmed: Potvrđeno - expires_in: Ističe za - last_delivery: Poslednja dostava title: Administracija admin_mailer: new_report: diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 1555fb235..88db0c4f4 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -440,10 +440,6 @@ sr: no_status_selected: Ниједан статус није промењен јер ниједан није изабран title: Статуси налога with_media: Са мултимедијом - subscriptions: - confirmed: Потврђено - expires_in: Истиче за - last_delivery: Последња достава tags: accounts: Налози hidden: Скривено @@ -693,10 +689,6 @@ sr: reply: proceed: Наставите да бисте одговорили prompt: 'Желите да одговорите на ову трубу:' - remote_unfollow: - error: Грешка - title: Наслов - unfollowed: Отпраћени scheduled_statuses: over_daily_limit: Прекорачили сте границу од %{limit} планираних труба за тај дан over_total_limit: Прекорачили сте границу од %{limit} планираних труба diff --git a/config/locales/sv.yml b/config/locales/sv.yml index d3d0cb888..c123e2889 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -313,12 +313,6 @@ sv: no_media: Ingen media title: Kontostatus with_media: med media - subscriptions: - callback_url: Återanrop URL - confirmed: Bekräftad - expires_in: Utgår om - last_delivery: Sista leverans - topic: Ämne admin_mailer: new_report: body: "%{reporter} har rapporterat %{target}" @@ -513,10 +507,6 @@ sv: missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto proceed: Fortsätt för att följa prompt: 'Du kommer att följa:' - remote_unfollow: - error: Fel - title: Titel - unfollowed: Slutade följa sessions: activity: Senaste aktivitet browser: Webbläsare diff --git a/config/locales/th.yml b/config/locales/th.yml index 7a16bc2f3..a009e4ebb 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -335,13 +335,6 @@ th: title: สื่อ no_media: ไม่มีสื่อ title: สถานะบัญชี - subscriptions: - callback_url: URL เรียกกลับ - confirmed: ยืนยันแล้ว - expires_in: หมดอายุภายใน - last_delivery: ส่งล่าสุด - title: WebSub - topic: หัวข้อ tags: accounts: บัญชี hidden: ซ่อนอยู่ @@ -360,6 +353,7 @@ th: appearance: advanced_web_interface: ส่วนติดต่อเว็บขั้นสูง animations_and_accessibility: ภาพเคลื่อนไหวและการช่วยการเข้าถึง + confirmation_dialogs: กล่องโต้ตอบการยืนยัน sensitive_content: เนื้อหาที่ละเอียดอ่อน application_mailer: notification_preferences: เปลี่ยนการกำหนดลักษณะอีเมล @@ -537,6 +531,7 @@ th: prev: ก่อนหน้า truncate: "…" preferences: + other: อื่น ๆ posting_defaults: ค่าเริ่มต้นการโพสต์ public_timelines: เส้นเวลาสาธารณะ relationships: @@ -562,10 +557,6 @@ th: reply: proceed: ดำเนินการต่อเพื่อตอบกลับ prompt: 'คุณต้องการตอบกลับโพสต์นี้:' - remote_unfollow: - error: ข้อผิดพลาด - title: ชื่อเรื่อง - unfollowed: เลิกติดตามแล้ว sessions: activity: กิจกรรมล่าสุด browser: เบราว์เซอร์ diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 3113e7a08..5929e1e07 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -207,12 +207,6 @@ tr: title: Sunucu hakkında detaylı bilgi site_title: Site başlığı title: Site Ayarları - subscriptions: - callback_url: Callback linki - confirmed: Onaylandı - expires_in: Bitiş Tarihi - last_delivery: Son gönderim - topic: Konu tags: accounts: Hesaplar name: Etiketler diff --git a/config/locales/uk.yml b/config/locales/uk.yml index e027b6bae..c2d422474 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -349,11 +349,6 @@ uk: no_media: Немає медіа title: Статуси аккаунтів with_media: З медіа - subscriptions: - confirmed: Підтверджено - expires_in: Спливає через - last_delivery: Остання доставка - topic: Тема title: Адміністрування admin_mailer: new_report: @@ -569,10 +564,6 @@ uk: no_account_html: Не маєте аккаунту? Не біда, ви можете зареєструватися proceed: Перейти до підписки prompt: 'Ви хочете підписатися на:' - remote_unfollow: - error: Помилка - title: Заголовок - unfollowed: Відписані sessions: activity: Остання активність browser: Браузер diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 0c9b291ad..42ab59d50 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -10,7 +10,7 @@ zh-CN: api: API apps: 移动应用 apps_platforms: 在 iOS、Android 和其他平台上使用 Mastodon - browse_directory: 浏览用户资料目录并按兴趣筛选 + browse_directory: 浏览用户目录并按兴趣筛选 browse_public_posts: 浏览 Mastodon 上公共嘟文的实时信息流 contact: 联系方式 contact_missing: 未设定 @@ -32,7 +32,7 @@ zh-CN: status_count_after: other: 条嘟文 status_count_before: 他们共嘟出了 - tagline: 关注朋友并发现新朋友 + tagline: 关注并发现新朋友 terms: 使用条款 user_count_after: other: 位用户 @@ -243,9 +243,9 @@ zh-CN: config: 服务器配置 feature_deletions: 帐户删除 feature_invites: 邀请链接 - feature_profile_directory: 用户资料目录 + feature_profile_directory: 用户目录 feature_registrations: 公开注册 - feature_relay: 同步中继 + feature_relay: 中继服务器 feature_timeline_preview: 时间轴预览 features: 功能 hidden_service: 匿名服务连通性 @@ -333,9 +333,9 @@ zh-CN: pending_accounts: title: 待处理的帐户 (%{count}) relays: - add_new: 添加新的中继 + add_new: 订阅新的中继 delete: 删除 - description_html: "同步中继是一种中间服务器,各实例可以通过订阅中继和向中继推送信息的方式来大量交换公开嘟文。它可以帮助中小型实例发现网络中的内容,而无需本地用户手动关注其他远程实例上的用户。" + description_html: "中继服务器是一个信息统合服务器,各服务器可以通过订阅中继服务器和向中继服务器推送信息来交换大量公开嘟文。它可以帮助中小型服务器发现联邦宇宙中的其他服务器的内容,而无需本站用户手动关注其他远程服务器上的用户。" disable: 禁用 disabled: 已禁用 enable: 启用 @@ -400,14 +400,14 @@ zh-CN: desc_html: 用于在首页展示。推荐分辨率 293×205px 以上。未指定的情况下将使用默认吉祥物。 title: 吉祥物图像 peers_api_enabled: - desc_html: 截至目前本实例在网络中已发现的域名 + desc_html: 截至目前本服务器在联邦宇宙中已发现的域名 title: 公开已知实例的列表 preview_sensitive_media: desc_html: 始终在站外链接预览中展示缩略图,无论媒体内容是否标记为敏感 title: 在 OpenGraph 预览中显示敏感媒体内容 profile_directory: - desc_html: 允许用户可被发现 - title: 启用用户资料目录 + desc_html: 允许用户被发现 + title: 启用用户目录 registrations: closed_message: desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签 @@ -463,20 +463,13 @@ zh-CN: no_status_selected: 因为没有嘟文被选中,所以没有更改 title: 帐户嘟文 with_media: 含有媒体文件 - subscriptions: - callback_url: 回调 URL - confirmed: 已确认 - expires_in: 失效时间 - last_delivery: 最后一次接收数据的时间 - title: WebSub - topic: 话题 tags: accounts: 帐户 hidden: 隐藏 - hide: 从目录隐藏 + hide: 从用户目录中隐藏 name: 话题标签 title: 话题标签 - unhide: 在目录中显示 + unhide: 在用户目录中显示 visible: 可见 title: 管理 warning_presets: @@ -573,12 +566,12 @@ zh-CN: warning_html: 我们只能保证本服务器上的内容将会被彻底删除。对于已经被广泛传播的内容,它们在本服务器以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器所存储的数据亦无法删除。 warning_title: 关于已传播的内容的警告 directories: - directory: 用户资料目录 - enabled: 您目前已被列入目录中。 - enabled_but_waiting: 您已选择列入目录,但是您没有达到关注者数量下限 (%{min_followers} 名) 。 + directory: 用户目录 + enabled: 您已被收录在用户目录中。 + enabled_but_waiting: 你已选择将账号收录到用户目录中,但是你的关注者不足 (%{min_followers}) 人 。 explanation: 根据兴趣发现用户 explore_mastodon: 探索 %{title} - how_to_enable: 您目前没有选择选择列入到目录中。您可以在下面选择列入。可以在个人简介中加上话题标签,话题标签也会显示在用户资料目录里! + how_to_enable: 您目前没有被收录到用户目录中。您可以在下面选择收录。在个人简介中加上话题标签后,话题标签也会显示在用户目录上! people: other: "%{count} 人" errors: @@ -748,6 +741,7 @@ zh-CN: number: human: decimal_units: + format: "%n%u" units: billion: B million: M @@ -759,6 +753,7 @@ zh-CN: next: 下一页 older: 更早 prev: 上一页 + truncate: "…" polls: errors: already_voted: 你已经在这里投过票了 @@ -803,10 +798,6 @@ zh-CN: reply: proceed: 确认回复 prompt: 您想要回复此嘟文: - remote_unfollow: - error: 错误 - title: 标题 - unfollowed: 已取消关注 scheduled_statuses: over_daily_limit: 您已超出每日定时嘟文的上限(%{limit} 条) over_total_limit: 您已超出定时嘟文的上限(%{limit} 条) @@ -870,7 +861,7 @@ zh-CN: notifications: 通知 preferences: 首选项 profile: 个人资料 - relationships: 正在关注以及关注者 + relationships: 关注管理 two_factor_authentication: 双重认证 statuses: attached: @@ -910,6 +901,87 @@ zh-CN: reblogged: 转嘟 sensitive_content: 敏感内容 terms: + body_html: | +

    隐私政策

    +

    我们收集什么信息?

    + +
      +
    • 基本帐户信息:如果您在此服务器上注册,可能会要求您输入用户名,电子邮件地址和密码。 您还可以输入其他个人资料信息,例如显示名称和传记,并上传个人资料照片和标题图像。 用户名,显示名称,传记,个人资料图片和标题图片始终公开列出。
    • +
    • 帖子,关注和其他公共信息: 您关注的人员列表会公开列出,您的粉丝也是如此。 提交邮件时,会存储日期和时间以及您提交邮件的应用程序。 消息可能包含媒体附件,例如图片和视频。 公开和非上市帖子可公开获取。 当您在个人资料中添加帖子时,这也是公开信息。 您的帖子会发送给您的关注者,在某些情况下,这意味着他们会将其发送到不同的服务器,并将副本存储在那里。 当您删除帖子时,同样会将其发送给您的关注者。 重新记录或赞成其他职位的行为始终是公开的。
    • +
    • 直接和关注者的帖子: 所有帖子都在服务器上存储和处理。 仅限关注者的帖子会发送给您的关注者和用户,并且直接帖子仅会发送给他们中提到的用户。 在某些情况下,这意味着它们被传送到不同的服务器并且副本存储在那里。 我们善意努力限制只有授权人员访问这些帖子,但其他服务器可能无法这样做。 因此,查看您的关注者所属的服务器非常重要。 您可以在设置中切换选项以手动批准和拒绝新关注者。 请记住,服务器和任何接收服务器的操作员可能会查看此类消息, 并且收件人可以截图,复制或以其他方式重新共享它们。 不要在 Mastodon 上分享任何危险信息。
    • +
    • IP和其他元数据: 登录时,我们会记录您登录的IP地址以及浏览器应用程序的名称。 所有登录的会话都可供您在设置中查看和撤销。 使用的最新IP地址最长可存储12个月。 我们还可以保留服务器日志,其中包括我们服务器的每个请求的IP地址。
    • +
    + +
    + +

    我们将您的信息用于什么?

    + +

    我们向您收集的任何信息均可通过以下方式使用:

    + +
      +
    • 提供Mastodon的核心功能。 您只能在登录时与其他人的内容进行互动并发布您自己的内容。例如,您可以关注其他人在您自己的个性化家庭时间轴中查看他们的组合帖子。
    • +
    • 为了帮助社区适度,例如将您的IP地址与其他已知的IP地址进行比较,以确定禁止逃税或其他违规行为。
    • +
    • 您提供的电子邮件地址可能用于向您发送信息,有关其他人与您的内容交互或向您发送消息的通知,以及回复查询和/或其他请求或问题。
    • +
    + +
    + +

    我们如何保护您的信息?

    + +

    当您输入,提交或访问您的个人信息时,我们会实施各种安全措施以维护您的个人信息的安全。 除此之外,您的浏览器会话以及应用程序和API之间的流量都使用SSL进行保护,您的密码使用强大的单向算法进行哈希处理。 您可以启用双因素身份验证,以进一步保护对您帐户的访问。

    + +
    + +

    我们的数据保留政策是什么?

    + +

    我们真诚的努力:

    + +
      +
    • 保留包含此服务器的所有请求的IP地址的服务器日志,只要保留此类日志,不超过90天。
    • +
    • 保留与注册用户关联的IP地址不超过12个月。
    • +
    + +

    您可以请求并下载我们内容的存档,包括您的帖子,媒体附件,个人资料图片和标题图片。

    + +

    您可以随时不可逆转地删除您的帐户。

    + +
    + +

    我们使用 cookies 吗?

    + +

    是。 Cookie是网站或其服务提供商通过Web浏览器传输到计算机硬盘的小文件(如果允许)。 这些cookie使网站能够识别您的浏览器,如果您有注册帐户,则将其与您的注册帐户相关联。

    + +

    我们使用Cookie来了解并保存您对未来访问的偏好。

    + +
    + +

    我们是否透露任何信息给其他方?

    + +

    我们不会将您的个人身份信息出售,交易或以其他方式转让给外方。 这不包括协助我们操作我们的网站,开展业务或为您服务的受信任的第三方,只要这些方同意保密这些信息。 当我们认为发布适合遵守法律,执行我们的网站政策或保护我们或他人的权利,财产或安全时,我们也可能会发布您的信息。

    + +

    您的公共内容可能会被网络中的其他服务器下载。 您的公开帖子和关注者帖子会发送到关注者所在的服务器,并且直接邮件会传递到收件人的服务器,只要这些关注者或收件人位于与此不同的服务器上。

    + +

    当您授权应用程序使用您的帐户时,根据您批准的权限范围,它可能会访问您的公开个人资料信息,以下列表,您的关注者,您的列表,所有帖子和您的收藏夹。 应用程序永远不能访问您的电子邮件地址或密码。

    + +
    + +

    儿童使用网站

    + +

    如果此服务器位于欧盟或欧洲经济区:我们的网站,产品和服务都是针对至少16岁的人。 如果您未满16岁,则符合GDPR的要求(General Data Protection Regulation) 不要使用这个网站。

    + +

    如果此服务器位于美国:我们的网站,产品和服务均面向至少13岁的人。 如果您未满13岁,则符合COPPA的要求 (Children's Online Privacy Protection Act) 不要使用这个网站。

    + +

    如果此服务器位于另一个辖区,则法律要求可能不同。

    + +
    + +

    我们隐私政策的变更

    + +

    如果我们决定更改我们的隐私政策,我们会在此页面上发布这些更改。

    + +

    本文件为CC-BY-SA。 它最后更新于2018年3月7日。

    + +

    最初改编自 Discourse 隐私政策.

    title: "%{instance} 使用条款和隐私权政策" themes: contrast: Mastodon(高对比度) @@ -945,7 +1017,7 @@ zh-CN: disable: 虽然您的帐户被冻结,您的帐户数据仍然完整;但是您无法在解锁前执行任何操作。 silence: 当您的帐户受限时,只有已经关注过你的人才会这台服务器上看到你的嘟文,并且您会被排除在各种公共列表之外。但是,其他人仍然可以手动关注你。 suspend: 您的帐户已被封禁,所有的嘟文和您上传的媒体文件都已经从该服务器和您的关注者的服务器上删除并且不可恢复。 - review_server_policies: 审阅服务器条款 + review_server_policies: 查看服务器政策 subject: disable: 您的帐户 %{acct} 已被冻结 none: 对 %{acct} 的警告 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 25e7475a8..520771da4 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -327,13 +327,6 @@ zh-HK: no_media: 不含媒體檔案 title: 帳戶文章 with_media: 含有媒體檔案 - subscriptions: - callback_url: 回傳 URL - confirmed: 確定 - expires_in: 期限 - last_delivery: 資料最後送抵時間 - title: PuSH 訂閱 - topic: 所訂閱資源 title: 管理 admin_mailer: new_report: @@ -524,10 +517,6 @@ zh-HK: missing_resource: 無法找到你用戶的轉接網址 proceed: 下一步 prompt: 你希望關注︰ - remote_unfollow: - error: 錯誤 - title: 標題 - unfollowed: 取消關注 sessions: activity: 最近活動 browser: 瀏覽器 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index d3dcf5133..801ea7cea 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -401,13 +401,6 @@ zh-TW: no_media: 不含媒體檔案 title: 帳戶嘟文 with_media: 含有媒體檔案 - subscriptions: - callback_url: 回傳網址 - confirmed: 已確認 - expires_in: 期限 - last_delivery: 最後遞送 - title: WebSub 訂閱 - topic: 主題 title: 管理介面 admin_mailer: new_report: @@ -587,10 +580,6 @@ zh-TW: missing_resource: 無法找到資源 proceed: 下一步 prompt: '您希望關注:' - remote_unfollow: - error: 錯誤 - title: 標題 - unfollowed: 取消關注 sessions: activity: 最近活動 browser: 瀏覽器 -- cgit From b3f44aa186487b4f93f3a457607870c90caaf5df Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 07:50:14 +0200 Subject: Add periodic removal of older thumbnails for preview cards (#11304) --- app/workers/maintenance/uncache_preview_worker.rb | 18 ++++++++++++++++++ .../scheduler/preview_cards_cleanup_scheduler.rb | 22 ++++++++++++++++++++++ config/sidekiq.yml | 3 +++ 3 files changed, 43 insertions(+) create mode 100644 app/workers/maintenance/uncache_preview_worker.rb create mode 100644 app/workers/scheduler/preview_cards_cleanup_scheduler.rb (limited to 'config') diff --git a/app/workers/maintenance/uncache_preview_worker.rb b/app/workers/maintenance/uncache_preview_worker.rb new file mode 100644 index 000000000..810ffd8cc --- /dev/null +++ b/app/workers/maintenance/uncache_preview_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Maintenance::UncachePreviewWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(preview_card_id) + preview_card = PreviewCard.find(preview_card_id) + + return if preview_card.image.blank? + + preview_card.image.destroy + preview_card.save + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/scheduler/preview_cards_cleanup_scheduler.rb b/app/workers/scheduler/preview_cards_cleanup_scheduler.rb new file mode 100644 index 000000000..2b38792f0 --- /dev/null +++ b/app/workers/scheduler/preview_cards_cleanup_scheduler.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Scheduler::PreviewCardsCleanupScheduler + include Sidekiq::Worker + + sidekiq_options unique: :until_executed, retry: 0 + + def perform + Maintenance::UncachePreviewWorker.push_bulk(recent_link_preview_cards.pluck(:id)) + Maintenance::UncachePreviewWorker.push_bulk(older_preview_cards.pluck(:id)) + end + + private + + def recent_link_preview_cards + PreviewCard.where(type: :link).where('updated_at < ?', 1.month.ago) + end + + def older_preview_cards + PreviewCard.where('updated_at < ?', 6.months.ago) + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index a16dea967..5c652792c 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -27,6 +27,9 @@ ip_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler + preview_cards_cleanup_scheduler: + cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' + class: Scheduler::PreviewCardsCleanupScheduler email_scheduler: cron: '0 10 * * 2' class: Scheduler::EmailScheduler -- cgit From 5b4a9345caea11be1a6cb8ecfb5cfb6173392264 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 15 Jul 2019 23:10:24 +0900 Subject: Fix consistent interpolations (#11317) --- config/locales/sk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 5577f9394..5b0bbdef4 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -301,7 +301,7 @@ sk: affected_accounts: few: "%{count} účtov v databázi ovplyvnených" many: "%{count} účtov v databázi ovplyvnených" - one: "%{count} účet v databázi ovplyvnený" + one: 1 účet v databázi ovplyvnený other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény -- cgit From 9b1d3e4acb8190509d98ee4ef2ab9b24b45362ee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 16 Jul 2019 06:30:47 +0200 Subject: Add option to disable real-time updates in web UI (#9984) Fix #9031 Fix #7913 --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/actions/notifications.js | 30 ++++++++++++--- app/javascript/mastodon/actions/timelines.js | 43 +++++++++++++++------- app/javascript/mastodon/compare_id.js | 5 ++- app/javascript/mastodon/components/load_pending.js | 22 +++++++++++ .../mastodon/components/scrollable_list.js | 13 ++++++- .../components/column_settings.js | 2 +- .../notifications/components/setting_toggle.js | 5 ++- .../mastodon/features/notifications/index.js | 12 +++++- .../ui/containers/status_list_container.js | 5 ++- app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/reducers/notifications.js | 30 ++++++++++----- app/javascript/mastodon/reducers/settings.js | 6 --- app/javascript/mastodon/reducers/timelines.js | 40 +++++++++++++------- app/lib/user_settings_decorator.rb | 5 +++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 23 ++++++------ .../settings/preferences/appearance/show.html.haml | 3 ++ config/locales/simple_form.en.yml | 2 + config/settings.yml | 1 + 20 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 app/javascript/mastodon/components/load_pending.js (limited to 'config') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 0a5c14cca..742c97cdb 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -55,6 +55,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_show_application, :setting_advanced_layout, :setting_use_blurhash, + :setting_use_pending_items, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 56c952cb0..d92d972bc 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFiltersRegex } from '../selectors'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import compareId from 'mastodon/compare_id'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -22,8 +24,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -38,6 +41,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -122,10 +130,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -134,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -151,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + usePendingItems, skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 06c21b96b..7eeba2aa7 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,8 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; -import api, { getLinks } from '../api'; +import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'mastodon/compare_id'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { return dispatch => { @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) { type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, }); }; }; @@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -115,7 +130,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -151,9 +167,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/mastodon/compare_id.js index aaff66481..66cf51c4b 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/mastodon/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/mastodon/components/load_pending.js b/app/javascript/mastodon/components/load_pending.js new file mode 100644 index 000000000..7e2702403 --- /dev/null +++ b/app/javascript/mastodon/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 0376cf85a..553c16352 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, @@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; + const loadPending = (numPending > 0) ? : null; let scrollableArea = null; if (showLoading) { @@ -251,6 +260,8 @@ export default class ScrollableList extends PureComponent {
    {prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => (
    - } /> + } />
    ); diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 7aec16d2e..e6f593ef8 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent { settingPath: PropTypes.array.isRequired, label: PropTypes.node.isRequired, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label } = this.props; + const { prefix, settings, settingPath, label, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return (
    - +
    ); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 006c45657..df4ad6f2a 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; +import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -41,6 +41,7 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, }); export default @connect(mapStateToProps) @@ -58,6 +59,7 @@ class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, }; static defaultProps = { @@ -80,6 +82,10 @@ class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -136,7 +142,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; const pinned = !!columnId; const emptyMessage = ; @@ -178,8 +184,10 @@ class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 3df5b7bea..7b8eb652b 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { scrollTopTimeline } from '../../../actions/timelines'; +import { scrollTopTimeline, loadPending } from '../../../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; @@ -37,6 +37,7 @@ const makeMapStateToProps = () => { isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size, }); return mapStateToProps; @@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index f0f131ff5..cb2ccc7c4 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -21,5 +21,6 @@ export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); export default initialState; diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 4d9604de9..e94a4946b 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -6,6 +6,7 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -16,6 +17,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from '../compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, @@ -31,7 +33,11 @@ const notificationToMap = notification => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { + if (usePendingItems) { + return state.update('pendingItems', list => list.unshift(notificationToMap(notification))); + } + const top = state.get('top'); if (!top) { @@ -47,7 +53,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { let items = ImmutableList(); notifications.forEach((n, i) => { @@ -56,7 +62,7 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -78,7 +84,8 @@ const expandNormalizedNotifications = (state, notifications, next) => { }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); + const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id); + return state.update('items', helper).update('pendingItems', helper); }; const updateTop = (state, top) => { @@ -90,34 +97,37 @@ const updateTop = (state, top) => { }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + return state.update('items', helper).update('pendingItems', helper); }; export default function notifications(state = initialState, action) { switch(action.type) { + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('hasMore', true); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case ACCOUNT_MUTE_SUCCESS: return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index a0eea137f..033bfc999 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -10,8 +10,6 @@ import uuid from '../uuid'; const initialState = ImmutableMap({ saved: true, - onboarded: false, - skinTone: 1, home: ImmutableMap({ @@ -74,10 +72,6 @@ const initialState = ImmutableMap({ body: '', }), }), - - trends: ImmutableMap({ - show: true, - }), }); const defaultColumns = fromJS([ diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 309a95a19..0b036f5fe 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from '../actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; @@ -57,7 +59,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { +const updateTimeline = (state, timeline, status, usePendingItems) => { + if (usePendingItems) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + } + const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -78,8 +88,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -109,11 +121,10 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { @@ -124,14 +135,17 @@ const updateTop = (state, timeline, top) => { export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -149,7 +163,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index aaf95cc19..9ae9986c2 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -35,6 +35,7 @@ class UserSettingsDecorator user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') + user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') end def merged_notification_emails @@ -117,6 +118,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_use_blurhash' end + def use_pending_items_preference + boolean_cast_setting 'setting_use_pending_items' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 3a4b415dd..31c99630c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false + :advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 54f99d570..7e5d3eda9 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -23,17 +23,18 @@ class InitialStateSerializer < ActiveModel::Serializer } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:display_media] = object.current_account.user.setting_display_media - store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers - store[:reduce_motion] = object.current_account.user.setting_reduce_motion - store[:advanced_layout] = object.current_account.user.setting_advanced_layout - store[:use_blurhash] = object.current_account.user.setting_use_blurhash - store[:is_staff] = object.current_account.user.staff? + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_media] = object.current_account.user.setting_display_media + store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:advanced_layout] = object.current_account.user.setting_advanced_layout + store[:use_blurhash] = object.current_account.user.setting_use_blurhash + store[:use_pending_items] = object.current_account.user.setting_use_pending_items + store[:is_staff] = object.current_account.user.staff? end store diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index c13882801..e279a61c4 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -17,6 +17,9 @@ %h4= t 'appearance.animations_and_accessibility' + .fields-group + = f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 75cbec9de..12a7ec2b3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -35,6 +35,7 @@ en: setting_noindex: Affects your public profile and status pages setting_show_application: The application you use to toot will be displayed in the detailed view of your toots setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details + setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word featured_tag: @@ -111,6 +112,7 @@ en: setting_theme: Site theme setting_unfollow_modal: Show confirmation dialog before unfollowing someone setting_use_blurhash: Show colorful gradients for hidden media + setting_use_pending_items: Slow mode severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index ad71b6008..75cb2dc85 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -33,6 +33,7 @@ defaults: &defaults aggregate_reblogs: true advanced_layout: false use_blurhash: true + use_pending_items: false notification_emails: follow: false reblog: false -- cgit From b4c9a860e5c349a9abdfbdc9b76f338e70fc89cc Mon Sep 17 00:00:00 2001 From: Daigo 3 Dango Date: Mon, 15 Jul 2019 18:51:36 -1000 Subject: Make puma bind address configurable with BIND env var (#11326) --- config/puma.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/puma.rb b/config/puma.rb index 25a5534b2..6a96867d5 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,7 +4,7 @@ threads threads_count, threads_count if ENV['SOCKET'] bind "unix://#{ENV['SOCKET']}" else - bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" + bind "tcp://#{ENV.fetch('BIND', '127.0.0.1')}:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } -- cgit From be053560e98dc9b702ee347518994e0b07914fae Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 05:56:35 +0200 Subject: Change default interface of web and streaming from 0.0.0.0 to 127.0.0.1 (#11302) --- config/puma.rb | 4 ++-- docker-compose.yml | 2 +- streaming/index.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'config') diff --git a/config/puma.rb b/config/puma.rb index 1afdb1c6d..25a5534b2 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -2,9 +2,9 @@ threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count if ENV['SOCKET'] - bind 'unix://' + ENV['SOCKET'] + bind "unix://#{ENV['SOCKET']}" else - port ENV.fetch('PORT') { 3000 } + bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } diff --git a/docker-compose.yml b/docker-compose.yml index 93d47f1a0..f3fe6cfd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: yarn start + command: BIND=0.0.0.0 node ./streaming networks: - external_network - internal_network diff --git a/streaming/index.js b/streaming/index.js index b016c45f1..12e4e3ab1 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -678,7 +678,7 @@ const attachServerWithConfig = (server, onSuccess) => { } }); } else { - server.listen(+process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => { + server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => { if (onSuccess) { onSuccess(`${server.address().address}:${server.address().port}`); } -- cgit From 729723f857d11434c0f78d63fe16537d77f1c77c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 16 Jul 2019 06:30:47 +0200 Subject: Add option to disable real-time updates in web UI (#9984) Fix #9031 Fix #7913 --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/actions/notifications.js | 30 ++++++++++++--- app/javascript/mastodon/actions/timelines.js | 43 +++++++++++++++------- app/javascript/mastodon/compare_id.js | 5 ++- app/javascript/mastodon/components/load_pending.js | 22 +++++++++++ .../mastodon/components/scrollable_list.js | 13 ++++++- .../components/column_settings.js | 2 +- .../notifications/components/setting_toggle.js | 5 ++- .../mastodon/features/notifications/index.js | 12 +++++- .../ui/containers/status_list_container.js | 5 ++- app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/reducers/notifications.js | 30 ++++++++++----- app/javascript/mastodon/reducers/settings.js | 6 --- app/javascript/mastodon/reducers/timelines.js | 40 +++++++++++++------- app/lib/user_settings_decorator.rb | 5 +++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 25 +++++++------ .../settings/preferences/appearance/show.html.haml | 3 ++ config/locales/simple_form.en.yml | 2 + config/settings.yml | 1 + 20 files changed, 182 insertions(+), 71 deletions(-) create mode 100644 app/javascript/mastodon/components/load_pending.js (limited to 'config') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 451742d41..372f253cb 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -53,6 +53,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_advanced_layout, :setting_default_content_type, :setting_use_blurhash, + :setting_use_pending_items, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 56c952cb0..d92d972bc 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFiltersRegex } from '../selectors'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import compareId from 'mastodon/compare_id'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -22,8 +24,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -38,6 +41,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -122,10 +130,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -134,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -151,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + usePendingItems, skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 06c21b96b..7eeba2aa7 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,8 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; -import api, { getLinks } from '../api'; +import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'mastodon/compare_id'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { return dispatch => { @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) { type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, }); }; }; @@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -115,7 +130,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -151,9 +167,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/mastodon/compare_id.js index aaff66481..66cf51c4b 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/mastodon/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/mastodon/components/load_pending.js b/app/javascript/mastodon/components/load_pending.js new file mode 100644 index 000000000..7e2702403 --- /dev/null +++ b/app/javascript/mastodon/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 0376cf85a..553c16352 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, @@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; + const loadPending = (numPending > 0) ? : null; let scrollableArea = null; if (showLoading) { @@ -251,6 +260,8 @@ export default class ScrollableList extends PureComponent {
    {prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => (
    - } /> + } />
    ); diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 7aec16d2e..e6f593ef8 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent { settingPath: PropTypes.array.isRequired, label: PropTypes.node.isRequired, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label } = this.props; + const { prefix, settings, settingPath, label, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return (
    - +
    ); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 006c45657..df4ad6f2a 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; +import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -41,6 +41,7 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, }); export default @connect(mapStateToProps) @@ -58,6 +59,7 @@ class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, }; static defaultProps = { @@ -80,6 +82,10 @@ class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -136,7 +142,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; const pinned = !!columnId; const emptyMessage = ; @@ -178,8 +184,10 @@ class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 3df5b7bea..7b8eb652b 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { scrollTopTimeline } from '../../../actions/timelines'; +import { scrollTopTimeline, loadPending } from '../../../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; @@ -37,6 +37,7 @@ const makeMapStateToProps = () => { isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size, }); return mapStateToProps; @@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 7df2a90bc..3c3c80e99 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -22,5 +22,6 @@ export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); export default initialState; diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 4d9604de9..e94a4946b 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -6,6 +6,7 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -16,6 +17,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from '../compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, @@ -31,7 +33,11 @@ const notificationToMap = notification => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { + if (usePendingItems) { + return state.update('pendingItems', list => list.unshift(notificationToMap(notification))); + } + const top = state.get('top'); if (!top) { @@ -47,7 +53,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { let items = ImmutableList(); notifications.forEach((n, i) => { @@ -56,7 +62,7 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -78,7 +84,8 @@ const expandNormalizedNotifications = (state, notifications, next) => { }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); + const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id); + return state.update('items', helper).update('pendingItems', helper); }; const updateTop = (state, top) => { @@ -90,34 +97,37 @@ const updateTop = (state, top) => { }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + return state.update('items', helper).update('pendingItems', helper); }; export default function notifications(state = initialState, action) { switch(action.type) { + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('hasMore', true); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case ACCOUNT_MUTE_SUCCESS: return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index a0eea137f..033bfc999 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -10,8 +10,6 @@ import uuid from '../uuid'; const initialState = ImmutableMap({ saved: true, - onboarded: false, - skinTone: 1, home: ImmutableMap({ @@ -74,10 +72,6 @@ const initialState = ImmutableMap({ body: '', }), }), - - trends: ImmutableMap({ - show: true, - }), }); const defaultColumns = fromJS([ diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 309a95a19..0b036f5fe 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from '../actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; @@ -57,7 +59,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { +const updateTimeline = (state, timeline, status, usePendingItems) => { + if (usePendingItems) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + } + const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -78,8 +88,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -109,11 +121,10 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { @@ -124,14 +135,17 @@ const updateTop = (state, timeline, top) => { export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -149,7 +163,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index ac35fd005..51d8c0970 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -39,6 +39,7 @@ class UserSettingsDecorator user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type') user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') + user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') end def merged_notification_emails @@ -137,6 +138,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_use_blurhash' end + def use_pending_items_preference + boolean_cast_setting 'setting_use_pending_items' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 9bc3dd608..bb903a45c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, :default_content_type, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false + :advanced_layout, :default_content_type, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index e3e2775fb..e22059182 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -37,18 +37,19 @@ class InitialStateSerializer < ActiveModel::Serializer } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:favourite_modal] = object.current_account.user.setting_favourite_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:display_media] = object.current_account.user.setting_display_media - store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers - store[:reduce_motion] = object.current_account.user.setting_reduce_motion - store[:advanced_layout] = object.current_account.user.setting_advanced_layout - store[:use_blurhash] = object.current_account.user.setting_use_blurhash - store[:is_staff] = object.current_account.user.staff? + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:favourite_modal] = object.current_account.user.setting_favourite_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_media] = object.current_account.user.setting_display_media + store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:advanced_layout] = object.current_account.user.setting_advanced_layout + store[:use_blurhash] = object.current_account.user.setting_use_blurhash + store[:use_pending_items] = object.current_account.user.setting_use_pending_items + store[:is_staff] = object.current_account.user.staff? store[:default_content_type] = object.current_account.user.setting_default_content_type end diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 1709c9c84..447958253 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -14,6 +14,9 @@ %h4= t 'appearance.animations_and_accessibility' + .fields-group + = f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index b35b9b6ec..3db07cb8b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -40,6 +40,7 @@ en: setting_show_application: The application you use to toot will be displayed in the detailed view of your toots setting_skin: Reskins the selected Mastodon flavour setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details + setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word featured_tag: @@ -122,6 +123,7 @@ en: setting_system_font_ui: Use system's default font setting_unfollow_modal: Show confirmation dialog before unfollowing someone setting_use_blurhash: Show colorful gradients for hidden media + setting_use_pending_items: Slow mode severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index 07e24f8e2..bc97f650b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -37,6 +37,7 @@ defaults: &defaults aggregate_reblogs: true advanced_layout: false use_blurhash: true + use_pending_items: false notification_emails: follow: false reblog: false -- cgit From c8a47595fb4a54af6a50a82bf0dbae4950586264 Mon Sep 17 00:00:00 2001 From: Daigo 3 Dango Date: Mon, 15 Jul 2019 18:51:36 -1000 Subject: Make puma bind address configurable with BIND env var (#11326) --- config/puma.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config') diff --git a/config/puma.rb b/config/puma.rb index 25a5534b2..6a96867d5 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,7 +4,7 @@ threads threads_count, threads_count if ENV['SOCKET'] bind "unix://#{ENV['SOCKET']}" else - bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" + bind "tcp://#{ENV.fetch('BIND', '127.0.0.1')}:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } -- cgit From 7e2b6da57f7689757a50fa261c480445b1846703 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 17 Jul 2019 21:09:15 +0200 Subject: Add setting to disable the anti-spam (#11296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add environment variable to disable the anti-spam * Move antispam setting to admin settings * Fix typo * antispam → spam_check --- app/controllers/admin/dashboard_controller.rb | 1 + app/lib/spam_check.rb | 6 +++++- app/models/form/admin_settings.rb | 2 ++ app/views/admin/dashboard/index.html.haml | 2 ++ app/views/admin/settings/edit.html.haml | 3 +++ config/locales/en.yml | 4 ++++ config/settings.yml | 1 + 7 files changed, 18 insertions(+), 1 deletion(-) (limited to 'config') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f23ed1508..e74e4755f 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -30,6 +30,7 @@ module Admin @trending_hashtags = TrendingTags.get(7) @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview + @spam_check_enabled = Setting.spam_check_enabled end private diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb index 923d48a02..0cf1b8790 100644 --- a/app/lib/spam_check.rb +++ b/app/lib/spam_check.rb @@ -14,7 +14,7 @@ class SpamCheck end def skip? - already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? + disabled? || already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? end def spam? @@ -80,6 +80,10 @@ class SpamCheck private + def disabled? + !Setting.spam_check_enabled + end + def remove_mentions(text) return text.gsub(Account::MENTION_RE, '') if @status.local? diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 86a86ec66..2c03c88a8 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -28,6 +28,7 @@ class Form::AdminSettings thumbnail hero mascot + spam_check_enabled ).freeze BOOLEAN_KEYS = %i( @@ -39,6 +40,7 @@ class Form::AdminSettings show_known_fediverse_at_about_page preview_sensitive_media profile_directory + spam_check_enabled ).freeze UPLOAD_KEYS = %i( diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d448e3862..77cc1a2a0 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -51,6 +51,8 @@ = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview) %li = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled) + %li + = feature_hint(link_to(t('admin.dashboard.feature_spam_check'), edit_admin_settings_path), @spam_check_enabled) .dashboard__widgets__versions %div diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index a67e6a2c8..b3bf3849c 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -66,6 +66,9 @@ .fields-group = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') + .fields-group + = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') + %hr.spacer/ .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index 89251ad40..4e252945f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -250,6 +250,7 @@ en: feature_profile_directory: Profile directory feature_registrations: Registrations feature_relay: Federation relay + feature_spam_check: Anti-spam feature_timeline_preview: Timeline preview features: Features hidden_service: Federation with hidden services @@ -449,6 +450,9 @@ en: desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags title: Custom terms of service site_title: Server name + spam_check_enabled: + desc_html: Mastodon can auto-silence and auto-report accounts based on measures such as detecting accounts who send repeated unsolicited messages. There may be false positives. + title: Anti-spam thumbnail: desc_html: Used for previews via OpenGraph and API. 1200x630px recommended title: Server thumbnail diff --git a/config/settings.yml b/config/settings.yml index 75cb2dc85..ad2970bb7 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -61,6 +61,7 @@ defaults: &defaults activity_api_enabled: true peers_api_enabled: true show_known_fediverse_at_about_page: true + spam_check_enabled: true development: <<: *defaults -- cgit From 730c4053d642024b9949d72c8a9f1873532c6212 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 01:44:42 +0200 Subject: Add ActivityPub actor representing the entire server (#11321) * Add support for an instance actor * Skip username validation for local Application accounts * Add migration script to create instance actor * Make Codeclimate happy * Switch to id -99 for instance actor * Remove unused `icon` and `image` attributes from instance actor * Use if/elsif/else instead of return + ternary operator * Add instance actor to fresh installs * Use instance actor as instance representative Use instance actor for forwarding reports, relay operations, and spam auto-reporting. * Seed database in test environment * Fix single-user mode * Fix tests * Fix specs to accomodate for an extra `Account` * Auto-reject follows on instance actor Following an instance actor might make sense, but we are not handling that right now, so auto-reject. * Fix webfinger lookup and serialization for instance actor * Rename instance actor * Make it clear in the HTML view that the instance actor should not be blocked * Raise cache time for instance actor as there's no dynamic content * Re-use /about/more with a flash message for instance actor profile --- app/controllers/about_controller.rb | 4 +- app/controllers/application_controller.rb | 2 +- app/controllers/home_controller.rb | 2 +- app/controllers/instance_actors_controller.rb | 20 ++++++++ app/javascript/styles/mastodon/containers.scss | 4 ++ app/lib/activitypub/activity/follow.rb | 2 +- app/lib/activitypub/tag_manager.rb | 5 +- app/lib/webfinger_resource.rb | 6 +++ app/models/account.rb | 8 ++- app/models/concerns/account_finder_concern.rb | 2 +- app/serializers/activitypub/actor_serializer.rb | 14 ++++-- app/serializers/webfinger_serializer.rb | 25 +++++++--- app/views/about/more.html.haml | 2 + app/views/well_known/webfinger/show.xml.ruby | 57 ++++++++++++++-------- config/locales/en.yml | 3 ++ config/routes.rb | 4 ++ db/migrate/20190715164535_add_instance_actor.rb | 9 ++++ db/schema.rb | 2 +- db/seeds.rb | 4 +- spec/models/account_spec.rb | 12 ++--- spec/services/fetch_remote_account_service_spec.rb | 1 - spec/services/fetch_resource_service_spec.rb | 4 +- spec/spec_helper.rb | 1 + 23 files changed, 141 insertions(+), 52 deletions(-) create mode 100644 app/controllers/instance_actors_controller.rb create mode 100644 db/migrate/20190715164535_add_instance_actor.rb (limited to 'config') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 52fb1dc1b..33bac9bbc 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -11,7 +11,9 @@ class AboutController < ApplicationController def show; end - def more; end + def more + flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] + end def terms; end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 26f3b1def..51e9764d4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,7 +91,7 @@ class ApplicationController < ActionController::Base end def single_user_mode? - @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? + @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end def use_seamless_external_login? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d1c525134..42493cd78 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -58,7 +58,7 @@ class HomeController < ApplicationController if request.path.start_with?('/web') new_user_session_path elsif single_user_mode? - short_account_path(Account.local.without_suspended.first) + short_account_path(Account.local.without_suspended.where('id > 0').first) else about_path end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb new file mode 100644 index 000000000..41f33602e --- /dev/null +++ b/app/controllers/instance_actors_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class InstanceActorsController < ApplicationController + include AccountControllerConcern + + def show + expires_in 10.minutes, public: true + render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to + end + + private + + def set_account + @account = Account.find(-99) + end + + def restrict_fields_to + %i(id type preferred_username inbox public_key endpoints url manually_approves_followers) + end +end diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 3564bf07b..2b6794ee2 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -145,6 +145,10 @@ min-height: 100%; } + .flash-message { + margin-bottom: 10px; + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 3eb88339a..28f1da19f 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -8,7 +8,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account) - if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? + if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor? reject_follow_request!(target_account) return end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 4d452f290..512272dbe 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -17,7 +17,7 @@ class ActivityPub::TagManager case target.object_type when :person - short_account_url(target) + target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) @@ -29,7 +29,7 @@ class ActivityPub::TagManager case target.object_type when :person - account_url(target) + target.instance_actor? ? instance_actor_url : account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? account_status_url(target.account, target) @@ -119,6 +119,7 @@ class ActivityPub::TagManager def uri_to_local_id(uri, param = :id) path_params = Rails.application.routes.recognize_path(uri) + path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' path_params[param] end diff --git a/app/lib/webfinger_resource.rb b/app/lib/webfinger_resource.rb index a54a702a2..22d78874a 100644 --- a/app/lib/webfinger_resource.rb +++ b/app/lib/webfinger_resource.rb @@ -23,11 +23,17 @@ class WebfingerResource def username_from_url if account_show_page? path_params[:username] + elsif instance_actor_page? + Rails.configuration.x.local_domain else raise ActiveRecord::RecordNotFound end end + def instance_actor_page? + path_params[:controller] == 'instance_actors' + end + def account_show_page? path_params[:controller] == 'accounts' && path_params[:action] == 'show' end diff --git a/app/models/account.rb b/app/models/account.rb index adf4586fa..ccd116d6e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -77,7 +77,7 @@ class Account < ApplicationRecord validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? } # Local user validations - validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? } + validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' } validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? } validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } @@ -139,6 +139,10 @@ class Account < ApplicationRecord %w(Application Service).include? actor_type end + def instance_actor? + id == -99 + end + alias bot bot? def bot=(val) @@ -498,7 +502,7 @@ class Account < ApplicationRecord end def generate_keys - return unless local? && !Rails.env.test? + return unless local? && private_key.blank? && public_key.blank? keypair = OpenSSL::PKey::RSA.new(2048) self.private_key = keypair.to_pem diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb index ccd7bfa12..a54c2174d 100644 --- a/app/models/concerns/account_finder_concern.rb +++ b/app/models/concerns/account_finder_concern.rb @@ -13,7 +13,7 @@ module AccountFinderConcern end def representative - find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.without_suspended.first + Account.find(-99) end def find_local(username) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0644219fb..0bd7aed2e 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -39,11 +39,17 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer delegate :moved?, to: :object def id - account_url(object) + object.instance_actor? ? instance_actor_url : account_url(object) end def type - object.bot? ? 'Service' : 'Person' + if object.instance_actor? + 'Application' + elsif object.bot? + 'Service' + else + 'Person' + end end def following @@ -55,7 +61,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def inbox - account_inbox_url(object) + object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object) end def outbox @@ -95,7 +101,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def url - short_account_url(object) + object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object) end def avatar_exists? diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index f4af21551..008d0c182 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -10,15 +10,26 @@ class WebfingerSerializer < ActiveModel::Serializer end def aliases - [short_account_url(object), account_url(object)] + if object.instance_actor? + [instance_actor_url] + else + [short_account_url(object), account_url(object)] + end end def links - [ - { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, - { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, - { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, - ] + if object.instance_actor? + [ + { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) }, + { rel: 'self', type: 'application/activity+json', href: instance_actor_url }, + ] + else + [ + { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, + { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, + { rel: 'self', type: 'application/activity+json', href: account_url(object) }, + { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, + ] + end end end diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index b248ed1d2..21431ef8e 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -43,5 +43,7 @@ = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email .column-3 + = render 'application/flashes' + .box-widget .rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html') diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index ae80df9d2..f5a54052a 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -4,30 +4,47 @@ doc << Ox::Element.new('XRD').tap do |xrd| xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0' xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s) - xrd << (Ox::Element.new('Alias') << short_account_url(@account)) - xrd << (Ox::Element.new('Alias') << account_url(@account)) - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://webfinger.net/rel/profile-page' - link['type'] = 'text/html' - link['href'] = short_account_url(@account) - end + if @account.instance_actor? + xrd << (Ox::Element.new('Alias') << instance_actor_url) - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://schemas.google.com/g/2010#updates-from' - link['type'] = 'application/atom+xml' - link['href'] = account_url(@account, format: 'atom') - end + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://webfinger.net/rel/profile-page' + link['type'] = 'text/html' + link['href'] = about_more_url(instance_actor: true) + end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'self' - link['type'] = 'application/activity+json' - link['href'] = account_url(@account) - end + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'self' + link['type'] = 'application/activity+json' + link['href'] = instance_actor_url + end + else + xrd << (Ox::Element.new('Alias') << short_account_url(@account)) + xrd << (Ox::Element.new('Alias') << account_url(@account)) + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://webfinger.net/rel/profile-page' + link['type'] = 'text/html' + link['href'] = short_account_url(@account) + end + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://schemas.google.com/g/2010#updates-from' + link['type'] = 'application/atom+xml' + link['href'] = account_url(@account, format: 'atom') + end + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'self' + link['type'] = 'application/activity+json' + link['href'] = account_url(@account) + end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' - link['template'] = "#{authorize_interaction_url}?acct={uri}" + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' + link['template'] = "#{authorize_interaction_url}?acct={uri}" + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4e252945f..89c52b84a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -24,6 +24,9 @@ en: generic_description: "%{domain} is one server in the network" get_apps: Try a mobile app hosted_on: Mastodon hosted on %{domain} + instance_actor_flash: | + This account is a virtual actor used to represent the server itself and not any individual user. + It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block. learn_more: Learn more privacy_policy: Privacy policy see_whats_happening: See what's happening diff --git a/config/routes.rb b/config/routes.rb index 95f8a39ad..27b536641 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,10 @@ Rails.application.routes.draw do get 'intent', to: 'intents#show' get 'custom.css', to: 'custom_css#show', as: :custom_css + resource :instance_actor, path: 'actor', only: [:show] do + resource :inbox, only: [:create], module: :activitypub + end + devise_scope :user do get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup diff --git a/db/migrate/20190715164535_add_instance_actor.rb b/db/migrate/20190715164535_add_instance_actor.rb new file mode 100644 index 000000000..a26d54949 --- /dev/null +++ b/db/migrate/20190715164535_add_instance_actor.rb @@ -0,0 +1,9 @@ +class AddInstanceActor < ActiveRecord::Migration[5.2] + def up + Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain) + end + + def down + Account.find_by(id: -99, actor_type: 'Application').destroy! + end +end diff --git a/db/schema.rb b/db/schema.rb index c7b6b9be6..a6a14827b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_06_233204) do +ActiveRecord::Schema.define(version: 2019_07_15_164535) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/db/seeds.rb b/db/seeds.rb index 9a6e9dd78..5f43fbac8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,9 @@ Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow') +domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain +Account.create!(id: -99, actor_type: 'Application', locked: true, username: domain) + if Rails.env.development? - domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain admin = Account.where(username: 'admin').first_or_initialize(username: 'admin') admin.save(validate: false) User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true, approved: true).save! diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index ce9ea250d..6495a6193 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -450,7 +450,7 @@ RSpec.describe Account, type: :model do describe '.domains' do it 'returns domains' do Fabricate(:account, domain: 'domain') - expect(Account.domains).to match_array(['domain']) + expect(Account.remote.domains).to match_array(['domain']) end end @@ -665,7 +665,7 @@ RSpec.describe Account, type: :model do { username: 'b', domain: 'b' }, ].map(&method(:Fabricate).curry(2).call(:account)) - expect(Account.alphabetic).to eq matches + expect(Account.where('id > 0').alphabetic).to eq matches end end @@ -732,7 +732,7 @@ RSpec.describe Account, type: :model do 2.times { Fabricate(:account, domain: 'example.com') } Fabricate(:account, domain: 'example2.com') - results = Account.by_domain_accounts + results = Account.where('id > 0').by_domain_accounts expect(results.length).to eq 2 expect(results.first.domain).to eq 'example.com' expect(results.first.accounts_count).to eq 2 @@ -745,7 +745,7 @@ RSpec.describe Account, type: :model do it 'returns an array of accounts who do not have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') - expect(Account.local).to match_array([account_1]) + expect(Account.where('id > 0').local).to match_array([account_1]) end end @@ -756,14 +756,14 @@ RSpec.describe Account, type: :model do matches[index] = Fabricate(:account, domain: matches[index]) end - expect(Account.partitioned).to match_array(matches) + expect(Account.where('id > 0').partitioned).to match_array(matches) end end describe 'recent' do it 'returns a relation of accounts sorted by recent creation' do matches = 2.times.map { Fabricate(:account) } - expect(Account.recent).to match_array(matches) + expect(Account.where('id > 0').recent).to match_array(matches) end end diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index b37445861..ee7325be2 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,7 +4,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } - let!(:representative) { Fabricate(:account) } subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 98630966b..f836147d3 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' RSpec.describe FetchResourceService, type: :service do - let!(:representative) { Fabricate(:account) } - describe '#call' do let(:url) { 'http://example.com' } @@ -60,7 +58,7 @@ RSpec.describe FetchResourceService, type: :service do it 'signs request' do subject - expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(representative) + '#main-key')}"/ })).to have_been_made + expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made end context 'when content type is application/atom+xml' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0cd1f91d0..45ba1bbd9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,6 +27,7 @@ RSpec.configure do |config| end config.before :suite do + Rails.application.load_seed Chewy.strategy(:bypass) end -- cgit From bd87e6667975bc3b5bfaf3e1cdff97e041ed4c98 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 21 Jul 2019 04:08:00 +0200 Subject: Remove WebSub subscriptions (#11303) --- app/models/concerns/account_associations.rb | 3 - app/models/subscription.rb | 62 -------------------- app/policies/subscription_policy.rb | 7 --- app/services/suspend_account_service.rb | 1 - .../scheduler/subscriptions_cleanup_scheduler.rb | 4 +- config/sidekiq.yml | 3 - .../20190715031050_drop_subscriptions.rb | 11 ++++ db/schema.rb | 14 ----- spec/fabricators/subscription_fabricator.rb | 7 --- spec/models/subscription_spec.rb | 67 ---------------------- spec/policies/subscription_policy_spec.rb | 24 -------- .../services/batched_remove_status_service_spec.rb | 3 - spec/services/remove_status_service_spec.rb | 3 - spec/services/suspend_account_service_spec.rb | 8 +-- 14 files changed, 14 insertions(+), 203 deletions(-) delete mode 100644 app/models/subscription.rb delete mode 100644 app/policies/subscription_policy.rb create mode 100644 db/post_migrate/20190715031050_drop_subscriptions.rb delete mode 100644 spec/fabricators/subscription_fabricator.rb delete mode 100644 spec/models/subscription_spec.rb delete mode 100644 spec/policies/subscription_policy_spec.rb (limited to 'config') diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 0921e3252..1db7771c7 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -30,9 +30,6 @@ module AccountAssociations has_many :media_attachments, dependent: :destroy has_many :polls, dependent: :destroy - # PuSH subscriptions - has_many :subscriptions, dependent: :destroy - # Report relationships has_many :reports, dependent: :destroy, inverse_of: :account has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account diff --git a/app/models/subscription.rb b/app/models/subscription.rb deleted file mode 100644 index 79b81828d..000000000 --- a/app/models/subscription.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: subscriptions -# -# id :bigint(8) not null, primary key -# callback_url :string default(""), not null -# secret :string -# expires_at :datetime -# confirmed :boolean default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# last_successful_delivery_at :datetime -# domain :string -# account_id :bigint(8) not null -# - -class Subscription < ApplicationRecord - MIN_EXPIRATION = 1.day.to_i - MAX_EXPIRATION = 30.days.to_i - - belongs_to :account - - validates :callback_url, presence: true - validates :callback_url, uniqueness: { scope: :account_id } - - scope :confirmed, -> { where(confirmed: true) } - scope :future_expiration, -> { where(arel_table[:expires_at].gt(Time.now.utc)) } - scope :expired, -> { where(arel_table[:expires_at].lt(Time.now.utc)) } - scope :active, -> { confirmed.future_expiration } - - def lease_seconds=(value) - self.expires_at = future_expiration(value) - end - - def lease_seconds - (expires_at - Time.now.utc).to_i - end - - def expired? - Time.now.utc > expires_at - end - - before_validation :set_min_expiration - - private - - def future_expiration(value) - Time.now.utc + future_offset(value).seconds - end - - def future_offset(seconds) - [ - [MIN_EXPIRATION, seconds.to_i].max, - MAX_EXPIRATION, - ].min - end - - def set_min_expiration - self.lease_seconds = 0 unless expires_at - end -end diff --git a/app/policies/subscription_policy.rb b/app/policies/subscription_policy.rb deleted file mode 100644 index ac9a8a6c4..000000000 --- a/app/policies/subscription_policy.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class SubscriptionPolicy < ApplicationPolicy - def index? - admin? - end -end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 0ebe0b562..00cffcdfc 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -24,7 +24,6 @@ class SuspendAccountService < BaseService report_notes scheduled_statuses status_pins - subscriptions ).freeze ASSOCIATIONS_ON_DESTROY = %w( diff --git a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb index 5fba120f6..75fe681a9 100644 --- a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb +++ b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb @@ -5,7 +5,5 @@ class Scheduler::SubscriptionsCleanupScheduler sidekiq_options unique: :until_executed, retry: 0 - def perform - Subscription.expired.in_batches.delete_all - end + def perform; end end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 5c652792c..7f41b6607 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -21,9 +21,6 @@ user_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *' class: Scheduler::UserCleanupScheduler - subscriptions_cleanup_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(1..3) %> * * 0' - class: Scheduler::SubscriptionsCleanupScheduler ip_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler diff --git a/db/post_migrate/20190715031050_drop_subscriptions.rb b/db/post_migrate/20190715031050_drop_subscriptions.rb new file mode 100644 index 000000000..3719afe4a --- /dev/null +++ b/db/post_migrate/20190715031050_drop_subscriptions.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DropSubscriptions < ActiveRecord::Migration[5.2] + def up + drop_table :subscriptions + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index a6a14827b..6319dd932 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -648,19 +648,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) do t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true end - create_table "subscriptions", force: :cascade do |t| - t.string "callback_url", default: "", null: false - t.string "secret" - t.datetime "expires_at" - t.boolean "confirmed", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.datetime "last_successful_delivery_at" - t.string "domain" - t.bigint "account_id", null: false - t.index ["account_id", "callback_url"], name: "index_subscriptions_on_account_id_and_callback_url", unique: true - end - create_table "tags", force: :cascade do |t| t.string "name", default: "", null: false t.datetime "created_at", null: false @@ -821,7 +808,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) do add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade add_foreign_key "statuses_tags", "statuses", on_delete: :cascade add_foreign_key "statuses_tags", "tags", name: "fk_3081861e21", on_delete: :cascade - add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "tombstones", "accounts", on_delete: :cascade add_foreign_key "user_invite_requests", "users", on_delete: :cascade add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade diff --git a/spec/fabricators/subscription_fabricator.rb b/spec/fabricators/subscription_fabricator.rb deleted file mode 100644 index 347dab5df..000000000 --- a/spec/fabricators/subscription_fabricator.rb +++ /dev/null @@ -1,7 +0,0 @@ -Fabricator(:subscription) do - account - callback_url "http://example.com/callback" - secret "foobar" - expires_at "2016-11-28 11:30:07" - confirmed false -end diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb deleted file mode 100644 index b83979d13..000000000 --- a/spec/models/subscription_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'rails_helper' - -RSpec.describe Subscription, type: :model do - let(:alice) { Fabricate(:account, username: 'alice') } - - subject { Fabricate(:subscription, account: alice) } - - describe '#expired?' do - it 'return true when expires_at is past' do - subject.expires_at = 2.days.ago - expect(subject.expired?).to be true - end - - it 'return false when expires_at is future' do - subject.expires_at = 2.days.from_now - expect(subject.expired?).to be false - end - end - - describe 'lease_seconds' do - it 'returns the time remaining until expiration' do - datetime = 1.day.from_now - subscription = Subscription.new(expires_at: datetime) - travel_to(datetime - 12.hours) do - expect(subscription.lease_seconds).to eq(12.hours) - end - end - end - - describe 'lease_seconds=' do - it 'sets expires_at to min expiration when small value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - too_low = Subscription::MIN_EXPIRATION - 1000 - travel_to(datetime) do - subscription.lease_seconds = too_low - end - - expected = datetime + Subscription::MIN_EXPIRATION.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - - it 'sets expires_at to value when valid value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - valid = Subscription::MIN_EXPIRATION + 1000 - travel_to(datetime) do - subscription.lease_seconds = valid - end - - expected = datetime + valid.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - - it 'sets expires_at to max expiration when large value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - too_high = Subscription::MAX_EXPIRATION + 1000 - travel_to(datetime) do - subscription.lease_seconds = too_high - end - - expected = datetime + Subscription::MAX_EXPIRATION.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - end -end diff --git a/spec/policies/subscription_policy_spec.rb b/spec/policies/subscription_policy_spec.rb deleted file mode 100644 index 21d60c15f..000000000 --- a/spec/policies/subscription_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe SubscriptionPolicy do - let(:subject) { described_class } - let(:admin) { Fabricate(:user, admin: true).account } - let(:john) { Fabricate(:user).account } - - permissions :index? do - context 'admin?' do - it 'permits' do - expect(subject).to permit(admin, Subscription) - end - end - - context '!admin?' do - it 'denies' do - expect(subject).to_not permit(john, Subscription) - end - end - end -end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index d52e7f484..f84256f18 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -14,11 +14,8 @@ RSpec.describe BatchedRemoveStatusService, type: :service do before do allow(Redis.current).to receive_messages(publish: nil) - stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) - stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.user.update(current_sign_in_at: Time.zone.now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 48191d47c..06676ec45 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -10,12 +10,9 @@ RSpec.describe RemoveStatusService, type: :service do let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') } before do - stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) - stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) stub_request(:post, 'http://example2.com/inbox').to_return(status: 200) - Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 896ac17a3..eebbbc12a 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -18,7 +18,6 @@ RSpec.describe SuspendAccountService, type: :service do let!(:favourite) { Fabricate(:favourite, account: account) } let!(:active_relationship) { Fabricate(:follow, account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: account) } - let!(:subscription) { Fabricate(:subscription, account: account) } let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) } let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } @@ -31,9 +30,8 @@ RSpec.describe SuspendAccountService, type: :service do account.favourites, account.active_relationships, account.passive_relationships, - account.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0]) end it 'sends a delete actor activity to all known inboxes' do @@ -62,7 +60,6 @@ RSpec.describe SuspendAccountService, type: :service do let!(:favourite) { Fabricate(:favourite, account: remote_bob) } let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) } - let!(:subscription) { Fabricate(:subscription, account: remote_bob) } it 'deletes associated records' do is_expected.to change { @@ -73,9 +70,8 @@ RSpec.describe SuspendAccountService, type: :service do remote_bob.favourites, remote_bob.active_relationships, remote_bob.passive_relationships, - remote_bob.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0]) end it 'sends a reject follow to follwer inboxes' do -- cgit From bd1545de5ee9655130e5357bb9cb6449520a6292 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 21 Jul 2019 18:08:02 +0200 Subject: Change locale detection to run once per session (#8657) Fix #6462 --- app/controllers/application_controller.rb | 6 +----- app/controllers/concerns/localized.rb | 13 ++++++++----- config/application.rb | 3 +++ spec/controllers/concerns/localized_spec.rb | 16 +++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) (limited to 'config') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 51e9764d4..5863fe168 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -138,11 +138,7 @@ class ApplicationController < ActionController::Base def respond_with_error(code) respond_to do |format| format.any { head code } - - format.html do - set_locale - render "errors/#{code}", layout: 'error', status: code - end + format.html { render "errors/#{code}", layout: 'error', status: code } end end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 145549bcd..b43859d9d 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -4,16 +4,19 @@ module Localized extend ActiveSupport::Concern included do - before_action :set_locale + around_action :set_locale end private def set_locale - I18n.locale = default_locale - I18n.locale = current_user.locale if user_signed_in? - rescue I18n::InvalidLocale - I18n.locale = default_locale + locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in? + locale ||= session[:locale] ||= default_locale + locale = default_locale unless I18n.available_locales.include?(locale.to_sym) + + I18n.with_locale(locale) do + yield + end end def default_locale diff --git a/config/application.rb b/config/application.rb index 4534ede49..f49deffbb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -114,6 +114,9 @@ module Mastodon Doorkeeper::AuthorizationsController.layout 'modal' Doorkeeper::AuthorizedApplicationsController.layout 'admin' Doorkeeper::Application.send :include, ApplicationExtension + Devise::FailureApp.send :include, AbstractController::Callbacks + Devise::FailureApp.send :include, HttpAcceptLanguage::EasyAccess + Devise::FailureApp.send :include, Localized end end end diff --git a/spec/controllers/concerns/localized_spec.rb b/spec/controllers/concerns/localized_spec.rb index 76c3de118..7635d10e1 100644 --- a/spec/controllers/concerns/localized_spec.rb +++ b/spec/controllers/concerns/localized_spec.rb @@ -7,16 +7,10 @@ describe ApplicationController, type: :controller do include Localized def success - head 200 + render plain: I18n.locale, status: 200 end end - around do |example| - current_locale = I18n.locale - example.run - I18n.locale = current_locale - end - before do routes.draw { get 'success' => 'anonymous#success' } end @@ -25,19 +19,19 @@ describe ApplicationController, type: :controller do it 'sets available and preferred language' do request.headers['Accept-Language'] = 'ca-ES, fa' get 'success' - expect(I18n.locale).to eq :fa + expect(response.body).to eq 'fa' end it 'sets available and compatible language if none of available languages are preferred' do request.headers['Accept-Language'] = 'fa-IR' get 'success' - expect(I18n.locale).to eq :fa + expect(response.body).to eq 'fa' end it 'sets default locale if none of available languages are compatible' do request.headers['Accept-Language'] = '' get 'success' - expect(I18n.locale).to eq :en + expect(response.body).to eq 'en' end end @@ -48,7 +42,7 @@ describe ApplicationController, type: :controller do sign_in(user) get 'success' - expect(I18n.locale).to eq :ca + expect(response.body).to eq 'ca' end end -- cgit From 964ae8eee593687f922c873fa7b378bb6e3e39bb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 22 Jul 2019 10:48:50 +0200 Subject: Change unconfirmed user login behaviour (#11375) Allow access to account settings, 2FA, authorized applications, and account deletions to unconfirmed and pending users, as well as users who had their accounts disabled. Suspended users cannot update their e-mail or password or delete their account. Display account status on account settings page, for example, when an account is frozen, limited, unconfirmed or pending review. After sign up, login users straight away and show a simple page that tells them the status of their account with links to account settings and logout, to reduce onboarding friction and allow users to correct wrongly typed e-mail addresses. Move the final sign-up step of SSO integrations to be the same as above to reduce code duplication. --- app/controllers/about_controller.rb | 2 +- app/controllers/api/base_controller.rb | 2 +- app/controllers/application_controller.rb | 6 +-- app/controllers/auth/confirmations_controller.rb | 21 +------- .../auth/omniauth_callbacks_controller.rb | 2 +- app/controllers/auth/registrations_controller.rb | 9 +++- app/controllers/auth/sessions_controller.rb | 4 +- app/controllers/auth/setup_controller.rb | 58 ++++++++++++++++++++++ .../oauth/authorized_applications_controller.rb | 2 + app/controllers/settings/deletes_controller.rb | 7 +++ app/controllers/settings/sessions_controller.rb | 2 + .../confirmations_controller.rb | 2 + .../recovery_codes_controller.rb | 2 + .../two_factor_authentications_controller.rb | 2 + app/javascript/styles/mastodon/admin.scss | 58 +++++++++++++--------- app/javascript/styles/mastodon/forms.scss | 7 +++ app/models/concerns/omniauthable.rb | 2 +- app/models/user.rb | 6 ++- .../auth/confirmations/finish_signup.html.haml | 15 ------ app/views/auth/registrations/_sessions.html.haml | 4 +- app/views/auth/registrations/_status.html.haml | 16 ++++++ app/views/auth/registrations/edit.html.haml | 35 +++++++------ app/views/auth/setup/show.html.haml | 23 +++++++++ .../oauth/authorized_applications/index.html.haml | 2 +- config/locales/en.yml | 9 +++- config/routes.rb | 5 +- db/seeds.rb | 2 +- spec/controllers/api/base_controller_spec.rb | 42 +++++++++++++++- spec/controllers/application_controller_spec.rb | 4 +- .../auth/confirmations_controller_spec.rb | 41 --------------- .../auth/registrations_controller_spec.rb | 25 +++++++--- spec/controllers/auth/sessions_controller_spec.rb | 4 +- .../settings/deletes_controller_spec.rb | 17 +++++++ spec/features/log_in_spec.rb | 4 +- spec/models/user_spec.rb | 4 +- 35 files changed, 298 insertions(+), 148 deletions(-) create mode 100644 app/controllers/auth/setup_controller.rb delete mode 100644 app/views/auth/confirmations/finish_signup.html.haml create mode 100644 app/views/auth/registrations/_status.html.haml create mode 100644 app/views/auth/setup/show.html.haml (limited to 'config') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 33bac9bbc..31cf17710 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -7,7 +7,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter before_action :set_expires_in - skip_before_action :check_user_permissions, only: [:more, :terms] + skip_before_action :require_functional!, only: [:more, :terms] def show; end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index eca558f42..6f33a1ea9 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController include RateLimitHeaders skip_before_action :store_current_location - skip_before_action :check_user_permissions + skip_before_action :require_functional! before_action :set_cache_headers diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b8a1faf77..41ce1a0ca 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base rescue_from Mastodon::NotPermittedError, with: :forbidden before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? - before_action :check_user_permissions, if: :user_signed_in? + before_action :require_functional!, if: :user_signed_in? def raise_not_found raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}" @@ -57,8 +57,8 @@ class ApplicationController < ActionController::Base forbidden unless current_user&.staff? end - def check_user_permissions - forbidden if current_user.disabled? || current_user.account.suspended? + def require_functional! + redirect_to edit_user_registration_path unless current_user.functional? end def after_sign_out_path_for(_resource_or_scope) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index c28c7471c..0d7c6e7c2 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -4,34 +4,15 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController layout 'auth' before_action :set_body_classes - before_action :set_user, only: [:finish_signup] - def finish_signup - return unless request.patch? && params[:user] - - if @user.update(user_params) - @user.skip_reconfirmation! - bypass_sign_in(@user) - redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') - else - @show_errors = true - end - end + skip_before_action :require_functional! private - def set_user - @user = current_user - end - def set_body_classes @body_classes = 'lighter' end - def user_params - params.require(:user).permit(:email) - end - def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.redirect_uri diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index bbf63bed3..682c77016 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -27,7 +27,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController if resource.email_verified? root_path else - finish_signup_path + auth_setup_path(missing_email: '1') end end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 83797cf1f..019caf9c1 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -9,6 +9,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_sessions, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update] + before_action :require_not_suspended!, only: [:update] + + skip_before_action :require_functional!, only: [:edit, :update] def new super(&:build_invite_request) @@ -43,7 +46,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def after_sign_up_path_for(_resource) - new_user_session_path + auth_setup_path end def after_sign_in_path_for(_resource) @@ -102,4 +105,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController def set_sessions @sessions = current_user.session_activations end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index fb8615c31..7e6dbf19e 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -6,8 +6,10 @@ class Auth::SessionsController < Devise::SessionsController layout 'auth' skip_before_action :require_no_authentication, only: [:create] - skip_before_action :check_user_permissions, only: [:destroy] + skip_before_action :require_functional! + prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] + before_action :set_instance_presenter, only: [:new] before_action :set_body_classes diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb new file mode 100644 index 000000000..46c5f2958 --- /dev/null +++ b/app/controllers/auth/setup_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class Auth::SetupController < ApplicationController + layout 'auth' + + before_action :authenticate_user! + before_action :require_unconfirmed_or_pending! + before_action :set_body_classes + before_action :set_user + + skip_before_action :require_functional! + + def show + flash.now[:notice] = begin + if @user.pending? + I18n.t('devise.registrations.signed_up_but_pending') + else + I18n.t('devise.registrations.signed_up_but_unconfirmed') + end + end + end + + def update + # This allows updating the e-mail without entering a password as is required + # on the account settings page; however, we only allow this for accounts + # that were not confirmed yet + + if @user.update(user_params) + redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') + else + render :show + end + end + + helper_method :missing_email? + + private + + def require_unconfirmed_or_pending! + redirect_to root_path if current_user.confirmed? && current_user.approved? + end + + def set_user + @user = current_user + end + + def set_body_classes + @body_classes = 'lighter' + end + + def user_params + params.require(:user).permit(:email) + end + + def missing_email? + truthy_param?(:missing_email) + end +end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index f3d235366..fb8389034 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -7,6 +7,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :authenticate_resource_owner! before_action :set_body_classes + skip_before_action :require_functional! + include Localized def destroy diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index dd19aadf6..97fe4d328 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -5,6 +5,9 @@ class Settings::DeletesController < Settings::BaseController before_action :check_enabled_deletion before_action :authenticate_user! + before_action :require_not_suspended! + + skip_before_action :require_functional! def show @confirmation = Form::DeleteConfirmation.new @@ -29,4 +32,8 @@ class Settings::DeletesController < Settings::BaseController def delete_params params.require(:form_delete_confirmation).permit(:password) end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 84ebb21f2..df5ace803 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -4,6 +4,8 @@ class Settings::SessionsController < Settings::BaseController before_action :authenticate_user! before_action :set_session, only: :destroy + skip_before_action :require_functional! + def destroy @session.destroy! flash[:notice] = I18n.t('sessions.revoke_success') diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 02652a36c..3145e092d 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -8,6 +8,8 @@ module Settings before_action :authenticate_user! before_action :ensure_otp_secret + skip_before_action :require_functional! + def new prepare_two_factor_form end diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index 874bf532b..09a759860 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -7,6 +7,8 @@ module Settings before_action :authenticate_user! + skip_before_action :require_functional! + def create @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index e12c43074..6904076e4 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -7,6 +7,8 @@ module Settings before_action :authenticate_user! before_action :verify_otp_required, only: [:create] + skip_before_action :require_functional! + def show @confirmation = Form::TwoFactorConfirmation.new end diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 373a10260..f625bc139 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -204,29 +204,6 @@ $content-width: 840px; border: 0; } } - - .muted-hint { - color: $darker-text-color; - - a { - color: $highlight-text-color; - } - } - - .positive-hint { - color: $valid-value-color; - font-weight: 500; - } - - .negative-hint { - color: $error-value-color; - font-weight: 500; - } - - .neutral-hint { - color: $dark-text-color; - font-weight: 500; - } } @media screen and (max-width: $no-columns-breakpoint) { @@ -249,6 +226,41 @@ $content-width: 840px; } } +hr.spacer { + width: 100%; + border: 0; + margin: 20px 0; + height: 1px; +} + +.muted-hint { + color: $darker-text-color; + + a { + color: $highlight-text-color; + } +} + +.positive-hint { + color: $valid-value-color; + font-weight: 500; +} + +.negative-hint { + color: $error-value-color; + font-weight: 500; +} + +.neutral-hint { + color: $dark-text-color; + font-weight: 500; +} + +.warning-hint { + color: $gold-star; + font-weight: 500; +} + .filters { display: flex; flex-wrap: wrap; diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 456ee4e0d..ac99124ea 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -300,6 +300,13 @@ code { } } + .input.static .label_input__wrapper { + font-size: 16px; + padding: 10px; + border: 1px solid $dark-text-color; + border-radius: 4px; + } + input[type=text], input[type=number], input[type=email], diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index 283033083..b9c124841 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -43,7 +43,7 @@ module Omniauthable # Check if the user exists with provided email if the provider gives us a # verified email. If no verified email was provided or the user already # exists, we assign a temporary email and ask the user to verify it on - # the next step via Auth::ConfirmationsController.finish_signup + # the next step via Auth::SetupController.show user = User.new(user_params_from_auth(auth)) user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/ diff --git a/app/models/user.rb b/app/models/user.rb index 31c99630c..474c77293 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -161,7 +161,11 @@ class User < ApplicationRecord end def active_for_authentication? - super && approved? + true + end + + def functional? + confirmed? && approved? && !disabled? && !account.suspended? end def inactive_message diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml deleted file mode 100644 index 9d09b74e1..000000000 --- a/app/views/auth/confirmations/finish_signup.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- content_for :page_title do - = t('auth.confirm_email') - -= simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f| - - if @show_errors && current_user.errors.any? - #error_explanation - - current_user.errors.full_messages.each do |msg| - = msg - %br - - .fields-group - = f.input :email, wrapper: :with_label, required: true, hint: false - - .actions - = f.submit t('auth.confirm_email'), class: 'button' diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index d7d96a1bb..395e36a9f 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -1,6 +1,8 @@ -%h4= t 'sessions.title' +%h3= t 'sessions.title' %p.muted-hint= t 'sessions.explanation' +%hr.spacer/ + .table-wrapper %table.table.inline-table %thead diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml new file mode 100644 index 000000000..b38a83d67 --- /dev/null +++ b/app/views/auth/registrations/_status.html.haml @@ -0,0 +1,16 @@ +%h3= t('auth.status.account_status') + +- if @user.account.suspended? + %span.negative-hint= t('user_mailer.warning.explanation.suspend') +- elsif @user.disabled? + %span.negative-hint= t('user_mailer.warning.explanation.disable') +- elsif @user.account.silenced? + %span.warning-hint= t('user_mailer.warning.explanation.silence') +- elsif !@user.confirmed? + %span.warning-hint= t('auth.status.confirming') +- elsif !@user.approved? + %span.warning-hint= t('auth.status.pending') +- else + %span.positive-hint= t('auth.status.functional') + +%hr.spacer/ diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 694461fdf..710ee5c68 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -1,25 +1,28 @@ - content_for :page_title do - = t('auth.security') + = t('settings.account_settings') + += render 'status' + +%h3= t('auth.security') = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f| = render 'shared/error_messages', object: resource - if !use_seamless_external_login? || resource.encrypted_password.present? - .fields-group - = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, hint: false - - .fields-group - = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true - - .fields-group - = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: false - - .fields-group - = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } - + .fields-row + .fields-row__column.fields-group.fields-row__column-6 + = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? + .fields-row__column.fields-group.fields-row__column-6 + = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended? + + .fields-row + .fields-row__column.fields-group.fields-row__column-6 + = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended? + .fields-row__column.fields-group.fields-row__column-6 + = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }, disabled: current_account.suspended? .actions - = f.button :button, t('generic.save_changes'), type: :submit + = f.button :button, t('generic.save_changes'), type: :submit, class: 'button', disabled: current_account.suspended? - else %p.hint= t('users.seamless_external_login') @@ -27,7 +30,7 @@ = render 'sessions' -- if open_deletion? +- if open_deletion? && !current_account.suspended? %hr.spacer/ - %h4= t('auth.delete_account') + %h3= t('auth.delete_account') %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml new file mode 100644 index 000000000..8bb44ca7f --- /dev/null +++ b/app/views/auth/setup/show.html.haml @@ -0,0 +1,23 @@ +- content_for :page_title do + = t('auth.setup.title') + +- if missing_email? + = simple_form_for(@user, url: auth_setup_path) do |f| + = render 'shared/error_messages', object: @user + + .fields-group + %p.hint= t('auth.setup.email_below_hint_html') + + .fields-group + = f.input :email, required: true, hint: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } + + .actions + = f.submit t('admin.accounts.change_email.label'), class: 'button' +- else + .simple_form + %p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email)) + +.form-footer + %ul.no-list + %li= link_to t('settings.account_settings'), edit_user_registration_path + %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete } diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 19af5f55d..7203d758d 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -17,7 +17,7 @@ = application.name - else = link_to application.name, application.website, target: '_blank', rel: 'noopener' - %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('
    ') + %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ') %td= l application.created_at %td - unless application.superapp? diff --git a/config/locales/en.yml b/config/locales/en.yml index 89c52b84a..9e1be87be 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -524,7 +524,6 @@ en: apply_for_account: Request an invite change_password: Password checkbox_agreement_html: I agree to the server rules and terms of service - confirm_email: Confirm email delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. didnt_get_confirmation: Didn't receive confirmation instructions? @@ -544,6 +543,14 @@ en: reset_password: Reset password security: Security set_new_password: Set new password + setup: + email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail. + email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings. + title: Setup + status: + account_status: Account status + confirming: Waiting for e-mail confirmation to be completed. + pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. trouble_logging_in: Trouble logging in? authorize_follow: already_following: You are already following this account diff --git a/config/routes.rb b/config/routes.rb index 27b536641..b6c215888 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,7 +34,10 @@ Rails.application.routes.draw do devise_scope :user do get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite - match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup + + namespace :auth do + resource :setup, only: [:show, :update], controller: :setup + end end devise_for :users, path: 'auth', controllers: { diff --git a/db/seeds.rb b/db/seeds.rb index b112cf073..0bfb5d0db 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,4 +1,4 @@ -Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow') +Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow push') domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain account = Account.find_or_initialize_by(id: -99, actor_type: 'Application', locked: true, username: domain) diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb index 750ccc8cf..05a42d1c1 100644 --- a/spec/controllers/api/base_controller_spec.rb +++ b/spec/controllers/api/base_controller_spec.rb @@ -15,7 +15,7 @@ describe Api::BaseController do end end - describe 'Forgery protection' do + describe 'forgery protection' do before do routes.draw { post 'success' => 'api/base#success' } end @@ -27,7 +27,45 @@ describe Api::BaseController do end end - describe 'Error handling' do + describe 'non-functional accounts handling' do + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + + controller do + before_action :require_user! + end + + before do + routes.draw { post 'success' => 'api/base#success' } + allow(controller).to receive(:doorkeeper_token) { token } + end + + it 'returns http forbidden for unconfirmed accounts' do + user.update(confirmed_at: nil) + post 'success' + expect(response).to have_http_status(403) + end + + it 'returns http forbidden for pending accounts' do + user.update(approved: false) + post 'success' + expect(response).to have_http_status(403) + end + + it 'returns http forbidden for disabled accounts' do + user.update(disabled: true) + post 'success' + expect(response).to have_http_status(403) + end + + it 'returns http forbidden for suspended accounts' do + user.account.suspend! + post 'success' + expect(response).to have_http_status(403) + end + end + + describe 'error handling' do ERRORS_WITH_CODES = { ActiveRecord::RecordInvalid => 422, Mastodon::ValidationError => 422, diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 27946b60f..1811500df 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -187,10 +187,10 @@ describe ApplicationController, type: :controller do expect(response).to have_http_status(200) end - it 'returns http 403 if user who signed in is suspended' do + it 'redirects to account status page' do sign_in(Fabricate(:user, account: Fabricate(:account, suspended: true))) get 'success' - expect(response).to have_http_status(403) + expect(response).to redirect_to(edit_user_registration_path) end end diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb index e9a471fc5..0b6b74ff9 100644 --- a/spec/controllers/auth/confirmations_controller_spec.rb +++ b/spec/controllers/auth/confirmations_controller_spec.rb @@ -50,45 +50,4 @@ describe Auth::ConfirmationsController, type: :controller do end end end - - describe 'GET #finish_signup' do - subject { get :finish_signup } - - let(:user) { Fabricate(:user) } - before do - sign_in user, scope: :user - @request.env['devise.mapping'] = Devise.mappings[:user] - end - - it 'renders finish_signup' do - is_expected.to render_template :finish_signup - expect(assigns(:user)).to have_attributes id: user.id - end - end - - describe 'PATCH #finish_signup' do - subject { patch :finish_signup, params: { user: { email: email } } } - - let(:user) { Fabricate(:user) } - before do - sign_in user, scope: :user - @request.env['devise.mapping'] = Devise.mappings[:user] - end - - context 'when email is valid' do - let(:email) { 'new_' + user.email } - - it 'redirects to root_path' do - is_expected.to redirect_to root_path - end - end - - context 'when email is invalid' do - let(:email) { '' } - - it 'renders finish_signup' do - is_expected.to render_template :finish_signup - end - end - end end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index a4337039e..3e11b34b5 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -46,6 +46,15 @@ RSpec.describe Auth::RegistrationsController, type: :controller do post :update expect(response).to have_http_status(200) end + + context 'when suspended' do + it 'returns http forbidden' do + request.env["devise.mapping"] = Devise.mappings[:user] + sign_in(Fabricate(:user, account_attributes: { username: 'test', suspended_at: Time.now.utc }), scope: :user) + post :update + expect(response).to have_http_status(403) + end + end end describe 'GET #new' do @@ -94,9 +103,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } end - it 'redirects to login page' do + it 'redirects to setup' do subject - expect(response).to redirect_to new_user_session_path + expect(response).to redirect_to auth_setup_path end it 'creates user' do @@ -120,9 +129,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } end - it 'redirects to login page' do + it 'redirects to setup' do subject - expect(response).to redirect_to new_user_session_path + expect(response).to redirect_to auth_setup_path end it 'creates user' do @@ -148,9 +157,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } } end - it 'redirects to login page' do + it 'redirects to setup' do subject - expect(response).to redirect_to new_user_session_path + expect(response).to redirect_to auth_setup_path end it 'creates user' do @@ -176,9 +185,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } } end - it 'redirects to login page' do + it 'redirects to setup' do subject - expect(response).to redirect_to new_user_session_path + expect(response).to redirect_to auth_setup_path end it 'creates user' do diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 71fcc1a6e..87ef4f2bb 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -160,8 +160,8 @@ RSpec.describe Auth::SessionsController, type: :controller do let(:unconfirmed_user) { user.tap { |u| u.update!(confirmed_at: nil) } } let(:accept_language) { 'fr' } - it 'shows a translated login error' do - expect(flash[:alert]).to eq(I18n.t('devise.failure.unconfirmed', locale: accept_language)) + it 'redirects to home' do + expect(response).to redirect_to(root_path) end end diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb index 35fd64e9b..996872efd 100644 --- a/spec/controllers/settings/deletes_controller_spec.rb +++ b/spec/controllers/settings/deletes_controller_spec.rb @@ -15,6 +15,15 @@ describe Settings::DeletesController do get :show expect(response).to have_http_status(200) end + + context 'when suspended' do + let(:user) { Fabricate(:user, account_attributes: { username: 'alice', suspended_at: Time.now.utc }) } + + it 'returns http forbidden' do + get :show + expect(response).to have_http_status(403) + end + end end context 'when not signed in' do @@ -49,6 +58,14 @@ describe Settings::DeletesController do it 'marks account as suspended' do expect(user.account.reload).to be_suspended end + + context 'when suspended' do + let(:user) { Fabricate(:user, account_attributes: { username: 'alice', suspended_at: Time.now.utc }) } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end end context 'with incorrect password' do diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb index 53a1f9b12..f6c26cd0f 100644 --- a/spec/features/log_in_spec.rb +++ b/spec/features/log_in_spec.rb @@ -31,12 +31,12 @@ feature "Log in" do context do given(:confirmed_at) { nil } - scenario "A unconfirmed user is not able to log in" do + scenario "A unconfirmed user is able to log in" do fill_in "user_email", with: email fill_in "user_password", with: password click_on I18n.t('auth.login') - is_expected.to have_css(".flash-message", text: failure_message("unconfirmed")) + is_expected.to have_css("div.admin-wrapper") end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 856254ce4..d7c0b5359 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -506,7 +506,7 @@ RSpec.describe User, type: :model do context 'when user is not confirmed' do let(:confirmed_at) { nil } - it { is_expected.to be false } + it { is_expected.to be true } end end @@ -522,7 +522,7 @@ RSpec.describe User, type: :model do context 'when user is not confirmed' do let(:confirmed_at) { nil } - it { is_expected.to be false } + it { is_expected.to be true } end end end -- cgit From 24367ef90619dab7dce357867cf72e3491be1330 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2019 11:07:47 +0200 Subject: Bump css-loader from 2.1.1 to 3.1.0 (#11382) * Bump css-loader from 2.1.1 to 3.1.0 Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 2.1.1 to 3.1.0. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v2.1.1...v3.1.0) Signed-off-by: dependabot-preview[bot] * localIdentName -> modules.localIdentName --- config/webpack/rules/css.js | 4 +- package.json | 2 +- yarn.lock | 93 ++++++++++++++++++++++++--------------------- 3 files changed, 53 insertions(+), 46 deletions(-) (limited to 'config') diff --git a/config/webpack/rules/css.js b/config/webpack/rules/css.js index 3b5b51232..52af1497e 100644 --- a/config/webpack/rules/css.js +++ b/config/webpack/rules/css.js @@ -9,7 +9,9 @@ module.exports = { options: { sourceMap: true, importLoaders: 2, - localIdentName: '[name]__[local]___[hash:base64:5]', + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, }, }, { diff --git a/package.json b/package.json index 58d594817..e48e1a640 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "classnames": "^2.2.5", "compression-webpack-plugin": "^3.0.0", "cross-env": "^5.1.4", - "css-loader": "^2.1.1", + "css-loader": "^3.1.0", "cssnano": "^4.1.10", "detect-passive-events": "^1.0.2", "dotenv": "^8.0.0", diff --git a/yarn.lock b/yarn.lock index c7dfd4ed3..e0643e864 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2163,15 +2163,10 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3" integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw== -camelcase@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== - -camelcase@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" - integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ== +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-api@^3.0.0: version "3.0.0" @@ -2744,22 +2739,23 @@ css-list-helpers@^1.0.1: dependencies: tcomb "^2.5.0" -css-loader@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" - integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== +css-loader@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.1.0.tgz#6f008b993b8ce812e6bab57f3cbfdc7a7cf28685" + integrity sha512-MuL8WsF/KSrHCBCYaozBKlx+r7vIfUaDTEreo7wR7Vv3J6N0z6fqWjRk3e/6wjneitXN1r/Y9FTK1psYNOBdJQ== dependencies: - camelcase "^5.2.0" - icss-utils "^4.1.0" + camelcase "^5.3.1" + cssesc "^3.0.0" + icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.14" + postcss "^7.0.17" postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^2.0.6" + postcss-modules-local-by-default "^3.0.2" postcss-modules-scope "^2.1.0" - postcss-modules-values "^2.0.0" - postcss-value-parser "^3.3.0" - schema-utils "^1.0.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.0.0" + schema-utils "^2.0.0" css-select-base-adapter@~0.1.0: version "0.1.1" @@ -4830,15 +4826,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e" - integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ== +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== dependencies: postcss "^7.0.14" @@ -7609,14 +7600,15 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" - integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== +postcss-modules-local-by-default@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" + integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - postcss-value-parser "^3.3.1" + icss-utils "^4.1.1" + postcss "^7.0.16" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.0" postcss-modules-scope@^2.1.0: version "2.1.0" @@ -7626,12 +7618,12 @@ postcss-modules-scope@^2.1.0: postcss "^7.0.6" postcss-selector-parser "^6.0.0" -postcss-modules-values@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" - integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== dependencies: - icss-replace-symbols "^1.1.0" + icss-utils "^4.0.0" postcss "^7.0.6" postcss-normalize-charset@^4.0.1: @@ -7771,7 +7763,7 @@ postcss-selector-parser@^5.0.0-rc.4: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.0: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== @@ -7799,11 +7791,16 @@ postcss-unique-selectors@^4.0.1: postcss "^7.0.0" uniqs "^2.0.0" -postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== +postcss-value-parser@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" + integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== + postcss@^5.0.16: version "5.2.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" @@ -7814,7 +7811,7 @@ postcss@^5.0.16: source-map "^0.5.6" supports-color "^3.2.3" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.5, postcss@^7.0.6: version "7.0.17" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== @@ -8944,6 +8941,14 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +schema-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.0.1.tgz#1eec2e059556af841b7f3a83b61af13d7a3f9196" + integrity sha512-HJFKJ4JixDpRur06QHwi8uu2kZbng318ahWEKgBjc0ZklcE4FDvmm2wghb448q0IRaABxIESt8vqPFvwgMB80A== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + scroll-behavior@^0.9.1: version "0.9.9" resolved "https://registry.yarnpkg.com/scroll-behavior/-/scroll-behavior-0.9.9.tgz#ebfe0658455b82ad885b66195215416674dacce2" -- cgit From fada60cbe72764756d8538017d8a0100429ccb23 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 23 Jul 2019 22:47:48 +0900 Subject: Remove modules.localIdentName (#11398) --- config/webpack/rules/css.js | 3 --- 1 file changed, 3 deletions(-) (limited to 'config') diff --git a/config/webpack/rules/css.js b/config/webpack/rules/css.js index 52af1497e..bc1f42c13 100644 --- a/config/webpack/rules/css.js +++ b/config/webpack/rules/css.js @@ -9,9 +9,6 @@ module.exports = { options: { sourceMap: true, importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, }, }, { -- cgit From d0c25584b881131986f5bb9b16c37879cddbd6c3 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 25 Jul 2019 04:22:09 +0200 Subject: Change settings to show restricted accounts only what they have access to (#11394) Follow-up to 964ae8eee593687f922c873fa7b378bb6e3e39bb --- config/navigation.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'config') diff --git a/config/navigation.rb b/config/navigation.rb index ef845d1fc..5ab2e4399 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -4,20 +4,20 @@ SimpleNavigation::Configuration.run do |navigation| navigation.items do |n| n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url - n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url do |s| + n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s| s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? } end - n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_url do |s| + n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_url, if: -> { current_user.functional? } do |s| s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_url s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_url s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url end - n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url - n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters} + n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, if: -> { current_user.functional? } + n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} @@ -25,13 +25,13 @@ SimpleNavigation::Configuration.run do |navigation| s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end - n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url do |s| + n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url, if: -> { current_user.functional? } do |s| s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url end - n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' } - n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url + n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' && current_user.functional? } + n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url, if: -> { current_user.functional? } n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s| s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url -- cgit From 0d80f686d81e4a12db1ac54b8f31f69f375dc8dc Mon Sep 17 00:00:00 2001 From: mayaeh Date: Sun, 28 Jul 2019 20:48:19 +0900 Subject: Add `tootctl preview_cards remove` (#11320) * Add `tootctl preview_cards remove` * fix code style * Remove `Scheduler::PreviewCardsCleanupScheduler` file * fix code style again Add exclude case where image_file_name is blank * Added a function to output confirmation if the specified number of days is less than 2 weeks --- .../scheduler/preview_cards_cleanup_scheduler.rb | 22 ----- config/sidekiq.yml | 3 - lib/cli.rb | 4 + lib/mastodon/preview_cards_cli.rb | 94 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 25 deletions(-) delete mode 100644 app/workers/scheduler/preview_cards_cleanup_scheduler.rb create mode 100644 lib/mastodon/preview_cards_cli.rb (limited to 'config') diff --git a/app/workers/scheduler/preview_cards_cleanup_scheduler.rb b/app/workers/scheduler/preview_cards_cleanup_scheduler.rb deleted file mode 100644 index 2b38792f0..000000000 --- a/app/workers/scheduler/preview_cards_cleanup_scheduler.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class Scheduler::PreviewCardsCleanupScheduler - include Sidekiq::Worker - - sidekiq_options unique: :until_executed, retry: 0 - - def perform - Maintenance::UncachePreviewWorker.push_bulk(recent_link_preview_cards.pluck(:id)) - Maintenance::UncachePreviewWorker.push_bulk(older_preview_cards.pluck(:id)) - end - - private - - def recent_link_preview_cards - PreviewCard.where(type: :link).where('updated_at < ?', 1.month.ago) - end - - def older_preview_cards - PreviewCard.where('updated_at < ?', 6.months.ago) - end -end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 7f41b6607..6ebe450b0 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -24,9 +24,6 @@ ip_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler - preview_cards_cleanup_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' - class: Scheduler::PreviewCardsCleanupScheduler email_scheduler: cron: '0 10 * * 2' class: Scheduler::EmailScheduler diff --git a/lib/cli.rb b/lib/cli.rb index be276583d..fbdf49fc3 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -9,6 +9,7 @@ require_relative 'mastodon/search_cli' require_relative 'mastodon/settings_cli' require_relative 'mastodon/statuses_cli' require_relative 'mastodon/domains_cli' +require_relative 'mastodon/preview_cards_cli' require_relative 'mastodon/cache_cli' require_relative 'mastodon/version' @@ -42,6 +43,9 @@ module Mastodon desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains' subcommand 'domains', Mastodon::DomainsCLI + desc 'preview_cards SUBCOMMAND ...ARGS', 'Manage preview cards' + subcommand 'preview_cards', Mastodon::PreviewCardsCLI + desc 'cache SUBCOMMAND ...ARGS', 'Manage cache' subcommand 'cache', Mastodon::CacheCLI diff --git a/lib/mastodon/preview_cards_cli.rb b/lib/mastodon/preview_cards_cli.rb new file mode 100644 index 000000000..465fe7d0b --- /dev/null +++ b/lib/mastodon/preview_cards_cli.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'tty-prompt' +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class PreviewCardsCLI < Thor + include ActionView::Helpers::NumberHelper + + def self.exit_on_failure? + true + end + + option :days, type: :numeric, default: 180 + option :background, type: :boolean, default: false + option :verbose, type: :boolean, default: false + option :dry_run, type: :boolean, default: false + option :link, type: :boolean, default: false + desc 'remove', 'Remove preview cards' + long_desc <<-DESC + Removes locally thumbnails for previews. + + The --days option specifies how old preview cards have to be before + they are removed. It defaults to 180 days. + + With the --background option, instead of deleting the files sequentially, + they will be queued into Sidekiq and the command will exit as soon as + possible. In Sidekiq they will be processed with higher concurrency, but + it may impact other operations of the Mastodon server, and it may overload + the underlying file storage. + + With the --dry-run option, no work will be done. + + With the --verbose option, when preview cards are processed sequentially in the + foreground, the IDs of the preview cards will be printed. + + With the --link option, delete only link-type preview cards. + DESC + def remove + prompt = TTY::Prompt.new + time_ago = options[:days].days.ago + queued = 0 + processed = 0 + size = 0 + dry_run = options[:dry_run] ? '(DRY RUN)' : '' + link = options[:link] ? 'link-type ' : '' + scope = PreviewCard.where.not(image_file_name: nil) + scope = scope.where.not(image_file_name: '') + scope = scope.where(type: :link) if options[:link] + scope = scope.where('updated_at < ?', time_ago) + + if time_ago > 2.weeks.ago + prompt.say "\n" + prompt.say('The preview cards less than the past two weeks will not be re-acquired even when needed.') + prompt.say "\n" + + unless prompt.yes?('Are you sure you want to delete the preview cards?', default: false) + prompt.say "\n" + prompt.warn 'Nothing execute. Bye!' + prompt.say "\n" + exit(1) + end + end + + if options[:background] + scope.select(:id, :image_file_size).reorder(nil).find_in_batches do |preview_cards| + queued += preview_cards.size + size += preview_cards.reduce(0) { |sum, p| sum + (p.image_file_size || 0) } + Maintenance::UncachePreviewWorker.push_bulk(preview_cards.map(&:id)) unless options[:dry_run] + end + + else + scope.select(:id, :image_file_size).reorder(nil).find_in_batches do |preview_cards| + preview_cards.each do |p| + size += p.image_file_size || 0 + Maintenance::UncachePreviewWorker.new.perform(p.id) unless options[:dry_run] + options[:verbose] ? say(p.id) : say('.', :green, false) + processed += 1 + end + end + end + + say + + if options[:background] + say("Scheduled the deletion of #{queued} #{link}preview cards (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true) + else + say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true) + end + end + end +end -- cgit From 24552b5160a5090e7d6056fb69a209aa48fe4fce Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 30 Jul 2019 11:10:46 +0200 Subject: Add whitelist mode (#11291) --- app/controllers/about_controller.rb | 5 +++ app/controllers/activitypub/base_controller.rb | 2 ++ app/controllers/activitypub/inboxes_controller.rb | 2 +- app/controllers/admin/domain_allows_controller.rb | 40 ++++++++++++++++++++++ app/controllers/admin/instances_controller.rb | 28 +++++++++++++-- app/controllers/api/base_controller.rb | 9 +++++ app/controllers/api/v1/accounts_controller.rb | 2 ++ app/controllers/api/v1/apps_controller.rb | 2 ++ .../api/v1/instances/activity_controller.rb | 3 +- .../api/v1/instances/peers_controller.rb | 3 +- app/controllers/api/v1/instances_controller.rb | 1 + app/controllers/application_controller.rb | 4 ++- app/controllers/concerns/account_owned_concern.rb | 1 + app/controllers/directories_controller.rb | 5 +-- app/controllers/home_controller.rb | 2 +- app/controllers/media_controller.rb | 1 + app/controllers/media_proxy_controller.rb | 2 ++ app/controllers/public_timelines_controller.rb | 5 +-- app/controllers/remote_interaction_controller.rb | 1 + app/controllers/tags_controller.rb | 1 + app/helpers/domain_control_helper.rb | 10 +++++- app/models/domain_allow.rb | 33 ++++++++++++++++++ app/models/instance.rb | 3 +- app/models/instance_filter.rb | 4 +++ app/policies/domain_allow_policy.rb | 11 ++++++ app/services/concerns/payloadable.rb | 2 +- app/services/unallow_domain_service.rb | 11 ++++++ app/views/admin/domain_allows/new.html.haml | 14 ++++++++ app/views/admin/instances/index.html.haml | 35 ++++++++++++------- app/views/admin/instances/show.html.haml | 4 ++- app/views/admin/settings/edit.html.haml | 28 ++++++++------- app/views/auth/registrations/new.html.haml | 2 +- app/views/layouts/public.html.haml | 9 +++-- config/initializers/2_whitelist_mode.rb | 5 +++ config/locales/en.yml | 7 ++++ config/locales/simple_form.en.yml | 2 ++ config/navigation.rb | 2 +- config/routes.rb | 1 + db/migrate/20190705002136_create_domain_allows.rb | 9 +++++ db/schema.rb | 9 ++++- lib/mastodon/domains_cli.rb | 22 ++++++++++-- spec/fabricators/domain_allow_fabricator.rb | 3 ++ spec/models/domain_allow_spec.rb | 5 +++ streaming/index.js | 5 +-- 44 files changed, 302 insertions(+), 53 deletions(-) create mode 100644 app/controllers/admin/domain_allows_controller.rb create mode 100644 app/models/domain_allow.rb create mode 100644 app/policies/domain_allow_policy.rb create mode 100644 app/services/unallow_domain_service.rb create mode 100644 app/views/admin/domain_allows/new.html.haml create mode 100644 config/initializers/2_whitelist_mode.rb create mode 100644 db/migrate/20190705002136_create_domain_allows.rb create mode 100644 spec/fabricators/domain_allow_fabricator.rb create mode 100644 spec/models/domain_allow_spec.rb (limited to 'config') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 31cf17710..d276e8fe5 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,6 +3,7 @@ class AboutController < ApplicationController layout 'public' + before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter before_action :set_expires_in @@ -19,6 +20,10 @@ class AboutController < ApplicationController private + def require_open_federation! + not_found if whitelist_mode? + end + def new_user User.new.tap do |user| user.build_account diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index a3b5c4dfa..0c2591e97 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::BaseController < Api::BaseController + skip_before_action :require_authenticated_user! + private def set_cache_headers diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 7cfd9a25e..bcfc1e6d4 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::InboxesController < Api::BaseController +class ActivityPub::InboxesController < ActivityPub::BaseController include SignatureVerification include JsonLdHelper include AccountOwnedConcern diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb new file mode 100644 index 000000000..31be1978b --- /dev/null +++ b/app/controllers/admin/domain_allows_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Admin::DomainAllowsController < Admin::BaseController + before_action :set_domain_allow, only: [:destroy] + + def new + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.new(domain: params[:_domain]) + end + + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.new(resource_params) + + if @domain_allow.save + log_action :create, @domain_allow + redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.created_msg') + else + render :new + end + end + + def destroy + authorize @domain_allow, :destroy? + UnallowDomainService.new.call(@domain_allow) + redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg') + end + + private + + def set_domain_allow + @domain_allow = DomainAllow.find(params[:id]) + end + + def resource_params + params.require(:domain_allow).permit(:domain) + end +end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 7888e844f..d4f201807 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -2,6 +2,10 @@ module Admin class InstancesController < BaseController + before_action :set_domain_block, only: :show + before_action :set_domain_allow, only: :show + before_action :set_instance, only: :show + def index authorize :instance, :index? @@ -11,20 +15,38 @@ module Admin def show authorize :instance, :show? - @instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id])) @following_count = Follow.where(account: Account.where(domain: params[:id])).count @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count @reports_count = Report.where(target_account: Account.where(domain: params[:id])).count @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.rule_for(params[:id]) end private + def set_domain_block + @domain_block = DomainBlock.rule_for(params[:id]) + end + + def set_domain_allow + @domain_allow = DomainAllow.rule_for(params[:id]) + end + + def set_instance + resource = Account.by_domain_accounts.find_by(domain: params[:id]) + resource ||= @domain_block + resource ||= @domain_allow + + if resource + @instance = Instance.new(resource) + else + not_found + end + end + def filtered_instances - InstanceFilter.new(filter_params).results + InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results end def paginated_instances diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 6f33a1ea9..109e38ffa 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController skip_before_action :store_current_location skip_before_action :require_functional! + before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :set_cache_headers protect_from_forgery with: :null_session @@ -69,6 +70,10 @@ class Api::BaseController < ApplicationController nil end + def require_authenticated_user! + render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user + end + def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 @@ -94,4 +99,8 @@ class Api::BaseController < ApplicationController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def disallow_unauthenticated_api_access? + authorized_fetch_mode? + end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b0c62778e..b306e8e8c 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -12,6 +12,8 @@ class Api::V1::AccountsController < Api::BaseController before_action :check_account_suspension, only: [:show] before_action :check_enabled_registrations, only: [:create] + skip_before_action :require_authenticated_user!, only: :create + respond_to :json def show diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index e9f7a7291..97177547a 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::AppsController < Api::BaseController + skip_before_action :require_authenticated_user! + def create @app = Doorkeeper::Application.create!(application_options) render json: @app, serializer: REST::ApplicationSerializer diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index d0080c5c2..4fb5a69d8 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json @@ -33,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController end def require_enabled_api! - head 404 unless Setting.activity_api_enabled + head 404 unless Setting.activity_api_enabled && !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 450e6502f..75c3cb4ba 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json @@ -14,6 +15,6 @@ class Api::V1::Instances::PeersController < Api::BaseController private def require_enabled_api! - head 404 unless Setting.peers_api_enabled + head 404 unless Setting.peers_api_enabled && !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 93e4f0003..8d8231423 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -2,6 +2,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json + skip_before_action :set_cache_headers def show diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41ce1a0ca..0d3913ee0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,12 +11,14 @@ class ApplicationController < ActionController::Base include UserTrackingConcern include SessionTrackingConcern include CacheConcern + include DomainControlHelper helper_method :current_account helper_method :current_session helper_method :current_theme helper_method :single_user_mode? helper_method :use_seamless_external_login? + helper_method :whitelist_mode? rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found @@ -38,7 +40,7 @@ class ApplicationController < ActionController::Base end def authorized_fetch_mode? - ENV['AUTHORIZED_FETCH'] == 'true' + ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode end def public_fetch_mode? diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 99c240fe9..460f71f65 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -4,6 +4,7 @@ module AccountOwnedConcern extend ActiveSupport::Concern included do + before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } before_action :set_account, if: :account_required? before_action :check_account_approval, if: :account_required? before_action :check_account_suspension, if: :account_required? diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 594907674..d2ef76f06 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -3,7 +3,8 @@ class DirectoriesController < ApplicationController layout 'public' - before_action :check_enabled + before_action :authenticate_user!, if: :whitelist_mode? + before_action :require_enabled! before_action :set_instance_presenter before_action :set_tag, only: :show before_action :set_tags @@ -19,7 +20,7 @@ class DirectoriesController < ApplicationController private - def check_enabled + def require_enabled! return not_found unless Setting.profile_directory end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 42493cd78..22d507e77 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -55,7 +55,7 @@ class HomeController < ApplicationController end def default_redirect_path - if request.path.start_with?('/web') + if request.path.start_with?('/web') || whitelist_mode? new_user_session_path elsif single_user_mode? short_account_path(Account.local.without_suspended.where('id > 0').first) diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index b3b7519a1..1f693de32 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -5,6 +5,7 @@ class MediaController < ApplicationController skip_before_action :store_current_location + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_media_attachment before_action :verify_permitted_status! before_action :check_playable, only: :player diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8fc18dd06..8da6c6fe0 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -5,6 +5,8 @@ class MediaProxyController < ApplicationController skip_before_action :store_current_location + before_action :authenticate_user!, if: :whitelist_mode? + def show RedisLock.acquire(lock_options) do |lock| if lock.acquired? diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 23506b990..324bdc508 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -3,7 +3,8 @@ class PublicTimelinesController < ApplicationController layout 'public' - before_action :check_enabled + before_action :authenticate_user!, if: :whitelist_mode? + before_action :require_enabled! before_action :set_body_classes before_action :set_instance_presenter @@ -16,7 +17,7 @@ class PublicTimelinesController < ApplicationController private - def check_enabled + def require_enabled! not_found unless Setting.timeline_preview end diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index cc6993c52..fa742fb0a 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController layout 'modal' + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_interaction_type before_action :set_status before_action :set_body_classes diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d08e5a61a..3cd2d9e20 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -8,6 +8,7 @@ class TagsController < ApplicationController layout 'public' before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_tag before_action :set_body_classes before_action :set_instance_presenter diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index efd328f81..067b2c2cd 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -12,6 +12,14 @@ module DomainControlHelper end end - DomainBlock.blocked?(domain) + if whitelist_mode? + !DomainAllow.allowed?(domain) + else + DomainBlock.blocked?(domain) + end + end + + def whitelist_mode? + Rails.configuration.x.whitelist_mode end end diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb new file mode 100644 index 000000000..85018b636 --- /dev/null +++ b/app/models/domain_allow.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: domain_allows +# +# id :bigint(8) not null, primary key +# domain :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class DomainAllow < ApplicationRecord + include DomainNormalizable + + validates :domain, presence: true, uniqueness: true + + scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + + class << self + def allowed?(domain) + !rule_for(domain).nil? + end + + def rule_for(domain) + return if domain.blank? + + uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') } + + find_by(domain: uri.normalized_host) + end + end +end diff --git a/app/models/instance.rb b/app/models/instance.rb index 797a191e0..3c740f8a2 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -7,8 +7,9 @@ class Instance def initialize(resource) @domain = resource.domain - @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count + @accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain) + @domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain) end def countable? diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb index 848fff53e..8bfab826d 100644 --- a/app/models/instance_filter.rb +++ b/app/models/instance_filter.rb @@ -12,6 +12,10 @@ class InstanceFilter scope = DomainBlock scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? scope.order(id: :desc) + elsif params[:allowed].present? + scope = DomainAllow + scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? + scope.order(id: :desc) else scope = Account.remote scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? diff --git a/app/policies/domain_allow_policy.rb b/app/policies/domain_allow_policy.rb new file mode 100644 index 000000000..5030453bb --- /dev/null +++ b/app/policies/domain_allow_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DomainAllowPolicy < ApplicationPolicy + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 953740faa..7f9f21c4b 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -14,6 +14,6 @@ module Payloadable end def signing_enabled? - ENV['AUTHORIZED_FETCH'] != 'true' + ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.whitelist_mode end end diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb new file mode 100644 index 000000000..d4387c1a1 --- /dev/null +++ b/app/services/unallow_domain_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class UnallowDomainService < BaseService + def call(domain_allow) + Account.where(domain: domain_allow.domain).find_each do |account| + SuspendAccountService.new.call(account, destroy: true) + end + + domain_allow.destroy + end +end diff --git a/app/views/admin/domain_allows/new.html.haml b/app/views/admin/domain_allows/new.html.haml new file mode 100644 index 000000000..52599857a --- /dev/null +++ b/app/views/admin/domain_allows/new.html.haml @@ -0,0 +1,14 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.domain_allows.add_new') + += simple_form_for @domain_allow, url: admin_domain_allows_path do |f| + = render 'shared/error_messages', object: @domain_allow + + .fields-group + = f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), required: true + + .actions + = f.button :button, t('admin.domain_allows.add_new'), type: :submit diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 61e578409..982dc5035 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -6,24 +6,30 @@ %strong= t('admin.instances.moderation.title') %ul %li= filter_link_to t('admin.instances.moderation.all'), limited: nil - %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' + + - unless whitelist_mode? + %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' %div{ style: 'flex: 1 1 auto; text-align: right' } - = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' + - if whitelist_mode? + = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button' + - else + = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' -= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do - .fields-group - - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] +- unless whitelist_mode? + = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| + - if params[key].present? + = hidden_field_tag key, params[key] - - %i(by_domain).each do |key| - .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") + - %i(by_domain).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") - .actions - %button= t('admin.accounts.search') - = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' %hr.spacer/ @@ -47,8 +53,11 @@ - unless first_item • = t('admin.domain_blocks.rejecting_reports') + - elsif whitelist_mode? + = t('admin.accounts.whitelisted') - else = t('admin.accounts.no_limits_imposed') - if instance.countable? .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true + = paginate paginated_instances diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index c7992a490..fbb49ba02 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -38,7 +38,9 @@ = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button' %div{ style: 'float: right' } - - if @domain_block + - if @domain_allow + = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } + - elsif @domain_block = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index b3bf3849c..1e2ed3f77 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -42,11 +42,12 @@ %hr.spacer/ - .fields-group - = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') + - unless whitelist_mode? + .fields-group + = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') - .fields-group - = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') + .fields-group + = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') .fields-group = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html') @@ -54,17 +55,18 @@ .fields-group = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html') - .fields-group - = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html') + - unless whitelist_mode? + .fields-group + = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html') - .fields-group - = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html') + .fields-group + = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html') - .fields-group - = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') + .fields-group + = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') - .fields-group - = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') + .fields-group + = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') .fields-group = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') @@ -76,7 +78,7 @@ .fields-group = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } - = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } + = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode? = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html') diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index b4a7cced5..83384d737 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -33,7 +33,7 @@ = f.input :invite_code, as: :hidden .fields-group - = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path) + = f.input :agreement, as: :boolean, wrapper: :with_label, label: whitelist_mode? ? t('auth.checkbox_agreement_without_rules_html', terms_path: terms_path) : t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path) .actions = f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 2929ac599..69738a2f7 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -10,10 +10,13 @@ = link_to root_url, class: 'brand' do = svg_logo_full - = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory - = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' - = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' + - unless whitelist_mode? + = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory + = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' + = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' + .nav-center + .nav-right - if user_signed_in? = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn' diff --git a/config/initializers/2_whitelist_mode.rb b/config/initializers/2_whitelist_mode.rb new file mode 100644 index 000000000..a17ad07a2 --- /dev/null +++ b/config/initializers/2_whitelist_mode.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.x.whitelist_mode = ENV['WHITELIST_MODE'] == 'true' +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9e1be87be..6c1a34300 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -186,6 +186,7 @@ en: username: Username warn: Warn web: Web + whitelisted: Whitelisted action_logs: actions: assigned_to_self_report: "%{name} assigned report %{target} to themselves" @@ -269,6 +270,11 @@ en: week_interactions: interactions this week week_users_active: active this week week_users_new: users this week + domain_allows: + add_new: Whitelist domain + created_msg: Domain has been successfully whitelisted + destroyed_msg: Domain has been removed from the whitelist + undo: Remove from whitelist domain_blocks: add_new: Add new domain block created_msg: Domain block is now being processed @@ -524,6 +530,7 @@ en: apply_for_account: Request an invite change_password: Password checkbox_agreement_html: I agree to the server rules and terms of service + checkbox_agreement_without_rules_html: I agree to the terms of service delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. didnt_get_confirmation: Didn't receive confirmation instructions? diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 12a7ec2b3..10b30e627 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,6 +38,8 @@ en: setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + domain_allow: + domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored featured_tag: name: 'You might want to use one of these:' imports: diff --git a/config/navigation.rb b/config/navigation.rb index 5ab2e4399..9b46da603 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -39,7 +39,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts} s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path s.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path - s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks}, if: -> { current_user.admin? } + s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index b6c215888..04424bbbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,6 +154,7 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' + resources :domain_allows, only: [:new, :create, :show, :destroy] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] diff --git a/db/migrate/20190705002136_create_domain_allows.rb b/db/migrate/20190705002136_create_domain_allows.rb new file mode 100644 index 000000000..83b0728d9 --- /dev/null +++ b/db/migrate/20190705002136_create_domain_allows.rb @@ -0,0 +1,9 @@ +class CreateDomainAllows < ActiveRecord::Migration[5.2] + def change + create_table :domain_allows do |t| + t.string :domain, default: '', null: false, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1847305c7..2d83d8b76 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_26_175042) do +ActiveRecord::Schema.define(version: 2019_07_28_084117) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -245,6 +245,13 @@ ActiveRecord::Schema.define(version: 2019_07_26_175042) do t.index ["account_id"], name: "index_custom_filters_on_account_id" end + create_table "domain_allows", force: :cascade do |t| + t.string "domain", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["domain"], name: "index_domain_allows_on_domain", unique: true + end + create_table "domain_blocks", force: :cascade do |t| t.string "domain", default: "", null: false t.datetime "created_at", null: false diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index b081581fe..f30062363 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -12,17 +12,33 @@ module Mastodon end option :dry_run, type: :boolean - desc 'purge DOMAIN', 'Remove accounts from a DOMAIN without a trace' + option :whitelist_mode, type: :boolean + desc 'purge [DOMAIN]', 'Remove accounts from a DOMAIN without a trace' long_desc <<-LONG_DESC Remove all accounts from a given DOMAIN without leaving behind any records. Unlike a suspension, if the DOMAIN still exists in the wild, it means the accounts could return if they are resolved again. + + When the --whitelist-mode option is given, instead of purging accounts + from a single domain, all accounts from domains that are not whitelisted + are removed from the database. LONG_DESC - def purge(domain) + def purge(domain = nil) removed = 0 dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - Account.where(domain: domain).find_each do |account| + scope = begin + if options[:whitelist_mode] + Account.remote.where.not(domain: DomainAllow.pluck(:domain)) + elsif domain.present? + Account.remote.where(domain: domain) + else + say('No domain given', :red) + exit(1) + end + end + + scope.find_each do |account| SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] removed += 1 say('.', :green, false) diff --git a/spec/fabricators/domain_allow_fabricator.rb b/spec/fabricators/domain_allow_fabricator.rb new file mode 100644 index 000000000..6226b1e20 --- /dev/null +++ b/spec/fabricators/domain_allow_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:domain_allow) do + domain "MyString" +end diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb new file mode 100644 index 000000000..e65435127 --- /dev/null +++ b/spec/models/domain_allow_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe DomainAllow, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/streaming/index.js b/streaming/index.js index 0529804b1..304e7e046 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -12,6 +12,7 @@ const uuid = require('uuid'); const fs = require('fs'); const env = process.env.NODE_ENV || 'development'; +const alwaysRequireAuth = process.env.WHITELIST_MODE === 'true' || process.env.AUTHORIZED_FETCH === 'true'; dotenv.config({ path: env === 'production' ? '.env.production' : '.env', @@ -271,7 +272,7 @@ const startWorker = (workerId) => { const wsVerifyClient = (info, cb) => { const location = url.parse(info.req.url, true); - const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream); + const authRequired = alwaysRequireAuth || !PUBLIC_STREAMS.some(stream => stream === location.query.stream); const allowedScopes = []; if (authRequired) { @@ -306,7 +307,7 @@ const startWorker = (workerId) => { return; } - const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); + const authRequired = alwaysRequireAuth || !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); const allowedScopes = []; if (authRequired) { -- cgit From 115dab78f1cc5357281dcb593f04ac8b2629cec6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 5 Aug 2019 19:54:29 +0200 Subject: Change admin UI for hashtags and add back whitelisted trends (#11490) Fix #271 Add back the `GET /api/v1/trends` API with the caveat that it does not return tags that have not been allowed to trend by the staff. When a hashtag begins to trend (internally) and that hashtag has not been previously reviewed by the staff, the staff is notified. The new admin UI for hashtags allows filtering hashtags by where they are used (e.g. in the profile directory), whether they have been reviewed or are pending reviewal, they show by how many people the hashtag is used in the directory, how many people used it today, how many statuses with it have been created today, and it allows fixing the name of the hashtag to make it more readable. The disallowed hashtags feature has been reworked. It is now controlled from the admin UI for hashtags instead of from the file `config/settings.yml` --- app/controllers/admin/dashboard_controller.rb | 2 +- app/controllers/admin/tags_controller.rb | 36 ++++++++----- app/controllers/api/v1/trends_controller.rb | 17 ++++++ app/controllers/settings/preferences_controller.rb | 2 +- app/helpers/admin/filter_helper.rb | 5 +- app/mailers/admin_mailer.rb | 10 ++++ app/models/application_record.rb | 11 ++++ app/models/tag.rb | 60 +++++++++++++++++++--- app/models/trending_tags.rb | 48 ++++++++--------- app/models/user.rb | 4 ++ app/policies/tag_policy.rb | 4 +- app/validators/disallowed_hashtags_validator.rb | 21 +------- app/views/admin/dashboard/index.html.haml | 2 +- app/views/admin/tags/_tag.html.haml | 24 +++++---- app/views/admin/tags/index.html.haml | 26 +++++----- app/views/admin/tags/show.html.haml | 16 ++++++ app/views/admin_mailer/new_trending_tag.text.erb | 5 ++ .../preferences/notifications/show.html.haml | 1 + config/locales/en.yml | 18 ++++--- config/locales/simple_form.en.yml | 7 +++ config/navigation.rb | 2 +- config/routes.rb | 9 +--- config/settings.yml | 1 + .../20190805123746_add_capabilities_to_tags.rb | 9 ++++ db/schema.rb | 7 ++- spec/controllers/admin/tags_controller_spec.rb | 56 ++------------------ spec/policies/tag_policy_spec.rb | 2 +- .../disallowed_hashtags_validator_spec.rb | 26 +++++----- 28 files changed, 258 insertions(+), 173 deletions(-) create mode 100644 app/controllers/api/v1/trends_controller.rb create mode 100644 app/views/admin/tags/show.html.haml create mode 100644 app/views/admin_mailer/new_trending_tag.text.erb create mode 100644 db/migrate/20190805123746_add_capabilities_to_tags.rb (limited to 'config') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index e74e4755f..70afdedd7 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -27,7 +27,7 @@ module Admin @saml_enabled = ENV['SAML_ENABLED'] == 'true' @pam_enabled = ENV['PAM_ENABLED'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' - @trending_hashtags = TrendingTags.get(7) + @trending_hashtags = TrendingTags.get(10, filtered: false) @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview @spam_check_enabled = Setting.spam_check_enabled diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index e9f4f2cfa..0e9dda302 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -4,41 +4,49 @@ module Admin class TagsController < BaseController before_action :set_tags, only: :index before_action :set_tag, except: :index - before_action :set_filter_params def index authorize :tag, :index? end - def hide - authorize @tag, :hide? - @tag.account_tag_stat.update!(hidden: true) - redirect_to admin_tags_path(@filter_params) + def show + authorize @tag, :show? end - def unhide - authorize @tag, :unhide? - @tag.account_tag_stat.update!(hidden: false) - redirect_to admin_tags_path(@filter_params) + def update + authorize @tag, :update? + + if @tag.update(tag_params.merge(reviewed_at: Time.now.utc)) + redirect_to admin_tag_path(@tag.id) + else + render :show + end end private def set_tags - @tags = Tag.discoverable - @tags.merge!(Tag.hidden) if filter_params[:hidden] + @tags = filtered_tags.page(params[:page]) end def set_tag @tag = Tag.find(params[:id]) end - def set_filter_params - @filter_params = filter_params.to_hash.symbolize_keys + def filtered_tags + scope = Tag + scope = scope.discoverable if filter_params[:context] == 'directory' + scope = scope.reviewed if filter_params[:review] == 'reviewed' + scope = scope.pending_review if filter_params[:review] == 'pending_review' + scope.reorder(score: :desc) end def filter_params - params.permit(:hidden) + params.slice(:context, :review).permit(:context, :review) + end + + def tag_params + params.require(:tag).permit(:name, :trendable, :usable, :listable) end end end diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb new file mode 100644 index 000000000..bcea9857e --- /dev/null +++ b/app/controllers/api/v1/trends_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::TrendsController < Api::BaseController + before_action :set_tags + + respond_to :json + + def index + render json: @tags, each_serializer: REST::TagSerializer + end + + private + + def set_tags + @tags = TrendingTags.get(limit_param(10)) + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 742c97cdb..d548072a8 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -56,7 +56,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_advanced_layout, :setting_use_blurhash, :setting_use_pending_items, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 0bda25974..506429e10 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,15 +5,16 @@ module Admin::FilterHelper REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze - TAGS_FILTERS = %i(hidden).freeze + TAGS_FILTERS = %i(context review).freeze INSTANCES_FILTERS = %i(limited by_domain).freeze FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS def filter_link_to(text, link_to_params, link_class_params = link_to_params) - new_url = filtered_url_for(link_to_params) + new_url = filtered_url_for(link_to_params) new_class = filtered_url_for(link_class_params) + link_to text, new_url, class: filter_link_class(new_class) end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 9ab3e2bbd..8abce5f05 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -24,4 +24,14 @@ class AdminMailer < ApplicationMailer mail to: @me.user_email, subject: I18n.t('admin_mailer.new_pending_account.subject', instance: @instance, username: @account.username) end end + + def new_trending_tag(recipient, tag) + @tag = tag + @me = recipient + @instance = Rails.configuration.x.local_domain + + locale_for_account(@me) do + mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trending_tag.subject', instance: @instance, name: @tag.name) + end + end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 83134d41a..c1b873da6 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,5 +2,16 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + include Remotable + + def boolean_with_default(key, default_value) + value = attributes[key] + + if value.nil? + default_value + else + value + end + end end diff --git a/app/models/tag.rb b/app/models/tag.rb index c7f0af86d..6a02581fa 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,11 +3,16 @@ # # Table name: tags # -# id :bigint(8) not null, primary key -# name :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# score :integer +# id :bigint(8) not null, primary key +# name :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# score :integer +# usable :boolean +# trendable :boolean +# listable :boolean +# reviewed_at :datetime +# requested_review_at :datetime # class Tag < ApplicationRecord @@ -22,16 +27,17 @@ class Tag < ApplicationRecord HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i validates :name, presence: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i } + validate :validate_name_change, if: -> { !new_record? && name_changed? } - scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } - scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :reviewed, -> { where.not(reviewed_at: nil) } + scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) } + scope :discoverable, -> { where.not(listable: false).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, :accounts_count=, :increment_count!, :decrement_count!, - :hidden?, to: :account_tag_stat after_save :save_account_tag_stat @@ -48,6 +54,40 @@ class Tag < ApplicationRecord name end + def usable + boolean_with_default('usable', true) + end + + alias usable? usable + + def listable + boolean_with_default('listable', true) + end + + alias listable? listable + + def trendable + boolean_with_default('trendable', false) + end + + alias trendable? trendable + + def requires_review? + reviewed_at.nil? + end + + def reviewed? + reviewed_at.present? + end + + def requested_review? + requested_review_at.present? + end + + def trending? + TrendingTags.trending?(self) + end + def history days = [] @@ -117,4 +157,8 @@ class Tag < ApplicationRecord return unless account_tag_stat&.changed? account_tag_stat.save end + + def validate_name_change + errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero? + end end diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 211c8f1dc..e9b9b25e3 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -10,20 +10,28 @@ class TrendingTags include Redisable def record_use!(tag, account, at_time = Time.now.utc) - return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot? + return if account.silenced? || account.bot? || !tag.usable? || !(tag.trendable? || tag.requires_review?) increment_historical_use!(tag.id, at_time) increment_unique_use!(tag.id, account.id, at_time) - increment_vote!(tag.id, at_time) + increment_vote!(tag, at_time) end - def get(limit) - key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}" - tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i) - tags = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag } + def get(limit, filtered: true) + tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, limit - 1).map(&:to_i) + + tags = Tag.where(id: tag_ids) + tags = tags.where(trendable: true) if filtered + tags = tags.each_with_object({}) { |tag, h| h[tag.id] = tag } + tag_ids.map { |tag_id| tags[tag_id] }.compact end + def trending?(tag) + rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id) + rank.present? && rank <= 10 + end + private def increment_historical_use!(tag_id, at_time) @@ -38,33 +46,27 @@ class TrendingTags redis.expire(key, EXPIRE_HISTORY_AFTER) end - def increment_vote!(tag_id, at_time) + def increment_vote!(tag, at_time) key = "#{KEY}:#{at_time.beginning_of_day.to_i}" - expected = redis.pfcount("activity:tags:#{tag_id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f + expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f expected = 1.0 if expected.zero? - observed = redis.pfcount("activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts").to_f + observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f if expected > observed || observed < THRESHOLD - redis.zrem(key, tag_id.to_s) + redis.zrem(key, tag.id) else - score = ((observed - expected)**2) / expected - added = redis.zadd(key, score, tag_id.to_s) - bump_tag_score!(tag_id) if added + score = ((observed - expected)**2) / expected + old_rank = redis.zrevrank(key, tag.id) + + redis.zadd(key, score, tag.id) + request_review!(tag) if (old_rank.nil? || old_rank > 10) && redis.zrevrank(key, tag.id) <= 10 && !tag.trendable? && tag.requires_review? && !tag.requested_review? end redis.expire(key, EXPIRE_TRENDS_AFTER) end - def bump_tag_score!(tag_id) - Tag.where(id: tag_id).update_all('score = COALESCE(score, 0) + 1') - end - - def disallowed_hashtags - return @disallowed_hashtags if defined?(@disallowed_hashtags) - - @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags - @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String - @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + def request_review!(tag) + User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? } end end end diff --git a/app/models/user.rb b/app/models/user.rb index 6806c0362..b83e26af3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -207,6 +207,10 @@ class User < ApplicationRecord settings.notification_emails['pending_account'] end + def allows_trending_tag_emails? + settings.notification_emails['trending_tag'] + end + def hides_network? @hides_network ||= settings.hide_network end diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb index c63de01db..aaf70fcab 100644 --- a/app/policies/tag_policy.rb +++ b/app/policies/tag_policy.rb @@ -5,11 +5,11 @@ class TagPolicy < ApplicationPolicy staff? end - def hide? + def show? staff? end - def unhide? + def update? staff? end end diff --git a/app/validators/disallowed_hashtags_validator.rb b/app/validators/disallowed_hashtags_validator.rb index ee06b20f6..d745b767f 100644 --- a/app/validators/disallowed_hashtags_validator.rb +++ b/app/validators/disallowed_hashtags_validator.rb @@ -4,24 +4,7 @@ class DisallowedHashtagsValidator < ActiveModel::Validator def validate(status) return unless status.local? && !status.reblog? - @status = status - tags = select_tags - - status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) unless tags.empty? - end - - private - - def select_tags - tags = Extractor.extract_hashtags(@status.text) - tags.keep_if { |tag| disallowed_hashtags.include? tag.downcase } - end - - def disallowed_hashtags - return @disallowed_hashtags if @disallowed_hashtags - - @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags - @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String - @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + disallowed_hashtags = Tag.matching_name(Extractor.extract_hashtags(status.text)).reject(&:usable?) + status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: disallowed_hashtags.map(&:name).join(', '), count: disallowed_hashtags.size)) unless disallowed_hashtags.empty? end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 77cc1a2a0..910896075 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -107,5 +107,5 @@ %ul - @trending_hashtags.each do |tag| %li - = link_to "##{tag.name}", web_url("timelines/tag/#{tag.name}") + = link_to content_tag(:span, "##{tag.name}", class: !tag.trendable? && !tag.reviewed? ? 'warning-hint' : (!tag.trendable? ? 'negative-hint' : nil)), admin_tag_path(tag.id) %span.pull-right= number_with_delimiter(tag.history[0][:accounts].to_i) diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml index 961b83f93..91af8e492 100644 --- a/app/views/admin/tags/_tag.html.haml +++ b/app/views/admin/tags/_tag.html.haml @@ -1,12 +1,16 @@ -%tr - %td - = link_to explore_hashtag_path(tag) do +.directory__tag + = link_to admin_tag_path(tag.id) do + %h4 = fa_icon 'hashtag' = tag.name - %td - = t('directories.people', count: tag.accounts_count) - %td - - if tag.hidden? - = table_link_to 'eye', t('admin.tags.unhide'), unhide_admin_tag_path(tag.id, **@filter_params), method: :post - - else - = table_link_to 'eye-slash', t('admin.tags.hide'), hide_admin_tag_path(tag.id, **@filter_params), method: :post + + %small + = t('admin.tags.in_directory', count: tag.accounts_count) + • + = t('admin.tags.unique_uses_today', count: tag.history.first[:accounts]) + + - if tag.trending? + = fa_icon 'fire fw' + = t('admin.tags.trending_right_now') + + .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index 4ba395860..5e4ee21f5 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -3,17 +3,19 @@ .filters .filter-subset - %strong= t('admin.reports.status') + %strong= t('admin.tags.context') %ul - %li= filter_link_to t('admin.tags.visible'), hidden: nil - %li= filter_link_to t('admin.tags.hidden'), hidden: '1' + %li= filter_link_to t('generic.all'), context: nil + %li= filter_link_to t('admin.tags.directory'), context: 'directory' -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.tags.name') - %th= t('admin.tags.accounts') - %th - %tbody - = render @tags + .filter-subset + %strong= t('admin.tags.review') + %ul + %li= filter_link_to t('generic.all'), review: nil + %li= filter_link_to t('admin.tags.reviewed'), review: 'reviewed' + %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), review: 'pending_review' + +%hr.spacer/ + += render @tags += paginate @tags diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml new file mode 100644 index 000000000..27c8dc92b --- /dev/null +++ b/app/views/admin/tags/show.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = "##{@tag.name}" + += simple_form_for @tag, url: admin_tag_path(@tag.id) do |f| + = render 'shared/error_messages', object: @tag + + .fields-group + = f.input :name, wrapper: :with_block_label + + .fields-group + = f.input :usable, as: :boolean, wrapper: :with_label + = f.input :trendable, as: :boolean, wrapper: :with_label + = f.input :listable, as: :boolean, wrapper: :with_label + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin_mailer/new_trending_tag.text.erb b/app/views/admin_mailer/new_trending_tag.text.erb new file mode 100644 index 000000000..f3087df37 --- /dev/null +++ b/app/views/admin_mailer/new_trending_tag.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_trending_tag.body', name: @tag.name) %> + +<%= raw t('application_mailer.view')%> <%= admin_tags_url(review: 'pending_review') %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index acc646fc3..f666ae4ff 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -15,6 +15,7 @@ - if current_user.staff? = ff.input :report, as: :boolean, wrapper: :with_label = ff.input :pending_account, as: :boolean, wrapper: :with_label + = ff.input :trending_tag, as: :boolean, wrapper: :with_label .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| diff --git a/config/locales/en.yml b/config/locales/en.yml index 6c1a34300..9b62aac3a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -483,13 +483,14 @@ en: title: Account statuses with_media: With media tags: - accounts: Accounts - hidden: Hidden - hide: Hide from directory - name: Hashtag + context: Context + directory: In directory + in_directory: "%{count} in directory" + review: Review status + reviewed: Reviewed title: Hashtags - unhide: Show in directory - visible: Visible + trending_right_now: Trending right now + unique_uses_today: "%{count} posting today" title: Administration warning_presets: add_new: Add new @@ -505,6 +506,9 @@ en: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} subject: New report for %{instance} (#%{id}) + new_trending_tag: + body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.' + subject: New hashtag up for review on %{instance} (#%{name}) appearance: advanced_web_interface: Advanced web interface advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.' @@ -939,6 +943,8 @@ en: pinned: Pinned toot reblogged: boosted sensitive_content: Sensitive content + tags: + does_not_match_previous_name: does not match the previous name terms: body_html: |

    Privacy Policy

    diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 10b30e627..6fdfc9d7b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -48,6 +48,8 @@ en: text: This will help us review your application sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' + tag: + name: You can only change the casing of the letters, for example, to make it more readable user: chosen_languages: When checked, only toots in selected languages will be displayed in public timelines labels: @@ -137,6 +139,11 @@ en: pending_account: Send e-mail when a new account needs review reblog: Send e-mail when someone boosts your status report: Send e-mail when a new report is submitted + trending_tag: Send e-mail when an unreviewed hashtag is trending + tag: + listable: Allow this hashtag to appear on the profile directory + trendable: Allow this hashtag to appear under trends + usable: Allow toots to use this hashtag 'no': 'No' recommended: Recommended required: diff --git a/config/navigation.rb b/config/navigation.rb index 9b46da603..38668bbf7 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -38,7 +38,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts} s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path - s.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path + s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags} s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 04424bbbd..60f7d2e05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -243,13 +243,7 @@ Rails.application.routes.draw do end resources :account_moderation_notes, only: [:create, :destroy] - - resources :tags, only: [:index] do - member do - post :hide - post :unhide - end - end + resources :tags, only: [:index, :show, :update] end get '/admin', to: redirect('/admin/dashboard', status: 302) @@ -311,6 +305,7 @@ Rails.application.routes.draw do resources :mutes, only: [:index] resources :favourites, only: [:index] resources :reports, only: [:create] + resources :trends, only: [:index] resources :filters, only: [:index, :create, :show, :update, :destroy] resources :endorsements, only: [:index] diff --git a/config/settings.yml b/config/settings.yml index ad2970bb7..10180201f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -43,6 +43,7 @@ defaults: &defaults digest: true report: true pending_account: true + trending_tag: true interactions: must_be_follower: false must_be_following: false diff --git a/db/migrate/20190805123746_add_capabilities_to_tags.rb b/db/migrate/20190805123746_add_capabilities_to_tags.rb new file mode 100644 index 000000000..43c7763b1 --- /dev/null +++ b/db/migrate/20190805123746_add_capabilities_to_tags.rb @@ -0,0 +1,9 @@ +class AddCapabilitiesToTags < ActiveRecord::Migration[5.2] + def change + add_column :tags, :usable, :boolean + add_column :tags, :trendable, :boolean + add_column :tags, :listable, :boolean + add_column :tags, :reviewed_at, :datetime + add_column :tags, :requested_review_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index e3af9c31a..d1b6825b4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_29_185330) do +ActiveRecord::Schema.define(version: 2019_08_05_123746) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -660,6 +660,11 @@ ActiveRecord::Schema.define(version: 2019_07_29_185330) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "score" + t.boolean "usable" + t.boolean "trendable" + t.boolean "listable" + t.datetime "reviewed_at" + t.datetime "requested_review_at" t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true end diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 3af994071..5c1944fc7 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -10,62 +10,14 @@ RSpec.describe Admin::TagsController, type: :controller do end describe 'GET #index' do - before do - account_tag_stat = Fabricate(:tag).account_tag_stat - account_tag_stat.update(hidden: hidden, accounts_count: 1) - get :index, params: { hidden: hidden } - end - - context 'with hidden tags' do - let(:hidden) { true } - - it 'returns status 200' do - expect(response).to have_http_status(200) - end - end - - context 'without hidden tags' do - let(:hidden) { false } - - it 'returns status 200' do - expect(response).to have_http_status(200) - end - end - end - - describe 'POST #hide' do - let(:tag) { Fabricate(:tag) } + let!(:tag) { Fabricate(:tag) } before do - tag.account_tag_stat.update(hidden: false) - post :hide, params: { id: tag.id } - end - - it 'hides tag' do - tag.reload - expect(tag).to be_hidden - end - - it 'redirects to admin_tags_path' do - expect(response).to redirect_to(admin_tags_path(controller.instance_variable_get(:@filter_params))) - end - end - - describe 'POST #unhide' do - let(:tag) { Fabricate(:tag) } - - before do - tag.account_tag_stat.update(hidden: true) - post :unhide, params: { id: tag.id } - end - - it 'unhides tag' do - tag.reload - expect(tag).not_to be_hidden + get :index end - it 'redirects to admin_tags_path' do - expect(response).to redirect_to(admin_tags_path(controller.instance_variable_get(:@filter_params))) + it 'returns status 200' do + expect(response).to have_http_status(200) end end end diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb index c7afaa7c9..c63875dc0 100644 --- a/spec/policies/tag_policy_spec.rb +++ b/spec/policies/tag_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe TagPolicy do let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } - permissions :index?, :hide?, :unhide? do + permissions :index?, :show?, :update? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Tag) diff --git a/spec/validators/disallowed_hashtags_validator_spec.rb b/spec/validators/disallowed_hashtags_validator_spec.rb index 8ec1302ab..9deec0bb9 100644 --- a/spec/validators/disallowed_hashtags_validator_spec.rb +++ b/spec/validators/disallowed_hashtags_validator_spec.rb @@ -3,42 +3,44 @@ require 'rails_helper' RSpec.describe DisallowedHashtagsValidator, type: :validator do + let(:disallowed_tags) { [] } + describe '#validate' do before do - allow_any_instance_of(described_class).to receive(:select_tags) { tags } + disallowed_tags.each { |name| Fabricate(:tag, name: name, usable: false) } described_class.new.validate(status) end - let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: '') } + let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| '#' + x }.join(' ')) } let(:errors) { double(add: nil) } - context 'unless status.local? && !status.reblog?' do + context 'for a remote reblog' do let(:local) { false } let(:reblog) { true } - it 'not calls errors.add' do + it 'does not add errors' do expect(errors).not_to have_received(:add).with(:text, any_args) end end - context 'status.local? && !status.reblog?' do + context 'for a local original status' do let(:local) { true } let(:reblog) { false } - context 'tags.empty?' do - let(:tags) { [] } + context 'when does not contain any disallowed hashtags' do + let(:disallowed_tags) { [] } - it 'not calls errors.add' do + it 'does not add errors' do expect(errors).not_to have_received(:add).with(:text, any_args) end end - context '!tags.empty?' do - let(:tags) { %w(a b c) } + context 'when contains disallowed hashtags' do + let(:disallowed_tags) { %w(a b c) } - it 'calls errors.add' do + it 'adds an error' do expect(errors).to have_received(:add) - .with(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) + .with(:text, I18n.t('statuses.disallowed_hashtags', tags: disallowed_tags.join(', '), count: disallowed_tags.size)) end end end -- cgit From 9072fe5ab6464cc9c7a871d388464c7afcf41cd0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 6 Aug 2019 17:57:52 +0200 Subject: Add trends UI with admin and user settings (#11502) --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/actions/trends.js | 32 ++++++++++++++++ .../features/getting_started/components/trends.js | 43 ++++++++++++++++++++++ .../getting_started/containers/trends_container.js | 13 +++++++ .../mastodon/features/getting_started/index.js | 5 ++- .../features/ui/components/navigation_panel.js | 6 ++- app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/settings.js | 4 ++ app/javascript/mastodon/reducers/trends.js | 23 ++++++++++++ app/javascript/styles/mastodon/components.scss | 38 +++++++++++++++---- app/lib/user_settings_decorator.rb | 5 +++ app/models/form/admin_settings.rb | 2 + app/models/trending_tags.rb | 4 ++ app/models/user.rb | 3 +- app/serializers/initial_state_serializer.rb | 2 + app/views/admin/settings/edit.html.haml | 3 ++ .../settings/preferences/appearance/show.html.haml | 5 +++ config/locales/en.yml | 8 +++- config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + 21 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 app/javascript/mastodon/actions/trends.js create mode 100644 app/javascript/mastodon/features/getting_started/components/trends.js create mode 100644 app/javascript/mastodon/features/getting_started/containers/trends_container.js create mode 100644 app/javascript/mastodon/reducers/trends.js (limited to 'config') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index d548072a8..edf29947b 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -56,6 +56,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_advanced_layout, :setting_use_blurhash, :setting_use_pending_items, + :setting_trends, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 000000000..853e4f60a --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js new file mode 100644 index 000000000..1dcacc8b3 --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/components/trends.js @@ -0,0 +1,43 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Hashtag from 'mastodon/components/hashtag'; + +export default class Trends extends ImmutablePureComponent { + + static defaultProps = { + loading: false, + }; + + static propTypes = { + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, + }; + + componentDidMount () { + this.props.fetchTrends(); + this.refreshInterval = setInterval(() => this.props.fetchTrends(), 36000); + } + + componentWillUnmount () { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + } + + render () { + const { trends } = this.props; + + if (!trends || trends.isEmpty()) { + return null; + } + + return ( +
    + {trends.take(3).map(hashtag => )} +
    + ); + } + +} diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js new file mode 100644 index 000000000..1df3fb4fe --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { fetchTrends } from '../../../actions/trends'; +import Trends from '../components/trends'; + +const mapStateToProps = state => ({ + trends: state.getIn(['trends', 'items']), +}); + +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Trends); diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 791f22d47..6a122a750 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -7,12 +7,13 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, profile_directory } from '../../initial_state'; +import { me, profile_directory, showTrends } from '../../initial_state'; import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { List as ImmutableList } from 'immutable'; import NavigationBar from '../compose/components/navigation_bar'; import Icon from 'mastodon/components/icon'; import LinkFooter from 'mastodon/features/ui/components/link_footer'; +import TrendsContainer from './containers/trends_container'; const messages = defineMessages({ home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, @@ -168,6 +169,8 @@ class GettingStarted extends ImmutablePureComponent { + + {multiColumn && showTrends && } ); } diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index ef3ad2e09..64a40a9da 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -2,10 +2,11 @@ import React from 'react'; import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; -import { profile_directory } from 'mastodon/initial_state'; +import { profile_directory, showTrends } from 'mastodon/initial_state'; import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; +import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container'; const NavigationPanel = () => (
    @@ -25,6 +26,9 @@ const NavigationPanel = () => ( {!!profile_directory && } + + {showTrends &&
    } + {showTrends && }
    ); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index cb2ccc7c4..38e7b0595 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -22,5 +22,6 @@ export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); +export const showTrends = getMeta('trends'); export default initialState; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 981ad8e64..3b60878eb 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -31,6 +31,7 @@ import conversations from './conversations'; import suggestions from './suggestions'; import polls from './polls'; import identity_proofs from './identity_proofs'; +import trends from './trends'; const reducers = { dropdown_menu, @@ -65,6 +66,7 @@ const reducers = { conversations, suggestions, polls, + trends, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 033bfc999..793a99f8f 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -12,6 +12,10 @@ const initialState = ImmutableMap({ skinTone: 1, + trends: ImmutableMap({ + show: true, + }), + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js new file mode 100644 index 000000000..5cecc8fca --- /dev/null +++ b/app/javascript/mastodon/reducers/trends.js @@ -0,0 +1,23 @@ +import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, +}); + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_REQUEST: + return state.set('isLoading', true); + case TRENDS_FETCH_SUCCESS: + return state.withMutations(map => { + map.set('items', fromJS(action.trends)); + map.set('isLoading', false); + }); + case TRENDS_FETCH_FAIL: + return state.set('isLoading', false); + default: + return state; + } +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f02458ded..8de72d72e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2212,7 +2212,6 @@ a.account__display-name { } .getting-started__wrapper, - .getting-started__trends, .search { margin-bottom: 10px; } @@ -2319,13 +2318,24 @@ a.account__display-name { margin-bottom: 10px; height: calc(100% - 20px); overflow-y: auto; + display: flex; + flex-direction: column; + + & > a { + flex: 0 0 auto; + } hr { + flex: 0 0 auto; border: 0; background: transparent; border-top: 1px solid lighten($ui-base-color, 4%); margin: 10px 0; } + + .flex-spacer { + background: transparent; + } } .drawer__pager { @@ -2717,8 +2727,10 @@ a.account__display-name { } &__trends { - background: $ui-base-color; flex: 0 1 auto; + opacity: 1; + animation: fade 150ms linear; + margin-top: 10px; @media screen and (max-height: 810px) { .trends__item:nth-child(3) { @@ -2735,11 +2747,15 @@ a.account__display-name { @media screen and (max-height: 670px) { display: none; } - } - &__scrollable { - max-height: 100%; - overflow-y: auto; + .trends__item { + border-bottom: 0; + padding: 10px; + + &__current { + color: $darker-text-color; + } + } } } @@ -5968,7 +5984,8 @@ noscript { font-size: 24px; line-height: 36px; font-weight: 500; - text-align: center; + text-align: right; + padding-right: 15px; color: $secondary-text-color; } @@ -5976,7 +5993,12 @@ noscript { flex: 0 0 auto; width: 50px; - path { + path:first-child { + fill: rgba($highlight-text-color, 0.25) !important; + fill-opacity: 1 !important; + } + + path:last-child { stroke: lighten($highlight-text-color, 6%) !important; } } diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 9ae9986c2..3568a3e11 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -36,6 +36,7 @@ class UserSettingsDecorator user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') + user.settings['trends'] = trends_preference if change?('setting_trends') end def merged_notification_emails @@ -122,6 +123,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_use_pending_items' end + def trends_preference + boolean_cast_setting 'setting_trends' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 2c03c88a8..051268375 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -29,6 +29,7 @@ class Form::AdminSettings hero mascot spam_check_enabled + trends ).freeze BOOLEAN_KEYS = %i( @@ -41,6 +42,7 @@ class Form::AdminSettings preview_sensitive_media profile_directory spam_check_enabled + trends ).freeze UPLOAD_KEYS = %i( diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index e9b9b25e3..0a7e2feac 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -66,6 +66,10 @@ class TrendingTags end def request_review!(tag) + return unless Setting.trends + + tag.touch(:requested_review_at) + User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? } end end diff --git a/app/models/user.rb b/app/models/user.rb index b83e26af3..a4a20d975 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -107,7 +107,8 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false + :advanced_layout, :use_blurhash, :use_pending_items, :trends, + to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 7e5d3eda9..c92c5e606 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -20,6 +20,7 @@ class InitialStateSerializer < ActiveModel::Serializer invites_enabled: Setting.min_invite_role == 'user', mascot: instance_presenter.mascot&.file&.url, profile_directory: Setting.profile_directory, + trends: Setting.trends, } if object.current_account @@ -35,6 +36,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:is_staff] = object.current_account.user.staff? + store[:trends] = Setting.trends && object.current_account.user.setting_trends end store diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 1e2ed3f77..28c0ece15 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -68,6 +68,9 @@ .fields-group = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') + .fields-group + = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html') + .fields-group = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index e279a61c4..d6ee1933f 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -25,6 +25,11 @@ = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label + %h4= t 'appearance.discovery' + + .fields-group + = f.input :setting_trends, as: :boolean, wrapper: :with_label + %h4= t 'appearance.confirmation_dialogs' .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index 9b62aac3a..67c392662 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -460,8 +460,8 @@ en: title: Custom terms of service site_title: Server name spam_check_enabled: - desc_html: Mastodon can auto-silence and auto-report accounts based on measures such as detecting accounts who send repeated unsolicited messages. There may be false positives. - title: Anti-spam + desc_html: Mastodon can auto-silence and auto-report accounts that send repeated unsolicited messages. There may be false positives. + title: Anti-spam automation thumbnail: desc_html: Used for previews via OpenGraph and API. 1200x630px recommended title: Server thumbnail @@ -469,6 +469,9 @@ en: desc_html: Display public timeline on landing page title: Timeline preview title: Site settings + trends: + desc_html: Publicly display previously reviewed hashtags that are currently trending + title: Trending hashtags statuses: back_to_account: Back to account page batch: @@ -514,6 +517,7 @@ en: advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.' animations_and_accessibility: Animations and accessibility confirmation_dialogs: Confirmation dialogs + discovery: Discovery sensitive_content: Sensitive content application_mailer: notification_preferences: Change e-mail preferences diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 6fdfc9d7b..e15d5904f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -114,6 +114,7 @@ en: setting_show_application: Disclose application used to send toots setting_system_font_ui: Use system's default font setting_theme: Site theme + setting_trends: Show today's trends setting_unfollow_modal: Show confirmation dialog before unfollowing someone setting_use_blurhash: Show colorful gradients for hidden media setting_use_pending_items: Slow mode diff --git a/config/settings.yml b/config/settings.yml index 10180201f..4e5eefb59 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -34,6 +34,7 @@ defaults: &defaults advanced_layout: false use_blurhash: true use_pending_items: false + trends: true notification_emails: follow: false reblog: false -- cgit From dd38c280a50a8feb70ad341c3561fe2f87c8cf3d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 6 Aug 2019 19:40:06 +0200 Subject: Fix admin dashboard missing latest features (#11505) Fix redis-namespace deprecation warning about administrative commands --- app/controllers/admin/dashboard_controller.rb | 11 ++++++++++- app/views/admin/dashboard/index.html.haml | 6 ++++++ config/locales/en.yml | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) (limited to 'config') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 70afdedd7..ab56065e0 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -28,9 +28,12 @@ module Admin @pam_enabled = ENV['PAM_ENABLED'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @trending_hashtags = TrendingTags.get(10, filtered: false) + @authorized_fetch = authorized_fetch_mode? + @whitelist_enabled = whitelist_mode? @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview @spam_check_enabled = Setting.spam_check_enabled + @trends_enabled = Setting.trends end private @@ -40,7 +43,13 @@ module Admin end def redis_info - @redis_info ||= Redis.current.info + @redis_info ||= begin + if Redis.current.is_a?(Redis::Namespace) + Redis.current.redis.info + else + Redis.current.info + end + end end end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 910896075..f567b81e8 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -49,6 +49,8 @@ = feature_hint(link_to(t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path), @profile_directory) %li = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview) + %li + = feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled) %li = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled) %li @@ -90,6 +92,10 @@ = feature_hint(t('admin.dashboard.search'), @search_enabled) %li = feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode) + %li + = feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch) + %li + = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_mode) %li = feature_hint('LDAP', @ldap_enabled) %li diff --git a/config/locales/en.yml b/config/locales/en.yml index 67c392662..333d4f172 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -247,6 +247,7 @@ en: updated_msg: Emoji successfully updated! upload: Upload dashboard: + authorized_fetch_mode: Authorized fetch mode backlog: backlogged jobs config: Configuration feature_deletions: Account deletions @@ -270,6 +271,7 @@ en: week_interactions: interactions this week week_users_active: active this week week_users_new: users this week + whitelist_mode: Whitelist mode domain_allows: add_new: Whitelist domain created_msg: Domain has been successfully whitelisted @@ -565,6 +567,7 @@ en: status: account_status: Account status confirming: Waiting for e-mail confirmation to be completed. + functional: Your account is fully operational. pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. trouble_logging_in: Trouble logging in? authorize_follow: -- cgit From ac33f1aedd9a6c72c6c176afb1f5d62a1ce5d44d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 7 Aug 2019 10:01:55 +0200 Subject: Fix account tags not being saved correctly (#11507) * Fix account tags not being saved correctly Regression from f371b32 Fix Tag#discoverable not returning tags where listable is nil instead of true Add notice when saving hashtags in admin UI Change public hashtag and directory pages to return 404 for forbidden tags * Remove unused locale string --- app/controllers/admin/tags_controller.rb | 2 +- app/controllers/directories_controller.rb | 2 +- app/controllers/tags_controller.rb | 2 +- app/models/account.rb | 12 +----------- app/models/tag.rb | 3 ++- config/locales/en.yml | 1 + 6 files changed, 7 insertions(+), 15 deletions(-) (limited to 'config') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 0e9dda302..ed271aedc 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -17,7 +17,7 @@ module Admin authorize @tag, :update? if @tag.update(tag_params.merge(reviewed_at: Time.now.utc)) - redirect_to admin_tag_path(@tag.id) + redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg') else render :show end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index d2ef76f06..a5c47b515 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -25,7 +25,7 @@ class DirectoriesController < ApplicationController end def set_tag - @tag = Tag.discoverable.find_by!(name: params[:id].downcase) + @tag = Tag.discoverable.find_normalized!(params[:id]) end def set_tags diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3cd2d9e20..5a6fcc8fd 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -47,7 +47,7 @@ class TagsController < ApplicationController private def set_tag - @tag = Tag.find_normalized!(params[:id]) + @tag = Tag.usable.find_normalized!(params[:id]) end def set_body_classes diff --git a/app/models/account.rb b/app/models/account.rb index ccd116d6e..b205c8c9e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -227,17 +227,7 @@ class Account < ApplicationRecord end def tags_as_strings=(tag_names) - tag_names.map! { |name| name.mb_chars.downcase.to_s } - tag_names.uniq! - - # Existing hashtags - hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } - - # Initialize not yet existing hashtags - tag_names.each do |name| - next if hashtags_map.key?(name) - hashtags_map[name] = Tag.new(name: name) - end + hashtags_map = Tag.find_or_create_by_names(tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } # Remove hashtags that are to be deleted tags.each do |tag| diff --git a/app/models/tag.rb b/app/models/tag.rb index 6a02581fa..e2fe91da1 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -31,7 +31,8 @@ class Tag < ApplicationRecord scope :reviewed, -> { where.not(reviewed_at: nil) } scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) } - scope :discoverable, -> { where.not(listable: false).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } + scope :usable, -> { where(usable: [true, nil]) } + scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, diff --git a/config/locales/en.yml b/config/locales/en.yml index 333d4f172..20baf634e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -496,6 +496,7 @@ en: title: Hashtags trending_right_now: Trending right now unique_uses_today: "%{count} posting today" + updated_msg: Hashtag settings updated successfully title: Administration warning_presets: add_new: Add new -- cgit From 7a737c79cc06e931afef2eaebd971ea0324e0741 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 7 Aug 2019 16:13:34 +0200 Subject: Add number of pending accounts and pending hashtags to admin dashboard (#11514) --- app/controllers/admin/dashboard_controller.rb | 4 +++- app/views/admin/dashboard/index.html.haml | 14 +++++++++++--- app/views/admin/tags/show.html.haml | 4 ++-- config/locales/en.yml | 2 ++ 4 files changed, 18 insertions(+), 6 deletions(-) (limited to 'config') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index ab56065e0..7c2951acb 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -5,6 +5,7 @@ module Admin class DashboardController < BaseController def index @users_count = User.count + @pending_users_count = User.pending.count @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @logins_week = Redis.current.pfcount("activity:logins:#{current_week}") @interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 @@ -19,7 +20,7 @@ module Admin @redis_version = redis_info['redis_version'] @reports_count = Report.unresolved.count @queue_backlog = Sidekiq::Stats.new.enqueued - @recent_users = User.confirmed.recent.includes(:account).limit(4) + @recent_users = User.confirmed.recent.includes(:account).limit(8) @database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size'] @redis_size = redis_info['used_memory'] @ldap_enabled = ENV['LDAP_ENABLED'] == 'true' @@ -28,6 +29,7 @@ module Admin @pam_enabled = ENV['PAM_ENABLED'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @trending_hashtags = TrendingTags.get(10, filtered: false) + @pending_tags_count = Tag.pending_review.count @authorized_fetch = authorized_fetch_mode? @whitelist_enabled = whitelist_mode? @profile_directory = Setting.profile_directory diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index f567b81e8..2fe1feb55 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -15,13 +15,21 @@ .dashboard__counters__num= number_with_delimiter @logins_week .dashboard__counters__label= t 'admin.dashboard.week_users_active' %div - %div - .dashboard__counters__num= number_with_delimiter @interactions_week - .dashboard__counters__label= t 'admin.dashboard.week_interactions' + = link_to admin_pending_accounts_path do + .dashboard__counters__num= number_with_delimiter @pending_users_count + .dashboard__counters__label= t 'admin.dashboard.pending_users' %div = link_to admin_reports_url do .dashboard__counters__num= number_with_delimiter @reports_count .dashboard__counters__label= t 'admin.dashboard.open_reports' + %div + = link_to admin_tags_path(review: 'pending_review') do + .dashboard__counters__num= number_with_delimiter @pending_tags_count + .dashboard__counters__label= t 'admin.dashboard.pending_tags' + %div + %div + .dashboard__counters__num= number_with_delimiter @interactions_week + .dashboard__counters__label= t 'admin.dashboard.week_interactions' %div = link_to sidekiq_url do .dashboard__counters__num= number_with_delimiter @queue_backlog diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml index 27c8dc92b..5f3a8e4d9 100644 --- a/app/views/admin/tags/show.html.haml +++ b/app/views/admin/tags/show.html.haml @@ -9,8 +9,8 @@ .fields-group = f.input :usable, as: :boolean, wrapper: :with_label - = f.input :trendable, as: :boolean, wrapper: :with_label - = f.input :listable, as: :boolean, wrapper: :with_label + = f.input :trendable, as: :boolean, wrapper: :with_label, disabled: !Setting.trends + = f.input :listable, as: :boolean, wrapper: :with_label, disabled: !Setting.profile_directory .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 20baf634e..7b24df016 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -260,6 +260,8 @@ en: features: Features hidden_service: Federation with hidden services open_reports: open reports + pending_tags: hashtags waiting for review + pending_users: users waiting for review recent_users: Recent users search: Full-text search single_user_mode: Single user mode -- cgit From 94c54997cf6dc3bef2af67a070a61cc10595339c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 7 Aug 2019 17:08:30 +0200 Subject: Fix trending tags returning less items than requested sometimes (#11513) Add better sorting defaults to the hashtags admin UI Add "not reviewed" filter to hashtags admin UI --- app/controllers/admin/tags_controller.rb | 7 ++++--- app/models/tag.rb | 3 ++- app/models/trending_tags.rb | 9 +++++---- app/views/admin/tags/index.html.haml | 1 + config/locales/en.yml | 1 + 5 files changed, 13 insertions(+), 8 deletions(-) (limited to 'config') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index ed271aedc..794bb114a 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -36,9 +36,10 @@ module Admin def filtered_tags scope = Tag scope = scope.discoverable if filter_params[:context] == 'directory' - scope = scope.reviewed if filter_params[:review] == 'reviewed' - scope = scope.pending_review if filter_params[:review] == 'pending_review' - scope.reorder(score: :desc) + scope = scope.unreviewed if filter_params[:review] == 'unreviewed' + scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed' + scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review' + scope.order(score: :desc) end def filter_params diff --git a/app/models/tag.rb b/app/models/tag.rb index e2fe91da1..1364d1dba 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -30,7 +30,8 @@ class Tag < ApplicationRecord validate :validate_name_change, if: -> { !new_record? && name_changed? } scope :reviewed, -> { where.not(reviewed_at: nil) } - scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) } + scope :unreviewed, -> { where(reviewed_at: nil) } + scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) } scope :usable, -> { where(usable: [true, nil]) } scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 0a7e2feac..594ae9520 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -5,6 +5,7 @@ class TrendingTags EXPIRE_HISTORY_AFTER = 7.days.seconds EXPIRE_TRENDS_AFTER = 1.day.seconds THRESHOLD = 5 + LIMIT = 10 class << self include Redisable @@ -18,18 +19,18 @@ class TrendingTags end def get(limit, filtered: true) - tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, limit - 1).map(&:to_i) + tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, LIMIT - 1).map(&:to_i) tags = Tag.where(id: tag_ids) tags = tags.where(trendable: true) if filtered tags = tags.each_with_object({}) { |tag, h| h[tag.id] = tag } - tag_ids.map { |tag_id| tags[tag_id] }.compact + tag_ids.map { |tag_id| tags[tag_id] }.compact.take(limit) end def trending?(tag) rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id) - rank.present? && rank <= 10 + rank.present? && rank <= LIMIT end private @@ -59,7 +60,7 @@ class TrendingTags old_rank = redis.zrevrank(key, tag.id) redis.zadd(key, score, tag.id) - request_review!(tag) if (old_rank.nil? || old_rank > 10) && redis.zrevrank(key, tag.id) <= 10 && !tag.trendable? && tag.requires_review? && !tag.requested_review? + request_review!(tag) if (old_rank.nil? || old_rank > LIMIT) && redis.zrevrank(key, tag.id) <= LIMIT && !tag.trendable? && tag.requires_review? && !tag.requested_review? end redis.expire(key, EXPIRE_TRENDS_AFTER) diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index 5e4ee21f5..d994955ef 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -12,6 +12,7 @@ %strong= t('admin.tags.review') %ul %li= filter_link_to t('generic.all'), review: nil + %li= filter_link_to t('admin.tags.unreviewed'), review: 'unreviewed' %li= filter_link_to t('admin.tags.reviewed'), review: 'reviewed' %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), review: 'pending_review' diff --git a/config/locales/en.yml b/config/locales/en.yml index 7b24df016..17ff24726 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -498,6 +498,7 @@ en: title: Hashtags trending_right_now: Trending right now unique_uses_today: "%{count} posting today" + unreviewed: Not reviewed updated_msg: Hashtag settings updated successfully title: Administration warning_presets: -- cgit From bced70469a6c4aecdb3c71055f329a0f579eb14c Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 7 Aug 2019 20:20:23 +0200 Subject: Add domain block notes (#11515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add database columns for adding notes to domain blocks/restrctions * Add admin UI to set private and public comments when blocking a domain * Add text for private and public comments on domain blocks * Show domain block comments in admin UI * Add comments to the domain block undo page * Make UnblockDomainService more robust regarding upgraded domain blocks * Allow editing domain blocks * Rename button from “undo domain block” to “view domain block” in account admin UI * Change test to unsilence silenced users from upgraded blocks --- app/controllers/admin/domain_blocks_controller.rb | 28 ++++++++++++++++++-- app/controllers/admin/instances_controller.rb | 2 ++ app/models/domain_block.rb | 16 +++++++----- app/services/block_domain_service.rb | 11 +++++++- app/services/unblock_domain_service.rb | 19 ++------------ app/views/admin/accounts/show.html.haml | 2 +- app/views/admin/domain_blocks/edit.html.haml | 30 ++++++++++++++++++++++ app/views/admin/domain_blocks/new.html.haml | 6 +++++ app/views/admin/domain_blocks/show.html.haml | 12 +++++++++ app/views/admin/instances/show.html.haml | 13 ++++++++++ app/workers/domain_block_worker.rb | 4 +-- config/locales/en.yml | 8 ++++++ config/routes.rb | 6 ++++- ...20190807135426_add_comments_to_domain_blocks.rb | 7 +++++ db/schema.rb | 4 ++- spec/services/unblock_domain_service_spec.rb | 2 +- spec/workers/domain_block_worker_spec.rb | 2 +- 17 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 app/views/admin/domain_blocks/edit.html.haml create mode 100644 db/migrate/20190807135426_add_comments_to_domain_blocks.rb (limited to 'config') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 7129656da..74a36b79c 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -2,13 +2,17 @@ module Admin class DomainBlocksController < BaseController - before_action :set_domain_block, only: [:show, :destroy] + before_action :set_domain_block, only: [:show, :destroy, :edit, :update] def new authorize :domain_block, :create? @domain_block = DomainBlock.new(domain: params[:_domain]) end + def edit + authorize :domain_block, :create? + end + def create authorize :domain_block, :create? @@ -35,6 +39,22 @@ module Admin end end + def update + authorize :domain_block, :create? + + @domain_block.update(update_params) + + severity_changed = @domain_block.severity_changed? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :edit + end + end + def show authorize @domain_block, :show? end @@ -52,8 +72,12 @@ module Admin @domain_block = DomainBlock.find(params[:id]) end + def update_params + params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment) + end + def resource_params - params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports) + params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment) end end end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index d4f201807..b47b18f8e 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -21,6 +21,8 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) + @private_comment = @domain_block&.private_comment + @public_comment = @domain_block&.public_comment end private diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 25d3b87ef..3f5b9f23e 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -3,13 +3,15 @@ # # Table name: domain_blocks # -# id :bigint(8) not null, primary key -# domain :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# severity :integer default("silence") -# reject_media :boolean default(FALSE), not null -# reject_reports :boolean default(FALSE), not null +# id :bigint(8) not null, primary key +# domain :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# severity :integer default("silence") +# reject_media :boolean default(FALSE), not null +# reject_reports :boolean default(FALSE), not null +# private_comment :text +# public_comment :text # class DomainBlock < ApplicationRecord diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index c5e5e5761..0ec6be503 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -3,13 +3,22 @@ class BlockDomainService < BaseService attr_reader :domain_block - def call(domain_block) + def call(domain_block, update = false) @domain_block = domain_block process_domain_block! + process_retroactive_updates! if update end private + def process_retroactive_updates! + # If the domain block severity has been changed, undo the appropriate limitations + scope = Account.by_domain_and_subdomains(domain_block.domain) + + scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence? + scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend? + end + def process_domain_block! clear_media! if domain_block.reject_media? diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb index fc262a50a..d502d9e49 100644 --- a/app/services/unblock_domain_service.rb +++ b/app/services/unblock_domain_service.rb @@ -10,24 +10,9 @@ class UnblockDomainService < BaseService end def process_retroactive_updates - blocked_accounts.in_batches.update_all(update_options) unless domain_block.noop? - end - - def blocked_accounts scope = Account.by_domain_and_subdomains(domain_block.domain) - if domain_block.silence? - scope.where(silenced_at: @domain_block.created_at) - else - scope.where(suspended_at: @domain_block.created_at) - end - end - - def update_options - { domain_block_impact => nil } - end - - def domain_block_impact - domain_block.silence? ? :silenced_at : :suspended_at + scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop? + scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend? end end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7494c9fa2..59babd3b0 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -174,7 +174,7 @@ - unless @account.local? - if DomainBlock.where(domain: @account.domain).exists? - = link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button' + = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml new file mode 100644 index 000000000..29e47ef3b --- /dev/null +++ b/app/views/admin/domain_blocks/edit.html.haml @@ -0,0 +1,30 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.domain_blocks.edit') + += simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :put do |f| + = render 'shared/error_messages', object: @domain_block + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('admin.domain_blocks.new.hint'), required: true, readonly: true, disabled: true + + .fields-row__column.fields-row__column-6.fields-group + = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html') + + .fields-group + = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint') + + .fields-group + = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') + + .field-group + = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 + + .field-group + = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index 055d2fbd7..ed1581936 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -20,5 +20,11 @@ .fields-group = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') + .field-group + = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 + + .field-group + = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 + .actions = f.button :button, t('.create'), type: :submit diff --git a/app/views/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml index dca4dbac7..e64aaa629 100644 --- a/app/views/admin/domain_blocks/show.html.haml +++ b/app/views/admin/domain_blocks/show.html.haml @@ -1,6 +1,18 @@ - content_for :page_title do = t('admin.domain_blocks.show.title', domain: @domain_block.domain) +- if @domain_block.private_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@domain_block.private_comment)) + .speech-bubble__owner= t 'admin.instances.private_comment' + +- if @domain_block.public_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@domain_block.public_comment)) + .speech-bubble__owner= t 'admin.instances.public_comment' + = simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f| - unless (@domain_block.noop?) diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index fbb49ba02..294c9495d 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -31,6 +31,18 @@ = fa_icon 'times' .dashboard__counters__label= t 'admin.instances.delivery_available' +- if @private_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@private_comment)) + .speech-bubble__owner= t 'admin.instances.private_comment' + +- if @public_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@public_comment)) + .speech-bubble__owner= t 'admin.instances.public_comment' + %hr.spacer/ %div{ style: 'overflow: hidden' } @@ -41,6 +53,7 @@ - if @domain_allow = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } - elsif @domain_block + = link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@domain_block), class: 'button' = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' diff --git a/app/workers/domain_block_worker.rb b/app/workers/domain_block_worker.rb index 884477829..35518d6b5 100644 --- a/app/workers/domain_block_worker.rb +++ b/app/workers/domain_block_worker.rb @@ -3,8 +3,8 @@ class DomainBlockWorker include Sidekiq::Worker - def perform(domain_block_id) - BlockDomainService.new.call(DomainBlock.find(domain_block_id)) + def perform(domain_block_id, update = false) + BlockDomainService.new.call(DomainBlock.find(domain_block_id), update) rescue ActiveRecord::RecordNotFound true end diff --git a/config/locales/en.yml b/config/locales/en.yml index 17ff24726..b677a6651 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -284,6 +284,7 @@ en: created_msg: Domain block is now being processed destroyed_msg: Domain block has been undone domain: Domain + edit: Edit domain block existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to unblock it first. new: create: Create block @@ -294,6 +295,10 @@ en: silence: Silence suspend: Suspend title: New domain block + private_comment: Private comment + private_comment_hint: Comment about this domain limitation for internal use by the moderators. + public_comment: Public comment + public_comment_hint: Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled. reject_media: Reject media files reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_reports: Reject reports @@ -313,6 +318,7 @@ en: title: Undo domain block for %{domain} undo: Undo undo: Undo domain block + view: View domain block email_domain_blocks: add_new: Add new created_msg: Successfully added e-mail domain to blacklist @@ -336,6 +342,8 @@ en: all: All limited: Limited title: Moderation + private_comment: Private comment + public_comment: Public comment title: Federation total_blocked_by_us: Blocked by us total_followed_by_them: Followed by them diff --git a/config/routes.rb b/config/routes.rb index 60f7d2e05..9c33b8190 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,7 +155,11 @@ Rails.application.routes.draw do get '/dashboard', to: 'dashboard#index' resources :domain_allows, only: [:new, :create, :show, :destroy] - resources :domain_blocks, only: [:new, :create, :show, :destroy] + resources :domain_blocks, only: [:new, :create, :show, :destroy, :update] do + member do + get :edit + end + end resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] resources :warning_presets, except: [:new] diff --git a/db/migrate/20190807135426_add_comments_to_domain_blocks.rb b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb new file mode 100644 index 000000000..b660a71ad --- /dev/null +++ b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb @@ -0,0 +1,7 @@ +class AddCommentsToDomainBlocks < ActiveRecord::Migration[5.2] + def change + add_column :domain_blocks, :private_comment, :text + add_column :domain_blocks, :public_comment, :text + end +end + diff --git a/db/schema.rb b/db/schema.rb index d1b6825b4..f8fc6a821 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_05_123746) do +ActiveRecord::Schema.define(version: 2019_08_07_135426) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -259,6 +259,8 @@ ActiveRecord::Schema.define(version: 2019_08_05_123746) do t.integer "severity", default: 0 t.boolean "reject_media", default: false, null: false t.boolean "reject_reports", default: false, null: false + t.text "private_comment" + t.text "public_comment" t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true end diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb index 619aefb5c..27dbc92ad 100644 --- a/spec/services/unblock_domain_service_spec.rb +++ b/spec/services/unblock_domain_service_spec.rb @@ -31,7 +31,7 @@ describe UnblockDomainService, type: :service do subject.call(@domain_block) expect_deleted_domain_block expect(@suspended.reload.suspended?).to be false - expect(@silenced.reload.silenced?).to be true + expect(@silenced.reload.silenced?).to be false expect(@independently_suspended.reload.suspended?).to be true expect(@independently_silenced.reload.silenced?).to be true end diff --git a/spec/workers/domain_block_worker_spec.rb b/spec/workers/domain_block_worker_spec.rb index c4138501f..48b3e38c4 100644 --- a/spec/workers/domain_block_worker_spec.rb +++ b/spec/workers/domain_block_worker_spec.rb @@ -14,7 +14,7 @@ describe DomainBlockWorker do result = subject.perform(domain_block.id) expect(result).to be_nil - expect(service).to have_received(:call).with(domain_block) + expect(service).to have_received(:call).with(domain_block, false) end it 'calls domain block service for relevant domain block' do -- cgit From 3a6b6c63f22e31c9b113428d6c69be451a3bcc17 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 7 Aug 2019 20:20:39 +0200 Subject: Add breakdown of usage by source to admin UI for hashtags (#11517) Allows determining where the majority of posts in a hashtag come from on a given day at a glance. --- app/controllers/admin/tags_controller.rb | 25 +++++++++++++++++++++++++ app/views/admin/tags/show.html.haml | 29 +++++++++++++++++++++++++++++ config/locales/en.yml | 3 +++ 3 files changed, 57 insertions(+) (limited to 'config') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 794bb114a..d62361eaa 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -4,6 +4,8 @@ module Admin class TagsController < BaseController before_action :set_tags, only: :index before_action :set_tag, except: :index + before_action :set_usage_by_domain, except: :index + before_action :set_counters, except: :index def index authorize :tag, :index? @@ -33,6 +35,21 @@ module Admin @tag = Tag.find(params[:id]) end + def set_usage_by_domain + @usage_by_domain = @tag.statuses + .where(visibility: :public) + .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day))) + .joins(:account) + .group('accounts.domain') + .reorder('statuses_count desc') + .pluck('accounts.domain, count(*) AS statuses_count') + end + + def set_counters + @accounts_today = @tag.history.first[:accounts] + @accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" }) + end + def filtered_tags scope = Tag scope = scope.discoverable if filter_params[:context] == 'directory' @@ -49,5 +66,13 @@ module Admin def tag_params params.require(:tag).permit(:name, :trendable, :usable, :listable) end + + def current_week_days + now = Time.now.utc.beginning_of_day.to_date + + (Date.commercial(now.cwyear, now.cweek)..now).map do |date| + date.to_time.utc.beginning_of_day.to_i + end + end end end diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml index 5f3a8e4d9..6a1e03065 100644 --- a/app/views/admin/tags/show.html.haml +++ b/app/views/admin/tags/show.html.haml @@ -1,6 +1,22 @@ - content_for :page_title do = "##{@tag.name}" +.dashboard__counters + %div + = link_to web_url("timelines/tag/#{@tag.name}") do + .dashboard__counters__num= number_with_delimiter @accounts_today + .dashboard__counters__label= t 'admin.tags.accounts_today' + %div + %div + .dashboard__counters__num= number_with_delimiter @accounts_week + .dashboard__counters__label= t 'admin.tags.accounts_week' + %div + = link_to explore_hashtag_path(@tag) do + .dashboard__counters__num= number_with_delimiter @tag.accounts_count + .dashboard__counters__label= t 'admin.tags.directory' + +%hr.spacer/ + = simple_form_for @tag, url: admin_tag_path(@tag.id) do |f| = render 'shared/error_messages', object: @tag @@ -14,3 +30,16 @@ .actions = f.button :button, t('generic.save_changes'), type: :submit + +%hr.spacer/ + +%h3= t 'admin.tags.breakdown' + +.table-wrapper + %table.table + %tbody + - @usage_by_domain.each do |(domain, count)| + %tr + %th= domain || site_hostname + %td= "#{number_with_delimiter((count.to_f / @tag.history[0][:uses].to_f) * 100)}%" + %td= number_with_delimiter count diff --git a/config/locales/en.yml b/config/locales/en.yml index b677a6651..7fd0536ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -498,6 +498,9 @@ en: title: Account statuses with_media: With media tags: + accounts_today: Unique uses today + accounts_week: Unique uses this week + breakdown: Breakdown of today's usage by source context: Context directory: In directory in_directory: "%{count} in directory" -- cgit From 7a1f8a58df7edeb4f1d03c9dd3c25d5370d858a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 8 Aug 2019 23:04:19 +0200 Subject: Fix crash when saving invalid domain name (#11528) Fix #7629 --- app/models/account_domain_block.rb | 2 +- app/models/concerns/domain_normalizable.rb | 2 +- app/models/domain_allow.rb | 2 +- app/models/domain_block.rb | 2 +- app/models/email_domain_block.rb | 2 +- app/validators/domain_validator.rb | 17 +++++++++++++++++ config/locales/en.yml | 2 ++ 7 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 app/validators/domain_validator.rb (limited to 'config') diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index 7c0d60379..3aaffde9a 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -15,7 +15,7 @@ class AccountDomainBlock < ApplicationRecord include DomainNormalizable belongs_to :account - validates :domain, presence: true, uniqueness: { scope: :account_id } + validates :domain, presence: true, uniqueness: { scope: :account_id }, domain: true after_commit :remove_blocking_cache after_commit :remove_relationship_cache diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index fb84058fc..c00b3142f 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -4,7 +4,7 @@ module DomainNormalizable extend ActiveSupport::Concern included do - before_validation :normalize_domain + before_save :normalize_domain end private diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 85018b636..5fe0e3a29 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -13,7 +13,7 @@ class DomainAllow < ApplicationRecord include DomainNormalizable - validates :domain, presence: true, uniqueness: true + validates :domain, presence: true, uniqueness: true, domain: true scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 3f5b9f23e..37b8d98c6 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -19,7 +19,7 @@ class DomainBlock < ApplicationRecord enum severity: [:silence, :suspend, :noop] - validates :domain, presence: true, uniqueness: true + validates :domain, presence: true, uniqueness: true, domain: true has_many :accounts, foreign_key: :domain, primary_key: :domain delegate :count, to: :accounts, prefix: true diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 0fcd36477..bc70dea25 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -12,7 +12,7 @@ class EmailDomainBlock < ApplicationRecord include DomainNormalizable - validates :domain, presence: true, uniqueness: true + validates :domain, presence: true, uniqueness: true, domain: true def self.block?(email) _, domain = email.split('@', 2) diff --git a/app/validators/domain_validator.rb b/app/validators/domain_validator.rb new file mode 100644 index 000000000..ae07f1798 --- /dev/null +++ b/app/validators/domain_validator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class DomainValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? + + record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(value) + end + + private + + def compliant?(value) + Addressable::URI.new.tap { |uri| uri.host = value } + rescue Addressable::URI::InvalidURIError + false + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 7fd0536ae..786906e2d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -628,6 +628,8 @@ en: people: one: "%{count} person" other: "%{count} people" + domain_validator: + invalid_domain: is not a valid domain name errors: '403': You don't have permission to view this page. '404': The page you are looking for isn't here. -- cgit From 597ea5687a3601c6cc462831bf570bae05b08240 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 12 Aug 2019 15:31:20 +0200 Subject: Add option to disable emoji replacements Fixes #647 The option is found in `/settings` (because that was easier to write it this way) but only affects the glitch-soc front-end. --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/flavours/glitch/util/emoji/index.js | 6 +++--- app/javascript/flavours/glitch/util/initial_state.js | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/appearance/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + 9 files changed, 15 insertions(+), 4 deletions(-) (limited to 'config') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 418ea5d7a..f05dbe0ea 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_expand_spoilers, :setting_reduce_motion, :setting_system_font_ui, + :setting_system_emoji_font, :setting_noindex, :setting_hide_network, :setting_hide_followers_count, diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index 2f40f9b08..b2d13cc95 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -1,4 +1,4 @@ -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; @@ -12,7 +12,7 @@ const emojify = (str, customEmojis = {}) => { let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; for (;;) { let match, i = 0, tag; - while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { + while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } let rend, replacement = ''; @@ -57,7 +57,7 @@ const emojify = (str, customEmojis = {}) => { } } i = rend; - } else { // matched to unicode emoji + } else if (!useSystemEmojiFont) { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; replacement = `${match}`; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index caaa79bb3..4b6227cac 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -31,5 +31,6 @@ export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); +export const useSystemEmojiFont = getMeta('system_emoji_font'); export default initialState; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index a52172707..c822d54de 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -29,6 +29,7 @@ class UserSettingsDecorator user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['system_emoji_font'] = system_emoji_font_preference if change?('setting_system_emoji_font') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['hide_followers_count']= hide_followers_count_preference if change?('setting_hide_followers_count') user.settings['flavour'] = flavour_preference if change?('setting_flavour') @@ -79,6 +80,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_system_font_ui' end + def system_emoji_font_preference + boolean_cast_setting 'setting_system_emoji_font' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/models/user.rb b/app/models/user.rb index 45a4b8989..341227d3e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -108,7 +108,7 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, - :default_content_type, + :default_content_type, :system_emoji_font, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index c8da6e725..c8f6bec7a 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -53,6 +53,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:is_staff] = object.current_account.user.staff? store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:default_content_type] = object.current_account.user.setting_default_content_type + store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font end store diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 0bda49f44..900a7c6fb 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -21,6 +21,7 @@ = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label + = f.input :setting_system_emoji_font, as: :boolean, wrapper: :with_label %h4= t 'appearance.discovery' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 5da0cc45d..8f63df013 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -124,6 +124,7 @@ en: setting_reduce_motion: Reduce motion in animations setting_show_application: Disclose application used to send toots setting_skin: Skin + setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only) setting_system_font_ui: Use system's default font setting_theme: Site theme setting_trends: Show today's trends diff --git a/config/settings.yml b/config/settings.yml index 10836db3f..70a9a3a44 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -29,6 +29,7 @@ defaults: &defaults reduce_motion: false show_application: false system_font_ui: false + system_emoji_font: false noindex: false hide_followers_count: false enable_keybase: true -- cgit From 28636f43e4b0c04befa243b847c38e81c90f1289 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 15 Aug 2019 15:13:26 +0200 Subject: Add OCR tool to media editing modal (#11566) --- .../features/compose/components/upload_form.js | 3 +- .../features/compose/components/upload_progress.js | 9 +- .../features/ui/components/focal_point_modal.js | 60 +++++++-- app/javascript/styles/mastodon/components.scss | 81 +++++++++--- config/initializers/content_security_policy.rb | 8 +- config/webpack/development.js | 1 + config/webpack/shared.js | 5 + package.json | 2 + public/ocr/lang-data/eng.traineddata.gz | Bin 0 -> 10923060 bytes yarn.lock | 140 ++++++++++++++++++++- 10 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 public/ocr/lang-data/eng.traineddata.gz (limited to 'config') diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js index 9ff2aa0fa..c6eac554e 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { @@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent { return (
    - + } />
    {mediaIds.map(id => ( diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js index cbe58f573..b0bfe0c9a 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.js +++ b/app/javascript/mastodon/features/compose/components/upload_progress.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; export default class UploadProgress extends React.PureComponent { @@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, + icon: PropTypes.string.isRequired, + message: PropTypes.node.isRequired, }; render () { - const { active, progress } = this.props; + const { active, progress, icon, message } = this.props; if (!active) { return null; @@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent { return (
    - +
    - + {message}
    diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index e15bf69d6..b21d5c9d4 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -10,6 +10,11 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import IconButton from 'mastodon/components/icon_button'; import Button from 'mastodon/components/button'; import Video from 'mastodon/features/video'; +import { TesseractWorker } from 'tesseract.js'; +import Textarea from 'react-textarea-autosize'; +import UploadProgress from 'mastodon/features/compose/components/upload_progress'; +import CharacterCounter from 'mastodon/features/compose/components/character_counter'; +import { length } from 'stringz'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -29,6 +34,12 @@ const mapDispatchToProps = (dispatch, { id }) => ({ }); +const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') + .replace(/\n/g, ' ') + .replace(/\*\*\*\*\*\*/g, '\n\n'); + +const assetHost = process.env.CDN_HOST || ''; + export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl class FocalPointModal extends ImmutablePureComponent { @@ -47,6 +58,7 @@ class FocalPointModal extends ImmutablePureComponent { dragging: false, description: '', dirty: false, + progress: 0, }; componentWillMount () { @@ -133,9 +145,27 @@ class FocalPointModal extends ImmutablePureComponent { this.node = c; } + handleTextDetection = () => { + const { media } = this.props; + + const worker = new TesseractWorker({ + workerPath: `${assetHost}/packs/ocr/worker.min.js`, + corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`, + langPath: `${assetHost}/ocr/lang-data`, + }); + + this.setState({ detecting: true }); + + worker.recognize(media.get('url')) + .progress(({ progress }) => this.setState({ progress })) + .finally(() => worker.terminate()) + .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false })) + .catch(() => this.setState({ detecting: false })); + } + render () { const { media, intl, onClose } = this.props; - const { x, y, dragging, description, dirty } = this.state; + const { x, y, dragging, description, dirty, detecting, progress } = this.state; const width = media.getIn(['meta', 'original', 'width']) || null; const height = media.getIn(['meta', 'original', 'height']) || null; @@ -158,15 +188,27 @@ class FocalPointModal extends ImmutablePureComponent { -