Documented some stuff, improved follow request logic
This commit is contained in:
parent
96805930ac
commit
8faa5b0ed5
12 changed files with 418 additions and 175 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1142,6 +1142,8 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"either",
|
||||
"fetch",
|
||||
"store",
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use puppy::{
|
||||
model::{schema, Bite, FollowRequest, Follows, Profile, Username},
|
||||
post::Author,
|
||||
store::{self, Error},
|
||||
tl::Post,
|
||||
Key, Store,
|
||||
};
|
||||
|
||||
|
@ -13,49 +13,49 @@ fn main() -> store::Result<()> {
|
|||
let linen = get_or_create_actor(&db, "linen")?;
|
||||
if true {
|
||||
println!("creating posts");
|
||||
puppy::create_post(&db, riley, "@linen <3")?;
|
||||
puppy::create_post(&db, linen, "@riley <3")?;
|
||||
puppy::post::create_post(&db, riley, "@linen <3")?;
|
||||
puppy::post::create_post(&db, linen, "@riley <3")?;
|
||||
}
|
||||
|
||||
if true {
|
||||
println!("making riley follow linen");
|
||||
if !db.exists::<Follows>(riley, linen)? {
|
||||
println!("follow relation does not exist yet");
|
||||
if !db.exists::<FollowRequest>(riley, linen)? {
|
||||
println!("no pending follow request; creating");
|
||||
puppy::fr::create(&db, riley, linen)?;
|
||||
puppy::follows::request(&db, riley, linen)?;
|
||||
} else {
|
||||
println!("accepting the pending follow request");
|
||||
puppy::fr::accept(&db, riley, linen)?;
|
||||
puppy::follows::accept(&db, riley, linen)?;
|
||||
}
|
||||
} else {
|
||||
println!("riley already follows linen");
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nPosts on the instance:");
|
||||
for Post {
|
||||
id,
|
||||
content,
|
||||
author,
|
||||
} in puppy::tl::fetch_all(&db)?
|
||||
{
|
||||
let Profile { account_name, .. } = db.get_mixin(author)?.unwrap();
|
||||
let content = content.content.unwrap();
|
||||
println!("- {id} by @{account_name} ({author}):\n{content}",)
|
||||
for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
|
||||
let Author { ref handle, .. } = post.author;
|
||||
let content = post.content.content.as_ref().unwrap();
|
||||
println!("- {} by {handle}:\n{content}", post.id)
|
||||
}
|
||||
|
||||
println!("\nLinen's followers:");
|
||||
for id in puppy::fr::followers_of(&db, linen)? {
|
||||
for id in puppy::follows::followers_of(&db, linen)? {
|
||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||
println!("- @{account_name} ({id})");
|
||||
}
|
||||
|
||||
println!("\nRiley's following:");
|
||||
for id in puppy::fr::following_of(&db, riley)? {
|
||||
for id in puppy::follows::following_of(&db, riley)? {
|
||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||
println!("- @{account_name} ({id})");
|
||||
}
|
||||
|
||||
if false {
|
||||
println!("Biting riley");
|
||||
puppy::bite_actor(&db, linen, riley).unwrap();
|
||||
for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() {
|
||||
puppy::bites::bite_actor(&db, linen, riley).unwrap();
|
||||
for Bite { id, biter, .. } in puppy::bites::bites_on(&db, riley).unwrap() {
|
||||
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
||||
println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ fn gen_spec(name: &Ident) -> impl ToTokens {
|
|||
let by_origin = format!("{prefix}/by-origin");
|
||||
let by_target = format!("{prefix}/by-target");
|
||||
quote! {
|
||||
impl store::types::Value for #name {
|
||||
impl store::types::DataType for #name {
|
||||
type Type = store::types::ArrowSpec;
|
||||
const SPEC: Self::Type = store::types::ArrowSpec {
|
||||
by_origin: store::types::Keyspace(#by_origin),
|
||||
|
|
|
@ -28,7 +28,7 @@ fn make_alias_impl(name: &syn::Ident, field: &syn::Field) -> TokenStream {
|
|||
let keyspace = format!("{prefix}/keyspace");
|
||||
let reversed = format!("{prefix}/reversed");
|
||||
let spec = quote::quote! {
|
||||
impl store::types::Value for #name {
|
||||
impl store::types::DataType for #name {
|
||||
type Type = store::types::AliasSpec;
|
||||
const SPEC: Self::Type = store::types::AliasSpec {
|
||||
keyspace: store::types::Keyspace(#keyspace),
|
||||
|
@ -58,7 +58,7 @@ pub fn mixin(item: TokenStream) -> TokenStream {
|
|||
let keyspace = format!("{prefix}/main");
|
||||
|
||||
let spec = quote::quote! {
|
||||
impl store::types::Value for #name {
|
||||
impl store::types::DataType for #name {
|
||||
type Type = store::types::MixinSpec;
|
||||
const SPEC: Self::Type = store::types::MixinSpec {
|
||||
keyspace: store::types::Keyspace(#keyspace),
|
||||
|
|
|
@ -10,3 +10,5 @@ store = { path = "../store" }
|
|||
fetch = { path = "../fetch" }
|
||||
bincode = "2.0.0-rc.3"
|
||||
chrono = "*"
|
||||
either = "*"
|
||||
derive_more = "*"
|
||||
|
|
|
@ -1,62 +1,38 @@
|
|||
#![feature(iterator_try_collect, try_blocks)]
|
||||
use model::{AuthorOf, Bite, Content, Profile, Username};
|
||||
use store::util::IterExt as _;
|
||||
use model::{Profile, Username};
|
||||
pub use store::{self, Key, Store};
|
||||
|
||||
pub mod model {
|
||||
use bincode::{Decode, Encode};
|
||||
use store::{types::Schema, Alias, Arrow, Key, Mixin};
|
||||
use derive_more::Display;
|
||||
use store::{types::Schema, Alias, Mixin};
|
||||
|
||||
#[derive(Mixin, Encode, Decode)]
|
||||
use crate::follows::Status;
|
||||
pub use crate::{
|
||||
bites::Bite,
|
||||
follows::{FollowRequest, Follows},
|
||||
post::{AuthorOf, Content},
|
||||
};
|
||||
|
||||
/// A "profile" in the social media sense.
|
||||
///
|
||||
/// Contains all presentation information about someone making posts.
|
||||
#[derive(Mixin, Encode, Decode, Debug, Clone)]
|
||||
pub struct Profile {
|
||||
/// How many posts has this user made?
|
||||
pub post_count: usize,
|
||||
pub account_name: String,
|
||||
/// The name used for the profile's handle.
|
||||
pub account_name: Username,
|
||||
/// The name displayed above their posts.
|
||||
pub display_name: Option<String>,
|
||||
/// The "bio", a freeform "about me" field.
|
||||
pub about_string: Option<String>,
|
||||
/// Arbitrary custom metadata fields.
|
||||
pub about_fields: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Mixin, Encode, Decode)]
|
||||
pub struct Content {
|
||||
pub content: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct AuthorOf {
|
||||
#[origin]
|
||||
pub author: Key,
|
||||
#[target]
|
||||
pub object: Key,
|
||||
}
|
||||
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct Follows {
|
||||
#[origin]
|
||||
pub follower: Key,
|
||||
#[target]
|
||||
pub followed: Key,
|
||||
}
|
||||
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct Bite {
|
||||
#[identity]
|
||||
pub id: Key,
|
||||
#[origin]
|
||||
pub biter: Key,
|
||||
#[target]
|
||||
pub victim: Key,
|
||||
}
|
||||
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct FollowRequest {
|
||||
#[identity]
|
||||
pub id: Key,
|
||||
pub origin: Key,
|
||||
pub target: Key,
|
||||
}
|
||||
|
||||
#[derive(Alias)]
|
||||
/// A unique name for an actor that is part of their "handle".
|
||||
#[derive(Alias, Encode, Decode, Clone, PartialEq, Eq, Debug, Hash, Display)]
|
||||
pub struct Username(pub String);
|
||||
|
||||
/// Construct the schema.
|
||||
|
@ -65,6 +41,7 @@ pub mod model {
|
|||
// Mixins
|
||||
.has::<Profile>()
|
||||
.has::<Content>()
|
||||
.has::<Status>()
|
||||
// Aliases
|
||||
.has::<Username>()
|
||||
// Arrows
|
||||
|
@ -75,32 +52,14 @@ pub mod model {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result<Key> {
|
||||
let key = Key::gen();
|
||||
db.run(|tx| {
|
||||
tx.update::<Profile>(author, |mut profile| {
|
||||
profile.post_count += 1;
|
||||
profile
|
||||
})?;
|
||||
tx.add_mixin(key, Content {
|
||||
content: Some(content.to_string()),
|
||||
summary: None,
|
||||
})?;
|
||||
tx.create(AuthorOf {
|
||||
author,
|
||||
object: key,
|
||||
})?;
|
||||
Ok(key)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
|
||||
let key = Key::gen();
|
||||
db.run(|tx| {
|
||||
tx.add_alias(key, Username(username.to_string()))?;
|
||||
let username: Username = username.to_string().into();
|
||||
tx.add_alias(key, username.clone())?;
|
||||
tx.add_mixin(key, Profile {
|
||||
post_count: 0,
|
||||
account_name: username.to_string(),
|
||||
account_name: username,
|
||||
display_name: None,
|
||||
about_string: None,
|
||||
about_fields: Vec::new(),
|
||||
|
@ -109,68 +68,298 @@ pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
|
||||
db.run(|tx| {
|
||||
let keys = tx.outgoing::<AuthorOf>(author).map_ok(|a| a.object);
|
||||
let posts = tx
|
||||
.join_on(keys)?
|
||||
.into_iter()
|
||||
.filter_map(|(k, opt)| try { (k, opt?) })
|
||||
.collect();
|
||||
Ok(posts)
|
||||
})
|
||||
}
|
||||
pub mod bites {
|
||||
//! The most essential feature of any social network.
|
||||
|
||||
pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> {
|
||||
use store::{Arrow, Key, Store};
|
||||
|
||||
/// *Bites you*
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct Bite {
|
||||
#[identity]
|
||||
pub id: Key,
|
||||
#[origin]
|
||||
pub biter: Key,
|
||||
#[target]
|
||||
pub victim: Key,
|
||||
}
|
||||
|
||||
pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result<Key> {
|
||||
db.run(|tx| {
|
||||
let id = Key::gen();
|
||||
tx.create(Bite { id, biter, victim })?;
|
||||
Ok(id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bites_on(db: &Store, victim: Key) -> store::Result<Vec<Bite>> {
|
||||
db.incoming::<Bite>(victim).try_collect()
|
||||
}
|
||||
|
||||
pub mod tl {
|
||||
//! Timelines
|
||||
|
||||
use store::{util::IterExt as _, Error, Key, Result, Store};
|
||||
|
||||
use crate::model::{AuthorOf, Content};
|
||||
|
||||
pub struct Post {
|
||||
pub id: Key,
|
||||
pub author: Key,
|
||||
pub content: Content,
|
||||
}
|
||||
|
||||
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
|
||||
db.run(|tx| {
|
||||
let iter = tx.range::<Content>(..);
|
||||
/// Who has bitten `victim`?
|
||||
pub fn bites_on(db: &Store, victim: Key) -> store::Result<Vec<Bite>> {
|
||||
db.incoming::<Bite>(victim).try_collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod post {
|
||||
//! Timelines: where you go to view the posts.
|
||||
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
use bincode::{Decode, Encode};
|
||||
use chrono::{DateTime, Utc};
|
||||
use either::Either::{Left, Right};
|
||||
use store::{util::IterExt as _, Arrow, Error, Key, Mixin, Result, Store, Transaction};
|
||||
|
||||
use crate::model::Profile;
|
||||
|
||||
/// The contents of a post.
|
||||
#[derive(Mixin, Encode, Decode, Debug, Clone, Default)]
|
||||
pub struct Content {
|
||||
/// Main post body.
|
||||
pub content: Option<String>,
|
||||
/// Content warning for the post.
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&str> for Content {
|
||||
fn from(value: &str) -> Self {
|
||||
value.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Content {
|
||||
fn from(value: String) -> Self {
|
||||
Content {
|
||||
content: Some(value),
|
||||
warning: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The relation that `author` has constructed and published `object`.
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct AuthorOf {
|
||||
#[origin]
|
||||
pub author: Key,
|
||||
#[target]
|
||||
pub object: Key,
|
||||
}
|
||||
|
||||
/// A piece of content posted by someone.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Post {
|
||||
/// The post's internal ID.
|
||||
pub id: Key,
|
||||
/// The actual post contents.
|
||||
pub content: Content,
|
||||
/// Metadata about the post's author.
|
||||
pub author: Author,
|
||||
}
|
||||
|
||||
/// Information about a [`Post`]'s author.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Author {
|
||||
/// The identifier of the author.
|
||||
pub id: Key,
|
||||
/// The name to display along with the post.
|
||||
pub display_name: String,
|
||||
/// An informal identifier for a particular author.
|
||||
pub handle: String,
|
||||
}
|
||||
|
||||
/// An ordered list of [`Post`]s for viewing.
|
||||
#[derive(Debug)]
|
||||
pub struct Timeline {
|
||||
items: Vec<Item>,
|
||||
}
|
||||
|
||||
/// Discrete events that can be displayed to a user as part of a timeline.
|
||||
#[derive(Debug)]
|
||||
enum Item {
|
||||
Post(Post),
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Get the timeline item if it is a [`Post`].
|
||||
pub fn as_post(&self) -> Option<&Post> {
|
||||
match self {
|
||||
Item::Post(ref post) => Some(post),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
/// Get all the posts in the timeline.
|
||||
pub fn posts(&self) -> impl Iterator<Item = &Post> {
|
||||
self.items.iter().filter_map(|x| x.as_post())
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets at most `limit` of the posts known to the instance that were inserted within `time_range`.
|
||||
pub fn fetch_timeline(
|
||||
db: &Store,
|
||||
time_range: impl RangeBounds<DateTime<Utc>>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Timeline> {
|
||||
let posts = db.run(|tx| {
|
||||
// Get all post content entries (the argument passed here is a range of chrono datetimes).
|
||||
let iter = tx.range::<Content>(time_range);
|
||||
let iter = match limit {
|
||||
Some(n) => Left(iter.take(n)),
|
||||
None => Right(iter),
|
||||
};
|
||||
// Then, we're gonna map each of them to their author, and get the profile information needed to
|
||||
// render the post (mostly display name and handle).
|
||||
iter.bind_results(|(id, content)| {
|
||||
let AuthorOf { author, .. } =
|
||||
tx.incoming::<AuthorOf>(id).next_or(Error::Missing)?;
|
||||
Ok(Post {
|
||||
// Take the first author. There is nothing stopping a post from having multiple authors, but
|
||||
// let's take it one step at a time.
|
||||
let (author, Some(Profile { display_name, account_name, .. })) = tx
|
||||
.join_on(|a: AuthorOf| a.author, tx.incoming(id))?
|
||||
.swap_remove(0)
|
||||
else {
|
||||
// We expect all posts to have at least one author, so we should complain if there is one
|
||||
// that doesn't (for now). For robustness, the `.collect()` down there should be replaced
|
||||
// with a strategy where we log a warning instead of failing, but in the current state of
|
||||
// the project, failing fast is a good thing.
|
||||
return Err(Error::Missing);
|
||||
};
|
||||
Ok(Item::Post(Post {
|
||||
id,
|
||||
author,
|
||||
author: Author {
|
||||
id: author,
|
||||
handle: format!("@{account_name}"),
|
||||
display_name: display_name.unwrap_or(account_name.0),
|
||||
},
|
||||
content,
|
||||
})
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
Ok(Timeline { items: posts })
|
||||
}
|
||||
|
||||
/// Create a new post.
|
||||
pub fn create_post(db: &Store, author: Key, content: impl Into<Content>) -> store::Result<Key> {
|
||||
db.run(|tx| mixin_post(tx, Key::gen(), author, content))
|
||||
}
|
||||
|
||||
/// Add a post's mixins and predicates to an existing `node`.
|
||||
pub fn mixin_post(
|
||||
tx: &Transaction<'_>,
|
||||
node: Key,
|
||||
author: Key,
|
||||
content: impl Into<Content>,
|
||||
) -> store::Result<Key> {
|
||||
tx.update::<Profile>(author, |mut profile| {
|
||||
profile.post_count += 1;
|
||||
profile
|
||||
})?;
|
||||
tx.add_mixin(node, content.into())?;
|
||||
tx.create(AuthorOf { author, object: node })?;
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn list_posts_by_author(db: &Store, author: Key) -> store::Result<Vec<(Key, Content)>> {
|
||||
db.run(|tx| {
|
||||
let posts = tx
|
||||
.join_on(|a: AuthorOf| a.object, tx.outgoing(author))?
|
||||
.into_iter()
|
||||
.filter_map(|(k, opt)| try { (k, opt?) })
|
||||
.collect();
|
||||
Ok(posts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fr {
|
||||
//! Follow requests
|
||||
pub mod follows {
|
||||
//! Follow requests and related stuff.
|
||||
|
||||
use store::{util::IterExt as _, Key, Store, OK};
|
||||
use bincode::{Decode, Encode};
|
||||
use store::{util::IterExt, Arrow, Error, Key, Mixin, Store, OK};
|
||||
|
||||
use crate::model::{FollowRequest, Follows};
|
||||
/// A predicate; `follower` "follows" `followed`.
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct Follows {
|
||||
#[origin]
|
||||
pub follower: Key,
|
||||
#[target]
|
||||
pub followed: Key,
|
||||
}
|
||||
|
||||
pub fn create(db: &Store, requester: Key, target: Key) -> store::Result<FollowRequest> {
|
||||
/// An instance of a request from some `origin` user to follow a `target` user.
|
||||
///
|
||||
/// This should not be used to determine whether two actors are following each other. For that, use
|
||||
/// [`Follows`], a basic arrow for exactly this purpose. *This* arrow is used to identify specific
|
||||
/// instances of *requests*, and serves mostly as a historical reference and for synchronizing with
|
||||
/// other servers.
|
||||
///
|
||||
/// Mixins always present for the `id`:
|
||||
///
|
||||
/// - [`Status`], carrying the status of the request.
|
||||
#[derive(Arrow, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct FollowRequest {
|
||||
/// The unique ID of this particular request.
|
||||
#[identity]
|
||||
pub id: Key,
|
||||
/// The "follower", the user that made the request.
|
||||
pub origin: Key,
|
||||
/// The one the request is made to.
|
||||
pub target: Key,
|
||||
}
|
||||
|
||||
impl FollowRequest {
|
||||
/// Determine if this follow request is pending.
|
||||
pub fn is_pending(&self, db: &Store) -> store::Result<bool> {
|
||||
// The status is stored as a mixin, so we need to get it.
|
||||
let Some(st) = db.get_mixin::<Status>(self.id)? else {
|
||||
// If we don't have a status for a follow request, something is borked.
|
||||
return Err(Error::Missing);
|
||||
};
|
||||
// If the status of the follow request is pending, it can't also be true that the follows
|
||||
// relation already exists.
|
||||
debug_assert! {
|
||||
!(st == Status::Pending)
|
||||
|| db.exists::<Follows>(self.origin, self.target).map(|x| !x)?,
|
||||
"fr.is_pending -> !(fr.origin follows fr.target)"
|
||||
};
|
||||
Ok(st == Status::Pending)
|
||||
}
|
||||
}
|
||||
|
||||
/// The status of a [`FollowRequest`].
|
||||
///
|
||||
/// Valid state transitions:
|
||||
///
|
||||
/// ```text
|
||||
/// ┌──────────────▶ Rejected
|
||||
/// │
|
||||
/// │
|
||||
/// │
|
||||
///
|
||||
/// None ─────────▶ Pending ────────▶ Accepted
|
||||
///
|
||||
/// │ │
|
||||
/// │ │
|
||||
/// │ │
|
||||
/// ▼ │
|
||||
/// Withdrawn ◀────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// In addition, a follow request will be deleted if either endpoint is removed from the graph.
|
||||
#[derive(Mixin, Encode, Decode, Eq, PartialEq, Clone)]
|
||||
pub enum Status {
|
||||
/// The follow request was previously pending or accepted, but since withdrawn.
|
||||
///
|
||||
/// This can happen when someone cancels their follow request or unfollows the target.
|
||||
Withdrawn,
|
||||
/// The follow request was accepted.
|
||||
Accepted,
|
||||
/// The follow request was denied.
|
||||
Rejected,
|
||||
/// The follow request is still under review.
|
||||
Pending,
|
||||
}
|
||||
|
||||
/// Request to follow another actor.
|
||||
pub fn request(db: &Store, requester: Key, target: Key) -> store::Result<FollowRequest> {
|
||||
db.run(|tx| {
|
||||
let req = FollowRequest {
|
||||
id: Key::gen(),
|
||||
|
@ -178,38 +367,56 @@ pub mod fr {
|
|||
target,
|
||||
};
|
||||
tx.create(req)?;
|
||||
tx.add_mixin(req.id, Status::Pending)?;
|
||||
Ok(req)
|
||||
})
|
||||
}
|
||||
|
||||
/// Accept the open follow request from `requester` to `target`, if one exists.
|
||||
pub fn accept(db: &Store, requester: Key, target: Key) -> store::Result<()> {
|
||||
db.run(|tx| {
|
||||
tx.delete_all::<FollowRequest>(requester, target)?;
|
||||
// TODO: This logic is a little broken but it'll do for now. i'll fix it later.
|
||||
let fr = tx
|
||||
.between::<FollowRequest>(requester, target)
|
||||
.filter(|fr| fr.as_ref().is_ok_and(|f| f.target == target))
|
||||
// We'll want the latest one, because that one was inserted last so it'll be the most
|
||||
// recent
|
||||
.last()
|
||||
.ok_or_else(|| Error::Missing)??;
|
||||
// Only apply the update if the last follow request is still in a pending state.
|
||||
if let Some(Status::Pending) = db.get_mixin(fr.id)? {
|
||||
tx.update(fr.id, |_| Status::Accepted)?;
|
||||
tx.create(Follows {
|
||||
follower: requester,
|
||||
followed: target,
|
||||
})?;
|
||||
}
|
||||
OK
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reject(db: &Store, requester: Key, target: Key) -> store::Result<()> {
|
||||
pub fn reject(db: &Store, request: Key) -> store::Result<()> {
|
||||
db.run(|tx| {
|
||||
tx.delete_all::<FollowRequest>(requester, target)?;
|
||||
tx.update(request, |_| Status::Rejected)?;
|
||||
OK
|
||||
})
|
||||
}
|
||||
|
||||
/// List all pending follow requests for a user.
|
||||
pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<FollowRequest>> {
|
||||
db.incoming::<FollowRequest>(target).collect()
|
||||
db.incoming::<FollowRequest>(target)
|
||||
.filter_bind_results(|req| Ok(if req.is_pending(db)? { Some(req) } else { None }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all actors followed by `actor`.
|
||||
pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
||||
db.outgoing::<Follows>(actor)
|
||||
.map_ok(|a| a.followed)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all actors following `actor`.
|
||||
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
|
||||
db.incoming::<Follows>(actor)
|
||||
.map_ok(|a| a.follower)
|
||||
|
@ -236,7 +443,7 @@ pub mod fr {
|
|||
fn create_fr() -> store::Result<()> {
|
||||
Store::test(schema(), |db| {
|
||||
let (alice, bob) = make_test_actors(&db)?;
|
||||
super::create(&db, alice, bob)?;
|
||||
super::request(&db, alice, bob)?;
|
||||
assert!(
|
||||
db.exists::<FollowRequest>(alice, bob)?,
|
||||
"(alice -> bob) ∈ follow-requested"
|
||||
|
@ -258,7 +465,7 @@ pub mod fr {
|
|||
fn accept_fr() -> store::Result<()> {
|
||||
Store::test(schema(), |db| {
|
||||
let (alice, bob) = make_test_actors(&db)?;
|
||||
super::create(&db, alice, bob)?;
|
||||
super::request(&db, alice, bob)?;
|
||||
super::accept(&db, alice, bob)?;
|
||||
|
||||
assert!(
|
||||
|
@ -284,7 +491,7 @@ pub mod fr {
|
|||
fn listing_follow_relations() -> store::Result<()> {
|
||||
Store::test(schema(), |db| {
|
||||
let (alice, bob) = make_test_actors(&db)?;
|
||||
super::create(&db, alice, bob)?;
|
||||
super::request(&db, alice, bob)?;
|
||||
super::accept(&db, alice, bob)?;
|
||||
|
||||
let followers_of_bob = super::followers_of(&db, bob)?;
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
pub use r#macro::Alias;
|
||||
|
||||
use super::{
|
||||
types::{AliasSpec, Value},
|
||||
types::{AliasSpec, DataType},
|
||||
Batch, Store, Transaction,
|
||||
};
|
||||
use crate::{Key, Result};
|
||||
|
||||
/// An alternative unique identifier for a node.
|
||||
pub trait Alias: Value<Type = AliasSpec> + From<String> + AsRef<str> {}
|
||||
pub trait Alias: DataType<Type = AliasSpec> + From<String> + AsRef<str> {}
|
||||
|
||||
impl Transaction<'_> {
|
||||
/// Look up the key associated with the alias.
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
pub use self::kinds::{Basic, Multi};
|
||||
use super::{
|
||||
types::{ArrowSpec, Value},
|
||||
types::{ArrowSpec, DataType},
|
||||
Batch, Store, Transaction,
|
||||
};
|
||||
use crate::{util::IterExt as _, Key, Result};
|
||||
|
@ -50,7 +50,7 @@ use crate::{util::IterExt as _, Key, Result};
|
|||
/// A directed edge.
|
||||
///
|
||||
/// See the [module docs][self] for an introduction.
|
||||
pub trait Arrow: Value<Type = ArrowSpec> + From<Self::Kind> + Into<Self::Kind> {
|
||||
pub trait Arrow: DataType<Type = ArrowSpec> + From<Self::Kind> + Into<Self::Kind> {
|
||||
/// The representation of this arrow, which also determines whether parallel edges are allowed.
|
||||
type Kind: ArrowKind = Basic;
|
||||
}
|
||||
|
@ -115,6 +115,13 @@ impl Store {
|
|||
{
|
||||
op::outgoing::<A>(self, origin).map_ok(A::from)
|
||||
}
|
||||
/// List all arrows between `a` and `b`, in either direction.
|
||||
pub fn between<'a, A>(&'a self, a: Key, b: Key) -> impl Iterator<Item = Result<A>> + 'a
|
||||
where
|
||||
A: Arrow<Kind = Multi> + 'a,
|
||||
{
|
||||
op::between::<A>(self, a, b).map_ok(A::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transaction<'_> {
|
||||
|
@ -191,6 +198,13 @@ impl Transaction<'_> {
|
|||
{
|
||||
op::delete_one::<A>(self, arrow.into())
|
||||
}
|
||||
/// List all arrows between `a` and `b`, in either direction.
|
||||
pub fn between<'a, A>(&'a self, a: Key, b: Key) -> impl Iterator<Item = Result<A>> + 'a
|
||||
where
|
||||
A: Arrow<Kind = Multi> + 'a,
|
||||
{
|
||||
op::between::<A>(self, a, b).map_ok(A::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Batch {
|
||||
|
@ -274,17 +288,29 @@ mod op {
|
|||
.map_ok(|(ref k, _)| A::Kind::dec(k))
|
||||
}
|
||||
|
||||
/// Get all arrows between the two endpoints (in either direction)
|
||||
pub fn between<'db, A>(
|
||||
cx: &'db impl Query,
|
||||
origin: Key,
|
||||
target: Key,
|
||||
) -> impl Iterator<Item = Result<A::Kind>> + 'db
|
||||
where
|
||||
A: Arrow,
|
||||
A::Kind: 'db,
|
||||
{
|
||||
let ks = cx.open(A::SPEC.by_origin);
|
||||
ks.scan(origin.fuse(target))
|
||||
.chain(ks.scan(target.fuse(origin)))
|
||||
.map_ok(|(ref k, _)| A::Kind::dec(k))
|
||||
}
|
||||
|
||||
/// Create a new arrow.
|
||||
pub fn create<A>(cx: &impl Write, arrow: A::Kind) -> Result<()>
|
||||
where
|
||||
A: Arrow,
|
||||
{
|
||||
if A::Kind::IS_MULTI {
|
||||
let Multi {
|
||||
identity,
|
||||
origin,
|
||||
target,
|
||||
} = unsafe { arrow.raw().multi };
|
||||
let Multi { identity, origin, target } = unsafe { arrow.raw().multi };
|
||||
cx.open(MULTIEDGE_HEADERS)
|
||||
.set(identity, origin.fuse(target))?;
|
||||
}
|
||||
|
@ -471,7 +497,7 @@ mod kinds {
|
|||
/// Derive [`Arrow`] for a struct.
|
||||
///
|
||||
/// This will generate the required [`Into`] and [`From`] impls, as well as an [`Arrow`](trait@Arrow) impl and
|
||||
/// a [`Value`] impl with the namespaces derived from the name of the struct. The macro works on structs with
|
||||
/// a [`DataType`] impl with the namespaces derived from the name of the struct. The macro works on structs with
|
||||
/// specific fields, or newtypes of any arrow kind.
|
||||
///
|
||||
/// # Attributes
|
||||
|
|
|
@ -157,10 +157,11 @@ mod cx {
|
|||
|
||||
impl Context for Store {
|
||||
fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> {
|
||||
Keyspace {
|
||||
cf: self.inner.cf_handle(cf.as_ref()).unwrap(),
|
||||
context: &self,
|
||||
}
|
||||
let name = cf.as_ref();
|
||||
let Some(cf) = self.inner.cf_handle(name) else {
|
||||
panic!("unregistered keyspace {name}! is it in the schema?")
|
||||
};
|
||||
Keyspace { context: &self, cf }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,10 +206,11 @@ mod cx {
|
|||
|
||||
impl Context for Transaction<'_> {
|
||||
fn open<'cx>(&'cx self, cf: impl AsRef<str>) -> Keyspace<'cx, Self> {
|
||||
Keyspace {
|
||||
cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(),
|
||||
context: &self,
|
||||
}
|
||||
let name = cf.as_ref();
|
||||
let Some(cf) = self.store.inner.cf_handle(name) else {
|
||||
panic!("unregistered keyspace {name}! is it in the schema?")
|
||||
};
|
||||
Keyspace { context: &self, cf }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,10 +273,11 @@ mod cx {
|
|||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Keyspace {
|
||||
cf: self.store.inner.cf_handle(cf.as_ref()).unwrap(),
|
||||
context: &self,
|
||||
}
|
||||
let name = cf.as_ref();
|
||||
let Some(cf) = self.store.inner.cf_handle(name) else {
|
||||
panic!("unregistered keyspace {name}! is it in the schema?")
|
||||
};
|
||||
Keyspace { context: &self, cf }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ use bincode::{Decode, Encode};
|
|||
use chrono::{DateTime, Utc};
|
||||
|
||||
use super::{
|
||||
types::{MixinSpec, Value},
|
||||
types::{DataType, MixinSpec},
|
||||
Batch, Store, Transaction,
|
||||
};
|
||||
use crate::{Error, Key, Result};
|
||||
use crate::{util::IterExt as _, Error, Key, Result};
|
||||
|
||||
/// Mixins are the simplest pieces of data in the store.
|
||||
pub trait Mixin: Value<Type = MixinSpec> + Encode + Decode {}
|
||||
pub trait Mixin: DataType<Type = MixinSpec> + Encode + Decode {}
|
||||
|
||||
/// Derive a [`Mixin`] implementation.
|
||||
///
|
||||
|
@ -111,14 +111,15 @@ impl Transaction<'_> {
|
|||
op::get_range(self, range)
|
||||
}
|
||||
/// Think "LEFT JOIN". In goes an iterator over keys, out come all the associated results.
|
||||
pub fn join_on<M>(
|
||||
pub fn join_on<M, T>(
|
||||
&self,
|
||||
iter: impl IntoIterator<Item = Result<Key>>,
|
||||
f: impl Fn(T) -> Key,
|
||||
iter: impl IntoIterator<Item = Result<T>>,
|
||||
) -> Result<Vec<(Key, Option<M>)>>
|
||||
where
|
||||
M: Mixin,
|
||||
{
|
||||
op::join_on(self, iter)
|
||||
op::join_on(self, iter.into_iter().map_ok(f))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! There is a lot of complicated machinery here to make it so that you have to write very little code to
|
||||
//! define new types. Basically, if you want to define a thing to store, you need to implement the trait
|
||||
//! for it (e.g. [`Arrow`]), and also implement [`Value`], where you create a specification describing which
|
||||
//! for it (e.g. [`Arrow`]), and also implement [`DataType`], where you create a specification describing which
|
||||
//! namespaces store records of that type.
|
||||
//!
|
||||
//! Then, when you construct a new `Store`, you need to pass in a [`Schema`], or the database won't be able
|
||||
|
@ -66,7 +66,7 @@ impl Schema {
|
|||
/// Add the component to the schema.
|
||||
pub fn has<C>(mut self) -> Schema
|
||||
where
|
||||
C: Value,
|
||||
C: DataType,
|
||||
{
|
||||
self.add(C::SPEC);
|
||||
self
|
||||
|
@ -94,7 +94,7 @@ impl AsRef<str> for Keyspace {
|
|||
/// [mixin](MixinSpec).
|
||||
///
|
||||
/// All namespaces must be unique, and added to the [`Schema`].
|
||||
pub trait Value {
|
||||
pub trait DataType {
|
||||
type Type: TypeSpec;
|
||||
const SPEC: Self::Type;
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ pub struct MixinSpec {
|
|||
pub keyspace: Keyspace,
|
||||
}
|
||||
|
||||
/// Describes how to add a [`Value`] to a [`Schema`].
|
||||
/// Describes how to add a [`DataType`] to a [`Schema`].
|
||||
pub trait TypeSpec {
|
||||
/// Register the namespaces.
|
||||
fn register(&self, set: &mut HashSet<Keyspace>);
|
||||
|
|
|
@ -2,3 +2,5 @@ unstable_features = true
|
|||
overflow_delimited_expr = true
|
||||
group_imports = "StdExternalCrate"
|
||||
use_field_init_shorthand = true
|
||||
reorder_modules = false
|
||||
struct_lit_width = 30
|
||||
|
|
Loading…
Reference in a new issue