Merge branch 'main' into main
This commit is contained in:
@@ -228,3 +228,8 @@ jobs:
|
|||||||
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
|
asset_path: app/build/outputs/bundle/fdroidRelease/app-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
|
||||||
|
|
||||||
|
- name: Drafts a description for the release
|
||||||
|
uses: release-drafter/release-drafter@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.10" />
|
<option name="version" value="1.9.22" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,22 +1,42 @@
|
|||||||
# Amethyst: Nostr client for Android
|
<div align="center">
|
||||||
|
|
||||||
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px"/>
|
<a href="https://amethyst.social">
|
||||||
|
<img src="./docs/design/3rd%20Logo%20-%20Zitron/amethyst.svg" alt="Amethyst Logo" title="Amethyst logo" width="80"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.
|
# Amethyst
|
||||||
|
|
||||||
|
## Nostr Client for Android
|
||||||
|
|
||||||
|
Join the social network you control.
|
||||||
|
|
||||||
|
[](https://github.com/vitorpamplona/amethyst/releases)
|
||||||
|
[](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
||||||
|
|
||||||
|
[](https://github.com/vitorpamplona/amethyst)
|
||||||
|
[](https://jitpack.io/#vitorpamplona/amethyst)
|
||||||
|
[](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml)
|
||||||
|
[](/LICENSE)
|
||||||
|
|
||||||
|
## Download and Install
|
||||||
|
|
||||||
[<img src="./docs/design/obtainium.png"
|
[<img src="./docs/design/obtainium.png"
|
||||||
alt="Get it on Obtaininum"
|
alt="Get it on Obtaininum"
|
||||||
height="80">](https://github.com/ImranR98/Obtainium)
|
height="70">](https://github.com/ImranR98/Obtainium)
|
||||||
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
|
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
|
||||||
height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
height="80">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
|
height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
|
||||||
alt="Get it on Google Play"
|
alt="Get it on Google Play"
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
||||||
|
|
||||||
# Current Features
|
</div>
|
||||||
|
|
||||||
|
## Supported Features
|
||||||
|
|
||||||
|
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
|
||||||
|
|
||||||
- [x] Events / Relay Subscriptions (NIP-01)
|
- [x] Events / Relay Subscriptions (NIP-01)
|
||||||
- [x] Follow List (NIP-02)
|
- [x] Follow List (NIP-02)
|
||||||
@@ -27,7 +47,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [ ] WebBrowser Signer (NIP-07, Not applicable)
|
- [ ] WebBrowser Signer (NIP-07, Not applicable)
|
||||||
- [x] Old-style mentions (NIP-08)
|
- [x] Old-style mentions (NIP-08)
|
||||||
- [x] Event Deletion (NIP-09)
|
- [x] Event Deletion (NIP-09)
|
||||||
- [x] Replies, mentions, Threads and Notifications (NIP-10)
|
- [x] Replies, mentions, Threads, and Notifications (NIP-10)
|
||||||
- [x] Relay Information Document (NIP-11)
|
- [x] Relay Information Document (NIP-11)
|
||||||
- [x] Generic Tag Queries (NIP-12)
|
- [x] Generic Tag Queries (NIP-12)
|
||||||
- [x] Proof of Work Display (NIP-13)
|
- [x] Proof of Work Display (NIP-13)
|
||||||
@@ -59,6 +79,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [ ] Nostr Connect (NIP-46)
|
- [ ] Nostr Connect (NIP-46)
|
||||||
- [x] Wallet Connect API (NIP-47)
|
- [x] Wallet Connect API (NIP-47)
|
||||||
- [ ] Proxy Tags (NIP-48, Not applicable)
|
- [ ] Proxy Tags (NIP-48, Not applicable)
|
||||||
|
- [x] Private key encryption for import/export (NIP-49)
|
||||||
- [x] Online Relay Search (NIP-50)
|
- [x] Online Relay Search (NIP-50)
|
||||||
- [x] Lists (NIP-51)
|
- [x] Lists (NIP-51)
|
||||||
- [ ] Calendar Events (NIP-52)
|
- [ ] Calendar Events (NIP-52)
|
||||||
@@ -69,6 +90,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [x] Zaps (NIP-57)
|
- [x] Zaps (NIP-57)
|
||||||
- [x] Private Zaps
|
- [x] Private Zaps
|
||||||
- [x] Zap Splits (NIP-57)
|
- [x] Zap Splits (NIP-57)
|
||||||
|
- [x] Gift Wraps & Seals (NIP-59)
|
||||||
- [x] Zapraiser (NIP-TBD)
|
- [x] Zapraiser (NIP-TBD)
|
||||||
- [x] Badges (NIP-58)
|
- [x] Badges (NIP-58)
|
||||||
- [ ] Relay List Metadata (NIP-65)
|
- [ ] Relay List Metadata (NIP-65)
|
||||||
@@ -79,13 +101,13 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [x] Highlights (NIP-84)
|
- [x] Highlights (NIP-84)
|
||||||
- [x] Recommended Application Handlers (NIP-89)
|
- [x] Recommended Application Handlers (NIP-89)
|
||||||
- [ ] Data Vending Machine (NIP-90)
|
- [ ] Data Vending Machine (NIP-90)
|
||||||
|
- [x] Inline Metadata (NIP-92)
|
||||||
- [x] Verifiable file URLs (NIP-94)
|
- [x] Verifiable file URLs (NIP-94)
|
||||||
- [x] Binary Blobs (NIP-95)
|
- [x] Binary Blobs (NIP-95)
|
||||||
- [x] HTTP File Storage Integration (NIP-96 Draft)
|
- [x] HTTP File Storage Integration (NIP-96 Draft)
|
||||||
- [x] HTTP Auth (NIP-98)
|
- [x] HTTP Auth (NIP-98)
|
||||||
- [x] Classifieds (NIP-99)
|
- [x] Classifieds (NIP-99)
|
||||||
- [x] Private Messages and Small Groups (NIP-24/Draft)
|
- [x] Private Messages and Small Groups (NIP-24/Draft)
|
||||||
- [x] Gift Wraps & Seals (NIP-59/Draft)
|
|
||||||
- [x] Versioned Encrypted Payloads (NIP-44/Draft)
|
- [x] Versioned Encrypted Payloads (NIP-44/Draft)
|
||||||
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
||||||
- [x] Push Notifications (Google and Unified Push)
|
- [x] Push Notifications (Google and Unified Push)
|
||||||
@@ -101,40 +123,50 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [ ] Workspaces
|
- [ ] Workspaces
|
||||||
- [ ] Infinity Scroll
|
- [ ] Infinity Scroll
|
||||||
|
|
||||||
|
## Privacy and Information Permanence
|
||||||
|
|
||||||
|
Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
|
||||||
|
|
||||||
|
If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN or Tor) from trackers online.
|
||||||
|
|
||||||
|
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
||||||
|
|
||||||
|
Information shared on Nostr can be re-broadcasted to other servers and should be assumed permanent for privacy purposes. There is no way to guarantee the deletion of any content once posted.
|
||||||
|
|
||||||
# Development Overview
|
# Development Overview
|
||||||
|
|
||||||
## Overall Architecture
|
This repository is split between Amethyst and Quartz:
|
||||||
|
- Amethyst is a native Android app made with Kotlin and Jetpack Compose.
|
||||||
|
- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients.
|
||||||
|
|
||||||
This is a native Android app made with Kotlin and Jetpack Compose.
|
The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
|
||||||
The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays.
|
|
||||||
The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays,
|
|
||||||
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
|
and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
|
||||||
|
|
||||||
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
|
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to
|
||||||
allow the UI and other parts of the app to subscribe to each individual Note/User and receive updates when they happen.
|
allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen.
|
||||||
They are also responsible for updating viewModels when needed. Filters react to changes in the screen. As the user
|
They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes
|
||||||
sees different Events, the Datasource classes are used to receive more information about those particular Events.
|
are updated to receive more information about those particular Events.
|
||||||
|
|
||||||
Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app,
|
Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app,
|
||||||
receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app.
|
receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app.
|
||||||
The UI receives a notification that objects were updated. Instances of User and Notes are mutable directly.
|
The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly.
|
||||||
There will never be two Notes with the same ID or two User instances with the same pubkey.
|
There will never be two Notes with the same ID or two User instances with the same pubkey.
|
||||||
|
|
||||||
Lastly, the user's account information (priv key/pub key) is stored in the Android KeyStore for security.
|
Lastly, the user's account information (private key/pub key) is stored in the Android KeyStore for security.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Make sure to have the following pre-requisites installed:
|
Make sure to have the following pre-requisites installed:
|
||||||
1. Java 17
|
1. Java 17+
|
||||||
2. Android Studio
|
2. Android Studio
|
||||||
3. Android 8.0+ Phone or Emulation setup
|
3. Android 8.0+ Phone or Emulation setup
|
||||||
|
|
||||||
Fork and clone this repository and import into Android Studio
|
Fork and clone this repository and import it into Android Studio
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/vitorpamplona/amethyst.git
|
git clone https://github.com/vitorpamplona/amethyst.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Use one of the Android Studio builds to install and run the app in your device or a simulator.
|
Use an Android Studio build action to install and run the app on your device or a simulator.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Build the app:
|
Build the app:
|
||||||
@@ -166,7 +198,7 @@ For the Play build:
|
|||||||
./gradlew installPlayDebug
|
./gradlew installPlayDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to Deploy
|
## Deploying
|
||||||
|
|
||||||
1. Generate a new signing key
|
1. Generate a new signing key
|
||||||
```
|
```
|
||||||
@@ -183,25 +215,37 @@ openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.j
|
|||||||
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.
|
||||||
7. Add your CHANGE LOG to the description of the new release
|
7. Add your CHANGE LOG to the description of the new release
|
||||||
8. Download the `aab` file and upload it to the` PlayStore.
|
8. Download the `aab` file and upload it to the PlayStore.
|
||||||
|
|
||||||
# Privacy on Relays & nostr
|
## Using the Quartz library
|
||||||
Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
|
|
||||||
|
|
||||||
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file
|
||||||
|
|
||||||
Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs.
|
Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories:
|
||||||
|
|
||||||
# DM Privacy #
|
```gradle
|
||||||
While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Visibility & Permanence of Your Content on nostr
|
Add the dependency
|
||||||
## Information Visibility ##
|
|
||||||
Content that you share can be shared to other relays.
|
|
||||||
Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you.
|
|
||||||
|
|
||||||
## Information Permanence ##
|
```gradle
|
||||||
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted.
|
implementation('com.github.vitorpamplona.amethyst:quartz:v0.84.3')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are very welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social)
|
||||||
|
|
||||||
|
You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac)
|
||||||
|
|
||||||
|
By contributing to this repository, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source.
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
||||||
@@ -209,11 +253,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
|
|||||||
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
|
|-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------|
|
||||||
|  |  |  |  |
|
|  |  |  |  |
|
||||||
|
|
||||||
# Contributing
|
# Contributors
|
||||||
|
|
||||||
[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome.
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
|
<a align="center" href="https://github.com/vitorpamplona/amethyst/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=vitorpamplona/amethyst" />
|
<img src="https://contrib.rocks/image?repo=vitorpamplona/amethyst" />
|
||||||
@@ -221,6 +261,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th
|
|||||||
|
|
||||||
# MIT License
|
# MIT License
|
||||||
|
|
||||||
|
<pre>
|
||||||
Copyright (c) 2023 Vitor Pamplona
|
Copyright (c) 2023 Vitor Pamplona
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -240,3 +281,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
</pre>
|
||||||
|
|||||||
+14
-10
@@ -12,8 +12,9 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 348
|
versionCode 358
|
||||||
versionName "0.83.7"
|
versionName "0.84.3"
|
||||||
|
buildConfigField "String", "RELEASE_NOTES_ID", "\"4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd\""
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -26,7 +27,7 @@ android {
|
|||||||
'cy-rGB',
|
'cy-rGB',
|
||||||
'da-rDK',
|
'da-rDK',
|
||||||
'de',
|
'de',
|
||||||
'el-rGB',
|
'el-rGR',
|
||||||
'en-rGB',
|
'en-rGB',
|
||||||
'eo',
|
'eo',
|
||||||
'es',
|
'es',
|
||||||
@@ -141,13 +142,15 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion "1.5.3"
|
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
|
||||||
|
kotlinCompilerExtensionVersion "1.5.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
resources {
|
resources {
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||||
}
|
}
|
||||||
|
exclude '**/libscrypt.dylib'
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
@@ -161,6 +164,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(path: ':quartz')
|
implementation project(path: ':quartz')
|
||||||
|
implementation project(path: ':commons')
|
||||||
implementation "androidx.core:core-ktx:$core_ktx_version"
|
implementation "androidx.core:core-ktx:$core_ktx_version"
|
||||||
implementation 'androidx.activity:activity-compose:1.8.2'
|
implementation 'androidx.activity:activity-compose:1.8.2'
|
||||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||||
@@ -181,7 +185,7 @@ dependencies {
|
|||||||
|
|
||||||
// Adaptive Layout / Two Pane
|
// Adaptive Layout / Two Pane
|
||||||
implementation "androidx.compose.material3:material3-window-size-class:${material3_version}"
|
implementation "androidx.compose.material3:material3-window-size-class:${material3_version}"
|
||||||
implementation "com.google.accompanist:accompanist-adaptive:0.32.0"
|
implementation 'com.google.accompanist:accompanist-adaptive:0.34.0'
|
||||||
|
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -191,7 +195,7 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||||
|
|
||||||
// Zoomable images
|
// Zoomable images
|
||||||
implementation 'net.engawapg.lib:zoomable:1.5.3'
|
implementation 'net.engawapg.lib:zoomable:1.6.0'
|
||||||
|
|
||||||
// Biometrics
|
// Biometrics
|
||||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
@@ -228,7 +232,7 @@ dependencies {
|
|||||||
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
||||||
|
|
||||||
// For QR generation
|
// For QR generation
|
||||||
implementation 'com.google.zxing:core:3.5.2'
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||||
|
|
||||||
// Markdown
|
// Markdown
|
||||||
@@ -251,7 +255,7 @@ dependencies {
|
|||||||
playImplementation 'com.google.mlkit:translate:17.0.2'
|
playImplementation 'com.google.mlkit:translate:17.0.2'
|
||||||
|
|
||||||
// PushNotifications
|
// PushNotifications
|
||||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
|
playImplementation platform('com.google.firebase:firebase-bom:32.7.2')
|
||||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||||
|
|
||||||
//PushNotifications(FDroid)
|
//PushNotifications(FDroid)
|
||||||
@@ -276,8 +280,8 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'io.mockk:mockk:1.13.9'
|
testImplementation 'io.mockk:mockk:1.13.9'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha02'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha02'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
||||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
|
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
|
||||||
|
|||||||
Vendored
+5
@@ -33,6 +33,11 @@
|
|||||||
# JNA For Libsodium
|
# JNA For Libsodium
|
||||||
-keep class com.goterl.lazysodium.** { *; }
|
-keep class com.goterl.lazysodium.** { *; }
|
||||||
|
|
||||||
|
# libscrypt
|
||||||
|
-keep class com.lambdaworks.codec.** { *; }
|
||||||
|
-keep class com.lambdaworks.crypto.** { *; }
|
||||||
|
-keep class com.lambdaworks.jni.** { *; }
|
||||||
|
|
||||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
||||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
-keep class com.sun.jna.ToNativeConverter { *; }
|
||||||
-keep class com.sun.jna.NativeMapped { *; }
|
-keep class com.sun.jna.NativeMapped { *; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
||||||
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
|
import com.vitorpamplona.quartz.ots.OpenTimestamps
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class OkHttpOtsTest {
|
||||||
|
val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}"
|
||||||
|
val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}"
|
||||||
|
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
|
||||||
|
|
||||||
|
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verifyNostrEvent() {
|
||||||
|
val ots = Event.fromJson(otsEvent) as OtsEvent
|
||||||
|
println(ots.info())
|
||||||
|
assertEquals(1707688818L, ots.verify())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verifyNostrEvent2() {
|
||||||
|
val ots = Event.fromJson(otsEvent2) as OtsEvent
|
||||||
|
println(ots.info())
|
||||||
|
assertEquals(1706322179L, ots.verify())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verifyNostrPendingEvent() {
|
||||||
|
val ots = Event.fromJson(otsPendingEvent) as OtsEvent
|
||||||
|
println(ots.info())
|
||||||
|
assertEquals(null, ots.verify())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createOTSEventAndVerify() {
|
||||||
|
val signer = NostrSignerInternal(KeyPair())
|
||||||
|
var ots: OtsEvent? = null
|
||||||
|
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
val otsFile = OtsEvent.stamp(otsEvent2Digest)
|
||||||
|
|
||||||
|
OtsEvent.create(otsEvent2Digest, otsFile, signer) {
|
||||||
|
ots = it
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
println(ots!!.toJson())
|
||||||
|
println(ots!!.info())
|
||||||
|
|
||||||
|
// Should not be valid because we need to wait for confirmations
|
||||||
|
assertEquals(null, ots!!.verify())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC
|
|||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -60,6 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
|
|||||||
try {
|
try {
|
||||||
parseMessage(messageStr)?.let { receiveIfNew(it) }
|
parseMessage(messageStr)?.let { receiveIfNew(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.d(TAG, "Message could not be parsed: ${e.message}")
|
Log.d(TAG, "Message could not be parsed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.AccountInfo
|
import com.vitorpamplona.amethyst.AccountInfo
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
object PushNotificationUtils {
|
object PushNotificationUtils {
|
||||||
var hasInit: Boolean = false
|
var hasInit: Boolean = false
|
||||||
@@ -36,6 +37,7 @@ object PushNotificationUtils {
|
|||||||
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
|
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
|
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="org.torproject.android"/>
|
<package android:name="org.torproject.android"/>
|
||||||
<package android:name="com.greenart7c3.nostrsigner"/>
|
<intent>
|
||||||
<package android:name="com.greenart7c3.nostrsigner.debug"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="nostrsigner" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<!-- Doesn't require a camera -->
|
<!-- Doesn't require a camera -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -26,9 +26,14 @@ import android.os.StrictMode
|
|||||||
import android.os.StrictMode.ThreadPolicy
|
import android.os.StrictMode.ThreadPolicy
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
||||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
|
import com.vitorpamplona.quartz.ots.OpenTimestamps
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -52,7 +57,7 @@ class Amethyst : Application() {
|
|||||||
newCache
|
newCache
|
||||||
}
|
}
|
||||||
|
|
||||||
private val imageCache: DiskCache by lazy {
|
val coilCache: DiskCache by lazy {
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
|
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
|
||||||
.maxSizePercent(0.2)
|
.maxSizePercent(0.2)
|
||||||
@@ -64,6 +69,8 @@ class Amethyst : Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.setThreadPolicy(
|
StrictMode.setThreadPolicy(
|
||||||
ThreadPolicy.Builder().detectAll().penaltyLog().build(),
|
ThreadPolicy.Builder().detectAll().penaltyLog().build(),
|
||||||
@@ -84,7 +91,11 @@ class Amethyst : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun imageLoaderBuilder(): ImageLoader.Builder {
|
fun imageLoaderBuilder(): ImageLoader.Builder {
|
||||||
return ImageLoader.Builder(applicationContext).diskCache { imageCache }
|
return ImageLoader.Builder(applicationContext).diskCache { coilCache }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences {
|
||||||
|
return EncryptedStorage.preferences(instance, npub)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
|
|
||||||
object EncryptedStorage {
|
class EncryptedStorage {
|
||||||
|
companion object {
|
||||||
private const val PREFERENCES_NAME = "secret_keeper"
|
private const val PREFERENCES_NAME = "secret_keeper"
|
||||||
|
|
||||||
// returns the preferences for each account or a global file if null.
|
// returns the preferences for each account or a global file if null.
|
||||||
@@ -31,21 +33,22 @@ object EncryptedStorage {
|
|||||||
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun preferences(npub: String? = null): EncryptedSharedPreferences {
|
fun preferences(
|
||||||
val context = Amethyst.instance
|
applicationContext: Context,
|
||||||
|
npub: String? = null,
|
||||||
|
): EncryptedSharedPreferences {
|
||||||
val masterKey: MasterKey =
|
val masterKey: MasterKey =
|
||||||
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val preferencesName = prefsFileName(npub)
|
|
||||||
|
|
||||||
return EncryptedSharedPreferences.create(
|
return EncryptedSharedPreferences.create(
|
||||||
context,
|
applicationContext,
|
||||||
preferencesName,
|
prefsFileName(npub),
|
||||||
masterKey,
|
masterKey,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
) as EncryptedSharedPreferences
|
) as EncryptedSharedPreferences
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -33,17 +33,18 @@ 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.Nip47URI
|
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.model.Settings
|
import com.vitorpamplona.amethyst.model.Settings
|
||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
import com.vitorpamplona.amethyst.model.ThemeType
|
||||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||||
import com.vitorpamplona.amethyst.model.parseThemeType
|
import com.vitorpamplona.amethyst.model.parseThemeType
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
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.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
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
|
||||||
@@ -53,6 +54,7 @@ import com.vitorpamplona.quartz.events.LnZapEvent
|
|||||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -113,6 +115,8 @@ private object PrefKeys {
|
|||||||
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
const val SIGNER_PACKAGE_NAME = "signer_package_name"
|
||||||
const val NEW_POST_DRAFT = "draft_new_post"
|
const val NEW_POST_DRAFT = "draft_new_post"
|
||||||
const val DRAFT_REPLY_POST = "draft_reply_post"
|
const val DRAFT_REPLY_POST = "draft_reply_post"
|
||||||
|
const val HAS_DONATED_IN_VERSION = "has_donated_in_version"
|
||||||
|
const val PENDING_ATTESTATIONS = "pending_attestations"
|
||||||
|
|
||||||
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
|
||||||
const val SHARED_SETTINGS = "shared_settings"
|
const val SHARED_SETTINGS = "shared_settings"
|
||||||
@@ -234,7 +238,7 @@ object LocalPreferences {
|
|||||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||||
} else {
|
} else {
|
||||||
return EncryptedStorage.preferences(npub)
|
return Amethyst.instance.encryptedStorage(npub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,12 +336,18 @@ object LocalPreferences {
|
|||||||
PrefKeys.LAST_READ_PER_ROUTE,
|
PrefKeys.LAST_READ_PER_ROUTE,
|
||||||
Event.mapper.writeValueAsString(account.lastReadPerRoute),
|
Event.mapper.writeValueAsString(account.lastReadPerRoute),
|
||||||
)
|
)
|
||||||
|
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
|
||||||
|
|
||||||
if (account.showSensitiveContent == null) {
|
if (account.showSensitiveContent == null) {
|
||||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||||
} else {
|
} else {
|
||||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
putString(
|
||||||
|
PrefKeys.PENDING_ATTESTATIONS,
|
||||||
|
Event.mapper.writeValueAsString(account.pendingAttestations),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
@@ -370,6 +380,7 @@ object LocalPreferences {
|
|||||||
return try {
|
return try {
|
||||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { Event.mapper.readValue<Settings>(it) }
|
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { Event.mapper.readValue<Settings>(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w(
|
Log.w(
|
||||||
"LocalPreferences",
|
"LocalPreferences",
|
||||||
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
|
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
|
||||||
@@ -544,6 +555,7 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
?: Nip96MediaServers.DEFAULT[0]
|
?: Nip96MediaServers.DEFAULT[0]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
|
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Nip96MediaServers.DEFAULT[0]
|
Nip96MediaServers.DEFAULT[0]
|
||||||
@@ -552,9 +564,10 @@ object LocalPreferences {
|
|||||||
val zapPaymentRequestServer =
|
val zapPaymentRequestServer =
|
||||||
try {
|
try {
|
||||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||||
Event.mapper.readValue<Nip47URI?>(it)
|
Event.mapper.readValue<Nip47WalletConnect.Nip47URI?>(it)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w(
|
Log.w(
|
||||||
"LocalPreferences",
|
"LocalPreferences",
|
||||||
"Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}",
|
"Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}",
|
||||||
@@ -575,6 +588,27 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.w(
|
||||||
|
"LocalPreferences",
|
||||||
|
"Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingAttestations =
|
||||||
|
try {
|
||||||
|
getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let {
|
||||||
|
println("Decoding Attestation List: " + it)
|
||||||
|
if (it != null) {
|
||||||
|
Event.mapper.readValue<Map<HexKey, String>>(it)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
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.LATEST_CONTACT_LIST, null)}",
|
||||||
@@ -590,6 +624,7 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
?: mapOf()
|
?: mapOf()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w(
|
Log.w(
|
||||||
"LocalPreferences",
|
"LocalPreferences",
|
||||||
"Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}",
|
"Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}",
|
||||||
@@ -604,7 +639,7 @@ object LocalPreferences {
|
|||||||
val hideNIP24WarningDialog = getBoolean(PrefKeys.HIDE_NIP_24_WARNING_DIALOG, false)
|
val hideNIP24WarningDialog = getBoolean(PrefKeys.HIDE_NIP_24_WARNING_DIALOG, false)
|
||||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||||
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
||||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||||
|
|
||||||
val showSensitiveContent =
|
val showSensitiveContent =
|
||||||
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||||
@@ -622,6 +657,7 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
?: mapOf()
|
?: mapOf()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w(
|
Log.w(
|
||||||
"LocalPreferences",
|
"LocalPreferences",
|
||||||
"Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}",
|
"Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}",
|
||||||
@@ -644,6 +680,8 @@ object LocalPreferences {
|
|||||||
NostrSignerInternal(keyPair)
|
NostrSignerInternal(keyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
Account(
|
Account(
|
||||||
keyPair = keyPair,
|
keyPair = keyPair,
|
||||||
@@ -671,6 +709,8 @@ object LocalPreferences {
|
|||||||
warnAboutPostsWithReports = warnAboutReports,
|
warnAboutPostsWithReports = warnAboutReports,
|
||||||
filterSpamFromStrangers = filterSpam,
|
filterSpamFromStrangers = filterSpam,
|
||||||
lastReadPerRoute = lastReadPerRoute,
|
lastReadPerRoute = lastReadPerRoute,
|
||||||
|
hasDonatedInVersion = hasDonatedInVersion,
|
||||||
|
pendingAttestations = pendingAttestations ?: emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Loads from DB
|
// Loads from DB
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -30,7 +30,7 @@ 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.HttpClient
|
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
|
||||||
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.service.relays.Client
|
|||||||
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
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -83,7 +84,7 @@ class ServiceManager {
|
|||||||
val myAccount = account
|
val myAccount = account
|
||||||
|
|
||||||
// Resets Proxy Use
|
// Resets Proxy Use
|
||||||
HttpClient.start(account?.proxy)
|
HttpClientManager.setDefaultProxy(account?.proxy)
|
||||||
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true
|
||||||
Coil.setImageLoader {
|
Coil.setImageLoader {
|
||||||
Amethyst.instance
|
Amethyst.instance
|
||||||
@@ -96,7 +97,7 @@ class ServiceManager {
|
|||||||
}
|
}
|
||||||
add(SvgDecoder.Factory())
|
add(SvgDecoder.Factory())
|
||||||
} // .logger(DebugLogger())
|
} // .logger(DebugLogger())
|
||||||
.okHttpClient { HttpClient.getHttpClient() }
|
.okHttpClient { HttpClientManager.getHttpClient() }
|
||||||
.precision(Precision.INEXACT)
|
.precision(Precision.INEXACT)
|
||||||
.respectCacheHeaders(false)
|
.respectCacheHeaders(false)
|
||||||
.build()
|
.build()
|
||||||
@@ -126,6 +127,7 @@ class ServiceManager {
|
|||||||
try {
|
try {
|
||||||
it.npub.bechToBytes().toHexKey()
|
it.npub.bechToBytes().toHexKey()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -31,6 +31,7 @@ import androidx.lifecycle.asLiveData
|
|||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
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
|
||||||
@@ -43,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
|||||||
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
|
||||||
|
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.events.BookmarkListEvent
|
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||||
@@ -74,6 +76,7 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
|||||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.NIP24Factory
|
import com.vitorpamplona.quartz.events.NIP24Factory
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.Price
|
import com.vitorpamplona.quartz.events.Price
|
||||||
@@ -96,6 +99,7 @@ import kotlinx.collections.immutable.ImmutableSet
|
|||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -170,7 +174,7 @@ class Account(
|
|||||||
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
var defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
var defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
var defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
var defaultDiscoveryFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
var zapPaymentRequest: Nip47URI? = null,
|
var zapPaymentRequest: Nip47WalletConnect.Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
var hideNIP24WarningDialog: Boolean = false,
|
var hideNIP24WarningDialog: Boolean = false,
|
||||||
@@ -181,10 +185,10 @@ class Account(
|
|||||||
var warnAboutPostsWithReports: Boolean = true,
|
var warnAboutPostsWithReports: Boolean = true,
|
||||||
var filterSpamFromStrangers: Boolean = true,
|
var filterSpamFromStrangers: Boolean = true,
|
||||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
||||||
|
var hasDonatedInVersion: Set<String> = setOf<String>(),
|
||||||
|
var pendingAttestations: Map<HexKey, String> = mapOf<HexKey, String>(),
|
||||||
|
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
||||||
) {
|
) {
|
||||||
// Uses a single scope for the entire application.
|
|
||||||
val scope = Amethyst.instance.applicationIOScope
|
|
||||||
|
|
||||||
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
|
||||||
|
|
||||||
data class PaymentRequest(
|
data class PaymentRequest(
|
||||||
@@ -727,7 +731,7 @@ class Account(
|
|||||||
zapResponseEvent.response(signer, onReady)
|
zapResponseEvent.response(signer, onReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateIfNoteWasZappedByAccount(
|
suspend fun calculateIfNoteWasZappedByAccount(
|
||||||
zappedNote: Note?,
|
zappedNote: Note?,
|
||||||
onWasZapped: () -> Unit,
|
onWasZapped: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -891,6 +895,38 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateAttestations() {
|
||||||
|
Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations")
|
||||||
|
|
||||||
|
pendingAttestations.toMap().forEach { pair ->
|
||||||
|
val newAttestation = OtsEvent.upgrade(pair.value, pair.key)
|
||||||
|
|
||||||
|
if (pair.value != newAttestation) {
|
||||||
|
OtsEvent.create(pair.key, newAttestation, signer) {
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
Client.send(it)
|
||||||
|
|
||||||
|
pendingAttestations = pendingAttestations - pair.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPendingAttestations(note: Note): Boolean {
|
||||||
|
val id = note.event?.id() ?: note.idHex
|
||||||
|
return pendingAttestations.get(id) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun timestamp(note: Note) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val id = note.event?.id() ?: note.idHex
|
||||||
|
|
||||||
|
pendingAttestations = pendingAttestations + Pair(id, OtsEvent.stamp(id))
|
||||||
|
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun follow(user: User) {
|
fun follow(user: User) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1166,7 +1202,7 @@ class Account(
|
|||||||
Client.send(signedEvent, relayList = relayList)
|
Client.send(signedEvent, relayList = relayList)
|
||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
|
|
||||||
return LocalCache.notes[signedEvent.id]
|
return LocalCache.getNoteIfExists(signedEvent.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumeNip95(
|
fun consumeNip95(
|
||||||
@@ -1176,7 +1212,7 @@ class Account(
|
|||||||
LocalCache.consume(data, null)
|
LocalCache.consume(data, null)
|
||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
|
|
||||||
return LocalCache.notes[signedEvent.id]
|
return LocalCache.getNoteIfExists(signedEvent.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendNip95(
|
fun sendNip95(
|
||||||
@@ -1196,7 +1232,7 @@ class Account(
|
|||||||
Client.send(signedEvent, relayList = relayList)
|
Client.send(signedEvent, relayList = relayList)
|
||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
|
|
||||||
LocalCache.notes[signedEvent.id]?.let { onReady(it) }
|
LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createHeader(
|
fun createHeader(
|
||||||
@@ -1323,9 +1359,10 @@ class Account(
|
|||||||
replyingTo: String?,
|
replyingTo: String?,
|
||||||
root: String?,
|
root: String?,
|
||||||
directMentions: Set<HexKey>,
|
directMentions: Set<HexKey>,
|
||||||
|
forkedFrom: Event?,
|
||||||
relayList: List<Relay>? = null,
|
relayList: List<Relay>? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
nip94attachments: List<Event>? = null,
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1347,6 +1384,7 @@ class Account(
|
|||||||
directMentions = directMentions,
|
directMentions = directMentions,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
nip94attachments = nip94attachments,
|
nip94attachments = nip94attachments,
|
||||||
|
forkedFrom = forkedFrom,
|
||||||
signer = signer,
|
signer = signer,
|
||||||
) {
|
) {
|
||||||
Client.send(it, relayList = relayList)
|
Client.send(it, relayList = relayList)
|
||||||
@@ -1381,7 +1419,7 @@ class Account(
|
|||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
relayList: List<Relay>? = null,
|
relayList: List<Relay>? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
nip94attachments: List<Event>? = null,
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1428,7 +1466,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
nip94attachments: List<Event>? = null,
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1461,7 +1499,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
nip94attachments: List<Event>? = null,
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1495,6 +1533,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
sendPrivateMessage(
|
sendPrivateMessage(
|
||||||
message,
|
message,
|
||||||
@@ -1505,6 +1544,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive,
|
wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount,
|
zapRaiserAmount,
|
||||||
geohash,
|
geohash,
|
||||||
|
nip94attachments,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1517,6 +1557,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1533,6 +1574,7 @@ class Account(
|
|||||||
markAsSensitive = wantsToMarkAsSensitive,
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer,
|
signer = signer,
|
||||||
advertiseNip18 = false,
|
advertiseNip18 = false,
|
||||||
) {
|
) {
|
||||||
@@ -1551,6 +1593,7 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@@ -1567,6 +1610,7 @@ class Account(
|
|||||||
markAsSensitive = wantsToMarkAsSensitive,
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer,
|
signer = signer,
|
||||||
) {
|
) {
|
||||||
broadcastPrivately(it)
|
broadcastPrivately(it)
|
||||||
@@ -2031,7 +2075,7 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeZapPaymentRequest(newServer: Nip47URI?) {
|
fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?) {
|
||||||
zapPaymentRequest = newServer
|
zapPaymentRequest = newServer
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
@@ -2348,6 +2392,16 @@ class Account(
|
|||||||
return lastReadPerRoute[route] ?: 0
|
return lastReadPerRoute[route] ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasDonatedInThisVersion(): Boolean {
|
||||||
|
return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markDonatedInThisVersion() {
|
||||||
|
hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME
|
||||||
|
saveable.invalidateData()
|
||||||
|
live.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19
|
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class AntiSpamFilter {
|
|||||||
// really long, make it ok.
|
// really long, make it ok.
|
||||||
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still
|
// The idea here is to avoid considering repeated "@Bot, command" messages spam, while still
|
||||||
// blocking repeated "lnbc..." invoices or fishing urls
|
// blocking repeated "lnbc..." invoices or fishing urls
|
||||||
if (event.content.length < 180 && Nip19.nip19regex.matcher(event.content).find()) return false
|
if (event.content.length < 180 && Nip19Bech32.nip19regex.matcher(event.content).find()) return false
|
||||||
|
|
||||||
// double list strategy:
|
// double list strategy:
|
||||||
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
|
// if duplicated, it goes into spam. 1000 spam messages are saved into the spam list.
|
||||||
@@ -71,7 +71,7 @@ class AntiSpamFilter {
|
|||||||
) {
|
) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"Potential SPAM Message for sharing",
|
"Potential SPAM Message for sharing",
|
||||||
"${Nip19.createNEvent(event.id, event.pubKey, event.kind, null)}",
|
"${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, null)}",
|
||||||
)
|
)
|
||||||
Log.w(
|
Log.w(
|
||||||
"Potential SPAM Message",
|
"Potential SPAM Message",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -32,6 +32,7 @@ fun checkForHashtagWithIcon(
|
|||||||
primary: Color,
|
primary: Color,
|
||||||
): HashtagIcon? {
|
): HashtagIcon? {
|
||||||
return when (tag.lowercase()) {
|
return when (tag.lowercase()) {
|
||||||
|
"₿itcoin",
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"btc",
|
"btc",
|
||||||
"timechain",
|
"timechain",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -27,12 +27,10 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
import com.vitorpamplona.quartz.encoders.ATag
|
import com.vitorpamplona.quartz.encoders.ATag
|
||||||
import com.vitorpamplona.quartz.encoders.Hex
|
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19
|
import com.vitorpamplona.quartz.encoders.decodeEventIdAsHexOrNull
|
||||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
|
||||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
@@ -50,6 +48,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
|||||||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelHideMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelHideMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.ChannelListEvent
|
||||||
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.ChannelMuteUserEvent
|
import com.vitorpamplona.quartz.events.ChannelMuteUserEvent
|
||||||
@@ -57,18 +56,24 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommunityListEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import com.vitorpamplona.quartz.events.FhirResourceEvent
|
||||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.FileServersEvent
|
import com.vitorpamplona.quartz.events.FileServersEvent
|
||||||
import com.vitorpamplona.quartz.events.FileStorageEvent
|
import com.vitorpamplona.quartz.events.FileStorageEvent
|
||||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||||
|
import com.vitorpamplona.quartz.events.GitIssueEvent
|
||||||
|
import com.vitorpamplona.quartz.events.GitPatchEvent
|
||||||
|
import com.vitorpamplona.quartz.events.GitReplyEvent
|
||||||
|
import com.vitorpamplona.quartz.events.GitRepositoryEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||||
@@ -80,6 +85,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.MetadataEvent
|
import com.vitorpamplona.quartz.events.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.NNSEvent
|
import com.vitorpamplona.quartz.events.NNSEvent
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
import com.vitorpamplona.quartz.events.PinListEvent
|
import com.vitorpamplona.quartz.events.PinListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
@@ -94,10 +100,13 @@ import com.vitorpamplona.quartz.events.StatusEvent
|
|||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||||
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -109,18 +118,31 @@ import java.time.Instant
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
object LocalCache {
|
object LocalCache {
|
||||||
val antiSpam = AntiSpamFilter()
|
val antiSpam = AntiSpamFilter()
|
||||||
|
|
||||||
val users = ConcurrentHashMap<HexKey, User>(5000)
|
private val users = ConcurrentHashMap<HexKey, User>(5000)
|
||||||
val notes = ConcurrentHashMap<HexKey, Note>(5000)
|
private val notes = ConcurrentHashMap<HexKey, Note>(5000)
|
||||||
val channels = ConcurrentHashMap<HexKey, Channel>()
|
val channels = ConcurrentHashMap<HexKey, Channel>()
|
||||||
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
|
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
|
||||||
|
|
||||||
val awaitingPaymentRequests =
|
val awaitingPaymentRequests =
|
||||||
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||||
|
|
||||||
|
var noteListCache: List<Note> = emptyList()
|
||||||
|
var userListCache: List<User> = emptyList()
|
||||||
|
|
||||||
|
fun updateListCache() {
|
||||||
|
val (value, elapsed) =
|
||||||
|
measureTimedValue {
|
||||||
|
noteListCache = ArrayList(notes.values)
|
||||||
|
userListCache = ArrayList(users.values)
|
||||||
|
}
|
||||||
|
Log.d("LocalCache", "UpdateListCache $elapsed")
|
||||||
|
}
|
||||||
|
|
||||||
fun checkGetOrCreateUser(key: String): User? {
|
fun checkGetOrCreateUser(key: String): User? {
|
||||||
// checkNotInMainThread()
|
// checkNotInMainThread()
|
||||||
|
|
||||||
@@ -357,6 +379,87 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: GitPatchEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
|
relay?.let { it.spamCounter++ }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: GitIssueEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
|
relay?.let { it.spamCounter++ }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: GitReplyEvent,
|
||||||
|
relay: Relay? = null,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
|
relay?.let { it.spamCounter++ }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyTo =
|
||||||
|
event
|
||||||
|
.tagsWithoutCitations()
|
||||||
|
.filter { it != event.repository()?.toTag() }
|
||||||
|
.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: LongTextNoteEvent,
|
event: LongTextNoteEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -392,6 +495,41 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: WikiNoteEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
version.moveAllReferencesTo(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
|
relay?.let { it.spamCounter++ }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, replyTo)
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: PollNoteEvent,
|
event: PollNoteEvent,
|
||||||
relay: Relay? = null,
|
relay: Relay? = null,
|
||||||
@@ -462,6 +600,27 @@ object LocalCache {
|
|||||||
consumeBaseReplaceable(event, relay)
|
consumeBaseReplaceable(event, relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: CommunityListEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
consumeBaseReplaceable(event, relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: GitRepositoryEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
consumeBaseReplaceable(event, relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: ChannelListEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
consumeBaseReplaceable(event, relay)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: FileServersEvent,
|
event: FileServersEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -571,6 +730,28 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: OtsEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
val version = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (version.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
// makes sure the OTS has a valid certificate
|
||||||
|
if (event.cacheVerify() == null) return // no valid OTS
|
||||||
|
|
||||||
|
if (version.event == null) {
|
||||||
|
version.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
version.liveSet?.innerOts?.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers(version)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: BadgeDefinitionEvent,
|
event: BadgeDefinitionEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -742,7 +923,7 @@ object LocalCache {
|
|||||||
|
|
||||||
event
|
event
|
||||||
.deleteEvents()
|
.deleteEvents()
|
||||||
.mapNotNull { notes[it] }
|
.mapNotNull { getNoteIfExists(it) }
|
||||||
.forEach { deleteNote ->
|
.forEach { deleteNote ->
|
||||||
// must be the same author
|
// must be the same author
|
||||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||||
@@ -1183,6 +1364,26 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: FhirResourceEvent,
|
||||||
|
relay: Relay?,
|
||||||
|
) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: HighlightEvent,
|
event: HighlightEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -1379,11 +1580,14 @@ object LocalCache {
|
|||||||
|
|
||||||
val key = decodePublicKeyAsHexOrNull(username)
|
val key = decodePublicKeyAsHexOrNull(username)
|
||||||
|
|
||||||
if (key != null && users[key] != null) {
|
if (key != null) {
|
||||||
return listOfNotNull(users[key])
|
val user = getUserIfExists(key)
|
||||||
|
if (user != null) {
|
||||||
|
return listOfNotNull(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return users.values.filter {
|
return userListCache.filter {
|
||||||
(it.anyNameStartsWith(username)) ||
|
(it.anyNameStartsWith(username)) ||
|
||||||
it.pubkeyHex.startsWith(username, true) ||
|
it.pubkeyHex.startsWith(username, true) ||
|
||||||
it.pubkeyNpub().startsWith(username, true)
|
it.pubkeyNpub().startsWith(username, true)
|
||||||
@@ -1393,18 +1597,16 @@ object LocalCache {
|
|||||||
fun findNotesStartingWith(text: String): List<Note> {
|
fun findNotesStartingWith(text: String): List<Note> {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val key =
|
val key = decodeEventIdAsHexOrNull(text)
|
||||||
try {
|
|
||||||
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
|
if (key != null) {
|
||||||
} catch (e: Exception) {
|
val note = getNoteIfExists(key)
|
||||||
null
|
if (note != null) {
|
||||||
|
return listOfNotNull(note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key != null && (notes[key] ?: addressables[key]) != null) {
|
return noteListCache.filter {
|
||||||
return listOfNotNull(notes[key] ?: addressables[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return notes.values.filter {
|
|
||||||
(
|
(
|
||||||
it.event !is GenericRepostEvent &&
|
it.event !is GenericRepostEvent &&
|
||||||
it.event !is RepostEvent &&
|
it.event !is RepostEvent &&
|
||||||
@@ -1442,13 +1644,7 @@ object LocalCache {
|
|||||||
fun findChannelsStartingWith(text: String): List<Channel> {
|
fun findChannelsStartingWith(text: String): List<Channel> {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val key =
|
val key = decodeEventIdAsHexOrNull(text)
|
||||||
try {
|
|
||||||
Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key != null && channels[key] != null) {
|
if (key != null && channels[key] != null) {
|
||||||
return listOfNotNull(channels[key])
|
return listOfNotNull(channels[key])
|
||||||
}
|
}
|
||||||
@@ -1479,12 +1675,32 @@ object LocalCache {
|
|||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findEarliestOtsForNote(note: Note): Long? {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
var minTime: Long? = null
|
||||||
|
val time = TimeUtils.now()
|
||||||
|
|
||||||
|
noteListCache.forEach { item ->
|
||||||
|
val noteEvent = item.event
|
||||||
|
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
||||||
|
noteEvent.verifiedTime?.let { stampedTime ->
|
||||||
|
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||||
|
minTime = stampedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minTime
|
||||||
|
}
|
||||||
|
|
||||||
fun cleanObservers() {
|
fun cleanObservers() {
|
||||||
notes.forEach { it.value.clearLive() }
|
noteListCache.forEach { it.clearLive() }
|
||||||
|
|
||||||
addressables.forEach { it.value.clearLive() }
|
addressables.forEach { it.value.clearLive() }
|
||||||
|
|
||||||
users.forEach { it.value.clearLive() }
|
userListCache.forEach { it.clearLive() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pruneOldAndHiddenMessages(account: Account) {
|
fun pruneOldAndHiddenMessages(account: Account) {
|
||||||
@@ -1510,8 +1726,8 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
users.forEach { userPair ->
|
userListCache.forEach { userPair ->
|
||||||
userPair.value.privateChatrooms.values.map {
|
userPair.privateChatrooms.values.map {
|
||||||
val toBeRemoved = it.pruneMessagesToTheLatestOnly()
|
val toBeRemoved = it.pruneMessagesToTheLatestOnly()
|
||||||
|
|
||||||
val childrenToBeRemoved = mutableListOf<Note>()
|
val childrenToBeRemoved = mutableListOf<Note>()
|
||||||
@@ -1526,7 +1742,7 @@ object LocalCache {
|
|||||||
|
|
||||||
if (toBeRemoved.size > 1) {
|
if (toBeRemoved.size > 1) {
|
||||||
println(
|
println(
|
||||||
"PRUNE: ${toBeRemoved.size} private messages with ${userPair.value.toBestDisplayName()} removed. ${it.roomMessages.size} kept",
|
"PRUNE: ${toBeRemoved.size} private messages with ${userPair.toBestDisplayName()} removed. ${it.roomMessages.size} kept",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1535,9 +1751,9 @@ object LocalCache {
|
|||||||
|
|
||||||
fun prunePastVersionsOfReplaceables() {
|
fun prunePastVersionsOfReplaceables() {
|
||||||
val toBeRemoved =
|
val toBeRemoved =
|
||||||
notes
|
noteListCache
|
||||||
.filter {
|
.filter {
|
||||||
val noteEvent = it.value.event
|
val noteEvent = it.event
|
||||||
if (noteEvent is AddressableEvent) {
|
if (noteEvent is AddressableEvent) {
|
||||||
noteEvent.createdAt() <
|
noteEvent.createdAt() <
|
||||||
(addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0)
|
(addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0)
|
||||||
@@ -1545,7 +1761,6 @@ object LocalCache {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.values
|
|
||||||
|
|
||||||
val childrenToBeRemoved = mutableListOf<Note>()
|
val childrenToBeRemoved = mutableListOf<Note>()
|
||||||
|
|
||||||
@@ -1570,24 +1785,23 @@ object LocalCache {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val toBeRemoved =
|
val toBeRemoved =
|
||||||
notes
|
noteListCache
|
||||||
.filter {
|
.filter {
|
||||||
(
|
(
|
||||||
(it.value.event is TextNoteEvent && !it.value.isNewThread()) ||
|
(it.event is TextNoteEvent && !it.isNewThread()) ||
|
||||||
it.value.event is ReactionEvent ||
|
it.event is ReactionEvent ||
|
||||||
it.value.event is LnZapEvent ||
|
it.event is LnZapEvent ||
|
||||||
it.value.event is LnZapRequestEvent ||
|
it.event is LnZapRequestEvent ||
|
||||||
it.value.event is ReportEvent ||
|
it.event is ReportEvent ||
|
||||||
it.value.event is GenericRepostEvent
|
it.event is GenericRepostEvent
|
||||||
) &&
|
) &&
|
||||||
it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
|
it.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
|
||||||
it.value.liveSet?.isInUse() != true && // don't delete if observing.
|
it.liveSet?.isInUse() != true && // don't delete if observing.
|
||||||
it.value.author?.pubkeyHex !in
|
it.author?.pubkeyHex !in
|
||||||
accounts && // don't delete if it is the logged in account
|
accounts && // don't delete if it is the logged in account
|
||||||
it.value.event?.isTaggedUsers(accounts) !=
|
it.event?.isTaggedUsers(accounts) !=
|
||||||
true // don't delete if it's a notification to the logged in user
|
true // don't delete if it's a notification to the logged in user
|
||||||
}
|
}
|
||||||
.values
|
|
||||||
|
|
||||||
val childrenToBeRemoved = mutableListOf<Note>()
|
val childrenToBeRemoved = mutableListOf<Note>()
|
||||||
|
|
||||||
@@ -1653,7 +1867,8 @@ object LocalCache {
|
|||||||
fun pruneExpiredEvents() {
|
fun pruneExpiredEvents() {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
val toBeRemoved = notes.filter { it.value.event?.isExpired() == true }.values
|
val now = TimeUtils.now()
|
||||||
|
val toBeRemoved = noteListCache.filter { it.event?.isExpirationBefore(now) == true }
|
||||||
|
|
||||||
val childrenToBeRemoved = mutableListOf<Note>()
|
val childrenToBeRemoved = mutableListOf<Note>()
|
||||||
|
|
||||||
@@ -1679,7 +1894,7 @@ object LocalCache {
|
|||||||
?.hiddenUsers
|
?.hiddenUsers
|
||||||
?.map { userHex ->
|
?.map { userHex ->
|
||||||
(
|
(
|
||||||
notes.values.filter { it.event?.pubKey() == userHex } +
|
noteListCache.filter { it.event?.pubKey() == userHex } +
|
||||||
addressables.values.filter { it.event?.pubKey() == userHex }
|
addressables.values.filter { it.event?.pubKey() == userHex }
|
||||||
)
|
)
|
||||||
.toSet()
|
.toSet()
|
||||||
@@ -1701,7 +1916,7 @@ object LocalCache {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
var removingContactList = 0
|
var removingContactList = 0
|
||||||
users.values.forEach {
|
userListCache.forEach {
|
||||||
if (
|
if (
|
||||||
it.pubkeyHex !in loggedIn &&
|
it.pubkeyHex !in loggedIn &&
|
||||||
(it.liveSet == null || it.liveSet?.isInUse() == false) &&
|
(it.liveSet == null || it.liveSet?.isInUse() == false) &&
|
||||||
@@ -1738,6 +1953,7 @@ object LocalCache {
|
|||||||
try {
|
try {
|
||||||
event.checkSignature()
|
event.checkSignature()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.w("Event failed retest ${event.kind}", (e.message ?: "") + event.toJson())
|
Log.w("Event failed retest ${event.kind}", (e.message ?: "") + event.toJson())
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -1768,6 +1984,7 @@ object LocalCache {
|
|||||||
is CalendarTimeSlotEvent -> consume(event, relay)
|
is CalendarTimeSlotEvent -> consume(event, relay)
|
||||||
is CalendarRSVPEvent -> consume(event, relay)
|
is CalendarRSVPEvent -> consume(event, relay)
|
||||||
is ChannelCreateEvent -> consume(event)
|
is ChannelCreateEvent -> consume(event)
|
||||||
|
is ChannelListEvent -> consume(event, relay)
|
||||||
is ChannelHideMessageEvent -> consume(event)
|
is ChannelHideMessageEvent -> consume(event)
|
||||||
is ChannelMessageEvent -> consume(event, relay)
|
is ChannelMessageEvent -> consume(event, relay)
|
||||||
is ChannelMetadataEvent -> consume(event)
|
is ChannelMetadataEvent -> consume(event)
|
||||||
@@ -1775,6 +1992,7 @@ object LocalCache {
|
|||||||
is ChatMessageEvent -> consume(event, relay)
|
is ChatMessageEvent -> consume(event, relay)
|
||||||
is ClassifiedsEvent -> consume(event, relay)
|
is ClassifiedsEvent -> consume(event, relay)
|
||||||
is CommunityDefinitionEvent -> consume(event, relay)
|
is CommunityDefinitionEvent -> consume(event, relay)
|
||||||
|
is CommunityListEvent -> consume(event, relay)
|
||||||
is CommunityPostApprovalEvent -> {
|
is CommunityPostApprovalEvent -> {
|
||||||
event.containedPost()?.let { verifyAndConsume(it, relay) }
|
event.containedPost()?.let { verifyAndConsume(it, relay) }
|
||||||
consume(event)
|
consume(event)
|
||||||
@@ -1784,11 +2002,16 @@ object LocalCache {
|
|||||||
is EmojiPackEvent -> consume(event, relay)
|
is EmojiPackEvent -> consume(event, relay)
|
||||||
is EmojiPackSelectionEvent -> consume(event, relay)
|
is EmojiPackSelectionEvent -> consume(event, relay)
|
||||||
is SealedGossipEvent -> consume(event, relay)
|
is SealedGossipEvent -> consume(event, relay)
|
||||||
|
is FhirResourceEvent -> consume(event, relay)
|
||||||
is FileHeaderEvent -> consume(event, relay)
|
is FileHeaderEvent -> consume(event, relay)
|
||||||
is FileServersEvent -> consume(event, relay)
|
is FileServersEvent -> consume(event, relay)
|
||||||
is FileStorageEvent -> consume(event, relay)
|
is FileStorageEvent -> consume(event, relay)
|
||||||
is FileStorageHeaderEvent -> consume(event, relay)
|
is FileStorageHeaderEvent -> consume(event, relay)
|
||||||
is GiftWrapEvent -> consume(event, relay)
|
is GiftWrapEvent -> consume(event, relay)
|
||||||
|
is GitIssueEvent -> consume(event, relay)
|
||||||
|
is GitReplyEvent -> consume(event, relay)
|
||||||
|
is GitPatchEvent -> consume(event, relay)
|
||||||
|
is GitRepositoryEvent -> consume(event, relay)
|
||||||
is HighlightEvent -> consume(event, relay)
|
is HighlightEvent -> consume(event, relay)
|
||||||
is LiveActivitiesEvent -> consume(event, relay)
|
is LiveActivitiesEvent -> consume(event, relay)
|
||||||
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
is LiveActivitiesChatMessageEvent -> consume(event, relay)
|
||||||
@@ -1806,6 +2029,7 @@ object LocalCache {
|
|||||||
is MetadataEvent -> consume(event)
|
is MetadataEvent -> consume(event)
|
||||||
is MuteListEvent -> consume(event, relay)
|
is MuteListEvent -> consume(event, relay)
|
||||||
is NNSEvent -> comsume(event, relay)
|
is NNSEvent -> comsume(event, relay)
|
||||||
|
is OtsEvent -> consume(event, relay)
|
||||||
is PrivateDmEvent -> consume(event, relay)
|
is PrivateDmEvent -> consume(event, relay)
|
||||||
is PinListEvent -> consume(event, relay)
|
is PinListEvent -> consume(event, relay)
|
||||||
is PeopleListEvent -> consume(event, relay)
|
is PeopleListEvent -> consume(event, relay)
|
||||||
@@ -1826,11 +2050,13 @@ object LocalCache {
|
|||||||
is TextNoteEvent -> consume(event, relay)
|
is TextNoteEvent -> consume(event, relay)
|
||||||
is VideoHorizontalEvent -> consume(event, relay)
|
is VideoHorizontalEvent -> consume(event, relay)
|
||||||
is VideoVerticalEvent -> consume(event, relay)
|
is VideoVerticalEvent -> consume(event, relay)
|
||||||
|
is WikiNoteEvent -> consume(event, relay)
|
||||||
else -> {
|
else -> {
|
||||||
Log.w("Event Not Supported", event.toJson())
|
Log.w("Event Not Supported", event.toJson())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1845,6 +2071,10 @@ class LocalCacheLiveData {
|
|||||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
|
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
|
||||||
|
|
||||||
fun invalidateData(newNote: Note) {
|
fun invalidateData(newNote: Note) {
|
||||||
bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) }
|
bundler.invalidateList(newNote) {
|
||||||
|
bundledNewNotes ->
|
||||||
|
_newEventBundles.emit(bundledNewNotes)
|
||||||
|
LocalCache.updateListCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -39,7 +39,7 @@ 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.Nip19
|
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||||
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
|
||||||
@@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
|||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||||
|
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
|
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
|
||||||
import com.vitorpamplona.quartz.events.RepostEvent
|
import com.vitorpamplona.quartz.events.RepostEvent
|
||||||
@@ -63,12 +64,16 @@ import com.vitorpamplona.quartz.events.WrappedEvent
|
|||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.containsAny
|
import com.vitorpamplona.quartz.utils.containsAny
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||||
@@ -137,17 +142,17 @@ open class Note(val idHex: String) {
|
|||||||
return if (myEvent is WrappedEvent) {
|
return if (myEvent is WrappedEvent) {
|
||||||
val host = myEvent.host
|
val host = myEvent.host
|
||||||
if (host != null) {
|
if (host != null) {
|
||||||
Nip19.createNEvent(
|
Nip19Bech32.createNEvent(
|
||||||
host.id,
|
host.id,
|
||||||
host.pubKey,
|
host.pubKey,
|
||||||
host.kind(),
|
host.kind(),
|
||||||
relays.firstOrNull()?.url,
|
relays.firstOrNull()?.url,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +525,7 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursiveIsZappedByCalculation(
|
private suspend fun isZappedByCalculation(
|
||||||
option: Int?,
|
option: Int?,
|
||||||
user: User,
|
user: User,
|
||||||
account: Account,
|
account: Account,
|
||||||
@@ -531,48 +536,65 @@ open class Note(val idHex: String) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val next = remainingZapEvents.first()
|
remainingZapEvents.forEach { next ->
|
||||||
|
val zapRequest = next.first.event as LnZapRequestEvent
|
||||||
|
val zapEvent = next.second?.event as? LnZapEvent
|
||||||
|
|
||||||
if (next.first.author?.pubkeyHex == user.pubkeyHex) {
|
if (!zapRequest.isPrivateZap()) {
|
||||||
|
// public events
|
||||||
|
if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||||
onWasZappedByAuthor()
|
onWasZappedByAuthor()
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
account.decryptZapContentAuthor(next.first) {
|
// private events
|
||||||
if (
|
|
||||||
it.pubKey == user.pubkeyHex &&
|
// if has already decrypted
|
||||||
(option == null || option == (it as? LnZapEvent)?.zappedPollOption())
|
val privateZap = zapRequest.cachedPrivateZap()
|
||||||
) {
|
if (privateZap != null) {
|
||||||
|
if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||||
onWasZappedByAuthor()
|
onWasZappedByAuthor()
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
recursiveIsZappedByCalculation(
|
if (account.isWriteable()) {
|
||||||
option,
|
val result =
|
||||||
user,
|
withTimeoutOrNull(1000) {
|
||||||
account,
|
suspendCancellableCoroutine { continuation ->
|
||||||
remainingZapEvents.minus(next),
|
zapRequest.decryptPrivateZap(account.signer) {
|
||||||
onWasZappedByAuthor,
|
continuation.resume(it)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) {
|
||||||
|
onWasZappedByAuthor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isZappedBy(
|
suspend fun isZappedBy(
|
||||||
user: User,
|
user: User,
|
||||||
account: Account,
|
account: Account,
|
||||||
onWasZappedByAuthor: () -> Unit,
|
onWasZappedByAuthor: () -> Unit,
|
||||||
) {
|
) {
|
||||||
recursiveIsZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
|
isZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||||
if (account.userProfile() == user) {
|
if (account.userProfile() == user) {
|
||||||
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isZappedBy(
|
suspend fun isZappedBy(
|
||||||
option: Int?,
|
option: Int?,
|
||||||
user: User,
|
user: User,
|
||||||
account: Account,
|
account: Account,
|
||||||
onWasZappedByAuthor: () -> Unit,
|
onWasZappedByAuthor: () -> Unit,
|
||||||
) {
|
) {
|
||||||
recursiveIsZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
|
isZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getReactionBy(user: User): String? {
|
fun getReactionBy(user: User): String? {
|
||||||
@@ -614,7 +636,7 @@ open class Note(val idHex: String) {
|
|||||||
|
|
||||||
// Regular Zap Receipts
|
// Regular Zap Receipts
|
||||||
zaps.forEach {
|
zaps.forEach {
|
||||||
val noteEvent = it?.value?.event
|
val noteEvent = it.value?.event
|
||||||
if (noteEvent is LnZapEvent) {
|
if (noteEvent is LnZapEvent) {
|
||||||
sumOfAmounts += noteEvent.amount ?: BigDecimal.ZERO
|
sumOfAmounts += noteEvent.amount ?: BigDecimal.ZERO
|
||||||
}
|
}
|
||||||
@@ -644,6 +666,7 @@ open class Note(val idHex: String) {
|
|||||||
try {
|
try {
|
||||||
LnInvoiceUtil.getAmountInSats(invoice)
|
LnInvoiceUtil.getAmountInSats(invoice)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,6 +717,7 @@ open class Note(val idHex: String) {
|
|||||||
try {
|
try {
|
||||||
BigDecimal(it.event?.content())
|
BigDecimal(it.event?.content())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
// do nothing if it can't convert to bigdecimal
|
// do nothing if it can't convert to bigdecimal
|
||||||
}
|
}
|
||||||
@@ -709,6 +733,7 @@ open class Note(val idHex: String) {
|
|||||||
try {
|
try {
|
||||||
BigDecimal(it.event?.content())
|
BigDecimal(it.event?.content())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
// do nothing if it can't convert to bigdecimal
|
// do nothing if it can't convert to bigdecimal
|
||||||
}
|
}
|
||||||
@@ -922,6 +947,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
val innerReports = NoteBundledRefresherLiveData(u)
|
val innerReports = NoteBundledRefresherLiveData(u)
|
||||||
val innerRelays = NoteBundledRefresherLiveData(u)
|
val innerRelays = NoteBundledRefresherLiveData(u)
|
||||||
val innerZaps = NoteBundledRefresherLiveData(u)
|
val innerZaps = NoteBundledRefresherLiveData(u)
|
||||||
|
val innerOts = NoteBundledRefresherLiveData(u)
|
||||||
|
|
||||||
val metadata = innerMetadata.map { it }
|
val metadata = innerMetadata.map { it }
|
||||||
val reactions = innerReactions.map { it }
|
val reactions = innerReactions.map { it }
|
||||||
@@ -986,6 +1012,7 @@ class NoteLiveSet(u: Note) {
|
|||||||
innerReports.destroy()
|
innerReports.destroy()
|
||||||
innerRelays.destroy()
|
innerRelays.destroy()
|
||||||
innerZaps.destroy()
|
innerZaps.destroy()
|
||||||
|
innerOts.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -139,10 +139,10 @@ class User(val pubkeyHex: String) {
|
|||||||
// Update Followers of the past user list
|
// Update Followers of the past user list
|
||||||
// Update Followers of the new contact list
|
// Update Followers of the new contact list
|
||||||
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
||||||
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
|
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||||
}
|
}
|
||||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||||
LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData()
|
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
liveSet?.innerRelays?.invalidateData()
|
liveSet?.innerRelays?.invalidateData()
|
||||||
@@ -363,7 +363,7 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun transientFollowerCount(): Int {
|
suspend fun transientFollowerCount(): Int {
|
||||||
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedFollowingKeySet(): Set<HexKey> {
|
fun cachedFollowingKeySet(): Set<HexKey> {
|
||||||
@@ -387,7 +387,7 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun cachedFollowerCount(): Int {
|
suspend fun cachedFollowerCount(): Int {
|
||||||
return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -20,40 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import android.util.Patterns
|
import com.vitorpamplona.amethyst.commons.RichTextParser
|
||||||
import androidx.compose.runtime.Immutable
|
import com.vitorpamplona.amethyst.commons.RichTextViewerState
|
||||||
import com.linkedin.urls.detection.UrlDetector
|
|
||||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlContent
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.tagIndex
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.videoExtensions
|
|
||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class RichTextViewerState(
|
|
||||||
val urlSet: ImmutableSet<String>,
|
|
||||||
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
|
|
||||||
val imageList: ImmutableList<ZoomableUrlContent>,
|
|
||||||
val customEmoji: ImmutableMap<String, String>,
|
|
||||||
val paragraphs: ImmutableList<ParagraphState>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ParagraphState(val words: ImmutableList<Segment>, val isRTL: Boolean)
|
|
||||||
|
|
||||||
object CachedRichTextParser {
|
object CachedRichTextParser {
|
||||||
val richTextCache = LruCache<String, RichTextViewerState>(200)
|
val richTextCache = LruCache<String, RichTextViewerState>(200)
|
||||||
@@ -71,288 +41,3 @@ object CachedRichTextParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group 1 = url, group 4 additional chars
|
|
||||||
// val noProtocolUrlValidator =
|
|
||||||
// Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
|
|
||||||
|
|
||||||
// Android9 seems to have an issue starting this regex.
|
|
||||||
val noProtocolUrlValidator =
|
|
||||||
try {
|
|
||||||
Pattern.compile(
|
|
||||||
"(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)",
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Pattern.compile(
|
|
||||||
"(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val HTTPRegex =
|
|
||||||
"^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?"
|
|
||||||
.toRegex(RegexOption.IGNORE_CASE)
|
|
||||||
|
|
||||||
class RichTextParser() {
|
|
||||||
fun parseMediaUrl(fullUrl: String): ZoomableUrlContent? {
|
|
||||||
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
|
|
||||||
return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
|
||||||
val frags = Nip44UrlParser().parse(fullUrl)
|
|
||||||
ZoomableUrlImage(
|
|
||||||
url = fullUrl,
|
|
||||||
description = frags["alt"],
|
|
||||||
hash = frags["x"],
|
|
||||||
blurhash = frags["blurhash"],
|
|
||||||
dim = frags["dim"],
|
|
||||||
)
|
|
||||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
|
||||||
val frags = Nip44UrlParser().parse(fullUrl)
|
|
||||||
ZoomableUrlVideo(
|
|
||||||
url = fullUrl,
|
|
||||||
description = frags["alt"],
|
|
||||||
hash = frags["x"],
|
|
||||||
blurhash = frags["blurhash"],
|
|
||||||
dim = frags["dim"],
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseText(
|
|
||||||
content: String,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
): RichTextViewerState {
|
|
||||||
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
|
|
||||||
|
|
||||||
val urlSet =
|
|
||||||
urls.mapNotNullTo(LinkedHashSet(urls.size)) {
|
|
||||||
// removes e-mails
|
|
||||||
if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) {
|
|
||||||
null
|
|
||||||
} else if (isNumber(it.originalUrl)) {
|
|
||||||
null
|
|
||||||
} else if (it.originalUrl.contains("。")) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
if (HTTPRegex.matches(it.originalUrl)) {
|
|
||||||
it.originalUrl
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val imagesForPager =
|
|
||||||
urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl) }.associateBy { it.url }
|
|
||||||
val imageList = imagesForPager.values.toList()
|
|
||||||
|
|
||||||
val emojiMap =
|
|
||||||
tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
|
|
||||||
|
|
||||||
val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags)
|
|
||||||
|
|
||||||
return RichTextViewerState(
|
|
||||||
urlSet.toImmutableSet(),
|
|
||||||
imagesForPager.toImmutableMap(),
|
|
||||||
imageList.toImmutableList(),
|
|
||||||
emojiMap.toImmutableMap(),
|
|
||||||
segments,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findTextSegments(
|
|
||||||
content: String,
|
|
||||||
images: Set<String>,
|
|
||||||
urls: Set<String>,
|
|
||||||
emojis: Map<String, String>,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
): ImmutableList<ParagraphState> {
|
|
||||||
var paragraphSegments = persistentListOf<ParagraphState>()
|
|
||||||
|
|
||||||
content.split('\n').forEach { paragraph ->
|
|
||||||
var segments = persistentListOf<Segment>()
|
|
||||||
var isDirty = false
|
|
||||||
|
|
||||||
val isRTL = isArabic(paragraph)
|
|
||||||
|
|
||||||
val wordList = paragraph.trimEnd().split(' ')
|
|
||||||
wordList.forEach { word ->
|
|
||||||
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
|
|
||||||
if (wordSegment !is RegularTextSegment) {
|
|
||||||
isDirty = true
|
|
||||||
}
|
|
||||||
segments = segments.add(wordSegment)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newSegments =
|
|
||||||
if (isDirty) {
|
|
||||||
ParagraphState(segments, isRTL)
|
|
||||||
} else {
|
|
||||||
ParagraphState(persistentListOf<Segment>(RegularTextSegment(paragraph)), isRTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
paragraphSegments = paragraphSegments.add(newSegments)
|
|
||||||
}
|
|
||||||
|
|
||||||
return paragraphSegments
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNumber(word: String): Boolean {
|
|
||||||
return numberPattern.matcher(word).matches()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDate(word: String): Boolean {
|
|
||||||
return shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isArabic(text: String): Boolean {
|
|
||||||
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun wordIdentifier(
|
|
||||||
word: String,
|
|
||||||
images: Set<String>,
|
|
||||||
urls: Set<String>,
|
|
||||||
emojis: Map<String, String>,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
): Segment {
|
|
||||||
val emailMatcher = Patterns.EMAIL_ADDRESS.matcher(word)
|
|
||||||
val phoneMatcher = Patterns.PHONE.matcher(word)
|
|
||||||
val schemelessMatcher = noProtocolUrlValidator.matcher(word)
|
|
||||||
|
|
||||||
return if (word.isEmpty()) {
|
|
||||||
RegularTextSegment(word)
|
|
||||||
} else if (images.contains(word)) {
|
|
||||||
ImageSegment(word)
|
|
||||||
} else if (urls.contains(word)) {
|
|
||||||
LinkSegment(word)
|
|
||||||
} else if (emojis.any { word.contains(it.key) }) {
|
|
||||||
EmojiSegment(word)
|
|
||||||
} else if (word.startsWith("lnbc", true)) {
|
|
||||||
InvoiceSegment(word)
|
|
||||||
} else if (word.startsWith("lnurl", true)) {
|
|
||||||
WithdrawSegment(word)
|
|
||||||
} else if (word.startsWith("cashuA", true)) {
|
|
||||||
CashuSegment(word)
|
|
||||||
} else if (emailMatcher.matches()) {
|
|
||||||
EmailSegment(word)
|
|
||||||
} else if (word.length in 7..14 && !isDate(word) && phoneMatcher.matches()) {
|
|
||||||
PhoneSegment(word)
|
|
||||||
} else if (startsWithNIP19Scheme(word)) {
|
|
||||||
BechSegment(word)
|
|
||||||
} else if (word.startsWith("#")) {
|
|
||||||
parseHash(word, tags)
|
|
||||||
} else if (word.contains(".") && schemelessMatcher.find()) {
|
|
||||||
val url = schemelessMatcher.group(1) // url
|
|
||||||
val additionalChars = schemelessMatcher.group(4) // additional chars
|
|
||||||
val pattern =
|
|
||||||
"""^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?"""
|
|
||||||
.toRegex(RegexOption.IGNORE_CASE)
|
|
||||||
if (pattern.find(word) != null) {
|
|
||||||
SchemelessUrlSegment(word, url, additionalChars)
|
|
||||||
} else {
|
|
||||||
RegularTextSegment(word)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RegularTextSegment(word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseHash(
|
|
||||||
word: String,
|
|
||||||
tags: ImmutableListOfLists<String>,
|
|
||||||
): Segment {
|
|
||||||
// First #[n]
|
|
||||||
|
|
||||||
val matcher = tagIndex.matcher(word)
|
|
||||||
try {
|
|
||||||
if (matcher.find()) {
|
|
||||||
val index = matcher.group(1)?.toInt()
|
|
||||||
val suffix = matcher.group(2)
|
|
||||||
|
|
||||||
if (index != null && index >= 0 && index < tags.lists.size) {
|
|
||||||
val tag = tags.lists[index]
|
|
||||||
|
|
||||||
if (tag.size > 1) {
|
|
||||||
if (tag[0] == "p") {
|
|
||||||
return HashIndexUserSegment(word, tag[1], suffix)
|
|
||||||
} else if (tag[0] == "e" || tag[0] == "a") {
|
|
||||||
return HashIndexEventSegment(word, tag[1], suffix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("Tag Parser", "Couldn't link tag $word", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second #Amethyst
|
|
||||||
val hashtagMatcher = hashTagsPattern.matcher(word)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hashtagMatcher.find()) {
|
|
||||||
val hashtag = hashtagMatcher.group(1)
|
|
||||||
if (hashtag != null) {
|
|
||||||
return HashTagSegment(word, hashtag, hashtagMatcher.group(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return RegularTextSegment(word)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$")
|
|
||||||
val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$")
|
|
||||||
val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable open class Segment(val segmentText: String)
|
|
||||||
|
|
||||||
@Immutable class ImageSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class LinkSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class EmojiSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class InvoiceSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class WithdrawSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class CashuSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class EmailSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class PhoneSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class BechSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
open class HashIndexSegment(segment: String, val hex: String, val extras: String?) :
|
|
||||||
Segment(segment)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
class HashIndexUserSegment(segment: String, hex: String, extras: String?) :
|
|
||||||
HashIndexSegment(segment, hex, extras)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
class HashIndexEventSegment(segment: String, hex: String, extras: String?) :
|
|
||||||
HashIndexSegment(segment, hex, extras)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
class HashTagSegment(segment: String, val hashtag: String, val extras: String?) : Segment(segment)
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
class SchemelessUrlSegment(segment: String, val url: String, val extras: String?) :
|
|
||||||
Segment(segment)
|
|
||||||
|
|
||||||
@Immutable class RegularTextSegment(segment: String) : Segment(segment)
|
|
||||||
|
|
||||||
fun startsWithNIP19Scheme(word: String): Boolean {
|
|
||||||
val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@")
|
|
||||||
|
|
||||||
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -32,6 +32,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CashuToken(
|
data class CashuToken(
|
||||||
@@ -59,6 +60,7 @@ class CashuProcessor {
|
|||||||
|
|
||||||
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
|
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
|
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +121,7 @@ class CashuProcessor {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
|
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
|
||||||
|
|
||||||
val factory = Event.mapper.nodeFactory
|
val factory = Event.mapper.nodeFactory
|
||||||
@@ -154,6 +156,7 @@ class CashuProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
onError(
|
onError(
|
||||||
context.getString(R.string.cashu_successful_redemption),
|
context.getString(R.string.cashu_successful_redemption),
|
||||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||||
@@ -170,7 +173,7 @@ class CashuProcessor {
|
|||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
||||||
|
|
||||||
val factory = Event.mapper.nodeFactory
|
val factory = Event.mapper.nodeFactory
|
||||||
@@ -211,6 +214,7 @@ class CashuProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
onError(
|
onError(
|
||||||
context.getString(R.string.cashu_successful_redemption),
|
context.getString(R.string.cashu_successful_redemption),
|
||||||
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
|||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import io.trbl.blurhash.BlurHash
|
import io.trbl.blurhash.BlurHash
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class FileHeader(
|
|||||||
onError(null)
|
onError(null)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
||||||
onError(e.message)
|
onError(e.message)
|
||||||
}
|
}
|
||||||
@@ -174,6 +176,7 @@ class FileHeader(
|
|||||||
|
|
||||||
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||||
onError(e.message)
|
onError(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-26
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -32,14 +32,13 @@ import java.net.Proxy
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
object HttpClient {
|
object HttpClientManager {
|
||||||
val DEFAULT_TIMEOUT_ON_WIFI = Duration.ofSeconds(10L)
|
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
|
||||||
val DEFAULT_TIMEOUT_ON_MOBILE = Duration.ofSeconds(30L)
|
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
|
||||||
|
|
||||||
var proxyChangeListeners = ArrayList<() -> Unit>()
|
var proxyChangeListeners = ArrayList<() -> Unit>()
|
||||||
var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
||||||
|
private var defaultHttpClient: OkHttpClient? = null
|
||||||
var defaultHttpClient: OkHttpClient? = null
|
|
||||||
|
|
||||||
// fires off every time value of the property changes
|
// fires off every time value of the property changes
|
||||||
private var internalProxy: Proxy? by
|
private var internalProxy: Proxy? by
|
||||||
@@ -49,26 +48,34 @@ object HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(proxy: Proxy?) {
|
fun setDefaultProxy(proxy: Proxy?) {
|
||||||
if (internalProxy != proxy) {
|
if (internalProxy != proxy) {
|
||||||
|
Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
|
||||||
this.internalProxy = proxy
|
this.internalProxy = proxy
|
||||||
this.defaultHttpClient = getHttpClient()
|
|
||||||
|
// recreates singleton
|
||||||
|
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeTimeouts(timeout: Duration) {
|
fun setDefaultTimeout(timeout: Duration) {
|
||||||
Log.d("HttpClient", "Changing timeout to: $timeout")
|
Log.d("HttpClient", "Changing timeout to: $timeout")
|
||||||
if (this.defaultTimeout.seconds != timeout.seconds) {
|
if (this.defaultTimeout.seconds != timeout.seconds) {
|
||||||
this.defaultTimeout = timeout
|
this.defaultTimeout = timeout
|
||||||
this.defaultHttpClient = getHttpClient()
|
|
||||||
|
// recreates singleton
|
||||||
|
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHttpClient(timeout: Duration): OkHttpClient {
|
private fun buildHttpClient(
|
||||||
val seconds = if (internalProxy != null) timeout.seconds * 2 else timeout.seconds
|
proxy: Proxy?,
|
||||||
|
timeout: Duration,
|
||||||
|
): OkHttpClient {
|
||||||
|
val seconds = if (proxy != null) timeout.seconds * 2 else timeout.seconds
|
||||||
val duration = Duration.ofSeconds(seconds)
|
val duration = Duration.ofSeconds(seconds)
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
.proxy(internalProxy)
|
.proxy(proxy)
|
||||||
.readTimeout(duration)
|
.readTimeout(duration)
|
||||||
.connectTimeout(duration)
|
.connectTimeout(duration)
|
||||||
.writeTimeout(duration)
|
.writeTimeout(duration)
|
||||||
@@ -91,24 +98,13 @@ object HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHttpClientForRelays(): OkHttpClient {
|
|
||||||
if (this.defaultHttpClient == null) {
|
|
||||||
this.defaultHttpClient = getHttpClient(defaultTimeout)
|
|
||||||
}
|
|
||||||
return defaultHttpClient!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHttpClient(): OkHttpClient {
|
fun getHttpClient(): OkHttpClient {
|
||||||
if (this.defaultHttpClient == null) {
|
if (this.defaultHttpClient == null) {
|
||||||
this.defaultHttpClient = getHttpClient(defaultTimeout)
|
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||||
}
|
}
|
||||||
return defaultHttpClient!!
|
return defaultHttpClient!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getProxy(): Proxy? {
|
|
||||||
return internalProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initProxy(
|
fun initProxy(
|
||||||
useProxy: Boolean,
|
useProxy: Boolean,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -29,6 +29,7 @@ import android.location.LocationManager
|
|||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class LocationUtil(context: Context) {
|
class LocationUtil(context: Context) {
|
||||||
@@ -105,6 +106,7 @@ class ReverseGeoLocationUtil {
|
|||||||
.joinToString(", ")
|
.joinToString(", ")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -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 kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
@@ -64,7 +65,7 @@ class Nip05NostrAddressVerifier() {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClient.getHttpClient()
|
HttpClientManager.getHttpClient()
|
||||||
.newCall(request)
|
.newCall(request)
|
||||||
.enqueue(
|
.enqueue(
|
||||||
object : Callback {
|
object : Callback {
|
||||||
@@ -96,7 +97,8 @@ class Nip05NostrAddressVerifier() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
onError("Could not resolve '$url': ${e.message}")
|
onError("Could not resolve '$url': ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +124,8 @@ class Nip05NostrAddressVerifier() {
|
|||||||
val nip05url =
|
val nip05url =
|
||||||
try {
|
try {
|
||||||
mapper.readTree(it.lowercase())
|
mapper.readTree(it.lowercase())
|
||||||
} catch (t: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -30,29 +32,64 @@ import okhttp3.Response
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
object Nip11CachedRetriever {
|
object Nip11CachedRetriever {
|
||||||
val relayInformationDocumentCache = LruCache<String, RelayInformation>(100)
|
open class RetrieveResult(val time: Long)
|
||||||
|
|
||||||
|
class RetrieveResultError(val error: Nip11Retriever.ErrorCode, val msg: String? = null) : RetrieveResult(TimeUtils.now())
|
||||||
|
|
||||||
|
class RetrieveResultSuccess(val data: Nip11RelayInformation) : RetrieveResult(TimeUtils.now())
|
||||||
|
|
||||||
|
val relayInformationDocumentCache = LruCache<String, RetrieveResult?>(100)
|
||||||
val retriever = Nip11Retriever()
|
val retriever = Nip11Retriever()
|
||||||
|
|
||||||
|
fun getFromCache(dirtyUrl: String): Nip11RelayInformation? {
|
||||||
|
val result = relayInformationDocumentCache.get(retriever.cleanUrl(dirtyUrl)) ?: return null
|
||||||
|
if (result is RetrieveResultSuccess) return result.data
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
onInfo: (RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
val url = retriever.cleanUrl(dirtyUrl)
|
val url = retriever.cleanUrl(dirtyUrl)
|
||||||
val doc = relayInformationDocumentCache.get(url)
|
val doc = relayInformationDocumentCache.get(url)
|
||||||
|
|
||||||
if (doc != null) {
|
if (doc != null) {
|
||||||
onInfo(doc)
|
if (doc is RetrieveResultSuccess) {
|
||||||
|
onInfo(doc.data)
|
||||||
|
} else if (doc is RetrieveResultError) {
|
||||||
|
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
||||||
|
onError(dirtyUrl, doc.error, null)
|
||||||
} else {
|
} else {
|
||||||
Nip11Retriever()
|
Nip11Retriever()
|
||||||
.loadRelayInfo(
|
.loadRelayInfo(
|
||||||
url,
|
url = url,
|
||||||
dirtyUrl,
|
dirtyUrl = dirtyUrl,
|
||||||
onInfo = {
|
onInfo = {
|
||||||
relayInformationDocumentCache.put(url, it)
|
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||||
onInfo(it)
|
onInfo(it)
|
||||||
},
|
},
|
||||||
onError,
|
onError = { dirtyUrl, code, errorMsg ->
|
||||||
|
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||||
|
onError(url, code, errorMsg)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Nip11Retriever()
|
||||||
|
.loadRelayInfo(
|
||||||
|
url = url,
|
||||||
|
dirtyUrl = dirtyUrl,
|
||||||
|
onInfo = {
|
||||||
|
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||||
|
onInfo(it)
|
||||||
|
},
|
||||||
|
onError = { dirtyUrl, code, errorMsg ->
|
||||||
|
relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg))
|
||||||
|
onError(url, code, errorMsg)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,14 +114,15 @@ class Nip11Retriever {
|
|||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
url: String,
|
url: String,
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
onInfo: (RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, ErrorCode, String?) -> Unit,
|
onError: (String, ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
try {
|
try {
|
||||||
val request: Request =
|
val request: Request =
|
||||||
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
|
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
|
||||||
|
|
||||||
HttpClient.getHttpClient()
|
HttpClientManager.getHttpClient()
|
||||||
.newCall(request)
|
.newCall(request)
|
||||||
.enqueue(
|
.enqueue(
|
||||||
object : Callback {
|
object : Callback {
|
||||||
@@ -97,11 +135,12 @@ class Nip11Retriever {
|
|||||||
val body = it.body.string()
|
val body = it.body.string()
|
||||||
try {
|
try {
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
onInfo(RelayInformation.fromJson(body))
|
onInfo(Nip11RelayInformation.fromJson(body))
|
||||||
} else {
|
} else {
|
||||||
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e(
|
Log.e(
|
||||||
"RelayInfoFail",
|
"RelayInfoFail",
|
||||||
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
"Resulting Message from Relay $dirtyUrl in not parseable: $body",
|
||||||
@@ -122,6 +161,7 @@ class Nip11Retriever {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e)
|
||||||
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023 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.ui.note
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
|
||||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
|
||||||
|
|
||||||
// Rename to the corect nip number when ready.
|
|
||||||
object Nip47WalletConnectParser {
|
|
||||||
fun parse(uri: String): Nip47URI {
|
|
||||||
// nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D
|
|
||||||
|
|
||||||
val url = Uri.parse(uri)
|
|
||||||
|
|
||||||
if (url.scheme != "nostrwalletconnect" && url.scheme != "nostr+walletconnect") {
|
|
||||||
throw IllegalArgumentException("Not a Wallet Connect QR Code")
|
|
||||||
}
|
|
||||||
|
|
||||||
val pubkey = url.host ?: throw IllegalArgumentException("Hostname cannot be null")
|
|
||||||
|
|
||||||
val pubkeyHex =
|
|
||||||
try {
|
|
||||||
decodePublicKey(pubkey).toHexKey()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalArgumentException("Hostname is not a valid Nostr Pubkey")
|
|
||||||
}
|
|
||||||
|
|
||||||
val relay = url.getQueryParameter("relay")
|
|
||||||
val secret = url.getQueryParameter("secret")
|
|
||||||
|
|
||||||
return Nip47URI(pubkeyHex, relay, secret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -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 kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
object Nip96MediaServers {
|
object Nip96MediaServers {
|
||||||
@@ -87,7 +88,7 @@ class Nip96Retriever {
|
|||||||
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
|
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClient.getHttpClient().newCall(request).execute().use { response ->
|
HttpClientManager.getHttpClient().newCall(request).execute().use { response ->
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
response.use {
|
response.use {
|
||||||
val body = it.body.string()
|
val body = it.body.string()
|
||||||
@@ -100,6 +101,7 @@ class Nip96Retriever {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e)
|
Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -128,7 +128,7 @@ class Nip96Uploader(val account: Account?) {
|
|||||||
val extension =
|
val extension =
|
||||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
val requestBody: RequestBody
|
val requestBody: RequestBody
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class Nip96Uploader(val account: Account?) {
|
|||||||
val extension =
|
val extension =
|
||||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class Nip96Uploader(val account: Account?) {
|
|||||||
server: Nip96Retriever.ServerInfo,
|
server: Nip96Retriever.ServerInfo,
|
||||||
onProgress: (percentage: Float) -> Unit,
|
onProgress: (percentage: Float) -> Unit,
|
||||||
): PartialEvent {
|
): PartialEvent {
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
var currentResult = result
|
var currentResult = result
|
||||||
|
|
||||||
while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) {
|
while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -248,19 +248,11 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
|
if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) {
|
||||||
Client.close(updatedSubscription.id)
|
Client.close(updatedSubscription.id)
|
||||||
if (active) {
|
if (active) {
|
||||||
Log.d(
|
|
||||||
this@NostrDataSource.javaClass.simpleName,
|
|
||||||
"Update Filter 1 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}",
|
|
||||||
)
|
|
||||||
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// hasn't changed, does nothing.
|
// hasn't changed, does nothing.
|
||||||
if (active) {
|
if (active) {
|
||||||
Log.d(
|
|
||||||
this@NostrDataSource.javaClass.simpleName,
|
|
||||||
"Update Filter 2 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}",
|
|
||||||
)
|
|
||||||
Client.sendFilterOnlyIfDisconnected(
|
Client.sendFilterOnlyIfDisconnected(
|
||||||
updatedSubscription.id,
|
updatedSubscription.id,
|
||||||
updatedSubscriptionNewFilters,
|
updatedSubscriptionNewFilters,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+49
-9
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -24,9 +24,12 @@ import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.encoders.ATag
|
||||||
import com.vitorpamplona.quartz.encoders.Hex
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19
|
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||||
|
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
@@ -46,6 +49,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
|||||||
import com.vitorpamplona.quartz.events.PinListEvent
|
import com.vitorpamplona.quartz.events.PinListEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||||
private var searchString: String? = null
|
private var searchString: String? = null
|
||||||
@@ -65,32 +69,68 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex
|
when (val parsed = Nip19Bech32.uriToRoute(mySearchString)?.entity) {
|
||||||
|
is Nip19Bech32.NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey()
|
||||||
|
is Nip19Bech32.NPub -> parsed.hex
|
||||||
|
is Nip19Bech32.NProfile -> parsed.hex
|
||||||
|
is Nip19Bech32.Note -> parsed.hex
|
||||||
|
is Nip19Bech32.NEvent -> parsed.hex
|
||||||
|
is Nip19Bech32.NEmbed -> parsed.event.id
|
||||||
|
is Nip19Bech32.NRelay -> null
|
||||||
|
is Nip19Bech32.NAddress -> parsed.atag
|
||||||
|
else -> isAStraightHex
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloads all the reactions to a given event.
|
val directReferenceFilters =
|
||||||
return listOfNotNull(
|
|
||||||
hexToWatch?.let {
|
hexToWatch?.let {
|
||||||
|
if (it.contains(":")) {
|
||||||
|
// naddr
|
||||||
|
listOfNotNull(
|
||||||
|
ATag.parse(it, null)?.let { aTag ->
|
||||||
|
TypedFilter(
|
||||||
|
types = COMMON_FEED_TYPES,
|
||||||
|
filter =
|
||||||
|
JsonFilter(
|
||||||
|
kinds = listOf(MetadataEvent.KIND, aTag.kind),
|
||||||
|
authors = listOfNotNull(aTag.pubKeyHex),
|
||||||
|
// just to be sure
|
||||||
|
limit = 5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// event ids
|
||||||
|
listOf(
|
||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter =
|
filter =
|
||||||
JsonFilter(
|
JsonFilter(
|
||||||
ids = listOfNotNull(hexToWatch),
|
ids = listOfNotNull(hexToWatch),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
},
|
// authors
|
||||||
hexToWatch?.let {
|
|
||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter =
|
filter =
|
||||||
JsonFilter(
|
JsonFilter(
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
kinds = listOf(MetadataEvent.KIND),
|
||||||
authors = listOfNotNull(hexToWatch),
|
authors = listOfNotNull(hexToWatch),
|
||||||
|
// just to be sure
|
||||||
|
limit = 5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
// downloads all the reactions to a given event.
|
||||||
|
return directReferenceFilters +
|
||||||
|
listOfNotNull(
|
||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = setOf(FeedType.SEARCH),
|
types = setOf(FeedType.SEARCH),
|
||||||
filter =
|
filter =
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
|||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
|
import com.vitorpamplona.quartz.events.OtsEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||||
import com.vitorpamplona.quartz.events.ReportEvent
|
import com.vitorpamplona.quartz.events.ReportEvent
|
||||||
@@ -120,6 +121,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return groupByEOSEPresence(eventsToWatch).map {
|
return groupByEOSEPresence(eventsToWatch).map {
|
||||||
|
listOf(
|
||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter =
|
filter =
|
||||||
@@ -133,14 +135,37 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
ReportEvent.KIND,
|
ReportEvent.KIND,
|
||||||
LnZapEvent.KIND,
|
LnZapEvent.KIND,
|
||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
|
OtsEvent.KIND,
|
||||||
),
|
),
|
||||||
tags = mapOf("e" to it.map { it.idHex }),
|
tags = mapOf("e" to it.map { it.idHex }),
|
||||||
since = findMinimumEOSEs(it),
|
since = findMinimumEOSEs(it),
|
||||||
// Max amount of "replies" to download on a specific event.
|
// Max amount of "replies" to download on a specific event.
|
||||||
limit = 1000,
|
limit = 1000,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
}.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createQuotesFilter(): List<TypedFilter>? {
|
||||||
|
if (eventsToWatch.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupByEOSEPresence(eventsToWatch).map {
|
||||||
|
listOf(
|
||||||
|
TypedFilter(
|
||||||
|
types = COMMON_FEED_TYPES,
|
||||||
|
filter =
|
||||||
|
JsonFilter(
|
||||||
|
tags = mapOf("q" to it.map { it.idHex }),
|
||||||
|
since = findMinimumEOSEs(it),
|
||||||
|
// Max amount of "replies" to download on a specific event.
|
||||||
|
limit = 1000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
|
||||||
@@ -205,9 +230,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
val missing = createLoadEventsIfNotLoadedFilter()
|
val missing = createLoadEventsIfNotLoadedFilter()
|
||||||
val addresses = createAddressFilter()
|
val addresses = createAddressFilter()
|
||||||
val addressReactions = createReactionsToWatchInAddressFilter()
|
val addressReactions = createReactionsToWatchInAddressFilter()
|
||||||
|
val quotes = createQuotesFilter()
|
||||||
|
|
||||||
singleEventChannel.typedFilters =
|
singleEventChannel.typedFilters =
|
||||||
listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
|
listOfNotNull(missing, addresses, reactions, addressReactions, quotes).flatten().ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(eventId: Note) {
|
fun add(eventId: Note) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -25,6 +25,7 @@ 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 okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
|
@Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
|
||||||
|
|
||||||
@@ -59,13 +60,14 @@ object OnlineChecker {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val result =
|
val result =
|
||||||
HttpClient.getHttpClient().newCall(request).execute().use {
|
HttpClientManager.getHttpClient().newCall(request).execute().use {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
it.isSuccessful
|
it.isSuccessful
|
||||||
}
|
}
|
||||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
|
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
|
||||||
result
|
result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
|
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
|
||||||
Log.e("LiveActivities", "Failed to check streaming url $url", e)
|
Log.e("LiveActivities", "Failed to check streaming url $url", e)
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
object PackageUtils {
|
object PackageUtils {
|
||||||
private fun isPackageInstalled(
|
private fun isPackageInstalled(
|
||||||
@@ -36,7 +38,13 @@ object PackageUtils {
|
|||||||
return isPackageInstalled(context, "org.torproject.android")
|
return isPackageInstalled(context, "org.torproject.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAmberInstalled(context: Context): Boolean {
|
fun isExternalSignerInstalled(context: Context): Boolean {
|
||||||
return isPackageInstalled(context, "com.greenart7c3.nostrsigner")
|
val intent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = Uri.parse("nostrsigner:")
|
||||||
|
}
|
||||||
|
val infos = context.packageManager.queryIntentActivities(intent, 0)
|
||||||
|
return infos.size > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+9
-4
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -24,7 +24,7 @@ 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.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||||
import com.vitorpamplona.quartz.encoders.Lud06
|
import com.vitorpamplona.quartz.encoders.Lud06
|
||||||
@@ -33,9 +33,10 @@ import okhttp3.Request
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
class LightningAddressResolver() {
|
class LightningAddressResolver() {
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
|
||||||
fun assembleUrl(lnaddress: String): String? {
|
fun assembleUrl(lnaddress: String): String? {
|
||||||
val parts = lnaddress.split("@")
|
val parts = lnaddress.split("@")
|
||||||
@@ -96,14 +97,16 @@ class LightningAddressResolver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
onError(
|
onError(
|
||||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string
|
R.string
|
||||||
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception,
|
||||||
url,
|
url,
|
||||||
lnaddress,
|
lnaddress,
|
||||||
|
e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -183,6 +186,7 @@ class LightningAddressResolver() {
|
|||||||
try {
|
try {
|
||||||
mapper.readTree(lnAddressJson)
|
mapper.readTree(lnAddressJson)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
if (t is CancellationException) throw t
|
||||||
onError(
|
onError(
|
||||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||||
context.getString(
|
context.getString(
|
||||||
@@ -218,6 +222,7 @@ class LightningAddressResolver() {
|
|||||||
try {
|
try {
|
||||||
mapper.readTree(it)
|
mapper.readTree(it)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
if (t is CancellationException) throw t
|
||||||
onError(
|
onError(
|
||||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||||
context.getString(
|
context.getString(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -112,7 +112,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
|||||||
event.pubKey != acc.userProfile().pubkeyHex
|
event.pubKey != acc.userProfile().pubkeyHex
|
||||||
) { // from the user
|
) { // from the user
|
||||||
|
|
||||||
val chatNote = LocalCache.notes[event.id] ?: return
|
val chatNote = LocalCache.getNoteIfExists(event.id) ?: return
|
||||||
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
|
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
|
||||||
|
|
||||||
val followingKeySet = acc.followingKeySet()
|
val followingKeySet = acc.followingKeySet()
|
||||||
@@ -145,7 +145,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
|||||||
event: PrivateDmEvent,
|
event: PrivateDmEvent,
|
||||||
acc: Account,
|
acc: Account,
|
||||||
) {
|
) {
|
||||||
val note = LocalCache.notes[event.id] ?: return
|
val note = LocalCache.getNoteIfExists(event.id) ?: return
|
||||||
|
|
||||||
// old event being re-broadcast
|
// old event being re-broadcast
|
||||||
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
||||||
@@ -184,7 +184,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
|||||||
event: LnZapEvent,
|
event: LnZapEvent,
|
||||||
acc: Account,
|
acc: Account,
|
||||||
) {
|
) {
|
||||||
val noteZapEvent = LocalCache.notes[event.id] ?: return
|
val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return
|
||||||
|
|
||||||
// old event being re-broadcast
|
// old event being re-broadcast
|
||||||
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -25,8 +25,9 @@ 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.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -106,10 +107,11 @@ class RegisterAccounts(
|
|||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val client = HttpClient.getHttpClient()
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
|
||||||
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
|
||||||
val tag =
|
val tag =
|
||||||
if (BuildConfig.FLAVOR == "play") {
|
if (BuildConfig.FLAVOR == "play") {
|
||||||
"FirebaseMsgService"
|
"FirebaseMsgService"
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* 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.ots
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
|
import com.vitorpamplona.quartz.ots.BitcoinExplorer
|
||||||
|
import com.vitorpamplona.quartz.ots.BlockHeader
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.UrlException
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
class OkHttpBlockstreamExplorer : BitcoinExplorer {
|
||||||
|
/**
|
||||||
|
* Retrieve the block information from the block hash.
|
||||||
|
*
|
||||||
|
* @param hash Hash of the block.
|
||||||
|
* @return the blockheader of the hash
|
||||||
|
* @throws Exception desc
|
||||||
|
*/
|
||||||
|
override fun block(hash: String): BlockHeader {
|
||||||
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
val url = "$BLOCKSTREAM_API_URL/block/$hash"
|
||||||
|
|
||||||
|
val request =
|
||||||
|
Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
val jsonObject = jacksonObjectMapper().readTree(it.body.string())
|
||||||
|
|
||||||
|
val blockHeader = BlockHeader()
|
||||||
|
blockHeader.merkleroot = jsonObject["merkle_root"].asText()
|
||||||
|
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
|
||||||
|
blockHeader.blockHash = hash
|
||||||
|
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
|
||||||
|
return blockHeader
|
||||||
|
} else {
|
||||||
|
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the block hash from the block height.
|
||||||
|
*
|
||||||
|
* @param height Height of the block.
|
||||||
|
* @return the hash of the block at height height
|
||||||
|
* @throws Exception desc
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun blockHash(height: Int): String {
|
||||||
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
|
||||||
|
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
|
||||||
|
|
||||||
|
val request =
|
||||||
|
Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
val blockHash = it.body.string()
|
||||||
|
|
||||||
|
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
|
||||||
|
return blockHash
|
||||||
|
} else {
|
||||||
|
throw UrlException(it.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* 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.ots
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
|
import com.vitorpamplona.quartz.ots.ICalendar
|
||||||
|
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
|
||||||
|
import com.vitorpamplona.quartz.ots.Timestamp
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.DeserializationException
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.UrlException
|
||||||
|
import com.vitorpamplona.quartz.ots.http.Request
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing remote calendar server interface.
|
||||||
|
*/
|
||||||
|
class OkHttpCalendar(val url: String) : ICalendar {
|
||||||
|
/**
|
||||||
|
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
|
||||||
|
*
|
||||||
|
* @param digest The digest hash to send.
|
||||||
|
* @return the Timestamp received from the calendar.
|
||||||
|
* @throws ExceededSizeException if response is too big.
|
||||||
|
* @throws UrlException if url is not reachable.
|
||||||
|
* @throws DeserializationException if the data is corrupt
|
||||||
|
*/
|
||||||
|
@Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class)
|
||||||
|
override fun submit(digest: ByteArray): Timestamp {
|
||||||
|
try {
|
||||||
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
val url = "$url/digest"
|
||||||
|
|
||||||
|
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
||||||
|
val requestBody = digest.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request =
|
||||||
|
okhttp3.Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.url(url)
|
||||||
|
.post(requestBody)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
val ctx = StreamDeserializationContext(it.body.bytes())
|
||||||
|
return Timestamp.deserialize(ctx, digest)
|
||||||
|
} else {
|
||||||
|
throw UrlException("Failed to open $url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ExceededSizeException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: DeserializationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw UrlException(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a timestamp for a given commitment.
|
||||||
|
*
|
||||||
|
* @param commitment The digest hash to send.
|
||||||
|
* @return the Timestamp from the calendar server (with blockchain information if already written).
|
||||||
|
* @throws ExceededSizeException if response is too big.
|
||||||
|
* @throws UrlException if url is not reachable.
|
||||||
|
* @throws CommitmentNotFoundException if commit is not found.
|
||||||
|
* @throws DeserializationException if the data is corrupt
|
||||||
|
*/
|
||||||
|
@Throws(
|
||||||
|
DeserializationException::class,
|
||||||
|
ExceededSizeException::class,
|
||||||
|
CommitmentNotFoundException::class,
|
||||||
|
UrlException::class,
|
||||||
|
)
|
||||||
|
override fun getTimestamp(commitment: ByteArray): Timestamp {
|
||||||
|
try {
|
||||||
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
val url = url + "/timestamp/" + Hex.encode(commitment)
|
||||||
|
|
||||||
|
val request =
|
||||||
|
okhttp3.Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
val ctx = StreamDeserializationContext(it.body.bytes())
|
||||||
|
return Timestamp.deserialize(ctx, commitment)
|
||||||
|
} else {
|
||||||
|
throw CommitmentNotFoundException("Calendar response a status code != 200: " + it.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: DeserializationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: ExceededSizeException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: CommitmentNotFoundException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw UrlException(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 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.ots
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
|
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
|
||||||
|
import com.vitorpamplona.quartz.ots.StreamDeserializationContext
|
||||||
|
import com.vitorpamplona.quartz.ots.Timestamp
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.util.Optional
|
||||||
|
import java.util.concurrent.BlockingQueue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For making async calls to a calendar server
|
||||||
|
*/
|
||||||
|
class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit {
|
||||||
|
private var queue: BlockingQueue<Optional<Timestamp>>? = null
|
||||||
|
|
||||||
|
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
|
||||||
|
this.queue = queue
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun call(): Optional<Timestamp> {
|
||||||
|
val client = HttpClientManager.getHttpClient()
|
||||||
|
val url = "$url/digest"
|
||||||
|
|
||||||
|
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
||||||
|
val requestBody = digest.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request =
|
||||||
|
okhttp3.Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.header("Accept", "application/vnd.opentimestamps.v1")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.url(url)
|
||||||
|
.post(requestBody)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
val ctx = StreamDeserializationContext(it.body.bytes())
|
||||||
|
val timestamp = Timestamp.deserialize(ctx, digest)
|
||||||
|
val of = Optional.of(timestamp)
|
||||||
|
queue!!.add(of)
|
||||||
|
return of
|
||||||
|
} else {
|
||||||
|
queue!!.add(Optional.empty())
|
||||||
|
return Optional.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-4
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -18,8 +18,21 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.ots.CalendarBuilder
|
||||||
|
import com.vitorpamplona.quartz.ots.ICalendar
|
||||||
|
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
|
||||||
|
|
||||||
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
|
class OkHttpCalendarBuilder : CalendarBuilder {
|
||||||
|
override fun newSyncCalendar(url: String): ICalendar {
|
||||||
|
return OkHttpCalendar(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newAsyncCalendar(
|
||||||
|
url: String,
|
||||||
|
digest: ByteArray,
|
||||||
|
): ICalendarAsyncSubmit {
|
||||||
|
return OkHttpCalendarAsyncSubmit(url, digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -28,6 +28,7 @@ import android.util.LruCache
|
|||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
object PlaybackClientController {
|
object PlaybackClientController {
|
||||||
val cache = LruCache<Int, SessionToken>(1)
|
val cache = LruCache<Int, SessionToken>(1)
|
||||||
@@ -62,12 +63,14 @@ object PlaybackClientController {
|
|||||||
try {
|
try {
|
||||||
onReady(controllerFuture.get())
|
onReady(controllerFuture.get())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MoreExecutors.directExecutor(),
|
MoreExecutors.directExecutor(),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -31,7 +31,7 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
|||||||
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.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
|
|
||||||
@UnstableApi // Extend MediaSessionService
|
@UnstableApi // Extend MediaSessionService
|
||||||
class PlaybackService : MediaSessionService() {
|
class PlaybackService : MediaSessionService() {
|
||||||
@@ -42,12 +42,12 @@ class PlaybackService : MediaSessionService() {
|
|||||||
private var managerLocal: MultiPlayerPlaybackManager? = null
|
private var managerLocal: MultiPlayerPlaybackManager? = null
|
||||||
|
|
||||||
fun newHslDataSource(): MediaSource.Factory {
|
fun newHslDataSource(): MediaSource.Factory {
|
||||||
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient()))
|
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newProgressiveDataSource(): MediaSource.Factory {
|
fun newProgressiveDataSource(): MediaSource.Factory {
|
||||||
return ProgressiveMediaSource.Factory(
|
return ProgressiveMediaSource.Factory(
|
||||||
(applicationContext as Amethyst).videoCache.get(HttpClient.getHttpClient()),
|
(applicationContext as Amethyst).videoCache.get(HttpClientManager.getHttpClient()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class PlaybackService : MediaSessionService() {
|
|||||||
Log.d("Lifetime Event", "PlaybackService.onCreate")
|
Log.d("Lifetime Event", "PlaybackService.onCreate")
|
||||||
|
|
||||||
// Stop all videos and recreates all managers when the proxy changes.
|
// Stop all videos and recreates all managers when the proxy changes.
|
||||||
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProxyUpdated() {
|
private fun onProxyUpdated() {
|
||||||
@@ -114,7 +114,7 @@ class PlaybackService : MediaSessionService() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d("Lifetime Event", "PlaybackService.onDestroy")
|
Log.d("Lifetime Event", "PlaybackService.onDestroy")
|
||||||
|
|
||||||
HttpClient.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
|
HttpClientManager.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated)
|
||||||
|
|
||||||
managerHls?.releaseAppPlayers()
|
managerHls?.releaseAppPlayers()
|
||||||
managerLocal?.releaseAppPlayers()
|
managerLocal?.releaseAppPlayers()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.previews
|
package com.vitorpamplona.amethyst.service.previews
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
|||||||
try {
|
try {
|
||||||
fetch(timeOut)
|
fetch(timeOut)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
if (t is CancellationException) throw t
|
||||||
callback?.onFailed(t)
|
callback?.onFailed(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.previews
|
package com.vitorpamplona.amethyst.service.previews
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
@@ -90,7 +91,8 @@ suspend fun getDocument(
|
|||||||
): UrlInfoItem =
|
): UrlInfoItem =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val request: Request = Request.Builder().url(url).get().build()
|
val request: Request = Request.Builder().url(url).get().build()
|
||||||
HttpClient.getHttpClient().newCall(request).execute().use {
|
HttpClientManager.getHttpClient().newCall(request).execute().use {
|
||||||
|
checkNotInMainThread()
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
val mimeType =
|
val mimeType =
|
||||||
it.headers.get("Content-Type")?.toMediaType()
|
it.headers.get("Content-Type")?.toMediaType()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -108,7 +108,7 @@ object Client : RelayPool.Listener {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
subscriptions = subscriptions + Pair(subscriptionId, filters)
|
||||||
RelayPool.sendFilterOnlyIfDisconnected(subscriptionId)
|
RelayPool.connectAndSendFiltersIfDisconnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(
|
fun send(
|
||||||
@@ -292,7 +292,7 @@ object Client : RelayPool.Listener {
|
|||||||
open fun onRelayStateChange(
|
open fun onRelayStateChange(
|
||||||
type: Relay.StateType,
|
type: Relay.StateType,
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
channel: String?,
|
subscriptionId: String?,
|
||||||
) = Unit
|
) = Unit
|
||||||
|
|
||||||
/** When an relay saves or rejects a new event. */
|
/** When an relay saves or rejects a new event. */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relays
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.EventInterface
|
|||||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.events.bytesUsedInMemory
|
import com.vitorpamplona.quartz.events.bytesUsedInMemory
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
@@ -62,7 +63,7 @@ class Relay(
|
|||||||
const val RECONNECTING_IN_SECONDS = 60 * 3
|
const val RECONNECTING_IN_SECONDS = 60 * 3
|
||||||
}
|
}
|
||||||
|
|
||||||
private val httpClient = HttpClient.getHttpClientForRelays()
|
private val httpClient = HttpClientManager.getHttpClient()
|
||||||
|
|
||||||
private var listeners = setOf<Listener>()
|
private var listeners = setOf<Listener>()
|
||||||
private var socket: WebSocket? = null
|
private var socket: WebSocket? = null
|
||||||
@@ -76,7 +77,7 @@ class Relay(
|
|||||||
var errorCounter = 0
|
var errorCounter = 0
|
||||||
var pingInMs: Long? = null
|
var pingInMs: Long? = null
|
||||||
|
|
||||||
var closingTimeInSeconds = 0L
|
var lastConnectTentative: Long = 0L
|
||||||
|
|
||||||
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ class Relay(
|
|||||||
private var connectingBlock = AtomicBoolean()
|
private var connectingBlock = AtomicBoolean()
|
||||||
|
|
||||||
fun connectAndRun(onConnected: (Relay) -> Unit) {
|
fun connectAndRun(onConnected: (Relay) -> Unit) {
|
||||||
Log.d("Relay", "Relay.connect $url")
|
Log.d("Relay", "Relay.connect $url hasProxy: ${this.httpClient.proxy != null}")
|
||||||
// BRB is crashing OkHttp Deflater object :(
|
// BRB is crashing OkHttp Deflater object :(
|
||||||
if (url.contains("brb.io")) return
|
if (url.contains("brb.io")) return
|
||||||
|
|
||||||
@@ -119,6 +120,8 @@ class Relay(
|
|||||||
|
|
||||||
if (socket != null) return
|
if (socket != null) return
|
||||||
|
|
||||||
|
lastConnectTentative = TimeUtils.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val request =
|
val request =
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
@@ -128,6 +131,8 @@ class Relay(
|
|||||||
|
|
||||||
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
|
socket = httpClient.newWebSocket(request, RelayListener(onConnected))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
|
||||||
errorCounter++
|
errorCounter++
|
||||||
markConnectionAsClosed()
|
markConnectionAsClosed()
|
||||||
Log.e("Relay", "Relay Invalid $url")
|
Log.e("Relay", "Relay Invalid $url")
|
||||||
@@ -167,8 +172,9 @@ class Relay(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
processNewRelayMessage(text)
|
processNewRelayMessage(text)
|
||||||
} catch (t: Throwable) {
|
} catch (e: Throwable) {
|
||||||
t.printStackTrace()
|
if (e is CancellationException) throw e
|
||||||
|
e.printStackTrace()
|
||||||
text.chunked(2000) { chunked ->
|
text.chunked(2000) { chunked ->
|
||||||
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
|
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
|
||||||
}
|
}
|
||||||
@@ -247,7 +253,6 @@ class Relay(
|
|||||||
this.isReady = false
|
this.isReady = false
|
||||||
this.usingCompression = false
|
this.usingCompression = false
|
||||||
this.resetEOSEStatuses()
|
this.resetEOSEStatuses()
|
||||||
this.closingTimeInSeconds = TimeUtils.now()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processNewRelayMessage(newMessage: String) {
|
fun processNewRelayMessage(newMessage: String) {
|
||||||
@@ -327,7 +332,7 @@ class Relay(
|
|||||||
Log.d("Relay", "Relay.disconnect $url")
|
Log.d("Relay", "Relay.disconnect $url")
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
closingTimeInSeconds = TimeUtils.now()
|
lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested.
|
||||||
socket?.cancel()
|
socket?.cancel()
|
||||||
socket = null
|
socket = null
|
||||||
isReady = false
|
isReady = false
|
||||||
@@ -369,7 +374,7 @@ class Relay(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// waits 60 seconds to reconnect after disconnected.
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) {
|
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
||||||
// sends all filters after connection is successful.
|
// sends all filters after connection is successful.
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
@@ -404,12 +409,12 @@ class Relay(
|
|||||||
return buffer.toString()
|
return buffer.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
|
fun connectAndSendFiltersIfDisconnected() {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
// waits 60 seconds to reconnect after disconnected.
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) {
|
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
||||||
// println("sendfilter Only if Disconnected ${url} ")
|
// println("sendfilter Only if Disconnected ${url} ")
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -81,8 +81,8 @@ object RelayPool : Relay.Listener {
|
|||||||
relays.forEach { it.sendFilter(subscriptionId) }
|
relays.forEach { it.sendFilter(subscriptionId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFilterOnlyIfDisconnected(subscriptionId: String) {
|
fun connectAndSendFiltersIfDisconnected() {
|
||||||
relays.forEach { it.sendFilterOnlyIfDisconnected(subscriptionId) }
|
relays.forEach { it.connectAndSendFiltersIfDisconnected() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendToSelectedRelays(
|
fun sendToSelectedRelays(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2023 Vitor Pamplona
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
* 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
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user