Skip to content

Instantly share code, notes, and snippets.

@Quodss
Last active June 2, 2025 12:12
Show Gist options
  • Save Quodss/48a0137f387061bf18331d06f10172da to your computer and use it in GitHub Desktop.
Save Quodss/48a0137f387061bf18331d06f10172da to your computer and use it in GitHub Desktop.

JS tlon-hooks documentation

hook-builder-js.hoon library allows to run Tlon hooks written in JS. This document describes the API for it.

Imports and exports

The interaction with the host environment is done in pseudo-CommonJS style: import Tlon hooks library by calling require("tlon_hooks"). No other packages are available in the hook-builder-js environment: bundle dependencies into a single file with JS bundlers if necessary.

hook-builder-js expects that the provided JS code will export a hook function to module.exports. Here is an example of a JS hook that passes through an event and releases no effects.

const hooks = require("tlon_hooks");

module.exports = (event) => {
    return {event: {allowed: event}, effects: []};
}

Hook function type signature

The exported function will be called with a single JS value as a parameter with the type Event defined below:

(note that some property names contain hyphens and need to be addressed accordingly)

/**
 * @typedef {
 *      {diary: {title: string, image: string}}
 *      | {heap: string | null}
 *      | {chat: null | {notice: null}}
 * } KindData
 */

/**
 * @typedef {
 *      string
 *      | {italics: Inline[]}
 *      | {bold: Inline[]}
 *      | {strike: Inline[]}
 *      | {blockquote: Inline[]}
 *      | {ship: string}
 *      | {"inline-code": string}
 *      | {code: string}
 *      | {tag: string}
 *      | {break: null}
 *      | {block: {index: number, text: string}}
 *      | {link: {href: string, content: string}}
 *      | {task: {checked: boolean, content: Inline[]}}
 * } Inline
 */

/**
 * @typedef {
 *      {list: {type: string, items: Listing[], contents: Inline[]}}
 *      | {item: Inline[]}
 * } Listing
 */

/**
 * @typedef {
 *      {group: string}
 *      | {desk: {flag: string, where: string}}
 *      | {chan: {nest: string, where: string}}
 *      | {bait: {group: string, graph: string, where: string}}
 * } Cite
 */

/**
 * @typedef {
 *      {rule: null}
 *      | {cite: Cite}
 *      | {listing: Listing}
 *      | {code: {code: string, lang: string}}
 *      | {header: {tag: string, content: Inline[]}}
 *      | {image: {src: string, height: number, width: number, alt: string}}
 * } Block
 */

/**
 * @typedef {{block: Block} | {inline: Inline[]}} Verse
 */

/**
 * @typedef {Verse[]} Story
 */

/**
 * @typedef {{
 *      content: Story,
 *      author: number,
 *      sent: number,
 *      "kind-data": KindData
 * }} Essay
 */

/**
 * @typedef {
 *      Record<string, {revision: string, react: null | string}>
 * } VReacts
 * @description Record key is a ship, react string is emoji shortcode
 */

/**
 * @typedef {{
 *      id: string,
 *      reacts: VReacts,
 *      revision: string,
 *      content: Story,
 *      author: number,
 *      sent: number
 * }} VReply
 * @description `id` is a unique identifier
 */

/**
 * @typedef {{
 *      id: string,
 *      replies: Record<string, VReply | null>,
 *      reacts:  VReacts
 * }} Seal
 * @description `id` is a unique identifier
 * @description Record key in Seal.replies is a unique identifier
 */

/**
 * @typedef { {seal: Seal, revision: string, essay: Essay} } VPost
 */

/**
 * @typedef {
 *      {add: VPost}
 *      | {edit: {original: VPost, essay: Essay}}
 *      | {del: VPost}
 *      | {react: {post: VPost, ship: number, react: null | string}}
 * } OnPost
 */

/**
 * @typedef {{content: Story, author: number, sent: number}} Memo
 */

/**
 * @typedef {
 *      {add: {parent: VPost, reply: VReply}}
 *      | {edit: {parent: VPost, original: VReply, memo: Memo}}
 *      | {del: {parent: VPost, original: VReply}}
 *      | {react:
 *          {
 *              parent: VPost,
 *              reply: VReply,
 *              ship: number,
 *              react: null | string
 *          }
 *        }
 * } OnReply
 */

/**
 * @typedef {{
 *      id: number,
 *      hook: number,
 *      data: number,
 *      "fires-at": number
 * }} WaitingHook
 */

/**
 * @typedef {
 *      {"on-post": OnPost}
 *      | {"on-reply": OnReply}
 *      | {cron: null}
 *      | {wake: WaitingHook}
 * } Event
 */

The return value of the function must be {event: EventResult, effects: Effect[]}:

/**
 * @typedef {
 *      {allowed: Event} | {denied: null | string}
 * } EventResult
 */

/**
 * @typedef {{
 *      kind: string,
 *      name: string,
 *      group: string,
 *      title: string,
 *      description: string,
 *      readers: string[],
 *      writers: string[]
 * }} CreateChannel
 */

/**
 * @typedef {
 *      {add: Memo}
 *      | {del: string}
 *      | {edit: {id: string, memo: Memo}}
 *      | {"add-react": {id: string, ship: string, react: string}}
 *      | {"del-react": {id: string, ship: string}}
 * } ActionReply
 */

/**
 * @typedef {
 *      {add: Essay}
 *      | {edit: {id: string, essay: Essay}}
 *      | {del: string}
 *      | {reply: {id: string, action: ActionReply}}
 *      | {"add-react": {id: string, ship: string, react: string}}
 *      | {"del-react": {id: string, ship: string}}
 * } ActionPost
 */

/**
 * @typedef {
 *      {join: string}
 *      | {leave: null}
 *      | {read: null}
 *      | {"read-at": string}
 *      | {watch: null}
 *      | {unwatch: null}
 *      | {post: ActionPost}
 *      | {view: string}
 *      | {sort: string}
 *      | {order: null | string[]}
 *      | {"add-writers": string[]}
 *      | {"del-writers": string[]}
 * } ActionChannel
 */

/**
 * @typedef {
 *      {hide: string} | {show: string}
 * } PostToggle
 */

/**
 * @typedef {
 *      {create: CreateChannel}
 *      | {pin: string[]}
 *      | {channel: {nest: string, action: ActionChannel}}
 *      | {"toggle-post": PostToggle}
 * } ActionChannels
 */

/**
 * @typedef {
 *      {add: null}
 *      | {del: null}
 *      | {"add-sects": string[]}
 *      | {"del-sects": string[]}
 * } FleetDiff
 */

/**
 * @typedef {{
 *      title: string,
 *      description: string,
 *      image: string,
 *      cover: string
 * }} Meta
 */

/**
 * @typedef {{
 *      meta: Meta,
 *      added: number,
 *      zone: string,
 *      join: boolean,
 *      readers: string[]
 * }} Channel
 */

/**
 * @typedef {
 *      {add: Channel}
 *      | {edit: Channel}
 *      | {del: null}
 *      | {"add-sects": string[]}
 *      | {"del-sects": string[]}
 *      | {zone: string}
 *      | {join: boolean}
 * } ChannelDiff
 */

/**
 * @typedef {
 *      {add: Meta}
 *      | {edit: Meta}
 *      | {del: null}
 * } CabalDiff
 */

/**
 * @typedef {
 *      {add: string[]}
 *      | {del: string[]}
 * } BlocDiff
 */

/**
 * @typedef {
 *      {"add-ships": number[]}
 *      | {"del-ships": number[]}
 *      | {"add-ranks": string[]}
 *      | {"del-ranks": string[]}
 * } OpenCordonDiff
 */

/**
 * @typedef {
 *      {"add-ships": {kind: string, ships: number[]}}
 *      | {"del-ships": {kind: string, ships: number[]}}
 * } ShutCordonDiff
 */

/**
 * @typedef {
 *      {open: {ships: number[], ranks: string[]}}
 *      | {shut: {pending: number[], ask: number[]}}
 *      | {afar: {app: string, path: string, desc: string}}
 * } Cordon
 */

/**
 * @typedef {
 *      {open: OpenCordonDiff}
 *      | {shut: ShutCordonDiff}
 *      | {swap: Cordon}
 * } CordonDiff
 */

/**
 * @typedef { Record<string, {sects: string[], joined: number}> } Fleet
 * @description Record key is a ship
 */

/**
 * @typedef { Record<string, {meta: Meta}> } Cabals
 * @description Record key is a role
 */

/**
 * @typedef { Record<string, {meta: Meta, idx: string[]}> } Zones
 * @description Record key is a zone id
 */

/**
 * @typedef { Record<string, Channel> } Channels
 * @description Record key is a nest
 */

/**
 * @typedef {
 *      Record<
 *          string,
 *          Record<
 *              string,
 *              {
 *                  flagged: boolean,
 *                  flaggers: string[],
 *                  replies: Record<string, string[]>
 *              }
 *          >
 *      >
 * } FlaggedContent
 */

/**
 * @typedef {{
 *      fleet: Fleet,
 *      cabals: Cabals,
 *      zones: Zones,
 *      "zone-ord": string[],
 *      channels: Channels,
 *      "active-channels": string[],
 *      bloc: string[],
 *      cordon: Cordon,
 *      meta: Meta,
 *      secret: boolean,
 *      "flagged-content": FlaggedContent
 *  }} Group
 */

/**
 * @typedef {
 *      {add: Meta}
 *      | {edit: Meta}
 *      | {del: null}
 *      | {mov: number}
 *      | {"mov-nest": {nest: string, idx: number}}
 * } ZoneDelta
 */

/**
 * @typedef {{
 *      zone: string,
 *      delta: ZoneDelta
 * }} ZoneDiff
 */

/**
 * @typedef {{
 *      nest: string,
 *      "post-key": {post: string, reply: null | string},
 *      src: strin
 * }} FlagContent
 */

/**
 * @typedef {
 *      {fleet: {ships: string[], diff: FleetDiff}}
 *      | {channel: {nest: string, diff: ChannelDiff}}
 *      | {cabal: {sect: string, diff: CabalDiff}}
 *      | {bloc: BlocDiff}
 *      | {cordon: CordonDiff}
 *      | {create: Group}
 *      | {zone: ZoneDiff}
 *      | {meta: Meta}
 *      | {secret: boolean}
 *      | {del: null}
 *      | {"flag-content": FlagContent}
 * } DiffGroups
 */

/**
 * @typedef {{
 *      flag: string,
 *      update: {time: string, diff: DiffGroups}
 * }} ActionGroups
 */


/**
 * @typedef {{ id: string, time: string }} MsgKey
 */

/**
 * @typedef {{
 *      key: MsgKey,
 *      channel: string,
 *      group: string,
 *      content: Story,
 *      mention: boolean
 * }} PostEvent
 */


/**
 * @typedef {{
 *      key: MsgKey,
 *      parent: MsgKey,
 *      channel: string,
 *      group: string,
 *      content: Story,
 *      mention: boolean
 * }} ReplyEvent
 */

/**
 * @typedef {{ channel: string, group: string }} ChanInitEvent
 */

/**
 * @typedef { {ship: string} | {club: string} } Whom
 */

/**
 * @typedef {{
 *      key: MsgKey,
 *      whom: Whom,
 *      content: Story,
 *      mention: boolean
 * }} DM_PostEvent
 */

/**
 * @typedef {{
 *      key: MsgKey,
 *      parent: MsgKey,
 *      whom: Whom,
 *      content: Story,
 *      mention: boolean
 * }} DM_ReplyEvent
 */

/**
 * @typedef {{
 *      key: MsgKey,
 *      channel: string,
 *      group: string
 * }} FlagPostEvent
 */

/**
 * @typedef {{
 *      key: MsgKey,
 *      parent: MsgKey,
 *      channel: string,
 *      group: string
 * }} FlagReplyEvent
 */

/**
 * @typedef {{
 *      group: string,
 *      ship: string
 * }} GroupEvent
 */

/**
 * @typedef {{
 *      group: string,
 *      ship: string,
 *      roles: string[]
 * }} GroupRoleEvent
 */

/**
 * @typedef {
 *      {post: PostEvent}
 *      | {reply: ReplyEvent}
 *      | {"chan-init": ChanInitEvent}
 *      | {"dm-invite": Whom}
 *      | {"dm-post": DM_PostEvent}
 *      | {"dm-reply": DM_ReplyEvent}
 *      | {"flag-post": FlagPostEvent}
 *      | {"flag-reply": FlagReplyEvent}
 *      | {"group-ask": GroupEvent}
 *      | {"group-join": GroupEvent}
 *      | {"group-kick": GroupEvent}
 *      | {"group-invite": GroupEvent}
 *      | {"group-role": GroupRoleEvent}
 * } IncomingEvent
 */

/**
 * @typedef {
 *      {base: null}
 *      | {group: string}
 *      | {dm: Whom}
 *      | {contact: string}
 *      | {channel: {nest: string, group: string}}
 *      | {thread: {key: MsgKey, channel: string, group: string}}
 *      | {"dm-thread": {key: MsgKey, whom: Whom}}
 * } ActivitySource
 */

/**
 * @typedef {
 *      {item: string}
 *      | {all: {time: null | string, deep: boolean | undefined}}
 *      | {event: IncomingEvent}
 * } ReadAction
 */

/**
 * @typedef { Record<string, {unreads: boolean, notify: boolean}> } VolumeMap
 */

/**
 * @typedef {
 *      {add: IncomingEvent}
 *      | {del: ActivitySource}
 *      | {read: {source: ActivitySource, action: ReadAction}}
 *      | {adjust: {source: ActivitySource, volume: null| VolumeMap}}
 *      | {"allow-notifications": string}
 * } ActionActivity
 */

/**
 * @typedef {{
 *      id: string,
 *      meta: null 
 *      delta: {del: null}
 *             | {"add-react": {ship: string, react: string}}
 *             | {"del-react": string}
 *             | {add: {memo: Memo, time: null | string}}
 * }} ReplyDelta
 */

/**
 * @typedef {
 *      {del: null}
 *      | {"add-react": {ship: string, react: string}}
 *      | {"del-react": number}
 *      | {reply: ReplyDelta}
 *      | {add: {
 *                  memo: Memo,
 *                  kind: null | {notice: null},
 *                  time: null | string
 *              }
 *        }
 * } WritsDelta
 */

/**
 * @typedef {{
 *      ship: string,
 *      diff: {id: string, delta: WritsDelta}
 * }} ActionDM
 */


/**
 * @typedef {
 *      {writ: {id: string, delta: WritsDelta}}
 *      | {meta: Meta}
 *      | {team: {ship: string, ok: boolean}}
 *      | {hive: {by: number, for: number, add: boolean}}
 *      | {init: {team: number[], hive: number[], meta: Meta}}
 * } ClubDelta
 */

/**
 * @typedef {
 *      null
 *      | {text: string}
 *      | {numb: number}
 *      | {date: number}
 *      | {tint: number}
 *      | {ship: string}
 *      | {look: string}
 *      | {flag: string}
 *      | {set: ValueContact[]}
 * } ValueContact
 */

/**
 * @typedef { {ship: string} | {id: number} } Kip
 */

/**
 * @typedef { Record<string, ValueContact> } Contact
 */

/**
 * @typedef {
 *      {anon: null}
 *      | {self: Contact}
 *      | {page: {kip: Kip, contact: Contact}}
 *      | {edit: {kip: Kip, contact: Contact}}
 *      | {wipe: Kip[]}
 *      | {meet: string[]}
 *      | {drop: string[]}
 *      | {snub: string[]}
 * } ActionContacts
 */

/**
 * @typedef {
 *      {channels: ActionChannels}
 *      | {groups: ActionGroups}
 *      | {activity: ActionActivity}
 *      | {dm: ActionDM}
 *      | {club: {id: string, diff: {uid: string, delta: ClubDelta}}}
 *      | {contacts: ActionContacts}
 *      | {wait: WaitingHook}
 * } Effect
 */

tlon_hooks library

State saving and loading

These functions are used to get and set the mutable state of the hook.

/**
 * @typedef {() => Object} get_state
 * @description Fetches JSON-encoded hook state. Initial value of the state is `null`
 */

/**
 * @typedef {(state: Object) => 0} set_state
 * @description Saves an object as new hook state
 */

Helper functions to load various data

These functions are used as a shorthand to fetch commonly used data.

/**
 * @typedef {() => VPost[]} get_chat_messages_here
 * @description Get a list of all chat messages in the channel where the hook is bound.
 */

/**
 * @typedef {() => string[]} get_members_here
 * @description Get a list of all users in the current group
 */

/**
 * @typedef {(ship: number | string) => string[]} get_roles
 * @description Get a list of roles of a given user
 */

/**
 * @typedef {(ship: number | string) => string | null} ship_normalize
 * @description Tries to normalize ship encoding to a string with `~` prefix. Returns a string-encoded @p or null on failure.
 */

/**
 * @typedef {(text: string) => 0} print
 * @description Prints a string to Dojo with ~&. Use it instead of console.log as console is not present.
 */

Effect builders

These functions return Effect objects and can be used as an alias for common operations

/**
 * @typedef {(ship: number | string) => Effect} effects.add_user
 * @description Constructs an Effect object to add a ship to the group
 */

/**
 * @typedef {(ship: number | string) => Effect} effects.kick_user
 * @description Constructs an Effect object to kick a ship from the group
 */

/**
 * @typedef {(ship: number | string, role: string) => Effect} effects.give_role
 * @description Constructs an Effect object to assign a role to a user
 */

/**
 * @typedef {(ship: number | string, role: string) => Effect} effects.remove_role
 * @description Constructs an Effect object to remove a role to a user
 */

/**
 * @typedef {(text: string) => Effect} effects.post_here
 * @description Constructs an Effect object to post a message to a channel
 */

/**
 * @typedef {(ship: number | string, text: string) => Effect} effects.send_dm
 * @description Constructs an Effect object to send a DM to a ship
 */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment