Merge branch 'main' into custom-media-servers

merge main branch
This commit is contained in:
KotlinGeekDev
2024-06-28 23:43:46 +01:00
683 changed files with 2814 additions and 1768 deletions
+2 -2
View File
@@ -46,11 +46,11 @@ jobs:
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: Debug APK name: Debug APK
path: app/build/outputs/apk/debug/app-debug.apk path: amethyst/build/outputs/apk/debug/app-debug.apk
- name: Upload Test Results - name: Upload Test Results
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: Build Reports name: Build Reports
path: app/build/reports path: amethyst/build/reports
+16 -16
View File
@@ -32,7 +32,7 @@ jobs:
- name: Sign AAB (Google Play) - name: Sign AAB (Google Play)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/bundle/playRelease releaseDirectory: amethyst/build/outputs/bundle/playRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -44,7 +44,7 @@ jobs:
- name: Sign AAB (F-Droid) - name: Sign AAB (F-Droid)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/bundle/fdroidRelease releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -59,7 +59,7 @@ jobs:
- name: Sign APK (Google Play) - name: Sign APK (Google Play)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/apk/play/release releaseDirectory: amethyst/build/outputs/apk/play/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -71,7 +71,7 @@ jobs:
- name: Sign APK (F-Droid) - name: Sign APK (F-Droid)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/apk/fdroid/release releaseDirectory: amethyst/build/outputs/apk/fdroid/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -99,7 +99,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -110,7 +110,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -121,7 +121,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -132,7 +132,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-arm64-v8a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -143,7 +143,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-armeabi-v7a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -155,7 +155,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -166,7 +166,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -177,7 +177,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -188,7 +188,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -199,7 +199,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release-unsigned-signed.apk asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
@@ -213,7 +213,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip asset_content_type: application/zip
@@ -225,6 +225,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip asset_content_type: application/zip
+1 -1
View File
@@ -226,7 +226,7 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
- `KEY_PASSWORD` <- `<your password>` - `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>` - `KEY_STORE_PASSWORD` <- `<your key store password>`
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>` - `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
3. Change the `versionCode` and `versionName` on `app/build.gradle` 3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
4. Commit and push. 4. Commit and push.
5. Tag the commit with `v{x.x.x}` 5. Tag the commit with `v{x.x.x}`
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file. 6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.
+3 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 34 targetSdk 34
versionCode 381 versionCode 382
versionName "0.88.4" versionName "0.88.5"
buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\"" buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -165,6 +165,7 @@ dependencies {
implementation project(path: ':quartz') implementation project(path: ':quartz')
implementation project(path: ':commons') implementation project(path: ':commons')
implementation project(path: ':ammolite')
implementation libs.androidx.core.ktx implementation libs.androidx.core.ktx
implementation libs.androidx.activity.compose implementation libs.androidx.activity.compose
@@ -63,3 +63,5 @@
-keep class com.vitorpamplona.quartz.events.** { *; } -keep class com.vitorpamplona.quartz.events.** { *; }
-keep class com.vitorpamplona.amethyst.model.** { *; } -keep class com.vitorpamplona.amethyst.model.** { *; }
-keep class com.vitorpamplona.amethyst.service.** { *; } -keep class com.vitorpamplona.amethyst.service.** { *; }
-keep class com.vitorpamplona.ammolite.service.** { *; }
-keep class com.vitorpamplona.ammolite.relays.** { *; }

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@@ -70,6 +70,7 @@ class Amethyst : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder()) OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
@@ -31,17 +31,19 @@ import com.vitorpamplona.amethyst.model.DefaultReactions
import com.vitorpamplona.amethyst.model.DefaultZapAmounts import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.Settings import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
@@ -90,6 +92,8 @@ private object PrefKeys {
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList" const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer" const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_CONTACT_LIST = "latestContactList" const val LATEST_CONTACT_LIST = "latestContactList"
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later const val HIDE_NIP_17_WARNING_DIALOG = "hide_nip24_warning_dialog" // delete later
@@ -311,10 +315,33 @@ object LocalPreferences {
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
Event.mapper.writeValueAsString(account.zapPaymentRequest), Event.mapper.writeValueAsString(account.zapPaymentRequest),
) )
if (account.backupContactList != null) {
putString( putString(
PrefKeys.LATEST_CONTACT_LIST, PrefKeys.LATEST_CONTACT_LIST,
Event.mapper.writeValueAsString(account.backupContactList), Event.mapper.writeValueAsString(account.backupContactList),
) )
} else {
remove(PrefKeys.LATEST_CONTACT_LIST)
}
if (account.backupDMRelayList != null) {
putString(
PrefKeys.LATEST_DM_RELAY_LIST,
Event.mapper.writeValueAsString(account.backupDMRelayList),
)
} else {
remove(PrefKeys.LATEST_DM_RELAY_LIST)
}
if (account.backupNIP65RelayList != null) {
putString(
PrefKeys.LATEST_NIP65_RELAY_LIST,
Event.mapper.writeValueAsString(account.backupNIP65RelayList),
)
} else {
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
}
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog) putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, account.hideNIP17WarningDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
@@ -471,8 +498,8 @@ object LocalPreferences {
val latestContactList = val latestContactList =
try { try {
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
println("Decoding Contact List: " + it) if (it != "null") {
if (it != null) { println("Decoding Contact List: $it")
Event.fromJson(it) as ContactListEvent? Event.fromJson(it) as ContactListEvent?
} else { } else {
null null
@@ -488,6 +515,46 @@ object LocalPreferences {
null null
} }
val latestDmRelayList =
try {
getString(PrefKeys.LATEST_DM_RELAY_LIST, null)?.let {
if (it != "null") {
println("Decoding DM Relay List: $it")
Event.fromJson(it) as ChatMessageRelayListEvent?
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding DM Relay List ${getString(PrefKeys.LATEST_DM_RELAY_LIST, null)}",
e,
)
null
}
val latestNip65RelayList =
try {
getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)?.let {
if (it != "null") {
println("Decoding NIP65 Relay List: $it")
Event.fromJson(it) as AdvertisedRelayListEvent?
} else {
null
}
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
"LocalPreferences",
"Error Decoding NIP65 Relay List ${getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)}",
e,
)
null
}
val pendingAttestations = val pendingAttestations =
try { try {
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let { getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
@@ -502,7 +569,7 @@ object LocalPreferences {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.w( Log.w(
"LocalPreferences", "LocalPreferences",
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", "Error Decoding Contact List ${getString(PrefKeys.PENDING_ATTESTATIONS, null)}",
e, e,
) )
null null
@@ -595,6 +662,8 @@ object LocalPreferences {
hideBlockAlertDialog = hideBlockAlertDialog, hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog, hideNIP17WarningDialog = hideNIP17WarningDialog,
backupContactList = latestContactList, backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
proxy = proxy, proxy = proxy,
proxyPort = proxyPort, proxyPort = proxyPort,
showSensitiveContent = MutableStateFlow(showSensitiveContent), showSensitiveContent = MutableStateFlow(showSensitiveContent),
@@ -30,7 +30,6 @@ import coil.decode.SvgDecoder
import coil.size.Precision import coil.size.Precision
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
@@ -47,7 +46,8 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
@@ -86,6 +86,7 @@ class ServiceManager {
// Resets Proxy Use // Resets Proxy Use
HttpClientManager.setDefaultProxy(account?.proxy) HttpClientManager.setDefaultProxy(account?.proxy)
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
Coil.setImageLoader { Coil.setImageLoader {
Amethyst.instance Amethyst.instance
@@ -35,13 +35,14 @@ import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@@ -191,6 +192,8 @@ class Account(
var hideBlockAlertDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false,
var hideNIP17WarningDialog: Boolean = false, var hideNIP17WarningDialog: Boolean = false,
var backupContactList: ContactListEvent? = null, var backupContactList: ContactListEvent? = null,
var backupDMRelayList: ChatMessageRelayListEvent? = null,
var backupNIP65RelayList: AdvertisedRelayListEvent? = null,
var proxy: Proxy? = null, var proxy: Proxy? = null,
var proxyPort: Int = 9050, var proxyPort: Int = 9050,
var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null), var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
@@ -266,7 +269,15 @@ class Account(
newDMRelaySet.forEach { newUrl -> newDMRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.PRIVATE_DMS)) mappedRelaySet = mappedRelaySet +
RelaySetupInfo(
newUrl,
true,
true,
setOf(
FeedType.PRIVATE_DMS,
),
)
} }
} }
@@ -285,7 +296,15 @@ class Account(
searchRelaySet.forEach { newUrl -> searchRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, false, setOf(FeedType.SEARCH)) mappedRelaySet = mappedRelaySet +
RelaySetupInfo(
newUrl,
true,
false,
setOf(
FeedType.SEARCH,
),
)
} }
} }
@@ -304,7 +323,18 @@ class Account(
privateOutboxRelaySet.forEach { newUrl -> privateOutboxRelaySet.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS)) mappedRelaySet = mappedRelaySet +
RelaySetupInfo(
newUrl,
true,
true,
setOf(
FeedType.FOLLOWS,
FeedType.PUBLIC_CHATS,
FeedType.GLOBAL,
FeedType.PRIVATE_DMS,
),
)
} }
} }
@@ -323,7 +353,18 @@ class Account(
localRelayServers.forEach { newUrl -> localRelayServers.forEach { newUrl ->
if (mappedRelaySet.none { it.url == newUrl }) { if (mappedRelaySet.none { it.url == newUrl }) {
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS)) mappedRelaySet = mappedRelaySet +
RelaySetupInfo(
newUrl,
true,
true,
setOf(
FeedType.FOLLOWS,
FeedType.PUBLIC_CHATS,
FeedType.GLOBAL,
FeedType.PRIVATE_DMS,
),
)
} }
} }
@@ -337,7 +378,17 @@ class Account(
if (nip65setup != null) { if (nip65setup != null) {
val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
RelaySetupInfo(relay.url, true, relay.write || write, relay.feedTypes + setOf(FeedType.FOLLOWS, FeedType.GLOBAL, FeedType.PUBLIC_CHATS)) RelaySetupInfo(
relay.url,
true,
relay.write || write,
relay.feedTypes +
setOf(
FeedType.FOLLOWS,
FeedType.GLOBAL,
FeedType.PUBLIC_CHATS,
),
)
} else { } else {
relay relay
} }
@@ -347,7 +398,16 @@ class Account(
if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) { if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) {
val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.READ
mappedRelaySet = mappedRelaySet + RelaySetupInfo(newNip65Setup.relayUrl, true, write, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS)) mappedRelaySet = mappedRelaySet +
RelaySetupInfo(
newNip65Setup.relayUrl,
true,
write,
setOf(
FeedType.FOLLOWS,
FeedType.PUBLIC_CHATS,
),
)
} }
} }
@@ -999,7 +1059,7 @@ class Account(
listOf( listOf(
TypedFilter( TypedFilter(
setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL), setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL),
JsonFilter( Filter(
ids = listOf(it.id), ids = listOf(it.id),
), ),
), ),
@@ -2598,6 +2658,26 @@ class Account(
} }
} }
private fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) {
if (newDMRelayList == null || newDMRelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupDMRelayList?.id != newDMRelayList.id) {
backupDMRelayList = newDMRelayList
saveable.invalidateData()
}
}
private fun updateNIP65RelayList(newNIP65RelayList: AdvertisedRelayListEvent?) {
if (newNIP65RelayList == null || newNIP65RelayList.tags.isEmpty()) return
// Events might be different objects, we have to compare their ids.
if (backupNIP65RelayList?.id != newNIP65RelayList.id) {
backupNIP65RelayList = newNIP65RelayList
saveable.invalidateData()
}
}
// Takes a User's relay list and adds the types of feeds they are active for. // Takes a User's relay list and adds the types of feeds they are active for.
fun activeRelays(): Array<RelaySetupInfo>? { fun activeRelays(): Array<RelaySetupInfo>? {
val usersRelayList = val usersRelayList =
@@ -2931,8 +3011,32 @@ class Account(
suspend fun registerObservers() = suspend fun registerObservers() =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// saves contact list for the next time. // saves contact list for the next time.
userProfile().live().follows.observeForever { scope.launch {
GlobalScope.launch(Dispatchers.IO) { updateContactListTo(userProfile().latestContactList) } Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
userProfile().flow().follows.stateFlow.collect {
Log.d("AccountRegisterObservers", "Updating Kind 3 ${userProfile().toBestDisplayName()}")
updateContactListTo(userProfile().latestContactList)
}
}
scope.launch {
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${userProfile().toBestDisplayName()}")
(it.note.event as? ChatMessageRelayListEvent)?.let {
updateDMRelayList(it)
}
}
}
scope.launch {
Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start")
getNIP65RelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${userProfile().toBestDisplayName()}")
(it.note.event as? AdvertisedRelayListEvent)?.let {
updateNIP65RelayList(it)
}
}
} }
// imports transient blocks due to spam. // imports transient blocks due to spam.
@@ -2953,7 +3057,7 @@ class Account(
} }
init { init {
Log.d("Init", "Account") Log.d("Account", "Init")
backupContactList?.let { backupContactList?.let {
println("Loading saved contacts ${it.toJson()}") println("Loading saved contacts ${it.toJson()}")
@@ -2961,6 +3065,16 @@ class Account(
GlobalScope.launch(Dispatchers.IO) { LocalCache.consume(it) } GlobalScope.launch(Dispatchers.IO) { LocalCache.consume(it) }
} }
} }
backupDMRelayList?.let {
println("Loading DM Relay List ${it.toJson()}")
GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) }
}
backupNIP65RelayList?.let {
println("Loading saved contacts ${it.toJson()}")
GlobalScope.launch(Dispatchers.IO) { LocalCache.verifyAndConsume(it, null) }
}
} }
} }
@@ -25,9 +25,9 @@ import android.util.LruCache
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.amethyst.service.relays.RelayStats import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
@@ -25,9 +25,9 @@ import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.commons.data.LargeCache import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.ammolite.relays.BundledInsert
import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.HexValidator
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import android.util.LruCache
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@@ -29,18 +28,18 @@ import androidx.lifecycle.distinctUntilChanged
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.actions.relays.updated import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.combineWith import com.vitorpamplona.amethyst.ui.note.combineWith
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.toNote import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.BaseTextNoteEvent
@@ -1010,24 +1009,3 @@ class NoteLoadingLiveData<Y>(
@Immutable class NoteState( @Immutable class NoteState(
val note: Note, val note: Note,
) )
object RelayBriefInfoCache {
val cache = LruCache<String, RelayBriefInfo?>(50)
@Immutable
class RelayBriefInfo(
val url: String,
) {
val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern()
val favIcon: String = "https://$displayUrl/favicon.ico".intern()
}
fun get(url: String): RelayBriefInfo {
val info = cache[url]
if (info != null) return info
val newInfo = RelayBriefInfo(url)
cache.put(url, newInfo)
return newInfo
}
}
@@ -27,10 +27,10 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.relays.NostrDataSource
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.Event
abstract class AmethystNostrDataSource(debugName: String) : NostrDataSource(debugName) {
override fun consume(
event: Event,
relay: Relay,
) {
LocalCache.verifyAndConsume(event, relay)
}
override fun markAsSeenOnRelay(
eventId: String,
relay: Relay,
) {
val note = LocalCache.getNoteIfExists(eventId)
val noteEvent = note?.event
if (noteEvent is AddressableEvent) {
LocalCache.getAddressableNoteIfExists(noteEvent.address().toTag())?.addRelay(relay)
} else {
note?.addRelay(relay)
}
}
}
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.R
class HttpStatusMessages {
companion object {
fun resourceIdFor(statusCode: Int = 0): Int? =
when (statusCode) {
400 -> R.string.http_status_400
401 -> R.string.http_status_401
402 -> R.string.http_status_402
403 -> R.string.http_status_403
404 -> R.string.http_status_404
405 -> R.string.http_status_405
406 -> R.string.http_status_406
407 -> R.string.http_status_407
408 -> R.string.http_status_408
409 -> R.string.http_status_409
410 -> R.string.http_status_410
411 -> R.string.http_status_411
412 -> R.string.http_status_412
413 -> R.string.http_status_413
414 -> R.string.http_status_414
415 -> R.string.http_status_415
416 -> R.string.http_status_416
417 -> R.string.http_status_417
426 -> R.string.http_status_426
500 -> R.string.http_status_500
501 -> R.string.http_status_501
502 -> R.string.http_status_502
503 -> R.string.http_status_503
504 -> R.string.http_status_504
505 -> R.string.http_status_505
506 -> R.string.http_status_506
507 -> R.string.http_status_507
508 -> R.string.http_status_508
511 -> R.string.http_status_511
else -> null
}
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@@ -24,6 +24,7 @@ import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Request import okhttp3.Request
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
@@ -29,7 +30,10 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@@ -47,7 +51,9 @@ val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun randomChars() = List(16) { charPool.random() }.joinToString("") fun randomChars() = List(16) { charPool.random() }.joinToString("")
class Nip96Uploader(val account: Account?) { class Nip96Uploader(
val account: Account?,
) {
suspend fun uploadImage( suspend fun uploadImage(
uri: Uri, uri: Uri,
contentType: String?, contentType: String?,
@@ -57,6 +63,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96MediaServers.ServerName, server: Nip96MediaServers.ServerName,
contentResolver: ContentResolver, contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
val serverInfo = val serverInfo =
Nip96Retriever() Nip96Retriever()
@@ -73,6 +80,7 @@ class Nip96Uploader(val account: Account?) {
serverInfo, serverInfo,
contentResolver, contentResolver,
onProgress, onProgress,
context,
) )
} }
@@ -85,6 +93,7 @@ class Nip96Uploader(val account: Account?) {
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
contentResolver: ContentResolver, contentResolver: ContentResolver,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
checkNotInMainThread() checkNotInMainThread()
@@ -110,6 +119,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent, sensitiveContent,
server, server,
onProgress, onProgress,
context,
) )
} }
@@ -121,6 +131,7 @@ class Nip96Uploader(val account: Account?) {
sensitiveContent: String?, sensitiveContent: String?,
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
onProgress: (percentage: Float) -> Unit, onProgress: (percentage: Float) -> Unit,
context: Context,
): PartialEvent { ): PartialEvent {
checkNotInMainThread() checkNotInMainThread()
@@ -129,11 +140,11 @@ class Nip96Uploader(val account: Account?) {
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
val client = HttpClientManager.getHttpClient() val client = HttpClientManager.getHttpClient()
val requestBody: RequestBody
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
requestBody = val requestBody: RequestBody =
MultipartBody.Builder() MultipartBody
.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart("expiration", "") .addFormDataPart("expiration", "")
.addFormDataPart("size", length.toString()) .addFormDataPart("size", length.toString())
@@ -141,8 +152,7 @@ class Nip96Uploader(val account: Account?) {
alt?.let { body.addFormDataPart("alt", it) } alt?.let { body.addFormDataPart("alt", it) }
sensitiveContent?.let { body.addFormDataPart("content-warning", it) } sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
contentType?.let { body.addFormDataPart("content_type", it) } contentType?.let { body.addFormDataPart("content_type", it) }
} }.addFormDataPart(
.addFormDataPart(
"file", "file",
"$fileName.$extension", "$fileName.$extension",
object : RequestBody() { object : RequestBody() {
@@ -154,8 +164,7 @@ class Nip96Uploader(val account: Account?) {
inputStream.source().use(sink::writeAll) inputStream.source().use(sink::writeAll)
} }
}, },
) ).build()
.build()
nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) } nip98Header(server.apiUrl)?.let { requestBuilder.addHeader("Authorization", it) }
@@ -177,11 +186,16 @@ class Nip96Uploader(val account: Account?) {
} else if (result.status == "success" && result.nip94Event != null) { } else if (result.status == "success" && result.nip94Event != null) {
return result.nip94Event return result.nip94Event
} else { } else {
throw RuntimeException("Failed to upload with message: ${result.message}") throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, result.message))
} }
} }
} else { } else {
throw RuntimeException("Error Uploading image: ${response.code}") val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, response.code))
}
} }
} }
} }
@@ -190,6 +204,7 @@ class Nip96Uploader(val account: Account?) {
hash: String, hash: String,
contentType: String?, contentType: String?,
server: Nip96Retriever.ServerInfo, server: Nip96Retriever.ServerInfo,
context: Context,
): Boolean { ): Boolean {
val extension = val extension =
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
@@ -215,7 +230,12 @@ class Nip96Uploader(val account: Account?) {
return result.status == "success" return result.status == "success"
} }
} else { } else {
throw RuntimeException("Error Uploading image: ${response.code}") val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
}
} }
} }
} }
@@ -232,7 +252,8 @@ class Nip96Uploader(val account: Account?) {
onProgress((currentResult.percentage ?: 100) / 100f) onProgress((currentResult.percentage ?: 100) / 100f)
val request: Request = val request: Request =
Request.Builder() Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(result.processingUrl) .url(result.processingUrl)
.build() .build()
@@ -256,13 +277,12 @@ class Nip96Uploader(val account: Account?) {
} }
} }
suspend fun nip98Header(url: String): String? { suspend fun nip98Header(url: String): String? =
return withTimeoutOrNull(5000) { withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) } nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
} }
} }
}
fun nip98Header( fun nip98Header(
url: String, url: String,
@@ -23,14 +23,14 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent
@@ -70,7 +70,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
// TODO: Migrate this to a property of AccountVi // TODO: Migrate this to a property of AccountVi
object NostrAccountDataSource : NostrDataSource("AccountData") { object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
lateinit var account: Account lateinit var account: Account
var otherAccounts = listOf<HexKey>() var otherAccounts = listOf<HexKey>()
@@ -81,7 +81,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ContactListEvent.KIND), kinds = listOf(ContactListEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
limit = 1, limit = 1,
@@ -92,7 +92,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND), kinds = listOf(MetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
limit = 1, limit = 1,
@@ -103,32 +103,39 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND, PrivateOutboxRelayListEvent.KIND), kinds =
listOf(
StatusEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
SearchRelayListEvent.KIND,
PrivateOutboxRelayListEvent.KIND,
),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
limit = 10, limit = 10,
), ),
) )
fun createOtherAccountsBaseFilter(): TypedFilter? { fun createOtherAccountsBaseFilter(): TypedFilter? {
if (otherAccounts.isEmpty()) return null val otherAuthors = otherAccounts.filter { it != account.userProfile().pubkeyHex }
if (otherAuthors.isEmpty()) return null
return TypedFilter( return TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
MetadataEvent.KIND, MetadataEvent.KIND,
ContactListEvent.KIND, ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND, AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND, ChatMessageRelayListEvent.KIND,
PrivateOutboxRelayListEvent.KIND,
SearchRelayListEvent.KIND, SearchRelayListEvent.KIND,
FileServersEvent.KIND, FileServersEvent.KIND,
MuteListEvent.KIND, MuteListEvent.KIND,
PeopleListEvent.KIND, PeopleListEvent.KIND,
), ),
authors = otherAccounts.filter { it != account.userProfile().pubkeyHex }, authors = otherAuthors,
limit = 100, limit = 100,
), ),
) )
@@ -138,7 +145,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND), kinds = listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, MuteListEvent.KIND, BadgeProfilesEvent.KIND, EmojiPackSelectionEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
limit = 100, limit = 100,
@@ -149,7 +156,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(DraftEvent.KIND, ReportEvent.KIND), kinds = listOf(DraftEvent.KIND, ReportEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
since = since =
@@ -164,7 +171,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
limit = 400, limit = 400,
), ),
@@ -181,7 +188,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -214,7 +221,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
GitReplyEvent.KIND, GitReplyEvent.KIND,
@@ -236,7 +243,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(GiftWrapEvent.KIND), kinds = listOf(GiftWrapEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since = since =
@@ -316,15 +323,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
val noteEvent = note?.event as? GiftWrapEvent val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) { if (noteEvent != null) {
if (relay.brief !in note.relays) { if (relay.brief !in note.relays) {
LocalCache.justConsume(noteEvent, relay)
noteEvent.cachedGift(account.signer) { noteEvent.cachedGift(account.signer) {
LocalCache.justConsume(noteEvent, relay)
this.consume(it, relay) this.consume(it, relay)
} }
} }
} else { } else {
// new event // new event
event.cachedGift(account.signer) { this.consume(it, relay) } event.cachedGift(account.signer) {
LocalCache.justConsume(event, relay) LocalCache.justConsume(event, relay)
this.consume(it, relay)
}
} }
} }
@@ -335,15 +344,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
if (noteEvent != null) { if (noteEvent != null) {
if (relay.brief !in note.relays) { if (relay.brief !in note.relays) {
// adds the relay to seal and inner chat // adds the relay to seal and inner chat
LocalCache.consume(noteEvent, relay)
noteEvent.cachedGossip(account.signer) { noteEvent.cachedGossip(account.signer) {
LocalCache.consume(noteEvent, relay)
LocalCache.justConsume(it, relay) LocalCache.justConsume(it, relay)
} }
} }
} else { } else {
// new event // new event
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) } event.cachedGossip(account.signer) {
LocalCache.justConsume(event, relay) LocalCache.justConsume(event, relay)
LocalCache.justConsume(it, relay)
}
} }
} }
@@ -377,24 +388,21 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
super.markAsSeenOnRelay(eventId, relay) super.markAsSeenOnRelay(eventId, relay)
val note = LocalCache.getNoteIfExists(eventId) ?: return val note = LocalCache.getNoteIfExists(eventId) ?: return
val privKey = account.keyPair.privKey ?: return
val noteEvent = note.event ?: return val noteEvent = note.event ?: return
markInnerAsSeenOnRelay(noteEvent, privKey, relay) markInnerAsSeenOnRelay(noteEvent, relay)
} }
private fun markInnerAsSeenOnRelay( private fun markInnerAsSeenOnRelay(
noteEvent: EventInterface, noteEvent: EventInterface,
privKey: ByteArray,
relay: Relay, relay: Relay,
) { ) {
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay) LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
if (noteEvent is GiftWrapEvent) { if (noteEvent is GiftWrapEvent) {
noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, privKey, relay) } noteEvent.cachedGift(account.signer) { gift -> markInnerAsSeenOnRelay(gift, relay) }
} else if (noteEvent is SealedGossipEvent) { } else if (noteEvent is SealedGossipEvent) {
noteEvent.cachedGossip(account.signer) { rumor -> noteEvent.cachedGossip(account.signer) { rumor ->
markInnerAsSeenOnRelay(rumor, privKey, relay) markInnerAsSeenOnRelay(rumor, relay)
} }
} }
} }
@@ -24,13 +24,13 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
object NostrChannelDataSource : NostrDataSource("ChatroomFeed") { object NostrChannelDataSource : AmethystNostrDataSource("ChatroomFeed") {
var account: Account? = null var account: Account? = null
var channel: Channel? = null var channel: Channel? = null
@@ -58,7 +58,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelMessageEvent.KIND), kinds = listOf(ChannelMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex), authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50, limit = 50,
@@ -71,7 +71,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH),
filter = filter =
JsonFilter( Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
authors = listOf(myAccount.userProfile().pubkeyHex), authors = listOf(myAccount.userProfile().pubkeyHex),
limit = 50, limit = 50,
@@ -86,7 +86,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelMessageEvent.KIND), kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOfNotNull(channel?.idHex)), tags = mapOf("e" to listOfNotNull(channel?.idHex)),
limit = 200, limit = 200,
@@ -96,7 +96,7 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
tags = mapOf("a" to listOfNotNull(channel?.idHex)), tags = mapOf("a" to listOfNotNull(channel?.idHex)),
limit = 200, limit = 200,
@@ -113,7 +113,6 @@ object NostrChannelDataSource : NostrDataSource("ChatroomFeed") {
listOfNotNull( listOfNotNull(
createMessagesToChannelFilter(), createMessagesToChannelFilter(),
createMessagesByMeToChannelFilter(), createMessagesByMeToChannelFilter(),
) ).ifEmpty { null }
.ifEmpty { null }
} }
} }
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") { object NostrChatroomDataSource : AmethystNostrDataSource("ChatroomFeed") {
lateinit var account: Account lateinit var account: Account
private var withRoom: ChatroomKey? = null private var withRoom: ChatroomKey? = null
@@ -50,7 +50,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(PrivateDmEvent.KIND), kinds = listOf(PrivateDmEvent.KIND),
authors = myPeer.users.toList(), authors = myPeer.users.toList(),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
@@ -73,7 +73,7 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(PrivateDmEvent.KIND), kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
tags = mapOf("p" to myPeer.users.map { it }), tags = mapOf("p" to myPeer.users.map { it }),
@@ -22,16 +22,16 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") { object NostrChatroomListDataSource : AmethystNostrDataSource("MailBoxFeed") {
lateinit var account: Account lateinit var account: Account
val latestEOSEs = EOSEAccount() val latestEOSEs = EOSEAccount()
@@ -41,11 +41,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(PrivateDmEvent.KIND), kinds = listOf(PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
since = since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList, latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
), ),
) )
@@ -53,11 +56,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(PrivateDmEvent.KIND), kinds = listOf(PrivateDmEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
since = since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList, latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
), ),
) )
@@ -65,11 +71,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
since = since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList, latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
), ),
) )
@@ -82,11 +91,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
// Metadata comes from any relay // Metadata comes from any relay
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelCreateEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND),
ids = followingEvents.toList(), ids = followingEvents.toList(),
since = since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList, latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
), ),
) )
} }
@@ -101,7 +113,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
// Metadata comes from any relay // Metadata comes from any relay
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelMetadataEvent.KIND), kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to listOf(it)), tags = mapOf("e" to listOf(it)),
limit = 1, limit = 1,
@@ -119,11 +131,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelMessageEvent.KIND), kinds = listOf(ChannelMessageEvent.KIND),
tags = mapOf("e" to listOf(it)), tags = mapOf("e" to listOf(it)),
since = since =
latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList, latestEOSEs.users[account.userProfile()]
?.followList
?.get(chatRoomList)
?.relayList,
// Remember to consider spam that is being removed from the UI // Remember to consider spam that is being removed from the UI
limit = 50, limit = 50,
), ),
@@ -149,8 +164,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
list, list,
createLastChannelInfoFilter(), createLastChannelInfoFilter(),
createLastMessageOfEachChannelFilter(), createLastMessageOfEachChannelFilter(),
) ).flatten()
.flatten()
.ifEmpty { null } .ifEmpty { null }
} }
} }
@@ -21,13 +21,13 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") { object NostrCommunityDataSource : AmethystNostrDataSource("SingleCommunityFeed") {
private var communityToWatch: AddressableNote? = null private var communityToWatch: AddressableNote? = null
private fun createLoadCommunityFilter(): TypedFilter? { private fun createLoadCommunityFilter(): TypedFilter? {
@@ -38,7 +38,7 @@ object NostrCommunityDataSource : NostrDataSource("SingleCommunityFeed") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
authors = authors =
community community
.moderators() .moderators()
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
@@ -39,7 +39,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { object NostrDiscoveryDataSource : AmethystNostrDataSource("DiscoveryFeed") {
lateinit var account: Account lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope val scope = Amethyst.instance.applicationIOScope
@@ -66,15 +66,24 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createMarketplaceFilter(): List<TypedFilter> { fun createMarketplaceFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val follows =
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() account.liveDiscoveryFollowLists.value
val geohashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() ?.users
?.toList()
val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
val geohashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
return listOfNotNull( return listOfNotNull(
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
authors = follows, authors = follows,
kinds = listOf(ClassifiedsEvent.KIND), kinds = listOf(ClassifiedsEvent.KIND),
limit = 300, limit = 300,
@@ -89,7 +98,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ClassifiedsEvent.KIND), kinds = listOf(ClassifiedsEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -111,7 +120,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ClassifiedsEvent.KIND), kinds = listOf(ClassifiedsEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -132,12 +141,12 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
) )
} }
fun createNIP89Filter(kTags: List<String>): List<TypedFilter> { fun createNIP89Filter(kTags: List<String>): List<TypedFilter> =
return listOfNotNull( listOfNotNull(
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(AppDefinitionEvent.KIND), kinds = listOf(AppDefinitionEvent.KIND),
limit = 300, limit = 300,
tags = mapOf("k" to kTags), tags = mapOf("k" to kTags),
@@ -149,16 +158,18 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
), ),
), ),
) )
}
fun createLiveStreamFilter(): List<TypedFilter> { fun createLiveStreamFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return listOfNotNull( return listOfNotNull(
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
authors = follows, authors = follows,
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
limit = 300, limit = 300,
@@ -173,7 +184,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
tags = mapOf("p" to it), tags = mapOf("p" to it),
kinds = listOf(LiveActivitiesEvent.KIND), kinds = listOf(LiveActivitiesEvent.KIND),
limit = 100, limit = 100,
@@ -189,14 +200,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createPublicChatFilter(): List<TypedFilter> { fun createPublicChatFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
val followChats = account.selectedChatsFollowList().toList() val followChats = account.selectedChatsFollowList().toList()
return listOfNotNull( return listOfNotNull(
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
authors = follows, authors = follows,
kinds = listOf(ChannelMessageEvent.KIND), kinds = listOf(ChannelMessageEvent.KIND),
limit = 500, limit = 500,
@@ -211,7 +225,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
ids = followChats, ids = followChats,
kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
limit = 300, limit = 300,
@@ -229,12 +243,15 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createCommunitiesFilter(): TypedFilter { fun createCommunitiesFilter(): TypedFilter {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val follows =
account.liveDiscoveryFollowLists.value
?.users
?.toList()
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
authors = follows, authors = follows,
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
limit = 300, limit = 300,
@@ -248,14 +265,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createLiveStreamTagsFilter(): TypedFilter? { fun createLiveStreamTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -275,14 +295,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createLiveStreamGeohashesFilter(): TypedFilter? { fun createLiveStreamGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -302,14 +325,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createPublicChatsTagsFilter(): TypedFilter? { fun createPublicChatsTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND), listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags = tags =
@@ -330,14 +356,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createPublicChatsGeohashesFilter(): TypedFilter? { fun createPublicChatsGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND), listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND),
tags = tags =
@@ -358,14 +387,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createCommunitiesTagsFilter(): TypedFilter? { fun createCommunitiesTagsFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.hashtags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -385,14 +417,17 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
fun createCommunitiesGeohashesFilter(): TypedFilter? { fun createCommunitiesGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList() val hashToLoad =
account.liveDiscoveryFollowLists.value
?.geotags
?.toList()
if (hashToLoad.isNullOrEmpty()) return null if (hashToLoad.isNullOrEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -437,8 +472,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
createPublicChatsTagsFilter(), createPublicChatsTagsFilter(),
createPublicChatsGeohashesFilter(), createPublicChatsGeohashesFilter(),
), ),
) ).toList()
.toList()
.ifEmpty { null } .ifEmpty { null }
} }
} }
@@ -20,9 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") { object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
private var geohashToWatch: String? = null private var geohashToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? { fun createLoadHashtagFilter(): TypedFilter? {
@@ -43,7 +43,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
tags = tags =
mapOf( mapOf(
"g" to "g" to
@@ -20,9 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
@@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") { object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? { fun createLoadHashtagFilter(): TypedFilter? {
@@ -43,7 +43,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
tags = tags =
mapOf( mapOf(
"t" to "t" to
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
@@ -45,7 +45,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
object NostrHomeDataSource : NostrDataSource("HomeFeed") { object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
lateinit var account: Account lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope val scope = Amethyst.instance.applicationIOScope
@@ -80,7 +80,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter( return TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -116,7 +116,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.FOLLOWS), types = setOf(FeedType.FOLLOWS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -155,7 +155,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.FOLLOWS), types = setOf(FeedType.FOLLOWS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -194,7 +194,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.FOLLOWS), types = setOf(FeedType.FOLLOWS),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -238,7 +238,6 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
createFollowCommunitiesFilter(), createFollowCommunitiesFilter(),
createFollowTagsFilter(), createFollowTagsFilter(),
createFollowGeohashesFilter(), createFollowGeohashesFilter(),
) ).ifEmpty { null }
.ifEmpty { null }
} }
} }
@@ -20,11 +20,11 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
@@ -34,7 +34,7 @@ class NostrLnZapPaymentResponseDataSource(
private val toUserHex: String, private val toUserHex: String,
private val replyingToHex: String, private val replyingToHex: String,
private val authSigner: NostrSigner, private val authSigner: NostrSigner,
) : NostrDataSource("LnZapPaymentResponseFeed") { ) : AmethystNostrDataSource("LnZapPaymentResponseFeed") {
val feedTypes = setOf(FeedType.WALLET_CONNECT) val feedTypes = setOf(FeedType.WALLET_CONNECT)
private fun createWalletConnectServiceWatcher(): TypedFilter { private fun createWalletConnectServiceWatcher(): TypedFilter {
@@ -42,7 +42,7 @@ class NostrLnZapPaymentResponseDataSource(
return TypedFilter( return TypedFilter(
types = feedTypes, types = feedTypes,
filter = filter =
JsonFilter( Filter(
kinds = listOf(LnZapPaymentResponseEvent.KIND), kinds = listOf(LnZapPaymentResponseEvent.KIND),
authors = listOf(fromServiceHex), authors = listOf(fromServiceHex),
tags = tags =
@@ -20,10 +20,10 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
@@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFeed") {
private var searchString: String? = null private var searchString: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? { private fun createAnythingWithIDFilter(): List<TypedFilter>? {
@@ -95,7 +95,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND, aTag.kind), kinds = listOf(MetadataEvent.KIND, aTag.kind),
authors = listOfNotNull(aTag.pubKeyHex), authors = listOfNotNull(aTag.pubKeyHex),
// just to be sure // just to be sure
@@ -110,7 +110,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
ids = listOfNotNull(hexToWatch), ids = listOfNotNull(hexToWatch),
), ),
), ),
@@ -118,7 +118,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND), kinds = listOf(MetadataEvent.KIND),
authors = listOfNotNull(hexToWatch), authors = listOfNotNull(hexToWatch),
// just to be sure // just to be sure
@@ -135,7 +135,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.SEARCH), types = setOf(FeedType.SEARCH),
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND), kinds = listOf(MetadataEvent.KIND),
search = mySearchString, search = mySearchString,
limit = 1000, limit = 1000,
@@ -144,7 +144,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.SEARCH), types = setOf(FeedType.SEARCH),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -165,7 +165,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
TypedFilter( TypedFilter(
types = setOf(FeedType.SEARCH), types = setOf(FeedType.SEARCH),
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
ChannelMetadataEvent.KIND, ChannelMetadataEvent.KIND,
@@ -23,14 +23,14 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") { object NostrSingleChannelDataSource : AmethystNostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<Channel>() private var channelsToWatch = setOf<Channel>()
private fun createMetadataChangeFilter(): TypedFilter? { private fun createMetadataChangeFilter(): TypedFilter? {
@@ -44,7 +44,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return TypedFilter( return TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelMetadataEvent.KIND), kinds = listOf(ChannelMetadataEvent.KIND),
tags = mapOf("e" to reactionsToWatch), tags = mapOf("e" to reactionsToWatch),
), ),
@@ -65,7 +65,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return TypedFilter( return TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ChannelCreateEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND),
ids = interestedEvents.toList(), ids = interestedEvents.toList(),
), ),
@@ -88,7 +88,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)), tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex), authors = listOf(aTag.pubKeyHex),
@@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.TextNoteModificationEvent
object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { object NostrSingleEventDataSource : AmethystNostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<Note>() private var eventsToWatch = setOf<Note>()
private var addressesToWatch = setOf<Note>() private var addressesToWatch = setOf<Note>()
@@ -52,19 +52,19 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
( (
eventsToWatch.filter { it.address() != null } + eventsToWatch.filter { it.address() != null } +
addressesToWatch.filter { it.address() != null } addressesToWatch.filter { it.address() != null }
) ).toSet()
.toSet()
if (addressesToWatch.isEmpty()) { if (addressesToWatch.isEmpty()) {
return null return null
} }
return groupByEOSEPresence(addressesToWatch).map { return groupByEOSEPresence(addressesToWatch)
.map {
listOf( listOf(
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -86,7 +86,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
DeletionEvent.KIND, DeletionEvent.KIND,
@@ -114,7 +114,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
authors = listOf(aTag.pubKeyHex), authors = listOf(aTag.pubKeyHex),
limit = 5, limit = 5,
@@ -124,7 +124,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)), tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex), authors = listOf(aTag.pubKeyHex),
@@ -141,12 +141,13 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null return null
} }
return groupByEOSEPresence(eventsToWatch).map { return groupByEOSEPresence(eventsToWatch)
.map {
listOf( listOf(
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -169,7 +170,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
DeletionEvent.KIND, DeletionEvent.KIND,
@@ -190,12 +191,13 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null return null
} }
return groupByEOSEPresence(eventsToWatch).map { return groupByEOSEPresence(eventsToWatch)
.map {
listOf( listOf(
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(TextNoteEvent.KIND), kinds = listOf(TextNoteEvent.KIND),
tags = mapOf("q" to it.map { it.idHex }), tags = mapOf("q" to it.map { it.idHex }),
since = findMinimumEOSEs(it), since = findMinimumEOSEs(it),
@@ -227,7 +229,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
ids = interestedEvents.toList(), ids = interestedEvents.toList(),
), ),
), ),
@@ -304,13 +306,21 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
} }
} }
fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> { fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> =
return notes.groupBy { it.lastReactionsDownloadTime.keys.sorted().joinToString(",") }.values notes
} .groupBy {
it.lastReactionsDownloadTime.keys
.sorted()
.joinToString(",")
}.values
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> { fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
return users.groupBy { it.latestEOSEs.keys.sorted().joinToString(",") }.values users
} .groupBy {
it.latestEOSEs.keys
.sorted()
.joinToString(",")
}.values
fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> { fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> {
val minLatestEOSEs = mutableMapOf<String, EOSETime>() val minLatestEOSEs = mutableMapOf<String, EOSETime>()
@@ -21,16 +21,17 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.ammolite.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.StatusEvent
object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") { object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
private var usersToWatch = setOf<User>() private var usersToWatch = setOf<User>()
fun createUserMetadataFilter(): List<TypedFilter>? { fun createUserMetadataFilter(): List<TypedFilter>? {
@@ -44,7 +45,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND), kinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND),
authors = firstTimers, authors = firstTimers,
), ),
@@ -67,8 +68,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND), kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND),
authors = groupIds, authors = groupIds,
since = minEOSEs, since = minEOSEs,
), ),
@@ -76,7 +77,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
TypedFilter( TypedFilter(
types = EVENT_FINDER_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ReportEvent.KIND), kinds = listOf(ReportEvent.KIND),
tags = mapOf("p" to groupIds), tags = mapOf("p" to groupIds),
since = minEOSEs, since = minEOSEs,
@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") { object NostrThreadDataSource : AmethystNostrDataSource("SingleThreadFeed") {
private var eventToWatch: String? = null private var eventToWatch: String? = null
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? { fun createLoadEventsIfNotLoadedFilter(): TypedFilter? {
@@ -45,7 +45,7 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
ids = eventsToLoad.toList(), ids = eventsToLoad.toList(),
), ),
) )
@@ -21,9 +21,9 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.AppRecommendationEvent import com.vitorpamplona.quartz.events.AppRecommendationEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
@@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
var user: User? = null var user: User? = null
fun loadUserProfile(user: User?) { fun loadUserProfile(user: User?) {
@@ -55,7 +55,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(MetadataEvent.KIND), kinds = listOf(MetadataEvent.KIND),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
limit = 1, limit = 1,
@@ -68,7 +68,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf( listOf(
TextNoteEvent.KIND, TextNoteEvent.KIND,
@@ -93,7 +93,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(LnZapEvent.KIND), kinds = listOf(LnZapEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)), tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 200, limit = 200,
@@ -106,7 +106,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ContactListEvent.KIND), kinds = listOf(ContactListEvent.KIND),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
limit = 1, limit = 1,
@@ -119,7 +119,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(ContactListEvent.KIND), kinds = listOf(ContactListEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)), tags = mapOf("p" to listOf(it.pubkeyHex)),
), ),
@@ -131,7 +131,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(BadgeProfilesEvent.KIND), kinds = listOf(BadgeProfilesEvent.KIND),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
limit = 1, limit = 1,
@@ -144,7 +144,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = kinds =
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND), listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
authors = listOf(it.pubkeyHex), authors = listOf(it.pubkeyHex),
@@ -158,7 +158,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = COMMON_FEED_TYPES,
filter = filter =
JsonFilter( Filter(
kinds = listOf(BadgeAwardEvent.KIND), kinds = listOf(BadgeAwardEvent.KIND),
tags = mapOf("p" to listOf(it.pubkeyHex)), tags = mapOf("p" to listOf(it.pubkeyHex)),
limit = 20, limit = 20,
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.ammolite.relays.Filter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
object NostrVideoDataSource : NostrDataSource("VideoFeed") { object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
lateinit var account: Account lateinit var account: Account
val scope = Amethyst.instance.applicationIOScope val scope = Amethyst.instance.applicationIOScope
@@ -64,12 +64,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
} }
fun createContextualFilter(): TypedFilter { fun createContextualFilter(): TypedFilter {
val follows = account.liveStoriesFollowLists.value?.users?.toList() val follows =
account.liveStoriesFollowLists.value
?.users
?.toList()
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
authors = follows, authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200, limit = 200,
@@ -84,14 +87,17 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
} }
fun createFollowTagsFilter(): TypedFilter? { fun createFollowTagsFilter(): TypedFilter? {
val hashToLoad = account.liveStoriesFollowLists.value?.hashtags?.toList() ?: return null val hashToLoad =
account.liveStoriesFollowLists.value
?.hashtags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null if (hashToLoad.isEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -112,14 +118,17 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
} }
fun createFollowGeohashesFilter(): TypedFilter? { fun createFollowGeohashesFilter(): TypedFilter? {
val hashToLoad = account.liveStoriesFollowLists.value?.geotags?.toList() ?: return null val hashToLoad =
account.liveStoriesFollowLists.value
?.geotags
?.toList() ?: return null
if (hashToLoad.isEmpty()) return null if (hashToLoad.isEmpty()) return null
return TypedFilter( return TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( Filter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = tags =
mapOf( mapOf(
@@ -155,7 +164,6 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
createContextualFilter(), createContextualFilter(),
createFollowTagsFilter(), createFollowTagsFilter(),
createFollowGeohashesFilter(), createFollowGeohashesFilter(),
) ).ifEmpty { null }
.ifEmpty { null }
} }
} }
@@ -24,6 +24,7 @@ import android.util.Log
import android.util.LruCache import android.util.LruCache
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import okhttp3.EventListener import okhttp3.EventListener
import okhttp3.Protocol import okhttp3.Protocol
@@ -24,9 +24,9 @@ import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import okhttp3.Request import okhttp3.Request
@@ -205,38 +205,54 @@ class EventNotificationConsumer(
event: LnZapEvent, event: LnZapEvent,
acc: Account, acc: Account,
) { ) {
Log.d("EventNotificationConsumer", "Notify Start ${event.toNostrUri()}")
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
Log.d("EventNotificationConsumer", "Notify Not Notified Yet")
// old event being re-broadcast // old event being re-broadcast
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
Log.d("EventNotificationConsumer", "Notify Not an old event")
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
val noteZapped = val noteZapped =
event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
Log.d("EventNotificationConsumer", "Notify ZapRequest $noteZapRequest zapped $noteZapped")
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
Log.d("EventNotificationConsumer", "Notify Amount Bigger than 10")
if (event.isTaggedUser(acc.userProfile().pubkeyHex)) { if (event.isTaggedUser(acc.userProfile().pubkeyHex)) {
val amount = showAmount(event.amount) val amount = showAmount(event.amount)
Log.d("EventNotificationConsumer", "Notify Amount $amount")
(noteZapRequest.event as? LnZapRequestEvent)?.let { event -> (noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
acc.decryptZapContentAuthor(noteZapRequest) { acc.decryptZapContentAuthor(noteZapRequest) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Zap ${event.id}")
val author = LocalCache.getOrCreateUser(it.pubKey) val author = LocalCache.getOrCreateUser(it.pubKey)
val senderInfo = Pair(author, it.content.ifBlank { null }) val senderInfo = Pair(author, it.content.ifBlank { null })
acc.decryptContent(noteZapped) { acc.decryptContent(noteZapped) {
Log.d("EventNotificationConsumer", "Notify Decrypted if Private Note")
val zappedContent = it.split("\n").get(0) val zappedContent = it.split("\n").get(0)
val user = senderInfo.first.toBestDisplayName() val user = senderInfo.first.toBestDisplayName()
var title = var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount)
senderInfo.second?.ifBlank { null }?.let { title += " ($it)" } senderInfo.second?.ifBlank { null }?.let { title += " ($it)" }
var content = var content =
stringRes( stringRes(
applicationContext, applicationContext,
R.string.app_notification_zaps_channel_message_from, R.string.app_notification_zaps_channel_message_from,
user, user,
) )
zappedContent?.let { zappedContent.let {
content += content +=
" " + " " +
stringRes( stringRes(
@@ -247,6 +263,9 @@ class EventNotificationConsumer(
} }
val userPicture = senderInfo?.first?.profilePicture() val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications" val noteUri = "nostr:Notifications"
Log.d("EventNotificationConsumer", "Notify ${event.id} $content $title $noteUri")
notificationManager() notificationManager()
.sendZapNotification( .sendZapNotification(
event.id, event.id,
@@ -25,19 +25,35 @@ import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.RelayAuthEvent
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.coroutines.resume
class RegisterAccounts( class RegisterAccounts(
private val accounts: List<AccountInfo>, private val accounts: List<AccountInfo>,
) { ) {
private fun recursiveAuthCreation( val tag =
if (BuildConfig.FLAVOR == "play") {
"RegisterAccounts FirebaseMsgService"
} else {
"RegisterAccounts UnifiedPushService"
}
private suspend fun signAllAuths(
notificationToken: String, notificationToken: String,
remainingTos: List<Pair<Account, String>>, remainingTos: List<Pair<Account, String>>,
output: MutableList<RelayAuthEvent>, output: MutableList<RelayAuthEvent>,
@@ -48,13 +64,33 @@ class RegisterAccounts(
return return
} }
val next = remainingTos.first() coroutineScope {
val jobs =
next.first.createAuthEvent(next.second, notificationToken) { remainingTos.map { accountRelayPair ->
output.add(it) async {
recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady) val result =
withTimeoutOrNull(10000) {
suspendCancellableCoroutine { continuation ->
accountRelayPair.first.createAuthEvent(accountRelayPair.second, notificationToken) { result ->
continuation.resume(result)
} }
} }
}
if (result != null) {
output.add(result)
}
}
}
// runs in parallel to avoid overcrowding Amber.
withTimeoutOrNull(15000) {
jobs.joinAll()
}
}
onReady(output)
}
// creates proof that it controls all accounts // creates proof that it controls all accounts
private suspend fun signEventsToProveControlOfAccounts( private suspend fun signEventsToProveControlOfAccounts(
@@ -63,25 +99,48 @@ class RegisterAccounts(
onReady: (List<RelayAuthEvent>) -> Unit, onReady: (List<RelayAuthEvent>) -> Unit,
) { ) {
val readyToSend = val readyToSend =
accounts.mapNotNull { accounts
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) .map {
if (acc != null && acc.isWriteable()) { Log.d(tag, "Register Account ${it.npub}")
val readRelays =
acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()
val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null } val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
if (relayToUse != null) {
Pair(acc, relayToUse) if (acc != null && acc.isWriteable()) {
} else { val nip65Read =
null (
LocalCache
.getAddressableNoteIfExists(
AdvertisedRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
)?.event as? AdvertisedRelayListEvent
)?.readRelays() ?: acc.backupNIP65RelayList?.readRelays() ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP65 Reads ${nip65Read.joinToString(", ")}")
val nip17Read =
(
LocalCache
.getAddressableNoteIfExists(
ChatMessageRelayListEvent.createAddressTag(acc.userProfile().pubkeyHex),
)?.event as? ChatMessageRelayListEvent
)?.relays() ?: acc.backupDMRelayList?.relays() ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} NIP17 Reads ${nip17Read.joinToString(", ")}")
val kind3Relays = (acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays())
val readKind3Relays = kind3Relays?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList<String>()
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
(nip65Read + nip17Read + readKind3Relays).map {
Pair(acc, it)
} }
} else { } else {
null emptyList<Pair<Account, String>>()
}
} }
}.flatten()
val listOfAuthEvents = mutableListOf<RelayAuthEvent>() val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
recursiveAuthCreation( signAllAuths(
notificationToken, notificationToken,
readyToSend, readyToSend,
listOfAuthEvents, listOfAuthEvents,
@@ -101,7 +160,8 @@ class RegisterAccounts(
val body = jsonObject.toRequestBody(mediaType) val body = jsonObject.toRequestBody(mediaType)
val request = val request =
Request.Builder() Request
.Builder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url("https://push.amethyst.social/register") .url("https://push.amethyst.social/register")
.post(body) .post(body)
@@ -112,12 +172,6 @@ class RegisterAccounts(
val isSucess = client.newCall(request).execute().use { it.isSuccessful } val isSucess = client.newCall(request).execute().use { it.isSuccessful }
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
val tag =
if (BuildConfig.FLAVOR == "play") {
"FirebaseMsgService"
} else {
"UnifiedPushService"
}
Log.e(tag, "Unable to register with push server", e) Log.e(tag, "Unable to register with push server", e)
} }
} }
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.ots
import android.util.Log import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.BitcoinExplorer import com.vitorpamplona.quartz.ots.BitcoinExplorer
import com.vitorpamplona.quartz.ots.BlockHeader import com.vitorpamplona.quartz.ots.BlockHeader
import com.vitorpamplona.quartz.ots.exceptions.UrlException import com.vitorpamplona.quartz.ots.exceptions.UrlException
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.ots.ICalendar import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.StreamDeserializationContext import com.vitorpamplona.quartz.ots.StreamDeserializationContext
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
import com.vitorpamplona.quartz.ots.StreamDeserializationContext import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp import com.vitorpamplona.quartz.ots.Timestamp
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.playback
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.ammolite.service.HttpClientManager
/**
* HLS LiveStreams cannot use cache.
*/
@UnstableApi
class CustomMediaSourceFactory : MediaSource.Factory {
private var cachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
private var nonCachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
return this
}
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
return this
}
override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}
@@ -68,14 +68,13 @@ class MultiPlayerPlaybackManager(
private fun getCallbackIntent( private fun getCallbackIntent(
callbackUri: String, callbackUri: String,
applicationContext: Context, applicationContext: Context,
): PendingIntent { ): PendingIntent =
return PendingIntent.getActivity( PendingIntent.getActivity(
applicationContext, applicationContext,
0, 0,
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), applicationContext, MainActivity::class.java), Intent(Intent.ACTION_VIEW, callbackUri.toUri(), applicationContext, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getMediaSession( fun getMediaSession(
@@ -86,8 +85,6 @@ class MultiPlayerPlaybackManager(
applicationContext: Context, applicationContext: Context,
): MediaSession { ): MediaSession {
val existingSession = playingMap.get(id) ?: cache.get(id) val existingSession = playingMap.get(id) ?: cache.get(id)
// avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true)
if (existingSession != null) return existingSession if (existingSession != null) return existingSession
val player = val player =
@@ -111,6 +108,9 @@ class MultiPlayerPlaybackManager(
player.addListener( player.addListener(
object : Player.Listener { object : Player.Listener {
// avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true)
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) { if (isPlaying) {
player.setWakeMode(C.WAKE_MODE_NETWORK) player.setWakeMode(C.WAKE_MODE_NETWORK)
@@ -180,7 +180,5 @@ class MultiPlayerPlaybackManager(
} }
} }
fun playingContent(): Collection<MediaSession> { fun playingContent(): Collection<MediaSession> = playingMap.values
return playingMap.values
}
} }
@@ -23,51 +23,10 @@ package com.vitorpamplona.amethyst.service.playback
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.amethyst.service.HttpClientManager
/**
* HLS LiveStreams cannot use cache.
*/
@UnstableApi
class CustomMediaSourceFactory() : MediaSource.Factory {
private var cachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
private var nonCachingFactory: MediaSource.Factory =
DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
nonCachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider)
return this
}
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
cachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
nonCachingFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
return this
}
override fun getSupportedTypes(): IntArray {
return nonCachingFactory.supportedTypes
}
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (mediaItem.mediaId.contains(".m3u8", true)) {
return nonCachingFactory.createMediaSource(mediaItem)
}
return cachingFactory.createMediaSource(mediaItem)
}
}
class PlaybackService : MediaSessionService() { class PlaybackService : MediaSessionService() {
private var videoViewedPositionCache = VideoViewedPositionCache() private var videoViewedPositionCache = VideoViewedPositionCache()
@@ -75,18 +34,12 @@ class PlaybackService : MediaSessionService() {
private var managerAllInOne: MultiPlayerPlaybackManager? = null private var managerAllInOne: MultiPlayerPlaybackManager? = null
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun newAllInOneDataSource(): MediaSource.Factory {
// This might be needed for live kit.
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
return CustomMediaSourceFactory()
}
fun lazyDS(): MultiPlayerPlaybackManager { fun lazyDS(): MultiPlayerPlaybackManager {
managerAllInOne?.let { managerAllInOne?.let {
return it return it
} }
val newInstance = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache) val newInstance = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
managerAllInOne = newInstance managerAllInOne = newInstance
return newInstance return newInstance
} }
@@ -102,10 +55,11 @@ class PlaybackService : MediaSessionService() {
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated) HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
} }
@OptIn(UnstableApi::class)
private fun onProxyUpdated() { private fun onProxyUpdated() {
val toDestroyAllInOne = managerAllInOne val toDestroyAllInOne = managerAllInOne
managerAllInOne = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache) managerAllInOne = MultiPlayerPlaybackManager(CustomMediaSourceFactory(), videoViewedPositionCache)
toDestroyAllInOne?.releaseAppPlayers() toDestroyAllInOne?.releaseAppPlayers()
} }

Some files were not shown because too many files have changed in this diff Show More