A sign-in with no Bluesky profile makes a bare grain profile
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")) | ||