~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1

A sign-in with no Bluesky profile makes a bare grain profile

Merged chadtmiller.com asked to merge into feature/rust-calls from grain.social/grain-android:profile/seed-on-sign-in
app/src/main/java/social/grain/data/api/ProfileSeed.kt+18−11
@@ -8,17 +8,21 @@ import kotlinx.serialization.json.jsonPrimitive
import social.grain.util.nowIso
/**
* A grain profile made from the account's Bluesky profile once it signs in,
* as grain-serve's web login seeds one: in the background, so a sign-in never
* waits on it and never fails by it.
* A grain profile made for the account once it signs in, from its Bluesky
* profile when it has one, as grain-serve's web login seeds one: in the
* background, so a sign-in never waits on it and never fails by it.
*
* An account whose `social.grain.actor.profile` has a `displayName` is left as
* it is. Otherwise its `app.bsky.actor.profile` gives the grain profile its
* `displayName`, `description` and `avatar` (the blob as the Bluesky profile
* names it), with the `createdAt` of the grain profile there is, or now. One
* there is is put over, swapped on its CID; else one is made at `self`. Both
* are read from the account's PDS, which has them before the index may; the
* write is then indexed, as every write is.
* there is is put over, swapped on its CID; else one is made at `self`. An
* account with no Bluesky profile and no grain profile gets a bare one, its
* `createdAt` alone, so every account that signs in has the profile that
* makes grain-serve track it; one with an unnamed grain profile and no
* Bluesky profile is left as it is. Both are read from the account's PDS,
* which has them before the index may; the write is then indexed, as every
* write is.
*/
object ProfileSeed {
const val GRAIN = "social.grain.actor.profile"
@@ -30,25 +34,28 @@ object ProfileSeed {
/** The grain profile has a name already. */
data object Named : Seeded
/** There is no Bluesky profile to make one from. */
/**
* There is no Bluesky profile to name it from, and the grain profile
* there is was left as it is.
*/
data object NoBluesky : Seeded
/** A grain profile was made, at this URI. */
/** A grain profile was made, from the Bluesky one or bare, at this URI. */
data class Made(val uri: String?) : Seeded
/** The grain profile there was was put over, at this URI. */
data class Put(val uri: String?) : Seeded
}
/** Seeds the signed-in account's grain profile from its Bluesky profile. */
/** Seeds the signed-in account's grain profile, as the object says. */
suspend fun seed(client: XrpcClient, auth: AuthContext): Seeded {
val did = auth.did ?: throw XrpcException.Unauthorized
val grain = client.getRecordOrNull("at://$did/$GRAIN/$SELF", auth)
val name = grain?.value?.get("displayName")?.jsonPrimitive?.contentOrNull
if (!name.isNullOrEmpty()) return Seeded.Named
val bsky = client.getRecordOrNull("at://$did/$BSKY/$SELF", auth)?.value
?: return Seeded.NoBluesky
val record = fromBluesky(bsky, grain?.value)
if (bsky == null && grain != null) return Seeded.NoBluesky
val record = fromBluesky(bsky ?: JsonObject(emptyMap()), grain?.value)
val cid = grain?.cid
return if (grain != null && cid != null) {
Seeded.Put(client.putRecord(GRAIN, SELF, record, did, auth, swapRecord = cid).uri)
app/src/main/java/social/grain/MainViewModel.kt+4−4
@@ -171,9 +171,9 @@ class MainViewModel @Inject constructor(
}
/**
* Make the account a grain profile from its Bluesky one, if it has no
* named one yet ([ProfileSeed]). In the background: a sign-in never waits
* on it, and its failure is only logged.
* Make the account a grain profile, from its Bluesky one when it has one,
* if it has no named one yet ([ProfileSeed]). In the background: a
* sign-in never waits on it, and its failure is only logged.
*/
private fun seedProfile() {
viewModelScope.launch {
@@ -182,7 +182,7 @@ class MainViewModel @Inject constructor(
.onSuccess { Log.i(TAG, "Profile seed: $it") }
.onFailure {
rethrowIfCancelled()
Log.w(TAG, "Couldn't seed a profile from Bluesky", it)
Log.w(TAG, "Couldn't seed a grain profile", it)
}
}
}
app/src/test/java/social/grain/ProfileSeedTest.kt+27−3
@@ -16,8 +16,9 @@ import social.grain.support.testAuth
import social.grain.support.xrpc
/**
* A grain profile made from the account's Bluesky profile at sign-in: left
* alone once it has a name, made or put over otherwise.
* A grain profile made for the account at sign-in: left alone once it has a
* name, made or put over from the Bluesky profile, made bare with neither, and
* an unnamed one left with no Bluesky profile.
*/
@RunWith(AndroidJUnit4::class)
class ProfileSeedTest {
@@ -41,8 +42,31 @@ class ProfileSeedTest {
}
@Test
fun withNoBlueskyProfileNothingIsWritten() = runTest {
fun withNeitherProfileABareOneIsMadeAtSelfAndIndexed() = runTest {
profileReads(grain = null, bsky = null)
server.on("com.atproto.repo.createRecord", """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c2"}""")
val seeded = ProfileSeed.seed(client, auth)
assertEquals(Seeded.Made("at://did:plc:me/social.grain.actor.profile/self"), seeded)
val sent = server.lastCall("com.atproto.repo.createRecord").json
assertEquals("self", sent["rkey"]!!.jsonPrimitive.content)
assertEquals(ProfileSeed.GRAIN, sent["collection"]!!.jsonPrimitive.content)
val record = sent["record"]!!.jsonObject
assertEquals(setOf("\$type", "createdAt"), record.keys)
assertEquals(ProfileSeed.GRAIN, record["\$type"]!!.jsonPrimitive.content)
assertTrue(record["createdAt"]!!.jsonPrimitive.content.endsWith("Z"))
assertEquals(0, server.countOf("com.atproto.repo.putRecord"))
assertTrue(server.lastCall("social.grain.sync.indexRecords").body.contains("social.grain.actor.profile/self"))
}
@Test
fun withNoBlueskyProfileAnUnnamedGrainProfileIsLeftAlone() = runTest {
profileReads(
grain = """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c1",""" +
""""value":{"createdAt":"2025-01-01T00:00:00.000Z"}}""",
bsky = null,
)
assertEquals(Seeded.NoBluesky, ProfileSeed.seed(client, auth))
assertEquals(0, server.countOf("com.atproto.repo.putRecord"))