···4646 pub is_read: bool,
4747 #[serde(skip_serializing_if = "core::option::Option::is_none")]
4848 pub labels: core::option::Option<Vec<crate::com::atproto::label::defs::Label>>,
4949- ///Expected values are 'like', 'repost', 'follow', 'mention', 'reply', 'quote', 'starterpack-joined', 'verified', and 'unverified'.
4949+ ///The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.
5050 pub reason: String,
5151 #[serde(skip_serializing_if = "core::option::Option::is_none")]
5252 pub reason_subject: core::option::Option<String>,
+1-1
crates/weaver-common/src/lexicons/client.rs
···11501150 _ => Err(atrium_xrpc::Error::UnexpectedResponseType),
11511151 }
11521152 }
11531153- ///Find posts matching search criteria, returning views of those posts.
11531153+ ///Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.
11541154 pub async fn search_posts(
11551155 &self,
11561156 params: crate::app::bsky::feed::search_posts::Parameters,
···22//!Definitions for the `sh.weaver.notebook` namespace.
33pub mod authors;
44pub mod book;
55+pub mod chapter;
56pub mod defs;
67pub mod entry;
78#[derive(Debug)]
···1516impl atrium_api::types::Collection for Book {
1617 const NSID: &'static str = "sh.weaver.notebook.book";
1718 type Record = book::Record;
1919+}
2020+#[derive(Debug)]
2121+pub struct Chapter;
2222+impl atrium_api::types::Collection for Chapter {
2323+ const NSID: &'static str = "sh.weaver.notebook.chapter";
2424+ type Record = chapter::Record;
1825}
1926#[derive(Debug)]
2027pub struct Entry;
···3030 pub prev: core::option::Option<BookEntryRef>,
3131}
3232pub type BookEntryView = atrium_api::types::Object<BookEntryViewData>;
3333+///The format of the content. This is used to determine how to render the content.
3434+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
3535+#[serde(rename_all = "camelCase")]
3636+pub struct ContentFormatData {
3737+ ///The format of the content. This is used to determine how to render the content.
3838+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
3939+ pub markdown: core::option::Option<String>,
4040+}
4141+pub type ContentFormat = atrium_api::types::Object<ContentFormatData>;
3342#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
3443#[serde(rename_all = "camelCase")]
3544pub struct EntryViewData {
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `sh.weaver.publish` namespace.
33+pub mod blob;
44+pub mod defs;
55+#[derive(Debug)]
66+pub struct Blob;
77+impl atrium_api::types::Collection for Blob {
88+ const NSID: &'static str = "sh.weaver.publish.blob";
99+ type Record = blob::Record;
1010+}
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `sh.weaver.publish.defs` namespace.
+85-1
crates/weaver-common/src/lib.rs
···88pub mod oauth;
99pub mod resolver;
1010pub mod xrpc_server;
1111-use atrium_api::types::{BlobRef, TypedBlobRef, string::Did};
1111+use std::sync::OnceLock;
1212+1313+pub use atrium_api::types::*;
1214pub use lexicons::*;
1515+use regex::Regex;
1616+use string::Did;
13171418pub use crate::error::{Error, IoError, ParseError, SerDeError};
1519···5660 mime_type.strip_prefix("image/").unwrap_or(mime_type)
5761 )
5862}
6363+6464+pub fn blob_url(did: &Did, pds: &str, blob_ref: &BlobRef) -> String {
6565+ let cid = match blob_ref {
6666+ BlobRef::Typed(TypedBlobRef::Blob(b)) => atrium_api::types::string::Cid::new(b.r#ref.0)
6767+ .as_ref()
6868+ .to_string(),
6969+7070+ BlobRef::Untyped(r) => r.cid.clone(),
7171+ };
7272+ format!(
7373+ "https://{}/xrpc/com.atproto.repo.getBlob?did={}&cid={}",
7474+ pds,
7575+ did.as_str(),
7676+ cid,
7777+ )
7878+}
7979+8080+pub fn match_identifier(maybe_identifier: &str) -> Option<&str> {
8181+ static RE_HANDLE: OnceLock<Regex> = OnceLock::new();
8282+ static RE_DID: OnceLock<Regex> = OnceLock::new();
8383+ if maybe_identifier.len() > 253 {
8484+ None
8585+ } else if !RE_DID.get_or_init(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap())
8686+ .is_match(&maybe_identifier) && !RE_HANDLE
8787+ .get_or_init(|| Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap())
8888+ .is_match(&maybe_identifier)
8989+ {
9090+ None
9191+ } else {
9292+ Some(maybe_identifier)
9393+ }
9494+}
9595+9696+pub fn match_nsid(maybe_nsid: &str) -> Option<&str> {
9797+ static RE_NSID: OnceLock<Regex> = OnceLock::new();
9898+ if maybe_nsid.len() > 317 {
9999+ None
100100+ } else if !RE_NSID
101101+ .get_or_init(|| Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62}[a-zA-Z0-9])?)$").unwrap())
102102+ .is_match(&maybe_nsid)
103103+ {
104104+ None
105105+ } else {
106106+ Some(maybe_nsid)
107107+ }
108108+}
109109+110110+/// Convert an ATURI to a HTTP URL
111111+/// Currently has some failure modes and should restrict the NSIDs to a known subset
112112+pub fn aturi_to_http<'s>(aturi: &'s str, appview: &'s str) -> Option<markdown_weaver::CowStr<'s>> {
113113+ use markdown_weaver::CowStr;
114114+115115+ if aturi.starts_with("at://") {
116116+ let rest = aturi.strip_prefix("at:://").unwrap();
117117+ let mut split = rest.splitn(2, '/');
118118+ let maybe_identifier = split.next()?;
119119+ let maybe_nsid = split.next()?;
120120+ let maybe_rkey = split.next()?;
121121+122122+ // https://atproto.com/specs/handle#handle-identifier-syntax
123123+ let identifier = match_identifier(maybe_identifier)?;
124124+125125+ let nsid = if let Some(nsid) = match_nsid(maybe_nsid) {
126126+ // Last part of the nsid is generally the middle component of the URL
127127+ // TODO: check for bsky ones specifically, because those are the ones where this is valid
128128+ nsid.rsplitn(1, '.').next()?
129129+ } else {
130130+ return None;
131131+ };
132132+ Some(CowStr::Boxed(
133133+ format!(
134134+ "https://{}/profile/{}/{}/{}",
135135+ appview, identifier, nsid, maybe_rkey
136136+ )
137137+ .into_boxed_str(),
138138+ ))
139139+ } else {
140140+ Some(CowStr::Borrowed(aturi))
141141+ }
142142+}
···11+//! Atproto renderer
22+//!
33+//! This mode of the renderer renders either an entire notebook or entries in it to files suitable for inclusion
44+//! in a single-page app and uploads them to your Atproto PDS
55+//! It can be accessed via the appview at {your-handle}.weaver.sh/{notebook-name}.
66+//!
77+//! It can also be edited there.
88+//!
99+//! Link altering logic:
1010+//! - Option 1: leave (non-embed) links the same as in the markdown, have the CSM deal with them via some means
1111+//! such as adding "data-did" and "data-cid" attributes to the `<a/>` tag containing the DID and CID
1212+//! Pushes toward having the SPA/Appview do a bit more, but makes this step MUCH simpler
1313+//! - In this scenario, the rendering step can happen upon access (and then be cached) in the appview
1414+//! - More flexible in some ways, less in others
1515+//! - Option 2: alter links to point to other rendered blobs. Requires a certain amount of work to handle
1616+//! scenarios with a complex mesh of internal links, as the CID is altered by the editing of the link.
1717+//! Such cycles are handled in the simplest way, by rendering an absolute url which will make a call to the appview.
1818+//!
···112112 },
113113 "maxLength": 10,
114114 "description": "An array of tags associated with the notebook entry. Tags can help categorize and organize entries."
115115+ },
116116+ "contentFormat": {
117117+ "type": "object",
118118+ "description": "The format of the content. This is used to determine how to render the content.",
119119+ "properties": {
120120+ "markdown": {
121121+ "type": "string",
122122+ "description": "The format of the content. This is used to determine how to render the content.",
123123+ "enum": ["commonmark", "gfm", "obsidian", "weaver"],
124124+ "default": "weaver"
125125+ }
126126+ }
115127 }
116128 }
117129}
+36
lexicons/sh/weaver/notebook/page.json
···11+{
22+ "lexicon": 1,
33+ "id": "sh.weaver.notebook.chapter",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "description": "A grouping of entries in a notebook, intended to be displayed as a single page.",
88+ "key": "tid",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["notebook", "authors", "entryList"],
1212+ "properties": {
1313+ "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" },
1414+ "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" },
1515+ "notebook": {
1616+ "type": "ref",
1717+ "ref": "com.atproto.repo.strongRef",
1818+ "description": "The notebook this page belongs to."
1919+ },
2020+ "entryList": {
2121+ "type": "array",
2222+ "items": {
2323+ "type": "ref",
2424+ "ref": "com.atproto.repo.strongRef"
2525+ }
2626+ },
2727+ "createdAt": {
2828+ "type": "string",
2929+ "format": "datetime",
3030+ "description": "Client-declared timestamp when this was originally created."
3131+ }
3232+ }
3333+ }
3434+ }
3535+ }
3636+}