Compare commits
No commits in common. "develop" and "compose-migration" have entirely different histories.
develop
...
compose-mi
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
*.html linguist-vendored
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
||||
custom: ["https://paypal.me/readropsapp"]
|
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,32 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix a bug
|
||||
title: "[Bug] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment information (please complete the following information):**
|
||||
- Account type: [e.g. FreshRSS, Nextcloud News]
|
||||
- App version: [e.g. 2.0]
|
||||
- Android version: [e.g. Android 13, 14]
|
||||
- Device type: [e.g. One Plus 12, Samsung Galaxy S23]
|
||||
- Store: [e.g F-Droid, Play Store, standalone apk]
|
||||
- [ ] Stacktrace collected from crash screen
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
18
.github/workflows/android.yml
vendored
18
.github/workflows/android.yml
vendored
@ -11,28 +11,22 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 1.17
|
||||
- name: set up JDK 1.11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
java-version: '11'
|
||||
- name: Android Emulator Runner
|
||||
uses: ReactiveCircus/android-emulator-runner@v2.33.0
|
||||
uses: ReactiveCircus/android-emulator-runner@v2.28.0
|
||||
with:
|
||||
api-level: 29
|
||||
script: ./gradlew clean build connectedCheck jacocoFullReport
|
||||
- uses: codecov/codecov-action@v4
|
||||
- uses: codecov/codecov-action@v2.1.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./build/reports/jacoco/jacocoFullReport/jacocoFullReport.xml
|
||||
files: ./build/reports/jacoco/jacocoFullReport.xml
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -135,5 +135,3 @@ Gemfile
|
||||
mapping/
|
||||
|
||||
**/*.exec
|
||||
|
||||
.kotlin/
|
||||
|
98
CHANGELOG.md
98
CHANGELOG.md
@ -1,94 +1,8 @@
|
||||
# v2.0.3
|
||||
- Fix Fever API compatibility with TinyTiny RSS and yarr, should also fix other providers (#228 + #229)
|
||||
- Fix Nextcloud News item duplicates when syncing which would made the app unusable
|
||||
- Fix Nextcloud News item parsing: items with no title will be ignored
|
||||
|
||||
# v2.0.2
|
||||
- Fix crash when opening app from a notification (#223)
|
||||
- Fix Fever API synchronization error (#228)
|
||||
|
||||
# v2.0.1
|
||||
|
||||
- Make Timeline tab filters persistent (#138)
|
||||
- Change Timeline tab order field default value (#202)
|
||||
- Fix crash when adding a Fever API account (#200)
|
||||
- Be less strict with feed and folder names (#206)
|
||||
|
||||
# v2.0
|
||||
|
||||
- Restore swipe to mark as read (#188)
|
||||
- Restore Ordering by article id in Timeline tab
|
||||
- Improve OPML file picker filtering (#195)
|
||||
- Translation updates
|
||||
- See previous beta versions to get full changelog since v1.3
|
||||
|
||||
# v2.0-beta02
|
||||
|
||||
- Fix migration issues from v1.3 and older (especially for F-Droid builds)
|
||||
- Make Preferences screen scrollable (#190)
|
||||
- Fix wrong translation in RadioButtonPreferenceDialog (#185)
|
||||
- Translation updates
|
||||
|
||||
# v2.0-beta01
|
||||
|
||||
## General
|
||||
|
||||
- 🆕 design:
|
||||
- 🆕 Material3: Readrops implements last material design system version
|
||||
- 🆕 Bottom bar navigation: you can now navigate to feeds and account management way more easily, with 4 tabs in total:
|
||||
- Timeline
|
||||
- Feeds
|
||||
- Account
|
||||
- More
|
||||
- Timeline tab:
|
||||
- 🆕 Article size: you can now choose among three article sizes: compact, regular and large
|
||||
- 🆕 You can now show only articles less than 24h old
|
||||
- 🆕 Mark all articles as read FAB: the floating action button now lets you mark all articles read, taking into account the current filter, replacing opening new feed activity action
|
||||
- 🆕 Mark articles read on scroll: an option is now available to mark items read on scroll
|
||||
- 🆕 Drawer: hide folders and feeds without unread articles
|
||||
- 🆕 Local account: sync now respects the current filter and will only synchronize affected feeds
|
||||
- Feeds Tab:
|
||||
- 🆕 Feeds and folder management have been merged into a single screen
|
||||
- Account Tab:
|
||||
- 🆕 Add, manage and remove any account from Account Tab
|
||||
- More Tab:
|
||||
- 🆕 This new screen gathers some app infos, parameters, open source libraries and a donation dialog
|
||||
- Articles screen:
|
||||
- The global UI has been improved with a new title layout
|
||||
- 🆕 Action bottom bar: you now have access to a collapsable bottom bar containing the following actions:
|
||||
- Mark as read/non read
|
||||
- Add to favorites/remove from favorites
|
||||
- Share
|
||||
- Open in external navigator/navigator view
|
||||
- 🆕 A new font, Inter is used for the article content
|
||||
- 🆕 Some html tags look have been improved:
|
||||
- blockquote
|
||||
- figure/figcaption
|
||||
- iframe
|
||||
- table
|
||||
- "Open in" option has been reduced to two options: navigator view and external navigator
|
||||
- 🆕 FEVER API implementation, should work with any provider which supports it
|
||||
- Migrate to Nextcloud News API 1.3
|
||||
- 🆕 Follow system theme option (default)
|
||||
- 🆕 Option to disable battery optimization for background synchronization
|
||||
- Add support for new Android versions until Android 14 (API 34)
|
||||
|
||||
## Technical
|
||||
|
||||
- The UI has been entirely rewritten in Kotlin using Jetpack Compose, moving from old traditional view system
|
||||
- All other Java parts have also been rewritten in Kotlin, including API implementations, repositories, etc
|
||||
- RXJava was replaced by Kotlin coroutines and flows
|
||||
- Migrate to Gradle Kotlin DSL
|
||||
- Migrate dependencies to Version Catalog
|
||||
- 🆕 Support user certificates
|
||||
|
||||
# v1.3.1
|
||||
|
||||
- FreshRSS : Fix items being fav unintentionally
|
||||
- FreshRSS : Fix 401 error when synchronising for the second time
|
||||
|
||||
# v1.3.0
|
||||
|
||||
- New local RSS parser, much reliable
|
||||
- New external navigator view for items (Custom tabs)
|
||||
- FreshRSS and Nextcloud News favorites
|
||||
@ -150,13 +64,15 @@ Fix a crash related to Proguard Rules.
|
||||
|
||||
# v1.0.2
|
||||
|
||||
- Add swipe background to main list items
|
||||
- Add preference to parse a fixed number of items when adding a local feed
|
||||
- Change feed/folders way to interact
|
||||
- Minor bug fixes and improvements
|
||||
- Add swipe background to main list items
|
||||
- Add preference to parse a fixed number of items when adding a local feed
|
||||
- Change feed/folders way to interact
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
|
||||
|
||||
# v1.0.1
|
||||
|
||||
|
||||
# v1.0 Initial release
|
||||
|
||||
- Local RSS parsing
|
||||
|
35
README.md
35
README.md
@ -1,45 +1,40 @@
|
||||
<p align="center">
|
||||
<img src="fastlane/metadata/android/en-US/images/icon.png" width=180>
|
||||
<img src="fastlane/metadata/android/en-US/images/icon.png" width=180>
|
||||
</p>
|
||||
|
||||
<h1 align="center"><b>Readrops</b></h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/readrops/Readrops/actions"><img src="https://github.com/readrops/Readrops/actions/workflows/android.yml/badge.svg?branch=develop"></a>
|
||||
<a href="https://github.com/readrops/Readrops/actions"><img src="https://github.com/readrops/Readrops/workflows/Android%20CI/badge.svg?branch=develop"></a>
|
||||
<a href="https://codecov.io/gh/readrops/Readrops"><img src="https://codecov.io/gh/readrops/Readrops/branch/develop/graph/badge.svg?token=229PNPQPMM"></a>
|
||||
<a href="https://hosted.weblate.org/engage/readrops/"><img src="https://hosted.weblate.org/widgets/readrops/-/strings/svg-badge.svg"/></a>
|
||||
|
||||
<h4 align="center">Readrops is a multi-services RSS client for Android. Its name is composed of "Read" and "drops", where drops are articles in an ocean of news.</h4>
|
||||
<h4 align="center">Readrops is a multi-services RSS client for Android. Its name is composed of "Read" and "drops", where drops are information drops in an ocean of news.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.readrops.app"><img src="images/google-play-badge.png" width=250></a>
|
||||
<a href="https://f-droid.org/en/packages/com.readrops.app/"><img src="images/fdroid-badge.png" width=250></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.readrops.app"><img src="images/google-play-badge.png" width=250></a>
|
||||
<a href="https://f-droid.org/en/packages/com.readrops.app/"><img src="images/fdroid-badge.png" width=250></a>
|
||||
</p>
|
||||
|
||||
</p>
|
||||
|
||||
# Features
|
||||
|
||||
- Local RSS parsing:
|
||||
- RSS2
|
||||
- RSS1
|
||||
- ATOM
|
||||
- JSONFeed
|
||||
- External services:
|
||||
- FreshRSS
|
||||
- Nextcloud News
|
||||
- Fever API
|
||||
- Multi-account
|
||||
- Feeds and folders management (create, update and delete feeds/folders if supported by the service API)
|
||||
- OPML import/export
|
||||
- Local RSS parsing : support for RSS 2, RSS 1, ATOM and JSONFeed
|
||||
- Nextcloud news support
|
||||
- FreshRSS support
|
||||
- Multiple accounts
|
||||
- Feeds and folders management (create, update and delete feeds/folders if your service API supports it)
|
||||
- Background synchronisation
|
||||
- Notifications
|
||||
|
||||
# Screenshots
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_1.jpg" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_2.jpg" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_3.jpg" width=250>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_1.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_2.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_3.png" width=250>
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_4.jpg" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_5.jpg" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_6.jpg" width=250>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_4.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_5.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_6.png" width=250>
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_7.jpg" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_8.jpg" width=250>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_7.png" width=250> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_8.png" width=250>
|
||||
|
||||
# Licence
|
||||
|
||||
|
80
api/build.gradle
Normal file
80
api/build.gradle
Normal file
@ -0,0 +1,80 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/androidTest/assets".toString())
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled false
|
||||
testCoverageEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
namespace 'com.readrops.api'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':db')
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
|
||||
testImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version"
|
||||
testImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
|
||||
implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0'
|
||||
implementation 'org.redundent:kotlin-xml-builder:1.7.3'
|
||||
|
||||
api 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
|
||||
implementation('com.squareup.retrofit2:retrofit:2.9.0') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp3'
|
||||
}
|
||||
implementation('com.squareup.retrofit2:converter-moshi:2.9.0') {
|
||||
exclude group: 'com.squareup.moshi', module: 'moshi'
|
||||
}
|
||||
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
|
||||
|
||||
implementation 'com.squareup.moshi:moshi:1.12.0'
|
||||
|
||||
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
debugApi 'com.chimerapps.niddler:niddler:1.5.5'
|
||||
releaseApi 'com.chimerapps.niddler:niddler-noop:1.5.5'
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.readrops.api"
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
enableUnitTestCoverage = true
|
||||
}
|
||||
|
||||
create("beta") {
|
||||
initWith(getByName("release"))
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("androidTest") {
|
||||
assets.srcDirs("$projectDir/androidTest/assets")
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xstring-concat=inline")
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":db"))
|
||||
|
||||
coreLibraryDesugaring(libs.jdk.desugar)
|
||||
|
||||
testImplementation(libs.junit4)
|
||||
|
||||
implementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
||||
implementation(platform(libs.koin.bom))
|
||||
implementation(libs.bundles.koin)
|
||||
testImplementation(libs.bundles.kointest)
|
||||
|
||||
implementation(libs.konsumexml)
|
||||
implementation(libs.kotlinxmlbuilder)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
testImplementation(libs.okhttp.mockserver)
|
||||
|
||||
implementation(libs.bundles.retrofit) {
|
||||
exclude("com.squareup.okhttp3", "okhttp3")
|
||||
exclude("com.squareup.moshi", "moshi")
|
||||
}
|
||||
|
||||
implementation(libs.moshi)
|
||||
implementation(libs.jsoup)
|
||||
}
|
@ -2,27 +2,14 @@ package com.readrops.api
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.api.services.fever.FeverDataSource
|
||||
import com.readrops.api.services.fever.FeverService
|
||||
import com.readrops.api.services.fever.adapters.FeverAPIAdapter
|
||||
import com.readrops.api.services.fever.adapters.FeverFaviconsAdapter
|
||||
import com.readrops.api.services.fever.adapters.FeverFeeds
|
||||
import com.readrops.api.services.fever.adapters.FeverFeedsAdapter
|
||||
import com.readrops.api.services.fever.adapters.FeverFoldersAdapter
|
||||
import com.readrops.api.services.fever.adapters.FeverItemsAdapter
|
||||
import com.readrops.api.services.fever.adapters.FeverItemsIdsAdapter
|
||||
import com.readrops.api.services.greader.GReaderDataSource
|
||||
import com.readrops.api.services.greader.GReaderService
|
||||
import com.readrops.api.services.greader.adapters.FreshRSSUserInfoAdapter
|
||||
import com.readrops.api.services.greader.adapters.GReaderFeedsAdapter
|
||||
import com.readrops.api.services.greader.adapters.GReaderFoldersAdapter
|
||||
import com.readrops.api.services.greader.adapters.GReaderItemsAdapter
|
||||
import com.readrops.api.services.greader.adapters.GReaderItemsIdsAdapter
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsDataSource
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsService
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextcloudNewsFeedsAdapter
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextcloudNewsFoldersAdapter
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextcloudNewsItemsAdapter
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource
|
||||
import com.readrops.api.services.freshrss.FreshRSSService
|
||||
import com.readrops.api.services.freshrss.adapters.*
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsDataSource
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsService
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.api.utils.ErrorInterceptor
|
||||
import com.readrops.db.entities.Item
|
||||
@ -32,6 +19,7 @@ import okhttp3.OkHttpClient
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -39,12 +27,12 @@ val apiModule = module {
|
||||
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.addInterceptor(get<AuthInterceptor>())
|
||||
.addInterceptor(get<ErrorInterceptor>())
|
||||
//.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler"))
|
||||
.build()
|
||||
.callTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.HOURS)
|
||||
.addInterceptor(get<AuthInterceptor>())
|
||||
.addInterceptor(get<ErrorInterceptor>())
|
||||
//.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler"))
|
||||
.build()
|
||||
}
|
||||
|
||||
single { AuthInterceptor() }
|
||||
@ -53,77 +41,53 @@ val apiModule = module {
|
||||
|
||||
single { LocalRSSDataSource(get()) }
|
||||
|
||||
//region greader/freshrss
|
||||
//region freshrss
|
||||
|
||||
factory { params -> GReaderDataSource(get(parameters = { params })) }
|
||||
factory { params -> FreshRSSDataSource(get(parameters = { params })) }
|
||||
|
||||
factory { (credentials: Credentials) ->
|
||||
Retrofit.Builder()
|
||||
.baseUrl(credentials.url)
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("greaderMoshi"))))
|
||||
.build()
|
||||
.create(GReaderService::class.java)
|
||||
.baseUrl(credentials.url)
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
|
||||
.build()
|
||||
.create(FreshRSSService::class.java)
|
||||
}
|
||||
|
||||
single(named("greaderMoshi")) {
|
||||
single(named("freshrssMoshi")) {
|
||||
Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), GReaderItemsAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, String::class.java), GReaderItemsIdsAdapter())
|
||||
.add(GReaderFeedsAdapter())
|
||||
.add(GReaderFoldersAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), FreshRSSItemsAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter())
|
||||
.add(FreshRSSFeedsAdapter())
|
||||
.add(FreshRSSFoldersAdapter())
|
||||
.add(FreshRSSUserInfoAdapter())
|
||||
.build()
|
||||
}
|
||||
|
||||
//endregion greader/freshrss
|
||||
//endregion freshrss
|
||||
|
||||
//region nextcloud news
|
||||
|
||||
factory { params -> NextcloudNewsDataSource(get(parameters = { params })) }
|
||||
factory { params -> NextNewsDataSource(get(parameters = { params })) }
|
||||
|
||||
factory { (credentials: Credentials) ->
|
||||
Retrofit.Builder()
|
||||
.baseUrl(credentials.url)
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi"))))
|
||||
.build()
|
||||
.create(NextcloudNewsService::class.java)
|
||||
.create(NextNewsService::class.java)
|
||||
}
|
||||
|
||||
single(named("nextcloudNewsMoshi")) {
|
||||
Moshi.Builder()
|
||||
.add(NextcloudNewsFeedsAdapter())
|
||||
.add(NextcloudNewsFoldersAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), NextcloudNewsItemsAdapter())
|
||||
.add(NextNewsFeedsAdapter())
|
||||
.add(NextNewsFoldersAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), NextNewsItemsAdapter())
|
||||
.build()
|
||||
}
|
||||
|
||||
//endregion nextcloud news
|
||||
|
||||
//region Fever
|
||||
|
||||
factory { params -> FeverDataSource(get(parameters = { params })) }
|
||||
|
||||
factory { (credentials: Credentials) ->
|
||||
Retrofit.Builder()
|
||||
.baseUrl(credentials.url)
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("feverMoshi"))))
|
||||
.build()
|
||||
.create(FeverService::class.java)
|
||||
}
|
||||
|
||||
single(named("feverMoshi")) {
|
||||
Moshi.Builder()
|
||||
.add(FeverFoldersAdapter())
|
||||
.add(FeverFeeds::class.java, FeverFeedsAdapter())
|
||||
.add(FeverItemsAdapter())
|
||||
.add(FeverFaviconsAdapter())
|
||||
.add(Boolean::class.java, FeverAPIAdapter())
|
||||
.add(FeverItemsIdsAdapter())
|
||||
.build()
|
||||
}
|
||||
|
||||
//endregion Fever
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import android.accounts.NetworkErrorException
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
@ -21,6 +22,7 @@ import okio.Buffer
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent {
|
||||
@ -73,8 +75,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent {
|
||||
val rootKonsumer = nextElement(LocalRSSHelper.RSS_ROOT_NAMES)
|
||||
rootKonsumer?.let { type = LocalRSSHelper.guessRSSType(rootKonsumer) }
|
||||
} catch (e: Exception) {
|
||||
close()
|
||||
return false
|
||||
throw UnknownFormatException(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.readrops.api.localfeed
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.readrops.api.utils.extensions.checkRoot
|
||||
import java.io.InputStream
|
||||
|
||||
object LocalRSSHelper {
|
||||
|
||||
@ -25,11 +26,12 @@ object LocalRSSHelper {
|
||||
RSS_1_CONTENT_TYPE -> RSSType.RSS_1
|
||||
RSS_2_CONTENT_TYPE -> RSSType.RSS_2
|
||||
ATOM_CONTENT_TYPE -> RSSType.ATOM
|
||||
JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
|
||||
JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
|
||||
else -> RSSType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isRSSType(type: String?): Boolean =
|
||||
if (type != null) getRSSType(type) != RSSType.UNKNOWN else false
|
||||
|
||||
|
@ -1,41 +0,0 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
object RSSMedia {
|
||||
|
||||
fun parseMediaContent(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
val url = attributes.getValueOrNull("url")
|
||||
|
||||
if (url != null && isUrlImage(url) && item.imageLink == null) {
|
||||
item.imageLink = url
|
||||
}
|
||||
|
||||
konsumer.skipContents() // ignore media content sub elements
|
||||
}
|
||||
|
||||
fun parseMediaGroup(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
allChildrenAutoIgnore(Names.of("content", "thumbnail", "description")) {
|
||||
when (tagName) {
|
||||
"media:content" -> parseMediaContent(this, item)
|
||||
"media:thumbnail"-> parseMediaContent(this, item)
|
||||
"media:description" -> {
|
||||
// Youtube case, might be useful for others
|
||||
val description = nullableTextRecursively()
|
||||
if (item.text == null) {
|
||||
item.content = description
|
||||
}
|
||||
}
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUrlImage(url: String): Boolean = with(url) {
|
||||
return endsWith(".jpg") || endsWith(".jpeg") || endsWith(".png")
|
||||
}
|
||||
}
|
@ -28,9 +28,7 @@ class ATOMFeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> parseLink(this@allChildrenAutoIgnore, feed)
|
||||
"subtitle" -> description = nullableText()
|
||||
"logo" -> imageUrl = nullableText()
|
||||
"entry" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,6 +51,6 @@ class ATOMFeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "subtitle", "logo", "entry")
|
||||
val names = Names.of("title", "link", "subtitle", "entry")
|
||||
}
|
||||
}
|
@ -3,15 +3,14 @@ package com.readrops.api.localfeed.atom
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.readrops.api.localfeed.RSSMedia
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import java.time.LocalDateTime
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class ATOMItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
@ -23,19 +22,12 @@ class ATOMItemAdapter : XmlAdapter<Item> {
|
||||
konsumer.allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"id" -> remoteId = nullableText()
|
||||
"published" -> pubDate = DateUtils.parse(nullableText())
|
||||
"updated" -> {
|
||||
val updated = nullableText()
|
||||
if (pubDate == null) {
|
||||
pubDate = DateUtils.parse(updated)
|
||||
}
|
||||
}
|
||||
"id" -> guid = nullableText()
|
||||
"updated" -> pubDate = DateUtils.parse(nullableText())
|
||||
"link" -> parseLink(this, this@apply)
|
||||
"author" -> allChildrenAutoIgnore("name") { author = nullableText() }
|
||||
"summary" -> description = nullableTextRecursively()
|
||||
"content" -> content = nullableTextRecursively()
|
||||
"media:group" -> RSSMedia.parseMediaGroup(this, item)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
@ -43,7 +35,7 @@ class ATOMItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
validateItem(item)
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.remoteId == null) item.remoteId = item.link
|
||||
if (item.guid == null) item.guid = item.link
|
||||
|
||||
item
|
||||
} catch (e: Exception) {
|
||||
@ -65,7 +57,6 @@ class ATOMItemAdapter : XmlAdapter<Item> {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "id", "updated", "link", "author", "summary",
|
||||
"content", "group", "published")
|
||||
val names = Names.of("title", "id", "updated", "link", "author", "summary", "content")
|
||||
}
|
||||
}
|
@ -5,9 +5,7 @@ import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.*
|
||||
|
||||
class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
|
||||
|
||||
@ -29,9 +27,8 @@ class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
|
||||
0 -> name = reader.nextNonEmptyString()
|
||||
1 -> siteUrl = reader.nextNullableString()
|
||||
2 -> url = reader.nextNullableString()
|
||||
3 -> imageUrl = reader.nextNullableString()
|
||||
4 -> description = reader.nextNullableString()
|
||||
5 -> items += itemAdapter.fromJson(reader)
|
||||
3 -> description = reader.nextNullableString()
|
||||
4 -> items += itemAdapter.fromJson(reader)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
@ -45,6 +42,6 @@ class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
|
||||
|
||||
companion object {
|
||||
val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url",
|
||||
"feed_url", "icon", "description", "items")
|
||||
"feed_url", "description", "items")
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import java.time.LocalDateTime
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class JSONItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
|
||||
@ -33,7 +33,7 @@ class JSONItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
while (hasNext()) {
|
||||
with(item) {
|
||||
when (selectName(names)) {
|
||||
0 -> remoteId = nextNonEmptyString()
|
||||
0 -> guid = nextNonEmptyString()
|
||||
1 -> link = nextNonEmptyString()
|
||||
2 -> title = nextNonEmptyString()
|
||||
3 -> contentHtml = nextNullableString()
|
||||
|
@ -26,7 +26,6 @@ class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
when (tagName) {
|
||||
"channel" -> parseChannel(this, feed)
|
||||
"item" -> items += itemAdapter.fromXml(this)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,10 +39,8 @@ class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
}
|
||||
|
||||
private fun parseChannel(konsumer: Konsumer, feed: Feed) = with(konsumer) {
|
||||
feed.url = attributes.getValueOrNull(
|
||||
localName = "about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
)
|
||||
feed.url = attributes.getValueOrNull("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
@ -51,16 +48,12 @@ class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> siteUrl = nonNullText()
|
||||
"description" -> description = nullableText()
|
||||
"image" -> imageUrl = attributes.getValueOrNull(
|
||||
localName = "resource",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "description", "image")
|
||||
val names = Names.of("title", "link", "description")
|
||||
}
|
||||
}
|
@ -5,13 +5,13 @@ import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import java.time.LocalDateTime
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class RSS1ItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
@ -40,7 +40,7 @@ class RSS1ItemAdapter : XmlAdapter<Item> {
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.link == null) item.link = about
|
||||
?: throw ParseException("RSS1 link or about element is required")
|
||||
item.remoteId = item.link
|
||||
item.guid = item.link
|
||||
|
||||
if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull()
|
||||
.joinToString(limit = AUTHORS_MAX)
|
||||
|
@ -35,7 +35,6 @@ class RSS2FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
url = attributes.getValueOrNull("href")
|
||||
}
|
||||
"item" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
|
||||
"image" -> imageUrl = parseImage(this@allChildrenAutoIgnore)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
@ -50,20 +49,7 @@ class RSS2FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseImage(konsumer: Konsumer): String? = with(konsumer) {
|
||||
var url: String? = null
|
||||
|
||||
allChildrenAutoIgnore(Names.of("url")) {
|
||||
when (tagName) {
|
||||
"url" -> url = nullableText()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "link", "item", "image")
|
||||
val names = Names.of("title", "description", "link", "item")
|
||||
}
|
||||
}
|
@ -1,20 +1,15 @@
|
||||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.KonsumerException
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.readrops.api.localfeed.RSSMedia
|
||||
import com.gitlab.mvysny.konsumexml.*
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import java.time.LocalDateTime
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class RSS2ItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
@ -22,6 +17,7 @@ class RSS2ItemAdapter : XmlAdapter<Item> {
|
||||
val item = Item()
|
||||
|
||||
return try {
|
||||
//konsumer.checkCurrent("item")
|
||||
val creators = arrayListOf<String?>()
|
||||
|
||||
item.apply {
|
||||
@ -33,12 +29,12 @@ class RSS2ItemAdapter : XmlAdapter<Item> {
|
||||
"dc:creator" -> creators += nullableText()
|
||||
"pubDate" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"guid" -> remoteId = nullableText()
|
||||
"guid" -> guid = nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
"enclosure" -> RSSMedia.parseMediaContent(this, item = this@apply)
|
||||
"media:content" -> RSSMedia.parseMediaContent(this, item = this@apply)
|
||||
"media:group" -> RSSMedia.parseMediaGroup(this, item = this@apply)
|
||||
"enclosure" -> parseEnclosure(this, item = this@apply)
|
||||
"media:content" -> parseMediaContent(this, item = this@apply)
|
||||
"media:group" -> parseMediaGroup(this, item = this@apply)
|
||||
else -> skipContents() // for example media:description
|
||||
}
|
||||
}
|
||||
@ -51,11 +47,41 @@ class RSS2ItemAdapter : XmlAdapter<Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnclosure(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if (attributes.getValueOrNull("type") != null
|
||||
&& ApiUtils.isMimeImage(attributes["type"]) && item.imageLink == null)
|
||||
item.imageLink = attributes.getValueOrNull("url")
|
||||
}
|
||||
|
||||
private fun isMediumImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOrNull("medium") != null && ApiUtils.isMimeImage(attributes["medium"])
|
||||
}
|
||||
|
||||
private fun isTypeImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOrNull("type") != null && ApiUtils.isMimeImage(attributes["type"])
|
||||
}
|
||||
|
||||
private fun parseMediaContent(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null)
|
||||
item.imageLink = konsumer.attributes.getValueOrNull("url")
|
||||
|
||||
konsumer.skipContents() // ignore media content sub elements
|
||||
}
|
||||
|
||||
private fun parseMediaGroup(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
allChildrenAutoIgnore("content") {
|
||||
when (tagName) {
|
||||
"media:content" -> parseMediaContent(this, item)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalizeItem(item: Item, creators: List<String?>) = with(item) {
|
||||
validateItem(this)
|
||||
|
||||
if (pubDate == null) pubDate = LocalDateTime.now()
|
||||
if (remoteId == null) remoteId = link
|
||||
if (guid == null) guid = link
|
||||
if (author == null && creators.filterNotNull().isNotEmpty())
|
||||
author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
|
||||
}
|
||||
|
@ -1,31 +1,33 @@
|
||||
package com.readrops.api.opml
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import org.redundent.kotlin.xml.xml
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object OPMLParser {
|
||||
|
||||
suspend fun read(stream: InputStream): Map<Folder?, List<Feed>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val adapter = OPMLAdapter()
|
||||
val opml = adapter.fromXml(stream.konsumeXml())
|
||||
@JvmStatic
|
||||
fun read(stream: InputStream): Single<Map<Folder?, List<Feed>>> {
|
||||
return Single.create { emitter ->
|
||||
try {
|
||||
val adapter = OPMLAdapter()
|
||||
val opml = adapter.fromXml(stream.konsumeXml())
|
||||
|
||||
stream.close()
|
||||
opml
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
emitter.onSuccess(opml)
|
||||
} catch (e: Exception) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream) =
|
||||
withContext(Dispatchers.IO) {
|
||||
@JvmStatic
|
||||
fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val opml = xml("opml") {
|
||||
attribute("version", "2.0")
|
||||
|
||||
@ -65,6 +67,8 @@ object OPMLParser {
|
||||
|
||||
outputStream.write(opml.toString().toByteArray())
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,30 @@
|
||||
package com.readrops.api.services
|
||||
|
||||
import com.readrops.api.services.fever.FeverCredentials
|
||||
import com.readrops.api.services.greader.GReaderCredentials
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsCredentials
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsService
|
||||
import com.readrops.api.services.freshrss.FreshRSSCredentials
|
||||
import com.readrops.api.services.freshrss.FreshRSSService
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsCredentials
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsService
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
|
||||
abstract class Credentials(val authorization: String?, val url: String) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun toCredentials(account: Account): Credentials {
|
||||
val endPoint = getEndPoint(account.type!!)
|
||||
val endPoint = getEndPoint(account.accountType!!)
|
||||
|
||||
return when (account.type) {
|
||||
AccountType.NEXTCLOUD_NEWS -> NextcloudNewsCredentials(account.login, account.password, account.url + endPoint)
|
||||
AccountType.FRESHRSS, AccountType.GREADER -> GReaderCredentials(account.token, account.url + endPoint)
|
||||
AccountType.FEVER -> FeverCredentials(account.login, account.password, account.url + endPoint)
|
||||
return when (account.accountType) {
|
||||
AccountType.NEXTCLOUD_NEWS -> NextNewsCredentials(account.login, account.password, account.url + endPoint)
|
||||
AccountType.FRESHRSS -> FreshRSSCredentials(account.token, account.url + endPoint)
|
||||
else -> throw IllegalArgumentException("Unknown account type")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEndPoint(accountType: AccountType): String {
|
||||
return when (accountType) {
|
||||
AccountType.NEXTCLOUD_NEWS -> NextcloudNewsService.END_POINT
|
||||
AccountType.FRESHRSS -> "api/greader.php/"
|
||||
AccountType.FEVER, AccountType.GREADER -> ""
|
||||
AccountType.FRESHRSS -> FreshRSSService.END_POINT
|
||||
AccountType.NEXTCLOUD_NEWS -> NextNewsService.END_POINT
|
||||
else -> throw IllegalArgumentException("Unknown account type")
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package com.readrops.api.services
|
||||
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
data class DataSourceResult(
|
||||
var items: List<Item> = mutableListOf(),
|
||||
var starredItems: List<Item> = mutableListOf(),
|
||||
var feeds: List<Feed> = listOf(),
|
||||
var folders: List<Folder> = listOf(),
|
||||
var unreadIds: List<String> = listOf(),
|
||||
var readIds: List<String> = listOf(),
|
||||
var starredIds: List<String> = listOf(),
|
||||
)
|
15
api/src/main/java/com/readrops/api/services/SyncResult.kt
Normal file
15
api/src/main/java/com/readrops/api/services/SyncResult.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package com.readrops.api.services
|
||||
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
class SyncResult(var items: List<Item> = mutableListOf(),
|
||||
var starredItems: List<Item> = mutableListOf(),
|
||||
var feeds: List<Feed> = listOf(),
|
||||
var folders: List<Folder> = listOf(),
|
||||
var unreadIds: List<String>? = null,
|
||||
var readIds: List<String>? = null,
|
||||
var starredIds: List<String>? = null,
|
||||
var isError: Boolean = false
|
||||
)
|
@ -1,7 +0,0 @@
|
||||
package com.readrops.api.services.fever
|
||||
|
||||
import com.readrops.api.services.Credentials
|
||||
|
||||
class FeverCredentials(login: String?, password: String?, url: String) :
|
||||
Credentials(/*(login != null && password != null)
|
||||
.let { "api_key=" + ApiUtils.md5hash("$login:p$password") }*/null, url)
|
@ -1,127 +0,0 @@
|
||||
package com.readrops.api.services.fever
|
||||
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.services.fever.adapters.FeverAPIAdapter
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import okhttp3.MultipartBody
|
||||
|
||||
class FeverDataSource(private val service: FeverService) {
|
||||
|
||||
suspend fun login(login: String, password: String): Boolean {
|
||||
val response = service.login(getFeverRequestBody(login, password))
|
||||
|
||||
val adapter = Moshi.Builder()
|
||||
.add(Boolean::class.java, FeverAPIAdapter())
|
||||
.build()
|
||||
.adapter(Boolean::class.java)
|
||||
|
||||
return adapter.fromJson(response.source())!!
|
||||
}
|
||||
|
||||
suspend fun synchronize(
|
||||
login: String,
|
||||
password: String,
|
||||
syncType: SyncType,
|
||||
lastSinceId: String,
|
||||
): FeverSyncResult = with(CoroutineScope(Dispatchers.IO)) {
|
||||
val body = getFeverRequestBody(login, password)
|
||||
|
||||
if (syncType == SyncType.INITIAL_SYNC) {
|
||||
return FeverSyncResult().apply {
|
||||
awaitAll(
|
||||
async { feverFeeds = service.getFeeds(body) },
|
||||
async { folders = service.getFolders(body) },
|
||||
async {
|
||||
unreadIds = service.getUnreadItemsIds(body)
|
||||
.reversed()
|
||||
.take(MAX_ITEMS_IDS)
|
||||
|
||||
var maxId = unreadIds.maxOfOrNull { it }
|
||||
items = buildList {
|
||||
for (index in 0 until INITIAL_SYNC_ITEMS_REQUESTS_COUNT) {
|
||||
val newItems = service.getItems(body, maxId, null)
|
||||
|
||||
if (newItems.isEmpty()) break
|
||||
// always take the lowest id
|
||||
maxId = newItems.minOfOrNull { it.remoteId!!.toLong() }.toString()
|
||||
addAll(newItems)
|
||||
}
|
||||
}
|
||||
|
||||
sinceId = unreadIds.maxOfOrNull { it.toLong() } ?: 0
|
||||
},
|
||||
async { starredIds = service.getStarredItemsIds(body) },
|
||||
async { favicons = service.getFavicons(body) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return FeverSyncResult().apply {
|
||||
awaitAll(
|
||||
async { folders = service.getFolders(body) },
|
||||
async { feverFeeds = service.getFeeds(body) },
|
||||
async { unreadIds = service.getUnreadItemsIds(body) },
|
||||
async { starredIds = service.getStarredItemsIds(body) },
|
||||
async { favicons = service.getFavicons(body) },
|
||||
async {
|
||||
items = buildList {
|
||||
var localSinceId = lastSinceId
|
||||
|
||||
while (true) {
|
||||
val newItems = service.getItems(body, null, localSinceId)
|
||||
|
||||
if (newItems.isEmpty()) break
|
||||
// always take the highest id
|
||||
localSinceId =
|
||||
newItems.maxOfOrNull { it.remoteId!!.toLong() }.toString()
|
||||
addAll(newItems)
|
||||
}
|
||||
|
||||
sinceId = if (items.isNotEmpty()) {
|
||||
items.maxOfOrNull { it.remoteId!!.toLong() }!!
|
||||
} else {
|
||||
localSinceId.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setItemState(login: String, password: String, action: String, id: String) {
|
||||
val body = getFeverRequestBody(login, password)
|
||||
|
||||
service.updateItemState(body, action, id)
|
||||
}
|
||||
|
||||
private fun getFeverRequestBody(login: String, password: String): MultipartBody {
|
||||
val credentials = ApiUtils.md5hash("$login:$password")
|
||||
|
||||
return MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("api_key", credentials)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS_IDS = 1000
|
||||
private const val INITIAL_SYNC_ITEMS_REQUESTS_COUNT = 20 // (1000 items max)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ItemAction(val value: String) {
|
||||
sealed class ReadStateAction(value: String) : ItemAction(value) {
|
||||
data object ReadAction : ReadStateAction("read")
|
||||
data object UnreadAction : ReadStateAction("unread")
|
||||
}
|
||||
|
||||
sealed class StarStateAction(value: String) : ItemAction(value) {
|
||||
data object StarAction : StarStateAction("saved")
|
||||
data object UnstarAction : StarStateAction("unsaved")
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package com.readrops.api.services.fever
|
||||
|
||||
import com.readrops.api.services.fever.adapters.Favicon
|
||||
import com.readrops.api.services.fever.adapters.FeverFeeds
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface FeverService {
|
||||
|
||||
@POST("?api")
|
||||
suspend fun login(@Body body: MultipartBody): ResponseBody
|
||||
|
||||
@POST("?feeds")
|
||||
suspend fun getFeeds(@Body body: MultipartBody): FeverFeeds
|
||||
|
||||
@POST("?groups")
|
||||
suspend fun getFolders(@Body body: MultipartBody): List<Folder>
|
||||
|
||||
@POST("?favicons")
|
||||
suspend fun getFavicons(@Body body: MultipartBody): List<Favicon>
|
||||
|
||||
@POST("?items")
|
||||
suspend fun getItems(@Body body: MultipartBody, @Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?): List<Item>
|
||||
|
||||
@POST("?unread_item_ids")
|
||||
suspend fun getUnreadItemsIds(@Body body: MultipartBody): List<String>
|
||||
|
||||
@POST("?saved_item_ids")
|
||||
suspend fun getStarredItemsIds(@Body body: MultipartBody): List<String>
|
||||
|
||||
@POST("?mark=item")
|
||||
suspend fun updateItemState(@Body body: MultipartBody, @Query("as") action: String,
|
||||
@Query("id") id: String)
|
||||
|
||||
companion object {
|
||||
const val END_POINT = "api/fever.php/"
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package com.readrops.api.services.fever
|
||||
|
||||
import com.readrops.api.services.fever.adapters.Favicon
|
||||
import com.readrops.api.services.fever.adapters.FeverFeeds
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
data class FeverSyncResult(
|
||||
var feverFeeds: FeverFeeds = FeverFeeds(),
|
||||
var folders: List<Folder> = listOf(),
|
||||
var items: List<Item> = listOf(),
|
||||
var unreadIds: List<String> = listOf(),
|
||||
var starredIds: List<String> = listOf(),
|
||||
var favicons: List<Favicon> = listOf(),
|
||||
var sinceId: Long = 0,
|
||||
)
|
@ -1,37 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.toBoolean
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class FeverAPIAdapter : JsonAdapter<Boolean>() {
|
||||
|
||||
@ToJson
|
||||
override fun toJson(writer: JsonWriter, value: Boolean?) {
|
||||
// useless here
|
||||
}
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): Boolean = with(reader) {
|
||||
return try {
|
||||
beginObject()
|
||||
|
||||
var authenticated = false
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"auth" -> authenticated = nextInt().toBoolean()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
authenticated
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
data class Favicon(
|
||||
val id: Int,
|
||||
val data: ByteArray
|
||||
)
|
||||
|
||||
class FeverFaviconsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(favicons: List<Favicon>) = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Favicon> = with(reader) {
|
||||
return try {
|
||||
val favicons = arrayListOf<Favicon>()
|
||||
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"favicons" -> {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
parseFavicon(reader)?.let { favicons += it }
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
favicons
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun parseFavicon(reader: JsonReader): Favicon? = with(reader) {
|
||||
var id = 0
|
||||
var data: ByteArray? = null
|
||||
|
||||
while (hasNext()) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> id = nextInt()
|
||||
1 -> data = Base64.decode(nextString().substringAfter("base64,"))
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
if (id > 0 && data != null) {
|
||||
return Favicon(
|
||||
id = id,
|
||||
data = data,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "data")
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
data class FeverFeeds(
|
||||
val feeds: List<Feed> = listOf(),
|
||||
val favicons: Map<Int, String> = mapOf(), // <faviconId, feedRemoteId>
|
||||
val feedsGroups: Map<Int, List<Int>> = emptyMap()
|
||||
)
|
||||
|
||||
class FeverFeedsAdapter : JsonAdapter<FeverFeeds>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: FeverFeeds?) {
|
||||
// not useful here
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun fromJson(reader: JsonReader): FeverFeeds = with(reader) {
|
||||
return try {
|
||||
val feeds = arrayListOf<Feed>()
|
||||
val favicons = mutableMapOf<Int, String>()
|
||||
val feedsGroups = mutableMapOf<Int, List<Int>>()
|
||||
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"feeds" -> {
|
||||
beginArray()
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
feeds += parseFeed(reader, favicons)
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
"feeds_groups" -> {
|
||||
beginArray()
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
val (folderId, feedsIds) = parseFeedsGroups(reader)
|
||||
folderId?.let { feedsGroups[it] = feedsIds }
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
|
||||
FeverFeeds(
|
||||
feeds = feeds,
|
||||
favicons = favicons,
|
||||
feedsGroups = feedsGroups
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseFeed(reader: JsonReader, favicons: MutableMap<Int, String>): Feed = with(reader) {
|
||||
val feed = Feed()
|
||||
while (hasNext()) {
|
||||
with(feed) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> remoteId = nextInt().toString()
|
||||
1 -> favicons[nextInt()] = remoteId!!
|
||||
2 -> name = nextNonEmptyString()
|
||||
3 -> url = nextNonEmptyString()
|
||||
4 -> siteUrl = nextNullableString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
private fun parseFeedsGroups(reader: JsonReader): Pair<Int?, List<Int>> = with(reader) {
|
||||
var folderId: Int? = null
|
||||
val feedsIds = mutableListOf<Int>()
|
||||
|
||||
while (hasNext()) {
|
||||
when (selectName(JsonReader.Options.of("group_id", "feed_ids"))) {
|
||||
0 -> folderId = nextInt()
|
||||
1 -> feedsIds += nextNonEmptyString().split(",").map { it.toInt() }
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
folderId to feedsIds
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options =
|
||||
JsonReader.Options.of("id", "favicon_id", "title", "url", "site_url")
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class FeverFoldersAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(folders: List<Folder>) = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Folder> = with(reader) {
|
||||
return try {
|
||||
val folders = arrayListOf<Folder>()
|
||||
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"groups" -> {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
val folder = Folder()
|
||||
while (hasNext()) {
|
||||
with(folder) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> remoteId = nextInt().toString()
|
||||
1 -> name = nextNonEmptyString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
folders += folder
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
folders
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "title")
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.api.utils.extensions.toBoolean
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class FeverItemsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(items: List<Item>) = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Item> = with(reader) {
|
||||
return try {
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"items" -> {
|
||||
beginArray()
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
items += parseItem(reader)
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseItem(reader: JsonReader): Item = with(reader) {
|
||||
val item = Item()
|
||||
|
||||
while (hasNext()) {
|
||||
with(item) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> {
|
||||
remoteId = if (reader.peek() == JsonReader.Token.STRING) {
|
||||
nextNonEmptyString()
|
||||
} else {
|
||||
nextInt().toString()
|
||||
}
|
||||
}
|
||||
1 -> feedRemoteId = nextNonEmptyString()
|
||||
2 -> title = nextNonEmptyString()
|
||||
3 -> author = nextNullableString()
|
||||
4 -> content = nextNullableString()
|
||||
5 -> link = nextNullableString()
|
||||
6 -> isRead = nextInt().toBoolean()
|
||||
7 -> isStarred = nextInt().toBoolean()
|
||||
8 -> pubDate = DateUtils.fromEpochSeconds(nextLong())
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of(
|
||||
"id", "feed_id", "title", "author", "html", "url",
|
||||
"is_read", "is_saved", "created_on_time"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class FeverItemsIdsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(ids: List<String>) = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<String> = with(reader) {
|
||||
return try {
|
||||
beginObject()
|
||||
|
||||
val ids = arrayListOf<String>()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"unread_item_ids" -> ids.addAll(nextString().split(","))
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
ids
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package com.readrops.api.services.greader
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.services.Credentials
|
||||
|
||||
class GReaderCredentials(token: String?, url: String) :
|
||||
class FreshRSSCredentials(token: String?, url: String) :
|
||||
Credentials(token?.let { AUTH_PREFIX + it }, url) {
|
||||
|
||||
companion object {
|
@ -0,0 +1,316 @@
|
||||
package com.readrops.api.services.freshrss;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.services.SyncType;
|
||||
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class FreshRSSDataSource {
|
||||
|
||||
private static final int MAX_ITEMS = 2500;
|
||||
private static final int MAX_STARRED_ITEMS = 1000;
|
||||
|
||||
public static final String GOOGLE_READ = "user/-/state/com.google/read";
|
||||
public static final String GOOGLE_UNREAD = "user/-/state/com.google/unread";
|
||||
public static final String GOOGLE_STARRED = "user/-/state/com.google/starred";
|
||||
public static final String GOOGLE_READING_LIST = "user/-/state/com.google/reading-list";
|
||||
|
||||
private static final String FEED_PREFIX = "feed/";
|
||||
|
||||
private final FreshRSSService api;
|
||||
|
||||
public FreshRSSDataSource(FreshRSSService api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call token API to generate a new token from account credentials
|
||||
*
|
||||
* @param login login
|
||||
* @param password password
|
||||
* @return the generated token
|
||||
*/
|
||||
public Single<String> login(@NonNull String login, @NonNull String password) {
|
||||
RequestBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Email", login)
|
||||
.addFormDataPart("Passwd", password)
|
||||
.build();
|
||||
|
||||
return api.login(requestBody)
|
||||
.flatMap(response -> {
|
||||
Properties properties = new Properties();
|
||||
properties.load(new StringReader(response.string()));
|
||||
|
||||
return Single.just(properties.getProperty("Auth"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a write token to modify feeds, folders and items on the server
|
||||
*
|
||||
* @return the write token generated by the server
|
||||
*/
|
||||
public Single<String> getWriteToken() {
|
||||
return api.getWriteToken()
|
||||
.flatMap(responseBody -> Single.just(responseBody.string()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user information : name, email, id, profileId
|
||||
*
|
||||
* @return user information
|
||||
*/
|
||||
public Single<FreshRSSUserInfo> getUserInfo() {
|
||||
return api.getUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize feeds, folders, items and push read/unread items
|
||||
*
|
||||
* @param syncType INITIAL or CLASSIC
|
||||
* @param syncData data to sync (read/unread items ids, lastModified timestamp)
|
||||
* @param writeToken token for making modifications on the server
|
||||
* @return the result of the synchronization
|
||||
*/
|
||||
public Single<SyncResult> sync(@NonNull SyncType syncType, @NonNull FreshRSSSyncData syncData, @NonNull String writeToken) {
|
||||
if (syncType == SyncType.INITIAL_SYNC) {
|
||||
return Single.zip(setItemsReadState(syncData, writeToken).toSingleDefault(""),
|
||||
setItemsStarState(syncData, writeToken).toSingleDefault(""),
|
||||
getFolders(),
|
||||
getFeeds(),
|
||||
getItems(Arrays.asList(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null),
|
||||
getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS), // unread items ids
|
||||
getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS), // starred items ids
|
||||
getStarredItems(MAX_STARRED_ITEMS),
|
||||
(readState, starState, folders, feeds, items, unreadItemsIds, starredItemsIds, starredItems) ->
|
||||
new SyncResult(items, starredItems, feeds, folders, unreadItemsIds, Collections.emptyList(), starredItemsIds, false)
|
||||
);
|
||||
} else {
|
||||
return Single.zip(setItemsReadState(syncData, writeToken).toSingleDefault(""),
|
||||
setItemsStarState(syncData, writeToken).toSingleDefault(""),
|
||||
getFolders(),
|
||||
getFeeds(),
|
||||
getItems(null, MAX_ITEMS, syncData.getLastModified()),
|
||||
getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS), // unread items ids
|
||||
getItemsIds(GOOGLE_UNREAD, GOOGLE_READING_LIST, MAX_ITEMS), // read items ids
|
||||
getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS), // starred items ids
|
||||
(readState, starState, folders, feeds, items, unreadItemsIds, readItemsIds, starredItemsIds) ->
|
||||
new SyncResult(items, Collections.emptyList(), feeds, folders, unreadItemsIds, readItemsIds, starredItemsIds, false)
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the feeds folders
|
||||
*
|
||||
* @return the feeds folders
|
||||
*/
|
||||
public Single<List<Folder>> getFolders() {
|
||||
return api.getFolders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the feeds
|
||||
*
|
||||
* @return the feeds
|
||||
*/
|
||||
public Single<List<Feed>> getFeeds() {
|
||||
return api.getFeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the items
|
||||
*
|
||||
* @param excludeTargets type of items to exclude (read items and starred items)
|
||||
* @param max max number of items to fetch
|
||||
* @param lastModified fetch only items created after this timestamp
|
||||
* @return the items
|
||||
*/
|
||||
public Single<List<Item>> getItems(@Nullable List<String> excludeTargets, int max, @Nullable Long lastModified) {
|
||||
return api.getItems(excludeTargets, max, lastModified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch starred items
|
||||
*
|
||||
* @param max max number of items to fetch
|
||||
* @return items
|
||||
*/
|
||||
public Single<List<Item>> getStarredItems(int max) {
|
||||
return api.getStarredItems(max);
|
||||
}
|
||||
|
||||
public Single<List<String>> getItemsIds(String excludeTarget, String includeTarget, int max) {
|
||||
return api.getItemsIds(excludeTarget, includeTarget, max);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mark items read or unread
|
||||
*
|
||||
* @param read true for read, false for unread
|
||||
* @param itemIds items ids to mark
|
||||
* @param token token for modifications
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable setItemsReadState(boolean read, @NonNull List<String> itemIds, @NonNull String token) {
|
||||
if (read) {
|
||||
return api.setItemsState(token, GOOGLE_READ, null, itemIds);
|
||||
} else {
|
||||
return api.setItemsState(token, null, GOOGLE_READ, itemIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark items as starred or unstarred
|
||||
*
|
||||
* @param starred true for starred, false for unstarred
|
||||
* @param itemIds items ids to mark
|
||||
* @param token token for modifications
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable setItemsStarState(boolean starred, @NonNull List<String> itemIds, @NonNull String token) {
|
||||
if (starred) {
|
||||
return api.setItemsState(token, GOOGLE_STARRED, null, itemIds);
|
||||
} else {
|
||||
return api.setItemsState(token, null, GOOGLE_STARRED, itemIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new feed
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param feedUrl url of the feed to parse
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable createFeed(@NonNull String token, @NonNull String feedUrl) {
|
||||
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a feed
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param feedUrl url of the feed to delete
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable deleteFeed(@NonNull String token, @NonNull String feedUrl) {
|
||||
return api.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update feed title and folder
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param feedUrl url of the feed to update
|
||||
* @param title new title
|
||||
* @param folderId id of the new folder
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable updateFeed(@NonNull String token, @NonNull String feedUrl, @NonNull String title, @NonNull String folderId) {
|
||||
return api.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new folder
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param tagName name of the new folder
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable createFolder(@NonNull String token, @NonNull String tagName) {
|
||||
return api.createFolder(token, "user/-/label/" + tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update folder name
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param folderId id of the folder
|
||||
* @param name new folder name
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable updateFolder(@NonNull String token, @NonNull String folderId, @NonNull String name) {
|
||||
return api.updateFolder(token, folderId, "user/-/label/" + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a folder
|
||||
*
|
||||
* @param token token for modifications
|
||||
* @param folderId id of the folder to delete
|
||||
* @return Completable
|
||||
*/
|
||||
public Completable deleteFolder(@NonNull String token, @NonNull String folderId) {
|
||||
return api.deleteFolder(token, folderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set items star state
|
||||
*
|
||||
* @param syncData data containing items to mark
|
||||
* @param token token for modifications
|
||||
* @return A concatenation of two completable (read and unread completable)
|
||||
*/
|
||||
private Completable setItemsReadState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
|
||||
Completable readItemsCompletable;
|
||||
if (syncData.getReadItemsIds().isEmpty()) {
|
||||
readItemsCompletable = Completable.complete();
|
||||
} else {
|
||||
readItemsCompletable = setItemsReadState(true, syncData.getReadItemsIds(), token);
|
||||
}
|
||||
|
||||
Completable unreadItemsCompletable;
|
||||
if (syncData.getUnreadItemsIds().isEmpty()) {
|
||||
unreadItemsCompletable = Completable.complete();
|
||||
} else {
|
||||
unreadItemsCompletable = setItemsReadState(false, syncData.getUnreadItemsIds(), token);
|
||||
}
|
||||
|
||||
return readItemsCompletable.concatWith(unreadItemsCompletable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set items star state
|
||||
*
|
||||
* @param syncData data containing items to mark
|
||||
* @param token token for modifications
|
||||
* @return A concatenation of two completable (starred and unstarred completable)
|
||||
*/
|
||||
private Completable setItemsStarState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
|
||||
Completable starredItemsCompletable;
|
||||
if (syncData.getStarredItemsIds().isEmpty()) {
|
||||
starredItemsCompletable = Completable.complete();
|
||||
} else {
|
||||
starredItemsCompletable = setItemsStarState(true, syncData.getStarredItemsIds(), token);
|
||||
}
|
||||
|
||||
Completable unstarredItemsCompletable;
|
||||
if (syncData.getUnstarredItemsIds().isEmpty()) {
|
||||
unstarredItemsCompletable = Completable.complete();
|
||||
} else {
|
||||
unstarredItemsCompletable = setItemsStarState(false, syncData.getUnstarredItemsIds(), token);
|
||||
}
|
||||
|
||||
return starredItemsCompletable.concatWith(unstarredItemsCompletable);
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.*
|
||||
|
||||
interface FreshRSSService {
|
||||
|
||||
@POST("accounts/ClientLogin")
|
||||
fun login(@Body body: RequestBody?): Single<ResponseBody?>?
|
||||
|
||||
@get:GET("reader/api/0/token")
|
||||
val writeToken: Single<ResponseBody>
|
||||
|
||||
@get:GET("reader/api/0/user-info")
|
||||
val userInfo: Single<FreshRSSUserInfo>
|
||||
|
||||
@get:GET("reader/api/0/subscription/list?output=json")
|
||||
val feeds: Single<List<Feed>>
|
||||
|
||||
@get:GET("reader/api/0/tag/list?output=json")
|
||||
val folders: Single<List<Folder>>
|
||||
|
||||
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
|
||||
fun getItems(@Query("xt") excludeTarget: List<String>?, @Query("n") max: Int,
|
||||
@Query("ot") lastModified: Long?): Single<List<Item>>
|
||||
|
||||
@GET("reader/api/0/stream/contents/user/-/state/com.google/starred")
|
||||
fun getStarredItems(@Query("n") max: Int): Single<List<Item>>
|
||||
|
||||
@GET("reader/api/0/stream/items/ids")
|
||||
fun getItemsIds(@Query("xt") excludeTarget: String?, @Query("s") includeTarget: String?,
|
||||
@Query("n") max: Int): Single<List<String>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/edit-tag")
|
||||
fun setItemsState(@Field("T") token: String, @Field("a") addAction: String?,
|
||||
@Field("r") removeAction: String?, @Field("i") itemIds: List<String>): Completable
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/subscription/edit")
|
||||
fun createOrDeleteFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("ac") action: String): Completable
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/subscription/edit")
|
||||
fun updateFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("t") title: String,
|
||||
@Field("a") folderId: String, @Field("ac") action: String): Completable
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/edit-tag")
|
||||
fun createFolder(@Field("T") token: String, @Field("a") tagName: String): Completable
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/rename-tag")
|
||||
fun updateFolder(@Field("T") token: String, @Field("s") folderId: String, @Field("dest") newFolderId: String): Completable
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/disable-tag")
|
||||
fun deleteFolder(@Field("T") token: String, @Field("s") folderId: String): Completable
|
||||
|
||||
companion object {
|
||||
const val END_POINT = "/api/greader.php/"
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
data class FreshRSSSyncData(
|
||||
var lastModified: Long = 0,
|
||||
var readItemsIds: List<String> = listOf(),
|
||||
var unreadItemsIds: List<String> = listOf(),
|
||||
var starredItemsIds: List<String> = listOf(),
|
||||
var unstarredItemsIds: List<String> = listOf(),
|
||||
)
|
@ -0,0 +1,121 @@
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
|
||||
import com.readrops.db.entities.Item
|
||||
import okhttp3.MultipartBody
|
||||
import java.io.StringReader
|
||||
import java.util.Properties
|
||||
|
||||
class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
|
||||
|
||||
suspend fun login(login: String, password: String): String {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Email", login)
|
||||
.addFormDataPart("Passwd", password)
|
||||
.build()
|
||||
|
||||
val response = service.login(requestBody)
|
||||
|
||||
val properties = Properties()
|
||||
properties.load(StringReader(response.string()))
|
||||
|
||||
response.close()
|
||||
return properties.getProperty("Auth")
|
||||
}
|
||||
|
||||
suspend fun getWriteToken(): String = service.getWriteToken().string()
|
||||
|
||||
suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo()
|
||||
|
||||
suspend fun sync() {
|
||||
|
||||
}
|
||||
|
||||
suspend fun getFolders() = service.getFolders()
|
||||
|
||||
suspend fun getFeeds() = service.getFeeds()
|
||||
|
||||
suspend fun getItems(excludeTargets: List<String>, max: Int, lastModified: Long): List<Item> {
|
||||
return service.getItems(excludeTargets, max, lastModified)
|
||||
}
|
||||
|
||||
suspend fun getStarredItems(max: Int) = service.getStarredItems(max)
|
||||
|
||||
suspend fun getItemsIds(excludeTarget: String, includeTarget: String, max: Int): List<String> {
|
||||
return service.getItemsIds(excludeTarget, includeTarget, max)
|
||||
}
|
||||
|
||||
private suspend fun setItemsReadState(read: Boolean, itemIds: List<String>, token: String) {
|
||||
return if (read) {
|
||||
service.setItemsState(token, GOOGLE_READ, null, itemIds)
|
||||
} else {
|
||||
service.setItemsState(token, null, GOOGLE_READ, itemIds)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setItemStarState(starred: Boolean, itemIds: List<String>, token: String) {
|
||||
return if (starred) {
|
||||
service.setItemsState(token, GOOGLE_STARRED, null, itemIds)
|
||||
} else {
|
||||
service.setItemsState(token, null, GOOGLE_STARRED, itemIds)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createFeed(token: String, feedUrl: String) {
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
|
||||
}
|
||||
|
||||
suspend fun deleteFeed(token: String, feedUrl: String) {
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe")
|
||||
}
|
||||
|
||||
suspend fun updateFeed(token: String, feedUrl: String, title: String, folderId: String) {
|
||||
service.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit")
|
||||
}
|
||||
|
||||
suspend fun createFolder(token: String, tagName: String) {
|
||||
service.createFolder(token, "$FOLDER_PREFIX$tagName")
|
||||
}
|
||||
|
||||
suspend fun updateFolder(token: String, folderId: String, name: String) {
|
||||
service.updateFolder(token, folderId, "$FOLDER_PREFIX$name")
|
||||
}
|
||||
|
||||
suspend fun deleteFolder(token: String, folderId: String) {
|
||||
service.deleteFolder(token, folderId)
|
||||
}
|
||||
|
||||
suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) {
|
||||
if (syncData.readItemsIds.isNotEmpty()) {
|
||||
setItemsReadState(true, syncData.readItemsIds, token)
|
||||
}
|
||||
|
||||
if (syncData.unreadItemsIds.isNotEmpty()) {
|
||||
setItemsReadState(false, syncData.unreadItemsIds, token)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) {
|
||||
if (syncData.starredItemsIds.isNotEmpty()) {
|
||||
setItemStarState(true, syncData.starredItemsIds, token)
|
||||
}
|
||||
|
||||
if (syncData.unstarredItemsIds.isNotEmpty()) {
|
||||
setItemStarState(false, syncData.unstarredItemsIds, token)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 2500
|
||||
private const val MAX_STARRED_ITEMS = 1000
|
||||
|
||||
const val GOOGLE_READ = "user/-/state/com.google/read"
|
||||
const val GOOGLE_UNREAD = "user/-/state/com.google/unread"
|
||||
const val GOOGLE_STARRED = "user/-/state/com.google/starred"
|
||||
const val GOOGLE_READING_LIST = "user/-/state/com.google/reading-list"
|
||||
|
||||
const val FEED_PREFIX = "feed/"
|
||||
const val FOLDER_PREFIX = "user/-/label/"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.readrops.api.services.greader
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.services.greader.adapters.FreshRSSUserInfo
|
||||
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
@ -13,7 +13,7 @@ import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface GReaderService {
|
||||
interface NewFreshRSSService {
|
||||
|
||||
@POST("accounts/ClientLogin")
|
||||
suspend fun login(@Body body: RequestBody?): ResponseBody
|
||||
@ -31,49 +31,29 @@ interface GReaderService {
|
||||
suspend fun getFolders(): List<Folder>
|
||||
|
||||
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
|
||||
suspend fun getItems(
|
||||
@Query("xt") excludeTarget: List<String>?,
|
||||
@Query("n") max: Int,
|
||||
@Query("ot") lastModified: Long?
|
||||
): List<Item>
|
||||
suspend fun getItems(@Query("xt") excludeTarget: List<String>?, @Query("n") max: Int,
|
||||
@Query("ot") lastModified: Long?): List<Item>
|
||||
|
||||
@GET("reader/api/0/stream/contents/user/-/state/com.google/starred")
|
||||
suspend fun getStarredItems(@Query("n") max: Int): List<Item>
|
||||
|
||||
@GET("reader/api/0/stream/items/ids")
|
||||
suspend fun getItemsIds(
|
||||
@Query("xt") excludeTarget: String?,
|
||||
@Query("s") includeTarget: String?,
|
||||
@Query("n") max: Int
|
||||
): List<String>
|
||||
suspend fun getItemsIds(@Query("xt") excludeTarget: String?, @Query("s") includeTarget: String?,
|
||||
@Query("n") max: Int): List<String>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/edit-tag")
|
||||
suspend fun setItemsState(
|
||||
@Field("T") token: String,
|
||||
@Field("a") addAction: String?,
|
||||
@Field("r") removeAction: String?,
|
||||
@Field("i") itemIds: List<String>
|
||||
)
|
||||
suspend fun setItemsState(@Field("T") token: String, @Field("a") addAction: String?,
|
||||
@Field("r") removeAction: String?, @Field("i") itemIds: List<String>)
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/subscription/edit")
|
||||
suspend fun createOrDeleteFeed(
|
||||
@Field("T") token: String,
|
||||
@Field("s") feedUrl: String,
|
||||
@Field("ac") action: String,
|
||||
@Field("a") folderId: String?
|
||||
)
|
||||
suspend fun createOrDeleteFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("ac") action: String)
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/subscription/edit")
|
||||
suspend fun updateFeed(
|
||||
@Field("T") token: String,
|
||||
@Field("s") feedUrl: String,
|
||||
@Field("t") title: String,
|
||||
@Field("a") folderId: String,
|
||||
@Field("ac") action: String
|
||||
)
|
||||
suspend fun updateFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("t") title: String,
|
||||
@Field("a") folderId: String, @Field("ac") action: String)
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/edit-tag")
|
||||
@ -81,13 +61,13 @@ interface GReaderService {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/rename-tag")
|
||||
suspend fun updateFolder(
|
||||
@Field("T") token: String,
|
||||
@Field("s") folderId: String,
|
||||
@Field("dest") newFolderId: String
|
||||
)
|
||||
suspend fun updateFolder(@Field("T") token: String, @Field("s") folderId: String, @Field("dest") newFolderId: String)
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("reader/api/0/disable-tag")
|
||||
suspend fun deleteFolder(@Field("T") token: String, @Field("s") folderId: String)
|
||||
|
||||
companion object {
|
||||
const val END_POINT = "/api/greader.php/"
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class FreshRSSFeedsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(feeds: List<Feed>): String = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Feed> {
|
||||
val feeds = mutableListOf<Feed>()
|
||||
|
||||
return try {
|
||||
reader.beginObject()
|
||||
reader.nextName() // "subscriptions", beginning of the feed array
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
|
||||
val feed = Feed()
|
||||
while (reader.hasNext()) {
|
||||
with(feed) {
|
||||
when (reader.selectName(NAMES)) {
|
||||
0 -> name = reader.nextNonEmptyString()
|
||||
1 -> url = reader.nextNonEmptyString()
|
||||
2 -> siteUrl = reader.nextNullableString()
|
||||
3 -> iconUrl = reader.nextNullableString()
|
||||
4 -> remoteId = reader.nextNonEmptyString()
|
||||
5 -> remoteFolderId = getCategoryId(reader)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
feeds += feed
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
reader.endObject()
|
||||
|
||||
feeds
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCategoryId(reader: JsonReader): String? {
|
||||
var id: String? = null
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"id" -> id = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
if (!id.isNullOrEmpty())
|
||||
break
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
return id
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl",
|
||||
"iconUrl", "id", "categories")
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.util.*
|
||||
|
||||
class FreshRSSFoldersAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(folders: List<Folder>): String = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Folder> {
|
||||
val folders = mutableListOf<Folder>()
|
||||
|
||||
return try {
|
||||
reader.beginObject()
|
||||
reader.nextName() // "tags", beginning of folder array
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
|
||||
val folder = Folder()
|
||||
var type: String? = null
|
||||
|
||||
while (reader.hasNext()) {
|
||||
with(folder) {
|
||||
when (reader.selectName(NAMES)) {
|
||||
0 -> {
|
||||
val id = reader.nextNonEmptyString()
|
||||
name = StringTokenizer(id, "/")
|
||||
.toList()
|
||||
.last() as String
|
||||
remoteId = id
|
||||
}
|
||||
1 -> type = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "folder") // add only folders and avoid tags
|
||||
folders += folder
|
||||
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
reader.endObject()
|
||||
|
||||
folders
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "type")
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import android.util.TimingLogger
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_STARRED
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<Item>?) {
|
||||
// no need of this
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader): List<Item>? {
|
||||
val items = mutableListOf<Item>()
|
||||
|
||||
return try {
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue()
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseItems(reader: JsonReader, items: MutableList<Item>) {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
val item = Item()
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
with(item) {
|
||||
when (reader.selectName(NAMES)) {
|
||||
0 -> remoteId = reader.nextNonEmptyString()
|
||||
1 -> pubDate = LocalDateTime(reader.nextLong() * 1000L,
|
||||
DateTimeZone.getDefault())
|
||||
2 -> title = reader.nextNonEmptyString()
|
||||
3 -> content = getContent(reader)
|
||||
4 -> link = getLink(reader)
|
||||
5 -> getStates(reader, this)
|
||||
6 -> feedRemoteId = getRemoteFeedId(reader)
|
||||
7 -> author = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items += item
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
|
||||
private fun getContent(reader: JsonReader): String? {
|
||||
var content: String? = null
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"content" -> content = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
return content
|
||||
}
|
||||
|
||||
private fun getLink(reader: JsonReader): String? {
|
||||
var href: String? = null
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
|
||||
when (reader.nextName()) {
|
||||
"href" -> href = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
return href
|
||||
}
|
||||
|
||||
private fun getStates(reader: JsonReader, item: Item) {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextString()) {
|
||||
GOOGLE_READ -> item.isRead = true
|
||||
GOOGLE_STARRED -> item.isStarred = true
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
|
||||
private fun getRemoteFeedId(reader: JsonReader): String? {
|
||||
var remoteFeedId: String? = null
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"streamId" -> remoteFeedId = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
return remoteFeedId
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title",
|
||||
"summary", "alternate", "categories", "origin", "author")
|
||||
|
||||
val TAG: String = FreshRSSItemsAdapter::class.java.simpleName
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class FreshRSSItemsIdsAdapter : JsonAdapter<List<String>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<String>?) {
|
||||
// not useful here
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun fromJson(reader: JsonReader): List<String>? = with(reader) {
|
||||
val ids = arrayListOf<String>()
|
||||
|
||||
return try {
|
||||
beginObject()
|
||||
nextName()
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
when (nextName()) {
|
||||
"id" -> {
|
||||
val value = nextNonEmptyString()
|
||||
ids += "tag:google.com,2005:reader/item/${
|
||||
value.toLong()
|
||||
.toString(16).padStart(value.length, '0')
|
||||
}"
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
|
||||
// skip continuation
|
||||
if (hasNext()) {
|
||||
skipName()
|
||||
skipValue()
|
||||
}
|
||||
|
||||
endObject()
|
||||
|
||||
ids
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
@ -1,165 +0,0 @@
|
||||
package com.readrops.api.services.greader
|
||||
|
||||
import com.readrops.api.services.DataSourceResult
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.services.greader.adapters.FreshRSSUserInfo
|
||||
import com.readrops.db.entities.Item
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import okhttp3.MultipartBody
|
||||
import java.io.StringReader
|
||||
import java.util.Properties
|
||||
|
||||
class GReaderDataSource(private val service: GReaderService) {
|
||||
|
||||
suspend fun login(login: String, password: String): String {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Email", login)
|
||||
.addFormDataPart("Passwd", password)
|
||||
.build()
|
||||
|
||||
val response = service.login(requestBody)
|
||||
|
||||
val properties = Properties()
|
||||
properties.load(StringReader(response.string()))
|
||||
|
||||
response.close()
|
||||
return properties.getProperty("Auth")
|
||||
}
|
||||
|
||||
suspend fun getWriteToken(): String = service.getWriteToken().string()
|
||||
|
||||
suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo()
|
||||
|
||||
suspend fun synchronize(
|
||||
syncType: SyncType,
|
||||
syncData: GReaderSyncData,
|
||||
writeToken: String
|
||||
): DataSourceResult = with(CoroutineScope(Dispatchers.IO)) {
|
||||
return if (syncType == SyncType.INITIAL_SYNC) {
|
||||
DataSourceResult().apply {
|
||||
awaitAll(
|
||||
async { folders = getFolders() },
|
||||
async { feeds = getFeeds() },
|
||||
async {
|
||||
items = getItems(listOf(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null)
|
||||
},
|
||||
async { starredItems = getStarredItems(MAX_STARRED_ITEMS) },
|
||||
async { unreadIds = getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS) },
|
||||
async { starredIds = getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS) }
|
||||
)
|
||||
|
||||
}
|
||||
} else {
|
||||
DataSourceResult().apply {
|
||||
awaitAll(
|
||||
async { setItemsReadState(syncData, writeToken) },
|
||||
async { setItemsStarState(syncData, writeToken) },
|
||||
)
|
||||
|
||||
awaitAll(
|
||||
async { folders = getFolders() },
|
||||
async { feeds = getFeeds() },
|
||||
async { items = getItems(null, MAX_ITEMS, syncData.lastModified) },
|
||||
async { unreadIds = getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS) },
|
||||
async {
|
||||
readIds = getItemsIds(GOOGLE_UNREAD, GOOGLE_READING_LIST, MAX_ITEMS)
|
||||
},
|
||||
async { starredIds = getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun getFolders() = service.getFolders()
|
||||
|
||||
suspend fun getFeeds() = service.getFeeds()
|
||||
|
||||
suspend fun getItems(excludeTargets: List<String>?, max: Int, lastModified: Long?): List<Item> {
|
||||
return service.getItems(excludeTargets, max, lastModified)
|
||||
}
|
||||
|
||||
suspend fun getStarredItems(max: Int) = service.getStarredItems(max)
|
||||
|
||||
suspend fun getItemsIds(excludeTarget: String?, includeTarget: String, max: Int): List<String> {
|
||||
return service.getItemsIds(excludeTarget, includeTarget, max)
|
||||
}
|
||||
|
||||
private suspend fun setItemsReadState(read: Boolean, itemIds: List<String>, token: String) {
|
||||
return if (read) {
|
||||
service.setItemsState(token, GOOGLE_READ, null, itemIds)
|
||||
} else {
|
||||
service.setItemsState(token, null, GOOGLE_READ, itemIds)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setItemStarState(starred: Boolean, itemIds: List<String>, token: String) {
|
||||
return if (starred) {
|
||||
service.setItemsState(token, GOOGLE_STARRED, null, itemIds)
|
||||
} else {
|
||||
service.setItemsState(token, null, GOOGLE_STARRED, itemIds)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createFeed(token: String, feedUrl: String, folderId: String?) {
|
||||
// no feed here of the folder prefix for the folder id
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe", folderId)
|
||||
}
|
||||
|
||||
suspend fun deleteFeed(token: String, feedUrl: String) {
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe", null)
|
||||
}
|
||||
|
||||
suspend fun updateFeed(token: String, feedUrl: String, title: String, folderId: String) {
|
||||
service.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit")
|
||||
}
|
||||
|
||||
suspend fun createFolder(token: String, tagName: String) {
|
||||
service.createFolder(token, "$FOLDER_PREFIX$tagName")
|
||||
}
|
||||
|
||||
suspend fun updateFolder(token: String, folderId: String, name: String) {
|
||||
service.updateFolder(token, folderId, "$FOLDER_PREFIX$name")
|
||||
}
|
||||
|
||||
suspend fun deleteFolder(token: String, folderId: String) {
|
||||
service.deleteFolder(token, folderId)
|
||||
}
|
||||
|
||||
private suspend fun setItemsReadState(syncData: GReaderSyncData, token: String) {
|
||||
if (syncData.readIds.isNotEmpty()) {
|
||||
setItemsReadState(true, syncData.readIds, token)
|
||||
}
|
||||
|
||||
if (syncData.unreadIds.isNotEmpty()) {
|
||||
setItemsReadState(false, syncData.unreadIds, token)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setItemsStarState(syncData: GReaderSyncData, token: String) {
|
||||
if (syncData.starredIds.isNotEmpty()) {
|
||||
setItemStarState(true, syncData.starredIds, token)
|
||||
}
|
||||
|
||||
if (syncData.unstarredIds.isNotEmpty()) {
|
||||
setItemStarState(false, syncData.unstarredIds, token)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 2500
|
||||
private const val MAX_STARRED_ITEMS = 1000
|
||||
|
||||
const val GOOGLE_READ = "user/-/state/com.google/read"
|
||||
const val GOOGLE_UNREAD = "user/-/state/com.google/unread"
|
||||
const val GOOGLE_STARRED = "user/-/state/com.google/starred"
|
||||
const val GOOGLE_READING_LIST = "user/-/state/com.google/reading-list"
|
||||
|
||||
const val FEED_PREFIX = "feed/"
|
||||
const val FOLDER_PREFIX = "user/-/label/"
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.readrops.api.services.greader
|
||||
|
||||
data class GReaderSyncData(
|
||||
var lastModified: Long = 0,
|
||||
var readIds: List<String> = listOf(),
|
||||
var unreadIds: List<String> = listOf(),
|
||||
var starredIds: List<String> = listOf(),
|
||||
var unstarredIds: List<String> = listOf(),
|
||||
)
|
@ -1,99 +0,0 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class GReaderFeedsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(feeds: List<Feed>): String = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Feed> = with(reader) {
|
||||
val feeds = mutableListOf<Feed>()
|
||||
|
||||
return try {
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"subscriptions" -> {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
feeds += parseFeed(reader)
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
feeds
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseFeed(reader: JsonReader): Feed = with(reader) {
|
||||
val feed = Feed()
|
||||
|
||||
while (hasNext()) {
|
||||
with(feed) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> name = nextNonEmptyString()
|
||||
1 -> url = nextNonEmptyString()
|
||||
2 -> siteUrl = nextNullableString()
|
||||
3 -> iconUrl = nextNullableString()
|
||||
4 -> remoteId = nextNonEmptyString()
|
||||
5 -> remoteFolderId = getCategoryId(reader)
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
private fun getCategoryId(reader: JsonReader): String? = with(reader) {
|
||||
var id: String? = null
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"id" -> id = nextNullableString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
if (!id.isNullOrEmpty())
|
||||
break
|
||||
}
|
||||
|
||||
endArray()
|
||||
return id
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of(
|
||||
"title", "url", "htmlUrl",
|
||||
"iconUrl", "id", "categories"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.util.StringTokenizer
|
||||
|
||||
class GReaderFoldersAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(folders: List<Folder>): String = ""
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Folder> = with(reader) {
|
||||
val folders = mutableListOf<Folder>()
|
||||
|
||||
return try {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"tags" -> {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
parseFolder(reader)?.let { folders += it }
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
folders
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseFolder(reader: JsonReader): Folder? = with(reader) {
|
||||
val folder = Folder()
|
||||
var type: String? = null
|
||||
|
||||
while (hasNext()) {
|
||||
with(folder) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> {
|
||||
val id = nextNonEmptyString()
|
||||
name = StringTokenizer(id, "/")
|
||||
.toList()
|
||||
.last() as String
|
||||
remoteId = id
|
||||
}
|
||||
|
||||
1 -> type = nextString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add only folders and avoid tags
|
||||
if (type == "folder") {
|
||||
folder
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "type")
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
|
||||
import com.readrops.api.services.greader.GReaderDataSource.Companion.GOOGLE_READ
|
||||
import com.readrops.api.services.greader.GReaderDataSource.Companion.GOOGLE_STARRED
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class GReaderItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<Item>?) {
|
||||
// no need of this
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader): List<Item> = with(reader) {
|
||||
val items = mutableListOf<Item>()
|
||||
|
||||
return try {
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"items" -> parseItems(reader, items)
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseItems(reader: JsonReader, items: MutableList<Item>) = with(reader) {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
val item = Item()
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
with(item) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> remoteId = nextNonEmptyString()
|
||||
1 -> pubDate = DateUtils.fromEpochSeconds(nextLong())
|
||||
2 -> title = nextNonEmptyString()
|
||||
3 -> content = getContent(reader)
|
||||
4 -> link = getLink(reader)
|
||||
5 -> getStates(reader, this)
|
||||
6 -> feedRemoteId = getRemoteFeedId(reader)
|
||||
7 -> author = nextNullableString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items += item
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
|
||||
private fun getContent(reader: JsonReader): String? = with(reader) {
|
||||
var content: String? = null
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"content" -> content = nextNullableString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
return content
|
||||
}
|
||||
|
||||
private fun getLink(reader: JsonReader): String? = with(reader) {
|
||||
var href: String? = null
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"href" -> href = nextString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
return href
|
||||
}
|
||||
|
||||
private fun getStates(reader: JsonReader, item: Item) = with(reader) {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextString()) {
|
||||
GOOGLE_READ -> item.isRead = true
|
||||
GOOGLE_STARRED -> item.isStarred = true
|
||||
}
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
|
||||
private fun getRemoteFeedId(reader: JsonReader): String? = with(reader) {
|
||||
var remoteFeedId: String? = null
|
||||
beginObject()
|
||||
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"streamId" -> remoteFeedId = nextString()
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
return remoteFeedId
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of(
|
||||
"id", "published", "title", "summary", "alternate", "categories", "origin", "author"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
|
||||
class GReaderItemsIdsAdapter : JsonAdapter<List<String>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<String>?) {
|
||||
// not useful here
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun fromJson(reader: JsonReader): List<String>? = with(reader) {
|
||||
val ids = arrayListOf<String>()
|
||||
|
||||
return try {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"itemRefs" -> {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
|
||||
when (nextName()) {
|
||||
"id" -> {
|
||||
val value = nextNonEmptyString()
|
||||
ids += "tag:google.com,2005:reader/item/" +
|
||||
value.toLong()
|
||||
.toString(16).padStart(value.length, '0')
|
||||
}
|
||||
|
||||
else -> skipValue()
|
||||
}
|
||||
|
||||
endObject()
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
endObject()
|
||||
ids
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.api.services.Credentials
|
||||
|
||||
class NextNewsCredentials(login: String?, password: String?, url: String):
|
||||
Credentials((login != null && password != null).let {
|
||||
okhttp3.Credentials.basic(login!!, password!!)
|
||||
}, url)
|
@ -0,0 +1,300 @@
|
||||
package com.readrops.api.services.nextcloudnews;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.services.SyncType;
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter;
|
||||
import com.readrops.api.utils.ApiUtils;
|
||||
import com.readrops.api.utils.exceptions.ConflictException;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.api.utils.extensions.KonsumerExtensionsKt;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.StarItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class NextNewsDataSource {
|
||||
|
||||
private static final String TAG = NextNewsDataSource.class.getSimpleName();
|
||||
|
||||
private static final int MAX_ITEMS = 5000;
|
||||
private static final int MAX_STARRED_ITEMS = 1000;
|
||||
|
||||
private NextNewsService api;
|
||||
|
||||
public NextNewsDataSource(NextNewsService api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String login(OkHttpClient client, Account account) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(account.getUrl() + "/ocs/v1.php/cloud/users/" + account.getLogin())
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.build();
|
||||
|
||||
okhttp3.Response response = client.newCall(request).execute();
|
||||
|
||||
String displayName = new NextNewsUserAdapter().fromXml(KonsumerExtensionsKt
|
||||
.instantiateKonsumer(response.body().byteStream()));
|
||||
response.body().close();
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Feed> createFeed(String url, int folderId) throws IOException, UnknownFormatException {
|
||||
Response<List<Feed>> response = api.createFeed(url, folderId).execute();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
if (response.code() == ApiUtils.HTTP_UNPROCESSABLE)
|
||||
throw new UnknownFormatException();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public SyncResult sync(@NonNull SyncType syncType, @Nullable NextNewsSyncData data) throws IOException {
|
||||
SyncResult syncResult = new SyncResult();
|
||||
switch (syncType) {
|
||||
case INITIAL_SYNC:
|
||||
initialSync(syncResult);
|
||||
break;
|
||||
case CLASSIC_SYNC:
|
||||
if (data == null)
|
||||
throw new NullPointerException("NextNewsSyncData can't be null");
|
||||
|
||||
classicSync(syncResult, data);
|
||||
break;
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
private void initialSync(SyncResult syncResult) throws IOException {
|
||||
getFeedsAndFolders(syncResult);
|
||||
|
||||
// unread items
|
||||
Response<List<Item>> itemsResponse = api.getItems(ItemQueryType.ALL.value, false, MAX_ITEMS).execute();
|
||||
List<Item> itemList = itemsResponse.body();
|
||||
|
||||
if (!itemsResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
|
||||
if (itemList != null)
|
||||
syncResult.setItems(itemList);
|
||||
|
||||
// starred items
|
||||
Response<List<Item>> starredItemsResponse = api.getItems(ItemQueryType.STARRED.value, true, MAX_STARRED_ITEMS).execute();
|
||||
List<Item> starredItems = starredItemsResponse.body();
|
||||
|
||||
if (!itemsResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
|
||||
if (itemList != null)
|
||||
syncResult.setStarredItems(starredItems);
|
||||
}
|
||||
|
||||
private void classicSync(SyncResult syncResult, NextNewsSyncData data) throws IOException {
|
||||
putModifiedItems(data, syncResult);
|
||||
getFeedsAndFolders(syncResult);
|
||||
|
||||
Response<List<Item>> itemsResponse = api.getNewItems(data.getLastModified(), ItemQueryType.ALL.value).execute();
|
||||
List<Item> itemList = itemsResponse.body();
|
||||
|
||||
if (!itemsResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
|
||||
if (itemList != null)
|
||||
syncResult.setItems(itemList);
|
||||
}
|
||||
|
||||
private void getFeedsAndFolders(SyncResult syncResult) throws IOException {
|
||||
Response<List<Feed>> feedResponse = api.getFeeds().execute();
|
||||
List<Feed> feedList = feedResponse.body();
|
||||
|
||||
if (!feedResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
|
||||
Response<List<Folder>> folderResponse = api.getFolders().execute();
|
||||
List<Folder> folderList = folderResponse.body();
|
||||
|
||||
if (!folderResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
|
||||
if (folderList != null)
|
||||
syncResult.setFolders(folderList);
|
||||
|
||||
if (feedList != null)
|
||||
syncResult.setFeeds(feedList);
|
||||
|
||||
}
|
||||
|
||||
private void putModifiedItems(NextNewsSyncData data, SyncResult syncResult) throws IOException {
|
||||
setReadState(data.getReadItems(), syncResult, StateType.READ);
|
||||
setReadState(data.getUnreadItems(), syncResult, StateType.UNREAD);
|
||||
|
||||
setStarState(data.getStarredItems(), syncResult, StateType.STAR);
|
||||
setStarState(data.getUnstarredItems(), syncResult, StateType.UNSTAR);
|
||||
}
|
||||
|
||||
public List<Folder> createFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException {
|
||||
Map<String, String> folderNameMap = new HashMap<>();
|
||||
folderNameMap.put("name", folder.getName());
|
||||
|
||||
Response<List<Folder>> foldersResponse = api.createFolder(folderNameMap).execute();
|
||||
|
||||
if (foldersResponse.isSuccessful())
|
||||
return foldersResponse.body();
|
||||
else if (foldersResponse.code() == ApiUtils.HTTP_UNPROCESSABLE)
|
||||
throw new UnknownFormatException();
|
||||
else if (foldersResponse.code() == ApiUtils.HTTP_CONFLICT)
|
||||
throw new ConflictException();
|
||||
else
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
public boolean deleteFolder(Folder folder) throws IOException {
|
||||
Response response = api.deleteFolder(Integer.parseInt(folder.getRemoteId())).execute();
|
||||
|
||||
if (response.isSuccessful())
|
||||
return true;
|
||||
else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
|
||||
throw new Resources.NotFoundException();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean renameFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException {
|
||||
Map<String, String> folderNameMap = new HashMap<>();
|
||||
folderNameMap.put("name", folder.getName());
|
||||
|
||||
Response response = api.renameFolder(Integer.parseInt(folder.getRemoteId()), folderNameMap).execute();
|
||||
|
||||
if (response.isSuccessful())
|
||||
return true;
|
||||
else {
|
||||
switch (response.code()) {
|
||||
case ApiUtils.HTTP_NOT_FOUND:
|
||||
throw new Resources.NotFoundException();
|
||||
case ApiUtils.HTTP_UNPROCESSABLE:
|
||||
throw new UnknownFormatException();
|
||||
case ApiUtils.HTTP_CONFLICT:
|
||||
throw new ConflictException();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deleteFeed(int feedId) throws IOException {
|
||||
Response response = api.deleteFeed(feedId).execute();
|
||||
|
||||
if (response.isSuccessful())
|
||||
return true;
|
||||
else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
|
||||
throw new Resources.NotFoundException();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean changeFeedFolder(Feed feed) throws IOException {
|
||||
Map<String, Integer> folderIdMap = new HashMap<>();
|
||||
folderIdMap.put("folderId", Integer.parseInt(feed.getRemoteFolderId()));
|
||||
|
||||
Response response = api.changeFeedFolder(Integer.parseInt(feed.getRemoteId()), folderIdMap).execute();
|
||||
|
||||
if (response.isSuccessful())
|
||||
return true;
|
||||
else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
|
||||
throw new Resources.NotFoundException();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean renameFeed(Feed feed) throws IOException {
|
||||
Map<String, String> feedTitleMap = new HashMap<>();
|
||||
feedTitleMap.put("feedTitle", feed.getName());
|
||||
|
||||
Response response = api.renameFeed(Integer.parseInt(feed.getRemoteId()), feedTitleMap).execute();
|
||||
|
||||
if (response.isSuccessful())
|
||||
return true;
|
||||
else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
|
||||
throw new Resources.NotFoundException();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setReadState(List<String> items, SyncResult syncResult, StateType stateType) throws IOException {
|
||||
if (!items.isEmpty()) {
|
||||
Map<String, List<String>> itemIdsMap = new HashMap<>();
|
||||
itemIdsMap.put("items", items);
|
||||
|
||||
Response readItemsResponse = api.setReadState(stateType.name().toLowerCase(),
|
||||
itemIdsMap).execute();
|
||||
|
||||
if (!readItemsResponse.isSuccessful())
|
||||
syncResult.setError(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setStarState(List<StarItem> items, SyncResult syncResult, StateType stateType) throws IOException {
|
||||
if (!items.isEmpty()) {
|
||||
List<Map<String, String>> body = new ArrayList<>();
|
||||
for (StarItem item : items) {
|
||||
Map<String, String> itemBody = new HashMap<>();
|
||||
itemBody.put("feedId", item.getFeedRemoteId());
|
||||
itemBody.put("guidHash", item.getGuidHash());
|
||||
|
||||
body.add(itemBody);
|
||||
}
|
||||
|
||||
Response response = api.setStarState(stateType.name().toLowerCase(),
|
||||
Collections.singletonMap("items", body)).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
syncResult.setError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StateType {
|
||||
READ,
|
||||
UNREAD,
|
||||
STAR,
|
||||
UNSTAR
|
||||
}
|
||||
|
||||
public enum ItemQueryType {
|
||||
ALL(3),
|
||||
STARRED(2);
|
||||
|
||||
private int value;
|
||||
|
||||
ItemQueryType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
||||
interface NextNewsService {
|
||||
|
||||
@GET("/ocs/v1.php/cloud/users/{userId}")
|
||||
@Headers("OCS-APIRequest: true")
|
||||
fun getUser(@Path("userId") userId: String): Call<ResponseBody>
|
||||
|
||||
@get:GET("folders")
|
||||
val folders: Call<List<Folder>>
|
||||
|
||||
@get:GET("feeds")
|
||||
val feeds: Call<List<Feed>>
|
||||
|
||||
@GET("items")
|
||||
fun getItems(@Query("type") type: Int, @Query("getRead") read: Boolean, @Query("batchSize") batchSize: Int): Call<List<Item>>
|
||||
|
||||
@GET("items/updated")
|
||||
fun getNewItems(@Query("lastModified") lastModified: Long, @Query("type") type: Int): Call<List<Item>>
|
||||
|
||||
@PUT("items/{stateType}/multiple")
|
||||
fun setReadState(@Path("stateType") stateType: String, @Body itemIdsMap: Map<String, List<String>>): Call<ResponseBody>
|
||||
|
||||
@PUT("items/{starType}/multiple")
|
||||
fun setStarState(@Path("starType") starType: String?, @Body body: Map<String?, List<Map<String, String>>>): Call<ResponseBody>
|
||||
|
||||
@POST("feeds")
|
||||
fun createFeed(@Query("url") url: String, @Query("folderId") folderId: Int): Call<List<Feed>>
|
||||
|
||||
@DELETE("feeds/{feedId}")
|
||||
fun deleteFeed(@Path("feedId") feedId: Int): Call<Void?>?
|
||||
|
||||
@PUT("feeds/{feedId}/move")
|
||||
fun changeFeedFolder(@Path("feedId") feedId: Int, @Body folderIdMap: Map<String, Int>): Call<ResponseBody>
|
||||
|
||||
@PUT("feeds/{feedId}/rename")
|
||||
fun renameFeed(@Path("feedId") feedId: Int, @Body feedTitleMap: Map<String, String>): Call<ResponseBody>
|
||||
|
||||
@POST("folders")
|
||||
fun createFolder(@Body folderName: Map<String, String>): Call<List<Folder>>
|
||||
|
||||
@DELETE("folders/{folderId}")
|
||||
fun deleteFolder(@Path("folderId") folderId: Int): Call<ResponseBody>
|
||||
|
||||
@PUT("folders/{folderId}")
|
||||
fun renameFolder(@Path("folderId") folderId: Int, @Body folderName: Map<String, String>): Call<ResponseBody>
|
||||
|
||||
companion object {
|
||||
const val END_POINT = "/index.php/apps/news/api/v1-2/"
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.db.pojo.StarItem
|
||||
|
||||
data class NextNewsSyncData(
|
||||
var lastModified: Long = 0,
|
||||
var unreadItems: List<String> = listOf(),
|
||||
var readItems: List<String> = listOf(),
|
||||
var starredItems: List<StarItem> = listOf(),
|
||||
var unstarredItems: List<StarItem> = listOf(),
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.api.services.Credentials
|
||||
|
||||
class NextcloudNewsCredentials(login: String?, password: String?, url: String):
|
||||
Credentials(if (login != null && password != null) {
|
||||
okhttp3.Credentials.basic(login, password)
|
||||
} else null, url)
|
@ -1,153 +0,0 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.services.DataSourceResult
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextcloudNewsUserAdapter
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
class NextcloudNewsDataSource(private val service: NextcloudNewsService) {
|
||||
|
||||
suspend fun login(client: OkHttpClient, account: Account): String {
|
||||
val request = Request.Builder()
|
||||
.url(account.url + "/ocs/v1.php/cloud/users/" + account.login)
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request)
|
||||
.execute()
|
||||
|
||||
val displayName = NextcloudNewsUserAdapter().fromXml(response.body!!.byteStream().konsumeXml())
|
||||
response.close()
|
||||
|
||||
return displayName
|
||||
}
|
||||
|
||||
suspend fun synchronize(syncType: SyncType, syncData: NextcloudNewsSyncData): DataSourceResult =
|
||||
with(CoroutineScope(Dispatchers.IO)) {
|
||||
return if (syncType == SyncType.INITIAL_SYNC) {
|
||||
DataSourceResult().apply {
|
||||
awaitAll(
|
||||
async { folders = getFolders() },
|
||||
async { feeds = getFeeds() },
|
||||
async { items = getItems(ItemQueryType.ALL.value, false, MAX_ITEMS) },
|
||||
async {
|
||||
starredItems =
|
||||
getItems(ItemQueryType.STARRED.value, true, MAX_STARRED_ITEMS)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
awaitAll(
|
||||
async { setItemsReadState(syncData) },
|
||||
async { setItemsStarState(syncData) },
|
||||
)
|
||||
|
||||
DataSourceResult().apply {
|
||||
awaitAll(
|
||||
async { folders = getFolders() },
|
||||
async { feeds = getFeeds() },
|
||||
async { items = getNewItems(syncData.lastModified, ItemQueryType.ALL) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFolders() = service.getFolders()
|
||||
|
||||
suspend fun getFeeds() = service.getFeeds()
|
||||
|
||||
suspend fun getItems(type: Int, read: Boolean, batchSize: Int): List<Item> {
|
||||
return service.getItems(type, read, batchSize)
|
||||
}
|
||||
|
||||
suspend fun getNewItems(lastModified: Long, itemQueryType: ItemQueryType): List<Item> {
|
||||
return service.getNewItems(lastModified, itemQueryType.value)
|
||||
}
|
||||
|
||||
suspend fun createFeed(url: String, folderId: Int?): List<Feed> {
|
||||
return service.createFeed(url, folderId)
|
||||
}
|
||||
|
||||
suspend fun changeFeedFolder(newFolderId: Int?, feedId: Int) {
|
||||
service.changeFeedFolder(feedId, mapOf("folderId" to newFolderId))
|
||||
}
|
||||
|
||||
suspend fun renameFeed(name: String, feedId: Int) {
|
||||
service.renameFeed(feedId, mapOf("feedTitle" to name))
|
||||
}
|
||||
|
||||
suspend fun deleteFeed(feedId: Int) {
|
||||
service.deleteFeed(feedId)
|
||||
}
|
||||
|
||||
suspend fun createFolder(name: String): List<Folder> {
|
||||
return service.createFolder(mapOf("name" to name))
|
||||
}
|
||||
|
||||
suspend fun renameFolder(name: String, folderId: Int) {
|
||||
service.renameFolder(folderId, mapOf("name" to name))
|
||||
}
|
||||
|
||||
suspend fun deleteFolder(folderId: Int) {
|
||||
service.deleteFolder(folderId)
|
||||
}
|
||||
|
||||
suspend fun setItemsReadState(syncData: NextcloudNewsSyncData) = with(syncData) {
|
||||
if (unreadIds.isNotEmpty()) {
|
||||
service.setReadState(
|
||||
StateType.UNREAD.name.lowercase(),
|
||||
mapOf("itemIds" to unreadIds)
|
||||
)
|
||||
}
|
||||
|
||||
if (readIds.isNotEmpty()) {
|
||||
service.setReadState(
|
||||
StateType.READ.name.lowercase(),
|
||||
mapOf("itemIds" to readIds)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setItemsStarState(syncData: NextcloudNewsSyncData) = with(syncData) {
|
||||
if (starredIds.isNotEmpty()) {
|
||||
service.setStarState(
|
||||
StateType.STAR.name.lowercase(),
|
||||
mapOf("itemIds" to starredIds)
|
||||
)
|
||||
}
|
||||
|
||||
if (unstarredIds.isNotEmpty()) {
|
||||
service.setStarState(
|
||||
StateType.UNSTAR.name.lowercase(),
|
||||
mapOf("itemIds" to unstarredIds)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ItemQueryType(val value: Int) {
|
||||
ALL(3),
|
||||
STARRED(2)
|
||||
}
|
||||
|
||||
enum class StateType {
|
||||
READ,
|
||||
UNREAD,
|
||||
STAR,
|
||||
UNSTAR
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 5000
|
||||
private const val MAX_STARRED_ITEMS = 1000
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.Item
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface NextcloudNewsService {
|
||||
|
||||
@GET("folders")
|
||||
suspend fun getFolders(): List<Folder>
|
||||
|
||||
@GET("feeds")
|
||||
suspend fun getFeeds(): List<Feed>
|
||||
|
||||
@GET("items")
|
||||
suspend fun getItems(
|
||||
@Query("type") type: Int,
|
||||
@Query("getRead") read: Boolean,
|
||||
@Query("batchSize") batchSize: Int
|
||||
): List<Item>
|
||||
|
||||
@GET("items/updated")
|
||||
suspend fun getNewItems(
|
||||
@Query("lastModified") lastModified: Long,
|
||||
@Query("type") type: Int
|
||||
): List<Item>
|
||||
|
||||
@POST("items/{stateType}/multiple")
|
||||
@JvmSuppressWildcards
|
||||
suspend fun setReadState(
|
||||
@Path("stateType") stateType: String,
|
||||
@Body itemIdsMap: Map<String, List<Int>>
|
||||
)
|
||||
|
||||
@POST("items/{starType}/multiple")
|
||||
@JvmSuppressWildcards
|
||||
suspend fun setStarState(
|
||||
@Path("starType") starType: String?,
|
||||
@Body body: Map<String, List<Int>>
|
||||
)
|
||||
|
||||
@POST("feeds")
|
||||
suspend fun createFeed(@Query("url") url: String, @Query("folderId") folderId: Int?): List<Feed>
|
||||
|
||||
@DELETE("feeds/{feedId}")
|
||||
suspend fun deleteFeed(@Path("feedId") feedId: Int)
|
||||
|
||||
@POST("feeds/{feedId}/move")
|
||||
suspend fun changeFeedFolder(@Path("feedId") feedId: Int, @Body folderIdMap: Map<String, Int?>)
|
||||
|
||||
@POST("feeds/{feedId}/rename")
|
||||
suspend fun renameFeed(@Path("feedId") feedId: Int, @Body feedTitleMap: Map<String, String>)
|
||||
|
||||
@POST("folders")
|
||||
suspend fun createFolder(@Body folderName: Map<String, String>): List<Folder>
|
||||
|
||||
@DELETE("folders/{folderId}")
|
||||
suspend fun deleteFolder(@Path("folderId") folderId: Int)
|
||||
|
||||
@PUT("folders/{folderId}")
|
||||
suspend fun renameFolder(@Path("folderId") folderId: Int, @Body folderName: Map<String, String>)
|
||||
|
||||
companion object {
|
||||
const val END_POINT = "index.php/apps/news/api/v1-3/"
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
data class NextcloudNewsSyncData(
|
||||
val lastModified: Long = 0,
|
||||
val readIds: List<Int> = listOf(),
|
||||
val unreadIds: List<Int> = listOf(),
|
||||
val starredIds: List<Int> = listOf(),
|
||||
val unstarredIds: List<Int> = listOf(),
|
||||
)
|
@ -11,7 +11,7 @@ import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.net.URI
|
||||
|
||||
class NextcloudNewsFeedsAdapter {
|
||||
class NextNewsFeedsAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(feeds: List<Feed>): String = ""
|
@ -8,7 +8,7 @@ import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class NextcloudNewsFoldersAdapter {
|
||||
class NextNewsFoldersAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(folders: List<Folder>): String = ""
|
@ -1,18 +1,18 @@
|
||||
package com.readrops.api.services.nextcloudnews.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nextNullableLong
|
||||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import java.time.LocalDateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class NextcloudNewsItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
class NextNewsItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<Item>?) {
|
||||
// no need of this
|
||||
@ -40,37 +40,25 @@ class NextcloudNewsItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
when (reader.selectName(NAMES)) {
|
||||
0 -> remoteId = reader.nextInt().toString()
|
||||
1 -> link = reader.nextNullableString()
|
||||
2 -> title = reader.nextNullableString()
|
||||
2 -> title = reader.nextNonEmptyString()
|
||||
3 -> author = reader.nextNullableString()
|
||||
4 -> {
|
||||
val value = reader.nextNullableLong()
|
||||
|
||||
pubDate = if (value != null) {
|
||||
DateUtils.fromEpochSeconds(value)
|
||||
} else {
|
||||
LocalDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault())
|
||||
5 -> content = reader.nextNullableString()
|
||||
6 -> enclosureMime = reader.nextNullableString()
|
||||
7 -> enclosureLink = reader.nextNullableString()
|
||||
8 -> feedRemoteId = reader.nextInt().toString()
|
||||
9 -> isRead = !reader.nextBoolean() // the negation is important here
|
||||
10 -> isStarred = reader.nextBoolean()
|
||||
11 -> guid = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!)) {
|
||||
if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!))
|
||||
item.imageLink = enclosureLink
|
||||
}
|
||||
|
||||
if (item.title != null) {
|
||||
items += item
|
||||
}
|
||||
|
||||
items += item
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
@ -84,9 +72,7 @@ class NextcloudNewsItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of(
|
||||
"id", "url", "title", "author",
|
||||
"pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread", "starred"
|
||||
)
|
||||
val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "author",
|
||||
"pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread", "starred", "guidHash")
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
|
||||
class NextcloudNewsUserAdapter : XmlAdapter<String> {
|
||||
class NextNewsUserAdapter : XmlAdapter<String> {
|
||||
|
||||
override fun fromXml(konsumer: Konsumer): String {
|
||||
var displayName: String? = null
|
@ -1,8 +1,6 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ApiUtils {
|
||||
@ -14,14 +12,16 @@ object ApiUtils {
|
||||
const val LAST_MODIFIED_HEADER = "Last-Modified"
|
||||
const val IF_MODIFIED_HEADER = "If-Modified-Since"
|
||||
|
||||
val OPML_MIMETYPES = listOf("application/xml", "text/xml", "text/x-opml", "application/octet-stream")
|
||||
const val HTTP_UNPROCESSABLE = 422
|
||||
const val HTTP_NOT_FOUND = 404
|
||||
const val HTTP_CONFLICT = 409
|
||||
|
||||
private const val RSS_CONTENT_TYPE_REGEX = "([^;]+)"
|
||||
|
||||
fun isMimeImage(type: String): Boolean =
|
||||
type == "image" || type == "image/jpeg" || type == "image/jpg" || type == "image/png"
|
||||
|
||||
fun parseContentType(header: String): String? {
|
||||
fun parseContentType(header: String?): String? {
|
||||
val matcher = Pattern.compile(RSS_CONTENT_TYPE_REGEX)
|
||||
.matcher(header)
|
||||
return if (matcher.find()) {
|
||||
@ -37,14 +37,7 @@ object ApiUtils {
|
||||
* @param text string to clean
|
||||
* @return cleaned text
|
||||
*/
|
||||
fun cleanText(text: String): String {
|
||||
fun cleanText(text: String?): String {
|
||||
return Jsoup.parse(text).text().trim()
|
||||
}
|
||||
|
||||
fun md5hash(value: String): String {
|
||||
val bytes = MessageDigest.getInstance("MD5")
|
||||
.digest(value.toByteArray())
|
||||
|
||||
return BigInteger(1, bytes).toString(16)
|
||||
}
|
||||
}
|
67
api/src/main/java/com/readrops/api/utils/DateUtils.kt
Normal file
67
api/src/main/java/com/readrops/api/utils/DateUtils.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import java.util.*
|
||||
|
||||
object DateUtils {
|
||||
|
||||
private val TAG = DateUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Base of common RSS 2 date formats.
|
||||
* Examples :
|
||||
* Fri, 04 Jan 2019 22:21:46 GMT
|
||||
* Fri, 04 Jan 2019 22:21:46 +0000
|
||||
*/
|
||||
private const val RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss"
|
||||
|
||||
private const val GMT_PATTERN = "ZZZ"
|
||||
|
||||
private const val OFFSET_PATTERN = "Z"
|
||||
|
||||
private const val ISO_PATTERN = ".SSSZZ"
|
||||
|
||||
private const val EDT_PATTERN = "zzz"
|
||||
|
||||
/**
|
||||
* Date pattern for format : 2019-01-04T22:21:46+00:00
|
||||
*/
|
||||
private const val ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(value: String?): LocalDateTime? = if (value == null) {
|
||||
null
|
||||
} else try {
|
||||
val formatter = DateTimeFormatterBuilder()
|
||||
.appendOptional(DateTimeFormat.forPattern("$RSS_2_BASE_PATTERN ").parser) // with timezone
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).parser) // no timezone, important order here
|
||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).parser)
|
||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).parser)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.ENGLISH)
|
||||
.withOffsetParsed()
|
||||
|
||||
formatter.parseLocalDateTime(value)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun formattedDateByLocal(dateTime: LocalDateTime): String {
|
||||
return DateTimeFormat.mediumDate()
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun formattedDateTimeByLocal(dateTime: LocalDateTime): String {
|
||||
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime)
|
||||
}
|
||||
}
|
@ -4,14 +4,13 @@ import com.readrops.api.utils.exceptions.HttpException
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class ErrorInterceptor : Interceptor {
|
||||
class ErrorInterceptor() : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
// TODO cover all cases
|
||||
if (!response.isSuccessful && response.code != 304) {
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,10 @@ package com.readrops.api.utils
|
||||
|
||||
import android.nfc.FormatException
|
||||
import com.readrops.api.localfeed.LocalRSSHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
data class ParsingResult(
|
||||
val url: String,
|
||||
@ -17,111 +14,75 @@ data class ParsingResult(
|
||||
|
||||
object HtmlParser {
|
||||
|
||||
@Throws(FormatException::class)
|
||||
suspend fun getFeedLink(url: String, client: OkHttpClient): List<ParsingResult> {
|
||||
suspend fun getFaviconLink(url: String, client: OkHttpClient): String? {
|
||||
val document = getHTMLHeadFromUrl(url, client)
|
||||
val elements = document.select("link")
|
||||
|
||||
return document.select("link")
|
||||
.filter { element ->
|
||||
val type = element.attributes()["type"]
|
||||
LocalRSSHelper.isRSSType(type)
|
||||
}.map {
|
||||
ParsingResult(
|
||||
url = it.absUrl("href"),
|
||||
label = it.attributes()["title"]
|
||||
for (element in elements) {
|
||||
if (element.attributes()["rel"].lowercase().contains("icon")) {
|
||||
return element.absUrl("href")
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getFeedLink(url: String, client: OkHttpClient): List<ParsingResult> {
|
||||
val results = mutableListOf<ParsingResult>()
|
||||
|
||||
val document = getHTMLHeadFromUrl(url, client)
|
||||
val elements = document.select("link")
|
||||
|
||||
for (element in elements) {
|
||||
val type = element.attributes()["type"]
|
||||
|
||||
if (LocalRSSHelper.isRSSType(type)) {
|
||||
results += ParsingResult(
|
||||
url = element.absUrl("href"),
|
||||
label = element.attributes()["title"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFaviconLink(document: Document): String? {
|
||||
val links = document.select("link")
|
||||
.filter { element -> element.attributes()["rel"].contains("icon") }
|
||||
.sortedWith(compareByDescending<Element> {
|
||||
it.attributes()["rel"] == "apple-touch-icon"
|
||||
}.thenByDescending { element ->
|
||||
val sizes = element.attr("sizes")
|
||||
|
||||
if (sizes.isNotEmpty()) {
|
||||
try {
|
||||
sizes.filter { it.isDigit() }
|
||||
.toInt()
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
|
||||
return links.firstOrNull()
|
||||
?.absUrl("href")
|
||||
}
|
||||
|
||||
fun getFeedImage(document: Document): String? {
|
||||
return document.select("meta")
|
||||
.firstOrNull { element ->
|
||||
val property = element.attr("property")
|
||||
listOf("og:image", "twitter:image").any { it == property }
|
||||
}
|
||||
?.absUrl("content")
|
||||
}
|
||||
|
||||
fun getFeedDescription(document: Document): String? {
|
||||
return document.select("meta")
|
||||
.firstOrNull { element ->
|
||||
val property = element.attr("property")
|
||||
listOf("og:title", "twitter:title").any { it == property }
|
||||
}
|
||||
?.attr("content")
|
||||
}
|
||||
|
||||
suspend fun getHTMLHeadFromUrl(url: String, client: OkHttpClient): Document =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.newCall(
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
).execute()
|
||||
.use { response ->
|
||||
if (response.header(ApiUtils.CONTENT_TYPE_HEADER)!!
|
||||
.contains(ApiUtils.HTML_CONTENT_TYPE)
|
||||
) {
|
||||
val body = response.body!!.source()
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
var collectionStarted = false
|
||||
|
||||
while (!body.exhausted()) {
|
||||
val currentLine = body.readUtf8LineStrict()
|
||||
|
||||
when {
|
||||
currentLine.contains("<head>") -> {
|
||||
stringBuilder.append(currentLine)
|
||||
collectionStarted = true
|
||||
}
|
||||
|
||||
currentLine.contains("</head>") -> {
|
||||
stringBuilder.append(currentLine)
|
||||
break
|
||||
}
|
||||
|
||||
collectionStarted -> {
|
||||
stringBuilder.append(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stringBuilder.contains("<head>") || !stringBuilder.contains("</head>")) {
|
||||
body.close()
|
||||
throw FormatException("Failed to get HTML head from $url")
|
||||
}
|
||||
|
||||
body.close()
|
||||
Jsoup.parse(stringBuilder.toString(), url)
|
||||
} else {
|
||||
response.close()
|
||||
throw FormatException("Response from $url is not a html file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private fun getHTMLHeadFromUrl(url: String, client: OkHttpClient): Document {
|
||||
client.newCall(Request.Builder().url(url).build()).execute().use { response ->
|
||||
if (response.header(ApiUtils.CONTENT_TYPE_HEADER)!!.contains(ApiUtils.HTML_CONTENT_TYPE)
|
||||
) {
|
||||
val body = response.body!!.source()
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
var collectionStarted = false
|
||||
|
||||
while (!body.exhausted()) {
|
||||
val currentLine = body.readUtf8LineStrict()
|
||||
|
||||
when {
|
||||
currentLine.contains("<head>") -> {
|
||||
stringBuilder.append(currentLine)
|
||||
collectionStarted = true
|
||||
}
|
||||
currentLine.contains("</head>") -> {
|
||||
stringBuilder.append(currentLine)
|
||||
break
|
||||
}
|
||||
collectionStarted -> {
|
||||
stringBuilder.append(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stringBuilder.contains("<head>") || !stringBuilder.contains("</head>"))
|
||||
throw FormatException("Failed to get HTML head")
|
||||
|
||||
body.close()
|
||||
return Jsoup.parse(stringBuilder.toString(), url)
|
||||
} else {
|
||||
throw FormatException("The response is not a html file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
package com.readrops.api.utils.exceptions
|
||||
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class HttpException(val response: Response) : IOException() {
|
||||
class HttpException(val response: Response) : Exception() {
|
||||
|
||||
val code: Int
|
||||
get() = response.code
|
||||
|
@ -1,3 +0,0 @@
|
||||
package com.readrops.api.utils.exceptions
|
||||
|
||||
class LoginFailedException(override val message: String? = null) : Exception()
|
@ -13,19 +13,3 @@ fun JsonReader.nextNonEmptyString(): String {
|
||||
|
||||
fun JsonReader.nextNullableInt(): Int? =
|
||||
if (peek() != JsonReader.Token.NULL) nextInt() else nextNull()
|
||||
|
||||
fun JsonReader.nextNullableLong(): Long? =
|
||||
if (peek() != JsonReader.Token.NULL) nextLong() else nextNull()
|
||||
|
||||
fun JsonReader.skipField() {
|
||||
skipName()
|
||||
skipValue()
|
||||
}
|
||||
|
||||
fun JsonReader.skipToEnd() {
|
||||
while (hasNext()) {
|
||||
skipField()
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toBoolean(): Boolean = this == 1
|
||||
|
@ -19,7 +19,7 @@ fun Konsumer.nullableText(): String? {
|
||||
}
|
||||
|
||||
fun Konsumer.nullableTextRecursively(): String? {
|
||||
val text = textRecursively(whitespace = Whitespace.preserve)
|
||||
val text = textRecursively()
|
||||
return if (text.isNotEmpty()) text.trim() else null
|
||||
}
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
package com.readrops.api
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.Buffer
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
fun MockWebServer.enqueueOK() {
|
||||
enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
)
|
||||
}
|
||||
|
||||
fun MockWebServer.enqueueOKStream(stream: InputStream) {
|
||||
enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
}
|
||||
|
||||
fun MockResponse.Companion.okResponseWithBody(stream: InputStream): MockResponse {
|
||||
return MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream))
|
||||
}
|
@ -2,10 +2,9 @@ package com.readrops.api.localfeed
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertFalse
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class LocalRSSHelperTest {
|
||||
|
||||
@ -17,6 +16,8 @@ class LocalRSSHelperTest {
|
||||
LocalRSSHelper.RSSType.RSS_2)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/atom+xml"),
|
||||
LocalRSSHelper.RSSType.ATOM)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/json"),
|
||||
LocalRSSHelper.RSSType.JSONFEED)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/feed+json"),
|
||||
LocalRSSHelper.RSSType.JSONFEED)
|
||||
}
|
||||
|
@ -3,12 +3,17 @@ package com.readrops.api.localfeed
|
||||
import com.readrops.api.localfeed.atom.ATOMFeedAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
class XmlAdapterTest {
|
||||
|
||||
@get:Rule
|
||||
val expectedException: ExpectedException = ExpectedException.none()
|
||||
|
||||
@Test
|
||||
fun xmlFeedAdapterFactoryTest() {
|
||||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1FeedAdapter)
|
||||
|
@ -2,12 +2,14 @@ package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.lang.Exception
|
||||
|
||||
class ATOMAdapterTest {
|
||||
|
||||
@ -26,17 +28,16 @@ class ATOMAdapterTest {
|
||||
assertEquals(url, "https://github.com/readrops/Readrops/commits/develop.atom")
|
||||
assertEquals(siteUrl, "https://github.com/readrops/Readrops/commits/develop")
|
||||
assertEquals(description, "Here is a subtitle")
|
||||
assertEquals(imageUrl, "https://github.com/readrops/Readrops/blob/develop/images/readrops_logo.png")
|
||||
}
|
||||
|
||||
with(items.first()) {
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(title, "Add an option to open item url in custom tab")
|
||||
assertEquals(link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertEquals(pubDate!!.year, 2019)
|
||||
assertEquals(pubDate, DateUtils.parse("2020-09-06T21:09:59Z"))
|
||||
assertEquals(author, "Shinokuni")
|
||||
assertEquals(description, "Summary")
|
||||
assertEquals(remoteId, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertEquals(guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
TestCase.assertNotNull(content)
|
||||
}
|
||||
}
|
||||
@ -70,15 +71,4 @@ class ATOMAdapterTest {
|
||||
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaGroupTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_item_media_group.xml")
|
||||
val pair = adapter.fromXml(stream.konsumeXml())
|
||||
|
||||
with(pair.second.first()) {
|
||||
assertEquals("description", text)
|
||||
assertEquals("https://i3.ytimg.com/vi/.../hqdefault.jpg", imageLink)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import okio.Buffer
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
@ -35,12 +36,11 @@ class JSONFeedAdapterTest {
|
||||
assertEquals(url, "http://flyingmeat.com/blog/feed.json")
|
||||
assertEquals(siteUrl, "http://flyingmeat.com/blog/")
|
||||
assertEquals(description, "News from your friends at Flying Meat.")
|
||||
assertEquals(imageUrl, "https://secure.flyingmeat.com/favicon.ico")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 10)
|
||||
assertEquals(remoteId, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(title, "Acorn and 10.13")
|
||||
assertEquals(link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00"))
|
||||
|
@ -2,11 +2,11 @@ package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.util.DateUtils
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertNotNull
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
@ -28,7 +28,6 @@ class RSS1AdapterTest {
|
||||
assertEquals(url, "https://slashdot.org/")
|
||||
assertEquals(siteUrl, "https://slashdot.org/")
|
||||
assertEquals(description, "News for nerds, stuff that matters")
|
||||
assertEquals(imageUrl, "https://a.fsdn.com/sd/topics/topicslashdot.gif")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
@ -36,7 +35,7 @@ class RSS1AdapterTest {
|
||||
assertEquals(title, "Google Expands its Flutter Development Kit To Windows Apps")
|
||||
assertEquals(link!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(remoteId!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
assertEquals(guid!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00"))
|
||||
assertEquals(author, "msmash")
|
||||
|
@ -2,8 +2,8 @@ package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.util.DateUtils
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
@ -27,7 +27,6 @@ class RSS2AdapterTest {
|
||||
assertEquals(url, "https://news.ycombinator.com/feed/")
|
||||
assertEquals(siteUrl, "https://news.ycombinator.com/")
|
||||
assertEquals(description, "Links for the intellectually curious, ranked by readers.")
|
||||
assertEquals(imageUrl, "https://news.ycombinator.com/y18.svg")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
@ -37,7 +36,7 @@ class RSS2AdapterTest {
|
||||
assertEquals(pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000"))
|
||||
assertEquals(author, "Author 1")
|
||||
assertEquals(description, "<a href=\"https://news.ycombinator.com/item?id=24273602\">Comments</a>")
|
||||
assertEquals(remoteId, "https://www.bbc.com/news/world-africa-53887947")
|
||||
assertEquals(guid, "https://www.bbc.com/news/world-africa-53887947")
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +55,7 @@ class RSS2AdapterTest {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_other_namespaces.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals(item.remoteId, "guid")
|
||||
assertEquals(item.guid, "guid")
|
||||
assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z"))
|
||||
assertEquals(item.content, "content:encoded")
|
||||
@ -95,9 +94,9 @@ class RSS2AdapterTest {
|
||||
@Test
|
||||
fun enclosureTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_enclosure.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second.first()
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals("https://image1.jpg", item.imageLink)
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -112,8 +111,8 @@ class RSS2AdapterTest {
|
||||
@Test
|
||||
fun mediaGroupTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_group.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second.first()
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals("https://image1.jpg", item.imageLink)
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
}
|
@ -4,8 +4,8 @@ import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@ -13,87 +13,84 @@ import java.io.FileOutputStream
|
||||
class OPMLParserTest {
|
||||
|
||||
@Test
|
||||
fun readOpmlTest() = runTest {
|
||||
fun readOpmlTest() {
|
||||
val stream = TestUtils.loadResource("opml/subscriptions.opml")
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
|
||||
assertEquals(foldersAndFeeds.size, 6)
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder 1")]?.size, 2)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 1")]?.size, 4)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Subfolder 2")]?.size, 1)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 1")]?.size, 2)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Sub subfolder 2")]?.size, 0)
|
||||
assertEquals(foldersAndFeeds[null]?.size, 2)
|
||||
OPMLParser.read(stream)
|
||||
.observeOn(Schedulers.trampoline())
|
||||
.subscribeOn(Schedulers.trampoline())
|
||||
.subscribe { result -> foldersAndFeeds = result }
|
||||
|
||||
assertEquals(foldersAndFeeds?.size, 6)
|
||||
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Folder 1"))?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Subfolder 1"))?.size, 4)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Subfolder 2"))?.size, 1)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Sub subfolder 1"))?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.get(Folder(name = "Sub subfolder 2"))?.size, 0)
|
||||
assertEquals(foldersAndFeeds?.get(null)?.size, 2)
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readLiteSubscriptionsTest() = runTest {
|
||||
fun readLiteSubscriptionsTest() {
|
||||
val stream = TestUtils.loadResource("opml/lite_subscriptions.opml")
|
||||
|
||||
val foldersAndFeeds = OPMLParser.read(stream)
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
|
||||
assertEquals(foldersAndFeeds.values.first().size, 2)
|
||||
assertEquals(
|
||||
foldersAndFeeds.values.first().first().url,
|
||||
"http://www.theverge.com/rss/index.xml"
|
||||
)
|
||||
assertEquals(foldersAndFeeds.values.first()[1].url, "https://techcrunch.com/feed/")
|
||||
OPMLParser.read(stream)
|
||||
.subscribe { result -> foldersAndFeeds = result }
|
||||
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.size, 2)
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.first()?.url, "http://www.theverge.com/rss/index.xml")
|
||||
assertEquals(foldersAndFeeds?.values?.first()?.get(1)?.url, "https://techcrunch.com/feed/")
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun opmlVersionTest() = runTest {
|
||||
@Test
|
||||
fun opmlVersionTest() {
|
||||
val stream = TestUtils.loadResource("opml/wrong_version.opml")
|
||||
|
||||
OPMLParser.read(stream)
|
||||
.test()
|
||||
.assertError(ParseException::class.java)
|
||||
|
||||
stream.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOpmlTest() = runTest {
|
||||
fun writeOpmlTest() {
|
||||
val file = File("subscriptions.opml")
|
||||
val outputStream = FileOutputStream(file)
|
||||
|
||||
val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
|
||||
put(
|
||||
null, listOf(
|
||||
Feed(name = "Feed1", url = "https://feed1.com"),
|
||||
Feed(name = "Feed2", url = "https://feed2.com")
|
||||
)
|
||||
)
|
||||
put(null, listOf(Feed(name = "Feed1", url = "https://feed1.com"),
|
||||
Feed(name = "Feed2", url = "https://feed2.com")))
|
||||
put(Folder(name = "Folder1"), listOf())
|
||||
put(
|
||||
Folder(name = "Folder2"), listOf(
|
||||
Feed(name = "Feed3", url = "https://feed3.com"),
|
||||
Feed(name = "Feed4", url = "https://feed4.com")
|
||||
)
|
||||
)
|
||||
put(Folder(name = "Folder2"), listOf(Feed(name = "Feed3", url = "https://feed3.com"),
|
||||
Feed(name = "Feed4", url ="https://feed4.com")))
|
||||
}
|
||||
|
||||
OPMLParser.write(foldersAndFeeds, outputStream)
|
||||
.subscribeOn(Schedulers.trampoline())
|
||||
.subscribe()
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
val inputStream = file.inputStream()
|
||||
val foldersAndFeeds2 = OPMLParser.read(inputStream)
|
||||
var foldersAndFeeds2: Map<Folder?, List<Feed>>? = null
|
||||
OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result }
|
||||
|
||||
assertEquals(foldersAndFeeds.size, foldersAndFeeds2.size)
|
||||
assertEquals(
|
||||
foldersAndFeeds[Folder(name = "Folder1")]?.size,
|
||||
foldersAndFeeds2[Folder(name = "Folder1")]?.size
|
||||
)
|
||||
assertEquals(
|
||||
foldersAndFeeds[Folder(name = "Folder2")]?.size,
|
||||
foldersAndFeeds2[Folder(name = "Folder2")]?.size
|
||||
)
|
||||
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2[null]?.size)
|
||||
assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder1")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder1"))?.size)
|
||||
assertEquals(foldersAndFeeds[Folder(name = "Folder2")]?.size, foldersAndFeeds2?.get(Folder(name = "Folder2"))?.size)
|
||||
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.size)
|
||||
|
||||
inputStream.close()
|
||||
file.delete()
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package com.readrops.api.services
|
||||
|
||||
import com.readrops.api.services.greader.GReaderCredentials
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsCredentials
|
||||
import com.readrops.api.services.freshrss.FreshRSSCredentials
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsCredentials
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -9,7 +9,7 @@ class CredentialsTest {
|
||||
|
||||
@Test
|
||||
fun credentialsTest() {
|
||||
val credentials = GReaderCredentials("token", "https://freshrss.org")
|
||||
val credentials = FreshRSSCredentials("token", "https://freshrss.org")
|
||||
|
||||
assertEquals(credentials.authorization!!, "GoogleLogin auth=token")
|
||||
assertEquals(credentials.url, "https://freshrss.org")
|
||||
@ -17,7 +17,7 @@ class CredentialsTest {
|
||||
|
||||
@Test
|
||||
fun nextcloudNewsCredentialsTest() {
|
||||
val credentials = NextcloudNewsCredentials("login", "password", "https://freshrss.org")
|
||||
val credentials = NextNewsCredentials("login", "password", "https://freshrss.org")
|
||||
|
||||
assertEquals(credentials.authorization!!, "Basic bG9naW46cGFzc3dvcmQ=")
|
||||
assertEquals(credentials.url, "https://freshrss.org")
|
||||
|
@ -1,241 +0,0 @@
|
||||
package com.readrops.api.services.fever
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.api.enqueueOK
|
||||
import com.readrops.api.enqueueOKStream
|
||||
import com.readrops.api.okResponseWithBody
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.dsl.module
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koin.test.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FeverDataSourceTest : KoinTest {
|
||||
|
||||
private lateinit var dataSource: FeverDataSource
|
||||
private val mockServer = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start(8080)
|
||||
val url = mockServer.url("")
|
||||
dataSource = get(parameters = {
|
||||
parametersOf(FeverCredentials(null, null, url.toString()))
|
||||
})
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.close()
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
modules(apiModule, module {
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.HOURS)
|
||||
.addInterceptor(get<AuthInterceptor>())
|
||||
.build()
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginSuccessfulTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/fever/successful_auth.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
assertTrue { dataSource.login("", "") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginFailedTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/fever/failed_auth.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
assertFalse { dataSource.login("", "") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setItemStateTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
dataSource.setItemState("login", "password", "saved", "itemId")
|
||||
val request = mockServer.takeRequest()
|
||||
val requestBody = request.body.readUtf8()
|
||||
|
||||
assertEquals("saved", request.requestUrl?.queryParameter("as"))
|
||||
assertEquals("itemId", request.requestUrl?.queryParameter("id"))
|
||||
|
||||
assertTrue { requestBody.contains("api_key") }
|
||||
assertTrue { requestBody.contains("fb2f5a9b0eccc1ee95c1d559a2dd797a") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialSyncTest() = runTest {
|
||||
var pageNumber = 0
|
||||
var firstMaxId = ""
|
||||
var secondMaxId = ""
|
||||
var thirdMaxId = ""
|
||||
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
return when {
|
||||
this == "/?feeds" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/feeds.json"))
|
||||
}
|
||||
|
||||
this == "/?groups" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/folders.json"))
|
||||
}
|
||||
|
||||
this == "/?favicons" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/favicons.json"))
|
||||
}
|
||||
|
||||
this == "/?unread_item_ids" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
|
||||
}
|
||||
|
||||
this == "/?saved_item_ids" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
|
||||
}
|
||||
|
||||
contains("/?items") -> {
|
||||
when (pageNumber++) {
|
||||
0 -> {
|
||||
firstMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page2.json"))
|
||||
}
|
||||
1 -> {
|
||||
secondMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page1.json"))
|
||||
}
|
||||
2 -> {
|
||||
thirdMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/empty_items.json"))
|
||||
}
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = dataSource.synchronize("login", "password", SyncType.INITIAL_SYNC, "")
|
||||
|
||||
assertEquals(1, result.folders.size)
|
||||
assertEquals(1, result.feverFeeds.feeds.size)
|
||||
assertEquals(3, result.favicons.size)
|
||||
assertEquals(6, result.unreadIds.size)
|
||||
assertEquals(6, result.starredIds.size)
|
||||
assertEquals(10, result.items.size)
|
||||
assertEquals(10, result.items.size)
|
||||
assertEquals(1564058340320135, result.sinceId)
|
||||
|
||||
assertEquals("1564058340320135", firstMaxId)
|
||||
assertEquals("6", secondMaxId)
|
||||
assertEquals("1", thirdMaxId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun classicSyncTest() = runTest {
|
||||
var pageNumber = 0
|
||||
|
||||
var firstLastSinceId = ""
|
||||
var secondLastSinceId = ""
|
||||
var thirdLastSinceId = ""
|
||||
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
return when {
|
||||
this == "/?feeds" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/feeds.json"))
|
||||
}
|
||||
|
||||
this == "/?groups" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/folders.json"))
|
||||
}
|
||||
|
||||
this == "/?favicons" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/favicons.json"))
|
||||
}
|
||||
|
||||
this == "/?unread_item_ids" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
|
||||
}
|
||||
|
||||
this == "/?saved_item_ids" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
|
||||
}
|
||||
|
||||
contains("/?items") -> {
|
||||
when (pageNumber++) {
|
||||
0 -> {
|
||||
firstLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page1.json"))
|
||||
}
|
||||
1 -> {
|
||||
secondLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page2.json"))
|
||||
}
|
||||
2 -> {
|
||||
thirdLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/empty_items.json"))
|
||||
}
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = dataSource.synchronize("login", "password", SyncType.CLASSIC_SYNC, "1")
|
||||
|
||||
assertEquals(1, result.folders.size)
|
||||
assertEquals(1, result.feverFeeds.feeds.size)
|
||||
assertEquals(3, result.favicons.size)
|
||||
assertEquals(6, result.unreadIds.size)
|
||||
assertEquals(6, result.starredIds.size)
|
||||
assertEquals(10, result.items.size)
|
||||
assertEquals(10, result.sinceId)
|
||||
|
||||
assertEquals("1", firstLastSinceId)
|
||||
assertEquals("5", secondLastSinceId)
|
||||
assertEquals("10", thirdLastSinceId)
|
||||
|
||||
mockServer.dispatcher.shutdown()
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FeverAPIAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Boolean::class.java, FeverAPIAdapter())
|
||||
.build()
|
||||
.adapter(Boolean::class.java)
|
||||
|
||||
@Test
|
||||
fun authenticatedTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/successful_auth.json")
|
||||
|
||||
assertTrue { adapter.fromJson(Buffer().readFrom(stream))!! }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unauthenticatedTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/unsuccessful_auth.json")
|
||||
|
||||
assertFalse { adapter.fromJson(Buffer().readFrom(stream))!! }
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class FeverFaviconsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(FeverFaviconsAdapter())
|
||||
.build()
|
||||
.adapter<List<Favicon>>(Types.newParameterizedType(List::class.java, Favicon::class.java))
|
||||
|
||||
|
||||
@Test
|
||||
fun validFaviconsTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/favicons.json")
|
||||
|
||||
val favicons = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
assertEquals(favicons.size, 3)
|
||||
|
||||
with(favicons.first()) {
|
||||
assertEquals(id, 85)
|
||||
assertNotNull(data)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FeverFeedsAdapterTest {
|
||||
|
||||
val adapter = Moshi.Builder()
|
||||
.add(FeverFeeds::class.java, FeverFeedsAdapter())
|
||||
.build()
|
||||
.adapter(FeverFeeds::class.java)!!
|
||||
|
||||
@Test
|
||||
fun validFeedsTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/feeds.json")
|
||||
|
||||
val feverFeeds = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
assertEquals(feverFeeds.feeds.size, 1)
|
||||
|
||||
with(feverFeeds.feeds.first()) {
|
||||
assertEquals(name, "xda-developers")
|
||||
assertEquals(url, "https://www.xda-developers.com/feed/")
|
||||
assertEquals(siteUrl, "https://www.xda-developers.com/")
|
||||
assertEquals(remoteId, "32")
|
||||
}
|
||||
|
||||
with(feverFeeds.feedsGroups.entries.first()) {
|
||||
assertEquals(key, 3)
|
||||
assertEquals(value, listOf(5, 4))
|
||||
}
|
||||
|
||||
with(feverFeeds.favicons.entries.first()) {
|
||||
assertEquals(30, key)
|
||||
assertEquals("32", value)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FeverFoldersAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(FeverFoldersAdapter())
|
||||
.build()
|
||||
.adapter<List<Folder>>(Types.newParameterizedType(List::class.java, Folder::class.java))
|
||||
|
||||
@Test
|
||||
fun validFoldersTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/folders.json")
|
||||
|
||||
val folders = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
with(folders.first()) {
|
||||
assertEquals(name, "Libre")
|
||||
assertEquals(remoteId, "4")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FeverItemsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(FeverItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
@Test
|
||||
fun validItemsTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/items_page2.json")
|
||||
|
||||
val items = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
with(items.first()) {
|
||||
assertEquals(title, "FreshRSS 1.9.0")
|
||||
assertEquals(author, "Alkarex")
|
||||
assertEquals(link, "https://github.com/FreshRSS/FreshRSS/releases/tag/1.9.0")
|
||||
assertNotNull(content)
|
||||
assertTrue(isStarred)
|
||||
assertTrue(isRead)
|
||||
assertNotNull(pubDate)
|
||||
assertEquals(remoteId, "6")
|
||||
assertEquals(feedRemoteId, "2")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package com.readrops.api.services.fever.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FeverItemsIdsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(FeverItemsIdsAdapter())
|
||||
.build()
|
||||
.adapter<List<String>>(Types.newParameterizedType(List::class.java, String::class.java))
|
||||
|
||||
@Test
|
||||
fun validIdsTest() {
|
||||
val stream = TestUtils.loadResource("services/fever/itemsIds.json")
|
||||
|
||||
val ids = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
assertEquals(ids.size, 6)
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.apiModule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.Buffer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koin.test.get
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URLEncoder
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FreshRSSDataSourceTest : KoinTest {
|
||||
|
||||
private lateinit var freshRSSDataSource: NewFreshRSSDataSource
|
||||
private val mockServer = MockWebServer()
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
modules(apiModule, module {
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://localhost:8080/")
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
|
||||
.build()
|
||||
.create(NewFreshRSSService::class.java)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start(8080)
|
||||
freshRSSDataSource = NewFreshRSSDataSource(get())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginTest() {
|
||||
runBlocking {
|
||||
val responseBody = TestUtils.loadResource("services/freshrss/login_response_body")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(responseBody)))
|
||||
|
||||
val authString = freshRSSDataSource.login("Login", "Password")
|
||||
assertEquals("login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a", authString)
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
val requestBody = request.body.readUtf8()
|
||||
|
||||
assertTrue {
|
||||
requestBody.contains("name=\"Email\"") && requestBody.contains("Login")
|
||||
}
|
||||
|
||||
assertTrue {
|
||||
requestBody.contains("name=\"Passwd\"") && requestBody.contains("Password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeTokenTest() = runBlocking {
|
||||
val responseBody = TestUtils.loadResource("services/freshrss/writetoken_response_body")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(responseBody)))
|
||||
|
||||
val writeToken = freshRSSDataSource.getWriteToken()
|
||||
|
||||
assertEquals("PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg", writeToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userInfoTest() = runBlocking {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foldersTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/folders.json")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val folders = freshRSSDataSource.getFolders()
|
||||
assertTrue { folders.size == 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feedsTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/feeds.json")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val feeds = freshRSSDataSource.getFeeds()
|
||||
assertTrue { feeds.size == 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun itemsTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/items.json")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val items = freshRSSDataSource.getItems(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), 100, 21343321321321)
|
||||
assertTrue { items.size == 2 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), queryParameterValues("xt"))
|
||||
assertEquals("100", queryParameter("n"))
|
||||
assertEquals("21343321321321", queryParameter("ot"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun starredItemsTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/items.json")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val items = freshRSSDataSource.getStarredItems(100)
|
||||
assertTrue { items.size == 2 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertEquals("100", request.requestUrl!!.queryParameter("n"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsIdsTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/items_starred_ids.json")
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val ids = freshRSSDataSource.getItemsIds(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_READING_LIST, 100)
|
||||
assertTrue { ids.size == 5 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals(NewFreshRSSDataSource.GOOGLE_READ, queryParameter("xt"))
|
||||
assertEquals(NewFreshRSSDataSource.GOOGLE_READING_LIST, queryParameter("s"))
|
||||
assertEquals("100", queryParameter("n"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFeedTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.createFeed("token", "https://feed.url")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
|
||||
assertTrue { contains("ac=subscribe") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFeedTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.deleteFeed("token", "https://feed.url")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
|
||||
assertTrue { contains("ac=unsubscribe") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateFeedTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.updateFeed("token", "https://feed.url", "title", "folderId")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
|
||||
assertTrue { contains("t=title") }
|
||||
assertTrue { contains("a=folderId") }
|
||||
assertTrue { contains("ac=edit") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFolderTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.createFolder("token", "folder")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("a=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateFolderTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.updateFolder("token", "folderId", "folder")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=folderId") }
|
||||
assertTrue { contains("dest=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFolderTest() = runBlocking {
|
||||
mockServer.enqueue(MockResponse()
|
||||
.setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
freshRSSDataSource.deleteFolder("token", "folderId")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=folderId") }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Feed
|
||||
@ -8,18 +8,18 @@ import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
|
||||
class GReaderFeedsAdapterTest {
|
||||
class FreshRSSFeedsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(GReaderFeedsAdapter())
|
||||
.add(FreshRSSFeedsAdapter())
|
||||
.build()
|
||||
.adapter<List<Feed>>(Types.newParameterizedType(List::class.java, Feed::class.java))
|
||||
|
||||
@Test
|
||||
fun validFeedsTest() {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/feeds.json")
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/feeds.json")
|
||||
|
||||
val feed = adapter.fromJson(Buffer().readFrom(stream))!!.first()
|
||||
val feed = adapter.fromJson(Buffer().readFrom(stream))!![0]
|
||||
|
||||
with(feed) {
|
||||
assertEquals(remoteId, "feed/2")
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Folder
|
||||
@ -8,22 +8,22 @@ import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
|
||||
class GReaderFoldersAdapterTest {
|
||||
class FreshRSSFoldersAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(GReaderFoldersAdapter())
|
||||
.add(FreshRSSFoldersAdapter())
|
||||
.build()
|
||||
.adapter<List<Folder>>(Types.newParameterizedType(List::class.java, Folder::class.java))
|
||||
|
||||
@Test
|
||||
fun validFoldersTest() {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/folders.json")
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/folders.json")
|
||||
|
||||
val folders = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
assertEquals(folders.size, 1)
|
||||
|
||||
with(folders.first()) {
|
||||
with(folders[0]) {
|
||||
assertEquals(name, "Blogs")
|
||||
assertEquals(remoteId, "user/-/label/Blogs")
|
||||
}
|
@ -1,35 +1,35 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import okio.Buffer
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.junit.Test
|
||||
|
||||
class GReaderItemsAdapterTest {
|
||||
class FreshRSSItemsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), GReaderItemsAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), FreshRSSItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
@Test
|
||||
fun validItemsTest() {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/items.json")
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/items.json")
|
||||
|
||||
val items = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
with(items.first()) {
|
||||
with(items[0]) {
|
||||
assertEquals(remoteId, "tag:google.com,2005:reader/item/0005c62466ee28fe")
|
||||
assertEquals(title, "GNOME’s Default Theme is Getting a Revamp")
|
||||
assertNotNull(content)
|
||||
assertEquals(link, "http://feedproxy.google.com/~r/d0od/~3/4Zk-fncSuek/adwaita-borderless-theme-in-development-gnome-41")
|
||||
assertEquals(author, "Joey Sneddon")
|
||||
assertEquals(pubDate, DateUtils.fromEpochSeconds(1625234040))
|
||||
assertEquals(pubDate, LocalDateTime(1625234040 * 1000L))
|
||||
assertEquals(isRead, false)
|
||||
assertEquals(isStarred, false)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
@ -6,16 +6,16 @@ import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
|
||||
class GReaderItemsIdsAdapterTest {
|
||||
class FreshRSSItemsIdsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, String::class.java), GReaderItemsIdsAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter())
|
||||
.build()
|
||||
.adapter<List<String>>(Types.newParameterizedType(List::class.java, String::class.java))
|
||||
|
||||
@Test
|
||||
fun validIdsTest() {
|
||||
val stream = javaClass.classLoader!!.getResourceAsStream("services/greader/adapters/items_starred_ids.json")
|
||||
val stream = javaClass.classLoader!!.getResourceAsStream("services/freshrss/adapters/items_starred_ids.json")
|
||||
|
||||
val ids = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.readrops.api.services.greader.adapters
|
||||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
@ -6,7 +6,7 @@ import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
|
||||
class GReaderUserInfoAdapterTest {
|
||||
class FreshRSSUserInfoAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(FreshRSSUserInfoAdapter())
|
||||
@ -15,7 +15,7 @@ class GReaderUserInfoAdapterTest {
|
||||
|
||||
@Test
|
||||
fun userInfoTest() {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/user_info.json")
|
||||
val stream = TestUtils.loadResource("services/freshrss/adapters/user_info.json")
|
||||
|
||||
val userInfo = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
@ -1,421 +0,0 @@
|
||||
package com.readrops.api.services.greader
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.api.enqueueOK
|
||||
import com.readrops.api.enqueueOKStream
|
||||
import com.readrops.api.okResponseWithBody
|
||||
import com.readrops.api.services.SyncType
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koin.test.get
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.net.URLEncoder
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class GReaderDataSourceTest : KoinTest {
|
||||
|
||||
private lateinit var freshRSSDataSource: GReaderDataSource
|
||||
private val mockServer = MockWebServer()
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
modules(apiModule, module {
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://localhost:8080/")
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("greaderMoshi"))))
|
||||
.build()
|
||||
.create(GReaderService::class.java)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start(8080)
|
||||
freshRSSDataSource = GReaderDataSource(get())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginTest() = runTest {
|
||||
val responseBody = TestUtils.loadResource("services/greader/login_response_body")
|
||||
mockServer.enqueueOKStream(responseBody)
|
||||
|
||||
val authString = freshRSSDataSource.login("Login", "Password")
|
||||
assertEquals("login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a", authString)
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
val requestBody = request.body.readUtf8()
|
||||
|
||||
assertTrue {
|
||||
requestBody.contains("name=\"Email\"") && requestBody.contains("Login")
|
||||
}
|
||||
|
||||
assertTrue {
|
||||
requestBody.contains("name=\"Passwd\"") && requestBody.contains("Password")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeTokenTest() = runTest {
|
||||
val responseBody = TestUtils.loadResource("services/greader/writetoken_response_body")
|
||||
mockServer.enqueueOKStream(responseBody)
|
||||
|
||||
val writeToken = freshRSSDataSource.getWriteToken()
|
||||
|
||||
assertEquals("PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg", writeToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userInfoTest() = runTest {
|
||||
val responseBody = TestUtils.loadResource("services/greader/adapters/user_info.json")
|
||||
mockServer.enqueueOKStream(responseBody)
|
||||
|
||||
val userInfo = freshRSSDataSource.getUserInfo()
|
||||
|
||||
assertEquals("test", userInfo.userName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foldersTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/folders.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val folders = freshRSSDataSource.getFolders()
|
||||
assertTrue { folders.size == 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feedsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/feeds.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val feeds = freshRSSDataSource.getFeeds()
|
||||
assertTrue { feeds.size == 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun itemsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/items.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val items = freshRSSDataSource.getItems(
|
||||
excludeTargets = listOf(
|
||||
GReaderDataSource.GOOGLE_READ,
|
||||
GReaderDataSource.GOOGLE_STARRED
|
||||
),
|
||||
max = 100,
|
||||
lastModified = 21343321321321
|
||||
)
|
||||
assertTrue { items.size == 2 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals(
|
||||
listOf(GReaderDataSource.GOOGLE_READ, GReaderDataSource.GOOGLE_STARRED),
|
||||
queryParameterValues("xt")
|
||||
)
|
||||
assertEquals("100", queryParameter("n"))
|
||||
assertEquals("21343321321321", queryParameter("ot"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun starredItemsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/items.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val items = freshRSSDataSource.getStarredItems(100)
|
||||
assertTrue { items.size == 2 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertEquals("100", request.requestUrl!!.queryParameter("n"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsIdsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/greader/adapters/items_starred_ids.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val ids = freshRSSDataSource.getItemsIds(
|
||||
excludeTarget = GReaderDataSource.GOOGLE_READ,
|
||||
includeTarget = GReaderDataSource.GOOGLE_READING_LIST,
|
||||
max = 100
|
||||
)
|
||||
assertTrue { ids.size == 5 }
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals(GReaderDataSource.GOOGLE_READ, queryParameter("xt"))
|
||||
assertEquals(GReaderDataSource.GOOGLE_READING_LIST, queryParameter("s"))
|
||||
assertEquals("100", queryParameter("n"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFeedTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.createFeed("token", "https://feed.url", "feed/1")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("a=feed%2F1") }
|
||||
assertTrue {
|
||||
contains(
|
||||
"s=${
|
||||
URLEncoder.encode(
|
||||
"${GReaderDataSource.FEED_PREFIX}https://feed.url", "UTF-8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
assertTrue { contains("ac=subscribe") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFeedTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.deleteFeed("token", "https://feed.url")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue {
|
||||
contains(
|
||||
"s=${
|
||||
URLEncoder.encode(
|
||||
"${GReaderDataSource.FEED_PREFIX}https://feed.url",
|
||||
"UTF-8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
assertTrue { contains("ac=unsubscribe") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateFeedTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.updateFeed("token", "https://feed.url", "title", "folderId")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue {
|
||||
contains(
|
||||
"s=${
|
||||
URLEncoder.encode(
|
||||
"${GReaderDataSource.FEED_PREFIX}https://feed.url",
|
||||
"UTF-8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
assertTrue { contains("t=title") }
|
||||
assertTrue { contains("a=folderId") }
|
||||
assertTrue { contains("ac=edit") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.createFolder("token", "folder")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue {
|
||||
contains(
|
||||
"a=${
|
||||
URLEncoder.encode(
|
||||
"${GReaderDataSource.FOLDER_PREFIX}folder",
|
||||
"UTF-8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.updateFolder("token", "folderId", "folder")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=folderId") }
|
||||
assertTrue {
|
||||
contains(
|
||||
"dest=${
|
||||
URLEncoder.encode(
|
||||
"${GReaderDataSource.FOLDER_PREFIX}folder",
|
||||
"UTF-8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
freshRSSDataSource.deleteFolder("token", "folderId")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
with(request.body.readUtf8()) {
|
||||
assertTrue { contains("T=token") }
|
||||
assertTrue { contains("s=folderId") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialSyncTest() = runTest {
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
return when {
|
||||
contains("tag/list") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/folders.json"))
|
||||
}
|
||||
|
||||
contains("subscription/list") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/feeds.json"))
|
||||
}
|
||||
|
||||
// items
|
||||
contains("contents/user/-/state/com.google/reading-list") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/items.json"))
|
||||
}
|
||||
|
||||
// starred items
|
||||
contains("contents/user/-/state/com.google/starred") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/items.json"))
|
||||
}
|
||||
|
||||
// unread ids & starred ids
|
||||
contains("stream/items/ids") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/items_starred_ids.json"))
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result =
|
||||
freshRSSDataSource.synchronize(SyncType.INITIAL_SYNC, GReaderSyncData(), "writeToken")
|
||||
|
||||
with(result) {
|
||||
assertEquals(1, folders.size)
|
||||
assertEquals(1, feeds.size)
|
||||
assertEquals(2, items.size)
|
||||
assertEquals(2, starredItems.size)
|
||||
assertEquals(5, unreadIds.size)
|
||||
assertEquals(5, starredIds.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun classicSync() = runTest {
|
||||
var setItemState = 0
|
||||
val ids = listOf("1", "2", "3", "4")
|
||||
val lastModified = 10L
|
||||
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
// printing request path before anything prevents a request being ignored and the test fail, I don't really know why
|
||||
println("request: ${request.path}")
|
||||
return when {
|
||||
contains("0/edit-tag") -> {
|
||||
setItemState++
|
||||
MockResponse().setResponseCode(200)
|
||||
}
|
||||
|
||||
contains("tag/list") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/folders.json"))
|
||||
}
|
||||
|
||||
contains("subscription/list") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/feeds.json"))
|
||||
}
|
||||
|
||||
// items
|
||||
contains("contents/user/-/state/com.google/reading-list") -> {
|
||||
assertTrue { request.path!!.contains("ot=$lastModified") }
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/items.json"))
|
||||
}
|
||||
|
||||
// unread & read ids
|
||||
contains("stream/items/ids") -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/greader/adapters/items_starred_ids.json"))
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = freshRSSDataSource.synchronize(
|
||||
syncType = SyncType.CLASSIC_SYNC,
|
||||
syncData = GReaderSyncData(
|
||||
lastModified = 10L,
|
||||
readIds = ids,
|
||||
unreadIds = ids,
|
||||
starredIds = ids,
|
||||
unstarredIds = ids
|
||||
),
|
||||
writeToken = "writeToken"
|
||||
)
|
||||
|
||||
with(result) {
|
||||
assertEquals(4, setItemState)
|
||||
assertEquals(1, folders.size)
|
||||
assertEquals(1, feeds.size)
|
||||
assertEquals(2, items.size)
|
||||
assertEquals(5, unreadIds.size)
|
||||
assertEquals(5, readIds.size)
|
||||
assertEquals(5, starredIds.size)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,389 +0,0 @@
|
||||
package com.readrops.api.services.nextcloudnews
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.api.enqueueOK
|
||||
import com.readrops.api.enqueueOKStream
|
||||
import com.readrops.api.okResponseWithBody
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koin.test.get
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NextcloudNewsDataSourceTest : KoinTest {
|
||||
|
||||
private lateinit var nextcloudNewsDataSource: NextcloudNewsDataSource
|
||||
private val mockServer = MockWebServer()
|
||||
private val moshi = Moshi.Builder()
|
||||
.build()
|
||||
|
||||
@get:Rule
|
||||
val koinTestRule = KoinTestRule.create {
|
||||
modules(apiModule, module {
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://localhost:8080/")
|
||||
.client(get())
|
||||
.addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi"))))
|
||||
.build()
|
||||
.create(NextcloudNewsService::class.java)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start(8080)
|
||||
nextcloudNewsDataSource = NextcloudNewsDataSource(get())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/user.xml")
|
||||
val account = Account(login = "login", url = mockServer.url("").toString())
|
||||
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val displayName = nextcloudNewsDataSource.login(get(), account)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertTrue { displayName == "Shinokuni" }
|
||||
assertTrue { request.headers.contains("OCS-APIRequest" to "true") }
|
||||
assertTrue { request.path == "//ocs/v1.php/cloud/users/login" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foldersTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/valid_folder.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val folders = nextcloudNewsDataSource.getFolders()
|
||||
assertTrue { folders.size == 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feedsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/feeds.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val feeds = nextcloudNewsDataSource.getFeeds()
|
||||
assertTrue { feeds.size == 3 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun itemsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/items.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val type = NextcloudNewsDataSource.ItemQueryType.ALL.value
|
||||
|
||||
val items = nextcloudNewsDataSource.getItems(
|
||||
type = type,
|
||||
read = false,
|
||||
batchSize = 10
|
||||
)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertTrue { items.size == 2 }
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals("$type", queryParameter("type"))
|
||||
assertEquals("false", queryParameter("getRead"))
|
||||
assertEquals("10", queryParameter("batchSize"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun newItemsTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/items.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val items =
|
||||
nextcloudNewsDataSource.getNewItems(1512, NextcloudNewsDataSource.ItemQueryType.ALL)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertTrue { items.size == 2 }
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals("1512", queryParameter("lastModified"))
|
||||
assertEquals("3", queryParameter("type"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFeedTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/feeds.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val feeds = nextcloudNewsDataSource.createFeed("https://news.ycombinator.com/rss", 100)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertTrue { feeds.isNotEmpty() }
|
||||
with(request.requestUrl!!) {
|
||||
assertEquals("https://news.ycombinator.com/rss", queryParameter("url"))
|
||||
assertEquals("100", queryParameter("folderId"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFeedTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
nextcloudNewsDataSource.deleteFeed(15)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertTrue { request.path!!.endsWith("/15") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changeFeedFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
nextcloudNewsDataSource.changeFeedFolder(15, 18)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
val type = Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Int::class.javaObjectType
|
||||
)
|
||||
val adapter = moshi.adapter<Map<String, Int>>(type)
|
||||
val body = adapter.fromJson(request.body)!!
|
||||
|
||||
assertTrue { request.path!!.endsWith("/18/move") }
|
||||
assertEquals(15, body["folderId"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameFeedTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
nextcloudNewsDataSource.renameFeed("name", 15)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
val type =
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)
|
||||
val adapter = moshi.adapter<Map<String, String>>(type)
|
||||
val body = adapter.fromJson(request.body)!!
|
||||
|
||||
assertTrue { request.path!!.endsWith("/15/rename") }
|
||||
assertEquals("name", body["feedTitle"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createFolderTest() = runTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/valid_folder.json")
|
||||
mockServer.enqueueOKStream(stream)
|
||||
|
||||
val folders = nextcloudNewsDataSource.createFolder("folder name")
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
val type =
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)
|
||||
val adapter = moshi.adapter<Map<String, String>>(type)
|
||||
val body = adapter.fromJson(request.body)!!
|
||||
|
||||
assertTrue { folders.size == 1 }
|
||||
assertEquals("folder name", body["name"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
nextcloudNewsDataSource.renameFolder("new name", 15)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
val type =
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)
|
||||
val adapter = moshi.adapter<Map<String, String>>(type)
|
||||
val body = adapter.fromJson(request.body)!!
|
||||
|
||||
assertTrue { request.path!!.endsWith("/15") }
|
||||
assertEquals("new name", body["name"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFolderTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
|
||||
nextcloudNewsDataSource.deleteFolder(15)
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertEquals(request.method, "DELETE")
|
||||
assertTrue { request.path!!.endsWith("/15") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setItemsReadStateTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
mockServer.enqueueOK()
|
||||
|
||||
val data = NextcloudNewsSyncData(
|
||||
readIds = listOf(15, 16, 17),
|
||||
unreadIds = listOf(18, 19, 20)
|
||||
)
|
||||
|
||||
nextcloudNewsDataSource.setItemsReadState(data)
|
||||
val unreadRequest = mockServer.takeRequest()
|
||||
val readRequest = mockServer.takeRequest()
|
||||
|
||||
val type =
|
||||
Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Types.newParameterizedType(List::class.java, Int::class.javaObjectType)
|
||||
)
|
||||
val adapter = moshi.adapter<Map<String, List<Int>>>(type)
|
||||
val unreadBody = adapter.fromJson(unreadRequest.body)!!
|
||||
val readBody = adapter.fromJson(readRequest.body)!!
|
||||
|
||||
assertEquals(data.readIds, readBody["itemIds"])
|
||||
assertEquals(data.unreadIds, unreadBody["itemIds"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setItemsStarStateTest() = runTest {
|
||||
mockServer.enqueueOK()
|
||||
mockServer.enqueueOK()
|
||||
|
||||
val data = NextcloudNewsSyncData(
|
||||
starredIds = listOf(15, 16, 17),
|
||||
unstarredIds = listOf(18, 19, 20)
|
||||
)
|
||||
|
||||
nextcloudNewsDataSource.setItemsStarState(data)
|
||||
val starRequest = mockServer.takeRequest()
|
||||
val unstarRequest = mockServer.takeRequest()
|
||||
|
||||
val type = Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Types.newParameterizedType(List::class.java, Int::class.javaObjectType)
|
||||
)
|
||||
val adapter = moshi.adapter<Map<String, List<Int>>>(type)
|
||||
|
||||
val starBody = adapter.fromJson(starRequest.body)!!
|
||||
val unstarBody = adapter.fromJson(unstarRequest.body)!!
|
||||
|
||||
assertEquals(data.starredIds, starBody["itemIds"])
|
||||
assertEquals(data.unstarredIds, unstarBody["itemIds"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun initialSyncTest() = runTest {
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
return when {
|
||||
this == "/folders" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/valid_folder.json"))
|
||||
}
|
||||
|
||||
this == "/feeds" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/feeds.json"))
|
||||
}
|
||||
|
||||
contains("/items") -> {
|
||||
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/items.json"))
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result =
|
||||
nextcloudNewsDataSource.synchronize(SyncType.INITIAL_SYNC, NextcloudNewsSyncData())
|
||||
|
||||
with(result) {
|
||||
assertEquals(1, folders.size)
|
||||
assertEquals(3, feeds.size)
|
||||
assertEquals(2, items.size)
|
||||
assertEquals(2, starredItems.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun classicSyncTest() = runTest {
|
||||
var setItemState = 0
|
||||
val lastModified = 10L
|
||||
val ids = listOf(1, 2, 3, 4)
|
||||
|
||||
mockServer.dispatcher = object : Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
with(request.path!!) {
|
||||
// important, otherwise test fails and I don't know why
|
||||
println("request: ${request.path}")
|
||||
return when {
|
||||
this == "/folders" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/valid_folder.json"))
|
||||
}
|
||||
|
||||
this == "/feeds" -> {
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/feeds.json"))
|
||||
}
|
||||
|
||||
contains("/items/updated") -> {
|
||||
assertEquals(
|
||||
"$lastModified",
|
||||
request.requestUrl!!.queryParameter("lastModified")
|
||||
)
|
||||
MockResponse.okResponseWithBody(TestUtils.loadResource("services/nextcloudnews/adapters/items.json"))
|
||||
}
|
||||
|
||||
this.matches(Regex("/items/(read|unread|star|unstar)/multiple")) -> {
|
||||
setItemState++
|
||||
MockResponse().setResponseCode(200)
|
||||
}
|
||||
|
||||
else -> MockResponse().setResponseCode(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = nextcloudNewsDataSource.synchronize(
|
||||
SyncType.CLASSIC_SYNC,
|
||||
NextcloudNewsSyncData(
|
||||
lastModified = lastModified,
|
||||
readIds = ids,
|
||||
unreadIds = ids,
|
||||
starredIds = ids,
|
||||
unstarredIds = ids
|
||||
)
|
||||
)
|
||||
|
||||
with(result) {
|
||||
assertEquals(4, setItemState)
|
||||
assertEquals(1, folders.size)
|
||||
assertEquals(3, feeds.size)
|
||||
assertEquals(2, items.size)
|
||||
}
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class NextcloudNewsFeedsAdapterTest {
|
||||
class NextNewsFeedsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(NextcloudNewsFeedsAdapter())
|
||||
.add(NextNewsFeedsAdapter())
|
||||
.build()
|
||||
.adapter<List<Feed>>(Types.newParameterizedType(List::class.java, Feed::class.java))
|
||||
|
@ -10,10 +10,10 @@ import okio.Buffer
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class NextcloudNewsFoldersAdapterTest {
|
||||
class NextNewsFoldersAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(NextcloudNewsFoldersAdapter())
|
||||
.add(NextNewsFoldersAdapter())
|
||||
.build()
|
||||
.adapter<List<Folder>>(Types.newParameterizedType(List::class.java, Folder::class.java))
|
||||
|
@ -2,17 +2,17 @@ package com.readrops.api.services.nextcloudnews.adapters
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.util.DateUtils
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.junit.Test
|
||||
|
||||
class NextcloudNewsItemsAdapterTest {
|
||||
class NextNewsItemsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), NextcloudNewsItemsAdapter())
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), NextNewsItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
@ -21,12 +21,11 @@ class NextcloudNewsItemsAdapterTest {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/adapters/items.json")
|
||||
|
||||
val items = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(2, items.size)
|
||||
val item = items[0]
|
||||
|
||||
with(item) {
|
||||
assertEquals(remoteId, "3443")
|
||||
assertEquals(guid, "3059047a572cd9cd5d0bf645faffd077")
|
||||
assertEquals(link, "http://grulja.wordpress.com/2013/04/29/plasma-nm-after-the-solid-sprint/")
|
||||
assertEquals(title, "Plasma-nm after the solid sprint")
|
||||
assertEquals(author, "Jan Grulich (grulja)")
|
||||
@ -34,12 +33,12 @@ class NextcloudNewsItemsAdapterTest {
|
||||
assertEquals(feedRemoteId, "67")
|
||||
assertEquals(isRead, false)
|
||||
assertEquals(isStarred, false)
|
||||
assertEquals(pubDate, DateUtils.fromEpochSeconds(1367270544))
|
||||
assertEquals(imageLink, "https://test.org/image.jpg")
|
||||
assertEquals(pubDate, LocalDateTime(1367270544000))
|
||||
assertEquals(imageLink, null)
|
||||
}
|
||||
|
||||
with(items[1]) {
|
||||
assertEquals(imageLink, null)
|
||||
assertEquals(imageLink, "https://test.org/image.jpg")
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user