mod collaborators;
mod drafts;
mod error;
mod ledger;
mod manifest;
mod patches;
mod session;
mod social;
#[cfg(test)]
mod tests;
pub(crate) use drafts::StoredManifest;
pub use error::WriteError;
pub(crate) use ledger::keep_ref;
pub use manifest::Settings;
pub use session::{Done, Method, Session, Written, fork_head, session};
pub(super) const NOT_STATE_AUTHORITY: &str =
"State changes belong to the ticket's author or a maintainer";
pub(super) const NOT_ASSIGN_AUTHORITY: &str = "Assignees belong to a maintainer";
pub(super) const NOT_LABEL_AUTHORITY: &str = "label defs belong to the repository's owner alone";
pub(super) const SETTINGS_DENIED: &str = "settings belong to the repository's owner alone";
pub(super) const COLLABORATE_REMEDY: &str = "Please ask the repo owner to add you as a collaborator, or to open the contribution policy to members or anyone, thanks.";
use std::cell::RefCell;
use knot_cob::{CobHome, WriteContext};
use knot_cobs::{Artifact, Comment, LabelDef, Patch, Reaction, Ticket};
use knot_git::Repo;
use knot_index::Index;
use knot_record::artifact::artifact_collection;
use knot_record::comment::comment_collection;
use knot_record::grant::COLLABORATOR_COLLECTION;
use knot_record::label::label_def_collection;
use knot_record::manifest::{MANIFEST_RKEY, manifest_collection};
use knot_record::reaction::reaction_collection;
use knot_record::ticket::ticket_collection;
use knot_runtime::Signer;
use knot_types::{BlobCid, RepoDid, UnixSeconds};
use drafts::{CreateRecordInput, PutRecordInput, SubmitPatchInput, SwapInput, rejected_collection};
use ledger::decode;
use crate::blobs::Store;
/// the caller holds the repo's exclusive lock while this is open.
pub struct Ledger<'a> {
pub(super) index: &'a Index,
pub(super) git: &'a Repo,
pub(super) meta: &'a Repo,
pub(super) home: &'a CobHome,
pub(super) repo: &'a RepoDid,
pub(super) signer: &'a dyn Signer,
pub(super) blobs: &'a Store,
pub(super) chain_root: &'a std::path::Path,
/// blobs are checked only after the write is authorized.
pub(super) cited: RefCell<Vec<(BlobCid, String, u64)>>,
pub(super) now: UnixSeconds,
}
impl Ledger<'_> {
pub fn apply(
&self,
session: &Session,
method: Method,
body: &[u8],
) -> Result<Done, WriteError> {
let cx = WriteContext::new(&session.actor, &session.repo, session.permission);
match method {
Method::CreateRecord => {
let input: CreateRecordInput = decode(body)?;
self.addressed(&input.repo)?;
self.create_record(&cx, &input)
}
Method::PutRecord => {
let input: PutRecordInput = decode(body)?;
self.addressed(&input.swap.repo)?;
self.put_record(&cx, input)
}
Method::DeleteRecord => {
let swap: SwapInput = decode(body)?;
self.addressed(&swap.repo)?;
self.delete_record(&cx, swap)
}
Method::SubmitPatch => {
let input: SubmitPatchInput = decode(body)?;
self.addressed(&input.repo)?;
self.submit_patch(&cx, input)
}
}
}
pub fn settings(&self, session: &Session, patch: &Settings) -> Result<Done, WriteError> {
let cx = WriteContext::new(&session.actor, &session.repo, session.permission);
let address = Self::manifest_address();
self.ensure_manifest(&cx, &address)?;
let (object, live) = self.standing(&address)?;
let stored = self.stored_manifest(object, &live)?;
let mut record: serde_json::Value = serde_ipld_dagcbor::from_slice(stored.as_bytes())
.map_err(|error| WriteError::internal(error.to_string()))?;
patch.apply(&mut record);
let swap: SwapInput = serde_json::from_value(serde_json::json!({
"repo": self.repo,
"collection": manifest_collection(),
"rkey": MANIFEST_RKEY,
"swapRecord": stored.cid().to_string(),
}))
.map_err(|error| WriteError::internal(error.to_string()))?;
self.manifest_put(&cx, swap, record).map(Done::Written)
}
fn addressed(&self, repo: &RepoDid) -> Result<(), WriteError> {
if repo != self.repo {
return Err(WriteError::invalid_request(format!(
"the write is for {repo}, and this session is on {}",
self.repo
)));
}
Ok(())
}
fn create_record(
&self,
cx: &WriteContext<'_>,
input: &CreateRecordInput,
) -> Result<Done, WriteError> {
let CreateRecordInput {
collection,
rkey,
record,
..
} = input;
if rkey.is_some()
&& [
ticket_collection(),
comment_collection(),
reaction_collection(),
artifact_collection(),
]
.contains(collection)
{
return Err(WriteError::invalid_request(format!(
"this knot mints the record key for {collection}; please leave rkey out"
)));
}
let record = record.clone();
let written = if *collection == ticket_collection() {
self.ticket_open(cx, record)?
} else if *collection == comment_collection() {
self.comment_open(cx, record)?
} else if *collection == reaction_collection() {
self.reaction_open(cx, record)?
} else if *collection == label_def_collection() {
self.label_def_open(cx, rkey.clone(), record)?
} else if collection.as_str() == COLLABORATOR_COLLECTION {
return self.collaborator_add(cx, rkey.clone(), record);
} else if *collection == artifact_collection() {
self.artifact_open(cx, record)?
} else {
return Err(rejected_collection(collection));
};
Ok(Done::Written(written))
}
fn put_record(&self, cx: &WriteContext<'_>, input: PutRecordInput) -> Result<Done, WriteError> {
use knot_record::{artifact, grant, manifest, pull, reaction};
let PutRecordInput { swap, record } = input;
let written = match swap.collection.as_str() {
reaction::REACTION_COLLECTION => Err(WriteError::invalid_request(
"createRecord makes a reaction and deleteRecord removes it; a reaction is never edited",
)),
knot_record::ticket::TICKET_COLLECTION => self.ticket_put(cx, swap, record),
knot_record::comment::COMMENT_COLLECTION => self.comment_put(cx, swap, record),
knot_record::label::LABEL_DEF_COLLECTION => self.label_def_put(cx, swap, record),
pull::PATCH_COLLECTION => Err(WriteError::invalid_request(
"submitPatch makes a patch and deleteRecord removes it; a patch is immutable, so \
resubmit a new patch with submitPatch",
)),
grant::MEMBER_COLLECTION | grant::COLLABORATOR_COLLECTION | grant::BLOCK_COLLECTION => {
Err(WriteError::invalid_request(
"a grant is never edited; delete and recreate",
))
}
manifest::MANIFEST_COLLECTION => self.manifest_put(cx, swap, record),
artifact::ARTIFACT_COLLECTION => Err(WriteError::invalid_request(
"an artifact is never edited; delete it and create a new one",
)),
_ => Err(rejected_collection(&swap.collection)),
}?;
Ok(Done::Written(written))
}
fn delete_record(&self, cx: &WriteContext<'_>, swap: SwapInput) -> Result<Done, WriteError> {
use knot_record::{artifact, comment, label, pull, reaction, ticket};
match swap.collection.as_str() {
COLLABORATOR_COLLECTION => self.collaborator_remove(cx, swap),
ticket::TICKET_COLLECTION => self.erase_of::<Ticket>(cx, swap),
comment::COMMENT_COLLECTION => self.erase_of::<Comment>(cx, swap),
reaction::REACTION_COLLECTION => self.erase_of::<Reaction>(cx, swap),
label::LABEL_DEF_COLLECTION => self.erase_of::<LabelDef>(cx, swap),
pull::PATCH_COLLECTION => self.erase_of::<Patch>(cx, swap),
artifact::ARTIFACT_COLLECTION => self.erase_of::<Artifact>(cx, swap),
_ => Err(rejected_collection(&swap.collection)),
}
}
}