Skip to content

Instantly share code, notes, and snippets.

@darighost
Last active June 17, 2024 00:41
Show Gist options
  • Save darighost/bb9839b9cd0d2742ed022392135e1376 to your computer and use it in GitHub Desktop.
Save darighost/bb9839b9cd0d2742ed022392135e1376 to your computer and use it in GitHub Desktop.
Chud Stack: a web app pattern for hopeless IT workers (and their families)

TL;DR too poor for DO droplet so my website's guestbook went offline, gonna use Urbit as a free backend instead. It worked, you can sign my guestbook here: darigo.su/33chan. Chud stack stands for Css/Htmx ~ Urbit Database

It's 4:33 AM and I'm wide awake, checking my email after a long night working on my grant from the Urbit Foundation.

Screenshot 2024-06-15 at 10 03 06 p m

Is my balance still overdraft, then? Thank God, I am alive. How can I cry1 over cashmoney, when the living Christ breathes every suchness into being? No thanks, billing support. Besides, why the fuck did I learn to program on Urbit if not to use it as a free personal forever cloud server?

So that's what I did.

(move my backend to Urbit, that is)

Here's how it went. (edit ~ in this project i write literally the ugliest code i've ever since i started using computers. so if ur a hiring manager and you found my blog, don't judge me)

How it should work

(or, welcome to the (food) desert of the (hyper)real)

Free. It should not cost money. There is actually no money left, anywhere, as far as I can tell. I can hear my aunt's voice nagging me, You should have studied TikTok like your brother. My brother can afford two nights a week with a meatspace wifeshare. Not me, I followed my dreams2, studied code at a nowhere state school so I could make my AI GF 2% cuter after a long shift at Buffalo Wild Wings. They say with my computer skills I'd make a great shift manager one day.

The AI people fucked up free APIs and now that's not a thing, so no more Jamstack. Urbit fixes this - I'll make my own free API.

I need a forever cloud computer. Back in my day, you could walk into a tech co, jump through some leetcode hoops, a small stack of FOSS contribs, and walk out with a dev job. The old future of ESR and Python is over3. The new future is using

and tags because you can't afford the bandwidth of another wordy style served over your per-request serverless cloud. Once more ours the age of the electron and the baud.[footnote Null copula | Yale Grammatical Diversity Project: English in North America)

I invented the Chud Stack for chuds like me. Trashed out whitebois, too zany and snailpilled to get along in the adult daycare environment of the modern professional Slack.

Actually building it

K let's go.

Imma start with the example starter app from the docs:

/+  default-agent, dbug
|%
+$  card  card:agent:gall
--
%-  agent:dbug
^-  agent:gall
|_  =bowl:gall
+*  this  .
    def   ~(. (default-agent this %.n) bowl)
++  on-init
  ^-  (quip card _this)
  `this
++  on-save   on-save:def
++  on-load   on-load:def
++  on-poke   on-poke:def
++  on-watch  on-watch:def
++  on-leave  on-leave:def
++  on-peek   on-peek:def
++  on-agent  on-agent:def
++  on-arvo   on-arvo:def
++  on-fail   on-fail:def
--

After a brief prayer, an angel's eyes appeared in my reflection in the screen and whispered (with her EYES!) where, in the advanced kernel docs, I needed to gaze gaze gaze gaze gaze gaze gaze gaze gaze gezebelle gaburgably gaze gaze.

Specifically, some docs (thanks nospur), right, here: https://docs.urbit.org/system/kernel/eyre/guides/guide#advanced

So I copy /app/eyre-agent.hoon straight from there. Ever since I been studying Enochian (Angelical) on Duolingo ever since. John Dee fans get it not John Deer. Okay, if I run this, I can make get requests. Yay. What about POST requests? Oh, and cors? Well, the trick is to just |pass a card to Eyre saying "hey, allow cors".

|pass [%e [%approve-origin 'https://darigo.su']]
|pass [%e [%approve-origin 'http://darigo.su']]
|pass [%e [%approve-origin 'http://localhost:3000']] :: so it works in dev lol

Since my form actually should refresh the page after submission, I left out e.preventDefault(). Turns out, for some reason, I need it. I think maybe the page refreshes before my form submission code can finish, which mungs everything? Whatever, added e.preventDefault() and then refresh manually once my POST request finishes in <form>. That fixed it. Random StackOverflow post told me this. Pure luck.

OH! Here's the code I added btw, it's all in the ++on-poke arm, so I'll just include that.

++  on-poke
  |=  [=mark =vase]
  ^-  (quip card _this)
  ?+    mark
    (on-poke:def [mark vase])
  ::
      %noun
    ?.  =(q.vase %bind)
      %-  (slog leaf+"Bad argument." ~)
      `this
    %-  (slog leaf+"Attempting to bind /moo." ~)
    :_  this
    [%pass /bind-moo %arvo %e %connect `/'moo' %guestbook]~
  ::
      %handle-http-request
    =/  req  !<  (pair @ta inbound-request:eyre)  vase
    ?+    method.request.q.req
      =/  data=octs
        (as-octs:mimes:html '<h1>405 Method Not Allowed</h1>')
      =/  content-length=@t
        (crip ((d-co:co 1) p.data))
      =/  =response-header:http
        :-  405
        :~  ['Content-Length' content-length]
            ['Content-Type' 'text/html']
            ['Allow' 'GET']
        ==
      :_  this
      :~
        [%give %fact [/http-response/[p.req]]~ %http-response-header !>(response-header)]
        [%give %fact [/http-response/[p.req]]~ %http-response-data !>(`data)]
        [%give %kick [/http-response/[p.req]]~ ~]
      ==
    ::
    %'POST'
      =/  ip-address  value:(snag 0 (skim header-list.request.q.req |=(h=[key=@t value=@t] =(-.h 'x-real-ip'))))
      :: 
      :: Tlon uses an internal proxy, so I can't just get the address like this.
      :: Instead, I have to rely on the x-forwarded-for header.
      ?:  &(!=(~ (find [ip-address]~ (turn posts |=(p=[@t @t] -.p)))) !=(ip-address '181.174.107.62'))
        =/  data=octs
        ~&  posts
        (as-octs:mimes:html (crip "<h1>403 IP already signed the guestbook ({(scow %if ip-address)})</h1>"))
        =/  content-length=@t  (crip ((d-co:co 1) p.data))
        =/  =response-header:http
          :-  403
          :~  ['Content-Length' content-length]
              ['Content-Type' 'text/html']
              ['Allow' 'GET']
          ==
        :_  this
        :~
          [%give %fact [/http-response/[p.req]]~ %http-response-header !>(response-header)]
          [%give %fact [/http-response/[p.req]]~ %http-response-data !>(`data)]
          [%give %kick [/http-response/[p.req]]~ ~]
        ==
      :: Code to run if the IP is NOT banned
      =/  body  (crip `tape`(turn (trip `@t`+.+.body.request.q.req) |=(c=@tD ?:(=(c '"') (crip "'") c))))
      ~&  body
      =/  data=octs
        (as-octs:mimes:html '{"ok": 1}')
      =/  content-length=@t
        (crip ((d-co:co 1) p.data))
      =/  =response-header:http
        :-  200
        :~  ['Content-Length' content-length]
            ['Content-Type' 'text/json']
        ==
      :_  this(posts `(list [@t @t])`(snoc `(list [@t @t])`posts [`@t`ip-address `@t`body]))
      :~
        [%give %fact [/http-response/[p.req]]~ %http-response-header !>(response-header)]
        [%give %fact [/http-response/[p.req]]~ %http-response-data !>(`data)]
        [%give %kick [/http-response/[p.req]]~ ~]
      ==
    %'GET'
      =/  data=octs
      (as-octs:mimes:html (crip "\{\"content\": \"{(join 'ILOVEJOANBEAZ' (turn posts |=(p=[@t @t] +.p)))}\"}"))
      =/  content-length=@t
        (crip ((d-co:co 1) p.data))
      =/  =response-header:http
        :-  200
        :~  ['Content-Length' content-length]
            ['Content-Type' 'text/json']
        ==
      :_  this
      :~
        [%give %fact [/http-response/[p.req]]~ %http-response-header !>(response-header)]
        [%give %fact [/http-response/[p.req]]~ %http-response-data !>(`data)]
        [%give %kick [/http-response/[p.req]]~ ~]
      ==
    ==
  ==

Note that I block repeat IP addresses. Afterall, you only get to sign a guestbook once. Well, guess what! Tlon uses a reverse proxy, so all IPs show up as some internal private IP in the 10.0 range. The real IP comes from the x-real-ip header. So that's why my code uses that instead of the "real" address.

I can access a list of headers at header-list.request.q.req, and it looks like this:

  header-list
~[
    [key='host' value='motluc-nammex.tlon.network']
    [key='x-request-id' value='yada yada yada']
    [key='x-real-ip' value='[my real ip address lulz]']
    ...
]

Funly, both x-real-ip and x-forwarded-for are present in the list. So we should be able to just,

=/  headers
  header-list.request.q.req  :: Headers, `(list [key=@t value=@t])`
%+  snag  0                  :: There should be exactly one result. Snag it.
%+  skim  headers            :: Filter list to remove everything but `x-real-ip`
|=  header=[key=@t value=@t]
.=  key.header  'x-real-ip'

That's a mouthful. Let's squish it into a crunchy 90s sysadmin Perl one-liner >:D

(snag 0 (skim header-list.request.q.req |=(h =(-.h 'x-real-ip'))))

Hot in the saggy-belly way of a mudcricket. White trash bitch. Wrinkly old aisana married to her Brayan yelling at her malcreado little wannabe gangbanger esquincles. Fucking garbage. But the dump is heaven to a vulture. See her through the eyes of God, she is God's daughter, too. Doesn't he care even for a little sparrow who falls?

Obtw it work'n:

Screenshot 2024-06-14 at 11 08 57 p m

Well well well, that makes it work, as long as I upload a tiny image! I run out of loom (Urbit app memory)! Hmm. I'll just compress the image on the frontend before sending it. The bad news is, anyone can DoS my Urbit ship by sending a 1 MB image lmfao. Hopefully no one tries.

Deployed to darigo.su via github pages and... I can't access the guestbook because GH pages doesn't work with SPAs.

Screenshot 2024-06-14 at 8 27 30 p m

Oh well, time to kill myself! Sewerslide and my pretty white skin with pretty strawberry juice gashes all over all over meeeee

jk, I'll have to use React Router hashroutes. Mk, no biggie, let's try it. Woo, it works. But now people have to use darigo.su/#/33chan, which breaks all the old links, and is just less nice. Later I'll make a redirect from /33chan. Maybe also a subdomain. Also, right now darigo.su/33chan takes you to my little fake computer with a guestbook, wheres darigo.su/guestbook takes you to a full page 33chan. This seems backwards.

I guess it works now

(or, there's an /etc/sudoers file in your brainstem full of usernames you don't recognize)

My feverish anarchist ramblings about These Hard Times help me cope with shame and fear that my lunacy makes me unemployable in IT. But it's not me, you see, it's the economy!

Asked my friends on X and Tlon to sign, let's see how that's goin

Screenshot 2024-06-15 at 3 39 03 p m

of course, this is a cheap trick. The true chud stack uses urbit's built in free federation to make federated apps with free backends.

Cocksickle. Someone uploaded single quotes, which breaks my JSON for reasons of "I didn't do good string handling". Here's the bad string:

This post breaks my fetch request for invalid JSON: tfw your brothers and cousins are wondering what you do for a living___what's a 'yourbit'?

The problem is, that everytime I make a patch to the Urbit code, I lose my state. (that shouldn't happen but I think I'm doing the state in the wrong place. i'm not gonna debug it, we're almost done). So what I can do, is before I do an update, I grab the state with a GET request and store it in a file. Then I do the change, and just write a script to reupload the previous state. Here's the script (for myself, as I'll prolly need to do this again sooner or later and who knows if I'll be able to find this file hehe).

const fs = require('fs');
const { content } = JSON.parse(fs.readFileSync('posts.txt', 'utf8'));
const rawPosts = content
  .split('ILOVEJOANBEAZ')
  .map(post => {
    const [content, subject, file] =  post.split('___')
    return [subject, content.slice("{'ok':'".length), file.slice(0, -2)]
  }).forEach((p) => {
    const [content, subject, file] =  p
    fetch("https://motluc-nammex.tlon.network/moo", {
      "credentials": "omit",
      "headers": {
          "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0",
          "Accept": "*/*",
          "Accept-Language": "en-US,en;q=0.5",
          "Content-Type": "application/json",
          "Sec-Fetch-Dest": "empty",
          "Sec-Fetch-Mode": "cors",
          "Sec-Fetch-Site": "cross-site",
          "Priority": "u=4",
          "Pragma": "no-cache",
          "Cache-Control": "no-cache"
      },
      "referrer": "http://localhost:3000/",
      "body": JSON.stringify({"ok": `${subject}___${content}___${file}`}),
      "method": "POST",
      "mode": "cors"
  }).then(response => {
      if (!response.ok) {
          throw new Error('Network response was not ok: ' + response.statusText);
      }
      return response.json();
  }).then(data => console.log(data))
  .catch(error => {alert('Unable to upload your post (see browser console for more details)');console.error('Fetch error msg lolol:', error.message)});
  });

I ran it and guess what!!1! I was able to save the munged posts. And I fixed the bug.

Creativity softens poverty. My fault, my fault. My conspiracy addled brain. I don't even see a conspiracy. Rather, a shared value system among nobility, and even norms for producing new norms. NGOs, intelligence agencies, academics, is it so hard to imagine informal, decentralized ways for distributed power to coordinate in seemingly spontaneous ways? Even if it didn't originate spontaneously, in the 20th century the US needed countercultures that showed the West's hundred flowers blooming, especially rebellions that subtly fortify the authority they fight. One day someone will invent social science.

Thanks for reading, hope this entertained you. Leave a nice comment <3

Footnotes

  1. Reflecting on my glum money situation made me think of the word "situation", and unoriginally, situationism. Like most ideas, it wasn't really refuted, and didn't evolve into something else, it just ran out of steam and stopped being a thing. Ditto for folk punk. A further reminder that ideas and social relations are downstream from material conditions and the tension between owner and who needs access to what is owned.

  2. Does that sound familiar? Not yet, but soon. Engineers lurching over their desk after a brutal day loading and unloading garden equipment at Lowe's, until finally the refreshing glow of their monitor and those precious words, which no longer meant money but still meant something: all tests have passed. From now on, measure progress in moral rather than material terms. Spaghetti straps in school, gay priest pride parades, 100% of boys in therapy.

    Influencers and sex workers have their own store, with food in it. The store you go to does not have food, per se. Or rather, it's like the story of Mexican food. Read a Mexican cook book from 1900, and compare it to the burnt pieces of vegetable oil sold as "tacos" in La Merced. Then, ponder your nation's future. Pan y putas.

  3. Why Python? | Linux Journal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment