hook-builder-js.hoon library allows to run Tlon hooks written in JS. This document describes the API for it.
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: []};
}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
*/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
*/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.
*/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
*/