Merge pull request #204 from urbit/app-workbook

App workbook
This commit is contained in:
Sigilante 2022-11-17 07:32:51 -06:00 committed by GitHub
commit 88f9c9926d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1199 additions and 5 deletions

View File

@ -0,0 +1,25 @@
+++
title = "App Workbook"
date = "2022-11-17"
description = "Announcing a collection showcasing small apps."
[extra]
author = "N E Davis"
ship = "~lagrev-nocfep"
image = "https://media.urbit.org/site/posts/essays/blog-workbook.png"
+++
![](https://media.urbit.org/site/posts/essays/blog-workbook.png)
# App Workbook
We introduced the [Hoon Workbook](https://developers.urbit.org/guides/additional/workbook) last summer as a way of showcasing how to solve problems with Hoon code. The Hoon Workbook mainly focuses on library and generator code rather than apps.
As a companion piece, we are pleased to inaugurate the [App Workbook](TODO), a collection of tutorials built around real-world Gall agents that exemplify some element of practice.
We'll be expanding it with more examples over time, but to start us off we have a discussion of ~midden-fabler's `%ahoy` and ~hanfel-dovned's `%page`, as well as moving ~lagrev-nocfep's `%flap` over to the App Workbook from its current home.
Each of these exemplifies some aspect of Gall agent architecture. For instance, `%ahoy` shows how to build a Gall agent that uses generators instead of raw pokes at the CLI to carry out target activities. `%page` illustrates simple HTTP page hosting from an Urbit ship.
(We're also adding _Gleichniszahlenreihe_ audioactive challenge from the Hoon School Live competition in summer 2022 to the Hoon Workbook. _Gleichniszahlenreihe_ illustrates a variety of approaches to parsing input.)
Take a look and give us suggestions for other small Gall agents you'd like to see tutorials based on as well!

View File

@ -0,0 +1,12 @@
+++
title = "App Workbook"
weight = 32
insert_anchor_links = "right"
+++
## [`%ahoy`](/guides/additional/app-workbook/ahoy)
## [`%flap`](/guides/additional/app-workbook/flap)
## [`%feature`](/guides/additional/app-workbook/feature)

View File

@ -0,0 +1,786 @@
+++
title = "`%ahoy` Ship Monitoring"
weight = 10
+++
# `%ahoy`
The `%ahoy` desk by [~midden-fabler](https://urbit.org/ids/~midden-fabler) provides a number of agents to automatically monitor ship activity such as breaching and network uptime. This tutorial examines the `%ahoy` agent specifically with some slight simplifications to demonstrate how an Urbit-native app can be constructed. You will see how to render a front-end using Sail, employ the `++abet` engine design pattern, construct CLI generators, and set wakeup timers using [Behn](https://developers.urbit.org/reference/glossary/behn).
`%ahoy` presents a web UI at `/ahoy` rendered using [Sail](https://developers.urbit.org/guides/additional/sail) and [~paldev](https://urbit.org/ids/~paldev)'s Rudder library alongside command-line generators to add, delete, and modify ship watches. Notifications are sent using `%hark-store` if a ship hasn't been contacted after a specified amount of time.
```hoon
:ahoy|add-watch ~sampel ~d1
:ahoy|del-watch ~sampel
:ahoy|set-update-interval ~m30
```
## `/sur` Structure Files
As with other agents, we think about our data structures and actions before we dive into the agent code. The structure file here defines the state for the agent, `records`, which is a collection of ships to watch and the update interval for sending notifications.
```hoon
+$ records
$: watchlist=(map ship @dr)
update-interval=@dr
==
```
Three commands are supported: to add a ship to the watchlist at a given watch interval, to delete the ship, or to change the check interval. (Modifying a ship is the same as adding it.)
```hoon
+$ command
$% [%add-watch =ship t=@dr]
[%del-watch =ship]
[%set-update-interval t=@dr]
==
```
**`/sur/ahoy.hoon`**:
```hoon {% copy=true, mode="collapse" %}
|%
+$ records
$: watchlist=(map ship @dr)
update-interval=@dr
==
+$ command
$% [%add-watch =ship t=@dr]
[%del-watch =ship]
[%set-update-interval t=@dr]
==
--
```
No special mark files are necessary for `%ahoy`.
## `/app` Agent Files
The agent itself is simple: it maintains `records` as state and processes pokes from generators or the front-end and gifts from `%behn` in particular.
In addition, `%ahoy` sends notifications using `%hark-store`, the notification process integrated with Landscape and Grid.
### Pokes
At the macro level, `++on-poke` recognizes three poke cages:
1. `%noun` for pinging a ship.
2. `%ahoy-command` for commands per `/sur/ahoy.hoon`.
3. `handle-http-request` for displaying the webpage.
Most of the poke work takes place through `%ahoy-command`, which checks on the ship state per [Ames](https://developers.urbit.org/reference/glossary/ames) scheme of `%alien` and `%known` ships, then maintains the agent state by its watchlist.
```hoon
%ahoy-command
=+ !<(cmd=command vase)
?- -.cmd
%add-watch
=/ ss=(unit ship-state:ames)
(~(ship-state ahoy bowl) ship.cmd)
?~ ss
~& >> [%ahoy '%alien ship not added']
[~ this]
:- [(send-plea:hc ship.cmd)]~
this(watchlist (~(put by watchlist) ship.cmd t.cmd))
::
%del-watch
`this(watchlist (~(del by watchlist) ship.cmd))
::
%set-update-interval
`this(update-interval t.cmd)
==
```
HTTP requests are processed into a form useful to `rudder`, a front-end rendering library for native Hoon webpages. `rudder` facilitates a Sail-based webpage being exposed through three arms:
1. `++argue` responds to `POST` requests.
2. `++final` is called after `POST` requests.
3. `++build` responds to `GET` requests, most commonly just yielding the webpage.
A number of other facilities in `rudder` are employed here as well:
- `++order:rudder` is a type for handling inbound requests from Eyre.
- `++steer:rudder` is the helper constructor for producing pages.
- `++point:rudder` is a routing arm.
- `++fours:rudder` is a 404 error handler.
- `++brief:rudder` is a type union, `?(~ @t)`.
```hoon
%handle-http-request
=; out=(quip card _+.state)
[-.out this(+.state +.out)]
%. [bowl !<(order:rudder vase) +.state]
%- (steer:rudder _+.state command)
:^ pages
(point:rudder /[dap.bowl] & ~(key by pages))
(fours:rudder +.state)
|= cmd=command
^- $@ brief:rudder
[brief:rudder (list card) _+.state]
=^ cards this
(on-poke %ahoy-command !>(cmd))
['Processed succesfully.' cards +.state]
```
### Gifts
The agent expects to receive a `%wake` gift periodically from Behn on the wire `%update-interval`. It handles this by means of an arm in the agent's helper core, `++on-update-interval`.
```hoon
[%update-interval ~]
=^ cards state
on-update-interval:hc
[cards this]
```
This helper core arm notably employs the `++abet` engine pattern for handling cards. The `++abet` engine is a design pattern rather than a specific core. It is designed to accumulate cards, often using `++emit` and `++emil`, then send them all at once.
The `++abet` engine pattern itself is rather simple to construct. It enables other arms to construct a list of cards rather than having to produce complex `=^`-style constructions. This instance of the engine pattern consists of three arms (omitting an `++abed` arm):
- `++emit` is used to submit a card to a collection of cards in the engine helper core.
- `++emil` is similar but accepts a list of cards.
- `++abet` issues the list of cards back along with the state to be updated. (Note that the core must be scoped such that the Gall agent's state is visible.)
Other arms (such as `++set-timer`) then simply construct cards which are inserted into the `++abet` engine's list.
```hoon {% mode="collapse" %}
=| cards=(list card)
|_ =bowl:gall
++ this .
++ abet
^- (quip card _state)
[(flop cards) state]
::
++ emit
|= car=card
this(cards [car cards])
::
++ emil
|= rac=(list card)
|- ^+ this
?~ rac
this
=. cards [i.rac cards]
$(rac t.rac)
::
++ on-update-interval
^- (quip card _state)
:: reset timer
=. this (emit (set-timer update-interval))
:: send pleas
=. this
%- emil
%+ turn ~(tap in ~(key by watchlist))
|= [who=ship]
(send-plea who)
:: send notifications
=. this
%- emil
%- zing
%+ turn ~(tap in down-status)
|= [who=ship]
(send-notification who)
abet
::
++ set-timer
|= t=@dr
^- card
=/ when=@da (add now.bowl t)
[%pass /update-interval %arvo %b %wait when]
::
++ send-plea
|= [who=ship]
^- card
[%pass /ahoy/(scot %p who) %arvo %a %plea who %evil-vane / ~]
::
++ down-status
^- (set ship)
%- silt
%+ murn ~(tap in ~(key by watchlist))
|= [who=ship]
=/ when=(unit @dr) (~(last-contact ahoy bowl) who)
?~ when ~
?. (gte u.when (~(got by watchlist) who))
~
`who
::
++ send-notification
|= [who=ship]
^- (list card)
?. .^(? %gu /(scot %p our.bowl)/hark-store/(scot %da now.bowl)) ~
=/ when=@dr (need (~(last-contact ahoy bowl) who))
=/ title=(list content:hark)
=- [ship+who - ~]
text+(crip " has not been contacted in {<when>}")
=/ =bin:hark [/[dap.bowl] q.byk.bowl /(scot %p who)]
=/ =action:hark [%add-note bin title ~ now.bowl / /[dap.bowl]]
=/ =cage [%hark-action !>(action)]
[%pass /hark %agent [our.bowl %hark-store] %poke cage]~
--
```
For `%ahoy`, the main arm we need to examine is `++on-update-interval`. This arm resets the timer, sends checks to all of the ships, and then sends notifications to `%hark-store` for anything unresponsive.
```hoon
++ on-update-interval
^- (quip card _state)
:: reset timer
=. this (emit (set-timer update-interval))
:: send pleas
=. this
%- emil
%+ turn ~(tap in ~(key by watchlist))
|= [who=ship]
(send-plea who)
:: send notifications
=. this
%- emil
%- zing
%+ turn ~(tap in down-status)
|= [who=ship]
(send-notification who)
abet
```
The `++send-plea` status check is interesting: it checks whether Ames is responsive on a particular ship without doing anything to the remote ship except eliciting an error. (`|hi` or similar would unnecessarily spam the recipient's Dojo.)
```hoon
++ send-plea
|= [who=ship]
^- card
[%pass /ahoy/(scot %p who) %arvo %a %plea who %evil-vane / ~]
```
`%hark-store` is the standard cross-agent notification store provided by Grid and recognized by Landscape. The notification message requires a little bit of explicit construction as `action` but can be treated as boilerplate code aside from the text.
```hoon
++ send-notification
|= [who=ship]
^- (list card)
?. .^(? %gu /(scot %p our.bowl)/hark-store/(scot %da now.bowl)) ~
=/ when=@dr (need (~(last-contact ahoy bowl) who))
=/ title=(list content:hark)
=- [ship+who - ~]
text+(crip " has not been contacted in {<when>}")
=/ =bin:hark [/[dap.bowl] q.byk.bowl /(scot %p who)]
=/ =action:hark [%add-note bin title ~ now.bowl / /[dap.bowl]]
=/ =cage [%hark-action !>(action)]
[%pass /hark %agent [our.bowl %hark-store] %poke cage]~
```
**`/app/ahoy.hoon`**:
```hoon {% copy=true, mode="collapse" %}
:: ahoy: ship monitoring
::
:: get notified if last-contact with a ship
:: exceeds a specified amount of time
::
:: usage:
:: :ahoy|add-watch ~sampel ~d1
:: :ahoy|del-watch ~sampel
:: :ahoy|set-update-interval ~m30
::
:: scrys:
:: .^((map @p @dr) %gx /=ahoy=/watchlist/noun)
:: .^((set ship) %gx /=ahoy=/watchlist/ships/noun)
:: .^(@dr %gx /=ahoy=/update-interval/noun)
::
/- *ahoy, hark=hark-store
/+ default-agent,
agentio,
rudder,
dbug,
ahoy
/~ pages (page:rudder records command) /app/ahoy/webui
::
=> |%
+$ card card:agent:gall
+$ versioned-state
$% state-0
==
+$ state-0 [%0 records]
--
::
=| state-0
=* state -
%- agent:dbug
^- agent:gall
=<
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
hc ~(. +> bowl)
io ~(. agentio bowl)
pass pass:io
::
++ on-init
^- (quip card _this)
=/ interval=@dr ~m5
=+ sponsor=(sein:title [our now our]:bowl)
:_ this(update-interval interval)
:~ (~(connect pass /eyre/connect) [~ /[dap.bowl]] dap.bowl)
(poke-self:pass %ahoy-command !>([%add-watch sponsor ~d1]))
(set-timer interval)
==
::
++ on-save !>(state)
++ on-load
|= ole=vase
^- (quip card _this)
=/ old !<(versioned-state ole)
?- -.old
%0 [~ this(state old)]
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> =(our src):bowl
?+ mark (on-poke:def mark vase)
%noun
=+ !<(who=ship vase)
:_ this
[(send-plea:hc who)]~
::
%ahoy-command
=+ !<(cmd=command vase)
?- -.cmd
%add-watch
=/ ss=(unit ship-state:ames)
(~(ship-state ahoy bowl) ship.cmd)
?~ ss
~& >> [%ahoy '%alien ship not added']
[~ this]
:- [(send-plea:hc ship.cmd)]~
this(watchlist (~(put by watchlist) ship.cmd t.cmd))
::
%del-watch
`this(watchlist (~(del by watchlist) ship.cmd))
::
%set-update-interval
`this(update-interval t.cmd)
==
::
%handle-http-request
=; out=(quip card _+.state)
[-.out this(+.state +.out)]
%. [bowl !<(order:rudder vase) +.state]
%- (steer:rudder _+.state command)
:^ pages
(point:rudder /[dap.bowl] & ~(key by pages))
(fours:rudder +.state)
|= cmd=command
^- $@ brief:rudder
[brief:rudder (list card) _+.state]
=^ cards this
(on-poke %ahoy-command !>(cmd))
['Processed succesfully.' cards +.state]
==
::
++ on-watch
|= =path
^- (quip card _this)
?+ path (on-watch:def path)
[%http-response *]
?> =(our src):bowl
[~ this]
==
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
[%ahoy @ ~] [~ this]
::
[%update-interval ~]
=^ cards state
on-update-interval:hc
[cards this]
::
[%eyre %connect ~]
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%eyre %bound *]
~? !accepted.sign-arvo
[dap.bowl 'eyre bind rejected!' binding.sign-arvo]
[~ this]
==
==
::
++ on-peek
|= =path
^- (unit (unit cage))
?> =(our src):bowl
?+ path (on-peek:def path)
[%x %watchlist ~] ``noun+!>(watchlist)
[%x %watchlist %ships ~] ``noun+!>(~(key by watchlist))
[%x %update-interval ~] ``noun+!>(update-interval)
==
::
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
::
=| cards=(list card)
|_ =bowl:gall
++ this .
++ abet
^- (quip card _state)
[(flop cards) state]
::
++ emit
|= car=card
this(cards [car cards])
::
++ emil
|= rac=(list card)
|- ^+ this
?~ rac
this
=. cards [i.rac cards]
$(rac t.rac)
::
++ on-update-interval
^- (quip card _state)
:: reset timer
=. this (emit (set-timer update-interval))
:: send pleas
=. this
%- emil
%+ turn ~(tap in ~(key by watchlist))
|= [who=ship]
(send-plea who)
:: send notifications
=. this
%- emil
%- zing
%+ turn ~(tap in down-status)
|= [who=ship]
(send-notification who)
abet
::
++ set-timer
|= t=@dr
^- card
=/ when=@da (add now.bowl t)
[%pass /update-interval %arvo %b %wait when]
::
++ send-plea
|= [who=ship]
^- card
[%pass /ahoy/(scot %p who) %arvo %a %plea who %evil-vane / ~]
::
++ down-status
^- (set ship)
%- silt
%+ murn ~(tap in ~(key by watchlist))
|= [who=ship]
=/ when=(unit @dr) (~(last-contact ahoy bowl) who)
?~ when ~
?. (gte u.when (~(got by watchlist) who))
~
`who
::
++ send-notification
|= [who=ship]
^- (list card)
?. .^(? %gu /(scot %p our.bowl)/hark-store/(scot %da now.bowl)) ~
=/ when=@dr (need (~(last-contact ahoy bowl) who))
=/ title=(list content:hark)
=- [ship+who - ~]
text+(crip " has not been contacted in {<when>}")
=/ =bin:hark [/[dap.bowl] q.byk.bowl /(scot %p who)]
=/ =action:hark [%add-note bin title ~ now.bowl / /[dap.bowl]]
=/ =cage [%hark-action !>(action)]
[%pass /hark %agent [our.bowl %hark-store] %poke cage]~
--
```
**`/lib/ahoy.hoon`**:
This library file provides helper logic for determining ship status. In particular, scries are simplified. For instance, `(~(last-contact ahoy bowl) ship)` can be used instead of the scry below.
```hoon {% copy=true, mode="collapse" %}
|_ =bowl:gall
++ ship-state
|= [who=ship]
^- (unit ship-state:ames)
?. (~(has in peers) who)
~
`.^(ship-state:ames %ax /(scot %p our.bowl)//(scot %da now.bowl)/peers/(scot %p who))
::
++ peers
^- (set ship)
=/ mips
.^((map ship ?(%alien %known)) %ax /(scot %p our.bowl)//(scot %da now.bowl)/peers)
~(key by mips)
::
++ last-contact
|= [who=ship]
^- (unit @dr)
=/ ss=(unit ship-state:ames) (ship-state who)
?~ ss ~
?. ?=([%known *] u.ss)
~
=/ last-contact=@da last-contact.qos.u.ss
=/ when=@dr (sub now.bowl last-contact)
`when
--
```
**`/app/ahoy/webui/index.hoon`**:
```hoon {% copy=true, mode="collapse" %}
/- *ahoy, contact=contact-store
/+ ahoy, rudder, ahoy-style, sigil-svg=sigil
::
^- (page:rudder records command)
|_ [=bowl:gall =order:rudder records]
++ argue
|= [headers=header-list:http body=(unit octs)]
^- $@(brief:rudder command)
=/ args=(map @t @t)
?~(body ~ (frisk:rudder q.u.body))
?~ what=(~(get by args) 'what') ~
?+ u.what ~
%add-watch
?~ who=(slaw %p (~(gut by args) 'who' '')) ~
?~ when=(slaw %dr (~(gut by args) 'when' '')) ~
[%add-watch u.who u.when]
::
%del-watch
?~ who=(slaw %p (~(gut by args) 'who' '')) ~
[%del-watch u.who]
==
::
++ final (alert:rudder (cat 3 '/' dap.bowl) build)
++ build
|= $: arg=(list [k=@t v=@t])
msg=(unit [o=? =@t])
==
^- reply:rudder
|^ [%page page]
++ page
^- manx
;html
;head
;title:"%ahoy"
;meta(charset "utf-8");
;meta(name "viewport", content "width=device-width, initial-scale=1");
;style:"{(trip style:ahoy-style)}"
==
;body
;a/"/ahoy"
;h2:"%ahoy"
==
;h4:"ship monitoring (tutorial)"
get notified if last-contact with a ship
exceeds a specified amount of time
;+ ?~ msg ;p:""
?: o.u.msg
;p.green:"{(trip t.u.msg)}"
;p.red:"{(trip t.u.msg)}"
;table#ahoy
;form(method "post")
:: table header
;tr(style "font-weight: bold")
;td(align "center"):"~"
;td(align "center"):"@p"
;td(align "center"):"notify after @dr"
;td(align "center"):"last-contact `@dr"
==
:: first row for adding new ships
;tr
;td
;button(type "submit", name "what", value "add-watch"):"+"
==
;td
;input(type "text", name "who", placeholder "~sampel");
==
;td
;input(type "text", name "when", placeholder "~d1.h12.m30");
==
;td(align "center"):"~"
== :: first row
== :: form
;* work
==
== :: body
== :: html
++ work
^- (list manx)
%+ turn ~(tap by watchlist)
|= [=ship t=@dr]
;tr
;td
:: %del-watch
;form(method "post")
;button(type "submit", name "what", value "del-watch"):"-"
;input(type "hidden", name "who", value "{(scow %p ship)}");
==
:: ship
;td
;+ (sigil ship)
; {(scow %p ship)}
==
:: when to notify
;form(method "post")
;td
;input(type "hidden", name "what", value "add-watch");
;input(type "hidden", name "who", value "{(scow %p ship)}");
;input(type "text", name "when", value "{(scow %dr t)}");
==
==
:: last-contact
;td(align "right")
; {<(~(last-contact ahoy bowl) ship)>}
==
==
==
::
++ contacts ~+
=/ base=path
/(scot %p our.bowl)/contact-store/(scot %da now.bowl)
?. .^(? %gu base) *rolodex:contact
.^(rolodex:contact %gx (weld base /all/noun))
::
++ sigil
|= =ship
^- manx
=/ bg=@ux
?~(p=(~(get by contacts) ship) 0xff.ffff color.u.p)
=/ fg=tape
=+ avg=(div (roll (rip 3 bg) add) 3)
?:((gth avg 0xc1) "black" "white")
=/ bg=tape
((x-co:co 6) bg)
;div.sigil(style "background-color: #{bg}; width: 20px; height: 20px;")
;img@"/ahoy/sigil.svg?p={(scow %p ship)}&fg={fg}&bg=%23{bg}&icon&size=20";
==
-- :: |^
-- :: |_
```
The CSS styling is included via a library core:
**`/lib/ahoy/style.hoon`**:
```hoon {% copy=true, mode="collapse" %}
|%
++ style
'''
* { margin: 0.2em; padding: 0.2em; font-family: monospace; }
body {
background-color: black;
color: white;
}
h2 { color: red; }
p { max-width: 50em; }
form { margin: 0; padding: 0; }
.red { font-weight: bold; color: #dd2222; }
.green { font-weight: bold; color: #229922; }
a {
display: inline-block;
color: inherit;
padding: 0;
margin-top: 0;
}
table#ahoy tr td:nth-child(2) {
padding: 0 0.5em;
}
.label {
display: inline-block;
background-color: #ccc;
border-radius: 3px;
margin-right: 0.5em;
padding: 0.1em;
}
.label input[type="text"] {
max-width: 100px;
}
.label span {
margin: 0 0 0 0.2em;
}
button {
padding: 0.2em 0.5em;
}
.sigil {
display: inline-block;
vertical-align: middle;
margin: 0 0.5em 0 0;
padding: 0.2em;
border-radius: 0.2em;
}
.sigil * {
margin: 0;
padding: 0;
}
'''
--
```
### Rendering Sigils
[Sigils](https://urbit.org/blog/creating-sigils) are unique visual representations of `@p` ship identifiers. Many Urbit apps use sigils in small or large sizes as ship icons.
A sigil library is provided with ~paldev's Suite tools. We do not include the contents of `/lib/sigil.hoon` or `/lib/sigil/symbols.hoon` here due to their length.
- [`/lib/sigil.hoon`](https://github.com/Fang-/suite/blob/master/lib/sigil.hoon)
- [`/lib/sigil/symbols.hoon`](https://github.com/Fang-/suite/blob/master/lib/sigil/symbols.hoon)
The sigils are rendered in `/app/ahoy/webui/index.hoon`.
## `/gen` Generator Files
Some agents (notably `%helm`, a Dojo tool) are instrumented to work directly with generators at the command line. The `%ahoy` agent demonstrates this with several generator files such as `/gen/add-watch.hoon`, used thus:
```hoon
:ahoy|add-watch ~sampel-palnet ~h2
```
**`/gen/ahoy/add-watch.hoon`**
```hoon {% mode="collapse" %}
:: :ahoy|add-watch ~sampel ~d1
::
:- %say
|= $: ^
[who=ship t=@dr ~]
~
==
[%ahoy-command [%add-watch who t]]
```
As you can see here, an `%ahoy-command` is generated which is then passed to the `%ahoy` agent as a poke using Dojo's `|` logic. (A generator called with Dojo's `+` logic would be located in `/gen`, whereas `|` tells Dojo to look inside the agent's folder, much like a `/mar` mark file.)
Such agent-specific generator files can be much cleaner than manual poke logic:
```hoon
:ahoy|add-watch ~sampel-palnet ~h2
```
is the equivalent of
```hoon
:ahoy &ahoy-command [%add-watch ~zod ~h2]
```
### Exercise: Compose a Generator
- Without consulting the `%ahoy` source code, compose a generator `/gen/ahoy/del-watch.hoon` which removes a ship from the watchlist.

View File

@ -0,0 +1,237 @@
+++
title = "%feature Page Hosting"
weight = 160
+++
# `%feature`
[`%feature`](https://github.com/hanfel-dovned/Feature) by [~hanfel-dovned](https://urbit.org/ids/~hanfel-dovned) hosts a simple HTML page from an Urbit ship at an associated URL. This tutorial examines how it uses the middleware [`%schooner`](https://github.com/dalten-collective/schooner/) library by Quartus to return a web page when contacted by a web browser. You will learn how a basic site hosting app can handle HTTP requests and render a page using an `%html` mark.
`%feature` presents a web page from `/app/feature-ui` at `/apps/feature/feature-ui`. These paths are both configurable by the developer.
## `/sur` Structure Files
Our primary event in this case is simply an `%action` to create a page.
**`/sur/feature.hoon`**:
```hoon
|%
+$ action
$% [%new-page html=@t]
==
--
```
No special mark files are necessary for `%feature` other than `%html`.
## `/app` Agent Files
The agent only maintains a state containing the page contents as a `cord.`
The system only handles pokes: there are no subscriptions or Arvo calls except for the Eyre binding.
**`/app/feature.hoon`**:
```hoon {% mode="collapse" copy=true %}
/- feature
/+ dbug, default-agent, server, schooner
/* feature-ui %html /app/feature-ui/html
|%
+$ versioned-state
$% state-0
==
+$ state-0 [%0 page=@t]
+$ card card:agent:gall
--
%- agent:dbug
^- agent:gall
=| state-0
=* state -
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
++ on-init
^- (quip card _this)
:_ this(page 'Hello World')
:~
:* %pass /eyre/connect %arvo %e
%connect `/apps/feature %feature
==
==
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
=/ old !<(versioned-state old-state)
?- -.old
%0 `this(state old)
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
|^
?+ mark (on-poke:def mark vase)
%handle-http-request
?> =(src.bowl our.bowl)
=^ cards state
(handle-http !<([@ta =inbound-request:eyre] vase))
[cards this]
==
++ handle-http
|= [eyre-id=@ta =inbound-request:eyre]
^- (quip card _state)
=/ ,request-line:server
(parse-request-line:server url.request.inbound-request)
=+ send=(cury response:schooner eyre-id)
::
?+ method.request.inbound-request
[(send [405 ~ [%stock ~]]) state]
::
%'POST'
?. authenticated.inbound-request
:_ state
%- send
[302 ~ [%login-redirect './apps/feature']]
?~ body.request.inbound-request
[(send [405 ~ [%stock ~]]) state]
=/ json (de-json:html q.u.body.request.inbound-request)
=/ action (dejs-action +.json)
(handle-action action)
::
%'GET'
?+ site
:_ state
(send [404 ~ [%plain "404 - Not Found"]])
::
[%apps %feature %public ~]
:_ state
%- send
:+ 200 ~
:- %html page
::
[%apps %feature ~]
?. authenticated.inbound-request
:_ state
%- send
[302 ~ [%login-redirect './apps/feature']]
:_ state
%- send
:+ 200 ~
:- %html feature-ui
==
==
::
++ dejs-action
=, dejs:format
|= jon=json
^- action:feature
%. jon
%- of
:~ new-page+so
==
::
++ handle-action
|= =action:feature
^- (quip card _state)
?- -.action
%new-page
?> =(src.bowl our.bowl)
`state(page html:action)
==
--
++ on-peek on-peek:def
++ on-watch
|= =path
^- (quip card _this)
?+ path (on-watch:def path)
[%http-response *]
`this
==
::
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--
```
### Pokes
`++on-poke` only responds to `%handle-http-request`, which is dealt with in a `|^` barket core.
The most interesting part of the whole app is the `++handle-http` arm:
```hoon {% mode="collapse" copy=true %}
++ handle-http
|= [eyre-id=@ta =inbound-request:eyre]
^- (quip card _state)
=/ ,request-line:server
(parse-request-line:server url.request.inbound-request)
=+ send=(cury response:schooner eyre-id)
::
?+ method.request.inbound-request
[(send [405 ~ [%stock ~]]) state]
::
%'POST'
?. authenticated.inbound-request
:_ state
%- send
[302 ~ [%login-redirect './apps/feature']]
?~ body.request.inbound-request
[(send [405 ~ [%stock ~]]) state]
=/ json (de-json:html q.u.body.request.inbound-request)
=/ action (dejs-action +.json)
(handle-action action)
::
%'GET'
?+ site
:_ state
(send [404 ~ [%plain "404 - Not Found"]])
::
[%apps %feature %public ~]
:_ state
%- send
:+ 200 ~
:- %html page
::
[%apps %feature ~]
?. authenticated.inbound-request
:_ state
%- send
[302 ~ [%login-redirect './apps/feature']]
:_ state
%- send
:+ 200 ~
:- %html feature-ui
::
:: [%apps %feature %state ~]
:: :_ state
:: %- send
:: :+ 200 ~
:: [%json (enjs-state +.state)]
==
==
```
This arm uses the `server` library and `schooner` to produce a response of a server state and associated data. HTTP requests to `/app/feature` are checked for login authentication, while `/app/feature/public` is not.
### `POST`
In response to a `POST` request, the default page in the state can be changed. This is the only state change supported by the agent.
### `GET`
A `GET` request defaults to a `404` error.
- `/apps/feature/public` returns `200` success and the default page in the state.
- `/apps/feature` returns `200` success and the target page, statically compiled on agent build.
### `/lib/schooner`
The [Schooner library](https://github.com/dalten-collective/schooner/) simplifies raw HTTP handling for Gall agents, in particular for MIME returns.

View File

@ -1,13 +1,15 @@
+++
title = "Serving a Browser Game"
weight = 11
title = "`%flap`"
weight = 60
+++
## Introduction
# `%flap`
## Introduction
In this tutorial, we will take an off-the-shelf JavaScript game which runs in the browser and connect it to an Urbit back-end. This page assumes that you have completed some version of Hoon School and App School, whether the [live courses](/courses) or the [written docs](/guides/core/hoon-school/A-intro). Our goal is to show you one way of directly serving client code from an Urbit ship as server.
_Flappy Bird_ is an "insanely irritating, difficult and frustrating game which combines a super-steep difficulty curve with bad, boring graphics and jerky movement" ([Huffington Post](https://web.archive.org/web/20140205084251/http://www.huffingtonpost.com/2014/02/03/flappy-bird-tips_n_4717406.html)). We are going to implement `%flap`, a _Flappy Bird_ leaderboard using `%pals`. The approach given in this tutorial will apply to any game which is primarily run in the browser and has some persistent state to retain across sessions or communicate between players at discrete intervals. Direct player-v.-player games will require other techniques to implement.
_Flappy Bird_ is an "insanely irritating, difficult and frustrating game which combines a super-steep difficulty curve with bad, boring graphics and jerky movement" ([Huffington Post](https://web.archive.org/web/20140205084251/http://www.huffingtonpost.com/2014/02/03/flappy-bird-tips_n_4717406.html)). We are going to implement `%flap`, a _Flappy Bird_ leaderboard using ~paldevs `%pals` peer tracking agent. The approach given in this tutorial will apply to any game which is primarily run in the browser and has some persistent state to retain across sessions or communicate between players at discrete intervals. Direct player-v.-player games will require other techniques to implement.
Our objective is to illustrate a minimum viable set of changes necessary to implement the task. We should the following components when complete:
@ -18,7 +20,7 @@ Our objective is to illustrate a minimum viable set of changes necessary to impl
We will conceive of this app's communications structure as consisting of a _vertical_ component (which is the communication between the client in the browser and the Urbit ship as database) and a _horizontal_ component (which is the communication between Urbit peers). Vertical communication will take place using JSON via the `%flap-action` mark, while horizontal communication will take place using the `%flap-update` mark. Apps can achieve good data modularity using this separation.
![](vert-horz.svg)
![](https://storage.googleapis.com/media.urbit.org/developers/vert-horz.svg)
## Desk Setup

View File

@ -0,0 +1,132 @@
+++
title = "Gleichniszahlenreihe"
weight = 30
+++
# Challenge: The Look-and-Say Sequence
_Gleichniszahlenreihe_, or the [look-and-say sequence](https://en.wikipedia.org/wiki/Look-and-say_sequence), is constructed from an aural description of a sequence of numbers.
Consider the sequence of numbers that begins with `1, 11, 21, 1211, 111221, 312211, 13112221, ...`. Each number in the sequence represents what would result if the digits in the preceding value were counted and spoken aloud. For instance, "1" yields "one 1 → 11"; "11" yields "two 1s → 21"; "21" yields "one 2, one 1 → 1211", and so forth. The next number in the sequence after "13112221" is thus "one 1, one 3, two 1s, three 2s, one 1 → 1113213211".
This is a fairly complicated program. You need a few parts: the ability to take a tape and parse it into components, the ability to count components, and the ability to produce a new tape. Then a recursing bit to produce a list of these values and (ultimately) return the last one. Think about the Caesar cipher's structure.
- Compose a `%say` generator which carries out the look-and-say sequence calculation for a given input. The input should be a number which indicates which value in the sequence is desired (e.g. 1→1, 2→11, 3→21).
## Solutions
_These solutions were submitted by the Urbit community as part of the Hoon School Live ~2022.2 cohort. They are made available under both the [MIT license](https://mit-license.org/) and the [CC0 license](https://creativecommons.org/share-your-work/public-domain/cc0). We ask you to acknowledge authorship should you utilize these elsewhere._
### Solution #1
_This solution was produced by ~midsum-salrux. This code exhibits good core structure and code encapsulation in arms._
**`/gen/look-and-say.hoon`**
```hoon
:- %say
|= [* [n=@ud ~] *]
:- %noun
=< (compute-sequence n)
|%
+$ counted-digit [count=@ud digit=@t]
++ compute-sequence
|= n=@ud
^- tape
=/ sequence "1"
|-
?: =(n 1)
sequence
$(sequence (progress sequence), n (dec n))
++ progress
|= sequence=tape
^- tape
(speak (count-digits sequence))
++ speak
|= cd=(list counted-digit)
^- tape
(zing (turn cd |=(d=counted-digit ~[(crip ~(rud at count.d)) digit.d])))
++ count-digits
|= sequence=tape
^- (list counted-digit)
(scan sequence several-repeated-digits)
++ several-repeated-digits (plus (cook unreap many-same-digit))
++ unreap
|= a=tape
^- counted-digit
[(lent a) (snag 0 a)]
++ many-same-digit
;~ pose
(many-particular-digit '1')
(many-particular-digit '2')
(many-particular-digit '3')
(many-particular-digit '4')
(many-particular-digit '5')
(many-particular-digit '6')
(many-particular-digit '7')
(many-particular-digit '8')
(many-particular-digit '9')
==
++ many-particular-digit (corl plus just)
--
```
Usage:
```hoon
> +look-and-say 1
"1"
> +look-and-say 2
"11"
> +look-and-say 5
"111221"
> +look-and-say 10
"13211311123113112211"
> +look-and-say 20
"11131221131211132221232112111312111213111213211231132132211211131221131211221321123113213221123113112221131112311332211211131221131211132211121312211231131112311211232221121321132132211331121321231231121113112221121321133112132112312321123113112221121113122113121113123112112322111213211322211312113211"
```
### Solution #2
_This solution was produced by ~nallux-dozryl. This code exemplifies parsimonious use of parsing rules and can parse any arbitrary sequence of digits._
**`/gen/look-and-say.hoon`**
```hoon
:- %say
|= [* [in=tape ~] ~]
:- %noun
^- tape
=| final=tape
|-
?~ in final
=+ nums=`tape`(scan in (star nud))
=+ slot=(head nums)
=+ parsed=((star (just slot)) [[1 1] nums])
=+ count=(scow %ud (dec (tail (head (tail (need (tail parsed)))))))
=+ return=:(weld final count (trip slot))
=+ newin=(tail (tail (need (tail parsed))))
$(final return, in newin)
```
Usage:
```hoon
> +look-and-say "12"
"1112"
> +look-and-say "123"
"111213"
> +look-and-say "1234"
"11121314"
> +look-and-say "123455"
"1112131425"
```