···2222gleam run # Run the project
2323gleam test # Run the tests
2424```
2525+2626+And sometimes, maybe.
2727+```sh
2828+curl https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.7/purify.min.js -osrc/vendored-purify.min.js
2929+curl https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.7/purify.min.js.map -osrc/purify.min.js.map
3030+```
+8
examples/lustre_chilp_app/.gitignore
···11+*.beam
22+*.ez
33+/build
44+erl_crash.dump
55+66+#Added automatically by Lustre Dev Tools
77+/.lustre
88+/dist
+23
examples/lustre_chilp_app/gleam.toml
···11+name = "lustre_chilp_app"
22+version = "1.0.0"
33+44+# Fill out these fields if you intend to generate HTML documentation or publish
55+# your project to the Hex package manager.
66+#
77+# description = ""
88+# licences = ["Apache-2.0"]
99+# repository = { type = "github", user = "", repo = "" }
1010+# links = [{ title = "Website", href = "" }]
1111+#
1212+# For a full reference of all the available options, you can have a look at
1313+# https://gleam.run/writing-gleam/gleam-toml/.
1414+1515+[dependencies]
1616+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1717+chilp = {path = "../.."}
1818+lustre = ">= 5.5.2 and < 6.0.0"
1919+glentities = ">= 6.2.1 and < 7.0.0"
2020+2121+[dev-dependencies]
2222+gleeunit = ">= 1.0.0 and < 2.0.0"
2323+lustre_dev_tools = ">= 2.3.4 and < 3.0.0"
···11+// IMPORTS ---------------------------------------------------------------------
22+33+import chilp/widget
44+import lustre
55+import lustre/effect.{type Effect}
66+import lustre/element.{type Element}
77+88+// MAIN ------------------------------------------------------------------------
99+1010+pub fn main() {
1111+ // In this example, we're not making much sense. Just Chilp.
1212+ let app = lustre.application(init, update, view)
1313+ let assert Ok(_) = lustre.start(app, "#app", Nil)
1414+1515+ Nil
1616+}
1717+1818+// MODEL -----------------------------------------------------------------------
1919+2020+type Model {
2121+ Model(
2222+ string: String,
2323+ // .. and other things your application would need to know, for chilp we have:
2424+ chilp_model: widget.ChilpDataInYourModel(Msg),
2525+ )
2626+}
2727+2828+fn init(_) -> #(Model, Effect(Msg)) {
2929+ let model = Model(string: "Hi", chilp_model: widget.init(ChilpMessage))
3030+ // No effects, though you could force Chilp to pre-fetch a post with widget.force()!
3131+ let effect = effect.none()
3232+3333+ #(model, effect)
3434+}
3535+3636+// HELPERS----------------------------------------------------------------------
3737+@external(javascript, "./ffi_lustre_chilp_app.mjs", "lassign")
3838+fn js_browse(to: String) -> Nil {
3939+ Nil
4040+}
4141+4242+fn browse(to: String) {
4343+ js_browse(to)
4444+ effect.none()
4545+}
4646+4747+// UPDATE ----------------------------------------------------------------------
4848+4949+type Msg {
5050+ ChilpMessage(widget.ChilpMsg)
5151+}
5252+5353+// You can't usually make `ChilpMsg`s, with a few exceptions.
5454+//
5555+// One of them being `widget.trigger()`, which does what `widget.force()` does but instead of an effect it returns a `ChilpMsg`!
5656+fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
5757+ case msg {
5858+ // Normally, your own variants would be here too.
5959+ ChilpMessage(message) -> {
6060+ let #(chilp_model, effect) =
6161+ widget.update(message, model.chilp_model, browse)
6262+ #(Model(..model, chilp_model:), effect)
6363+ }
6464+ }
6565+}
6666+6767+// VIEW ------------------------------------------------------------------------
6868+6969+fn view(model: Model) -> Element(Msg) {
7070+ // Let's render comments under https://pony.social/@strawmelonjuice/115911235653686237 and nothing else
7171+ widget.new("pony.social", "115911235653686237", model.chilp_model)
7272+ |> widget.show(model.chilp_model)
7373+}
+8-10
gleam.toml
···11name = "chilp"
22+description = "Allows you to use Mastodon comments on your Lustre blog."
23version = "1.0.0"
33-44-# Fill out these fields if you intend to generate HTML documentation or publish
55-# your project to the Hex package manager.
66-#
77-# description = ""
88-# licences = ["Apache-2.0"]
99-# repository = { type = "github", user = "", repo = "" }
44+licences = ["Apache-2.0"]
55+repository = { type = "forgejo", host = "forge.strawmelonjuice.com", user = "strawmelonjuice", repo = "chilp" }
106# links = [{ title = "Website", href = "" }]
1111-#
1212-# For a full reference of all the available options, you can have a look at
1313-# https://gleam.run/writing-gleam/gleam-toml/.
147158[dependencies]
169gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1010+gleam_json = ">= 3.1.0 and < 4.0.0"
1111+lustre = ">= 5.5.2 and < 6.0.0"
1212+rsvp = ">= 1.2.0 and < 2.0.0"
1313+gleam_time = ">= 1.7.0 and < 2.0.0"
1414+glentities = ">= 6.2.1 and < 7.0.0"
17151816[dev-dependencies]
1917gleeunit = ">= 1.0.0 and < 2.0.0"
+17
manifest.toml
···22# You typically do not need to edit this file
3344packages = [
55+ { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
66+ { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" },
77+ { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
88+ { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" },
99+ { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
1010+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
1111+ { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
512 { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" },
1313+ { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" },
614 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
1515+ { name = "glentities", version = "6.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glentities", source = "hex", outer_checksum = "78A0B28789C1A7840468C683FC9588B0B59AA38BE8CF5DACD1AF2E60A91AE638" },
1616+ { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
1717+ { name = "lustre", version = "5.5.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "2DC2973D81C12E63251B636773217B8E09C5C84590A729750F6BCF009420B38E" },
1818+ { name = "rsvp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "40F9E0E662FF258E10C7041A9591261FE802D56625FB444B91510969644F7722" },
719]
820921[requirements]
2222+gleam_json = { version = ">= 3.1.0 and < 4.0.0" }
1023gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
2424+gleam_time = { version = ">= 1.7.0 and < 2.0.0" }
1125gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
2626+glentities = { version = ">= 6.2.1 and < 7.0.0" }
2727+lustre = { version = ">= 5.5.2 and < 6.0.0" }
2828+rsvp = { version = ">= 1.2.0 and < 2.0.0" }
+1-4
src/chilp.gleam
···11-import gleam/io
11+//// More API focussed stuff should go here
2233-pub fn main() -> Nil {
44- io.println("Hello from chilp!")
55-}
+193
src/chilp/api_typing.gleam
···11+//// Module to match and type JSON data from the endpoints, I just typed these by hand, so if innacurate, they might bug out.
22+33+import gleam/dynamic/decode
44+import gleam/option.{type Option}
55+66+/// A status, like the ones you get from urls like: https://pony.social/api/v1/statuses/115911235653686237/
77+pub type Status {
88+ Status(
99+ id: String,
1010+ /// E.g. 2026-01-17T15:51:34.812Z
1111+ created_at: String,
1212+ // in_reply_to_id: String,
1313+ // in_reply_to_account_id: String,
1414+ sensitive: Bool,
1515+ spoiler_text: String,
1616+ visibility: String,
1717+ language: String,
1818+ uri: String,
1919+ url: String,
2020+ replies_count: Int,
2121+ reblogs_count: Int,
2222+ favourites_count: Int,
2323+ quotes_count: Int,
2424+ edited_at: Option(String),
2525+ content: String,
2626+ // application: StatusApplication,
2727+ account: Account,
2828+ media_attachments: List(String),
2929+ mentions: List(Mentions),
3030+ tags: List(String),
3131+ )
3232+}
3333+3434+pub fn status_decoder() -> decode.Decoder(Status) {
3535+ use id <- decode.field("id", decode.string)
3636+ use created_at <- decode.field("created_at", decode.string)
3737+ use sensitive <- decode.field("sensitive", decode.bool)
3838+ use spoiler_text <- decode.field("spoiler_text", decode.string)
3939+ use visibility <- decode.field("visibility", decode.string)
4040+ use language <- decode.field("language", decode.string)
4141+ use uri <- decode.field("uri", decode.string)
4242+ use url <- decode.field("url", decode.string)
4343+ use replies_count <- decode.field("replies_count", decode.int)
4444+ use reblogs_count <- decode.field("reblogs_count", decode.int)
4545+ use favourites_count <- decode.field("favourites_count", decode.int)
4646+ use quotes_count <- decode.field("quotes_count", decode.int)
4747+ use edited_at <- decode.field("edited_at", decode.optional(decode.string))
4848+ use content <- decode.field("content", decode.string)
4949+ // use application <- decode.field("application", status_application_decoder())
5050+ use account <- decode.field("account", account_decoder())
5151+ use media_attachments <- decode.field(
5252+ "media_attachments",
5353+ decode.list(decode.string),
5454+ )
5555+ use mentions <- decode.field("mentions", decode.list(mentions_decoder()))
5656+ use tags <- decode.field("tags", decode.list(decode.string))
5757+ decode.success(Status(
5858+ id:,
5959+ created_at:,
6060+ sensitive:,
6161+ spoiler_text:,
6262+ visibility:,
6363+ language:,
6464+ uri:,
6565+ url:,
6666+ replies_count:,
6767+ reblogs_count:,
6868+ favourites_count:,
6969+ quotes_count:,
7070+ edited_at:,
7171+ content:,
7272+ // application:,
7373+ account:,
7474+ media_attachments:,
7575+ mentions:,
7676+ tags:,
7777+ ))
7878+}
7979+8080+// pub type StatusApplication {
8181+// StatusApplication(name: String, website: String)
8282+// }
8383+8484+// pub fn status_application_decoder() -> decode.Decoder(StatusApplication) {
8585+// use name <- decode.field("name", decode.string)
8686+// use website <- decode.field("website", decode.string)
8787+// decode.success(StatusApplication(name:, website:))
8888+// }
8989+9090+pub type Account {
9191+ Account(
9292+ id: String,
9393+ username: String,
9494+ acct: String,
9595+ display_name: String,
9696+ locked: Bool,
9797+ bot: Bool,
9898+ discoverable: Bool,
9999+ indexable: Bool,
100100+ group: Bool,
101101+ created_at: String,
102102+ note: String,
103103+ url: String,
104104+ uri: String,
105105+ avatar: String,
106106+ avatar_static: String,
107107+ header: String,
108108+ header_static: String,
109109+ followers_count: Int,
110110+ following_count: Int,
111111+ statuses_count: Int,
112112+ last_status_at: String,
113113+ hide_collections: Bool,
114114+ // noindex: Bool,
115115+ // emojis: todo[],
116116+ // roles: todo[],
117117+ // fields: todo[],
118118+ )
119119+}
120120+121121+pub fn account_decoder() -> decode.Decoder(Account) {
122122+ use id <- decode.field("id", decode.string)
123123+ use username <- decode.field("username", decode.string)
124124+ use acct <- decode.field("acct", decode.string)
125125+ use display_name <- decode.field("display_name", decode.string)
126126+ use locked <- decode.field("locked", decode.bool)
127127+ use bot <- decode.field("bot", decode.bool)
128128+ use discoverable <- decode.field("discoverable", decode.bool)
129129+ use indexable <- decode.field("indexable", decode.bool)
130130+ use group <- decode.field("group", decode.bool)
131131+ use created_at <- decode.field("created_at", decode.string)
132132+ use note <- decode.field("note", decode.string)
133133+ use url <- decode.field("url", decode.string)
134134+ use uri <- decode.field("uri", decode.string)
135135+ use avatar <- decode.field("avatar", decode.string)
136136+ use avatar_static <- decode.field("avatar_static", decode.string)
137137+ use header <- decode.field("header", decode.string)
138138+ use header_static <- decode.field("header_static", decode.string)
139139+ use followers_count <- decode.field("followers_count", decode.int)
140140+ use following_count <- decode.field("following_count", decode.int)
141141+ use statuses_count <- decode.field("statuses_count", decode.int)
142142+ use last_status_at <- decode.field("last_status_at", decode.string)
143143+ use hide_collections <- decode.field("hide_collections", decode.bool)
144144+ // use noindex <- decode.field("noindex", decode.bool)
145145+ decode.success(Account(
146146+ id:,
147147+ username:,
148148+ acct:,
149149+ display_name:,
150150+ locked:,
151151+ bot:,
152152+ discoverable:,
153153+ indexable:,
154154+ group:,
155155+ created_at:,
156156+ note:,
157157+ url:,
158158+ uri:,
159159+ avatar:,
160160+ avatar_static:,
161161+ header:,
162162+ header_static:,
163163+ followers_count:,
164164+ following_count:,
165165+ statuses_count:,
166166+ last_status_at:,
167167+ hide_collections:,
168168+ // noindex:,
169169+ ))
170170+}
171171+172172+pub type Mentions {
173173+ Mentions(id: String, username: String, url: String, acct: String)
174174+}
175175+176176+fn mentions_decoder() -> decode.Decoder(Mentions) {
177177+ use id <- decode.field("id", decode.string)
178178+ use username <- decode.field("username", decode.string)
179179+ use url <- decode.field("url", decode.string)
180180+ use acct <- decode.field("acct", decode.string)
181181+ decode.success(Mentions(id:, username:, url:, acct:))
182182+}
183183+184184+pub type StatusContext {
185185+ StatusContext(ancestors: List(Status), descendants: List(Status))
186186+}
187187+188188+pub fn status_context_decoder() -> decode.Decoder(StatusContext) {
189189+ let dec = status_decoder()
190190+ use ancestors <- decode.field("ancestors", decode.list(dec))
191191+ use descendants <- decode.field("descendants", decode.list(dec))
192192+ decode.success(StatusContext(ancestors:, descendants:))
193193+}