refactor: move test functions to automation and storage utilities, add compose component activity dependency and create group fragment content descriptions, add compose only testing for create group logic, update protos to match latest for chunk 2

This commit is contained in:
0x330a 2023-10-19 17:18:56 +11:00
parent f69e3846e3
commit e79a980f2c
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
12 changed files with 364 additions and 97 deletions

View File

@ -299,9 +299,9 @@ dependencies {
implementation "com.opencsv:opencsv:4.6"
testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.10.0"
testImplementation "org.mockito:mockito-inline:4.11.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3'
androidTestImplementation "org.mockito:mockito-android:4.11.0"
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.2.0"
@ -330,6 +330,8 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.2"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.2"
androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'org.robolectric:robolectric:4.4'

View File

@ -1,8 +1,10 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="network.loki.messenger.test">
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<uses-library android:name="android.test.runner"
android:required="false" />
<activity android:name="androidx.activity.ComponentActivity"/>
</application>
</manifest>

View File

@ -0,0 +1,63 @@
package network.loki.messenger
import androidx.compose.ui.test.hasContentDescriptionExactly
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.groups.CreateGroup
import org.thoughtcrime.securesms.groups.CreateGroupFragment
import org.thoughtcrime.securesms.groups.CreateGroupState
import org.thoughtcrime.securesms.ui.AppTheme
@RunWith(AndroidJUnit4::class)
@SmallTest
class CreateGroupTests {
@get:Rule
val composeTest = createComposeRule()
@Test
fun testNavigateToCreateGroup() {
val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
// Accessibility IDs
val nameDesc = application.getString(R.string.AccessibilityId_closed_group_edit_group_name)
val buttonDesc = application.getString(R.string.AccessibilityId_create_closed_group_create_button)
lateinit var postedGroup: CreateGroupState
var backPressed = false
var closePressed = false
composeTest.setContent {
AppTheme {
CreateGroup(
viewState = CreateGroupFragment.ViewState.DEFAULT,
createGroupState = CreateGroupState("", "", emptySet()),
onCreate = { submitted ->
postedGroup = submitted
},
onBack = { backPressed = true },
onClose = { closePressed = true })
}
}
with(composeTest) {
onNode(hasContentDescriptionExactly(nameDesc)).performTextInput("Name")
onNode(hasContentDescriptionExactly(buttonDesc)).performClick()
}
assertThat(postedGroup.groupName, equalTo("Name"))
assertThat(backPressed, equalTo(false))
assertThat(closePressed, equalTo(false))
}
}

View File

@ -1,14 +1,10 @@
package network.loki.messenger
import android.Manifest
import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
@ -20,11 +16,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.adevinta.android.barista.interaction.PermissionGranter
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
import org.hamcrest.Matcher
import network.loki.messenger.util.sendMessage
import network.loki.messenger.util.setupLoggedInState
import network.loki.messenger.util.waitFor
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
import org.junit.After
@ -36,12 +32,10 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.mms.GlideApp
@RunWith(AndroidJUnit4::class)
@LargeTest
@SmallTest
class HomeActivityTests {
@get:Rule
@ -59,38 +53,6 @@ class HomeActivityTests {
InstrumentationRegistry.getInstrumentation().removeMonitor(activityMonitor)
}
private fun sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
// assume in chat activity
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
if (linkPreview != null) {
val activity = activityMonitor.waitForActivity() as ConversationActivityV2
val glide = GlideApp.with(activity)
activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
}
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
// TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
onView(isRoot()).perform(waitFor(500))
}
private fun setupLoggedInState(hasViewedSeed: Boolean = false) {
// landing activity
onView(withId(R.id.registerButton)).perform(ViewActions.click())
// session ID - register activity
onView(withId(R.id.registerButton)).perform(ViewActions.click())
// display name selection
onView(withId(R.id.displayNameEditText)).perform(ViewActions.typeText("test-user123"))
onView(withId(R.id.registerButton)).perform(ViewActions.click())
// PN select
if (hasViewedSeed) {
// has viewed seed is set to false after register activity
TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true)
}
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
onView(withId(R.id.registerButton)).perform(ViewActions.click())
// allow notification permission
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
private fun goToMyChat() {
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
@ -134,11 +96,13 @@ class HomeActivityTests {
setupLoggedInState()
goToMyChat()
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
sendMessage("howdy")
sendMessage("test")
// tests url rewriter doesn't crash
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
sendMessage("https://www.ámazon.com")
with (activityMonitor.waitForActivity() as ConversationActivityV2) {
sendMessage("howdy")
sendMessage("test")
// tests url rewriter doesn't crash
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
sendMessage("https://www.ámazon.com")
}
}
@Test
@ -148,7 +112,9 @@ class HomeActivityTests {
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
// given the link url text
val url = "https://www.ámazon.com"
sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
with (activityMonitor.waitForActivity() as ConversationActivityV2) {
sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
}
// when the URL span is clicked
onView(withSubstring(url)).perform(ViewActions.click())
@ -162,21 +128,4 @@ class HomeActivityTests {
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}
/**
* Perform action of waiting for a specific time.
*/
fun waitFor(millis: Long): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return isRoot()
}
override fun getDescription(): String = "Wait for $millis milliseconds."
override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadForAtLeast(millis)
}
}
}
}

View File

@ -9,12 +9,15 @@ import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.util.applySpiedStorage
import network.loki.messenger.util.maybeGetUserInfo
import network.loki.messenger.util.randomSeedBytes
import network.loki.messenger.util.randomSessionId
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.TextSecurePreferences
@ -22,32 +25,15 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@SmallTest
class LibSessionTests {
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
private var fakeHashI = 0
private val nextFakeHash: String
get() = "fakehash${fakeHashI++}"
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val prefs = appContext.prefs
val localUserPublicKey = prefs.getLocalNumber()
val secretKey = with(appContext) {
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
edKey.secretKey.asBytes
}
return if (localUserPublicKey == null || secretKey == null) null
else secretKey to localUserPublicKey
}
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
val (key,_) = maybeGetUserInfo()!!
val contacts = Contacts.Companion.newInstance(key)
@ -80,9 +66,8 @@ class LibSessionTests {
@Test
fun migration_one_to_ones() {
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val storageSpy = spy(app.storage)
app.storage = storageSpy
val applicationContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val storage = applicationContext.applySpiedStorage()
val newContactId = randomSessionId()
val singleContact = Contact(
@ -93,10 +78,10 @@ class LibSessionTests {
val newContactMerge = buildContactMessage(listOf(singleContact))
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
fakePollNewConfig(contacts, newContactMerge)
verify(storageSpy).addLibSessionContacts(argThat {
verify(storage).addLibSessionContacts(argThat {
first().let { it.id == newContactId && it.approved } && size == 1
})
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
verify(storage).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
}
}

View File

@ -0,0 +1,82 @@
package network.loki.messenger.util
import android.Manifest
import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import com.adevinta.android.barista.interaction.PermissionGranter
import network.loki.messenger.R
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.mms.GlideApp
fun setupLoggedInState(hasViewedSeed: Boolean = false) {
// landing activity
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
// session ID - register activity
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
// display name selection
onView(ViewMatchers.withId(R.id.displayNameEditText))
.perform(ViewActions.typeText("test-user123"))
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
// PN select
if (hasViewedSeed) {
// has viewed seed is set to false after register activity
TextSecurePreferences.setHasViewedSeed(
InstrumentationRegistry.getInstrumentation().targetContext,
true
)
}
onView(ViewMatchers.withId(R.id.backgroundPollingOptionView))
.perform(ViewActions.click())
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
// allow notification permission
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
}
fun ConversationActivityV2.sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
// assume in chat activity
onView(
Matchers.allOf(
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
ViewMatchers.withId(R.id.inputBarEditText)
)
).perform(ViewActions.replaceText(messageToSend))
if (linkPreview != null) {
val glide = GlideApp.with(this)
this.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
}
onView(
Matchers.allOf(
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
InputBarButtonDrawableMatcher.inputButtonWithDrawable(R.drawable.ic_arrow_up)
)
).perform(ViewActions.click())
// TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
onView(ViewMatchers.isRoot()).perform(waitFor(500))
}
/**
* Perform action of waiting for a specific time.
*/
fun waitFor(millis: Long): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return ViewMatchers.isRoot()
}
override fun getDescription(): String = "Wait for $millis milliseconds."
override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadForAtLeast(millis)
}
}
}

View File

@ -0,0 +1,31 @@
package network.loki.messenger.util
import androidx.test.platform.app.InstrumentationRegistry
import org.mockito.kotlin.spy
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.database.Storage
import kotlin.random.Random
fun maybeGetUserInfo(): Pair<ByteArray, String>? {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
val prefs = appContext.prefs
val localUserPublicKey = prefs.getLocalNumber()
val secretKey = with(appContext) {
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
edKey.secretKey.asBytes
}
return if (localUserPublicKey == null || secretKey == null) null
else secretKey to localUserPublicKey
}
fun ApplicationContext.applySpiedStorage(): Storage {
val storageSpy = spy(storage)!!
storage = storageSpy
return storageSpy
}
fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey

View File

@ -35,6 +35,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -170,33 +172,46 @@ fun CreateGroup(
.padding(top = 16.dp)
)
// Title
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
OutlinedTextField(
value = name,
onValueChange = { name = it },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp, horizontal = 24.dp),
.padding(vertical = 8.dp, horizontal = 24.dp)
.semantics {
contentDescription = nameDescription
},
)
// Description
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
OutlinedTextField(
value = description,
onValueChange = { description = it },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp, horizontal = 24.dp),
.padding(vertical = 8.dp, horizontal = 24.dp)
.semantics {
contentDescription = descriptionDescription
},
)
// Group list
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
}
// Create button
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
OutlinedButton(
onClick = { onCreate(CreateGroupState(name, description, members)) },
enabled = name.isNotBlank() && !viewState.isLoading,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
.padding(16.dp)
.semantics {
contentDescription = createDescription
}
,
shape = RoundedCornerShape(32.dp)
) {
Text(
@ -209,7 +224,9 @@ fun CreateGroup(
}
}
if (viewState.isLoading) {
Box(modifier = modifier.fillMaxSize().background(Color.Gray.copy(alpha = 0.5f))) {
Box(modifier = modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.5f))) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)

View File

@ -3,4 +3,9 @@
<item type="id" name="holder_tag"/>
<item type="id" name="contact_info_tag"/>
<item type="id" name="motion_view_edittext"/>
<!-- Create closed groups -->
<item type="id" name="create_group_name_text_field"/>
<item type="id" name="create_group_create_button"/>
</resources>

View File

@ -80,6 +80,9 @@
<string name="AccessibilityId_done">Done</string>
<string name="AccessibilityId_mentions_list">Mentions list</string>
<string name="AccessibilityId_contact_mentions">Contact mentions</string>
<string name="AccessibilityId_closed_group_edit_group_name">Group name</string>
<string name="AccessibilityId_closed_group_edit_group_description">Group description</string>
<string name="AccessibilityId_create_closed_group_create_button">Create group</string>
<!-- Conversation icons -->
<string name="AccessibilityId_call_button">Call button</string>
<string name="AccessibilityId_settings">Settings</string>

View File

@ -702,4 +702,51 @@ class InstrumentedTests {
assertThat(groupInfo.getDescription(), equalTo("This is a test group"))
}
@Test
fun testGroupKeyConfig() {
val (userPubKey, userSecret) = keyPair
val groupConfig = UserGroupsConfig.newInstance(userSecret)
val group = groupConfig.createGroup()
groupConfig.set(group)
val groupInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
groupInfo.setName("test")
val groupMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
groupMembers.set(
GroupMember(
sessionId = SessionId(IdPrefix.STANDARD, Sodium.ed25519PkToCurve25519(userPubKey)).hexString(),
name = "admin",
admin = true
)
)
val membersDump = groupMembers.dump()
val infoDump = groupInfo.dump()
val ourKeyConfig = GroupKeysConfig.newInstance(
userSecret,
group.groupSessionId.pubKeyBytes,
group.signingKey(),
info = groupInfo,
members = groupMembers
)
assertThat(ourKeyConfig.needsRekey(), equalTo(false))
val pushed = ourKeyConfig.pendingConfig()!!
val messageTimestamp = System.currentTimeMillis()
ourKeyConfig.loadKey(pushed, "testabc", messageTimestamp, groupInfo, groupMembers)
assertThat(ourKeyConfig.needsDump(), equalTo(true))
ourKeyConfig.dump()
assertThat(ourKeyConfig.needsRekey(), equalTo(false))
val mergeInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), infoDump)
val mergeMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), membersDump)
val mergeConfig = GroupKeysConfig.newInstance(userSecret, group.groupSessionId.pubKeyBytes, group.signingKey(), info = mergeInfo, members = mergeMembers)
mergeConfig.loadKey(pushed, "testabc", messageTimestamp, mergeInfo, mergeMembers)
assertThat(mergeConfig.needsRekey(), equalTo(false))
assertThat(mergeConfig.keys().size, equalTo(1))
assertThat(ourKeyConfig.keys().size, equalTo(1))
assertThat(mergeConfig.keys().first(), equalTo(ourKeyConfig.keys().first()))
assertThat(ourKeyConfig.groupKeys().size, equalTo(1))
assertThat(mergeConfig.groupKeys().size, equalTo(1))
assertThat(ourKeyConfig.groupKeys().first(), equalTo(mergeConfig.groupKeys().first()))
}
}

View File

@ -127,6 +127,87 @@ message DataMessage {
optional GroupPromoteMessage promoteMessage = 34;
}
// New closed group update messages
message GroupUpdateMessage {
optional GroupUpdateInviteMessage inviteMessage = 1;
optional GroupUpdateDeleteMessage deleteMessage = 2;
optional GroupUpdateInfoChangeMessage infoChangeMessage = 3;
optional GroupUpdateMemberChangeMessage memberChangeMessage = 4;
optional GroupUpdatePromoteMessage promoteMessage = 5;
optional GroupUpdateMemberLeftMessage memberLeftMessage = 6;
optional GroupUpdateInviteResponseMessage inviteResponse = 7;
optional GroupUpdateDeleteMemberContentMessage deleteMemberContent = 8;
}
// New closed groups
message GroupUpdateInviteMessage {
// @required
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required string name = 2;
// @required
required bytes memberAuthData = 3;
optional bytes profileKey = 4;
optional LokiProfile profile = 5;
// @required
required bytes adminSignature = 6;
}
message GroupUpdateDeleteMessage {
// @required
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required bytes adminSignature = 2;
}
message GroupUpdatePromoteMessage {
// @required
required bytes groupIdentitySeed = 1;
}
message GroupUpdateInfoChangeMessage {
enum Type {
NAME = 1;
AVATAR = 2;
DISAPPEARING_MESSAGES = 3;
}
// @required
required Type type = 1;
optional string updatedName = 2;
optional uint32 updatedExpiration = 3;
}
message GroupUpdateMemberChangeMessage {
enum Type {
ADDED = 1;
REMOVED = 2;
PROMOTED = 3;
}
// @required
required Type type = 1;
repeated bytes memberPublicKeys = 2;
}
message GroupUpdateMemberLeftMessage {
// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop)
}
message GroupUpdateInviteResponseMessage {
// @required
required bool isApproved = 1; // Whether the request was approved
optional bytes profileKey = 2;
optional LokiProfile profile = 3;
}
message GroupUpdateDeleteMemberContentMessage {
repeated bytes memberPublicKeys = 1;
// @required
required bytes adminSignature = 2;
}
message ClosedGroupControlMessage {
enum Type {