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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Groups are what spaces are for: no private galleries

Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:web/drop-private-galleries

Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.

crates/grain/src/lexicon.rs+1−1
@@ -1067,7 +1067,7 @@ mod tests {
"at://did:plc:a/social.grain.gallery/3l3qo2vuowo2a"
));
assert!(valid_at_uri(
"at://did:plc:a/space/social.grain.privateGallery/k"
"at://did:plc:a/space/fyi.opensocial.members/self"
));
assert!(valid_at_uri(
"at://did:plc:a/space/social.grain.group/k/did:plc:b/social.grain.gallery/r"
crates/grain/src/oauth.rs+5−11
@@ -34,9 +34,9 @@ const CALLBACK: &str = "/callback";
/// gallery to Bluesky, which `putRecord` does so that a publish resumed
/// overwrites its own post, and so asks to update as well as create.
///
/// Private galleries and groups ask for [`SPACE_SCOPE`] beside these, and
/// only of a PDS that serves spaces: a login that needs them adds them to
/// what it passes to [`authorize`].
/// Groups ask for [`SPACE_SCOPE`] beside these, and only of a PDS that
/// serves spaces: a login that needs them adds them to what it passes to
/// [`authorize`].
pub const SCOPE: &str = "atproto \
blob:image/* \
repo:social.grain.gallery \
@@ -52,8 +52,6 @@ repo:social.grain.graph.block \
repo:app.bsky.feed.post?action=create&action=update";
/// What a login asks for beside [`SCOPE`] of a PDS that serves spaces: to
/// make private galleries, and read and write them (each is a space of
/// `social.grain.privateGallery` the author makes and alone writes); to
/// read a group's pool and write galleries, favorites and comments to it
/// (a space of `social.grain.group` the group makes, hence any
/// authority); to offer a gallery to a group and, acting as one, answer
@@ -61,10 +59,6 @@ repo:app.bsky.feed.post?action=create&action=update";
/// pool, by service auth for any host; and to read a group's member list
/// and keep its own acceptance there.
pub const SPACE_SCOPE: &str = "\
space:social.grain.privateGallery?authority=*&skey=*&collection=social.grain.gallery\
&collection=social.grain.gallery.item&collection=social.grain.photo\
&action=read&action=create&action=update&action=delete\
&manage=create&manage=update&manage=delete \
space:social.grain.group?authority=*&skey=*&collection=social.grain.gallery\
&collection=social.grain.gallery.item&collection=social.grain.photo\
&collection=social.grain.favorite&collection=social.grain.comment\
@@ -996,9 +990,9 @@ pub(crate) mod tests {
assert!(!SCOPE.contains(" "));
assert_eq!(scope(false), SCOPE);
let spaces = scope(true);
assert!(spaces.starts_with(&format!("{SCOPE} space:social.grain.privateGallery?")));
assert!(spaces.starts_with(&format!("{SCOPE} space:social.grain.group?")));
assert!(!spaces.contains(" ") && !spaces.ends_with(' '));
assert_eq!(spaces.split(' ').count(), SCOPE.split(' ').count() + 11);
assert_eq!(spaces.split(' ').count(), SCOPE.split(' ').count() + 10);
// It may call the appview's moderation as the account, as the
// web's client, which asks SCOPE alone, may not.
let loopback = loopback_scope();
crates/grain/src/space.rs+11−60
@@ -1,4 +1,4 @@
//! Permissioned spaces, where private galleries and groups' pools live.
//! Permissioned spaces, where groups' pools and member lists live.
//!
//! A space is `at://<authority>/space/<type>/<skey>`: its authority is the
//! account it is anchored on, which keeps its member list, and each member
@@ -32,10 +32,6 @@ use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;
/// A private gallery's space type: one gallery a space, keyed by the
/// gallery's rkey, anchored on its author, who alone writes there.
pub const PRIVATE_GALLERY: &str = "social.grain.privateGallery";
/// A group's pool's space type: one a group, keyed `self`, anchored on the
/// group, where each member writes their own galleries, favorites and
/// comments.
@@ -394,12 +390,6 @@ pub struct Admin<'a> {
pub session: &'a Session,
}
/// A member of a space, as its authority lists them.
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct Member {
pub did: String,
}
impl Admin<'_> {
fn pds(&self) -> Xrpc {
Xrpc::new(self.ctx.guarded.clone(), &self.session.service)
@@ -470,46 +460,6 @@ impl Admin<'_> {
.map(drop)
}
/// The member list of `space`, as this account, its authority, keeps
/// it: every page.
pub async fn members(&self, space: &str) -> Result<Vec<Member>> {
#[derive(Deserialize)]
struct Page {
#[serde(default)]
members: Vec<Member>,
cursor: Option<String>,
}
let auth = self.session.auth();
let pds = self.pds();
let mut nsid = "com.atproto.memberspace.listMembers";
let mut cursor: Option<String> = None;
let mut out: Vec<Member> = Vec::new();
for _ in 0..MOST_PAGES {
let mut params = vec![("space", space), ("limit", "100")];
if let Some(c) = &cursor {
params.push(("cursor", c));
}
let page: Page = match pds.query(nsid, &params, Some(&auth)).await {
Err(e) if not_served(&e) && nsid.contains("memberspace") => {
nsid = "com.atproto.simplespace.listMembers";
pds.query(nsid, &params, Some(&auth)).await?
}
answered => answered?,
};
let empty = page.members.is_empty();
for m in page.members {
if !out.contains(&m) {
out.push(m);
}
}
match page.cursor {
Some(next) if !empty && cursor.as_ref() != Some(&next) => cursor = Some(next),
_ => break,
}
}
Ok(out)
}
/// Makes `writes`, each `{$type, collection, rkey, value}` with its
/// `$type` `com.atproto.space.applyWrites#create`, `#update` or
/// `#delete`, in this account's repo in `space`, at once.
@@ -656,12 +606,12 @@ mod tests {
members("did:plc:g"),
"at://did:plc:g/space/fyi.opensocial.members/self"
);
let named = parse(&uri("did:plc:a", PRIVATE_GALLERY, "3l")).unwrap();
let named = parse(&uri("did:plc:a", POOL, "3l")).unwrap();
assert_eq!(
named,
Named {
authority: "did:plc:a".into(),
kind: PRIVATE_GALLERY.into(),
kind: POOL.into(),
skey: "3l".into(),
}
);
@@ -676,7 +626,7 @@ mod tests {
assert_eq!(parse(wrong), None, "{wrong}");
}
assert!(of_kind(&pool("did:plc:g"), POOL).is_ok());
assert!(of_kind(&pool("did:plc:g"), PRIVATE_GALLERY).is_err());
assert!(of_kind(&pool("did:plc:g"), MEMBERS).is_err());
}
#[test]
@@ -801,11 +751,14 @@ mod tests {
ctx: &ctx,
session: &session,
};
let space = uri("did:plc:a", PRIVATE_GALLERY, "k");
let made = admin.create(PRIVATE_GALLERY, "k").await.unwrap_err();
let made = admin.create(POOL, "self").await.unwrap_err();
assert!(not_served(&made), "{made:#}");
assert!(admin.put_member(&space, "did:plc:b", false).await.is_err());
assert!(admin.members(&space).await.is_err());
assert!(
admin
.put_member(&pool("did:plc:a"), "did:plc:b", false)
.await
.is_err()
);
let asked = asked.lock().unwrap();
let methods: Vec<&str> = asked
.iter()
@@ -825,8 +778,6 @@ mod tests {
"/xrpc/com.atproto.simplespace.createSpace",
"/xrpc/com.atproto.memberspace.putMember",
"/xrpc/com.atproto.simplespace.addMember",
"/xrpc/com.atproto.memberspace.listMembers",
"/xrpc/com.atproto.simplespace.listMembers",
]
);
assert!(
crates/grain/tests/spaces.rs+39−103
@@ -1,6 +1,6 @@
//! Permissioned spaces against the reference PDS on the spaces alpha
//! (`GRAIN_DEV_ENV`): a space made, members put on it, and its repos read
//! by its members with credentials and refused to everyone else. These
//! (`GRAIN_DEV_ENV`): a group's pool made, members put on it, and its
//! repos read with credentials and refused to everyone else. These
//! pass trivially on a PDS without spaces.
mod common;
@@ -28,107 +28,6 @@ async fn someone(ctx: &Ctx) -> Session {
session::create(ctx, &a.handle, &a.password).await.unwrap()
}
#[tokio::test]
async fn a_private_gallery_is_read_by_its_members_alone() {
if !spaces() {
return;
}
let dir = tempfile::tempdir().unwrap();
let ctx = ctx(dir.path(), &[]);
let (owner, reader, outsider) = (
someone(&ctx).await,
someone(&ctx).await,
someone(&ctx).await,
);
let admin = Admin {
ctx: &ctx,
session: &owner,
};
let rkey = keys::tid();
let gallery = admin.create(space::PRIVATE_GALLERY, &rkey).await.unwrap();
assert_eq!(
gallery,
space::uri(&owner.did, space::PRIVATE_GALLERY, &rkey)
);
// The photo's blob is the owner's, uploaded as any other.
let pds = Xrpc::new(ctx.guarded.clone(), &owner.service);
let blob = pds
.upload_blob(PNG.to_vec(), "image/png", &owner.auth())
.await
.unwrap();
let now = keys::now_iso();
let writes = [
space::create(
"social.grain.gallery",
&rkey,
json!({"$type": "social.grain.gallery", "title": "Hidden", "createdAt": now}),
),
space::create(
"social.grain.photo",
&format!("{rkey}-0"),
json!({"$type": "social.grain.photo", "photo": blob,
"aspectRatio": {"width": 1, "height": 1}, "createdAt": now}),
),
];
admin.apply_writes(&gallery, &writes).await.unwrap();
admin
.put_member(&gallery, &reader.did, false)
.await
.unwrap();
let members = admin.members(&gallery).await.unwrap();
assert!(members.iter().any(|m| m.did == reader.did), "{members:?}");
assert!(!members.iter().any(|m| m.did == outsider.did));
// The owner reads their own repo with their session, and finds the
// space among their own.
let own = admin
.records(&gallery, "social.grain.gallery")
.await
.unwrap();
assert_eq!(own[0].rkey, rkey);
assert_eq!(own[0].value["title"], "Hidden");
let listed = space::list_spaces(&ctx, &owner, space::PRIVATE_GALLERY)
.await
.unwrap();
assert!(listed.contains(&gallery), "{listed:?}");
// A member reads it with a credential, blob and all.
let credential = space::credential(&ctx, &reader, &gallery).await.unwrap();
assert!(credential.lasts());
assert_eq!(credential.reader, reader.did);
let photos = space::read_with(&ctx, &credential, &owner.did, "social.grain.photo")
.await
.unwrap();
assert_eq!(photos.len(), 1);
let cid = space::blob_cid(&photos[0].value["photo"])
.unwrap()
.to_string();
let bytes = space::blob_with(&ctx, &credential, &owner.did, &cid)
.await
.unwrap();
assert_eq!(bytes, PNG);
let repos = space::repos_with(&ctx, &credential).await.unwrap();
assert_eq!(repos, std::slice::from_ref(&owner.did));
// Anyone else is refused at the exchange, as if there were no such
// space.
let refused = space::credential(&ctx, &outsider, &gallery)
.await
.err()
.unwrap();
assert!(space::denied(&refused), "{refused:#}");
// A delegation token names its reader to the space's authority, and is
// checked against the reader's key.
let token = space::delegation(&ctx, &outsider, &gallery).await.unwrap();
let found = auth::verify_delegation(&ctx, &token).await.unwrap();
assert_eq!(
(found.did, found.space),
(outsider.did.clone(), gallery.clone())
);
}
#[tokio::test]
async fn a_pools_members_write_their_own_repos_and_read_each_others() {
if !spaces() {
@@ -178,6 +77,21 @@ async fn a_pools_members_write_their_own_repos_and_read_each_others() {
["c1"]
);
// And a photo, its blob uploaded as any other.
let pds = Xrpc::new(ctx.guarded.clone(), &member.service);
let blob = pds
.upload_blob(PNG.to_vec(), "image/png", &member.auth())
.await
.unwrap();
let photo = json!({"$type": "social.grain.photo", "photo": blob,
"aspectRatio": {"width": 1, "height": 1}, "createdAt": keys::now_iso()});
let writes = [space::create("social.grain.photo", "p1", photo)];
mine.apply_writes(&pool, &writes).await.unwrap();
// The group finds the space among its own.
let listed = space::list_spaces(&ctx, &group, space::POOL).await.unwrap();
assert!(listed.contains(&pool), "{listed:?}");
// The group reads its members' repos with a credential of its own, and
// asks its authority which repos the space holds.
let credential = space::credential(&ctx, &group, &pool).await.unwrap();
@@ -187,6 +101,19 @@ async fn a_pools_members_write_their_own_repos_and_read_each_others() {
.await
.unwrap();
assert_eq!(read[0].value["text"], "lovely");
assert!(credential.lasts());
assert_eq!(credential.reader, group.did);
let photos = space::read_with(&ctx, &credential, &member.did, "social.grain.photo")
.await
.unwrap();
assert_eq!(photos.len(), 1);
let cid = space::blob_cid(&photos[0].value["photo"])
.unwrap()
.to_string();
let bytes = space::blob_with(&ctx, &credential, &member.did, &cid)
.await
.unwrap();
assert_eq!(bytes, PNG);
// An outsider's own PDS keeps what they write there, but the space
// does not take them for a writer, and will not let them read it.
@@ -205,4 +132,13 @@ async fn a_pools_members_write_their_own_repos_and_read_each_others() {
.err()
.unwrap();
assert!(space::denied(&refused), "{refused:#}");
// A delegation token names its reader to the space's authority, and is
// checked against the reader's key.
let token = space::delegation(&ctx, &outsider, &pool).await.unwrap();
let found = auth::verify_delegation(&ctx, &token).await.unwrap();
assert_eq!(
(found.did, found.space),
(outsider.did.clone(), pool.clone())
);
}
crates/grain-import/src/lib.rs+2−10
@@ -30,7 +30,8 @@
//! label's sequence number and a report's id mean what they meant:
//! `_labels` (an account's labels name it by its bare DID, and stay so),
//! `_reports`, `_mutes`, `_preferences`, `_push_tokens`, `_story_views`,
//! `_space_invites`, `_space_support`.
//! `_space_support`. hatk's `_space_invites`, whom each private gallery
//! was shared with, is not read: grain-serve has no private galleries.
//!
//! hatk gave its labels out as `admin`, from its admin routes, and `self`,
//! from its label rules, which name no labeler: each is grain's, and given
@@ -470,15 +471,6 @@ pub const KEPT: &[Kept] = &[
select: "SELECT did, subject, created_at FROM _story_views",
insert: "INSERT INTO story_view (did, subject, created_at) VALUES (?1, ?2, ?3)",
},
Kept {
table: "space_invite",
source: "_space_invites",
from: "_space_invites",
count: "SELECT count(*) FROM _space_invites",
select: "SELECT space, member_did, author_did, created_at FROM _space_invites",
insert: "INSERT INTO space_invite (space, member_did, author_did, created_at)
VALUES (?1, ?2, ?3, ?4)",
},
Kept {
table: "oauth_key",
source: "_oauth_keys",
crates/grain-import/src/tests.rs+0−26
@@ -93,8 +93,6 @@ CREATE TABLE _mutes (did TEXT NOT NULL, subject TEXT NOT NULL, created_at TEXT N
PRIMARY KEY (did, subject));
CREATE TABLE _space_support (pds_endpoint TEXT PRIMARY KEY, supported INTEGER NOT NULL,
missing TEXT NOT NULL, checked_at TEXT NOT NULL);
CREATE TABLE _space_invites (space TEXT NOT NULL, member_did TEXT NOT NULL,
author_did TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (space, member_did));
CREATE TABLE _story_views (did TEXT NOT NULL, subject TEXT NOT NULL, created_at TEXT NOT NULL,
PRIMARY KEY (did, subject));
"#;
@@ -141,9 +139,6 @@ INSERT INTO _mutes VALUES
INSERT INTO _story_views VALUES
('did:plc:alice', 'at://did:plc:bob/social.grain.story/3l3qo2vuowo2a', '2025-01-01T00:00:00.000Z'),
('did:plc:carol', 'at://did:plc:bob/social.grain.story/3l3qo2vuowo2a', '2025-01-02T00:00:00.000Z');
INSERT INTO _space_invites VALUES
('at://did:plc:alice/space/social.grain.privateGallery/one', 'did:plc:bob', 'did:plc:alice', '2025-01-01T00:00:00.000Z'),
('at://did:plc:alice/space/social.grain.privateGallery/one', 'did:plc:carol', 'did:plc:alice', '2025-01-02T00:00:00.000Z');
INSERT INTO _space_support VALUES
('https://pds.example', 1, '[]', '2025-01-01T00:00:00.000Z'),
('https://old.example', 0, '["com.atproto.space.createSpace"]', '2025-01-02T00:00:00.000Z');
@@ -433,15 +428,6 @@ fn every_row_of_what_the_network_cannot_rebuild_is_imported() {
"did:plc:carol|at://did:plc:bob/social.grain.story/3l3qo2vuowo2a|2025-01-02T00:00:00.000Z",
]
);
assert_eq!(
rows(
"SELECT space, member_did, author_did, created_at FROM space_invite ORDER BY member_did"
),
[
"at://did:plc:alice/space/social.grain.privateGallery/one|did:plc:bob|did:plc:alice|2025-01-01T00:00:00.000Z",
"at://did:plc:alice/space/social.grain.privateGallery/one|did:plc:carol|did:plc:alice|2025-01-02T00:00:00.000Z",
]
);
assert_eq!(
rows(
"SELECT pds_endpoint, supported, missing, checked_at FROM space_support ORDER BY checked_at"
@@ -472,7 +458,6 @@ fn every_row_of_what_the_network_cannot_rebuild_is_imported() {
("notification_pref", 4, 4),
("push_token", 4, 4),
("story_view", 2, 2),
("space_invite", 2, 2),
("oauth_key", 1, 1),
("space_support", 2, 2),
]
@@ -533,17 +518,6 @@ fn every_row_of_what_the_network_cannot_rebuild_is_imported() {
.map(|l| l.neg)
.collect();
assert_eq!(negated, [false, true]);
// A private gallery shared with bob is among the ones shared with
// him, as grain-serve lists them, named as a private gallery's space.
let shared = index.invites("did:plc:bob", 10).unwrap();
let shared: Vec<(&str, &str)> = shared
.iter()
.map(|i| (i.space.as_str(), i.author.as_str()))
.collect();
let space = "at://did:plc:alice/space/social.grain.privateGallery/one";
assert_eq!(shared, [(space, "did:plc:alice")]);
let named = grain::space::of_kind(space, grain::space::PRIVATE_GALLERY).unwrap();
assert_eq!(named.authority, "did:plc:alice");
// What a PDS was found to serve is what grain-serve asks before it
// asks the PDS again.
let (found, _) = index.space_support("https://pds.example").unwrap().unwrap();
crates/grain-serve/src/index/mod.rs+1−2
@@ -138,7 +138,7 @@ pub fn row<T: Record>(r: &rusqlite::Row) -> rusqlite::Result<Row<T>> {
/// What grain keeps for an account, which goes with its records
/// ([`Index::purge`]): its web session, preferences, how and when it
/// reads its notifications, the devices they are pushed to, whom it
/// muted, the stories it watched, and its invitations to spaces.
/// muted, and the stories it watched.
pub const KEPT_FOR: &[&str] = &[
"DELETE FROM web_session WHERE did = ?1",
"DELETE FROM preference WHERE did = ?1",
@@ -147,7 +147,6 @@ pub const KEPT_FOR: &[&str] = &[
"DELETE FROM push_token WHERE did = ?1",
"DELETE FROM mute WHERE did = ?1",
"DELETE FROM story_view WHERE did = ?1",
"DELETE FROM space_invite WHERE member_did = ?1 OR author_did = ?1",
];
/// The author, collection and key of a record's AT-URI.
crates/grain-serve/src/index/schema.rs+7−3
@@ -68,8 +68,7 @@
//! (`com.atproto.moderation.defs#reasonType`, none for one made before
//! grain took them by it) and its `reason`, who made it, and whether it is
//! `open`, `resolved` or `dismissed`, by whom and when;
//! - `mute`, `preference`, `push_token`, `story_view`, `space_invite`,
//! `space_support`;
//! - `mute`, `preference`, `push_token`, `story_view`, `space_support`;
//! - `notification_seen`, when each account last saw its notifications
//! (`updateSeen`'s `seenAt`, in UTC as [`grain::keys::iso`] writes it),
//! and `notification_pref`, how each kind of notification reaches it
@@ -113,7 +112,7 @@
/// Each step of the schema, run once, in order, by `PRAGMA user_version`.
/// A step is only ever appended: one that has run is never changed.
pub const MIGRATIONS: &[&str] = &[V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11];
pub const MIGRATIONS: &[&str] = &[V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12];
const V1: &str = "
CREATE TABLE gallery (
@@ -517,3 +516,8 @@ const V11: &str = "
ALTER TABLE report ADD COLUMN reason_type TEXT;
CREATE INDEX report_subject_did ON report (subject_did, id);
";
/// No private galleries, so no record of whom one was shared with.
const V12: &str = "
DROP TABLE space_invite;
";
crates/grain-serve/src/index/spaces.rs+2−78
@@ -1,9 +1,6 @@
//! What grain keeps of permissioned spaces: `space_support`, whether each
//! PDS was last found to serve them ([`crate::spaces::support`]), and
//! `space_invite`, whom grain shared each private gallery with, for them
//! to find it by ([`crate::spaces::gallery`]): a space leaves no trace on
//! a member's own PDS. An invite grants nothing; the space's authority
//! does. Neither is rebuilt from the network.
//! PDS was last found to serve them ([`crate::spaces::support`]). It is
//! not rebuilt from the network.
use super::Index;
use crate::spaces::Support;
@@ -46,51 +43,6 @@ impl Index {
}
}
const INVITES: &str = "SELECT space, author_did, created_at FROM space_invite v
WHERE v.member_did = ?1 ORDER BY v.created_at DESC LIMIT ?2";
/// A private gallery shared with an account, as grain shared it.
#[derive(Debug, Clone, PartialEq)]
pub struct Invite {
pub space: String,
pub author: String,
pub created_at: String,
}
impl Index {
/// Keeps that `author` shared `space` with `member` at `at`, unless
/// it was kept before.
pub fn invite(
&self,
space: &str,
member: &str,
author: &str,
at: &str,
) -> rusqlite::Result<()> {
let db = self.db.lock().unwrap();
db.execute(
"INSERT INTO space_invite (space, member_did, author_did, created_at)
VALUES (?1, ?2, ?3, ?4) ON CONFLICT (space, member_did) DO NOTHING",
params![space, member, author, at],
)?;
Ok(())
}
/// The `limit` galleries shared with `member` latest.
pub fn invites(&self, member: &str, limit: usize) -> rusqlite::Result<Vec<Invite>> {
let db = self.db.lock().unwrap();
let mut stmt = db.prepare(INVITES)?;
let rows = stmt.query_map(params![member, limit as i64], |r| {
Ok(Invite {
space: r.get(0)?,
author: r.get(1)?,
created_at: r.get(2)?,
})
})?;
rows.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -121,32 +73,4 @@ mod tests {
let (_, age) = index.space_support("https://pds.test").unwrap().unwrap();
assert_eq!(age, i64::MAX);
}
#[test]
fn invites_are_kept_once_and_read_latest_first() {
let index = Index::memory().unwrap();
let at = |n: u8| format!("2026-10-3{n}T00:00:00.000Z");
index
.invite("at://a/space/t/1", "did:plc:m", "did:plc:a", &at(0))
.unwrap();
index
.invite("at://a/space/t/2", "did:plc:m", "did:plc:a", &at(1))
.unwrap();
index
.invite("at://a/space/t/1", "did:plc:m", "did:plc:a", &at(1))
.unwrap();
index
.invite("at://a/space/t/1", "did:plc:n", "did:plc:a", &at(0))
.unwrap();
let found = index.invites("did:plc:m", 10).unwrap();
let spaces: Vec<&str> = found.iter().map(|i| i.space.as_str()).collect();
assert_eq!(spaces, ["at://a/space/t/2", "at://a/space/t/1"]);
assert_eq!(found[1].created_at, at(0), "kept once");
assert_eq!(found[1].author, "did:plc:a");
assert_eq!(index.invites("did:plc:m", 1).unwrap().len(), 1);
index.read(|db| {
super::super::plan::sized(db);
assert_eq!(super::super::plan::scans(db, INVITES), Vec::<String>::new());
});
}
}
crates/grain-serve/src/index/tests.rs+1−4
@@ -349,7 +349,7 @@ fn the_tables_have_the_rows_columns() {
assert_eq!(stmt, *columns, "{table}");
}
assert_eq!(table_of("social.grain.story"), Some("story"));
assert_eq!(table_of("social.grain.privateGallery"), None);
assert_eq!(table_of("social.grain.group"), None);
// A JSON column that holds what is not JSON fails to read.
let at = uri("social.grain.gallery");
index.put(&at, CID, &gallery("Fog")).unwrap();
@@ -675,7 +675,6 @@ fn a_purge_takes_what_grain_keeps_for_the_account_and_leaves_what_admins_hold()
"INSERT INTO push_token VALUES (?1, 'token', 'ios', '')",
"INSERT INTO mute VALUES (?1, 'did:plc:carol', '')",
"INSERT INTO story_view VALUES (?1, 'at://did:plc:carol/social.grain.story/s', '')",
"INSERT INTO space_invite VALUES ('ats://space', ?1, 'did:plc:carol', '')",
"INSERT INTO takedown VALUES (?1, '')",
"INSERT INTO report (subject_uri, subject_did, label, reported_by, created_at)
VALUES (?1, ?1, 'spam', 'did:plc:carol', '')",
@@ -693,7 +692,6 @@ fn a_purge_takes_what_grain_keeps_for_the_account_and_leaves_what_admins_hold()
assert_eq!(index.purge(DID).unwrap(), 1);
let count = |table: &str, did: &str| -> i64 {
let column = match table {
"space_invite" => "member_did",
"report" => "subject_did",
"label" => "uri",
_ => "did",
@@ -709,7 +707,6 @@ fn a_purge_takes_what_grain_keeps_for_the_account_and_leaves_what_admins_hold()
"push_token",
"mute",
"story_view",
"space_invite",
] {
assert_eq!(count(table, DID), 0, "{table}");
assert!(count(table, other) >= 1, "{table} of another");
crates/grain-serve/src/spaces/blob.rs+1−1
crates/grain-serve/src/spaces/gallery.rsdeleted+0−544
544 lines changed: not loaded until asked for.
crates/grain-serve/src/spaces/group.rs+7−8
crates/grain-serve/src/spaces/mod.rs+56−35
crates/grain-serve/src/spaces/pool.rs+118−14
crates/grain-serve/src/spaces/support.rs+1−1
crates/grain-serve/src/login.rs+2−6
crates/grain-serve/tests/spaces.rs+169−342
511 lines changed: not loaded until asked for.
crates/grain-serve/tests/write.rs+9−14
harness/lexicons.mjs+1−5
harness/pds.mjs+4−5
lexicons/social/grain/group/createGallery.json+1−1
lexicons/social/grain/group/defs.json+41−2
lexicons/social/grain/group/getGallery.json+2−2
lexicons/social/grain/privateGallery/addMember.jsondeleted+0−36
lexicons/social/grain/privateGallery/createPrivateGallery.jsondeleted+0−58
lexicons/social/grain/privateGallery/defs.jsondeleted+0−77
lexicons/social/grain/privateGallery/getPrivateGallery.jsondeleted+0−40
lexicons/social/grain/privateGallery/listMembers.jsondeleted+0−34
lexicons/social/grain/privateGallery/listPrivateGalleries.jsondeleted+0−32
lexicons/social/grain/privateGallery/listSharedPrivateGalleries.jsondeleted+0−27
lexicons/README.md+47−54
web/src/lib/components/molecules/FavoriteButton.svelte+1−1
web/src/lib/components/molecules/GalleryCard.svelte+13−15
web/src/lib/queries.ts+0−67
web/src/routes/feeds/groups/+page.svelte+0−1
web/src/routes/private/[authority]/[skey]/+page.sveltedeleted+0−313
web/src/routes/private/create/+page.sveltedeleted+0−279
web/src/routes/profile/[actor]/gallery/[rkey]/+page.svelte+1−2
web/src/routes/settings/account/+page.svelte+2−57
hatk.generated.client.ts+2−2
hatk.generated.ts+25−55