Skip to content

Commit

Permalink
Nostr feed support. (#945)
Browse files Browse the repository at this point in the history
* Add rust nostr library.

* Build FetchedFeed abstractions to be used for representing fetched feeds.

* Replace some occurrences of SyndFeed with FetchedFeed where needed.

* Add a function for checking a Nostr URI. Modify other occurrences in AbstractRssRepository.

* Add validation for Nostr URIs.

* Actually take Nostr URIs into account when searching for feeds.

* Move fetchFeedFrom() to a companion object instead.

* Update Nostr-sdk library and add Proguard rules for the Nostr sdk library.

* Create a singleton DI module for the Nostr Client.

* Add some fixes for summary, and others.

* Include Jetbrains Markdown library.

* Introduce htmlFromMarkdown(), and use it where needed.

* Revert to using the Nostr URI for the article link, for full content support.

* Implement full content support for Nostr articles, by reusing the content, since it's all the same anyway.

* Switch to throwing an exception when no Nostr feed info is found.

* Add feed syncing for Nostr feeds.

* Use a new Nostr client instance for each syncing feed, to avoid race conditions when using the client.

* Change syncNostrFeed return signature. Improve Nostr feed syncing. Use randomUuid for Nostr article Ids.

* Add OPML feed support for Nostr feeds. Try getting metadata first before saving the feed(else it won't modify it later.)

* Just use the author's profile name as feed title when fetching feed.

* Fix RssHelperTest.

* Fix bug caused by non-differentiation between outlines and sub-outlines when handling sub-outlines.

* Re-use original code for importing feeds from OPML(for GH runner happiness, maybe).

* Revert previous change(just uncommenting code).

* Just return feed with empty article list if nothing is found.

* Manage Nostr Client lifecycle when using it during OPML import.

* Undo changes of previous commit.
  • Loading branch information
KotlinGeekDev authored Jan 24, 2025
1 parent e0cd9ac commit ec05bdd
Show file tree
Hide file tree
Showing 16 changed files with 548 additions and 39 deletions.
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ dependencies {
implementation(libs.activity.compose)
implementation(libs.appcompat)

// Markdown
implementation(libs.jetbrains.markdown)

// Nostr
implementation(libs.rust.nostr)

// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)
Expand Down
8 changes: 8 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
# Provider API
-keep class me.ash.reader.** { *; }

# Nostr
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
-dontwarn java.awt.Component
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window

# https://github.com/flutter/flutter/issues/127388
-dontwarn org.kxml2.io.KXml**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.paging.PagingSource
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -15,7 +14,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
Expand All @@ -32,9 +30,13 @@ import me.ash.reader.domain.repository.GroupDao
import me.ash.reader.infrastructure.android.NotificationHelper
import me.ash.reader.infrastructure.preference.KeepArchivedPreference
import me.ash.reader.infrastructure.preference.SyncIntervalPreference
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.NostrFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.SyndFeedDelegate
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
import me.ash.reader.ui.ext.isNostrUri
import me.ash.reader.ui.ext.spacerDollar
import java.util.Date
import java.util.UUID
Expand Down Expand Up @@ -63,19 +65,26 @@ abstract class AbstractRssRepository(
open suspend fun clearAuthorization() {}

open suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean
) {
val accountId = context.currentAccountId
val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
name = searchedFeed.title.decodeHTML()!!,
name = with(searchedFeed.title){ if (this.isNostrUri()) this else this.decodeHTML()!!},
url = feedLink,
groupId = groupId,
accountId = accountId,
icon = searchedFeed.icon?.link
icon = searchedFeed.getIconLink()
)
val articles = searchedFeed.entries.map { rssHelper.buildArticleFromSyndEntry(feed, accountId, it) }
val articles = when(searchedFeed) {
is NostrFeed -> searchedFeed.getArticles().map {
rssHelper.buildArticleFromNostrEvent(feed, accountId, it, searchedFeed.getFeedAuthor())
}
is SyndFeedDelegate -> searchedFeed.getArticles().map {
rssHelper.buildArticleFromSyndEntry(feed, accountId, it)
}
}
feedDao.insert(feed)
articleDao.insertList(articles.map {
it.copy(feedId = feed.id)
Expand Down Expand Up @@ -178,17 +187,29 @@ abstract class AbstractRssRepository(

private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
val articles = rssHelper.queryRssXml(feed, "", preDate)
if (feed.icon == null) {
val iconLink = rssHelper.queryRssIconLink(feed.url)
if (iconLink != null) {
rssHelper.saveRssIcon(feedDao, feed, iconLink)
if (feed.url.isNostrUri()) {
val syncedFeed = rssHelper.syncNostrFeed(feed, "", preDate)
return FeedWithArticle(
feed = syncedFeed.feed
.apply { isNotification = feed.isNotification && syncedFeed.articles.isNotEmpty() },
articles = syncedFeed.articles
)
}
else {
val articles = rssHelper.queryRssXml(feed, "", preDate)
if (feed.icon == null) {
val iconLink = rssHelper.queryRssIconLink(feed.url)
if (iconLink != null) {
rssHelper.saveRssIcon(feedDao, feed, iconLink)
}
}

return FeedWithArticle(
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
articles = articles
)
}
return FeedWithArticle(
feed = feed.apply { isNotification = feed.isNotification && articles.isNotEmpty() },
articles = articles
)

}

suspend fun clearKeepArchivedArticles() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.supervisorScope
Expand All @@ -27,6 +26,7 @@ import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.di.MainDispatcher
import me.ash.reader.infrastructure.exception.FeverAPIException
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI
import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
Expand Down Expand Up @@ -86,7 +86,7 @@ class FeverRssService @Inject constructor(
}

override suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean,
) {
throw FeverAPIException("Unsupported")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import com.rometools.rome.feed.synd.SyndFeed
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.supervisorScope
Expand All @@ -25,6 +24,7 @@ import me.ash.reader.infrastructure.di.DefaultDispatcher
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.di.MainDispatcher
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.infrastructure.rss.FetchedFeed
import me.ash.reader.infrastructure.rss.RssHelper
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI
import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId
Expand Down Expand Up @@ -97,7 +97,7 @@ class GoogleReaderRssService @Inject constructor(
}

override suspend fun subscribe(
feedLink: String, searchedFeed: SyndFeed, groupId: String,
feedLink: String, searchedFeed: FetchedFeed, groupId: String,
isNotification: Boolean, isFullContent: Boolean,
) {
val accountId = context.currentAccountId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import me.ash.reader.ui.ext.del
import me.ash.reader.ui.ext.getLatestApk
import me.ash.reader.ui.ext.isGitHub
import okhttp3.OkHttpClient
import rust.nostr.sdk.Client
import javax.inject.Inject

/**
Expand Down Expand Up @@ -58,6 +59,9 @@ class AndroidApp : Application(), Configuration.Provider {
@Inject
lateinit var rssHelper: RssHelper

@Inject
lateinit var nostrClient: Client

@Inject
lateinit var notificationHelper: NotificationHelper

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.ash.reader.infrastructure.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import rust.nostr.sdk.Client
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NostrClientModule {

@Provides
@Singleton
fun provideNostrClient(): Client {
return Client()
}
}
Loading

0 comments on commit ec05bdd

Please sign in to comment.