From 3e32a91e34691fccc29107d1bbd23fda64bbaa67 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 03:02:04 +0800 Subject: [PATCH 01/13] Move nsec backup to Drawer and Dialog, organize Drawer --- .../amethyst/ui/navigation/DrawerContent.kt | 182 ++++++++++-------- .../amethyst/ui/navigation/Routes.kt | 2 +- .../ui/screen/loggedIn/AccountBackupDialog.kt | 123 ++++++++++++ .../ui/screen/loggedIn/ProfileScreen.kt | 39 ---- app/src/main/res/drawable-anydpi/ic_key.xml | 11 ++ .../main/res/drawable-anydpi/ic_logout.xml | 12 ++ .../main/res/drawable-anydpi/ic_security.xml | 11 ++ app/src/main/res/drawable-hdpi/ic_key.png | Bin 0 -> 380 bytes app/src/main/res/drawable-hdpi/ic_logout.png | Bin 0 -> 326 bytes .../main/res/drawable-hdpi/ic_security.png | Bin 0 -> 510 bytes app/src/main/res/drawable-mdpi/ic_key.png | Bin 0 -> 250 bytes app/src/main/res/drawable-mdpi/ic_logout.png | Bin 0 -> 199 bytes .../main/res/drawable-mdpi/ic_security.png | Bin 0 -> 339 bytes app/src/main/res/drawable-xhdpi/ic_key.png | Bin 0 -> 443 bytes app/src/main/res/drawable-xhdpi/ic_logout.png | Bin 0 -> 361 bytes .../main/res/drawable-xhdpi/ic_security.png | Bin 0 -> 662 bytes app/src/main/res/drawable-xxhdpi/ic_key.png | Bin 0 -> 675 bytes .../main/res/drawable-xxhdpi/ic_logout.png | Bin 0 -> 509 bytes .../main/res/drawable-xxhdpi/ic_security.png | Bin 0 -> 991 bytes app/src/main/res/values/strings.xml | 10 +- 20 files changed, 271 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt create mode 100644 app/src/main/res/drawable-anydpi/ic_key.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_logout.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_security.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_key.png create mode 100644 app/src/main/res/drawable-hdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-hdpi/ic_security.png create mode 100644 app/src/main/res/drawable-mdpi/ic_key.png create mode 100644 app/src/main/res/drawable-mdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-mdpi/ic_security.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_key.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_security.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_key.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_security.png diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index acdc717d0..5964847c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -7,12 +7,13 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -36,8 +37,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.FontWeight.Companion.W500 import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -45,14 +46,14 @@ import androidx.navigation.NavHostController import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.RoboHashCache +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.platform.LocalContext @Composable fun DrawerContent(navController: NavHostController, @@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController, modifier = Modifier .fillMaxWidth() .weight(1F), - accountStateViewModel + accountStateViewModel, + account, ) BottomContent(account.userProfile(), scaffoldState, navController) @@ -155,38 +157,44 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol if (accountUser.bestDisplayName() != null) { Text( accountUser.bestDisplayName() ?: "", - modifier = Modifier.padding(top = 7.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }), + modifier = Modifier + .padding(top = 7.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }), fontWeight = FontWeight.Bold, fontSize = 18.sp ) } if (accountUser.bestUsername() != null) { Text(" @${accountUser.bestUsername()}", color = Color.LightGray, - modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) ) } - Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - })) { + Row(modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + })) { Row() { Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) Text(stringResource(R.string.following)) @@ -206,66 +214,84 @@ fun ListContent( navController: NavHostController, scaffoldState: ScaffoldState, modifier: Modifier, - accountViewModel: AccountStateViewModel + accountViewModel: AccountStateViewModel, + account: Account, ) { val coroutineScope = rememberCoroutineScope() + var backupDialogOpen by remember { mutableStateOf(false) } - Column(modifier = modifier) { - LazyColumn() { - item { - if (accountUser != null) - NavigationRow(navController, - scaffoldState, - "User/${accountUser.pubkeyHex}", - Route.Profile.icon, - stringResource(R.string.profile) - ) + Column(modifier = modifier.fillMaxHeight()) { + if (accountUser != null) + NavigationRow( + title = stringResource(R.string.profile), + icon = Route.Profile.icon, + tint = MaterialTheme.colors.primary, + navController = navController, + scaffoldState = scaffoldState, + route = "User/${accountUser.pubkeyHex}", + ) - Divider( - modifier = Modifier.padding(bottom = 15.dp), - thickness = 0.25.dp - ) - Column(modifier = modifier.padding(horizontal = 25.dp)) { - Row(modifier = Modifier.clickable(onClick = { - navController.navigate(Route.Filters.route) - coroutineScope.launch { - scaffoldState.drawerState.close() - } - })) { - Text( - text = stringResource(R.string.security_filters), - fontSize = 18.sp, - fontWeight = W500 - ) - } - Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) { - Text( - text = stringResource(R.string.log_out), - modifier = Modifier.padding(vertical = 15.dp), - fontSize = 18.sp, - fontWeight = W500 - ) - } - } - } - } + Divider(thickness = 0.25.dp) + + NavigationRow( + title = stringResource(R.string.security_filters), + icon = Route.Filters.icon, + tint = MaterialTheme.colors.onBackground, + navController = navController, + scaffoldState = scaffoldState, + route = Route.Filters.route, + ) + + Divider(thickness = 0.25.dp) + + IconRow( + title = "Backup Keys", + icon = R.drawable.ic_key, + tint = MaterialTheme.colors.onBackground, + onClick = { backupDialogOpen = true } + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconRow( + "Logout", + R.drawable.ic_logout, + MaterialTheme.colors.onBackground, + onClick = { accountViewModel.logOff() } + ) + } + + if (backupDialogOpen) { + AccountBackupDialog(account, onClose = { backupDialogOpen = false }) } } @Composable -fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) { +fun NavigationRow( + title: String, + icon: Int, + tint: Color, + navController: NavHostController, + scaffoldState: ScaffoldState, + route: String, +) { val coroutineScope = rememberCoroutineScope() val currentRoute = currentRoute(navController) + IconRow(title, icon, tint, onClick = { + if (currentRoute != route) { + navController.navigate(route) + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) +} + +@Composable +fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) { Row(modifier = Modifier .fillMaxWidth() - .clickable(onClick = { - if (currentRoute != route) { - navController.navigate(route) - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + .clickable(onClick = onClick) ) { Row( modifier = Modifier @@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState Icon( painter = painterResource(icon), null, modifier = Modifier.size(22.dp), - tint = MaterialTheme.colors.primary + tint = tint ) Text( modifier = Modifier.padding(start = 16.dp), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 12595420b..94fc1b4ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -53,7 +53,7 @@ sealed class Route( buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} ) - object Filters : Route("Filters", R.drawable.ic_dm, + object Filters : Route("Filters", R.drawable.ic_security, buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }} ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt new file mode 100644 index 000000000..e56431e08 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -0,0 +1,123 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Key +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material.MaterialRichText +import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import kotlinx.coroutines.launch +import nostr.postr.toNsec + +@Composable +fun AccountBackupDialog(account: Account, onClose: () -> Unit) { + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onCancel = onClose) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + MaterialRichText( + style = RichTextStyle().resolveDefaults(), + ) { + Markdown( + content = stringResource(R.string.account_backup_tips_md), + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + NSecCopyButton(account) + } + } + } + } +} + +@Composable +private fun NSecCopyButton( + account: Account +) { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + account.loggedIn.privKey?.let { + clipboardManager.setText(AnnotatedString(it.toNsec())) + scope.launch { + Toast.makeText( + context, + context.getString(R.string.secret_key_copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + } + } + }, + shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Icon( + tint = Color.White, + imageVector = Icons.Default.Key, + contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup) + ) + Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index b16677610..df52949e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.EditNote -import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Share @@ -78,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import nostr.postr.toNsec @OptIn(ExperimentalPagerApi::class) @Composable @@ -337,10 +335,6 @@ private fun ProfileHeader( .padding(bottom = 3.dp)) { MessageButton(baseUser, navController) - if (accountUser == baseUser && account.isWriteable()) { - NSecCopyButton(account) - } - NPubCopyButton(baseUser) if (accountUser == baseUser) { @@ -637,40 +631,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: Nav } } -@Composable -private fun NSecCopyButton( - account: Account -) { - val clipboardManager = LocalClipboardManager.current - var popupExpanded by remember { mutableStateOf(false) } - Button( - modifier = Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { popupExpanded = true }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - ) { - Icon( - tint = Color.White, - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup) - ) - - DropdownMenu( - expanded = popupExpanded, - onDismissRequest = { popupExpanded = false } - ) { - DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) { - Text(stringResource(R.string.copy_private_key_to_the_clipboard)) - } - } - } -} @Composable private fun NPubCopyButton( diff --git a/app/src/main/res/drawable-anydpi/ic_key.xml b/app/src/main/res/drawable-anydpi/ic_key.xml new file mode 100644 index 000000000..0db74fa0d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_key.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_logout.xml b/app/src/main/res/drawable-anydpi/ic_logout.xml new file mode 100644 index 000000000..6555606de --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_logout.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_security.xml b/app/src/main/res/drawable-anydpi/ic_security.xml new file mode 100644 index 000000000..717b89c05 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_security.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_key.png b/app/src/main/res/drawable-hdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..81ef03db80d47008d6b2394c207b9644fa73f7c2 GIT binary patch literal 380 zcmV-?0fYXDP)H_=P_mn#{ia_i^rzhvE$s2q08I;pvB^JE57@K<~WY? a&)flbHGXVp^eyB70000C9@!Z5gW-(B?BUa%rd}RXps!?7LvhtW*Oiu1oE(-sbxTf z5P%dPt0GFs%ufsPJ~S8ZazS3vYbm7(cex-BUn^2i9;_zZ>p zX5RSTV2D91=c`7N?j+$e3*|!2B6z?|@EBXpQQv3WADkTH+Mj(aTij&ND$5B-c;TH-*I_R#T84kYTZih4Nnu^0*e{ zaV^N>&lQwD4_lL9&p3}oJ3cWKe}+cDiNzwuc`Rh-e^qgQL4d^~#yLvxF@?uXtn<&D zagGwQT>HTv8a!?yfd>}!jB}LG4+;$)H^EVZ1wG>&OrqY5o+7@Wx4|`zXJ~d#Jnph| zmN;I+E#MvYOeE2fg)QUQ0m-pV>PQ^g3@xzEIFz$^$v75pJgyh_4udCbX1=?uf^yu8 zDh6lsE&ElG0=u{NZ_8gUv0XU|MCN8H`(Rc^n`)jJ|8u8?ELiL*y*%n3zHHZ7CJQuW zL1=>Px#07*qoM6N<$f?1a0 ATmS$7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_key.png b/app/src/main/res/drawable-mdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..67341e7e157c2f11d8be2af586be98c575ceabe4 GIT binary patch literal 250 zcmV<22B6FRNAPjStw zNsXuvnLwH>&6$6qA~j|vI~jaNDI=N^?H7I+QNAZ#GT!|qE}3U-CX+ZwTz&+N&vc3P zZ=)b_S)AbP*G!lz?PoK1i)e)J9h_Hh*3q#$p5XwkU-a+l?EnA(07*qoM6N<$f}&1p A^Z)<= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_logout.png b/app/src/main/res/drawable-mdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2dabde5eb1761a359e093eca5b725b84facc11 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjeV#6kAr*0Nryk^OP~dTuJR~d8 zP*liXRv>jg?(Vg32UuI{ygTG7Je@CH_&-VRz)nss!Jh($&ON`ms)&z`^Sw|Bd&ZwI zf#f?&PW|KUZg~IOX1SLRQ)6?uN0KPVge7Vx{+fT)`03MdVXG3aM$86|AC-{{3Jxj> xSXiul*c8fO9CP5e&88)xPi1&|{}rg2>XmQlFWYosMGVko44$rjF6*2UngCdjO`HG# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_security.png b/app/src/main/res/drawable-mdpi/ic_security.png new file mode 100644 index 0000000000000000000000000000000000000000..3dc8abccedc4ff47494f035b24b59a6f4bcbb6be GIT binary patch literal 339 zcmV-Z0j&OsP)$-K$ z^8ff9+oDYcDinybh_A<6f zG~6IEcx{^b1{Mp~In5Z^6QP3K<8s&#*%MI|n16$kz%0GN(2ixYM}d`xIx@HxA?yCc zflT%&uo+QD2G=5Rws0VmJqkSmkioSGeasx$1Ejv(CVisxnph8r=ZISN^(abD!Fn09 zS}V?8Av4)?@K6IBhCUqW@T9dO^iN|Zb*vf38uj@Bm3K5JTiJecAv3002ovPDHLkV1oa-mAwD} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_key.png b/app/src/main/res/drawable-xhdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..79ffc2e6744e9fb3ea053e9da09d10c587150232 GIT binary patch literal 443 zcmV;s0Yv_ZP)Tog4284qI@{bK63ixBF4ApthIX4R-S)QeQ*FsL^9NyoO8%s$pxA~tKQKO>9LI4S z$8nr2UDusM2m}3okUsmq|AFZGf&VymwspJ1#^1$oNyw#|H+!F5ATw7oHCmGu8mabX z@3$#15HU7OyA6V8eqv~eHXj7`e7|hlq%>~&PLy6)#7P&Gf2pZak7x(a{=@)8v2ShM zf*5jaovLsDi6s+*eXuBJUO^(_<~giR9Jyb`v)g+lCnp)K@f6FK>4TWD$?{F9eBZXSf=e@4^hLGB>ye>=?N zAlQ7e1>v?l*&t%&3h}&op|l0~L5o|7n8}KGoq2$n%C!V@g?rg%PMg%`6Y+ZULRhz* zzS{c(7>k(~I6mLJ5L?Q*dda{VVD^@5C+=MLH81ejBm0tSA=mEtL%;(WNOyf=)Aa-Y lA+|}+?>LU*IF94|;R$kXlb_%7NR$8o002ovPDHLkV1iw>(m((J literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_logout.png b/app/src/main/res/drawable-xhdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..10fd5751c309573eb28cddc24d880b0cc876a40f GIT binary patch literal 361 zcmV-v0ha!WP)b;@6ht{hT2A40Lz_dSrA-b(3oWRvolzLj8CVcD2lgZ znx>DK@0eR_?b$Q*PDkP6;5&nC9r~v;&+`vM^bGyeX~uV7L;rMk570{$*qbV_f%xXT zYWVslMi)@<*IAYlBMU^}FP_(3f`-(cMA&ou@;`WHR|%R?cVjO>BkCDgOVEUR7Pb;J zpq`1P1UFMBT%R?oOK>A~!u6T6ssuMtCtRO7hc2)xyx>Oandn2@7H+6khZo#TJqt^C zK?CX;*uo2%PTP z-Wn;)6e~W|DfnKw|_}Wv6si=@mKl#d^jB5>ga#A?UM7(PATfD6n9$+ePQ8^ zylAZE(6c$Em|ZF6zENZ-Hg`wwr!)083u2-Nx5+wL>TeYDTE_jJdY#*s@+s>i+TXVG z8_wsrV8++UA=uy6O0#{;@x4i}6N(s#BWH;7vZ2?70)_n@fsSnGb)lfu-x27^hF+Ip zfk96;^tucS40^Jm*JW5>(31_lF2e$Yo^0rK85S7yWJ9mZu)v@v8+u)a1qMCY(CacR zFzCsKUYB8kK~FaHx(o{pda|L{h0p!m9)*QuqFU&6vB%^Ovfj`eK~K50L$z>>%Q+9A zJUITS9jOB-k~%sn4dOkqC zqfC6FtT%Le=v#?;zG~o@FqCCNrw1?Js)1v|=&dVxWl=q$a|wNHt>Gqk3e6_L^ z`j%*%{dC}VuT~oFeUiPg z(NohZUP;}!B4idc9TQgkAC6yT;V!Z7e8}T`p~cxc^Kx_S>e!hJ-=R6*TJghI^>VJ> z_jB|dL^Vfez4`8KTKt4|7+DkE&rx!0UVP2S^U~0B&YGw_N-=*~;9N9TbLcrcrMTTv wyV$n=pNqD}-Vy!UsKxnYufN_I|7HdL0m}ayRE`W09smFU07*qoM6N<$f__X;)Bpeg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_key.png b/app/src/main/res/drawable-xxhdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..e0cdfef0ab6512e256235cec672d450d15b7766c GIT binary patch literal 675 zcmV;U0$lxxP)8qv5QJU3PV4&G10)@gJV@N8joY+w>v!@Svy90G*ja4%wt6k6B?=CQNu7!6i zp^sgfwB^cS!6fd#YH&RBB(_%S^OeU@C26oH9{dee|LGRpT6TZ)Bk3jQz-Rn#My|TI z8gArx&_ha-d2bz895E^OgWHd2GUwg!UU8TcIeF|vgrWcCVqpy2z6vl29&DeY=48yI z$V%E&IEZ+Rqo8JRk^#-$0%+Tl?`{g2l!D(l0c(xr@7>0?lG`NDz_H4>U6jB|y_sAl zHIB!M@+hARa&X=nG-(ny=o7b@3+Jsxqb6~1pSaCjIBzW)Eg_!!l!I@vVROKP7wPGF zJ@7F5xr42RK6xA^<)p{V$x!@y=47JVV~KKMo&po~n3J*i z_00ix@=QK{EEW2QoQPFnCT+HokbAf0lx$IxrfSaaXxIDsi|cjKZb?`E+&La>SuhuE zIp1MH?XXkg&1%WDpf|-&@DS5H(ogV&5JCtcgb+dqA%qYQ%0Ff(uFsf>j7I?ac{IX@Wu?lY^}{h|ihs9aQ=!VBLy-cQ4JQQ-ZwGXFva0EYUW< zQ0D(SxqG>Hn?41yUT(9F2=86|kZ+ss~*9(4X}fZf`{lDPTXeN=eOTPL_W#-U|I&Ww${b~W&lJ+MxZ;Iu!>Ixh{xWv0qk%Se zBv;6|e`E?@pl8uF-GZs~aNwR3A}jd&9;NmeC+}n z!SR@9iP((wMl!Av2rIat2FFUNcZs(z=Bup-(bu+r00!ck*B>K~$gYX5zZcYX oL^f~YzB{WHRFp%aVcK8jR<)bS-%Pre0i%_{)78&qol`;+0QG9&hX4Qo literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_security.png b/app/src/main/res/drawable-xxhdpi/ic_security.png new file mode 100644 index 0000000000000000000000000000000000000000..dba255c73a9a08bbb1d8a13f0e8abae84f49d4d3 GIT binary patch literal 991 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2U|#0w;uum9_jdOF{>u&mZo$i! zE%oabu@>4Ae%YzoR9AP)mU7+yy1xaZqiid$e|zI2q+WSRJz&$k=XY-;$Jf0n^Pgu^ z$>O*C^7F?Ze*|Cr|MQRYB(dcC-~W35`tEfn~pI zS7pD@ugrf~oLyX8d+JvFCA0ULM)!ZNDxPB+{x(?mZ|b`If|~4SHS4x~o4)0(_%)R~ z=L`#LR~5g~_kQqAFmkv5<^Y4~LG_VL*{*GQ#yT-w$A6xiHF4o>9&Tgr-=~Eltc^-$ zCq6WM_x#hkH(eZe(;{xfPH=Qjd0}?=(7PrcD<$Xcl451zDHjAKcb(m^VYfna--!#k zZLN1%B}-gD2ENfdu|OGQMq;@mn=IJi-P}emI(TScRLGZomUq*>Ow(cwT)11bw!oo% zk!9hUnY#J`_rK475fCG;;khqJKy+8M=G)zJ&m)X~X|y-3xNz6&<%{pXM4s8Nyy4EZ z$f;cW*P1>)*WyPv)HFJ{_Uc}Vi|ET;RCX;)TzBP(+`@d_t#K=BpI4M;Sw9f`buH+Xtye^`+C+E3*z6TwPjt)JDp#on z-P36Z=>J7KH4{L;=T{zMcFSRz208Aw6CbfVwaQ*clj#ah3TAIgX4Zn z)IHUjSoYw6Y! z#ilmD1+)DFUt69E{q4s&_qE*gx@%`8<2qhlTvfZH>b~Z~jUH<(FU`NdZ^oYUlEs(1 zjW6x;SW|9ue@a-tRH`tLyU8QDH}9zbhAYKpn=O=`_ecC?yZu4W#m& znxC^>bn8p*B$JuVe$0LsW-XlddVSBjK0oHWH;Xo9FP55ZxAfVC(9F!wM-T1EioUix zx~=un#Q>YHZL23AvpILj=i*%bm%juz{ByamXvN0+;r*4Lo}3i<&+x2NQS!l?6`(B4 N;OXk;vd$@?2>|%H?qmP} literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 952cd426c..0871a623f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,7 +118,7 @@ Website Lightning Address Copies the Nsec ID (your password) to the clipboard for backup - Copy Private Key to the Clipboard + Copy Secret Key to the Clipboard Copies the public key to the clipboard for sharing Copy Public Key (NPub) to the Clipboard Send a Direct Message @@ -177,4 +177,12 @@ Report Hateful speech Report Nudity / Porn others + + ## Key Backup and Safety Tips + \n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. + \n\n- Do **not** put your secret key in any website or software you do not trust. + \n- Amethyst developers will **never** ask you for your secret key. + \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. + + Secret key (nsec) copied to clipboard \ No newline at end of file From 3674f6b354458821486ded21c3144096c6dc4863 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 21:22:54 +0100 Subject: [PATCH 02/13] Fix some typos in ES translations --- app/src/main/res/values-es/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 70267bb78..83c976168 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -24,7 +24,7 @@ Copiar PubKey del usuario Copiar ID de la nota Transmisión - Bloquear y ocultar usuario> + Bloquear y ocultar usuario Reportar Spam / Estafa Reportar suplantación de identidad Reportar contenido explícito @@ -46,7 +46,7 @@ Banner de perfil " Siguiendo" " Seguidores" - Peril + Perfil Filtros de seguridad Cerrar sesión Mostrar más @@ -67,7 +67,7 @@ Descripción "Sobre nosotros… " ¿Qué tienes en mente? - Publicación + Enviar Guardar Crear Cancelar @@ -100,15 +100,15 @@ Copia la nota-ID del canal Edita los metadatos del canal Unirse - Conocido + Conocidos Nuevas solicitudes Usuarios bloqueados Temas nuevos Conversaciones Notas Respuestas - "Sigue" - "Reportes" + Siguiendo + Reportes Más opciones " Retransmisores" Sitio web @@ -136,7 +136,7 @@ Se requiere la clave Acceso Generar una nueva clave - Cargando el tablón + Cargando el tablón… "Error al cargar las respuestas: " Intentar otra vez Tablón vacío @@ -150,7 +150,7 @@ Dejar Dejar de seguir Canal creado - "La información del canal cambió a" + La información del canal cambió a Chat público publicaciones recibidas Eliminar @@ -160,7 +160,7 @@ a Mostrar en primero - Traducir siempre a + "Traducir siempre a " NIP-05 LNURL... nunca From f3bc190b0b7be7959490513a059bfaf351900901 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 21:42:40 +0100 Subject: [PATCH 03/13] Use boost=impulsar --- app/src/main/res/values-es/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 83c976168..c135450e2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -36,7 +36,7 @@ Inicie sesión con una clave privada para poder enviar Zaps Zaps Total vistas - Aumentar + Impulsar Cita Nueva cantidad en Sats Añadir From 5ae552117ddb537f885220c03d2d3333689c3dfd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 16:00:47 -0500 Subject: [PATCH 04/13] Migrates Events to the App's source code as opposed to NostrPostr: - Changes them to remove all secondary fields and turn them into functions - Changes them to from being based in ByteArrays to String (since we use Hex everywhere and strings are immutable, we avoid duplicating memory with ByteArrays) --- app/proguard-rules.pro | 2 +- .../amethyst/LocalPreferences.kt | 6 +- .../vitorpamplona/amethyst/model/Account.kt | 96 ++------- .../amethyst/model/AntiSpamFilter.kt | 10 +- .../amethyst/model/LocalCache.kt | 86 ++++---- .../com/vitorpamplona/amethyst/model/Note.kt | 20 +- .../com/vitorpamplona/amethyst/model/User.kt | 4 +- .../service/NostrAccountDataSource.kt | 4 +- .../service/NostrChatroomDataSource.kt | 2 +- .../service/NostrChatroomListDataSource.kt | 2 +- .../amethyst/service/NostrDataSource.kt | 67 +++--- .../NostrSearchEventOrUserDataSource.kt | 2 +- .../service/NostrSingleUserDataSource.kt | 2 +- .../service/NostrUserProfileDataSource.kt | 4 +- .../amethyst/service/model/ATag.kt | 8 +- .../service/model/ChannelCreateEvent.kt | 14 +- .../service/model/ChannelHideMessageEvent.kt | 13 +- .../service/model/ChannelMessageEvent.kt | 13 +- .../service/model/ChannelMetadataEvent.kt | 14 +- .../service/model/ChannelMuteUserEvent.kt | 14 +- .../service/model/ContactListEvent.kt | 59 ++++++ .../amethyst/service/model/DeletionEvent.kt | 30 +++ .../amethyst/service/model/Event.kt | 197 ++++++++++++++++++ .../amethyst/service/model/LnZapEvent.kt | 9 +- .../service/model/LnZapRequestEvent.kt | 21 +- .../service/model/LongTextNoteEvent.kt | 14 +- .../amethyst/service/model/MetadataEvent.kt | 48 +++++ .../amethyst/service/model/PrivateDmEvent.kt | 86 ++++++++ .../amethyst/service/model/ReactionEvent.kt | 15 +- .../service/model/RecommendRelayEvent.kt | 37 ++++ .../amethyst/service/model/ReportEvent.kt | 21 +- .../amethyst/service/model/RepostEvent.kt | 17 +- .../amethyst/service/model/TextNoteEvent.kt | 13 +- .../amethyst/service/relays/Client.kt | 6 +- .../amethyst/service/relays/EventVerifier.kt | 16 -- .../amethyst/service/relays/Relay.kt | 2 +- .../amethyst/service/relays/RelayPool.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 17 +- .../ui/screen/loggedIn/ChannelScreen.kt | 3 +- .../ui/screen/loggedIn/ChatroomListScreen.kt | 21 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 4 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 8 +- .../ui/screen/loggedIn/NotificationScreen.kt | 2 +- .../ui/screen/loggedIn/SearchScreen.kt | 6 +- 44 files changed, 729 insertions(+), 308 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bbee40884..5acc1b972 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -25,7 +25,7 @@ -keep class fr.acinq.secp256k1.jni.** { *; } # For the NostrPostr library -keep class nostr.postr.** { *; } --keep class nostr.postr.events.** { *; } +-keep class com.vitorpamplona.amethyst.service.model.** { *; } # Json parsing -keep class com.google.gson.reflect.** { *; } -keep class * extends com.google.gson.reflect.TypeToken diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 8684b11d1..38be7f0da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -8,9 +8,9 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.toByteArray import java.util.Locale import nostr.postr.Persona -import nostr.postr.events.ContactListEvent -import nostr.postr.events.Event -import nostr.postr.events.Event.Companion.getRefinedEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.toHex class LocalPreferences(context: Context) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ed433bebf..b91282926 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent +import com.vitorpamplona.amethyst.service.model.Contact import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent @@ -26,16 +27,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.Contact import nostr.postr.Persona -import nostr.postr.Utils -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import nostr.postr.toHex val DefaultChannels = setOf( "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr @@ -89,10 +86,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - if (contactList != null && contactList.follows.size > 0) { + if (contactList != null && follows.isNotEmpty()) { val event = ContactListEvent.create( - contactList.follows, + follows, relays, loggedIn.privKey!!) @@ -111,14 +109,7 @@ class Account( if (!isWriteable()) return loggedIn.privKey?.let { - val createdAt = Date().time / 1000 - val content = toString - val pubKey = Utils.pubkeyCreate(it) - val tags = listOf>() - val id = Event.generateId(pubKey, createdAt, MetadataEvent.kind, tags, content) - val sig = Utils.sign(id, it) - val event = MetadataEvent(id, pubKey, createdAt, tags, content, sig) - + val event = MetadataEvent.create(toString, loggedIn.privKey!!) Client.send(event) LocalCache.consume(event) } @@ -250,10 +241,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - val event = if (contactList != null && contactList.follows.size > 0) { + val event = if (contactList != null && follows.isNotEmpty()) { ContactListEvent.create( - contactList.follows.plus(Contact(user.pubkeyHex, null)), + follows.plus(Contact(user.pubkeyHex, null)), userProfile().relays, loggedIn.privKey!!) } else { @@ -273,10 +265,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - if (contactList != null && contactList.follows.size > 0) { + if (contactList != null && follows.isNotEmpty()) { val event = ContactListEvent.create( - contactList.follows.filter { it.pubKeyHex != user.pubkeyHex }, + follows.filter { it.pubKeyHex != user.pubkeyHex }, userProfile().relays, loggedIn.privKey!!) @@ -320,37 +313,6 @@ class Account( LocalCache.consume(signedEvent, null) } - fun createPrivateMessageWithReply( - recipientPubKey: ByteArray, - msg: String, - replyTos: List? = null, mentions: List? = null, - privateKey: ByteArray, - createdAt: Long = Date().time / 1000, - publishedRecipientPubKey: ByteArray? = null, - advertiseNip18: Boolean = true - ): PrivateDmEvent { - val content = Utils.encrypt( - if (advertiseNip18) { - PrivateDmEvent.nip18Advertisement - } else { "" } + msg, - privateKey, - recipientPubKey) - val pubKey = Utils.pubkeyCreate(privateKey) - val tags = mutableListOf>() - publishedRecipientPubKey?.let { - tags.add(listOf("p", publishedRecipientPubKey.toHex())) - } - replyTos?.forEach { - tags.add(listOf("e", it)) - } - mentions?.forEach { - tags.add(listOf("p", it)) - } - val id = Event.generateId(pubKey, createdAt, PrivateDmEvent.kind, tags, content) - val sig = Utils.sign(id, privateKey) - return PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) - } - fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { if (!isWriteable()) return val user = LocalCache.users[toUser] ?: return @@ -358,7 +320,7 @@ class Account( val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = emptyList() - val signedEvent = createPrivateMessageWithReply( + val signedEvent = PrivateDmEvent.create( recipientPubKey = user.pubkey(), publishedRecipientPubKey = user.pubkey(), msg = message, @@ -386,7 +348,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun joinChannel(idHex: String) { @@ -438,7 +400,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun decryptContent(note: Note): String? { @@ -446,26 +408,12 @@ class Account( return if (event is PrivateDmEvent && loggedIn.privKey != null) { var pubkeyToUse = event.pubKey - val recepientPK = event.recipientPubKey + val recepientPK = event.recipientPubKey() if (note.author == userProfile() && recepientPK != null) pubkeyToUse = recepientPK - return try { - val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse) - - val retVal = Utils.decrypt(event.content, sharedSecret) - - if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) { - retVal.substring(16) - } else { - retVal - } - - } catch (e: Exception) { - e.printStackTrace() - null - } + event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) } else { event?.content } @@ -495,10 +443,10 @@ class Account( } private fun updateContactListTo(newContactList: ContactListEvent?) { - if (newContactList?.follows.isNullOrEmpty()) return + if (newContactList?.follows().isNullOrEmpty()) return // Events might be different objects, we have to compare their ids. - if (backupContactList?.id?.toHex() != newContactList?.id?.toHex()) { + if (backupContactList?.id != newContactList?.id) { backupContactList = newContactList saveable.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 3fec4edf8..a4bb82eae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import nostr.postr.toHex data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set) @@ -22,7 +22,7 @@ class AntiSpamFilter { @Synchronized fun isSpam(event: Event): Boolean { - val idHex = event.id.toHexKey() + val idHex = event.id // if already processed, ok if (LocalCache.notes[idHex] != null) return false @@ -37,15 +37,15 @@ class AntiSpamFilter { val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) { - Log.w("Potential SPAM Message", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") + Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") // Log down offenders if (spamMessages.get(hash) == null) { - spamMessages.put(hash, Spammer(event.pubKey.toHexKey(), setOf(recentMessages[hash], event.id.toHex()))) + spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id))) liveSpam.invalidateData() } else { val spammer = spamMessages.get(hash) - spammer.duplicatedMessages = spammer.duplicatedMessages + event.id.toHex() + spammer.duplicatedMessages = spammer.duplicatedMessages + event.id liveSpam.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 66b388368..ae3f07104 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -17,6 +17,13 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex import java.io.ByteArrayInputStream @@ -32,13 +39,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.RecommendRelayEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent import nostr.postr.toHex import nostr.postr.toNpub @@ -139,7 +139,7 @@ object LocalCache { fun consume(event: MetadataEvent) { // new event - val oldUser = getOrCreateUser(event.pubKey.toHexKey()) + val oldUser = getOrCreateUser(event.pubKey) if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { val newUser = try { metadataParser.readValue( @@ -173,8 +173,8 @@ object LocalCache { return } - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -220,7 +220,7 @@ object LocalCache { } val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -228,7 +228,7 @@ object LocalCache { } // Already processed this event. - if (note.event?.id?.toHex() == event.id.toHex()) return + if (note.event?.id == event.id) return val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } @@ -284,14 +284,15 @@ object LocalCache { } fun consume(event: ContactListEvent) { - val user = getOrCreateUser(event.pubKey.toHexKey()) + val user = getOrCreateUser(event.pubKey) + val follows = event.follows() - if (event.createdAt > user.updatedFollowsAt && event.follows.isNotEmpty()) { + if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) { // Saves relay list only if it's a user that is currently been seen user.latestContactList = event user.updateFollows( - event.follows.map { + follows.map { try { val pubKey = decodePublicKey(it.pubKeyHex) getOrCreateUser(pubKey.toHexKey()) @@ -316,20 +317,17 @@ object LocalCache { user.updateRelays(relays) } } catch (e: Exception) { - println("relay import issue") + Log.w("Relay List Parser","Relay import issue ${e.message}", e) e.printStackTrace() } - Log.d( - "CL", - "AAA ${user.toBestDisplayName()} ${event.follows.size}" - ) + Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") } } fun consume(event: PrivateDmEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -339,7 +337,7 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val recipient = event.recipientPubKey?.let { getOrCreateUser(it.toHexKey()) } + val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") @@ -359,9 +357,9 @@ object LocalCache { fun consume(event: DeletionEvent) { var deletedAtLeastOne = false - event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote -> + event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote -> // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) { + if (deleteNote.author?.pubkeyHex == event.pubKey) { deleteNote.author?.removeNote(deleteNote) // reverts the add @@ -395,14 +393,14 @@ object LocalCache { } fun consume(event: RepostEvent) { - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -429,12 +427,12 @@ object LocalCache { } fun consume(event: ReactionEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -475,8 +473,8 @@ object LocalCache { } fun consume(event: ReportEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -507,13 +505,13 @@ object LocalCache { fun consume(event: ChannelCreateEvent) { //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") // new event - val oldChannel = getOrCreateChannel(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val oldChannel = getOrCreateChannel(event.id) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -530,12 +528,12 @@ object LocalCache { // new event val oldChannel = checkGetOrCreateChannel(channelId) ?: return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -559,10 +557,10 @@ object LocalCache { val channel = checkGetOrCreateChannel(channelId) ?: return - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) channel.addNote(note) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -606,14 +604,14 @@ object LocalCache { } fun consume(event: LnZapEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) } + val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + @@ -645,15 +643,15 @@ object LocalCache { } fun consume(event: LnZapRequestEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 12d130b8a..892792119 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") @@ -72,7 +72,7 @@ open class Note(val idHex: String) { val channelHex = (event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel() ?: - (event as? ChannelCreateEvent)?.let { it.id.toHexKey() } + (event as? ChannelCreateEvent)?.let { it.id } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } @@ -251,6 +251,22 @@ open class Note(val idHex: String) { }?.isNotEmpty() ?: false) } + fun directlyCiteUsersHex(): Set { + val matcher = tagSearch.matcher(event?.content ?: "") + val returningList = mutableSetOf() + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) } + if (tag != null && tag[0] == "p") { + returningList.add(tag[1]) + } + } catch (e: Exception) { + + } + } + return returningList + } + fun directlyCiteUsers(): Set { val matcher = tagSearch.matcher(event?.content ?: "") val returningList = mutableSetOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 1b7de0513..d0f0e41f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -18,8 +18,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.Bech32 -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toNpub val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index e7958ff7c..daa2fe6b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -9,8 +9,8 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrAccountDataSource: NostrDataSource("AccountData") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 1e08e1bea..7ad19edad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index e1e721344..cdf92ad24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 2956fdd7c..ffdde932b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import android.util.Log import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent @@ -15,7 +16,6 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Subscription -import com.vitorpamplona.amethyst.service.relays.hasValidSignature import java.util.Date import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean @@ -26,12 +26,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.RecommendRelayEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent abstract class NostrDataSource(val debugName: String) { @@ -62,39 +62,32 @@ abstract class NostrDataSource(val debugName: String) { try { when (event) { - is MetadataEvent -> LocalCache.consume(event) - //is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote - is RecommendRelayEvent -> LocalCache.consume(event) + is ChannelCreateEvent -> LocalCache.consume(event) + is ChannelHideMessageEvent -> LocalCache.consume(event) + is ChannelMessageEvent -> LocalCache.consume(event, relay) + is ChannelMetadataEvent -> LocalCache.consume(event) + is ChannelMuteUserEvent -> LocalCache.consume(event) is ContactListEvent -> LocalCache.consume(event) - is PrivateDmEvent -> LocalCache.consume(event, relay) is DeletionEvent -> LocalCache.consume(event) - else -> when (event.kind) { - TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - RepostEvent.kind -> { - val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(repostEvent) - } - ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - - LnZapEvent.kind -> { - val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(zapEvent) - } - LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - - ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - - LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) + is LnZapEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is LnZapRequestEvent -> LocalCache.consume(event) + is LongTextNoteEvent -> LocalCache.consume(event, relay) + is MetadataEvent -> LocalCache.consume(event) + is PrivateDmEvent -> LocalCache.consume(event, relay) + is ReactionEvent -> LocalCache.consume(event) + is RecommendRelayEvent -> LocalCache.consume(event) + is ReportEvent -> LocalCache.consume(event, relay) + is RepostEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is TextNoteEvent -> LocalCache.consume(event, relay) + else -> { + Log.w("Event Not Supported", event.toJson()) } } } catch (e: Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index dee1eea3a..6f3abc42f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.bechToBytes -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toHex object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 387b08a1c..823d1d159 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { var usersToWatch = setOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 18fbafe2f..04ce33011 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -7,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 0ecfd8f5d..4a8ae0374 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -21,8 +21,8 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { val fullArray = byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + - byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + - byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind + byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + + byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) } @@ -41,7 +41,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { Hex.decode(parts[1]) ATag(parts[0].toInt(), parts[1], parts[2]) } catch (t: Throwable) { - Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}") + Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}") null } } @@ -62,7 +62,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { } } catch (e: Throwable) { - println("Issue trying to Decode NIP19 ${this}: ${e.message}") + Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}") //e.printStackTrace() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index 791aafecf..6a1d93f93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelCreateEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channelInfo() = try { MetadataEvent.gson.fromJson(content, ChannelData::class.java) @@ -35,11 +35,11 @@ class ChannelCreateEvent ( "" } - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = emptyList>() val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index 41c526e4f..4f8b57f18 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelHideMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } @@ -19,7 +20,7 @@ class ChannelHideMessageEvent ( fun create(reason: String, messagesToHide: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = messagesToHide?.map { listOf("e", it) @@ -27,7 +28,7 @@ class ChannelHideMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 39f0e6bae..411a258d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) @@ -22,7 +23,7 @@ class ChannelMessageEvent ( fun create(message: String, channel: String, replyTos: List? = null, mentions: List? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent { val content = message - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf( listOf("e", channel, "", "root") ) @@ -35,7 +36,7 @@ class ChannelMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 2552ad89c..d84ea2ab8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelMetadataEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun channelInfo() = @@ -33,11 +33,11 @@ class ChannelMetadataEvent ( else "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("e", originalChannelIdHex, "", "root") ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index 23c1c52fd..6b12e96bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -1,27 +1,27 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelMuteUserEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - companion object { const val kind = 44 fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = usersToMute?.map { listOf("p", it) @@ -29,7 +29,7 @@ class ChannelMuteUserEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt new file mode 100644 index 000000000..502fb7551 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -0,0 +1,59 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.reflect.TypeToken +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +data class Contact(val pubKeyHex: String, val relayUri: String?) + +class ContactListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun follows() = try { + tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) } + } catch (e: Exception) { + Log.e("ContactListEvent", "can't parse tags as follows: $tags", e) + null + } + + fun relayUse() = try { + if (content.isNotEmpty()) + gson.fromJson(content, object: TypeToken>() {}.type) + else + null + } catch (e: Exception) { + Log.e("ContactListEvent", "can't parse content as relay lists: $tags", e) + null + } + + companion object { + const val kind = 3 + + fun create(follows: List, relayUse: Map?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent { + val content = if (relayUse != null) + gson.toJson(relayUse) + else + "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = follows.map { + if (it.relayUri != null) + listOf("p", it.pubKeyHex, it.relayUri) + else + listOf("p", it.pubKeyHex) + } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } + + data class ReadWrite(val read: Boolean, val write: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt new file mode 100644 index 000000000..a29cd6261 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt @@ -0,0 +1,30 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +class DeletionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun deleteEvents() = tags.map { it[1] } + + companion object { + const val kind = 5 + + fun create(deleteEvents: List, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent { + val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = deleteEvents.map { listOf("e", it) } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt new file mode 100644 index 000000000..6ef8f2e24 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -0,0 +1,197 @@ +package com.vitorpamplona.amethyst.service.model + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.SerializedName +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import fr.acinq.secp256k1.Hex +import fr.acinq.secp256k1.Secp256k1 +import java.lang.reflect.Type +import java.security.MessageDigest +import java.util.Date +import nostr.postr.Utils +import nostr.postr.toHex + +open class Event( + val id: HexKey, + @SerializedName("pubkey") val pubKey: HexKey, + @SerializedName("created_at") val createdAt: Long, + val kind: Int, + val tags: List>, + val content: String, + val sig: HexKey +) { + fun toJson(): String = gson.toJson(this) + + fun generateId(): String { + val rawEvent = listOf( + 0, + pubKey, + createdAt, + kind, + tags, + content + ) + val rawEventJson = gson.toJson(rawEvent) + return sha256.digest(rawEventJson.toByteArray()).toHexKey() + } + + /** + * Checks if the ID is correct and then if the pubKey's secret key signed the event. + */ + fun checkSignature() { + if (!id.contentEquals(generateId())) { + throw Exception( + """|Unexpected ID. + | Event: ${toJson()} + | Actual ID: ${id} + | Generated: ${generateId()}""".trimIndent() + ) + } + if (!secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { + throw Exception("""Bad signature!""") + } + } + + fun hasValidSignature(): Boolean { + if (!id.contentEquals(generateId())) { + return false + } + if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { + return false + } + + return true + } + + class EventDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Event { + val jsonObject = json.asJsonObject + return Event( + id = jsonObject.get("id").asString, + pubKey = jsonObject.get("pubkey").asString, + createdAt = jsonObject.get("created_at").asLong, + kind = jsonObject.get("kind").asInt, + tags = jsonObject.get("tags").asJsonArray.map { + it.asJsonArray.map { s -> s.asString } + }, + content = jsonObject.get("content").asString, + sig = jsonObject.get("sig").asString + ) + } + } + + class EventSerializer : JsonSerializer { + override fun serialize( + src: Event, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + return JsonObject().apply { + addProperty("id", src.id) + addProperty("pubkey", src.pubKey) + addProperty("created_at", src.createdAt) + addProperty("kind", src.kind) + add("tags", JsonArray().also { jsonTags -> + src.tags.forEach { tag -> + jsonTags.add(JsonArray().also { jsonTagElement -> + tag.forEach { tagElement -> + jsonTagElement.add(tagElement) + } + }) + } + }) + addProperty("content", src.content) + addProperty("sig", src.sig) + } + } + } + + class ByteArrayDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): ByteArray = Hex.decode(json.asString) + } + + class ByteArraySerializer : JsonSerializer { + override fun serialize( + src: ByteArray, + typeOfSrc: Type?, + context: JsonSerializationContext? + ) = JsonPrimitive(src.toHex()) + } + + companion object { + private val secp256k1 = Secp256k1.get() + + val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") + val gson: Gson = GsonBuilder() + .disableHtmlEscaping() + .registerTypeAdapter(Event::class.java, EventSerializer()) + .registerTypeAdapter(Event::class.java, EventDeserializer()) + .registerTypeAdapter(ByteArray::class.java, ByteArraySerializer()) + .registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer()) + .create() + + fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) + + fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) + + fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) { + ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) + ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) + DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) + + LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) + LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) + MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) + PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) + ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) + RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient) + ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig) + RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig) + TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) + else -> this + } + + fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List>, content: String): ByteArray { + val rawEvent = listOf( + 0, + pubKey, + createdAt, + kind, + tags, + content + ) + val rawEventJson = gson.toJson(rawEvent) + return sha256.digest(rawEventJson.toByteArray()) + } + + fun create(privateKey: ByteArray, kind: Int, tags: List> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = Companion.generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey).toHexKey() + return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 2a38c8cf4..6b43b627b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -1,17 +1,16 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal -import nostr.postr.events.Event class LnZapEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 049b4b434..e17c6e672 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -1,17 +1,18 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class LnZapRequestEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } @@ -22,10 +23,10 @@ class LnZapRequestEvent ( fun create(originalNote: Event, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( - listOf("e", originalNote.id.toHex()), - listOf("p", originalNote.pubKey.toHex()), + listOf("e", originalNote.id), + listOf("p", originalNote.pubKey), listOf("relays") + relays ) if (originalNote is LongTextNoteEvent) { @@ -34,19 +35,19 @@ class LnZapRequestEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } fun create(userHex: String, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("p", userHex), listOf("relays") + relays ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 037c23c83..14fd66d47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -1,23 +1,23 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class LongTextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey.toHexKey(), dTag()) + fun address() = ATag(kind, pubKey, dTag()) fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() @@ -34,7 +34,7 @@ class LongTextNoteEvent( const val kind = 30023 fun create(msg: String, replyTos: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -44,7 +44,7 @@ class LongTextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt new file mode 100644 index 000000000..bd4c4c6a6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -0,0 +1,48 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.Gson +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +data class ContactMetaData( + val name: String, + val picture: String, + val about: String, + val nip05: String?) + +class MetadataEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun contactMetaData() = try { + gson.fromJson(content, ContactMetaData::class.java) + } catch (e: Exception) { + Log.e("MetadataEvent", "Can't parse $content", e) + null + } + + companion object { + const val kind = 0 + val gson = Gson() + + fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt) + } + + fun create(contactMetaData: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + val content = contactMetaData + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt new file mode 100644 index 000000000..d0f5111f1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -0,0 +1,86 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import fr.acinq.secp256k1.Hex +import java.util.Date +import nostr.postr.Utils +import nostr.postr.toHex + +class PrivateDmEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + /** + * This may or may not be the actual recipient's pub key. The event is intended to look like a + * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used + * for initial messages. + */ + fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one + + /** + * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. + * + * Nip-18 messages should refer to other events by inline references in the content like + * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). + */ + fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) + + fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { + return try { + val sharedSecret = Utils.getSharedSecret(privKey, pubKey) + + val retVal = Utils.decrypt(content, sharedSecret) + + if (retVal.startsWith(nip18Advertisement)) { + retVal.substring(16) + } else { + retVal + } + } catch (e: Exception) { + Log.w("PrivateDM", "Error decrypting the message ${e.message}") + null + } + } + + + companion object { + const val kind = 4 + + const val nip18Advertisement = "[//]: # (nip18)\n" + + fun create( + recipientPubKey: ByteArray, + msg: String, + replyTos: List? = null, mentions: List? = null, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000, + publishedRecipientPubKey: ByteArray? = null, + advertiseNip18: Boolean = true + ): PrivateDmEvent { + val content = Utils.encrypt( + if (advertiseNip18) { nip18Advertisement } else { "" } + msg, + privateKey, + recipientPubKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = mutableListOf>() + publishedRecipientPubKey?.let { + tags.add(listOf("p", publishedRecipientPubKey.toHex())) + } + replyTos?.forEach { + tags.add(listOf("e", it)) + } + mentions?.forEach { + tags.add(listOf("p", it)) + } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 6048a0978..c01972758 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -1,17 +1,18 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class ReactionEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } @@ -30,16 +31,16 @@ class ReactionEvent ( } fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) + var tags = listOf( listOf("e", originalNote.id), listOf("p", originalNote.pubKey)) if (originalNote is LongTextNoteEvent) { tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReactionEvent(id, pubKey, createdAt, tags, content, sig) + return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt new file mode 100644 index 000000000..a2ef2eaec --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt @@ -0,0 +1,37 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.net.URI +import java.util.Date +import nostr.postr.Utils + +class RecommendRelayEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey, + val lenient: Boolean = false +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + + fun relay() = if (lenient) + URI.create(content.trim()) + else + URI.create(content) + + + companion object { + const val kind = 2 + + fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent { + val content = relay.toString() + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index a1d83c155..4eebabae6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -1,20 +1,21 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) // NIP 56 event. class ReportEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { private fun defaultReportType(): ReportType { @@ -55,10 +56,10 @@ class ReportEvent ( fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { val content = "" - val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase()) - val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) + val reportPostTag = listOf("e", reportedPost.id, type.name.toLowerCase()) + val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.toLowerCase()) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = listOf(reportPostTag, reportAuthorTag) if (reportedPost is LongTextNoteEvent) { @@ -67,7 +68,7 @@ class ReportEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReportEvent(id, pubKey, createdAt, tags, content, sig) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { @@ -75,11 +76,11 @@ class ReportEvent ( val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase()) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags:List> = listOf(reportAuthorTag) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReportEvent(id, pubKey, createdAt, tags, content, sig) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 3da165f2c..74756c6d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -1,18 +1,19 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.relays.Client import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class RepostEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { @@ -32,10 +33,10 @@ class RepostEvent ( fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { val content = boostedPost.toJson() - val replyToPost = listOf("e", boostedPost.id.toHex()) - val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) + val replyToPost = listOf("e", boostedPost.id) + val replyToAuthor = listOf("p", boostedPost.pubKey) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) if (boostedPost is LongTextNoteEvent) { @@ -44,7 +45,7 @@ class RepostEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return RepostEvent(id, pubKey, createdAt, tags, content, sig) + return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index 9e1634b16..63839802b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class TextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } @@ -20,7 +21,7 @@ class TextNoteEvent( const val kind = 1 fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -33,7 +34,7 @@ class TextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 555d5d83a..776a670fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -4,7 +4,7 @@ import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event /** * The Nostr Client manages multiple personae the user may switch between. Events are received and @@ -38,9 +38,7 @@ object Client: RelayPool.Listener { if (relays.size != newRelayConfig.size) return false relays.forEach { oldRelayInfo -> - val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } - - if (newRelayInfo == null) return false + val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt deleted file mode 100644 index 3bbab0e16..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vitorpamplona.amethyst.service.relays - -import fr.acinq.secp256k1.Secp256k1 -import nostr.postr.events.Event -import nostr.postr.events.generateId - -fun Event.hasValidSignature(): Boolean { - if (!id.contentEquals(generateId())) { - return false - } - if (!Secp256k1.get().verifySchnorr(sig, id, pubKey)) { - return false - } - - return true -} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 214f5c1ae..dc28b944e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays import android.util.Log import com.google.gson.JsonElement import java.util.Date -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 7b96ca7ea..bb9952a82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event /** * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 8c353220a..9fd223e2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -38,7 +38,9 @@ import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent @@ -51,8 +53,9 @@ import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Following import kotlin.time.ExperimentalTime -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader @OptIn(ExperimentalFoundationApi::class) @Composable @@ -85,6 +88,7 @@ fun NoteCompose( var moreActionsExpanded by remember { mutableStateOf(false) } val noteEvent = note?.event + val baseChannel = note?.channel() if (noteEvent == null) { BlankNote(modifier.combinedClickable( @@ -100,6 +104,8 @@ fun NoteCompose( navController, onClick = { showHiddenNote = true } ) + } else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) { + ChannelHeader(baseChannel = baseChannel, account = account, navController = navController) } else { var isNew by remember { mutableStateOf(false) } @@ -134,9 +140,11 @@ fun NoteCompose( launchSingleTop = true } } else { - note.channel()?.let { - navController.navigate("Channel/${it.idHex}") - } + note + .channel() + ?.let { + navController.navigate("Channel/${it.idHex}") + } } }, onLongClick = { popupExpanded = true } @@ -176,7 +184,6 @@ fun NoteCompose( } // boosted picture - val baseChannel = note.channel() if (noteEvent is ChannelMessageEvent && baseChannel != null) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 55f9d219f..7fc6aff8b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -121,7 +121,6 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun Column(Modifier.fillMaxHeight()) { ChannelHeader( channel, account, - accountStateViewModel = accountStateViewModel, navController = navController ) @@ -213,7 +212,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun } @Composable -fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel, navController: NavController) { +fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavController) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 0087b96ef..d8959fd62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -91,7 +91,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -99,7 +99,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } } @@ -128,7 +128,22 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() // refresh view + feedViewModel.refresh() + } + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrChatroomListDataSource.resetFilters() + feedViewModel.refresh() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 1edbc6155..17bda6fcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -79,7 +79,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(userId) { - feedViewModel.invalidateData() + feedViewModel.refresh() } DisposableEffect(userId) { @@ -87,7 +87,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr if (event == Lifecycle.Event.ON_RESUME) { println("Private Message Start") NostrChatroomDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Private Message Stop") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 5b91d4c31..466ad2959 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -55,8 +55,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) LaunchedEffect(accountViewModel) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -64,8 +64,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6221913c7..ca0724285 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -36,7 +36,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon DisposableEffect(accountViewModel) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { - feedViewModel.invalidateData() + feedViewModel.refresh() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 7c66c1afe..65e5eb519 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -86,8 +86,8 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle val feedViewModel: NostrGlobalFeedViewModel = viewModel() val lifeCycleOwner = LocalLifecycleOwner.current - LaunchedEffect(Unit) { - feedViewModel.invalidateData() + LaunchedEffect(accountViewModel) { + feedViewModel.refresh() } DisposableEffect(accountViewModel) { @@ -95,7 +95,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle if (event == Lifecycle.Event.ON_RESUME) { println("Global Start") NostrGlobalDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Global Stop") From b4699159054700b6fa266d77c4eb117f21093501 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 16:12:11 -0500 Subject: [PATCH 05/13] v0.22.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 74c477013..36a0f7cc2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 84 - versionName "0.22.1" + versionCode 85 + versionName "0.22.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From b6e16ad4707a3a7d0d857354cc58903354bf86c0 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:40:46 +0100 Subject: [PATCH 06/13] Create Nip19Test testing toInt32() --- .../amethyst/service/Nip19Test.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt new file mode 100644 index 000000000..dd38517f2 --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -0,0 +1,23 @@ +package com.vitorpamplona.amethyst.service + +import org.junit.Assert +import org.junit.Test + +class Nip19Test { + + @Test(expected = IllegalArgumentException::class) + fun to_int_32_length_smaller_than_4() { + toInt32(ByteArray(3)) + } + + @Test(expected = IllegalArgumentException::class) + fun to_int_32_length_bigger_than_4() { + toInt32(ByteArray(5)) + } + + @Test() + fun to_int_32_length_4() { + val actual = toInt32(ByteArray(4)) + Assert.assertEquals(0, actual) + } +} From 47f3fe5cc62227727ba0f8f8a7131b99ba0b0149 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:46:15 +0100 Subject: [PATCH 07/13] refactor Nip19Test introduce byteArrayOfInts() --- .../com/vitorpamplona/amethyst/service/Nip19Test.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index dd38517f2..02e6a5695 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -7,17 +7,19 @@ class Nip19Test { @Test(expected = IllegalArgumentException::class) fun to_int_32_length_smaller_than_4() { - toInt32(ByteArray(3)) + toInt32(byteArrayOfInts(1, 2, 3)) } @Test(expected = IllegalArgumentException::class) fun to_int_32_length_bigger_than_4() { - toInt32(ByteArray(5)) + toInt32(byteArrayOfInts(1, 2, 3, 4, 5)) } @Test() fun to_int_32_length_4() { - val actual = toInt32(ByteArray(4)) - Assert.assertEquals(0, actual) + val actual = toInt32(byteArrayOfInts(1, 2, 3, 4)) + Assert.assertEquals(16909060, actual) } + + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From bd3d7e1aa3b6a971d183596cda25935035161f01 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:58:24 +0100 Subject: [PATCH 08/13] Prepare test parse_TLV --- .../main/java/com/vitorpamplona/amethyst/service/Nip19.kt | 6 +++--- .../java/com/vitorpamplona/amethyst/service/Nip19Test.kt | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index f4b551be1..d2d20b6df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -78,7 +78,7 @@ fun toInt32(bytes: ByteArray): Int { } fun parseTLV(data: ByteArray): Map> { - var result = mutableMapOf>() + val result = mutableMapOf>() var rest = data while (rest.isNotEmpty()) { val t = rest[0] @@ -88,9 +88,9 @@ fun parseTLV(data: ByteArray): Map> { if (v.size < l) continue if (!result.containsKey(t)) { - result.put(t, mutableListOf()) + result[t] = mutableListOf() } - result.get(t)?.add(v) + result[t]?.add(v) } return result } diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 02e6a5695..414422dd3 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service import org.junit.Assert +import org.junit.Ignore import org.junit.Test class Nip19Test { @@ -18,8 +19,15 @@ class Nip19Test { @Test() fun to_int_32_length_4() { val actual = toInt32(byteArrayOfInts(1, 2, 3, 4)) + Assert.assertEquals(16909060, actual) } + @Ignore("Not implemented yet") + @Test() + fun parse_TLV() { + // TODO + } + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From c1113f9df940a7ff3ea09260fbb31fe0128d70d3 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:19:05 +0100 Subject: [PATCH 09/13] Add test uri_to_route_npub --- .../amethyst/service/Nip19Test.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 414422dd3..68bfe38b8 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -6,6 +6,8 @@ import org.junit.Test class Nip19Test { + private val nip19 = Nip19(); + @Test(expected = IllegalArgumentException::class) fun to_int_32_length_smaller_than_4() { toInt32(byteArrayOfInts(1, 2, 3)) @@ -29,5 +31,27 @@ class Nip19Test { // TODO } + @Test() + fun uri_to_route_null() { + val actual = nip19.uriToRoute(null) + + Assert.assertEquals(null, actual) + } + + @Test() + fun uri_to_route_unknown() { + val actual = nip19.uriToRoute("nostr:unknown") + + Assert.assertEquals(null, actual) + } + + @Test() + fun uri_to_route_npub() { + val actual = nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", actual?.hex) + } + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From b35a59372c4838ffbd7eb8184d3aa4df41839402 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:23:42 +0100 Subject: [PATCH 10/13] Prepare move unit tests for uri_to_route behaviour --- .../amethyst/service/Nip19Test.kt | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 68bfe38b8..df6d5800f 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -47,10 +47,62 @@ class Nip19Test { @Test() fun uri_to_route_npub() { - val actual = nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") + val actual = + nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") Assert.assertEquals(Nip19.Type.USER, actual?.type) - Assert.assertEquals("bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", actual?.hex) + Assert.assertEquals( + "bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", + actual?.hex + ) + } + + @Test() + fun uri_to_route_note() { + val actual = + nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv") + + Assert.assertEquals(Nip19.Type.NOTE, actual?.type) + Assert.assertEquals( + "82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3", + actual?.hex + ) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nprofile() { + val actual = nip19.uriToRoute("nostr:nprofile") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nevent() { + val actual = nip19.uriToRoute("nostr:nevent") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nrelay() { + val actual = nip19.uriToRoute("nostr:nrelay") + + Assert.assertEquals(Nip19.Type.RELAY, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_naddr() { + val actual = nip19.uriToRoute("nostr:naddr") + + Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type) + Assert.assertEquals("*", actual?.hex) } private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } From fd58da2a93b3bfa24b7b6a6c65b458b533fd55a8 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:26:35 +0100 Subject: [PATCH 11/13] Remove 1 indentation level from uriToRoute() --- .../vitorpamplona/amethyst/service/Nip19.kt | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index d2d20b6df..db73fbc5f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -14,47 +14,46 @@ class Nip19 { enum class Type { USER, NOTE, RELAY, ADDRESS } + data class Return(val type: Type, val hex: String) fun uriToRoute(uri: String?): Return? { try { - val key = uri?.removePrefix("nostr:") + val key = uri?.removePrefix("nostr:") ?: return null - if (key != null) { - val bytes = key.bechToBytes() - if (key.startsWith("npub")) { - return Return(Type.USER, bytes.toHexKey()) - } - if (key.startsWith("note")) { - return Return(Type.NOTE, bytes.toHexKey()) - } - if (key.startsWith("nprofile")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nevent")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nrelay")) { - val tlv = parseTLV(bytes) - val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - if (relayUrl != null) - return Return(Type.RELAY, relayUrl) - } - if (key.startsWith("naddr")) { - val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } - if (d != null) - return Return(Type.ADDRESS, "$kind:$author:$d") - } + val bytes = key.bechToBytes() + if (key.startsWith("npub")) { + return Return(Type.USER, bytes.toHexKey()) + } + if (key.startsWith("note")) { + return Return(Type.NOTE, bytes.toHexKey()) + } + if (key.startsWith("nprofile")) { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() + if (hex != null) + return Return(Type.USER, hex) + } + if (key.startsWith("nevent")) { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() + if (hex != null) + return Return(Type.USER, hex) + } + if (key.startsWith("nrelay")) { + val tlv = parseTLV(bytes) + val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + if (relayUrl != null) + return Return(Type.RELAY, relayUrl) + } + if (key.startsWith("naddr")) { + val tlv = parseTLV(bytes) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + if (d != null) + return Return(Type.ADDRESS, "$kind:$author:$d") } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") @@ -84,7 +83,7 @@ fun parseTLV(data: ByteArray): Map> { val t = rest[0] val l = rest[1] val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size-1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) if (v.size < l) continue if (!result.containsKey(t)) { From 91591abd140dd1c3fafd661ca5dbe19f2b4a9728 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:36:16 +0100 Subject: [PATCH 12/13] Extract method refactoring in Nip19::uriToRoute() --- .../vitorpamplona/amethyst/service/Nip19.kt | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index db73fbc5f..ee373c06f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -23,37 +23,17 @@ class Nip19 { val bytes = key.bechToBytes() if (key.startsWith("npub")) { - return Return(Type.USER, bytes.toHexKey()) - } - if (key.startsWith("note")) { - return Return(Type.NOTE, bytes.toHexKey()) - } - if (key.startsWith("nprofile")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nevent")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nrelay")) { - val tlv = parseTLV(bytes) - val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - if (relayUrl != null) - return Return(Type.RELAY, relayUrl) - } - if (key.startsWith("naddr")) { - val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } - if (d != null) - return Return(Type.ADDRESS, "$kind:$author:$d") + return npub(bytes) + } else if (key.startsWith("note")) { + return note(bytes) + } else if (key.startsWith("nprofile")) { + return nprofile(bytes) + } else if (key.startsWith("nevent")) { + return nevent(bytes) + } else if (key.startsWith("nrelay")) { + return nrelay(bytes) + } else if (key.startsWith("naddr")) { + return naddr(bytes) } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") @@ -62,6 +42,49 @@ class Nip19 { return null } + + private fun npub(bytes: ByteArray): Return { + return Return(Type.USER, bytes.toHexKey()) + } + + private fun note(bytes: ByteArray): Return { + return Return(Type.NOTE, bytes.toHexKey()); + } + + private fun nprofile(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() ?: return null + + return Return(Type.USER, hex) + } + + private fun nevent(bytes: ByteArray): Return? { + val hex = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null + + return Return(Type.USER, hex) + } + + private fun nrelay(bytes: ByteArray): Return? { + val relayUrl = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null + + return Return(Type.RELAY, relayUrl) + } + + private fun naddr(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: return null + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + + return Return(Type.ADDRESS, "$kind:$author:$d") + } } enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin From 657f99a65a315b0a369876d9d5c2468b47e8ee4a Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:36:37 +0100 Subject: [PATCH 13/13] Remove non-used imports in Nip19 --- .../vitorpamplona/amethyst/service/Nip19.kt | 36 ++++++++++++------- .../amethyst/service/Nip19Test.kt | 12 +++---- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index ee373c06f..54d765940 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -1,13 +1,9 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toHexKey -import com.vitorpamplona.amethyst.service.model.ATag +import nostr.postr.bechToBytes import java.nio.ByteBuffer import java.nio.ByteOrder -import nostr.postr.Bech32 -import nostr.postr.bechToBytes -import nostr.postr.toByteArray class Nip19 { @@ -37,7 +33,6 @@ class Nip19 { } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") - //e.printStackTrace() } return null @@ -52,8 +47,10 @@ class Nip19 { } private fun nprofile(bytes: ByteArray): Return? { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() ?: return null + val hex = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null return Return(Type.USER, hex) } @@ -78,16 +75,29 @@ class Nip19 { private fun naddr(bytes: ByteArray): Return? { val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: return null - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + + val d = tlv.get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null + + val relay = tlv.get(NIP19TLVTypes.RELAY.id) + ?.get(0) + ?.toString(Charsets.UTF_8) + + val author = tlv.get(NIP19TLVTypes.AUTHOR.id) + ?.get(0) + ?.toHexKey() + + val kind = tlv.get(NIP19TLVTypes.KIND.id) + ?.get(0) + ?.let { toInt32(it) } return Return(Type.ADDRESS, "$kind:$author:$d") } } -enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin +// Classes should start with an uppercase letter in kotlin +enum class NIP19TLVTypes(val id: Byte) { SPECIAL(0), RELAY(1), AUTHOR(2), diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index df6d5800f..53de03970 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -25,10 +25,10 @@ class Nip19Test { Assert.assertEquals(16909060, actual) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun parse_TLV() { - // TODO + // TODO: I don't know how to test this (?) } @Test() @@ -69,7 +69,7 @@ class Nip19Test { ) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nprofile() { val actual = nip19.uriToRoute("nostr:nprofile") @@ -78,7 +78,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nevent() { val actual = nip19.uriToRoute("nostr:nevent") @@ -87,7 +87,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nrelay() { val actual = nip19.uriToRoute("nostr:nrelay") @@ -96,7 +96,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_naddr() { val actual = nip19.uriToRoute("nostr:naddr")