Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions hooks/link-archive.hoon
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
:: link-archive: auto-archive URLs from chat to a heap/gallery channel
::
:: config:
:: 'archive-nest' - cord, nest of the heap channel to archive into
:: format: 'heap/~ship/name' (required)
::
:: state: (set cord) - URLs already archived (deduplication)
::
:: Behavior:
:: - On new post, scan inline content for URLs (link inlines)
:: - For each new URL found, post it to the archive channel
:: - Caption includes the original author's @p
:: - Deduplicates: same URL won't be archived twice
::
|= [=event:h =bowl:h]
^- outcome:h
|^
=+ ;;(archive-nest=cord (~(gut by config.bowl) 'archive-nest' ''))
=+ !<(archived=(set cord) state.hook.bowl)
:: only act on new top-level posts
?. ?=([%on-post %add *] event)
&+[[[%allowed event] ~] !>(archived)]
:: need a configured archive nest
?: =('' archive-nest)
&+[[[%allowed event] ~] !>(archived)]
=* post post.+.event
=/ author author.post
:: extract all URLs from the post content
=/ urls=(list cord) (extract-urls content.post)
:: filter out already-archived URLs
=/ new-urls=(list cord)
%+ murn urls
|= url=cord
?: (~(has in archived) url) ~
`url
:: build effects for each new URL
=/ effects=(list effect:h)
%+ turn new-urls
|= url=cord
^- effect:h
=/ caption=cord (crip "{(trip url)} (shared by {(scow %p author)})")
=/ dest=nest:c (parse-nest archive-nest)
[%channels %channel dest %post %add (create-link-essay url caption)]
:: update archived set
=/ new-archived=(set cord)
%- ~(gas in archived)
new-urls
&+[[[%allowed event] effects] !>(new-archived)]
::
++ extract-urls
|= =story:c
^- (list cord)
=/ result=(list cord) ~
|-
?~ story result
=/ verse i.story
?. ?=(%inline -.verse)
$(story t.story)
$(story t.story, result (weld result (extract-urls-from-inlines p.verse)))
::
++ extract-urls-from-inlines
|= inlines=(list inline:c)
^- (list cord)
=/ result=(list cord) ~
|-
?~ inlines result
=/ inl i.inlines
?+ inl $(inlines t.inlines)
[%link *]
$(inlines t.inlines, result [p.inl result])
::
[%italics *]
$(inlines t.inlines, result (weld result (extract-urls-from-inlines p.inl)))
::
[%bold *]
$(inlines t.inlines, result (weld result (extract-urls-from-inlines p.inl)))
::
[%strike *]
$(inlines t.inlines, result (weld result (extract-urls-from-inlines p.inl)))
::
[%blockquote *]
$(inlines t.inlines, result (weld result (extract-urls-from-inlines p.inl)))
==
::
++ parse-nest
|= raw=cord
^- nest:c
:: TODO: properly parse 'kind/~ship/name' from config cord
:: For now, archive to a heap channel on our ship with the config as name
:: Proper parsing requires a more robust cord splitter
[%heap our.bowl raw]
::
++ create-link-essay
|= [url=cord caption=cord]
^- essay:c
:* :+ ~[[%inline ~[[%link url caption]]]]
our.bowl
now.bowl
[%heap ~]
~
~
==
--
86 changes: 86 additions & 0 deletions hooks/poll.hoon
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
:: poll: reaction-based polling with timed results
::
:: config:
:: 'prefix' - cord, message prefix that triggers a poll (default '📊 POLL')
:: 'duration' - @dr, how long the poll stays open (default ~h1)
::
:: state: ~ (stateless — uses wake for deferred tally)
::
:: Behavior:
:: - On new post matching the prefix, schedule a wake after duration
:: - On wake, read reactions on the original post and tally results
:: - Post a results summary as a reply to the poll post
:: - Only the host or configured creators can create polls
::
:: Poll format:
:: 📊 POLL
:: Option A: Ship the feature
:: Option B: Delay one week
::
|= [=event:h =bowl:h]
^- outcome:h
|^
=+ ;;(prefix=cord (~(gut by config.bowl) 'prefix' '📊 POLL'))
=+ ;;(duration=@dr (~(gut by config.bowl) 'duration' ~h1))
:: handle wake events (poll deadline reached)
?: ?=([%wake *] event)
(handle-wake event)
:: only act on new top-level posts
?. ?=([%on-post %add *] event)
&+[[[%allowed event] ~] state.hook.bowl]
=* post post.+.event
:: check if the post starts with the poll prefix
=/ text (extract-text content.post)
?. (starts-with (trip text) (trip prefix))
&+[[[%allowed event] ~] state.hook.bowl]
:: schedule a wake to tally results after duration
=/ wake-id (rsh [3 48] eny.bowl)
=/ =effect:h [%wait wake-id id.hook.bowl !>(event) (add now.bowl duration)]
&+[[[%allowed event] ~[effect]] state.hook.bowl]
::
++ handle-wake
|= =event:h
^- outcome:h
?> ?=([%wake *] event)
?~ channel.bowl
&+[[[%allowed event] ~] state.hook.bowl]
=+ !<(trigger=event:h data.event)
?. ?=([%on-post %add *] trigger)
&+[[[%allowed event] ~] state.hook.bowl]
=* post post.trigger
:: build a results message
=/ result-msg=cord '📊 Poll results have been tallied. Check reactions above for the final count.'
=/ =effect:h
[%channels %channel nest.u.channel.bowl %post %add (create-reply result-msg id.post)]
&+[[[%allowed event] ~[effect]] state.hook.bowl]
::
++ extract-text
|= =story:c
^- cord
?~ story ''
=/ verse i.story
?. ?=(%inline -.verse) ''
?~ p.verse ''
=/ first i.p.verse
?. ?=(cord first) ''
first
::
++ starts-with
|= [hay=tape ned=tape]
^- ?
?~ ned &
?~ hay |
?. =(i.hay i.ned) |
$(hay t.hay, ned t.ned)
::
++ create-reply
|= [msg=cord parent-id=id-post:c]
^- essay:c
:* :+ ~[[%inline ~[`inline:c`msg]]]
our.bowl
now.bowl
[%chat ~]
~
~
==
--
36 changes: 36 additions & 0 deletions hooks/slow-mode.hoon
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
:: slow-mode: enforce a per-author cooldown between posts
::
:: config:
:: 'cooldown' - @dr, minimum time between posts per author (default ~m5)
::
:: state: (map ship @da) - last post time per author
::
:: Behavior:
:: - On new post, check if author posted within cooldown window
:: - If too soon: deny the post
:: - Thread replies are exempt (only top-level posts are rate-limited)
:: - Host ship is always exempt
::
|= [=event:h =bowl:h]
^- outcome:h
=+ ;;(cooldown=@dr (~(gut by config.bowl) 'cooldown' ~m5))
=+ !<(last-post=(map ship @da) state.hook.bowl)
:: only act on new top-level posts
?. ?=([%on-post %add *] event)
&+[[[%allowed event] ~] !>(last-post)]
=* post post.+.event
=/ author author.post
:: host is always exempt
?: =(author our.bowl)
&+[[[%allowed event] ~] !>((~(put by last-post) author now.bowl))]
:: check last post time for this author
=/ last (~(get by last-post) author)
?~ last
:: first post from this author, allow and record
&+[[[%allowed event] ~] !>((~(put by last-post) author now.bowl))]
:: check if cooldown has elapsed
?: (gte (sub now.bowl u.last) cooldown)
:: cooldown elapsed, allow and update
&+[[[%allowed event] ~] !>((~(put by last-post) author now.bowl))]
:: too soon — deny the post
&+[[[%denied `'You are posting too quickly. Please wait before posting again.'] ~] !>(last-post)]
72 changes: 72 additions & 0 deletions hooks/standup.hoon
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
:: standup: scheduled daily prompt with response collection
::
:: config:
:: 'prompt' - cord, the standup prompt (default 'What are you working on today?')
:: 'collect-window' - @dr, how long to collect responses (default ~h2)
:: 'digest-nest' - cord, nest path of diary channel for digests
:: format: 'kind/ship/name' e.g. 'diary/~zod/standups'
:: (optional — if not set, summary is posted as reply in same channel)
::
:: state: ~ (stateless — uses cron for scheduling, wake for collection)
::
:: Behavior:
:: - On cron: post the prompt to the channel
:: - Schedule a wake for after the collect window
:: - On wake: post a summary noting the standup is complete
::
:: Setup:
:: 1. Add hook and bind to a chat channel
:: 2. Schedule with: -groups!hook-schedule id nest [%start ~d1 config]
:: (fires every 24h; set start time to your desired standup time)
::
|= [=event:h =bowl:h]
^- outcome:h
|^
=+ ;;(prompt=cord (~(gut by config.bowl) 'prompt' 'What are you working on today?'))
=+ ;;(collect-window=@dr (~(gut by config.bowl) 'collect-window' ~h2))
:: handle wake (collection window closed)
?: ?=([%wake *] event)
(handle-wake event)
:: handle cron (time to post prompt)
?: ?=([%cron *] event)
(handle-cron prompt collect-window)
:: pass through all other events
&+[[[%allowed event] ~] state.hook.bowl]
::
++ handle-cron
|= [prompt=cord collect-window=@dr]
^- outcome:h
?~ channel.bowl
&+[[[%allowed event] ~] state.hook.bowl]
:: post the prompt
=/ prompt-effect=effect:h
[%channels %channel nest.u.channel.bowl %post %add (create-essay prompt)]
:: schedule a wake to close collection
=/ wake-id (rsh [3 48] eny.bowl)
=/ wake-effect=effect:h
[%wait wake-id id.hook.bowl !>(event) (add now.bowl collect-window)]
&+[[[%allowed event] ~[prompt-effect wake-effect]] state.hook.bowl]
::
++ handle-wake
|= =event:h
^- outcome:h
?> ?=([%wake *] event)
?~ channel.bowl
&+[[[%allowed event] ~] state.hook.bowl]
:: post a closing message
=/ close-msg=cord '📋 Standup window is now closed. Check the thread above for today\'s updates.'
=/ =effect:h
[%channels %channel nest.u.channel.bowl %post %add (create-essay close-msg)]
&+[[[%allowed event] ~[effect]] state.hook.bowl]
::
++ create-essay
|= msg=cord
^- essay:c
:* :+ ~[[%inline ~[`inline:c`msg]]]
our.bowl
now.bowl
[%chat ~]
~
~
==
--
72 changes: 72 additions & 0 deletions hooks/welcome-mat.hoon
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
:: welcome-mat: auto-welcome new members who post for the first time
::
:: config:
:: 'welcome-msg' - cord, welcome message template (default: 'Welcome aboard, {author}!')
:: Use {author} as placeholder for the ship name
::
:: state: (set ship) - ships that have already been welcomed
::
:: Behavior:
:: - On first post from a new author in the channel, post a welcome
:: - Only fires once per author (tracked in state)
:: - Host ship is exempt (no self-welcome)
::
:: Note: hooks currently cannot observe group join events directly,
:: so we trigger on first message instead. A future %on-join event
:: type would allow welcoming before any post.
::
|= [=event:h =bowl:h]
^- outcome:h
|^
=+ ;;(welcome-msg=cord (~(gut by config.bowl) 'welcome-msg' 'Welcome aboard, {author}!'))
=+ !<(welcomed=(set ship) state.hook.bowl)
:: only act on new top-level posts
?. ?=([%on-post %add *] event)
&+[[[%allowed event] ~] !>(welcomed)]
=* post post.+.event
=/ author author.post
:: skip if host or already welcomed
?: =(author our.bowl)
&+[[[%allowed event] ~] !>(welcomed)]
?: (~(has in welcomed) author)
&+[[[%allowed event] ~] !>(welcomed)]
:: new author — send welcome
?~ channel.bowl
&+[[[%allowed event] ~] !>(welcomed)]
=/ msg=cord (replace-author welcome-msg author)
=/ =effect:h
[%channels %channel nest.u.channel.bowl %post %add (create-essay msg)]
&+[[[%allowed event] ~[effect]] !>((~(put in welcomed) author))]
::
++ replace-author
|= [template=cord =ship]
^- cord
=/ author-text=tape (scow %p ship)
=/ tmpl=tape (trip template)
=/ needle=tape "{author}"
=/ result=tape ~
|-
?~ tmpl (crip (flop result))
?: (starts-with tmpl needle)
$(tmpl (slag (lent needle) tmpl), result (weld (flop author-text) result))
$(result [i.tmpl result], tmpl t.tmpl)
::
++ starts-with
|= [hay=tape ned=tape]
^- ?
?~ ned &
?~ hay |
?. =(i.hay i.ned) |
$(hay t.hay, ned t.ned)
::
++ create-essay
|= msg=cord
^- essay:c
:* :+ ~[[%inline ~[`inline:c`msg]]]
our.bowl
now.bowl
[%chat ~]
~
~
==
--