learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs

feat: update lexicon fields

* address linting fixes

+147 -36
+4
crates/core/src/model.rs
··· 11 11 pub published_at: Option<String>, 12 12 #[serde(default)] 13 13 pub links: Vec<String>, 14 + pub language: Option<String>, 14 15 } 15 16 16 17 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] ··· 33 34 pub card_type: CardType, 34 35 #[serde(default)] 35 36 pub hints: Vec<String>, 37 + pub visibility: Option<Visibility>, 38 + pub language: Option<String>, 36 39 } 37 40 38 41 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] ··· 54 57 pub visibility: Visibility, 55 58 pub published_at: Option<String>, 56 59 pub fork_of: Option<String>, 60 + pub language: Option<String>, 57 61 } 58 62 59 63 #[derive(Debug, Clone, Serialize, Deserialize)]
+4
crates/server/src/api/card.rs
··· 165 165 media_url: None, 166 166 card_type: CardType::default(), 167 167 hints: vec![], 168 + visibility: None, 169 + language: None, 168 170 }, 169 171 Card { 170 172 id: "card-2".to_string(), ··· 175 177 media_url: None, 176 178 card_type: CardType::default(), 177 179 hints: vec![], 180 + visibility: None, 181 + language: None, 178 182 }, 179 183 ]; 180 184
+3
crates/server/src/api/note.rs
··· 206 206 visibility: Visibility::Public, 207 207 published_at: None, 208 208 links: vec![], 209 + language: None, 209 210 }, 210 211 Note { 211 212 id: "note-2".to_string(), ··· 216 217 visibility: Visibility::Private, 217 218 published_at: None, 218 219 links: vec![], 220 + language: None, 219 221 }, 220 222 ]; 221 223 ··· 249 251 visibility: Visibility::Private, 250 252 published_at: None, 251 253 links: vec![], 254 + language: None, 252 255 }]; 253 256 254 257 let note_repo =
+5 -1
crates/server/src/pds/records.rs
··· 187 187 visibility: Visibility::Public, 188 188 published_at: None, 189 189 fork_of: None, 190 + language: None, 190 191 } 191 192 } 192 193 ··· 200 201 media_url: None, 201 202 card_type: malfestio_core::model::CardType::default(), 202 203 hints: vec![], 204 + language: None, 205 + visibility: Some(Visibility::Public), 203 206 } 204 207 } 205 208 ··· 210 213 title: "Test Note".to_string(), 211 214 body: "This is a test note with **markdown**.".to_string(), 212 215 tags: vec!["notes".to_string()], 213 - visibility: Visibility::Public, 214 216 published_at: None, 215 217 links: vec![], 218 + language: None, 219 + visibility: Visibility::Public, 216 220 } 217 221 } 218 222
+7 -1
crates/server/src/repository/card.rs
··· 1 1 use async_trait::async_trait; 2 - use malfestio_core::model::{Card, CardType}; 2 + use malfestio_core::model::{Card, CardType, Visibility}; 3 3 4 4 #[derive(Debug)] 5 5 pub enum CardRepoError { ··· 95 95 media_url: params.media_url, 96 96 card_type: params.card_type, 97 97 hints: params.hints, 98 + visibility: Some(Visibility::Public), 99 + language: None, 98 100 }) 99 101 } 100 102 ··· 148 150 media_url: row.get("media_url"), 149 151 card_type, 150 152 hints: row.get("hints"), 153 + visibility: Some(Visibility::Public), 154 + language: None, 151 155 }); 152 156 } 153 157 ··· 253 257 media_url: params.media_url, 254 258 card_type: params.card_type, 255 259 hints: params.hints, 260 + visibility: Some(Visibility::Public), 261 + language: None, 256 262 }; 257 263 258 264 self.cards.lock().unwrap().push(card.clone());
+10
crates/server/src/repository/deck.rs
··· 86 86 visibility: params.visibility, 87 87 published_at: None, 88 88 fork_of: None, 89 + language: None, 89 90 }) 90 91 } 91 92 ··· 126 127 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 127 128 .map(|dt| dt.to_rfc3339()), 128 129 fork_of: fork_of.map(|u| u.to_string()), 130 + language: None, 129 131 }) 130 132 } 131 133 ··· 175 177 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 176 178 .map(|dt| dt.to_rfc3339()), 177 179 fork_of: fork_of.map(|u| u.to_string()), 180 + language: None, 178 181 }); 179 182 } 180 183 Ok(decks) ··· 312 315 visibility, 313 316 published_at: None, 314 317 fork_of: Some(original_deck_id.to_string()), 318 + language: None, 315 319 }) 316 320 } 317 321 ··· 349 353 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 350 354 .map(|dt| dt.to_rfc3339()), 351 355 fork_of: fork_of.map(|u| u.to_string()), 356 + language: None, 352 357 }); 353 358 } 354 359 Ok(decks) ··· 379 384 visibility: Visibility::Public, 380 385 published_at: None, 381 386 fork_of: None, 387 + language: None, 382 388 }; 383 389 384 390 let card_rows = client ··· 402 408 hints, 403 409 // TODO: support other card types 404 410 card_type: malfestio_core::model::CardType::Basic, 411 + visibility: Some(Visibility::Public), 412 + language: None, 405 413 }); 406 414 } 407 415 ··· 443 451 visibility: params.visibility, 444 452 published_at: None, 445 453 fork_of: None, 454 + language: None, 446 455 }; 447 456 self.decks.lock().unwrap().push(deck.clone()); 448 457 Ok(deck) ··· 495 504 visibility: Visibility::Private, 496 505 published_at: None, 497 506 fork_of: Some(original_deck_id.to_string()), 507 + language: None, 498 508 }; 499 509 decks.push(deck.clone()); 500 510 decks.push(deck.clone());
+5
crates/server/src/repository/note.rs
··· 62 62 visibility, 63 63 published_at: None, 64 64 links: Vec::new(), 65 + language: None, 65 66 }) 66 67 } 67 68 ··· 115 116 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 116 117 .map(|dt| dt.to_rfc3339()), 117 118 links, 119 + language: None, 118 120 }); 119 121 } 120 122 ··· 174 176 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 175 177 .map(|dt| dt.to_rfc3339()), 176 178 links, 179 + language: None, 177 180 }) 178 181 } 179 182 ··· 213 216 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 214 217 .map(|dt| dt.to_rfc3339()), 215 218 links, 219 + language: None, 216 220 }); 217 221 } 218 222 Ok(notes) ··· 268 272 visibility, 269 273 published_at: None, 270 274 links: Vec::new(), 275 + language: None, 271 276 }; 272 277 273 278 self.notes.lock().unwrap().push(note.clone());
+1
crates/server/src/repository/social.rs
··· 66 66 .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 67 67 .map(|dt| dt.to_rfc3339()), 68 68 fork_of: fork_of.map(|u| u.to_string()), 69 + language: None, 69 70 }); 70 71 } 71 72 decks
+2 -1
lexicons/README.md
··· 86 86 - Add new optional fields 87 87 - Update descriptions 88 88 - Add new `knownValues` (don't remove old ones) 89 - - Increment patch version in documentation 89 + - Document version with timestamp (use `date +%s` to get Unix timestamp) 90 + - Note in changelog: `// Updated: 1735862400 (2026-01-02)` 90 91 91 92 2. **Breaking Changes** (avoid if possible): 92 93 - Create new lexicon with new NSID (e.g., `org.stormlightlabs.malfestio.cardV2`)
+20 -16
lexicons/org/stormlightlabs/malfestio/card.json
··· 15 15 "format": "at-uri", 16 16 "description": "Reference to the deck this card belongs to." 17 17 }, 18 - "front": { 19 - "type": "string", 20 - "format": "markdown", 21 - "maxLength": 10000, 22 - "description": "Content on the front of the card." 23 - }, 24 - "back": { 25 - "type": "string", 26 - "format": "markdown", 27 - "maxLength": 10000, 28 - "description": "Content on the back of the card." 29 - }, 18 + "front": { "type": "string", "maxLength": 10000, "description": "Content on the front of the card." }, 19 + "back": { "type": "string", "maxLength": 10000, "description": "Content on the back of the card." }, 30 20 "cardType": { 31 21 "type": "string", 22 + "maxLength": 100, 32 23 "knownValues": ["basic", "cloze"], 33 24 "default": "basic", 34 25 "description": "Type of the card (e.g., basic or cloze deletion)." 35 26 }, 36 27 "hints": { 37 28 "type": "array", 38 - "items": { "type": "string" }, 29 + "items": { "type": "string", "maxLength": 1000 }, 39 30 "description": "Optional hints to display before revealing the answer." 40 31 }, 41 32 "media": { ··· 45 36 "required": ["uri", "kind"], 46 37 "properties": { 47 38 "uri": { "type": "string", "format": "uri" }, 48 - "kind": { "type": "string", "knownValues": ["image", "audio"] }, 49 - "alt": { "type": "string" } 39 + "kind": { "type": "string", "maxLength": 100, "knownValues": ["image", "audio"] }, 40 + "alt": { "type": "string", "maxLength": 1000 } 50 41 } 51 42 }, 52 43 "description": "Multimedia attachments for the card." 53 44 }, 54 - "createdAt": { "type": "string", "format": "datetime" } 45 + "visibility": { 46 + "type": "string", 47 + "maxLength": 100, 48 + "knownValues": ["private", "unlisted", "public"], 49 + "default": "public", 50 + "description": "Visibility setting for the card." 51 + }, 52 + "language": { 53 + "type": "string", 54 + "maxLength": 20, 55 + "description": "Language code for the card content (e.g., 'en', 'es', 'fr')." 56 + }, 57 + "createdAt": { "type": "string", "format": "datetime" }, 58 + "updatedAt": { "type": "string", "format": "datetime", "description": "Timestamp of last update." } 55 59 } 56 60 } 57 61 }
+17 -4
lexicons/org/stormlightlabs/malfestio/collection.json
··· 18 18 "type": "object", 19 19 "required": ["type", "ref"], 20 20 "properties": { 21 - "type": { "type": "string", "knownValues": ["deck", "note", "article", "lecture"] }, 21 + "type": { "type": "string", "maxLength": 100, "knownValues": ["deck", "note", "article", "lecture"] }, 22 22 "ref": { "type": "string", "format": "at-uri" }, 23 - "note": { "type": "string", "description": "Curator's note about this item." } 23 + "note": { "type": "string", "maxLength": 1000, "description": "Curator's note about this item." } 24 24 } 25 25 }, 26 26 "description": "Ordered items in the collection." 27 27 }, 28 - "tags": { "type": "array", "items": { "type": "string" }, "maxLength": 64 }, 29 - "createdAt": { "type": "string", "format": "datetime" } 28 + "tags": { "type": "array", "items": { "type": "string", "maxLength": 100 }, "maxLength": 64 }, 29 + "visibility": { 30 + "type": "string", 31 + "maxLength": 100, 32 + "knownValues": ["private", "unlisted", "public"], 33 + "default": "public", 34 + "description": "Visibility setting for the collection." 35 + }, 36 + "language": { 37 + "type": "string", 38 + "maxLength": 20, 39 + "description": "Language code for the collection content (e.g., 'en', 'es', 'fr')." 40 + }, 41 + "createdAt": { "type": "string", "format": "datetime" }, 42 + "updatedAt": { "type": "string", "format": "datetime", "description": "Timestamp of last update." } 30 43 } 31 44 } 32 45 }
+16 -3
lexicons/org/stormlightlabs/malfestio/deck.json
··· 12 12 "properties": { 13 13 "title": { "type": "string", "maxLength": 300, "description": "Title of the deck." }, 14 14 "description": { "type": "string", "maxLength": 3000, "description": "Description of the deck context." }, 15 - "tags": { "type": "array", "items": { "type": "string" }, "maxLength": 64 }, 15 + "tags": { "type": "array", "items": { "type": "string", "maxLength": 100 }, "maxLength": 64 }, 16 16 "cardRefs": { 17 17 "type": "array", 18 18 "items": { "type": "string", "format": "at-uri" }, ··· 23 23 "items": { "type": "string", "format": "at-uri" }, 24 24 "description": "References to source materials (articles, lectures) used in this deck." 25 25 }, 26 - "license": { "type": "string", "description": "License for the deck content." }, 27 - "createdAt": { "type": "string", "format": "datetime" } 26 + "license": { "type": "string", "maxLength": 500, "description": "License for the deck content." }, 27 + "visibility": { 28 + "type": "string", 29 + "maxLength": 100, 30 + "knownValues": ["private", "unlisted", "public"], 31 + "default": "public", 32 + "description": "Visibility setting for the deck." 33 + }, 34 + "language": { 35 + "type": "string", 36 + "maxLength": 20, 37 + "description": "Language code for the deck content (e.g., 'en', 'es', 'fr')." 38 + }, 39 + "createdAt": { "type": "string", "format": "datetime" }, 40 + "updatedAt": { "type": "string", "format": "datetime", "description": "Timestamp of last update." } 28 41 } 29 42 } 30 43 }
+9 -4
lexicons/org/stormlightlabs/malfestio/note.json
··· 13 13 "title": { "type": "string", "maxLength": 300, "description": "Title of the note." }, 14 14 "body": { 15 15 "type": "string", 16 - "format": "markdown", 17 16 "maxLength": 100000, 18 17 "description": "The body content of the note in Markdown format." 19 18 }, 20 19 "tags": { 21 20 "type": "array", 22 - "items": { "type": "string" }, 21 + "items": { "type": "string", "maxLength": 100 }, 23 22 "maxLength": 64, 24 23 "description": "Tags associated with the note." 25 24 }, ··· 30 29 "required": ["uri"], 31 30 "properties": { 32 31 "uri": { "type": "string", "format": "uri" }, 33 - "title": { "type": "string" }, 34 - "type": { "type": "string", "description": "Type hint for the linked resource." } 32 + "title": { "type": "string", "maxLength": 500 }, 33 + "type": { "type": "string", "maxLength": 100, "description": "Type hint for the linked resource." } 35 34 } 36 35 }, 37 36 "description": "External or internal links referenced in the note." ··· 40 39 "updatedAt": { "type": "string", "format": "datetime", "description": "Timestamp of last update." }, 41 40 "visibility": { 42 41 "type": "string", 42 + "maxLength": 100, 43 43 "knownValues": ["private", "unlisted", "public"], 44 44 "default": "private", 45 45 "description": "Visibility setting for the note." 46 + }, 47 + "language": { 48 + "type": "string", 49 + "maxLength": 20, 50 + "description": "Language code for the note content (e.g., 'en', 'es', 'fr')." 46 51 } 47 52 } 48 53 }
+7 -2
lexicons/org/stormlightlabs/malfestio/source/article.json
··· 11 11 "required": ["url", "title", "createdAt"], 12 12 "properties": { 13 13 "url": { "type": "string", "format": "uri", "description": "URL of the article." }, 14 - "title": { "type": "string", "description": "Title of the article." }, 15 - "author": { "type": "string", "description": "Author of the article." }, 14 + "title": { "type": "string", "maxLength": 500, "description": "Title of the article." }, 15 + "author": { "type": "string", "maxLength": 300, "description": "Author of the article." }, 16 16 "publishedAt": { 17 17 "type": "string", 18 18 "format": "datetime", ··· 34 34 } 35 35 }, 36 36 "description": "User highlights from the article." 37 + }, 38 + "language": { 39 + "type": "string", 40 + "maxLength": 20, 41 + "description": "Language code for the article content (e.g., 'en', 'es', 'fr')." 37 42 }, 38 43 "createdAt": { "type": "string", "format": "datetime" } 39 44 }
+8 -3
lexicons/org/stormlightlabs/malfestio/source/lecture.json
··· 11 11 "required": ["url", "title", "createdAt"], 12 12 "properties": { 13 13 "url": { "type": "string", "format": "uri", "description": "URL of the lecture (e.g., YouTube link)." }, 14 - "title": { "type": "string", "description": "Title of the lecture." }, 15 - "creator": { "type": "string", "description": "Creator or channel name." }, 14 + "title": { "type": "string", "maxLength": 500, "description": "Title of the lecture." }, 15 + "creator": { "type": "string", "maxLength": 300, "description": "Creator or channel name." }, 16 16 "timestamps": { 17 17 "type": "array", 18 18 "items": { ··· 20 20 "required": ["t", "label"], 21 21 "properties": { 22 22 "t": { "type": "integer", "description": "Time in seconds." }, 23 - "label": { "type": "string", "description": "Description of the timestamp." }, 23 + "label": { "type": "string", "maxLength": 500, "description": "Description of the timestamp." }, 24 24 "noteRef": { 25 25 "type": "string", 26 26 "format": "at-uri", ··· 29 29 } 30 30 }, 31 31 "description": "Important timestamps or chapters in the lecture." 32 + }, 33 + "language": { 34 + "type": "string", 35 + "maxLength": 20, 36 + "description": "Language code for the lecture content (e.g., 'en', 'es', 'fr')." 32 37 }, 33 38 "createdAt": { "type": "string", "format": "datetime" } 34 39 }
+1 -1
lexicons/org/stormlightlabs/malfestio/thread/comment.json
··· 12 12 "properties": { 13 13 "subjectRef": { "type": "string", "format": "at-uri", "description": "The root subject being commented on." }, 14 14 "replyTo": { "type": "string", "format": "at-uri", "description": "The parent comment if this is a reply." }, 15 - "body": { "type": "string", "format": "markdown", "maxLength": 5000, "description": "The comment text." }, 15 + "body": { "type": "string", "maxLength": 5000, "description": "The comment text." }, 16 16 "createdAt": { "type": "string", "format": "datetime" } 17 17 } 18 18 }
+28
migrations/013_2026_01_02_add_lexicon_fields.sql
··· 1 + -- Migration: Add language, visibility, updatedAt for lexicon consistency 2 + -- Date: 2026-01-02 3 + -- Description: Adds optional fields to match lexicon schema updates 4 + 5 + -- Local tables: Add language to all content types 6 + ALTER TABLE decks ADD COLUMN IF NOT EXISTS language TEXT; 7 + ALTER TABLE cards ADD COLUMN IF NOT EXISTS language TEXT; 8 + ALTER TABLE notes ADD COLUMN IF NOT EXISTS language TEXT; 9 + 10 + -- Local tables: Add visibility to cards (decks and notes already have it) 11 + ALTER TABLE cards ADD COLUMN IF NOT EXISTS visibility JSONB; 12 + 13 + -- Indexed tables: Add visibility, updatedAt, language for remote records 14 + ALTER TABLE indexed_decks ADD COLUMN IF NOT EXISTS visibility TEXT; 15 + ALTER TABLE indexed_decks ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ; 16 + ALTER TABLE indexed_decks ADD COLUMN IF NOT EXISTS language TEXT; 17 + 18 + ALTER TABLE indexed_cards ADD COLUMN IF NOT EXISTS visibility TEXT; 19 + ALTER TABLE indexed_cards ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ; 20 + ALTER TABLE indexed_cards ADD COLUMN IF NOT EXISTS language TEXT; 21 + 22 + ALTER TABLE indexed_notes ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ; 23 + ALTER TABLE indexed_notes ADD COLUMN IF NOT EXISTS language TEXT; 24 + 25 + -- Add indexes for common visibility queries 26 + CREATE INDEX IF NOT EXISTS idx_cards_visibility ON cards USING GIN(visibility); 27 + CREATE INDEX IF NOT EXISTS idx_indexed_decks_visibility ON indexed_decks(visibility); 28 + CREATE INDEX IF NOT EXISTS idx_indexed_cards_visibility ON indexed_cards(visibility);