feat: implement MIP-04 encrypted media upload for Marmot groups
Adds full MIP-04 (Encrypted Media) support for Marmot group chats:
Protocol layer (quartz):
- Mip04MediaEncryption: ChaCha20-Poly1305 AEAD with MLS exporter key
derivation (HKDF-Expand), random nonces, and AAD binding
- Mip04Cipher/Mip04NostrCipher: NostrCipher adapters for the existing
EncryptedBlobInterceptor download-and-decrypt pipeline
- Mip04IMetaTag: NIP-92 imeta tag builder/parser with MIP-04 fields
(url, m, filename, x, n, v=mip04-v2)
- MlsGroupManager.mediaExporterSecret(): MLS-Exporter("marmot",
"encrypted-media", 32)
Upload flow (amethyst):
- MarmotFileUploader: per-file Mip04NostrCipher → existing
UploadOrchestrator.uploadEncrypted pipeline (compression, stripping,
Blossom upload)
- MarmotFileSender: builds imeta tags and sends kind:9 inner events
through the MLS group message pipeline
- MarmotGroupMessageComposer: adds SelectFromGallery leading icon and
ChatFileUploadDialog (mirrors NIP-17 DM pattern)
Display flow:
- RenderMarmotEncryptedMedia: parses imeta v=mip04-v2 tags, derives
Mip04Cipher, registers in EncryptionKeyCache, renders via
ZoomableContentView (same path as NIP-17 encrypted files)
- MarmotGroupList.noteToGroupIndex: reverse index for mapping notes
back to their group ID (needed for exporter secret lookup)
- ChatMessageCompose NoteRow: routes MIP-04 events to the new renderer
https://claude.ai/code/session_01TckzZLpJdmE6p198DHBQ5i
This commit is contained in:
+6
@@ -403,6 +403,12 @@ class MarmotManager(
|
||||
*/
|
||||
fun groupEpoch(nostrGroupId: HexKey): Long? = groupManager.getGroup(nostrGroupId)?.epoch
|
||||
|
||||
/**
|
||||
* Get the MIP-04 media exporter secret for a group.
|
||||
* MLS-Exporter("marmot", "encrypted-media", 32)
|
||||
*/
|
||||
fun mediaExporterSecret(nostrGroupId: HexKey): ByteArray = groupManager.mediaExporterSecret(nostrGroupId)
|
||||
|
||||
/**
|
||||
* Get the MIP-01 group metadata from the MLS GroupContext extensions.
|
||||
* Returns null if the group doesn't exist or has no MarmotGroupData extension.
|
||||
|
||||
+6
@@ -36,6 +36,8 @@ class MarmotGroupList(
|
||||
var rooms = LargeCache<HexKey, MarmotGroupChatroom>()
|
||||
private set
|
||||
|
||||
private val noteToGroupIndex = LargeCache<HexKey, HexKey>()
|
||||
|
||||
private val _groupListChanges = MutableSharedFlow<HexKey>(0, 20, BufferOverflow.DROP_OLDEST)
|
||||
val groupListChanges = _groupListChanges
|
||||
|
||||
@@ -56,6 +58,7 @@ class MarmotGroupList(
|
||||
chatroom.addMessageSync(msg)
|
||||
}
|
||||
if (added) {
|
||||
noteToGroupIndex.getOrCreate(msg.idHex) { nostrGroupId }
|
||||
if (isSelfAuthored) {
|
||||
chatroom.ownerSentMessage = true
|
||||
}
|
||||
@@ -73,6 +76,7 @@ class MarmotGroupList(
|
||||
) {
|
||||
val chatroom = getOrCreateGroup(nostrGroupId)
|
||||
if (chatroom.restoreMessageSync(msg)) {
|
||||
noteToGroupIndex.getOrCreate(msg.idHex) { nostrGroupId }
|
||||
if (msg.author?.pubkeyHex == ownerPubKey) {
|
||||
chatroom.ownerSentMessage = true
|
||||
}
|
||||
@@ -80,6 +84,8 @@ class MarmotGroupList(
|
||||
}
|
||||
}
|
||||
|
||||
fun groupIdForNote(noteId: HexKey): HexKey? = noteToGroupIndex.get(noteId)
|
||||
|
||||
/**
|
||||
* Mark a group as "known" by the local user — used right after the user
|
||||
* creates a group, so the creator doesn't appear under "New Requests"
|
||||
|
||||
Reference in New Issue
Block a user