Merge branch 'main' into amber

pull/543/head
greenart7c3 2023-09-11 07:20:03 -03:00 zatwierdzone przez GitHub
commit 7ff6f16df0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
13 zmienionych plików z 386 dodań i 129 usunięć

Wyświetl plik

@ -106,7 +106,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
This is a native Android app made with Kotlin and Jetpack Compose.
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 in the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with 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.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to
@ -172,7 +172,7 @@ For the Play build:
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
```
2. Create 4 Secret Key variables on your GitHub repository and fill in with the signing key information
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
- `KEY_ALIAS` <- `<alias_name>`
- `KEY_PASSWORD` <- `<your password>`
- `KEY_STORE_PASSWORD` <- `<your key store password>`
@ -192,15 +192,15 @@ The relay also learns which public keys you are requesting, meaning your public
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.
# DM Privacy #
While the content of direct messages (DMs) is only visible to you, and your DM nostr counterparty, everyone can see that and when you and your counterparty are DM-ing each other.
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.
# Visibility & Permanence of Your Content on nostr
## Information Visibility ##
Content that you share can be shared to other relays.
Information that you share is publicly 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 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 ##
Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted.
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.
# Screenshots

Wyświetl plik

@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 293
versionName "0.75.14"
versionCode 294
versionName "0.76.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

Wyświetl plik

@ -56,6 +56,10 @@ object LocalCache {
// checkNotInMainThread()
return users[key] ?: run {
require(isValidHex(key = key)) {
"$key is not a valid hex"
}
val newObject = User(key)
users.putIfAbsent(key, newObject) ?: newObject
}
@ -101,6 +105,10 @@ object LocalCache {
checkNotInMainThread()
return notes.get(idHex) ?: run {
require(isValidHex(idHex)) {
"$idHex is not a valid hex"
}
val newObject = Note(idHex)
notes.putIfAbsent(idHex, newObject) ?: newObject
}
@ -818,23 +826,23 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) }
if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
(
(zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>()
)
note.loadEvent(event, author, repliesTo)
if (zapRequest == null) {
Log.e("ZP", "Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
}
// Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
repliesTo.forEach {

Wyświetl plik

@ -26,25 +26,41 @@ import kotlinx.collections.immutable.persistentSetOf
import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) {
suspend fun consume(event: GiftWrapEvent) {
if (!LocalCache.justVerify(event)) return
if (!notificationManager().areNotificationsEnabled()) return
suspend fun consume(event: Event) {
if (LocalCache.notes[event.id] == null) {
if (LocalCache.justVerify(event)) {
LocalCache.justConsume(event, null)
// PushNotification Wraps don't include a receiver.
// Test with all logged in accounts
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && acc.keyPair.privKey != null) {
consumeIfMatchesAccount(event, acc)
}
}
}
val manager = notificationManager()
if (manager.areNotificationsEnabled()) {
when (event) {
is PrivateDmEvent -> notify(event)
is LnZapEvent -> notify(event)
is GiftWrapEvent -> unwrapAndNotify(event)
}
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
val key = account.keyPair.privKey ?: return
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
}
suspend fun unwrapAndConsume(event: Event, account: Account): Event? {
private fun unwrapAndConsume(event: Event, account: Account): Event? {
if (!LocalCache.justVerify(event)) return null
return when (event) {
@ -111,75 +127,67 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
}
private suspend fun unwrapAndNotify(giftWrap: GiftWrapEvent) {
val giftWrapNote = LocalCache.notes[giftWrap.id] ?: return
private fun notify(event: ChatMessageEvent, acc: Account) {
if (event.createdAt > TimeUtils.fiveMinutesAgo() && // old event being re-broadcasted
event.pubKey != acc.userProfile().pubkeyHex
) { // from the user
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
val chatNote = LocalCache.notes[event.id] ?: return
val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey())
if (acc != null && acc.userProfile().pubkeyHex == giftWrap.recipientPubKey()) {
val chatEvent = unwrapAndConsume(giftWrap, account = acc)
val followingKeySet = acc.followingKeySet()
if (chatEvent is ChatMessageEvent && // must be messages, not any other event
acc.keyPair.privKey != null && // can't decrypt
chatEvent.createdAt > TimeUtils.fiveMinutesAgo() && // old event being re-broadcasted
chatEvent.pubKey != acc.userProfile().pubkeyHex // from the user
) {
val chatNote = LocalCache.notes[chatEvent.id] ?: return
val chatRoom = chatEvent.chatroomKey(acc.keyPair.pubKey.toHexKey())
val isKnownRoom = (
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
val followingKeySet = acc.followingKeySet()
val isKnownRoom = (
acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
val noteUri = chatNote.toNEvent()
notificationManager().sendDMNotification(chatEvent.id, content, user, userPicture, noteUri, applicationContext)
}
}
if (isKnownRoom) {
val content = chatNote.event?.content() ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
val noteUri = chatNote.toNEvent()
notificationManager().sendDMNotification(
event.id,
content,
user,
userPicture,
noteUri,
applicationContext
)
}
}
}
private fun notify(event: PrivateDmEvent) {
private fun notify(event: PrivateDmEvent, acc: Account) {
val note = LocalCache.notes[event.id] ?: return
// old event being re-broadcast
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
val followingKeySet = acc.followingKeySet()
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
val followingKeySet = acc.followingKeySet()
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
}.toSet()
val knownChatrooms = acc.userProfile().privateChatrooms.keys.filter {
(
acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
}.toSet()
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
val content = acc.decryptContent(note) ?: ""
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
}
note.author?.let {
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
val content = acc.decryptContent(note) ?: ""
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
val noteUri = note.toNEvent()
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
}
}
}
}
private fun notify(event: LnZapEvent) {
private fun notify(event: LnZapEvent, acc: Account) {
val noteZapEvent = LocalCache.notes[event.id] ?: return
// old event being re-broadcast
@ -190,39 +198,35 @@ class EventNotificationConsumer(private val applicationContext: Context) {
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
LocalPreferences.allSavedAccounts().forEach {
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
if (acc != null && acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
val amount = showAmount(event.amount)
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
if (decryptedContent != null) {
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
Pair(author, decryptedContent.content)
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
Pair(noteZapRequest.author, noteZapRequest.event?.content())
} else {
Pair(noteZapRequest.author, null)
}
if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
val amount = showAmount(event.amount)
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
if (decryptedContent != null) {
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
Pair(author, decryptedContent.content)
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
Pair(noteZapRequest.author, noteZapRequest.event?.content())
} else {
Pair(noteZapRequest.author, null)
}
val zappedContent =
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
val user = senderInfo?.first?.toBestDisplayName() ?: ""
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
senderInfo?.second?.ifBlank { null }?.let {
title += " ($it)"
}
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
zappedContent?.let {
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
}
val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications"
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
}
val zappedContent =
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
val user = senderInfo?.first?.toBestDisplayName() ?: ""
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
senderInfo?.second?.ifBlank { null }?.let {
title += " ($it)"
}
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
zappedContent?.let {
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
}
val userPicture = senderInfo?.first?.profilePicture()
val noteUri = "nostr:Notifications"
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
}
}

Wyświetl plik

@ -101,37 +101,50 @@ class NewMessageTagger(
key = key.removePrefix("@")
if (key.length < 63) {
return null
}
try {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
// Converts to npub
val pubkey = Nip19.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val pubkeyRelay = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val noteRelayId = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
val address = Nip19.uriToRoute(key) ?: return null
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
}

Wyświetl plik

@ -444,6 +444,10 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
Text(stringResource(R.string.quick_action_share))
}
Divider()
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isPrivateBookmarkNote) {
DropdownMenuItem(
onClick = {
@ -545,10 +549,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
}
}
Divider()
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(
onClick = {

Wyświetl plik

@ -89,6 +89,56 @@
<string name="display_name">Nome Visualizzato</string>
<string name="my_display_name">Il mio nome visualizzato</string>
<string name="username">Username</string>
<string name="my_username">Il mio nome utente</string>
<string name="about_me">Su di me</string>
<string name="avatar_url">Immagine del profilo URL</string>
<string name="banner_url">Copertina URL</string>
<string name="website_url">Sito web URL</string>
<string name="ln_address">Indirizzo LN</string>
<string name="ln_url_outdated">LN URL (obsoleto)</string>
<string name="image_saved_to_the_gallery">Immagine salvata nella galleria</string>
<string name="failed_to_save_the_image">Salvataggio dell\'immagine non riuscito</string>
<string name="upload_image">Carica immagine</string>
<string name="uploading">Caricamento…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utente non dispone di un indirizzo Lightning per ricevere sats</string>
<string name="reply_here">"rispondi qui.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copia l\'ID della nota negli appunti per la condivisione in Nostr</string>
<string name="copy_channel_id_note_to_the_clipboard">Copia l\'ID del canale (Nota) negli appunti</string>
<string name="edits_the_channel_metadata">Modifica i metadati del canale</string>
<string name="join">Unisciti</string>
<string name="known">Conosciuto</string>
<string name="new_requests">Nuove richieste</string>
<string name="blocked_users">Utenti bloccati</string>
<string name="new_threads">Nuove Discussioni</string>
<string name="conversations">Conversazioni</string>
<string name="notes">Note</string>
<string name="replies">Risposte</string>
<string name="follows">"Seguiti"</string>
<string name="reports">"Segnalazioni"</string>
<string name="more_options">Più Opzioni</string>
<string name="relays">" Relè"</string>
<string name="website">Sito web</string>
<string name="lightning_address">Indirizzo Lightning</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copia l\'ID Nsec (la password) negli appunti per il backup</string>
<string name="copy_private_key_to_the_clipboard">Copia la chiave segreta negli appunti</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copia la chiave pubblica negli appunti per la condivisione</string>
<string name="copy_public_key_npub_to_the_clipboard">Copia la chiave pubblica (Npub) negli appunti</string>
<string name="send_a_direct_message">Invia un messaggio diretto</string>
<string name="edits_the_user_s_metadata">Modifica i metadati dell\'utente</string>
<string name="follow">Segui</string>
<string name="follow_back">Segui anche tu</string>
<string name="unblock">Sblocca</string>
<string name="copy_user_id">Copia ID Utente</string>
<string name="unblock_user">Sblocca utente</string>
<string name="npub_hex_username">"npub, nome utente, testo"</string>
<string name="clear">Pulisci</string>
<string name="app_logo">Logo dell\'app</string>
<string name="nsec_npub_hex_private_key">nsec.. o npub..</string>
<string name="show_password">Mostra password</string>
<string name="hide_password">Nascondi password</string>
<string name="invalid_key">Chiave non valida</string>
<string name="i_accept_the">"Accetto il/lo/la/i/gli/le "</string>
<string name="terms_of_use">Condizioni di utilizzo</string>
<string name="acceptance_of_terms_is_required">L\'accettazione dei termini è obbligatoria</string>
<string name="key_is_required">La chiave è obbligatoria</string>
<string name="login">Accesso</string>
@ -137,4 +187,54 @@
\n- Gli sviluppatori di Amethyst non chiederanno **mai** la tua chiave privata.
\n- Mantieni **sempre** al sicuro un backup della tua chiave privata per recuperare l\'account. Ti suggeriamo di usare un password manager.
</string>
<string name="report_dialog_select_reason_label">Motivo</string>
<string name="report_dialog_select_reason_placeholder">Seleziona un motivo…</string>
<string name="report_dialog_post_report_btn">Pubblica una segnalazione</string>
<string name="report_dialog_title">Blocca e Segnala</string>
<string name="block_only">Blocca</string>
<string name="bookmarks">Segnalibri</string>
<string name="private_bookmarks">Segnalibri privati</string>
<string name="public_bookmarks">Segnalibri pubblici</string>
<string name="add_to_private_bookmarks">Aggiungi ai segnalibri privati</string>
<string name="add_to_public_bookmarks">Aggiungi ai segnalibri pubblici</string>
<string name="remove_from_private_bookmarks">Rimuovi dai segnalibri privati</string>
<string name="remove_from_public_bookmarks">Rimuovi dai segnalibri pubblici</string>
<string name="wallet_connect_service">Servizio di Connessione Portafoglio</string>
<string name="wallet_connect_service_explainer">Autorizza un Nostr Secret a pagare gli zaps senza lasciare l\'app. Mantieni il segreto al sicuro e usa un relè privato se possibile</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
<string name="wallet_connect_service_relay">Wallet Connect Relè</string>
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
<string name="wallet_connect_service_show_secret">Mostra chiave segreta</string>
<string name="wallet_connect_service_secret_placeholder">nsec / chiave privata esadecimale</string>
<string name="pledge_amount_in_sats">Importo in pegno in sats</string>
<string name="post_poll">Pubblica sondaggio</string>
<string name="poll_heading_required">Campi obbligatori:</string>
<string name="poll_zap_recipients">Destinatari zap</string>
<string name="poll_primary_description">Descrizione principale del sondaggio…</string>
<string name="poll_option_index">Opzione %s</string>
<string name="poll_option_description">Descrizione opzione sondaggio</string>
<string name="poll_heading_optional">Campi facoltativi:</string>
<string name="poll_zap_value_min">Zap minimo</string>
<string name="poll_zap_value_max">Zap massimo</string>
<string name="poll_consensus_threshold">Consenso</string>
<string name="poll_consensus_threshold_percent">(0–100)%</string>
<string name="poll_closing_time">Chiudi dopo</string>
<string name="poll_closing_time_days">giorni</string>
<string name="poll_is_closed">Il sondaggio è chiuso a nuovi voti</string>
<string name="poll_zap_amount">Quantità in zap</string>
<string name="one_vote_per_user_on_atomic_votes">Per questo tipo di sondaggio è consentito solo un voto per utente</string>
<string name="looking_for_event">"Cercando l'evento %1$s"</string>
<string name="custom_zaps_add_a_message">Aggiungi un messaggio pubblico</string>
<string name="custom_zaps_add_a_message_private">Aggiungi un messaggio privato</string>
<string name="custom_zaps_add_a_message_nonzap">Aggiungi un messaggio di fattura</string>
<string name="custom_zaps_add_a_message_example">Grazie per tutto il tuo lavoro!</string>
<string name="lightning_create_and_add_invoice">Crea e Aggiungi</string>
<string name="poll_author_no_vote">Gli autori del sondaggio non possono votare nei propri sondaggi.</string>
<string name="hash_verification_passed">Questo contenuto è lo stesso da quando è stato pubblicato</string>
<string name="hash_verification_failed">Questo contenuto è cambiato. L\'autore potrebbe non aver visto o approvato la modifica</string>
<string name="content_description_add_image">Aggiungi Immagine</string>
<string name="content_description_add_video">Aggiungi Video</string>
<string name="content_description_add_document">Aggiungi Documento</string>
<string name="add_content">Aggiungi al Messaggio</string>
<string name="content_description">Descrizione dei contenuti</string>
</resources>

Wyświetl plik

@ -6,6 +6,7 @@
<string name="scan_qr">扫描二维码</string>
<string name="show_anyway">一直显示</string>
<string name="post_was_flagged_as_inappropriate_by">帖文被标记为不当</string>
<string name="post_not_found">事件正在加载或无法在你的中继列表中找到</string>
<string name="channel_image">频道图片</string>
<string name="referenced_event_not_found">未找到相关事件</string>
<string name="could_not_decrypt_the_message">无法解密消息</string>
@ -22,6 +23,7 @@
<string name="copy_note_id">复制笔记ID</string>
<string name="broadcast">广播</string>
<string name="request_deletion">请求删除</string>
<string name="block_report">屏蔽/举报</string>
<string name="block_hide_user"><![CDATA[阻止并隐藏用户]]></string>
<string name="report_spam_scam">举报垃圾邮件/诈骗</string>
<string name="report_impersonation">举报冒充</string>
@ -46,6 +48,7 @@
<string name="in_channel">" 在频道 "</string>
<string name="profile_banner">个人档案横幅</string>
<string name="payment_successful">付款成功</string>
<string name="error_parsing_error_message">错误解析错误消息</string>
<string name="following">" 关注"</string>
<string name="followers">" 粉丝"</string>
<string name="profile">个人档案</string>
@ -249,23 +252,35 @@
<string name="wallet_connect_service_explainer">使用您的私钥在不离开应用程序的情况下支付 zaps。 任何具有访问 Nostr 私钥的人都可以使用您的钱包余额。要保留您的资金避免损失,如果可能,请使用私人中继器。中继器操作员可以看到您的付款元数据。</string>
<string name="wallet_connect_service_pubkey">钱包绑定公钥</string>
<string name="wallet_connect_service_relay">钱包绑定中继</string>
<string name="wallet_connect_service_secret">钱包连接密码</string>
<string name="wallet_connect_service_show_secret">显示私钥</string>
<string name="wallet_connect_service_secret_placeholder">nsec / 十六进制私钥</string>
<string name="pledge_amount_in_sats">聪数量</string>
<string name="post_poll">发布投票</string>
<string name="poll_heading_required">必填字段:</string>
<string name="poll_zap_recipients">打闪接收人</string>
<string name="poll_primary_description">投票主要说明…</string>
<string name="poll_option_index">选项 %s</string>
<string name="poll_option_description">投票选项说明</string>
<string name="poll_heading_optional">可选字段:</string>
<string name="poll_zap_value_min">打闪最低金额</string>
<string name="poll_zap_value_max">打闪最高金额</string>
<string name="poll_consensus_threshold">共识</string>
<string name="poll_consensus_threshold_percent">(0–100)%</string>
<string name="poll_closing_time">后关闭</string>
<string name="poll_closing_time_days"></string>
<string name="poll_is_closed">投票不再接收新投票</string>
<string name="poll_zap_amount">打闪金额</string>
<string name="one_vote_per_user_on_atomic_votes">这种投票只允许每个用户一票</string>
<string name="looking_for_event">"“正在查找事件%1$s”"</string>
<string name="custom_zaps_add_a_message">添加公开消息</string>
<string name="custom_zaps_add_a_message_private">添加私信</string>
<string name="custom_zaps_add_a_message_nonzap">添加发票消息</string>
<string name="custom_zaps_add_a_message_example">感谢你的所有工作!</string>
<string name="lightning_create_and_add_invoice">创建并添加</string>
<string name="poll_author_no_vote">投票作者不能在自己的投票中投票。</string>
<string name="hash_verification_passed">此内容与发布时相同</string>
<string name="hash_verification_failed">此内容已更改。作者可能没有看到或批准修改</string>
<string name="content_description_add_image">添加图片</string>
<string name="content_description_add_video">添加视频</string>
<string name="content_description_add_document">添加文件</string>
@ -279,16 +294,50 @@
<string name="zap_type_private">私人</string>
<string name="zap_type_private_explainer">发送方和接收方能互相看到并读取消息</string>
<string name="zap_type_anonymous">匿名</string>
<string name="zap_type_anonymous_explainer">接收人和公众不知道谁发送了付款</string>
<string name="zap_type_nonzap">非打闪</string>
<string name="zap_type_nonzap_explainer">Nostr 中没有痕迹,仅在闪电上</string>
<string name="file_server">文件服务器</string>
<string name="zap_forward_lnAddress">LnAddress 或 @User</string>
<string name="upload_server_imgur">imgur.com - 受信任的</string>
<string name="upload_server_imgur_explainer">Imgur 可以修改文件</string>
<string name="upload_server_nostrimg">nostrimg.com - 受信任的</string>
<string name="upload_server_nostrimg_explainer">NostrImg 可以修改文件</string>
<string name="upload_server_nostrbuild">nostr.build - 受信任的</string>
<string name="upload_server_nostrbuild_explainer">NostrImg 可以修改文件</string>
<string name="upload_server_nostrfilesdev">nostrfiles.dev - 受信任的</string>
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev 可以修改文件</string>
<string name="upload_server_nostrcheckme">nostrcheck.me - 受信任的</string>
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me 可以修改文件</string>
<string name="upload_server_imgur_nip94">可验证的 Imgur (NIP-94)</string>
<string name="upload_server_imgur_nip94_explainer">检查 Imgur 是否修改了文件。新的 NIP其他客户端可能看不到它</string>
<string name="upload_server_nostrimg_nip94">可验证的 NostrImg (NIP-94)</string>
<string name="upload_server_nostrimg_nip94_explainer">检查 NostrImg 是否修改了文件。新的 NIP其他客户端可能看不到它</string>
<string name="upload_server_nostrbuild_nip94">可验证的 Nostr.build (NIP-94)</string>
<string name="upload_server_nostrbuild_nip94_explainer">检查 Nostr.build 是否修改了文件。新的 NIP其他客户端可能看不到它</string>
<string name="upload_server_nostrfilesdev_nip94">可验证的 Nostrfiles.dev (NIP-94)</string>
<string name="upload_server_nostrfilesdev_nip94_explainer">检查 Nostrfiles.dev 是否修改了文件。新的 NIP其他客户端可能看不到它</string>
<string name="upload_server_nostrcheckme_nip94">可验证的 Nostrcheck.me (NIP-94)</string>
<string name="upload_server_nostrcheckme_nip94_explainer">检查 Nostrcheck.me 是否修改了文件。新的 NIP其他客户端可能看不到它</string>
<string name="upload_server_relays_nip95">你的中继器 (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">文件由你的继电器托管。新NIP检查它们是否支持</string>
<string name="connect_via_tor_short">Tor/Orbot 设置</string>
<string name="connect_via_tor">通过你的 Orbot 设置连接</string>
<string name="do_you_really_want_to_disable_tor_title">断开与你的 Orbot/Tor 连接?</string>
<string name="do_you_really_want_to_disable_tor_text">你的数据将立即在普通网络上传输</string>
<string name="yes"></string>
<string name="no"></string>
<string name="follow_list_selection">关注列表</string>
<string name="follow_list_kind3follows">所有关注</string>
<string name="follow_list_global">全球</string>
<string name="connect_through_your_orbot_setup_markdown"> ## 通过 Orbot 连线 Tor
\n\n1. 安装 [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. 开启 Orbot
\n3. 在 Orbot 中检查 Socks 端口。默认使用9050
\n4. 如果需要,在 Orbot 中更改端口
\n5. 在此屏幕中配置 Socks 端口
\n6. 按“启用”按钮以使用 Orbot 作为代理
</string>
<string name="orbot_socks_port">Orbot Socks 端口</string>
<string name="invalid_port_number">无效端口</string>
<string name="use_orbot">使用 Orbot</string>
@ -298,6 +347,8 @@
<string name="app_notification_zaps_channel_name">收到打闪</string>
<string name="app_notification_zaps_channel_description">收到打闪时通知你</string>
<string name="app_notification_zaps_channel_message">%1$s聪</string>
<string name="app_notification_zaps_channel_message_from">来自 %1$s</string>
<string name="app_notification_zaps_channel_message_for">为 %1$s</string>
<string name="reply_notify">通知:</string>
<string name="channel_list_join_conversation">加入对话</string>
<string name="channel_list_user_or_group_id">用户或群组 ID</string>
@ -306,17 +357,23 @@
<string name="channel_list_join_channel">加入</string>
<string name="today">今天</string>
<string name="content_warning">内容警告</string>
<string name="content_warning_explanation">这个帖子包含敏感内容,一些人可能会觉得有冒犯性或令人不安。</string>
<string name="content_warning_hide_all_sensitive_content">始终隐藏敏感内容</string>
<string name="content_warning_show_all_sensitive_content">始终显示敏感内容</string>
<string name="content_warning_see_warnings">始终显示内容警告</string>
<string name="recommended_apps">推荐:</string>
<string name="filter_spam_from_strangers">过滤来自陌生人的垃圾信息</string>
<string name="warn_when_posts_have_reports_from_your_follows">当帖子有你的关注的报告时警告</string>
<string name="new_reaction_symbol">新反应符号</string>
<string name="no_reaction_type_setup_long_press_to_change">未选择回应类型。长按可更改</string>
<string name="zapraiser">Zapraiser</string>
<string name="zapraiser_explainer">为这个帖子添加目标聪金额。支持的客户端可以将此显示为奖励捐赠的进度条</string>
<string name="zapraiser_target_amount_in_sats">目标聪金额</string>
<string name="sats_to_complete">Zapraiser 位于 %1$s。距离目标%2$s聪</string>
<string name="read_from_relay">从中继器读取</string>
<string name="write_to_relay">写入到继电器</string>
<string name="an_error_occurred_trying_to_get_relay_information">尝试从 %1$s 获取中继器信息时出错</string>
<string name="owner">机主</string>
<string name="version">版本</string>
<string name="software">软件</string>
<string name="contact">联络</string>
@ -327,28 +384,38 @@
<string name="countries">国家</string>
<string name="languages">语言</string>
<string name="tags">标签</string>
<string name="posting_policy">发布政策</string>
<string name="message_length">消息长度</string>
<string name="subscriptions">订阅</string>
<string name="filters">筛选器</string>
<string name="subscription_id_length">订阅 ID 长度</string>
<string name="minimum_prefix">最少前缀</string>
<string name="maximum_event_tags">最多事件标签</string>
<string name="content_length">内容长度</string>
<string name="minimum_pow">最低工作量证明</string>
<string name="auth">认证</string>
<string name="payment">支付</string>
<string name="cashu">Cashu 代币</string>
<string name="cashu_redeem">兑换</string>
<string name="no_lightning_address_set">未设置闪电地址</string>
<string name="copied_token_to_clipboard">已复制令牌至剪贴板</string>
<string name="live_stream_live_tag">直播</string>
<string name="live_stream_offline_tag">离线</string>
<string name="live_stream_ended_tag">已结束</string>
<string name="live_stream_planned_tag">预定</string>
<string name="live_stream_is_offline">直播处于离线状态</string>
<string name="live_stream_has_ended">直播已结束</string>
<string name="are_you_sure_you_want_to_log_out">登出将删除你的本地信息。 请确保备份你的私钥以避免失去你的帐户。你想要继续吗?</string>
<string name="followed_tags">已关注的标签</string>
<string name="relay_setup">中继器</string>
<string name="discover_live">直播</string>
<string name="discover_community">社区</string>
<string name="discover_chat">聊天</string>
<string name="community_approved_posts">批准帖子</string>
<string name="groups_no_descriptor">此群组没有描述或规则。联系群主来添加</string>
<string name="community_no_descriptor">此社区没有描述或规则。联系群主来添加</string>
<string name="add_sensitive_content_label">敏感内容</string>
<string name="add_sensitive_content_description">在显示此内容之前添加敏感内容警告</string>
<string name="settings">设置</string>
<string name="connectivity_type_always">始终</string>
<string name="connectivity_type_wifi_only">仅限 Wifi</string>
@ -363,16 +430,23 @@
<string name="automatically_play_videos">视频播放</string>
<string name="automatically_show_url_preview">URL 预览</string>
<string name="load_image">加载图像</string>
<string name="spamming_users">垃圾邮件</string>
<string name="muted_button">静音。点击取消静音</string>
<string name="mute_button">声音开启。点击静音</string>
<string name="search_button">搜索本地和远程记录</string>
<string name="nip05_verified">Nostr 地址已验证</string>
<string name="nip05_failed">Nostr 地址验证失败</string>
<string name="nip05_checking">正在检查 Nostr 地址</string>
<string name="select_deselect_all">全部选择/取消选择</string>
<string name="default_relays">默认</string>
<string name="select_a_relay_to_continue">选择中继器以继续</string>
<string name="zap_forward_title">将打闪转发到:</string>
<string name="zap_forward_explainer">支持的客户端会将打闪转发到以下的闪电地址或用户个人档案,而不是你的</string>
<string name="geohash_title">将位置显示为 </string>
<string name="geohash_explainer">将你所在位置的地理位置添加到帖子。公众会知道你在当前位置的5千米之内(3mi)</string>
<string name="add_sensitive_content_explainer">在显示你的内容之前添加敏感的内容警告。针对任何 NSFW 内容或一些人可能觉得有冒犯性或令人不安的内容。</string>
<string name="new_feature_nip24_might_not_be_available_title">新功能</string>
<string name="new_feature_nip24_might_not_be_available_description">启用此模式需要 Amethyst 发送一条 NIP-24 消息(包装的、密封的私信和群聊消息)。因为 NIP-24 是新的,大多数客户端尚未执行。请确保接收方正在使用兼容的客户端。</string>
<string name="new_feature_nip24_activate">启用</string>
<string name="messages_create_public_chat">公开</string>
<string name="messages_new_message">私人</string>
@ -382,6 +456,7 @@
<string name="messages_new_message_to_caption">"\@User1、@User2、@User3"</string>
<string name="messages_group_descriptor">此群组成员</string>
<string name="messages_new_subject_message">对成员的解释</string>
<string name="messages_new_subject_message_placeholder">为新目标更改名称。</string>
<string name="language_description">用于应用程序界面</string>
<string name="theme_description">暗色、亮色或系统主题</string>
<string name="automatically_load_images_gifs_description">自动加载图像和 GIF</string>
@ -394,7 +469,18 @@
<string name="rules">规则</string>
<string name="status_update">更新你的状态</string>
<string name="lightning_wallets_not_found">错误解析错误消息</string>
<string name="poll_zap_value_min_max_explainer">投票按照打闪金额进行加权。 你可以设置最小金额以避免垃圾邮件,也可以设置最大金额以避免大型攻击者攻击民意调查。在两个字段中使用相同的金额,以确保每张选票的价值相同。 留空即可接受任何金额。</string>
<string name="error_dialog_zap_error">无法发送打闪</string>
<string name="error_dialog_talk_to_user">向用户发送消息</string>
<string name="error_dialog_button_ok"></string>
<string name="relay_information_document_error_assemble_url">无法连接 %1$s: %2$s</string>
<string name="relay_information_document_error_reach_server">无法连接 %1$s: %2$s</string>
<string name="relay_information_document_error_parse_result">无法解析来自 %1$s的结果: %2$s</string>
<string name="relay_information_document_error_http_status">%1$s 失败,代码 %2$s</string>
<string name="active_for">活跃于: </string>
<string name="active_for_home">主页</string>
<string name="active_for_msg">私信</string>
<string name="active_for_chats">聊天</string>
<string name="active_for_global">全球</string>
<string name="active_for_search">搜索</string>
</resources>

Wyświetl plik

@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
import android.util.LruCache
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
@ -8,6 +9,7 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -15,19 +17,34 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class PushNotificationReceiverService : FirebaseMessagingService() {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventCache = LruCache<String, String>(100)
// this is called when a message is received
override fun onMessageReceived(remoteMessage: RemoteMessage) {
scope.launch(Dispatchers.IO) {
remoteMessage.data.let {
val eventStr = remoteMessage.data["event"] ?: return@let
val event = Event.fromJson(eventStr)
EventNotificationConsumer(applicationContext).consume(event)
parseMessage(remoteMessage.data)?.let {
receiveIfNew(it)
}
}
}
private suspend fun parseMessage(params: Map<String, String>): GiftWrapEvent? {
params["encryptedEvent"]?.let { eventStr ->
(Event.fromJson(eventStr) as? GiftWrapEvent)?.let {
return it
}
}
return null
}
private suspend fun receiveIfNew(event: GiftWrapEvent) {
if (eventCache.get(event.id) == null) {
eventCache.put(event.id, event.id)
EventNotificationConsumer(applicationContext).consume(event)
}
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()

Wyświetl plik

@ -5,6 +5,8 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.decodePublicKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

Wyświetl plik

@ -5,7 +5,9 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.decodePublicKey
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent
@ -557,4 +559,28 @@ class GiftWrapEventTest {
null
}
}
@Test
fun decryptMsgFromNostrTools() {
val receiversPrivateKey = Hex.decode("df51ec558372612918a83446d279d683039bece79b7a721274b1d3cb612dc6af")
val msg = """
{
"tags": [],
"content": "AUC1i3lHsEOYQZaqav8jAw/Dv25r6BpUX4r7ARaj/7JEqvtHkbtaWXEx3LvMlDJstNX1C90RIelgYTzxb4Xnql7zFmXtxGGd/gXOZzW/OCNWECTrhFTruZUcsyn2ssJMgEMBZKY3PgbAKykHlGCuWR3KI9bo+IA5sTqHlrwDGAysxBypRuAxTdtEApw1LSu2A+1UQsdHK/4HcW/fQLPguWGyPv09dftJIJkFWM8VYBQT7b5FeAEMhjlUM+lEmLMnx6qb07Ji/YMESkhzFlgGjHNVl1Q/BT4i6X+Skogl6Si3lWQzlS9oebUim1BQW+RO0IOyQLalZwjzGP+eE7Ry62ukQg7cPiqk62p7NNula17SF2Q8aVFLxr8WjbLXoWhZOWY25uFbTl7OPGGQb5TewRsjHoFeU4h05Ien3Ymf1VPqJVJCMIxU+yFZ1IMZh/vQW4BSx8VotRdNA05fz03ST88GzGxUvqEm4VW/Yp5q4UUkCDQTKmUImaSFmTser39WmvS5+dHY6ne4RwnrZR0ZYrG1bthRHycnPmaJiYsHn9Ox37EzgLR07pmNxr2+86NR3S3TLAVfTDN3XaXRee/7UfW/MXULVyuyweksIHOYBvANC0PxmGSs4UiFoCbwNi45DT2y0SwP6CxzDuM=",
"kind": 1059,
"created_at": 1694192155914,
"pubkey": "8253eb518413b57f0df329d3d4287bdef4031fd71c32ad1952d854e703dae6a7",
"id": "ae625fd43612127d63bfd1967ba32ae915100842a205fc2c3b3fc02ab3827f08",
"sig": "2807a7ab5728984144676fd34686267cbe6fe38bc2f65a3640ba9243c13e8a1ae5a9a051e8852aa0c997a3623d7fa066cf2073a233c6d7db46fb1a0d4c01e5a3"
}
""".trimIndent()
val wrap = Event.fromJson(msg) as GiftWrapEvent
wrap.checkSignature()
val event = wrap.unwrap(receiversPrivateKey)
assertNotNull(event)
println(event)
}
}

Wyświetl plik

@ -12,18 +12,19 @@ fun HexKey.hexToByteArray(): ByteArray {
}
object HexValidator {
private fun isHex2(c: Char): Boolean {
private fun isHexChar(c: Char): Boolean {
return when (c) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', ' ' -> true
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F' -> true
else -> false
}
}
fun isHex(hex: String?): Boolean {
if (hex == null) return false
if (hex.length % 2 != 0) return false // must be even
var isHex = true
for (c in hex.toCharArray()) {
if (!isHex2(c)) {
if (!isHexChar(c)) {
isHex = false
break
}

Wyświetl plik

@ -21,7 +21,7 @@ class LnZapEvent(
fromJson(it)
} as? LnZapRequestEvent
} catch (e: Exception) {
Log.w("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
Log.w("LnZapEvent", "Failed to Parse Contained Post ${description()} in event ${id}", e)
null
}