diff --git a/README.md b/README.md index 7a121faf..72170c38 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc - Supports thousands of users and millions of feeds - OPML import/export - REST API -- Fever-compatible API for native mobile apps +- Fever and Google Reader API for native mobile apps - Can automatically mark articles as read based on user-defined rules - Push notifications when new articles are published - Highly customizable with [custom CSS](https://athou.github.io/commafeed/documentation/custom-css) and JavaScript diff --git a/commafeed-client/src/components/settings/ProfileSettings.tsx b/commafeed-client/src/components/settings/ProfileSettings.tsx index 05c7e4ba..d03f180c 100644 --- a/commafeed-client/src/components/settings/ProfileSettings.tsx +++ b/commafeed-client/src/components/settings/ProfileSettings.tsx @@ -91,8 +91,8 @@ export function ProfileSettings() { label={API key} description={ - This is your API key. It can be used for some read-only API operations and grants access to the Fever API. - Use the form at the bottom of the page to generate a new API key + This is your API key. It can be used for some read-only API operations and grants access to the Fever API + and the Google Reader API. Use the form at the bottom of the page to generate a new API key } readOnly @@ -130,6 +130,22 @@ export function ProfileSettings() { + Google Reader API} + description={ + + CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible + mobile client. Login with your username and your API key. + + } + > + + + Google Reader API URL + + + + API key." msgstr "CommaFeed متوافق مع Fever API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Fever. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API الخاص بك." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed متوافق مع Google Reader API. استخدم عنوان URL التالي في تطبيق الهاتف المتوافق مع Google Reader. قم بتسجيل الدخول باستخدام اسم المستخدم و <0>مفتاح API الخاص بك." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed التالي العنصر غير المقروء" @@ -454,6 +458,14 @@ msgstr "انتقل إلى وثائق API." msgid "Goodies" msgstr "الأشياء الجيدة" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "عنبي" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "تحتوي هذه الخلاصة على فلتر قديم لا يمكن تحريره ولا يتم تطبيقه. يرجى إعادة إنشاء الفلتر باستخدام محرر التعبيرات الجديد. كان تعبير الفلتر القديم هو: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح الوصول إلى Fever API. استخدم النموذج الموجود في أسفل الصفحة لإنشاء مفتاح API جديد" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "هذا هو مفتاح API الخاص بك. يمكن استخدامه لبعض عمليات API للقراءة فقط ويمنح حق الوصول إلى Fever API و Google Reader API. استخدم النموذج الموجود أسفل الصفحة لإنشاء مفتاح API جديد" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/ca/messages.po b/commafeed-client/src/locales/ca/messages.po index 4a988915..b5f77395 100644 --- a/commafeed-client/src/locales/ca/messages.po +++ b/commafeed-client/src/locales/ca/messages.po @@ -210,6 +210,10 @@ msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion} msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed és compatible amb l'API de Google Reader. Utilitzeu l'URL següent al vostre client mòbil compatible amb Google Reader. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed següent element no llegit" @@ -454,6 +458,14 @@ msgstr "Vés a la documentació de l'API." msgid "Goodies" msgstr "Extres" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Raïm" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Aquest feed té un filtre heretat que no es pot editar i no s'aplica. Torneu a crear el filtre amb el nou editor d'expressions. L'expressió del filtre heretat era: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Aquesta és la vostra clau de l'API. Es pot utilitzar per a algunes operacions de l'API de només lectura i permet accedir a l'API Fever. Utilitzeu el formulari de la part inferior de la pàgina per generar una nova clau d'API." +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Aquesta és la vostra clau API. Es pot utilitzar per a algunes operacions de l'API de només lectura i dona accés a l'API Fever i a l'API de Google Reader. Utilitzeu el formulari a la part inferior de la pàgina per generar una nova clau API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/cs/messages.po b/commafeed-client/src/locales/cs/messages.po index 6122c6cc..17f8c49f 100644 --- a/commafeed-client/src/locales/cs/messages.po +++ b/commafeed-client/src/locales/cs/messages.po @@ -210,6 +210,10 @@ msgstr "Verze rozšíření prohlížeče CommaFeed {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed je kompatibilní s Fever API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Fever. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed je kompatibilní s Google Reader API. Použijte následující URL ve svém mobilním klientovi kompatibilním s Google Reader. Přihlaste se pomocí svého uživatelského jména a svého <0>klíče API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed další nepřečtená položka" @@ -454,6 +458,14 @@ msgstr "Přejděte na dokumentaci API." msgid "Goodies" msgstr "Dobroty" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Hroznová" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Tento zdroj má zastaralý filtr, který nelze upravovat a není aplikován. Znovu vytvořte filtr pomocí nového editoru výrazů. Původní výraz filtru byl: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a poskytuje přístup k Fever API. Pro vygenerování nového klíče API použijte formulář v dolní části stránky." +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Toto je váš klíč API. Lze jej použít pro některé operace API pouze pro čtení a uděluje přístup k Fever API a Google Reader API. Použijte formulář v dolní části stránky k vygenerování nového klíče API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/cy/messages.po b/commafeed-client/src/locales/cy/messages.po index 55c59b03..fee6ddec 100644 --- a/commafeed-client/src/locales/cy/messages.po +++ b/commafeed-client/src/locales/cy/messages.po @@ -210,6 +210,10 @@ msgstr "Fersiwn estyniad porwr CommaFeed {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "Mae CommaFeed yn gydnaws â'r Fever API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Fever. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "Mae CommaFeed yn gydnaws â Google Reader API. Defnyddiwch y URL canlynol yn eich cleient symudol sy'n gydnaws â Google Reader. Mewngofnodwch gyda'ch enw defnyddiwr a'ch <0>allwedd API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed eitem nesaf heb ei darllen" @@ -454,6 +458,14 @@ msgstr "Ewch i'r ddogfennaeth API." msgid "Goodies" msgstr "nwyddau" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Grawnwin" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Mae gan y porthiant hwn hen hidlydd na ellir ei olygu ac nid yw'n cael ei gymhwyso. Crëwch yr hidlydd eto gan ddefnyddio'r golygydd mynegiant newydd. Yr hen fynegiant hidlo oedd: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dyma'ch allwedd API. Gellir ei ddefnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n caniatáu mynediad i'r Fever API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dyma eich allwedd API. Gellir ei defnyddio ar gyfer rhai gweithrediadau API darllen-yn-unig ac mae'n rhoi mynediad i'r Fever API a'r Google Reader API. Defnyddiwch y ffurflen ar waelod y dudalen i gynhyrchu allwedd API newydd" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/da/messages.po b/commafeed-client/src/locales/da/messages.po index ba6ec3ea..0d579b8f 100644 --- a/commafeed-client/src/locales/da/messages.po +++ b/commafeed-client/src/locales/da/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed browserudvidelse version {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed er kompatibel med Fever API. Brug følgende URL i din Fever-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed er kompatibel med Google Reader API. Brug følgende URL i din Google Reader-kompatible mobilklient. Log ind med dit brugernavn og din <0>API-nøgle." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed næste ulæste element" @@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentationen." msgid "Goodies" msgstr "Godbidder" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Grape" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Dette feed har et ældre filter, der ikke kan redigeres og ikke anvendes. Genopret venligst filteret ved hjælp af den nye udtrykseditor. Det ældre filterudtryk var: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dette er din API-nøgle. Den kan bruges til visse skrivebeskyttede API-operationer og giver adgang til Fever API. Brug formularen nederst på siden til at generere en ny API-nøgle" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dette er din API-nøgle. Den kan bruges til nogle skrivebeskyttede API-operationer og giver adgang til Fever API og Google Reader API. Brug formularen nederst på siden for at generere en ny API-nøgle" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/de/messages.po b/commafeed-client/src/locales/de/messages.po index d3e9538e..d05bf3b5 100644 --- a/commafeed-client/src/locales/de/messages.po +++ b/commafeed-client/src/locales/de/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel an." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed ist mit der Google-Reader-API kompatibel. Verwenden Sie die folgende URL in Ihrem Google-Reader-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel an." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed nächstes ungelesenes Element" @@ -454,6 +458,14 @@ msgstr "Gehen Sie zur API-Dokumentation." msgid "Goodies" msgstr "Goodies" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Traube" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Dieser Feed verfügt über einen veralteten Filter, der nicht bearbeitet werden kann und nicht angewendet wird. Bitte erstellen Sie den Filter mit dem neuen Ausdruckseditor neu. Der veraltete Filterausdruck war: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und ermöglicht den Zugriff auf die Fever-API. Verwenden Sie das Formular unten auf der Seite, um einen neuen API-Schlüssel zu generieren" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und gewährt Zugriff auf die Fever-API und die Google-Reader-API. Verwenden Sie das Formular am Ende der Seite, um einen neuen API-Schlüssel zu generieren" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/en/messages.po b/commafeed-client/src/locales/en/messages.po index ef332d03..1239a514 100644 --- a/commafeed-client/src/locales/en/messages.po +++ b/commafeed-client/src/locales/en/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed next unread item" @@ -454,6 +458,14 @@ msgstr "Go to the API documentation." msgid "Goodies" msgstr "Goodies" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "Google Reader API" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "Google Reader API URL" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Grape" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using the new expression editor. The legacy filter expression was: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/es/messages.po b/commafeed-client/src/locales/es/messages.po index f1ce937f..6a6a2ccf 100644 --- a/commafeed-client/src/locales/es/messages.po +++ b/commafeed-client/src/locales/es/messages.po @@ -211,6 +211,10 @@ msgstr "Versión de la extensión del navegador CommaFeed {browserExtensionVersi msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed es compatible con Google Reader API. Utilice la siguiente URL en su cliente móvil compatible con Google Reader. Inicie sesión con su nombre de usuario y su <0>clave API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed siguiente elemento no leído" @@ -455,6 +459,14 @@ msgstr "Ir a la documentación de la API." msgid "Goodies" msgstr "Golosinas" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Uva" @@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Este feed tiene un filtro heredado que no se puede editar y no se aplica. Por favor, vuelve a crear el filtro usando el nuevo editor de expresiones. La expresión del filtro heredado era: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a Fever API. Utilice el formulario en la parte inferior de la página para generar una nueva clave API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a la API Fever y a la API de Google Reader. Utilice el formulario en la parte inferior de la página para generar una nueva clave API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/fa/messages.po b/commafeed-client/src/locales/fa/messages.po index b78dd0ff..786c3b44 100644 --- a/commafeed-client/src/locales/fa/messages.po +++ b/commafeed-client/src/locales/fa/messages.po @@ -210,6 +210,10 @@ msgstr "نسخه افزونه مرورگر CommaFeed {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed با Fever API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Fever استفاده کنید. با نام کاربری و <0>کلید API خود وارد شوید." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed با Google Reader API سازگار است. از آدرس زیر در کلاینت موبایل سازگار با Google Reader استفاده کنید. با نام کاربری و <0>کلید API خود وارد شوید." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "مورد خوانده نشده بعدی CommaFeed" @@ -454,6 +458,14 @@ msgstr "به مستندات API بروید." msgid "Goodies" msgstr "خوبی ها" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "عنبی" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "این فید دارای یک فیلتر قدیمی است که قابل ویرایش نیست و اعمال نمی‌شود. لطفاً فیلتر را با استفاده از ویرایشگر جدید بازسازی کنید. عبارت فیلتر قدیمی: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "این کلید API شماست. می‌توان از آن برای برخی عملیات API فقط خواندنی استفاده کرد و دسترسی به Fever API را فراهم می‌کند. از فرم پایین صفحه برای تولید کلید جدید استفاده کنید" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "این کلید API شماست. می‌توان از آن برای برخی عملیات فقط خواندنی API استفاده کرد و دسترسی به Fever API و Google Reader API را می‌دهد. از فرم پایین صفحه برای ایجاد کلید API جدید استفاده کنید" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/fi/messages.po b/commafeed-client/src/locales/fi/messages.po index b6272bb7..31b1a7ba 100644 --- a/commafeed-client/src/locales/fi/messages.po +++ b/commafeed-client/src/locales/fi/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed-selainlaajennuksen versio {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed on yhteensopiva Fever-API:n kanssa. Käytä seuraavaa URL-osoitetta Fever-yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed on yhteensopiva Google Reader -API:n kanssa. Käytä seuraavaa URL-osoitetta Google Reader -yhteensopivassa mobiiliohjelmassasi. Kirjaudu sisään käyttäjätunnuksellasi ja <0>API-avaimellasi." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed seuraava lukematon kohde" @@ -454,6 +458,14 @@ msgstr "Siirry API-dokumentaatioon." msgid "Goodies" msgstr "Hyvää" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Viinirypäle" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Tässä syötteessä on vanha suodatin, jota ei voi muokata ja jota ei sovelleta. Luo suodatin uudelleen käyttämällä uutta lauseke-editoria. Vanha suodatinlauseke oli: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin ja se antaa pääsyn Fever-API:iin. Käytä sivun alareunassa olevaa lomaketta uuden API-avaimen luomiseen" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Tämä on API-avaimesi. Sitä voidaan käyttää joihinkin vain luku -API-toimintoihin, ja se myöntää pääsyn Fever-API:hin ja Google Reader -API:hin. Käytä sivun alareunassa olevaa lomaketta luodaksesi uuden API-avaimen" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/fr/messages.po b/commafeed-client/src/locales/fr/messages.po index 4ceeeabf..a83518ae 100644 --- a/commafeed-client/src/locales/fr/messages.po +++ b/commafeed-client/src/locales/fr/messages.po @@ -210,6 +210,10 @@ msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API comme mot de passe." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "Commafeed est compatible avec l'API Google Reader, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API comme mot de passe." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed prochain article non lu" @@ -454,6 +458,14 @@ msgstr "Aller à la documentation de l'API." msgid "Goodies" msgstr "Extensions" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Bordeaux" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Ce flux est configuré avec un filtre obsolète, qui ne peut être ni modifié ni appliqué. Vous devez recréer ce filtre à l'aide du nouvel éditeur d'expressions régulières. Le filtre configuré était : <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever et à l'API Google Reader. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/gl/messages.po b/commafeed-client/src/locales/gl/messages.po index 3819b30b..babc3f5d 100644 --- a/commafeed-client/src/locales/gl/messages.po +++ b/commafeed-client/src/locales/gl/messages.po @@ -211,6 +211,10 @@ msgstr "Versión do complemento do navegador para CommaFeed {browserExtensionVer msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed é compatible coa API de Fever. Usa o seguinte URL no teu cliente móbil compatible con Fever. Accede co identificador e a túa <0>clave API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed é compatible coa API de Google Reader. Usa o seguinte URL no teu cliente móbil compatible con Google Reader. Accede co identificador e a túa <0>clave API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "Seguinte elemento non lido en CommaFeed" @@ -455,6 +459,14 @@ msgstr "Ir á documentación da API." msgid "Goodies" msgstr "Agasallos" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Bago" @@ -1066,8 +1078,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Esta canle ten un filtro antigo que non se pode editar e que non se aplica. Volve crear o filtro usando o novo editor de expresións. A expresión do filtro antigo era: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever. Usa o formulario da parte inferior da páxina para crear unha nova clave da API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Esta é a túa clave da API. Pode usarse para algunhas operacións da API de só-lectura e concede acceso á API Fever e á API de Google Reader. Usa o formulario da parte inferior da páxina para crear unha nova clave da API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/hu/messages.po b/commafeed-client/src/locales/hu/messages.po index 7fc69e1c..260a797b 100644 --- a/commafeed-client/src/locales/hu/messages.po +++ b/commafeed-client/src/locales/hu/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed böngészőbővítmény {browserExtensionVersion} verzió." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "A CommaFeed kompatibilis a Fever API-val. Használja a következő URL-t Fever-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "A CommaFeed kompatibilis a Google Reader API-val. Használja a következő URL-t Google Reader-kompatibilis mobilkliensében. Jelentkezzen be felhasználónevével és <0>API-kulcsával." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed következő olvasatlan elem" @@ -454,6 +458,14 @@ msgstr "Nyissa meg az API dokumentációját." msgid "Goodies" msgstr "Jók" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Szőlő" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Ez a feed egy régi szűrővel rendelkezik, amely nem szerkeszthető és nem kerül alkalmazásra. Kérjük, hozza létre újra a szűrőt az új kifejezésszerkesztővel. A régi szűrőkifejezés a következő volt: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Ez az Ön API-kulcsa. Felhasználható néhány írásvédett API-művelethez, és hozzáférést biztosít a Fever API-hoz. Használja az oldal alján található űrlapot egy új API-kulcs létrehozásához" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Ez az Ön API-kulcsa. Néhány csak olvasható API-művelethez használható, és hozzáférést biztosít a Fever API-hoz és a Google Reader API-hoz. Használja az oldal alján található űrlapot új API-kulcs létrehozásához" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/id/messages.po b/commafeed-client/src/locales/id/messages.po index e2dd0dc7..27b6b893 100644 --- a/commafeed-client/src/locales/id/messages.po +++ b/commafeed-client/src/locales/id/messages.po @@ -210,6 +210,10 @@ msgstr "Ekstensi browser CommaFeed versi {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed kompatibel dengan Fever API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Fever. Login dengan nama pengguna dan <0>kunci API Anda." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed kompatibel dengan Google Reader API. Gunakan URL berikut di klien seluler Anda yang kompatibel dengan Google Reader. Login dengan nama pengguna dan <0>kunci API Anda." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed item yang belum dibaca berikutnya" @@ -454,6 +458,14 @@ msgstr "Buka dokumentasi API." msgid "Goodies" msgstr "Pernak-pernik" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Anggur" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Umpan ini memiliki filter lama yang tidak dapat diedit dan tidak diterapkan. Silakan buat ulang filter menggunakan editor ekspresi baru. Ekspresi filter lama adalah: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API baca-saja dan memberikan akses ke Fever API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Ini adalah kunci API Anda. Ini dapat digunakan untuk beberapa operasi API yang hanya-baca dan memberikan akses ke Fever API dan Google Reader API. Gunakan formulir di bagian bawah halaman untuk membuat kunci API baru" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/it/messages.po b/commafeed-client/src/locales/it/messages.po index 65549075..d3f07316 100644 --- a/commafeed-client/src/locales/it/messages.po +++ b/commafeed-client/src/locales/it/messages.po @@ -210,6 +210,10 @@ msgstr "Versione dell'estensione del browser CommaFeed {browserExtensionVersion} msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed è compatibile con l'API Fever. Utilizza il seguente URL nel tuo client mobile compatibile con Fever. Accedi con il tuo nome utente e la tua <0>chiave API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed è compatibile con l'API Google Reader. Utilizza il seguente URL nel tuo client mobile compatibile con Google Reader. Accedi con il tuo nome utente e la tua <0>chiave API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed successivo elemento non letto" @@ -454,6 +458,14 @@ msgstr "Vai alla documentazione dell'API." msgid "Goodies" msgstr "Chicche" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Uva" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Questo feed ha un filtro legacy che non può essere modificato e non viene applicato. Ricrea il filtro utilizzando il nuovo editor di espressioni. L'espressione del filtro legacy era: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e garantisce l'accesso all'API Fever. Utilizza il modulo in fondo alla pagina per generare una nuova chiave API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Questa è la tua chiave API. Può essere utilizzata per alcune operazioni API di sola lettura e concede l'accesso all'API Fever e all'API Google Reader. Usa il modulo in fondo alla pagina per generare una nuova chiave API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/ja/messages.po b/commafeed-client/src/locales/ja/messages.po index 5db76810..c4c79054 100644 --- a/commafeed-client/src/locales/ja/messages.po +++ b/commafeed-client/src/locales/ja/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed ブラウザー拡張機能のバージョンは {browserExten msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー でログインしてください。" +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeedはGoogle Reader APIと互換性があります。Google Reader互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー でログインしてください。" + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed 次の未読アイテム" @@ -454,6 +458,14 @@ msgstr "APIドキュメントに移動します。" msgid "Goodies" msgstr "グッズ" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "グレープ" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "このフィードには、編集できず適用もされない古い形式のフィルタがあります。新しい式エディタを使用してフィルタを再作成してください。古いフィルタ式は次のとおりでした: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "これはあなたのAPIキーです。一部の読み取り専用API操作に使用でき、Fever APIおよびGoogle Reader APIへのアクセスを許可します。新しいAPIキーを生成するには、ページ下部のフォームを使用してください" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/ko/messages.po b/commafeed-client/src/locales/ko/messages.po index 7bf679bc..caaeb854 100644 --- a/commafeed-client/src/locales/ko/messages.po +++ b/commafeed-client/src/locales/ko/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed 브라우저 확장 프로그램 버전 {browserExtensionVersi msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed는 Fever API와 호환됩니다. Fever 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키로 로그인하십시오." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed는 Google Reader API와 호환됩니다. Google Reader 호환 모바일 클라이언트에서 다음 URL을 사용하십시오. 사용자 이름과 <0>API 키로 로그인하십시오." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "다음 읽지 않은 항목을 쉼표로 피드" @@ -454,6 +458,14 @@ msgstr "API 문서로 이동합니다." msgid "Goodies" msgstr "굿즈" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "그레이프" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "이 피드에는 편집할 수 없고 적용되지 않는 기존 필터가 있습니다. 새 표현식 편집기를 사용하여 필터를 다시 만드십시오. 기존 필터 표현식은 <0>{0}이었습니다." #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "이것은 귀하의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오." +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "이것은 회원님의 API 키입니다. 일부 읽기 전용 API 작업에 사용할 수 있으며 Fever API 및 Google Reader API에 대한 액세스 권한을 부여합니다. 새 API 키를 생성하려면 페이지 하단의 양식을 사용하십시오" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/ms/messages.po b/commafeed-client/src/locales/ms/messages.po index a83dcee7..47641ce8 100644 --- a/commafeed-client/src/locales/ms/messages.po +++ b/commafeed-client/src/locales/ms/messages.po @@ -210,6 +210,10 @@ msgstr "Ekstensi penyemak imbas CommaFeed versi {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed serasi dengan Fever API. Gunakan URL berikut dalam klien mudah alih serasi Fever anda. Log masuk dengan nama pengguna anda dan <0>kunci API anda." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed serasi dengan Google Reader API. Gunakan URL berikut dalam klien mudah alih serasi Google Reader anda. Log masuk dengan nama pengguna anda dan <0>kunci API anda." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed item belum dibaca seterusnya" @@ -454,6 +458,14 @@ msgstr "Pergi ke dokumentasi API." msgid "Goodies" msgstr "Barangan" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Anggur" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Suapan ini mempunyai penapis warisan yang tidak boleh diedit dan tidak digunakan. Sila buat semula penapis menggunakan editor ungkapan baharu. Ungkapan penapis warisan ialah: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk beberapa operasi API baca sahaja dan memberikan akses kepada Fever API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Ini ialah kunci API anda. Ia boleh digunakan untuk sesetengah operasi API baca sahaja dan memberikan akses kepada Fever API dan Google Reader API. Gunakan borang di bahagian bawah halaman untuk menjana kunci API baharu" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/nb/messages.po b/commafeed-client/src/locales/nb/messages.po index 79cb75e7..e282672c 100644 --- a/commafeed-client/src/locales/nb/messages.po +++ b/commafeed-client/src/locales/nb/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed nettleserutvidelse versjon {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed er kompatibel med Fever API. Bruk følgende URL i din Fever-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgende URL i din Google Reader-kompatible mobilklient. Logg inn med brukernavn og din <0>API-nøkkel." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed neste uleste element" @@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen." msgid "Goodies" msgstr "Godbiter" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Drue" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Denne feeden har et utdatert filter som ikke kan redigeres og ikke blir brukt. Vennligst opprett filteret på nytt med den nye uttrykkseditoren. Det utdaterte filteruttrykket var: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dette er API-nøkkelen din. Den kan brukes til enkelte skrivebeskyttede API-operasjoner og gir tilgang til Fever API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dette er din API-nøkkel. Den kan brukes til noen skrivebeskyttede API-operasjoner og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nederst på siden for å generere en ny API-nøkkel" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/nl/messages.po b/commafeed-client/src/locales/nl/messages.po index 57dc7c48..c3556446 100644 --- a/commafeed-client/src/locales/nl/messages.po +++ b/commafeed-client/src/locales/nl/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed browser extensie versie {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed is compatibel met de Fever API. Gebruik de volgende URL in uw Fever-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed is compatibel met de Google Reader API. Gebruik de volgende URL in uw Google Reader-compatibele mobiele client. Log in met uw gebruikersnaam en uw <0>API-sleutel." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed volgende ongelezen item" @@ -454,6 +458,14 @@ msgstr "Ga naar de API-documentatie." msgid "Goodies" msgstr "Goederen" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Druif" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Deze feed heeft een legacy-filter dat niet kan worden bewerkt en niet wordt toegepast. Maak het filter opnieuw met de nieuwe expressie-editor. De legacy-filterexpressie was: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dit is je API-sleutel. Het kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dit is uw API-sleutel. Deze kan worden gebruikt voor sommige alleen-lezen API-bewerkingen en geeft toegang tot de Fever API en de Google Reader API. Gebruik het formulier onderaan de pagina om een nieuwe API-sleutel te genereren" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/nn/messages.po b/commafeed-client/src/locales/nn/messages.po index 4f3cadc0..6e1b0e55 100644 --- a/commafeed-client/src/locales/nn/messages.po +++ b/commafeed-client/src/locales/nn/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed nettlesarutviding versjon {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed er kompatibel med Fever API. Bruk følgjande URL i din Fever-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed er kompatibel med Google Reader API. Bruk følgjande URL i din Google Reader-kompatible mobilklient. Logg inn med brukarnamn og din <0>API-nøkkel." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed neste ulesne element" @@ -454,6 +458,14 @@ msgstr "Gå til API-dokumentasjonen." msgid "Goodies" msgstr "Godbitar" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Drue" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Denne suapen har eit gamalt filter som ikkje kan redigerast og som ikkje vert brukt. Ver venleg og lag filteret på nytt med den nye uttrykkseditoren. Det gamle filteruttrykket var: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Dette er API-nøkkelen din. Han kan brukast til enkelte skrivebeskytta API-handlingar og gjev tilgang til Fever API. Bruk skjemaet nedst på sida for å laga ein ny API-nøkkel" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Dette er din API-nøkkel. Ho kan brukast til nokre skrivebeskytta API-operasjonar og gir tilgang til Fever API og Google Reader API. Bruk skjemaet nedst på sida for å generere ein ny API-nøkkel" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/pl/messages.po b/commafeed-client/src/locales/pl/messages.po index cae3fa2c..87b99d5f 100644 --- a/commafeed-client/src/locales/pl/messages.po +++ b/commafeed-client/src/locales/pl/messages.po @@ -210,6 +210,10 @@ msgstr "Wersja rozszerzenia przeglądarki CommaFeed {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed jest kompatybilny z API Fever. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Fever. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed jest kompatybilny z API Google Reader. Użyj następującego adresu URL w swoim kliencie mobilnym zgodnym z Google Reader. Zaloguj się przy użyciu swojej nazwy użytkownika i <0>klucza API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "Przecinek następny nieprzeczytany element" @@ -454,6 +458,14 @@ msgstr "Przejdź do dokumentacji API." msgid "Goodies" msgstr "Gadżety" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Winogronowy" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Ten kanał ma stary filtr, którego nie można edytować i który nie jest stosowany. Proszę utworzyć filtr ponownie, korzystając z nowego edytora wyrażeń. Stare wyrażenie filtra to: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever. Użyj formularza na dole strony, aby wygenerować nowy klucz API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "To jest Twój klucz API. Może być używany do niektórych operacji API tylko do odczytu i zapewnia dostęp do API Fever i API Google Reader. Użyj formularza na dole strony, aby wygenerować nowy klucz API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/pt/messages.po b/commafeed-client/src/locales/pt/messages.po index cf39ed15..29d9f98a 100644 --- a/commafeed-client/src/locales/pt/messages.po +++ b/commafeed-client/src/locales/pt/messages.po @@ -210,6 +210,10 @@ msgstr "Extensão de navegador CommaFeed versão {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed é compatível com a API Fever. Use a URL a seguir no seu aplicativo móvel compatível com Fever. Entre com seu nome de usuário e sua <0>chave de API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed é compatível com a API Google Reader. Use a URL a seguir no seu aplicativo móvel compatível com Google Reader. Entre com seu nome de usuário e sua <0>chave de API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed próximo item não lido" @@ -454,6 +458,14 @@ msgstr "Vá para a documentação da API." msgid "Goodies" msgstr "Brindes" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Uva" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Este feed tem um filtro legado que não pode ser editado e não é aplicado. Recrie o filtro usando o novo editor de expressões. A expressão do filtro legado era: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações somente leitura da API e concede acesso à API do Fever. Use o formulário abaixo para gerar uma nova chave de API" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Esta é sua chave de API. Ela pode ser usada para algumas operações de API somente leitura e concede acesso à API Fever e à API Google Reader. Use o formulário na parte inferior da página para gerar uma nova chave de API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/ru/messages.po b/commafeed-client/src/locales/ru/messages.po index 5f8d5f4e..4f7333b1 100644 --- a/commafeed-client/src/locales/ru/messages.po +++ b/commafeed-client/src/locales/ru/messages.po @@ -210,6 +210,10 @@ msgstr "Версия расширения браузера CommaFeed {browserExt msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed совместим с API Fever. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Fever. Войдите в систему, указав свое имя пользователя и <0>API-ключ." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed совместим с API Google Reader. Используйте следующий URL-адрес в мобильном клиенте, совместимом с Google Reader. Войдите в систему, указав свое имя пользователя и <0>API-ключ." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed следующий непрочитанный элемент" @@ -454,6 +458,14 @@ msgstr "Перейдите к документации по API." msgid "Goodies" msgstr "Сладости" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Виноградный" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Этот фид имеет устаревший фильтр, который нельзя редактировать и который не применяется. Пожалуйста, создайте фильтр заново, используя новый редактор выражений. Устаревшее выражение фильтра было: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever. Чтобы сгенерировать новый ключ API, воспользуйтесь формой в нижней части страницы" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Это ваш ключ API. Он может использоваться для некоторых операций API только для чтения и предоставляет доступ к API Fever и API Google Reader. Используйте форму внизу страницы, чтобы сгенерировать новый ключ API" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/sk/messages.po b/commafeed-client/src/locales/sk/messages.po index c80bffb5..c41f01cb 100644 --- a/commafeed-client/src/locales/sk/messages.po +++ b/commafeed-client/src/locales/sk/messages.po @@ -210,6 +210,10 @@ msgstr "Rozšírenie prehliadača CommaFeed verzie {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed je kompatibilný s Fever API. Vo svojom mobilnom klientovi kompatibilnom s Fever použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed je kompatibilný s Google Reader API. Vo svojom mobilnom klientovi kompatibilnom s Google Reader použite nasledujúcu adresu URL. Prihláste sa pomocou svojho používateľského mena a <0>kľúča API." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed ďalšia neprečítaná položka" @@ -454,6 +458,14 @@ msgstr "Prejdite na dokumentáciu API." msgid "Goodies" msgstr "Dobrôtky" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Hroznová" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Tento kanál má starší filter, ktorý sa nedá upravovať a nepoužíva sa. Znova vytvorte filter pomocou nového editora výrazov. Pôvodný výraz filtra bol: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Toto je váš kľúč API. Môže sa použiť na niektoré operácie rozhrania API len na čítanie a poskytuje prístup k rozhraniu Fever API. Na vygenerovanie nového kľúča API použite formulár v dolnej časti stránky." +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Toto je váš kľúč API. Môže sa použiť pre niektoré operácie API iba na čítanie a udeľuje prístup k Fever API a Google Reader API. Na vygenerovanie nového kľúča API použite formulár v spodnej časti stránky" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/sv/messages.po b/commafeed-client/src/locales/sv/messages.po index 347350fa..45a55941 100644 --- a/commafeed-client/src/locales/sv/messages.po +++ b/commafeed-client/src/locales/sv/messages.po @@ -210,6 +210,10 @@ msgstr "Webbläsartillägget CommaFeed version {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed är kompatibelt med Fever API. Använd följande URL i din Fever-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed är kompatibelt med Google Reader API. Använd följande URL i din Google Reader-kompatibla mobilklient. Logga in med ditt användarnamn och din <0>API-nyckel." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed nästa olästa objekt" @@ -454,6 +458,14 @@ msgstr "Gå till API-dokumentationen." msgid "Goodies" msgstr "Godsaker" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Lila" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Detta flöde har ett gammalt filter som inte kan redigeras och inte tillämpas. Skapa filtret på nytt med den nya uttryckseditorern. Det gamla filteruttrycket var: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Detta är din API-nyckel. Den kan användas för vissa skrivskyddade API-operationer och ger åtkomst till Fever API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Det här är din API-nyckel. Den kan användas för vissa skrivskyddade API-åtgärder och ger åtkomst till Fever API och Google Reader API. Använd formuläret längst ner på sidan för att generera en ny API-nyckel" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/tr/messages.po b/commafeed-client/src/locales/tr/messages.po index fcde7521..4e77c8b3 100644 --- a/commafeed-client/src/locales/tr/messages.po +++ b/commafeed-client/src/locales/tr/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}." msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed, Fever API ile uyumludur. Fever uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız ile giriş yapın." +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed, Google Reader API ile uyumludur. Google Reader uyumlu mobil istemcinizde aşağıdaki URL'yi kullanın. Kullanıcı adınız ve <0>API anahtarınız ile giriş yapın." + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed sonraki okunmamış öğe" @@ -454,6 +458,14 @@ msgstr "API belgelerine gidin." msgid "Goodies" msgstr "İyilikler" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "Üzüm" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "Bu beslemenin düzenlenemeyen ve uygulanmayan eski bir filtresi var. Lütfen yeni ifade düzenleyiciyi kullanarak filtreyi yeniden oluşturun. Eski filtre ifadesi şuydu: <0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "Bu sizin API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API'sine erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "Bu, API anahtarınızdır. Bazı salt okunur API işlemleri için kullanılabilir ve Fever API ile Google Reader API'ye erişim sağlar. Yeni bir API anahtarı oluşturmak için sayfanın altındaki formu kullanın" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-client/src/locales/zh/messages.po b/commafeed-client/src/locales/zh/messages.po index 1987338b..8d41c712 100644 --- a/commafeed-client/src/locales/zh/messages.po +++ b/commafeed-client/src/locales/zh/messages.po @@ -210,6 +210,10 @@ msgstr "CommaFeed的浏览器扩展版本:{browserExtensionVersion}。" msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key." msgstr "CommaFeed 和 Fever API 兼容,可以在您Fever兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥登录。" +#: src/components/settings/ProfileSettings.tsx +msgid "CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible mobile client. Login with your username and your <0>API key." +msgstr "CommaFeed 和 Google Reader API 兼容,可以在您Google Reader兼容的移动客户端中使用以下网址,通过用户名和<0>API密钥登录。" + #: src/pages/app/AboutPage.tsx msgid "CommaFeed next unread item" msgstr "CommaFeed 下一个未读项目" @@ -454,6 +458,14 @@ msgstr "API 文档" msgid "Goodies" msgstr "好东西" +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API" +msgstr "" + +#: src/components/settings/ProfileSettings.tsx +msgid "Google Reader API URL" +msgstr "" + #: src/components/settings/DisplaySettings.tsx msgid "Grape" msgstr "深紫" @@ -1065,8 +1077,8 @@ msgid "This feed has a legacy filter that cannot be edited and is not applied. P msgstr "此信息流具有无法编辑且未应用的旧版过滤器。请使用新的表达式编辑器重新创建过滤器。旧版过滤器表达式为:<0>{0}" #: src/components/settings/ProfileSettings.tsx -msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key" -msgstr "这是您的API 密钥,它可以被用于Fever API的只读操作及访问授权。使用页面底部的表单生成一个新的API密钥。" +msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API and the Google Reader API. Use the form at the bottom of the page to generate a new API key" +msgstr "这是您的 API 密钥。它可用于某些只读 API 操作,并授予对 Fever API 和 Google Reader API 的访问权限。使用页面底部的表单生成新的 API 密钥" #: src/components/settings/DisplaySettings.tsx msgid "This setting can cause scrolling issues on some browsers (e.g. Safari)" diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderModel.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderModel.java new file mode 100644 index 00000000..570674e6 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderModel.java @@ -0,0 +1,229 @@ +package com.commafeed.frontend.resource.googlereader; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@JsonInclude(Include.NON_NULL) +public class GoogleReaderModel { + + private GoogleReaderModel() {} + + @Data + public static class UserInfo { + + @JsonProperty("userId") + private String userId; + + @JsonProperty("userName") + private String userName; + + @JsonProperty("userProfileId") + private String userProfileId; + + @JsonProperty("userEmail") + private String userEmail; + } + + @Data + public static class SubscriptionList { + + @JsonProperty("subscriptions") + private List subscriptions = new ArrayList<>(); + } + + @Data + public static class Subscription { + + @JsonProperty("id") + private String id; + + @JsonProperty("title") + private String title; + + @JsonProperty("categories") + private List categories = new ArrayList<>(); + + @JsonProperty("url") + private String url; + + @JsonProperty("htmlUrl") + private String htmlUrl; + + @JsonProperty("iconUrl") + private String iconUrl; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Category { + + @JsonProperty("id") + private String id; + + @JsonProperty("label") + private String label; + } + + @Data + public static class TagList { + + @JsonProperty("tags") + private List tags = new ArrayList<>(); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Tag { + + @JsonProperty("id") + private String id; + } + + @Data + public static class StreamContents { + + @JsonProperty("id") + private String id; + + @JsonProperty("title") + private String title; + + @JsonProperty("updated") + private long updated; + + @JsonProperty("items") + private List items = new ArrayList<>(); + + @JsonProperty("continuation") + private String continuation; + } + + @Data + public static class StreamItem { + + @JsonProperty("id") + private String id; + + @JsonProperty("crawlTimeMsec") + private String crawlTimeMsec; + + @JsonProperty("timestampUsec") + private String timestampUsec; + + @JsonProperty("published") + private long published; + + @JsonProperty("updated") + private long updated; + + @JsonProperty("title") + private String title; + + @JsonProperty("author") + private String author; + + @JsonProperty("categories") + private List categories = new ArrayList<>(); + + @JsonProperty("canonical") + private List canonical = new ArrayList<>(); + + @JsonProperty("alternate") + private List alternate = new ArrayList<>(); + + @JsonProperty("summary") + private Content summary; + + @JsonProperty("origin") + private Origin origin; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class HrefEntry { + + @JsonProperty("href") + private String href; + + @JsonProperty("type") + private String type; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Content { + + @JsonProperty("content") + private String content; + + @JsonProperty("direction") + private String direction; + } + + @Data + public static class Origin { + + @JsonProperty("streamId") + private String streamId; + + @JsonProperty("title") + private String title; + + @JsonProperty("htmlUrl") + private String htmlUrl; + } + + @Data + public static class ItemRefs { + + @JsonProperty("itemRefs") + private List itemRefs = new ArrayList<>(); + + @JsonProperty("continuation") + private String continuation; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ItemRef { + + @JsonProperty("id") + private String id; + } + + @Data + public static class UnreadCounts { + + @JsonProperty("max") + private long max; + + @JsonProperty("unreadcounts") + private List unreadCounts = new ArrayList<>(); + } + + @Data + public static class UnreadCountEntry { + + @JsonProperty("id") + private String id; + + @JsonProperty("count") + private long count; + + @JsonProperty("newestItemTimestampUsec") + private String newestItemTimestampUsec; + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderREST.java new file mode 100644 index 00000000..8a25329d --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderREST.java @@ -0,0 +1,834 @@ +package com.commafeed.frontend.resource.googlereader; + +import com.commafeed.backend.Digests; +import com.commafeed.backend.dao.FeedCategoryDAO; +import com.commafeed.backend.dao.FeedEntryDAO; +import com.commafeed.backend.dao.FeedEntryStatusDAO; +import com.commafeed.backend.dao.FeedSubscriptionDAO; +import com.commafeed.backend.feed.FeedUtils; +import com.commafeed.backend.model.FeedCategory; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryContent; +import com.commafeed.backend.model.FeedEntryStatus; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserSettings.ReadingOrder; +import com.commafeed.backend.service.FeedEntryService; +import com.commafeed.backend.service.FeedSubscriptionService; +import com.commafeed.backend.service.UserService; +import com.commafeed.frontend.model.UnreadCount; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; +import com.commafeed.security.mechanism.GoogleReaderAuthenticationMechanism; + +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; + +import lombok.RequiredArgsConstructor; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Google Reader compatible API + * + *
    + *
  • url: /rest/googlereader + *
  • login: username + *
  • password: api key + *
+ */ +@Path(GoogleReaderAuthenticationMechanism.PATH_PREFIX) +@RolesAllowed(Roles.USER) +@Produces(MediaType.APPLICATION_JSON) +@RequiredArgsConstructor +@Singleton +public class GoogleReaderREST { + + private static final int DEFAULT_ITEM_COUNT = 20; + private static final int MAX_ITEM_COUNT = 1000; + // cap at which clients should display "count+" instead of the exact unread count + private static final int MAX_UNREAD_COUNT = 10000; + + private final AuthenticationContext authenticationContext; + private final UserService userService; + private final FeedSubscriptionService feedSubscriptionService; + private final FeedEntryService feedEntryService; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryDAO feedEntryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + + @GET + @PermitAll + @Path("/") + @Produces(MediaType.TEXT_PLAIN) + @Operation(hidden = true) + public Response welcome() { + return Response.ok("This is the Google Reader API endpoint.").build(); + } + + @POST + @PermitAll + @Path("/accounts/ClientLogin") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Transactional + @Operation(hidden = true) + public Response clientLogin(@Context UriInfo uri, MultivaluedMap form) { + Map params = new HashMap<>(); + uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); + form.forEach((k, v) -> params.put(k, v.getFirst())); + + String password = params.get("Passwd"); + User user = StringUtils.isBlank(password) ? null : userService.login(password).orElse(null); + if (user == null) { + return Response.status(Status.FORBIDDEN).entity("Error=BadAuthentication\n").build(); + } + + String token = user.getName() + "/" + user.getApiKey(); + String body = "SID=" + token + "\nLSID=" + token + "\nAuth=" + token + "\n"; + return Response.ok(body).build(); + } + + @GET + @Path("/reader/api/0/token") + @Produces(MediaType.TEXT_PLAIN) + @Transactional + @Operation(hidden = true) + public Response token() { + return Response.ok(computeEditToken(currentUser())).build(); + } + + @GET + @Path("/reader/api/0/user-info") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.UserInfo userInfo() { + User user = currentUser(); + GoogleReaderModel.UserInfo info = new GoogleReaderModel.UserInfo(); + info.setUserId(String.valueOf(user.getId())); + info.setUserName(user.getName()); + info.setUserProfileId(String.valueOf(user.getId())); + info.setUserEmail(user.getEmail()); + return info; + } + + @GET + @Path("/reader/api/0/subscription/list") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.SubscriptionList subscriptionList() { + User user = currentUser(); + GoogleReaderModel.SubscriptionList resp = new GoogleReaderModel.SubscriptionList(); + resp.setSubscriptions( + feedSubscriptionDAO.findAll(user).stream().map(this::mapSubscription).toList()); + return resp; + } + + @POST + @Path("/reader/api/0/subscription/edit") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Transactional + @Operation(hidden = true) + public Response subscriptionEdit( + @FormParam("ac") String action, + @FormParam("s") String streamId, + @FormParam("t") String title, + @FormParam("a") String addLabel, + @FormParam("r") String removeLabel, + @FormParam("T") String token) { + User user = currentUser(); + if (!validToken(user, token)) { + return Response.status(Status.UNAUTHORIZED).build(); + } + if (StringUtils.isBlank(action) || StringUtils.isBlank(streamId)) { + return Response.status(Status.BAD_REQUEST).build(); + } + + if ("subscribe".equals(action)) { + String url = + streamId.startsWith(GoogleReaderStreamId.FEED_PREFIX) + ? streamId.substring(GoogleReaderStreamId.FEED_PREFIX.length()) + : streamId; + FeedCategory category = resolveLabelCategory(user, addLabel, true); + String subTitle = StringUtils.defaultIfBlank(title, url); + feedSubscriptionService.subscribe(user, url, subTitle, category, 0); + } else { + GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId); + Long subId = parsed.feedSubscriptionId(); + if (subId == null) { + return Response.status(Status.BAD_REQUEST).build(); + } + + if ("unsubscribe".equals(action)) { + feedSubscriptionService.unsubscribe(user, subId); + } else if ("edit".equals(action)) { + FeedSubscription sub = feedSubscriptionDAO.findById(user, subId); + if (sub != null) { + if (StringUtils.isNotBlank(title)) { + sub.setTitle(title); + } + if (StringUtils.isNotBlank(addLabel)) { + sub.setCategory(resolveLabelCategory(user, addLabel, true)); + } + if (StringUtils.isNotBlank(removeLabel)) { + sub.setCategory(null); + } + } + } + } + + return Response.ok("OK").build(); + } + + @GET + @Path("/reader/api/0/tag/list") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.TagList tagList() { + User user = currentUser(); + List tags = new ArrayList<>(); + feedCategoryDAO + .findAll(user) + .forEach( + c -> + tags.add( + new GoogleReaderModel.Tag( + GoogleReaderStreamId.label(c.getName())))); + tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED)); + + GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList(); + resp.setTags(tags); + return resp; + } + + @GET + @Path("/reader/api/0/unread-count") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.UnreadCounts unreadCounts() { + User user = currentUser(); + List subs = feedSubscriptionDAO.findAll(user); + Map unreadCounts = feedSubscriptionService.getUnreadCount(user); + + List entries = new ArrayList<>(); + Map unreadByCategory = new HashMap<>(); + long totalUnread = 0; + + for (FeedSubscription sub : subs) { + UnreadCount uc = unreadCounts.get(sub.getId()); + + GoogleReaderModel.UnreadCountEntry entry = new GoogleReaderModel.UnreadCountEntry(); + entry.setId(GoogleReaderStreamId.feed(sub.getId())); + entry.setCount(uc.getUnreadCount()); + entry.setNewestItemTimestampUsec(toUsec(uc.getNewestItemTime())); + entries.add(entry); + + totalUnread += uc.getUnreadCount(); + + long categoryId = sub.getCategory() == null ? 0 : sub.getCategory().getId(); + unreadByCategory.merge(categoryId, uc.getUnreadCount(), Long::sum); + } + + Map categoriesById = + feedCategoryDAO.findAll(user).stream() + .collect(Collectors.toMap(FeedCategory::getId, c -> c)); + unreadByCategory.forEach( + (categoryId, count) -> { + if (categoryId == 0) { + return; + } + FeedCategory category = categoriesById.get(categoryId); + if (category == null) { + return; + } + GoogleReaderModel.UnreadCountEntry entry = + new GoogleReaderModel.UnreadCountEntry(); + entry.setId(GoogleReaderStreamId.label(category.getName())); + entry.setCount(count); + entries.add(entry); + }); + + GoogleReaderModel.UnreadCountEntry readingList = new GoogleReaderModel.UnreadCountEntry(); + readingList.setId(GoogleReaderStreamId.READING_LIST); + readingList.setCount(totalUnread); + entries.add(readingList); + + GoogleReaderModel.UnreadCounts resp = new GoogleReaderModel.UnreadCounts(); + resp.setMax(MAX_UNREAD_COUNT); + resp.setUnreadCounts(entries); + return resp; + } + + @GET + @Path("/reader/api/0/stream/contents/{streamId : .+}") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.StreamContents streamContentsPath( + @PathParam("streamId") String streamId, + @QueryParam("n") Integer n, + @QueryParam("c") String continuation, + @QueryParam("r") String order, + @QueryParam("xt") List excludeTargets) { + return buildStreamContents(streamId, n, continuation, order, excludeTargets); + } + + @GET + @Path("/reader/api/0/stream/contents") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.StreamContents streamContentsQuery( + @QueryParam("s") String streamId, + @QueryParam("n") Integer n, + @QueryParam("c") String continuation, + @QueryParam("r") String order, + @QueryParam("xt") List excludeTargets) { + return buildStreamContents(streamId, n, continuation, order, excludeTargets); + } + + @GET + @Path("/reader/api/0/stream/items/ids") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.ItemRefs streamItemIds( + @QueryParam("s") String streamId, + @QueryParam("n") Integer n, + @QueryParam("c") String continuation, + @QueryParam("r") String order, + @QueryParam("xt") List excludeTargets) { + User user = currentUser(); + GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId); + int limit = clampLimit(n); + ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC; + boolean unreadOnly = isUnreadOnly(excludeTargets); + + PagedStatuses paged = + fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly); + + GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs(); + resp.setItemRefs( + paged.statuses().stream() + .map( + s -> + new GoogleReaderModel.ItemRef( + String.valueOf(s.getEntry().getId()))) + .toList()); + resp.setContinuation(paged.continuation()); + return resp; + } + + @POST + @Path("/reader/api/0/stream/items/contents") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.StreamContents streamItemsContentsPost( + @FormParam("i") List ids) { + return buildStreamItemsContents(ids); + } + + @GET + @Path("/reader/api/0/stream/items/contents") + @Transactional + @Operation(hidden = true) + public GoogleReaderModel.StreamContents streamItemsContentsGet( + @QueryParam("i") List ids) { + return buildStreamItemsContents(ids); + } + + @POST + @Path("/reader/api/0/edit-tag") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Transactional + @Operation(hidden = true) + public Response editTag( + @FormParam("i") List ids, + @FormParam("a") List add, + @FormParam("r") List remove, + @FormParam("T") String token) { + User user = currentUser(); + if (!validToken(user, token)) { + return Response.status(Status.UNAUTHORIZED).build(); + } + if (ids == null) { + return Response.ok("OK").build(); + } + + boolean markRead = containsTag(add, GoogleReaderStreamId.READ); + boolean markUnread = containsTag(remove, GoogleReaderStreamId.READ); + boolean star = containsTag(add, GoogleReaderStreamId.STARRED); + boolean unstar = containsTag(remove, GoogleReaderStreamId.STARRED); + + for (String rawId : ids) { + Long entryId = parseItemId(rawId); + if (entryId == null) { + continue; + } + FeedEntry entry = feedEntryDAO.findById(entryId); + if (entry == null) { + continue; + } + + if (markRead) { + feedEntryService.markEntry(user, entryId, true); + } + if (markUnread) { + feedEntryService.markEntry(user, entryId, false); + } + if (star || unstar) { + FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed()); + if (sub != null) { + if (star) { + feedEntryService.starEntry(user, entryId, sub.getId(), true); + } + if (unstar) { + feedEntryService.starEntry(user, entryId, sub.getId(), false); + } + } + } + } + + return Response.ok("OK").build(); + } + + @POST + @Path("/reader/api/0/mark-all-as-read") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + @Transactional + @Operation(hidden = true) + public Response markAllAsRead( + @FormParam("s") String streamId, + @FormParam("ts") Long ts, + @FormParam("T") String token) { + User user = currentUser(); + if (!validToken(user, token)) { + return Response.status(Status.UNAUTHORIZED).build(); + } + + GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(streamId); + Instant olderThan = (ts != null && ts > 0) ? Instant.ofEpochSecond(ts / 1_000_000) : null; + + switch (parsed.type()) { + case STARRED -> feedEntryService.markStarredEntries(user, olderThan, null); + case FEED -> { + Long subId = parsed.feedSubscriptionId(); + FeedSubscription sub = + subId == null ? null : feedSubscriptionDAO.findById(user, subId); + if (sub != null) { + feedEntryService.markSubscriptionEntries( + user, List.of(sub), olderThan, null, null); + } + } + case LABEL -> { + FeedCategory category = findCategoryByName(user, parsed.value()); + if (category != null) { + List categories = + feedCategoryDAO.findAllChildrenCategories(user, category); + List subs = + feedSubscriptionDAO.findByCategories(user, categories); + feedEntryService.markSubscriptionEntries(user, subs, olderThan, null, null); + } + } + default -> + feedEntryService.markSubscriptionEntries( + user, feedSubscriptionDAO.findAll(user), olderThan, null, null); + } + + return Response.ok("OK").build(); + } + + private GoogleReaderModel.StreamContents buildStreamContents( + String rawStreamId, + Integer n, + String continuation, + String order, + List excludeTargets) { + User user = currentUser(); + GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(rawStreamId); + int limit = clampLimit(n); + ReadingOrder readingOrder = "o".equals(order) ? ReadingOrder.ASC : ReadingOrder.DESC; + boolean unreadOnly = isUnreadOnly(excludeTargets); + + PagedStatuses paged = + fetchStatuses(user, parsed, limit, continuation, readingOrder, unreadOnly); + + GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents(); + resp.setId(streamIdString(parsed)); + resp.setTitle(streamTitle(user, parsed)); + resp.setUpdated(Instant.now().getEpochSecond()); + resp.setItems(paged.statuses().stream().map(this::mapItem).toList()); + resp.setContinuation(paged.continuation()); + return resp; + } + + private GoogleReaderModel.StreamContents buildStreamItemsContents(List ids) { + User user = currentUser(); + List subs = feedSubscriptionDAO.findAll(user); + Map subsByFeedId = + subs.stream() + .collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s, (a, b) -> a)); + + List items = new ArrayList<>(); + if (ids != null) { + for (String rawId : ids) { + Long entryId = parseItemId(rawId); + if (entryId == null) { + continue; + } + FeedEntry entry = feedEntryDAO.findById(entryId); + if (entry == null) { + continue; + } + FeedSubscription sub = subsByFeedId.get(entry.getFeed().getId()); + if (sub == null) { + continue; + } + FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); + items.add(mapItem(status)); + } + } + + GoogleReaderModel.StreamContents resp = new GoogleReaderModel.StreamContents(); + resp.setId(GoogleReaderStreamId.READING_LIST); + resp.setUpdated(Instant.now().getEpochSecond()); + resp.setItems(items); + return resp; + } + + private PagedStatuses fetchStatuses( + User user, + GoogleReaderStreamId streamId, + int limit, + String continuation, + ReadingOrder order, + boolean unreadOnly) { + Long minEntryId = null; + Long maxEntryId = null; + int offset = 0; + + if (streamId.type() == GoogleReaderStreamId.Type.STARRED) { + offset = parseOffset(continuation); + } else if (StringUtils.isNotBlank(continuation)) { + try { + long cursor = Long.parseLong(continuation); + if (order == ReadingOrder.ASC) { + minEntryId = cursor; + } else { + maxEntryId = cursor; + } + } catch (NumberFormatException ignored) { + // invalid continuation token, ignore and start from the beginning + } + } + + List statuses; + int fetchedCount; + switch (streamId.type()) { + case STARRED -> { + List starred = + feedEntryStatusDAO.findStarred( + user, null, null, offset, limit, order, true); + fetchedCount = starred.size(); + statuses = + unreadOnly ? starred.stream().filter(s -> !s.isRead()).toList() : starred; + } + case FEED -> { + Long subId = streamId.feedSubscriptionId(); + FeedSubscription sub = + subId == null ? null : feedSubscriptionDAO.findById(user, subId); + List subs = sub == null ? List.of() : List.of(sub); + statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subs, + unreadOnly, + null, + null, + 0, + limit, + order, + true, + null, + minEntryId, + maxEntryId); + fetchedCount = statuses.size(); + } + case LABEL -> { + FeedCategory category = findCategoryByName(user, streamId.value()); + List subs = + category == null + ? List.of() + : feedSubscriptionDAO.findByCategories( + user, + feedCategoryDAO.findAllChildrenCategories(user, category)); + statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subs, + unreadOnly, + null, + null, + 0, + limit, + order, + true, + null, + minEntryId, + maxEntryId); + fetchedCount = statuses.size(); + } + default -> { + List subs = feedSubscriptionDAO.findAll(user); + statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subs, + unreadOnly, + null, + null, + 0, + limit, + order, + true, + null, + minEntryId, + maxEntryId); + fetchedCount = statuses.size(); + } + } + + String nextContinuation = null; + if (fetchedCount >= limit && fetchedCount > 0) { + if (streamId.type() == GoogleReaderStreamId.Type.STARRED) { + nextContinuation = String.valueOf(offset + limit); + } else if (!statuses.isEmpty()) { + nextContinuation = String.valueOf(statuses.getLast().getEntry().getId()); + } + } + + return new PagedStatuses(statuses, nextContinuation); + } + + private GoogleReaderModel.StreamItem mapItem(FeedEntryStatus status) { + FeedEntry entry = status.getEntry(); + FeedSubscription sub = status.getSubscription(); + + GoogleReaderModel.StreamItem item = new GoogleReaderModel.StreamItem(); + item.setId(formatItemId(entry.getId())); + + Instant published = entry.getPublished() != null ? entry.getPublished() : Instant.EPOCH; + item.setPublished(published.getEpochSecond()); + item.setUpdated(published.getEpochSecond()); + item.setCrawlTimeMsec(String.valueOf(entry.getInserted().toEpochMilli())); + item.setTimestampUsec(String.valueOf(published.toEpochMilli() * 1000)); + item.setTitle(entry.getContent().getTitle()); + item.setAuthor(entry.getContent().getAuthor()); + + if (StringUtils.isNotBlank(entry.getUrl())) { + item.setCanonical(List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), null))); + item.setAlternate( + List.of(new GoogleReaderModel.HrefEntry(entry.getUrl(), "text/html"))); + } + item.setSummary( + new GoogleReaderModel.Content( + Optional.ofNullable(entry.getContent().getContent()).orElse(""), + entry.getContent().getDirection() == FeedEntryContent.Direction.RTL + ? "rtl" + : "ltr")); + + List categories = new ArrayList<>(); + categories.add(GoogleReaderStreamId.READING_LIST); + if (status.isRead()) { + categories.add(GoogleReaderStreamId.READ); + } + if (status.isStarred()) { + categories.add(GoogleReaderStreamId.STARRED); + } + if (sub != null && sub.getCategory() != null) { + categories.add(GoogleReaderStreamId.label(sub.getCategory().getName())); + } + item.setCategories(categories); + + if (sub != null) { + GoogleReaderModel.Origin origin = new GoogleReaderModel.Origin(); + origin.setStreamId(GoogleReaderStreamId.feed(sub.getId())); + origin.setTitle(sub.getTitle()); + origin.setHtmlUrl(sub.getFeed().getLink()); + item.setOrigin(origin); + } + + return item; + } + + private GoogleReaderModel.Subscription mapSubscription(FeedSubscription sub) { + GoogleReaderModel.Subscription s = new GoogleReaderModel.Subscription(); + s.setId(GoogleReaderStreamId.feed(sub.getId())); + s.setTitle(sub.getTitle()); + s.setUrl(sub.getFeed().getUrl()); + s.setHtmlUrl(sub.getFeed().getLink()); + s.setIconUrl(sub.getFeed().getIconUrl()); + if (sub.getCategory() != null) { + s.getCategories() + .add( + new GoogleReaderModel.Category( + GoogleReaderStreamId.label(sub.getCategory().getName()), + sub.getCategory().getName())); + } + return s; + } + + private FeedCategory resolveLabelCategory( + User user, String labelParam, boolean createIfMissing) { + if (StringUtils.isBlank(labelParam)) { + return null; + } + GoogleReaderStreamId parsed = GoogleReaderStreamId.parse(labelParam); + if (parsed.type() != GoogleReaderStreamId.Type.LABEL) { + return null; + } + + String name = parsed.value(); + FeedCategory category = findCategoryByName(user, name); + if (category == null && createIfMissing) { + category = new FeedCategory(); + category.setUser(user); + category.setName(FeedUtils.truncate(name, 128)); + feedCategoryDAO.persist(category); + } + return category; + } + + private FeedCategory findCategoryByName(User user, String name) { + return feedCategoryDAO.findAll(user).stream() + .filter(c -> c.getName().equals(name)) + .findFirst() + .orElse(null); + } + + private String streamIdString(GoogleReaderStreamId parsed) { + return switch (parsed.type()) { + case FEED -> GoogleReaderStreamId.FEED_PREFIX + parsed.value(); + case LABEL -> GoogleReaderStreamId.label(parsed.value()); + case STARRED -> GoogleReaderStreamId.STARRED; + default -> GoogleReaderStreamId.READING_LIST; + }; + } + + private String streamTitle(User user, GoogleReaderStreamId parsed) { + return switch (parsed.type()) { + case FEED -> { + Long subId = parsed.feedSubscriptionId(); + FeedSubscription sub = + subId == null ? null : feedSubscriptionDAO.findById(user, subId); + yield sub == null ? null : sub.getTitle(); + } + case LABEL -> parsed.value(); + case STARRED -> "Starred items"; + default -> "reading list"; + }; + } + + private boolean isUnreadOnly(List excludeTargets) { + return excludeTargets != null + && excludeTargets.stream().anyMatch(t -> t.endsWith("/state/com.google/read")); + } + + private boolean containsTag(List tags, String suffix) { + return tags != null && tags.stream().anyMatch(t -> t.endsWith(suffix)); + } + + private String formatItemId(long id) { + // Google Reader's canonical long-form item id, e.g. + // "tag:google.com,2005:reader/item/00000000148b9369", where the suffix is the item's + // numeric id, represented as a zero-padded, 16-digit hex string + return "tag:google.com,2005:reader/item/" + String.format("%016x", id); + } + + private Long parseItemId(String raw) { + if (StringUtils.isBlank(raw)) { + return null; + } + int idx = raw.lastIndexOf('/'); + if (raw.startsWith("tag:google.com") && idx != -1) { + try { + return Long.parseUnsignedLong(raw.substring(idx + 1), 16); + } catch (NumberFormatException e) { + return null; + } + } + try { + return Long.valueOf(raw); + } catch (NumberFormatException e) { + return null; + } + } + + private int parseOffset(String continuation) { + if (StringUtils.isBlank(continuation)) { + return 0; + } + try { + return Integer.parseInt(continuation); + } catch (NumberFormatException e) { + return 0; + } + } + + private int clampLimit(Integer n) { + if (n == null || n <= 0) { + return DEFAULT_ITEM_COUNT; + } + return Math.min(n, MAX_ITEM_COUNT); + } + + private String toUsec(Instant instant) { + return instant == null ? "0" : String.valueOf(instant.toEpochMilli() * 1000); + } + + private boolean validToken(User user, String token) { + // the edit token is a CSRF protection inherited from Google Reader's cookie-based + // authentication model. Since this API is authenticated via an api key header instead of + // cookies, that risk does not apply here. Some real-world clients (e.g. RSSGuard, unless + // configured as "Reedah"/"Miniflux") never call the "token" endpoint and always submit a + // blank token, so, blank tokens are accepted. + return StringUtils.isBlank(token) || computeEditToken(user).equals(token); + } + + private String computeEditToken(User user) { + return Digests.md5Hex(user.getApiKey() + ":" + user.getId()); + } + + private User currentUser() { + return authenticationContext.getCurrentUser(); + } + + private record PagedStatuses(List statuses, String continuation) {} +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderStreamId.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderStreamId.java new file mode 100644 index 00000000..f0bc0238 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/googlereader/GoogleReaderStreamId.java @@ -0,0 +1,60 @@ +package com.commafeed.frontend.resource.googlereader; + +import org.apache.commons.lang3.StringUtils; + +public record GoogleReaderStreamId(Type type, String value) { + + public enum Type { + ALL, + FEED, + LABEL, + STARRED + } + + public static final String READING_LIST = "user/-/state/com.google/reading-list"; + public static final String STARRED = "user/-/state/com.google/starred"; + public static final String READ = "user/-/state/com.google/read"; + public static final String LABEL_PREFIX = "user/-/label/"; + public static final String FEED_PREFIX = "feed/"; + + public static final GoogleReaderStreamId ALL_STREAM = new GoogleReaderStreamId(Type.ALL, null); + public static final GoogleReaderStreamId STARRED_STREAM = + new GoogleReaderStreamId(Type.STARRED, null); + + public static GoogleReaderStreamId parse(String raw) { + if (StringUtils.isBlank(raw) || "reading-list".equals(raw) || READING_LIST.equals(raw)) { + return ALL_STREAM; + } + if (STARRED.equals(raw)) { + return STARRED_STREAM; + } + if (raw.startsWith(LABEL_PREFIX)) { + return new GoogleReaderStreamId(Type.LABEL, raw.substring(LABEL_PREFIX.length())); + } + if (raw.startsWith(FEED_PREFIX)) { + return new GoogleReaderStreamId(Type.FEED, raw.substring(FEED_PREFIX.length())); + } + // unrecognized stream id, default to the whole reading list + return ALL_STREAM; + } + + public static String feed(long subscriptionId) { + return FEED_PREFIX + subscriptionId; + } + + public static String label(String categoryName) { + return LABEL_PREFIX + categoryName; + } + + /** extracts the subscription id out of a "feed/<id>" stream id, or null if invalid */ + public Long feedSubscriptionId() { + if (type != Type.FEED) { + return null; + } + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/GoogleReaderAuthenticationMechanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/GoogleReaderAuthenticationMechanism.java new file mode 100644 index 00000000..864989a1 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/GoogleReaderAuthenticationMechanism.java @@ -0,0 +1,69 @@ +package com.commafeed.security.mechanism; + +import io.quarkus.security.credential.TokenCredential; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +import jakarta.inject.Singleton; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; +import java.util.Set; + +/** + * Authenticates requests to the Google Reader compatible API, using the "Authorization: GoogleLogin + * auth=<token>" header set by Google Reader API clients. + * + *

This mechanism only applies to requests under {@link #PATH_PREFIX}, so that a stray + * "Authorization: GoogleLogin ..." header sent to any other endpoint is not mistakenly accepted as + * an api key authentication. + */ +@Singleton +public class GoogleReaderAuthenticationMechanism implements HttpAuthenticationMechanism { + + public static final String PATH_PREFIX = "/rest/googlereader"; + + private static final String AUTH_HEADER = "Authorization"; + private static final String AUTH_PREFIX = "GoogleLogin auth="; + + @Override + public Uni authenticate( + RoutingContext context, IdentityProviderManager identityProviderManager) { + if (!context.normalizedPath().startsWith(PATH_PREFIX)) { + return Uni.createFrom().optional(Optional.empty()); + } + + String header = context.request().getHeader(AUTH_HEADER); + if (StringUtils.isBlank(header) || !header.startsWith(AUTH_PREFIX)) { + return Uni.createFrom().optional(Optional.empty()); + } + + String token = header.substring(AUTH_PREFIX.length()).trim(); + int slashIndex = token.lastIndexOf('/'); + String apiKey = slashIndex == -1 ? token : token.substring(slashIndex + 1); + if (StringUtils.isBlank(apiKey)) { + return Uni.createFrom().optional(Optional.empty()); + } + + TokenCredential credential = new TokenCredential(apiKey, "apiKey"); + TokenAuthenticationRequest request = new TokenAuthenticationRequest(credential); + return identityProviderManager.authenticate(request); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().optional(Optional.empty()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(TokenAuthenticationRequest.class); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/GoogleReaderIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/GoogleReaderIT.java new file mode 100644 index 00000000..90c0f417 --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/GoogleReaderIT.java @@ -0,0 +1,322 @@ +package com.commafeed.integration.rest; + +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.Entry; +import com.commafeed.frontend.model.UserModel; +import com.commafeed.frontend.model.request.ProfileModificationRequest; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.ItemRefs; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.StreamContents; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.SubscriptionList; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.TagList; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UnreadCounts; +import com.commafeed.frontend.resource.googlereader.GoogleReaderModel.UserInfo; +import com.commafeed.integration.BaseIT; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; + +import lombok.Getter; + +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@QuarkusTest +class GoogleReaderIT extends BaseIT { + + private static final String BASE = "rest/googlereader"; + + @Getter private String apiKey; + private GoogleReaderClient client; + + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + + // create api key + ProfileModificationRequest req = new ProfileModificationRequest(); + req.setCurrentPassword(TestConstants.ADMIN_PASSWORD); + req.setNewApiKey(true); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); + + // retrieve api key + UserModel user = + RestAssured.given() + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(UserModel.class); + this.apiKey = user.getApiKey(); + this.client = new GoogleReaderClient(clientLogin(TestConstants.ADMIN_USERNAME, apiKey)); + } + + @AfterEach + void cleanup() { + RestAssured.reset(); + } + + private String clientLogin(String email, String password) { + String body = + RestAssured.given() + .auth() + .none() + .formParam("Email", email) + .formParam("Passwd", password) + .post(BASE + "/accounts/ClientLogin") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .asString(); + + Map values = + Arrays.stream(body.split("\n")) + .map(line -> line.split("=", 2)) + .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1])); + return values.get("Auth"); + } + + @Test + void invalidClientLogin() { + RestAssured.given() + .auth() + .none() + .formParam("Email", TestConstants.ADMIN_USERNAME) + .formParam("Passwd", "invalid-key") + .post(BASE + "/accounts/ClientLogin") + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + void validClientLogin() { + String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey); + Assertions.assertNotNull(token); + Assertions.assertTrue(token.endsWith("/" + apiKey)); + } + + @Test + void invalidAuthToken() { + Response response = + RestAssured.given() + .auth() + .none() + .header("Authorization", "GoogleLogin auth=invalid/invalid-key") + .get(BASE + "/reader/api/0/user-info"); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.statusCode()); + } + + @Test + void authTokenOnlyAppliesToGoogleReaderEndpoints() { + String token = clientLogin(TestConstants.ADMIN_USERNAME, apiKey); + Response response = + RestAssured.given() + .auth() + .none() + .header("Authorization", "GoogleLogin auth=" + token) + .get("rest/user/profile"); + Assertions.assertNotEquals(HttpStatus.SC_OK, response.statusCode()); + } + + @Test + void userInfo() { + UserInfo info = client.get(BASE + "/reader/api/0/user-info").as(UserInfo.class); + Assertions.assertEquals(TestConstants.ADMIN_USERNAME, info.getUserName()); + } + + @Test + void subscriptionList() { + subscribe(getFeedUrl()); + SubscriptionList list = + client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class); + Assertions.assertEquals(1, list.getSubscriptions().size()); + Assertions.assertEquals( + "my title for this feed", list.getSubscriptions().getFirst().getTitle()); + } + + @Test + void tagList() { + createCategory("test-category"); + TagList tagList = client.get(BASE + "/reader/api/0/tag/list").as(TagList.class); + Assertions.assertTrue( + tagList.getTags().stream() + .anyMatch(t -> t.getId().equals("user/-/label/test-category"))); + Assertions.assertTrue( + tagList.getTags().stream() + .anyMatch(t -> t.getId().equals("user/-/state/com.google/starred"))); + } + + @Test + void unreadCount() { + subscribeAndWaitForEntries(getFeedUrl()); + UnreadCounts counts = + client.get(BASE + "/reader/api/0/unread-count").as(UnreadCounts.class); + Assertions.assertTrue( + counts.getUnreadCounts().stream() + .anyMatch( + c -> + c.getId().equals("user/-/state/com.google/reading-list") + && c.getCount() == 2)); + } + + @Test + void streamContents() { + subscribeAndWaitForEntries(getFeedUrl()); + StreamContents contents = + client.get(BASE + "/reader/api/0/stream/contents/reading-list") + .as(StreamContents.class); + Assertions.assertEquals(2, contents.getItems().size()); + Assertions.assertEquals("Item 2", contents.getItems().getFirst().getTitle()); + Assertions.assertTrue( + contents.getItems() + .getFirst() + .getId() + .matches("tag:google\\.com,2005:reader/item/[0-9a-f]{16}")); + } + + @Test + void streamItemIds() { + subscribeAndWaitForEntries(getFeedUrl()); + ItemRefs refs = + client.get( + BASE + + "/reader/api/0/stream/items/ids?s=user/-/state/com.google/reading-list") + .as(ItemRefs.class); + Assertions.assertEquals(2, refs.getItemRefs().size()); + } + + @Test + void editTagMarkRead() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + + String token = client.get(BASE + "/reader/api/0/token").asString(); + client.postForm( + BASE + "/reader/api/0/edit-tag", + Map.of("i", entry.getId(), "a", "user/-/state/com.google/read", "T", token)); + + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); + + client.postForm( + BASE + "/reader/api/0/edit-tag", + Map.of("i", entry.getId(), "r", "user/-/state/com.google/read", "T", token)); + + Assertions.assertEquals( + 0, + getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); + } + + @Test + void editTagMarkReadWithoutToken() { + // some real-world Google Reader API clients never fetch the edit token and always submit + // a blank one, this must still be accepted + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + + client.postForm( + BASE + "/reader/api/0/edit-tag", + Map.of("i", entry.getId(), "a", "user/-/state/com.google/read")); + + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); + } + + @Test + void editTagStar() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + + String token = client.get(BASE + "/reader/api/0/token").asString(); + client.postForm( + BASE + "/reader/api/0/edit-tag", + Map.of("i", entry.getId(), "a", "user/-/state/com.google/starred", "T", token)); + + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream() + .filter(Entry::isStarred) + .count()); + } + + @Test + void markAllAsRead() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + + String token = client.get(BASE + "/reader/api/0/token").asString(); + client.postForm( + BASE + "/reader/api/0/mark-all-as-read", + Map.of("s", "user/-/state/com.google/reading-list", "T", token)); + + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); + } + + @Test + void subscribeAndUnsubscribe() { + String token = client.get(BASE + "/reader/api/0/token").asString(); + + client.postForm( + BASE + "/reader/api/0/subscription/edit", + Map.of( + "ac", + "subscribe", + "s", + "feed/" + getFeedUrl(), + "t", + "new subscription", + "T", + token)); + + SubscriptionList list = + client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class); + Assertions.assertEquals(1, list.getSubscriptions().size()); + String feedStreamId = list.getSubscriptions().getFirst().getId(); + + client.postForm( + BASE + "/reader/api/0/subscription/edit", + Map.of("ac", "unsubscribe", "s", feedStreamId, "T", token)); + + list = client.get(BASE + "/reader/api/0/subscription/list").as(SubscriptionList.class); + Assertions.assertTrue(list.getSubscriptions().isEmpty()); + } + + private record GoogleReaderClient(String token) { + + private RequestSpecification given() { + return RestAssured.given() + .auth() + .none() + .header("Authorization", "GoogleLogin auth=" + token); + } + + Response get(String path) { + return given().get(path).then().statusCode(HttpStatus.SC_OK).extract().response(); + } + + void postForm(String path, Map form) { + RequestSpecification spec = given(); + form.forEach(spec::formParam); + spec.post(path).then().statusCode(HttpStatus.SC_OK); + } + } +}