💻 My corner on the internet kacaii.dev/
lustre evergarden wisp-place gleam atproto
0

Configure Feed

Select the types of activity you want to include in your feed.

blog: fetch bluesky profile on init

author
kacaii.dev
date (Jul 21, 2026, 3:09 PM -0300) commit dee9b454 parent c4af7105 change-id lwmqlpqy
+142 -116
+65 -31
src/blog.gleam
··· 8 8 import blog/route 9 9 import gleam/bool 10 10 import gleam/list 11 + import gleam/option 11 12 import gleam/string 12 13 import gleam/uri 13 14 import lustre ··· 62 63 NavbarTitleUpdated(new: String) 63 64 DocumentTitleUpdated(new: String) 64 65 65 - AtProtoMessage(atproto.Message) 66 + AtProtoMessage(message: atproto.Message) 66 67 } 67 68 68 69 fn update_navbar_title(to new: String) -> effect.Effect(Message) { ··· 100 101 atproto.fetch_standard_documents(atproto) 101 102 |> effect.map(AtProtoMessage) 102 103 104 + let fetch_bluesky_profile = 105 + atproto.fetch_bluesky_profile(atproto) 106 + |> effect.map(AtProtoMessage) 107 + 103 108 let new_document_title = 104 109 { "/" <> route.to_string(route) } 105 110 |> update_document_title 106 111 107 112 let effect = 108 - effect.batch([page_effect, init_modem, fetch_documents, new_document_title]) 113 + effect.batch([ 114 + page_effect, 115 + init_modem, 116 + fetch_documents, 117 + new_document_title, 118 + fetch_bluesky_profile, 119 + ]) 109 120 110 121 #(Model(title: "kacaii.dev", route:, page:, atproto:), effect) 111 122 } ··· 122 133 } 123 134 124 135 route.Home -> { 125 - let #(model, effect) = home.init(atproto) 136 + let #(model, effect) = home.init() 126 137 let page_effect = effect.map(effect, HomeMessage) 127 - let title_effect = case model { 128 - home.Pending -> update_navbar_title("xh --body $PDS") 129 - home.Model(..) -> effect.none() 138 + 139 + let title_effect = case atproto.bluesky_profile { 140 + option.None -> update_navbar_title("xh --body $PDS") 141 + option.Some(_) -> effect.none() 130 142 } 131 143 132 144 let effect = effect.batch([page_effect, title_effect]) ··· 163 175 model, NavbarTitleUpdated(title) -> #(Model(..model, title:), effect.none()) 164 176 model, DocumentTitleUpdated(_) -> continue(model) 165 177 166 - Model(page: Home(page), ..), HomeMessage(message) -> { 167 - let #(page, effect) = home.update(page, message) 168 - 169 - let page = Home(page) 170 - let effect = 171 - effect.batch([ 172 - effect.map(effect, HomeMessage), 173 - update_navbar_title("cd /home/kacaii"), 174 - ]) 175 - 176 - #(Model(..model, page:), effect) 177 - } 178 + Model(page: Home(_), ..), HomeMessage(_) -> continue(model) 179 + Model(page: About(_), ..), AboutMessage(_) -> continue(model) 180 + Model(page: Writing(_), ..), WritingMessage(_) -> continue(model) 178 181 179 182 _model, _message -> continue(model) 180 183 } ··· 182 185 183 186 fn handle_atproto_message(model: Model, message: atproto.Message) { 184 187 case model, message { 185 - model, atproto.PdsReturnedDocuments(returned: Ok(standard_documents)) -> { 188 + model, 189 + atproto.PdsReturnedStandardDocuments(returned: Ok(standard_documents)) 190 + -> { 191 + let effect = 192 + update_navbar_title("cd " <> route.to_working_directory(model.route)) 193 + 186 194 let atproto = atproto.AtProto(..model.atproto, standard_documents:) 187 - #(Model(..model, atproto:), effect.none()) 195 + #(Model(..model, atproto:), effect) 188 196 } 189 197 190 - model, atproto.PdsReturnedDocuments(returned: Error(_)) -> { 198 + model, atproto.PdsReturnedStandardDocuments(returned: Error(_)) -> { 199 + let effect = 200 + update_navbar_title("cd " <> route.to_working_directory(model.route)) 201 + 191 202 let atproto = atproto.AtProto(..model.atproto, standard_documents: []) 192 - #(Model(..model, atproto:), effect.none()) 203 + #(Model(..model, atproto:), effect) 204 + } 205 + 206 + model, atproto.PdsReturnedBlueSkyProfile(Ok(profile)) -> { 207 + let atproto = 208 + atproto.AtProto(..model.atproto, bluesky_profile: option.Some(profile)) 209 + 210 + let effect = 211 + update_navbar_title("cd " <> route.to_working_directory(model.route)) 212 + 213 + #(Model(..model, atproto:), effect) 214 + } 215 + 216 + model, atproto.PdsReturnedBlueSkyProfile(Error(..)) -> { 217 + let profile = 218 + atproto.BlueskyProfile( 219 + display_name: "Oops!", 220 + pronouns: "im/sorry", 221 + description: " 222 + Aparently my profile failed to load, this was suposed to show my 223 + bluesky photo and description. But its alright, you can still use 224 + the website normally.", 225 + avatar_cid: "", 226 + ) 227 + 228 + let effect = 229 + update_navbar_title("cd " <> route.to_working_directory(model.route)) 230 + 231 + let atproto = 232 + atproto.AtProto(..model.atproto, bluesky_profile: option.Some(profile)) 233 + #(Model(..model, atproto:), effect) 193 234 } 194 235 } 195 236 } ··· 201 242 use <- bool.guard(model.route == route, continue(model)) 202 243 let #(page, effect) = init_page(route, model.atproto) 203 244 204 - let new_navbar_title = case route { 205 - route.Home -> "/home/kacaii" 206 - other -> "~/" <> route.to_string(other) 207 - } 208 - 209 - let new_page_title = "/" <> route.to_string(route) 210 - 211 245 let effect = 212 246 effect.batch([ 213 247 effect, 214 - update_navbar_title("cd " <> new_navbar_title), 215 - update_document_title(new_page_title), 248 + update_navbar_title("cd " <> route.to_working_directory(route)), 249 + update_document_title("/" <> route.to_string(route)), 216 250 ]) 217 251 218 252 #(Model(..model, route:, page:), effect)
+51 -4
src/blog/atproto.gleam
··· 10 10 const pds = "amanita.us-east.host.bsky.network" 11 11 12 12 pub type Message { 13 - PdsReturnedDocuments(returned: Result(List(Document), rsvp.Error(String))) 13 + PdsReturnedStandardDocuments( 14 + returned: Result(List(Document), rsvp.Error(String)), 15 + ) 16 + 17 + PdsReturnedBlueSkyProfile( 18 + returned: Result(BlueSkyProfile, rsvp.Error(String)), 19 + ) 14 20 } 15 21 16 22 pub type AtProto { ··· 18 24 did: did.Did, 19 25 pds: String, 20 26 standard_documents_nsdi: nsid.Nsid, 27 + standard_documents: List(Document), 21 28 bluesky_profile_nsid: nsid.Nsid, 22 - standard_documents: List(Document), 29 + bluesky_profile: option.Option(BlueSkyProfile), 23 30 ) 24 31 } 25 32 ··· 32 39 did:, 33 40 pds:, 34 41 bluesky_profile_nsid:, 42 + standard_documents: [], 35 43 standard_documents_nsdi:, 36 - standard_documents: [], 44 + bluesky_profile: option.None, 37 45 ) 38 46 } 39 47 ··· 91 99 reverse: option.None, 92 100 ) 93 101 94 - let handler = rsvp.expect_json(decoder, PdsReturnedDocuments) 102 + let handler = rsvp.expect_json(decoder, PdsReturnedStandardDocuments) 95 103 rsvp.send(request, handler) 96 104 } 105 + 106 + pub type BlueSkyProfile { 107 + BlueskyProfile( 108 + display_name: String, 109 + pronouns: String, 110 + description: String, 111 + avatar_cid: String, 112 + ) 113 + } 114 + 115 + pub fn fetch_bluesky_profile(atproto: AtProto) -> effect.Effect(Message) { 116 + let decoder = { 117 + use display_name <- decode.field("displayName", decode.string) 118 + use description <- decode.field("description", decode.string) 119 + use pronouns <- decode.field("pronouns", decode.string) 120 + use avatar_cid <- decode.subfield(["avatar", "ref", "$link"], decode.string) 121 + 122 + decode.success(BlueskyProfile( 123 + display_name:, 124 + pronouns:, 125 + description:, 126 + avatar_cid:, 127 + )) 128 + } 129 + 130 + let request = 131 + possum.get_record( 132 + atproto.did, 133 + pds: "slingshot.microcosm.blue", 134 + collection: atproto.bluesky_profile_nsid, 135 + rkey: "self", 136 + cid: option.None, 137 + ) 138 + 139 + rsvp.send( 140 + request, 141 + rsvp.expect_json(decode.at(["value"], decoder), PdsReturnedBlueSkyProfile), 142 + ) 143 + }
+19 -81
src/blog/page/home.gleam
··· 1 1 import blog/atproto 2 2 import blog/icon 3 - import gleam/dynamic/decode 4 3 import gleam/http/request 5 4 import gleam/list 6 5 import gleam/option ··· 13 12 import lustre/element/keyed 14 13 import possum 15 14 import possum/did 16 - import rsvp 17 15 18 16 pub type Message { 19 - ApiReturnedProfile(Result(Profile, rsvp.Error(String))) 17 + Message 20 18 } 21 19 22 20 pub type Model { 23 - Pending 24 - Model(profile: Profile) 25 - } 26 - 27 - pub type Profile { 28 - Profile( 29 - display_name: String, 30 - pronouns: String, 31 - description: String, 32 - avatar_cid: String, 33 - ) 34 - } 35 - 36 - fn fetch_bluesky_profile(atproto: atproto.AtProto) -> effect.Effect(Message) { 37 - let decoder = { 38 - use display_name <- decode.field("displayName", decode.string) 39 - use description <- decode.field("description", decode.string) 40 - use pronouns <- decode.field("pronouns", decode.string) 41 - use avatar_cid <- decode.subfield(["avatar", "ref", "$link"], decode.string) 42 - decode.success(Profile(display_name:, pronouns:, description:, avatar_cid:)) 43 - } 44 - 45 - let request = 46 - possum.get_record( 47 - atproto.did, 48 - pds: "slingshot.microcosm.blue", 49 - collection: atproto.bluesky_profile_nsid, 50 - rkey: "self", 51 - cid: option.None, 52 - ) 53 - 54 - rsvp.send( 55 - request, 56 - rsvp.expect_json(decode.at(["value"], decoder), ApiReturnedProfile), 57 - ) 21 + Model 58 22 } 59 23 60 - pub fn init(atproto: atproto.AtProto) -> #(Model, effect.Effect(Message)) { 61 - #(Pending, fetch_bluesky_profile(atproto)) 24 + pub fn init() -> #(Model, effect.Effect(Message)) { 25 + #(Model, effect.none()) 62 26 } 63 27 64 28 pub fn update( 65 - model: Model, 66 - message: Message, 29 + _model: Model, 30 + _message: Message, 67 31 ) -> #(Model, effect.Effect(Message)) { 68 - case model, message { 69 - Pending, ApiReturnedProfile(Ok(profile)) -> #( 70 - Model(profile:), 71 - effect.none(), 72 - ) 73 - 74 - Pending, ApiReturnedProfile(Error(..)) -> { 75 - let profile = 76 - Profile( 77 - display_name: "Oops!", 78 - pronouns: "im/sorry", 79 - description: " 80 - Aparently my profile failed to load, this was suposed to show my 81 - bluesky photo and description. But its alright, you can still use 82 - the website normally.", 83 - avatar_cid: "", 84 - ) 85 - 86 - let effect = effect.none() 87 - #(Model(profile:), effect) 88 - } 89 - 90 - _, _ -> #(model, effect.none()) 91 - } 32 + #(Model, effect.none()) 92 33 } 93 34 94 35 pub fn view( 95 - model: Model, 36 + _model: Model, 96 37 atproto: atproto.AtProto, 97 38 ) -> element.Element(Message) { 98 39 let rss_href = attr.href("https://blog.kacaii.dev/rss") ··· 101 42 102 43 let html = [ 103 44 html.h1([], [html.text("home")]), 104 - view_profile_card(model, atproto), 45 + view_profile_card(atproto), 105 46 view_introduction(rss_href, bluesky_href), 106 47 view_showcases(), 107 48 ] ··· 109 50 element.fragment(html) 110 51 } 111 52 112 - fn view_profile_card( 113 - model: Model, 114 - atproto: atproto.AtProto, 115 - ) -> element.Element(Message) { 116 - let image = case model { 117 - Pending -> 53 + fn view_profile_card(atproto: atproto.AtProto) -> element.Element(Message) { 54 + let image = case atproto.bluesky_profile { 55 + option.None -> 118 56 html.img([ 119 57 attr.src("/favicon.webp"), 120 58 attr.class("border-muted size-20 rounded-sm border"), 121 59 attr.class("aspect-square animate-pulse opacity-50"), 122 60 ]) 123 61 124 - Model(profile:) -> 62 + option.Some(profile) -> 125 63 html.img([ 126 64 attr.class("aspect-square size-20 rounded-sm"), 127 65 attr.class("hover:brightness-110"), ··· 152 90 image, 153 91 html.div([attr.class("w-full grid-rows-2 space-y-1")], [ 154 92 // Username and pronouns 155 - case model { 156 - Pending -> { 93 + case atproto.bluesky_profile { 94 + option.None -> { 157 95 html.div([attr.class("mb-2 flex gap-2")], [ 158 96 skeleton([attr.class("h-4 w-11")]), 159 97 skeleton([attr.class("h-4 w-18")]), 160 98 ]) 161 99 } 162 100 163 - Model(profile:) -> { 101 + option.Some(profile) -> { 164 102 let wrapper_style = attr.class("hidden gap-2 sm:inline-flex") 165 103 let username_style = attr.class("font-bold") 166 104 ··· 172 110 }, 173 111 174 112 // Description 175 - case model { 176 - Pending -> 113 + case atproto.bluesky_profile { 114 + option.None -> 177 115 html.div([attr.class("flex flex-col space-y-2")], [ 178 116 skeleton([attr.class("h-4")]), 179 117 skeleton([attr.class("h-4 w-49")]), 180 118 ]) 181 119 182 - Model(profile:) -> 120 + option.Some(profile) -> 183 121 html.p([attr.class("text-sm sm:text-base")], [ 184 122 html.text(profile.description), 185 123 ])
+7
src/blog/route.gleam
··· 36 36 } 37 37 } 38 38 39 + pub fn to_working_directory(self: Route) -> String { 40 + case self { 41 + Home -> "/home/kacaii" 42 + other -> "~/" <> to_string(other) 43 + } 44 + } 45 + 39 46 pub fn href(self: Route) -> attr.Attribute(_) { 40 47 path(self) 41 48 |> attr.href