feat(contact): add manually verified shared contact support (#3283)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich 2025-10-02 11:46:12 -05:00 committed by GitHub
parent 04991dbc5a
commit 24f0417b28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 826 additions and 9 deletions

View file

@ -918,8 +918,17 @@ class MeshService : Service() {
sessionPasskey = a.sessionPasskey
}
private fun handleSharedContactImport(contact: AdminProtos.SharedContact) {
handleReceivedUser(contact.nodeNum, contact.user, manuallyVerified = true)
}
// Update our DB of users based on someone sending out a User subpacket
private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User, channel: Int = 0) {
private fun handleReceivedUser(
fromNum: Int,
p: MeshProtos.User,
channel: Int = 0,
manuallyVerified: Boolean = false,
) {
updateNodeInfo(fromNum) {
val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET)
@ -936,6 +945,7 @@ class MeshService : Service() {
it.longName = p.longName
it.shortName = p.shortName
it.channel = channel
it.manuallyVerified = manuallyVerified
if (newNode) {
serviceNotifications.showNewNodeSeenNotification(it)
}
@ -1913,14 +1923,33 @@ class MeshService : Service() {
is ServiceAction.Favorite -> favoriteNode(action.node)
is ServiceAction.Ignore -> ignoreNode(action.node)
is ServiceAction.Reaction -> sendReaction(action)
is ServiceAction.AddSharedContact -> importContact(action.contact)
is ServiceAction.ImportContact -> importContact(action.contact)
is ServiceAction.SendContact -> sendContact(action.contact)
}
}
}
/**
* Imports a manually shared contact.
*
* This function takes a [AdminProtos.SharedContact] proto, marks it as manually verified, sends it for further
* processing, and then handles the import specific logic.
*
* @param contact The [AdminProtos.SharedContact] to be imported.
*/
private fun importContact(contact: AdminProtos.SharedContact) {
val verifiedContact = contact.copy { manuallyVerified = true }
sendContact(verifiedContact)
handleSharedContactImport(contact = verifiedContact)
}
/**
* Sends a shared contact to the radio via [AdminProtos.AdminMessage]
*
* @param contact The contact to send.
*/
private fun sendContact(contact: AdminProtos.SharedContact) {
packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { addContact = contact })
handleReceivedUser(contact.nodeNum, contact.user)
}
private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions {

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.sharedContact
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -40,12 +41,15 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
private const val VERIFIED_CONTACT_FIRMWARE_CUTOFF = "2.7.12"
@HiltViewModel
class MessageViewModel
@Inject
@ -122,6 +126,20 @@ constructor(
fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
/**
* Sends a message to a contact or channel.
*
* If the message is a direct message (no channel specified), this function will:
* - If the device firmware version is older than 2.7.12, it will mark the destination node as a favorite to prevent
* it from being removed from the on-device node database.
* - If the device firmware version is 2.7.12 or newer, it will send a shared contact to the destination node.
*
* @param str The message content.
* @param contactKey The unique contact key, which is a combination of channel (optional) and node ID. Defaults to
* broadcasting on channel 0.
* @param replyId The ID of the message this is a reply to, if any.
*/
@Suppress("NestedBlockDepth")
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}", replyId: Int? = null) {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
@ -130,9 +148,23 @@ constructor(
// if the destination is a node, we need to ensure it's a
// favorite so it does not get removed from the on-device node database.
if (channel == null) { // no channel specified, so we assume it's a direct message
val node = nodeRepository.getNode(dest)
if (!node.isFavorite) {
favoriteNode(nodeRepository.getNode(dest))
val fwVersion = ourNodeInfo.value?.metadata?.firmwareVersion
val destNode = nodeRepository.getNode(dest)
fwVersion?.let { fw ->
val ver = DeviceVersion(asString = fw)
val verifiedSharedContactsVersion =
DeviceVersion(
asString = VERIFIED_CONTACT_FIRMWARE_CUTOFF,
) // Version cutover to verified shared contacts
if (ver >= verifiedSharedContactsVersion) {
sendSharedContact(destNode)
} else {
if (!destNode.isFavorite) {
favoriteNode(destNode)
}
}
}
}
val p = DataPacket(dest, channel ?: 0, str, replyId)
@ -159,6 +191,19 @@ constructor(
}
}
private fun sendSharedContact(node: Node) = viewModelScope.launch {
try {
val contact = sharedContact {
nodeNum = node.num
user = node.user
manuallyVerified = node.manuallyVerified
}
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
} catch (ex: RemoteException) {
Timber.e(ex, "Send shared contact error")
}
}
private fun sendDataPacket(p: DataPacket) {
try {
serviceRepository.meshService?.send(p)