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

grain.social/grain-ios

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-ios:profile/seed-on-sign-in
Grain/Services/ProfileSeed.swift+17−12
@@ -3,19 +3,22 @@ import os
private let logger = Logger(subsystem: "social.grain.grain", category: "ProfileSeed")
/// A grain profile made from the account's Bluesky profile once it signs
/// in, as grain-serve makes one for the web (`login/profile.rs`). The app
/// signs in at the account's own PDS, so grain-serve never sees the
/// sign-in: the app does it, 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 makes one for the web
/// (`login/profile.rs`). The app signs in at the account's own PDS, so
/// grain-serve never sees the sign-in: the app does it, 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, and the write
/// is then indexed.
/// 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, and the write is then indexed.
enum ProfileSeed {
static let grain = "social.grain.actor.profile"
static let bluesky = "app.bsky.actor.profile"
@@ -24,9 +27,10 @@ enum ProfileSeed {
enum Outcome: Equatable {
/// The grain profile has a name already.
case named
/// 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.
case noBluesky
/// A grain profile was made.
/// A grain profile was made, from the Bluesky one or bare.
case made
/// The grain profile there was was put over.
case put
@@ -38,7 +42,7 @@ enum ProfileSeed {
let outcome = try await seed(client: client, auth: auth)
logger.info("profile seed: \(String(describing: outcome), privacy: .public)")
} catch {
logger.info("seeding a profile from Bluesky failed: \(error)")
logger.info("seeding a grain profile failed: \(error)")
}
}
@@ -47,10 +51,11 @@ enum ProfileSeed {
if let name = existing?.value?.dictValue?["displayName"]?.stringValue, !name.isEmpty {
return .named
}
guard let bsky = try await read(bluesky, client: client, auth: auth)?.value?.dictValue else {
let bsky = try await read(bluesky, client: client, auth: auth)?.value?.dictValue
if bsky == nil, existing != nil {
return .noBluesky
}
let record = AnyCodable(fromBluesky(bsky, grain: existing?.value?.dictValue))
let record = AnyCodable(fromBluesky(bsky ?? [:], grain: existing?.value?.dictValue))
if let existing {
_ = try await client.putRecord(collection: grain, rkey: "self", record: record, swapRecord: existing.cid, auth: auth)
return .put
GrainTests/ProfileSeedTests.swift+23−3
@@ -2,8 +2,8 @@ import CryptoKit
@testable import Grain
import XCTest
/// The grain profile the app makes from the account's Bluesky profile once
/// it signs in, as grain-serve makes one for the web.
/// The grain profile the app makes once an account signs in, from its
/// Bluesky profile or bare, as grain-serve makes one for the web.
final class ProfileSeedTests: GrainTestCase {
private var log: SeedLog!
@@ -60,11 +60,31 @@ final class ProfileSeedTests: GrainTestCase {
XCTAssertNil(log.write)
}
func testWithNoBlueskyProfileNothingIsWritten() async throws {
/// No Bluesky profile and no grain profile: a bare one is made at
/// `self`, its `createdAt` alone, so grain-serve tracks the account.
func testWithNeitherProfileABareOneIsMadeAtSelf() async throws {
let client = makeClient(grain: nil, bluesky: nil)
let outcome = try await ProfileSeed.seed(client: client, auth: auth)
XCTAssertEqual(outcome, .made)
let write = try XCTUnwrap(log.write)
XCTAssertTrue(write.path.hasSuffix("com.atproto.repo.createRecord"))
XCTAssertEqual(write.body["rkey"] as? String, "self")
XCTAssertEqual(write.body["collection"] as? String, "social.grain.actor.profile")
let record = try XCTUnwrap(write.body["record"] as? [String: Any])
XCTAssertEqual(Set(record.keys), ["$type", "createdAt"])
XCTAssertEqual(record["$type"] as? String, "social.grain.actor.profile")
XCTAssertNotNil(DateFormatting.parse(record["createdAt"] as? String ?? ""))
XCTAssertNotNil(log.indexProxy)
}
/// No Bluesky profile and an unnamed grain profile: it is left as it is.
func testWithNoBlueskyProfileAnUnnamedGrainProfileIsLeft() async throws {
let client = makeClient(grain: #"{"createdAt": "2025-01-01T00:00:00.000Z"}"#, bluesky: nil)
let outcome = try await ProfileSeed.seed(client: client, auth: auth)
XCTAssertEqual(outcome, .noBluesky)
XCTAssertNil(log.write)
}