Gamend.Hooks behaviour (gamend_core v1.0.1192)

Copy Markdown View Source

Behaviour for application-level hooks / callbacks.

Implement this behaviour to receive lifecycle events from core flows (registration, login, provider linking, deletion) and run custom logic.

A module implementing this behaviour can be configured with

config :gamend_core, :hooks_module, MyApp.HooksImpl

The default implementation is a no-op.

Summary

Lifecycle hooks

Handle a dynamically-exported RPC function.

KV hooks

Called before a KV get/2 is performed. Implementations should return one of these client KV API access decisions

Types

Options passed to hooks that accept an options map/keyword list.

A veto-only hook: {:error, reason} rejects, anything else allows. The return never rewrites the args, so a bare :ok is the usual "allow".

Functions

Call an arbitrary function exported by the configured hooks module.

When a hooks function is executed via call/3 or internal_call/3, an optional :caller can be provided in the options. The caller will be injected into the spawned task's process dictionary and is accessible via Gamend.Hooks.caller/0 (the raw value) or caller_id/0 (the numeric id when the value is a user struct or map containing :id).

Return the user struct for the current caller when available. This will attempt to resolve the caller via Gamend.Accounts.get_user!/1 when the caller is a user id or a map containing an :id key. Returns nil when no caller or user is found.

Return a list of exported functions on the currently registered hooks module.

Call an internal lifecycle callback. When a callback is missing this returns a sensible default (eg. {:ok, attrs} for before callbacks) so domain code doesn't need to handle missing hooks specially in most cases.

Returns the set of internal lifecycle hook names that are not callable through the public RPC interface.

Invoke a dynamic hook function by name.

Return the configured module that implements the hooks behaviour.

True when the hook transforms its input (a before_* pipeline hook) rather than fanning out notifications. Exposed for the admin runtime page.

Lifecycle hooks

after_startup()

(optional)
@callback after_startup() :: any()

before_stop()

(optional)
@callback before_stop() :: any()

on_custom_hook(t, list)

(optional)
@callback on_custom_hook(String.t(), list()) :: any()

Handle a dynamically-exported RPC function.

This callback is used for function names that were registered at runtime (eg. via a plugin's after_startup/0 return value) and therefore may not exist as exported Elixir functions on the hooks module.

Receives the function name and the argument list.

User hooks

after_user_deleted(t)

(optional)
@callback after_user_deleted(Gamend.Accounts.User.t()) :: any()

after_user_logged_in(t)

(optional)
@callback after_user_logged_in(Gamend.Accounts.User.t()) :: any()

after_user_muted(t)

(optional)
@callback after_user_muted(Gamend.Chat.Mute.t()) :: any()

after_user_offline(t)

(optional)
@callback after_user_offline(Gamend.Accounts.User.t()) :: any()

after_user_online(t)

(optional)
@callback after_user_online(Gamend.Accounts.User.t()) :: any()

after_user_register(t)

(optional)
@callback after_user_register(Gamend.Accounts.User.t()) :: any()

after_user_updated(t)

(optional)
@callback after_user_updated(Gamend.Accounts.User.t()) :: any()

before_user_register(t, user_registration_hook_attrs)

(optional)

Called before a new user row is inserted, on every registration path: email, device, and all OAuth providers (which register mid-login).

Receives the tentative user (not yet inserted, id is nil) and the registration attrs (string keys), which already contain the generated "username". Return {:ok, attrs} — possibly with a different username or other changes — or {:error, reason} to abort the registration.

Core re-validates after all hooks ran: format and uniqueness are not overridable. A hook-supplied username that is invalid or already taken is replaced with a generated one (a plugin bug must never lock a player out of login). For strict policy on player-initiated changes — profanity or reserved names — use before_user_update/2, where errors are returned to the player:

def before_user_update(_user, %{"username" => name} = attrs) do
  if MyGame.Profanity.allowed?(name),
    do: {:ok, attrs},
    else: {:error, :invalid_username}
end

def before_user_update(_user, attrs), do: {:ok, attrs}

before_user_update(t, map)

(optional)
@callback before_user_update(Gamend.Accounts.User.t(), map()) :: hook_result(map())

Lobby hooks

after_lobby_create(t)

(optional)
@callback after_lobby_create(Gamend.Lobbies.Lobby.t()) :: any()

after_lobby_deleted(t)

(optional)
@callback after_lobby_deleted(Gamend.Lobbies.Lobby.t()) :: any()

after_lobby_host_change(t, t)

(optional)
@callback after_lobby_host_change(Gamend.Lobbies.Lobby.t(), String.t()) :: any()

after_lobby_join(t, t)

(optional)
@callback after_lobby_join(Gamend.Accounts.User.t(), Gamend.Lobbies.Lobby.t()) :: any()

after_lobby_kick(t, t, t)

(optional)
@callback after_lobby_kick(
  Gamend.Accounts.User.t(),
  Gamend.Accounts.User.t(),
  Gamend.Lobbies.Lobby.t()
) ::
  any()

after_lobby_leave(t, t)

(optional)
@callback after_lobby_leave(Gamend.Accounts.User.t(), Gamend.Lobbies.Lobby.t()) :: any()

after_lobby_state_changed(t, t, t)

(optional)
@callback after_lobby_state_changed(Gamend.Lobbies.Lobby.t(), String.t(), String.t()) ::
  any()

after_lobby_updated(t)

(optional)
@callback after_lobby_updated(Gamend.Lobbies.Lobby.t()) :: any()

before_lobby_create(map)

(optional)
@callback before_lobby_create(map()) :: hook_result(map())

before_lobby_delete(t)

(optional)
@callback before_lobby_delete(Gamend.Lobbies.Lobby.t()) ::
  hook_result(Gamend.Lobbies.Lobby.t())

before_lobby_join(t, t, keyword)

(optional)

before_lobby_kick(t, t, t)

(optional)

before_lobby_leave(t, t)

(optional)
@callback before_lobby_leave(Gamend.Accounts.User.t(), Gamend.Lobbies.Lobby.t()) :: any()

before_lobby_state_change(t, t, t)

(optional)
@callback before_lobby_state_change(Gamend.Lobbies.Lobby.t(), String.t(), String.t()) ::
  veto_result()

before_lobby_update(t, map)

(optional)
@callback before_lobby_update(Gamend.Lobbies.Lobby.t(), map()) :: hook_result(map())

Group hooks

after_group_create(t)

(optional)
@callback after_group_create(Gamend.Groups.Group.t()) :: any()

after_group_deleted(t)

(optional)
@callback after_group_deleted(Gamend.Groups.Group.t()) :: any()

after_group_join(t, t)

(optional)
@callback after_group_join(String.t(), Gamend.Groups.Group.t()) :: any()

after_group_kick(t, t, t)

(optional)
@callback after_group_kick(String.t(), String.t(), String.t()) :: any()

after_group_leave(t, t)

(optional)
@callback after_group_leave(String.t(), String.t()) :: any()

after_group_updated(t)

(optional)
@callback after_group_updated(Gamend.Groups.Group.t()) :: any()

before_group_create(t, map)

(optional)
@callback before_group_create(Gamend.Accounts.User.t(), map()) :: hook_result(map())

before_group_delete(t)

(optional)
@callback before_group_delete(Gamend.Groups.Group.t()) ::
  hook_result(Gamend.Groups.Group.t())

before_group_join(t, t, map)

(optional)

before_group_kick(t, t, t)

(optional)
@callback before_group_kick(String.t(), String.t(), String.t()) ::
  hook_result({String.t(), String.t(), String.t()})

before_group_update(t, map)

(optional)
@callback before_group_update(Gamend.Groups.Group.t(), map()) :: hook_result(map())

Party hooks

after_party_create(t)

(optional)
@callback after_party_create(Gamend.Parties.Party.t()) :: any()

after_party_disband(t)

(optional)
@callback after_party_disband(Gamend.Parties.Party.t()) :: any()

after_party_join(t, t)

(optional)
@callback after_party_join(Gamend.Accounts.User.t(), Gamend.Parties.Party.t()) :: any()

after_party_kick(t, t, t)

(optional)
@callback after_party_kick(
  Gamend.Accounts.User.t(),
  Gamend.Accounts.User.t(),
  Gamend.Parties.Party.t()
) ::
  any()

after_party_leave(t, t)

(optional)
@callback after_party_leave(Gamend.Accounts.User.t(), String.t()) :: any()

after_party_updated(t)

(optional)
@callback after_party_updated(Gamend.Parties.Party.t()) :: any()

before_party_create(t, map)

(optional)
@callback before_party_create(Gamend.Accounts.User.t(), map()) :: hook_result(map())

before_party_join(t, t)

(optional)

before_party_kick(t, t, t)

(optional)

before_party_update(t, map)

(optional)
@callback before_party_update(Gamend.Parties.Party.t(), map()) :: hook_result(map())

Chat hooks

after_chat_message(t)

(optional)
@callback after_chat_message(Gamend.Chat.Message.t()) :: any()

after_chat_message_reported(t)

(optional)
@callback after_chat_message_reported(Gamend.Chat.Report.t()) :: any()

before_chat_message(t, map)

(optional)
@callback before_chat_message(Gamend.Accounts.User.t(), map()) :: hook_result(map())

Quest hooks

after_quest_claimed(t)

(optional)
@callback after_quest_claimed(Gamend.Quests.QuestProgress.t()) :: any()

after_quest_completed(t)

(optional)
@callback after_quest_completed(Gamend.Quests.QuestProgress.t()) :: any()

before_quest_claim(t, t, t)

(optional)
@callback before_quest_claim(
  String.t(),
  Gamend.Quests.Quest.t(),
  Gamend.Quests.QuestProgress.t()
) :: veto_result()

Leaderboard hooks

after_score_submitted(t)

(optional)
@callback after_score_submitted(Gamend.Leaderboards.Record.t()) :: any()

Tournament hooks

after_tournament_finished(t, map)

(optional)
@callback after_tournament_finished(Gamend.Tournaments.Tournament.t(), map()) :: any()

after_tournament_match_resolved(t)

(optional)
@callback after_tournament_match_resolved(Gamend.Tournaments.Match.t()) :: any()

after_tournament_register(t, t)

(optional)
@callback after_tournament_register(
  Gamend.Accounts.User.t(),
  Gamend.Tournaments.Tournament.t()
) :: any()

before_tournament_leave(t, t)

(optional)
@callback before_tournament_leave(
  Gamend.Accounts.User.t(),
  Gamend.Tournaments.Tournament.t()
) ::
  hook_result(term())

before_tournament_register(t, t)

(optional)
@callback before_tournament_register(
  Gamend.Accounts.User.t(),
  Gamend.Tournaments.Tournament.t()
) ::
  hook_result(term())

before_tournament_result(t, term)

(optional)
@callback before_tournament_result(Gamend.Tournaments.Match.t(), term()) ::
  hook_result(term())

tournament_match_expired(t)

(optional)
@callback tournament_match_expired(Gamend.Tournaments.Match.t()) :: any()

tournament_match_ready(t)

(optional)
@callback tournament_match_ready(Gamend.Tournaments.Match.t()) :: any()

Matchmaking hooks

after_matchmaking_cancel(t, non_neg_integer)

(optional)
@callback after_matchmaking_cancel(Ecto.UUID.t(), non_neg_integer()) :: any()

after_matchmaking_join(t, t)

(optional)
@callback after_matchmaking_join(Gamend.Accounts.User.t(), Gamend.Matchmaking.Ticket.t()) ::
  any()

after_matchmaking_matched(list, t)

(optional)
@callback after_matchmaking_matched([Gamend.Matchmaking.Ticket.t()], Ecto.UUID.t()) ::
  any()

before_matchmaking_join(t, map)

(optional)
@callback before_matchmaking_join(Gamend.Accounts.User.t(), map()) :: hook_result(map())

matchmaking_form_matches(map, list)

(optional)
@callback matchmaking_form_matches(map(), [Gamend.Matchmaking.Ticket.t()]) ::
  [[Gamend.Matchmaking.Ticket.t()]] | :default

ReadyCheck hooks

after_ready_check_failed(t, t, list)

(optional)
@callback after_ready_check_failed(Gamend.ReadyChecks.Check.t(), String.t(), [map()]) ::
  any()

after_ready_check_passed(t)

(optional)
@callback after_ready_check_passed(Gamend.ReadyChecks.Check.t()) :: any()

before_ready_check_open(arg1, list)

(optional)
@callback before_ready_check_open(
  Gamend.Lobbies.Lobby.t() | Gamend.Parties.Party.t() | :matchmaking,
  [String.t()]
) :: veto_result()

Payments hooks

after_entitlement_changed(t)

(optional)
@callback after_entitlement_changed(Gamend.Payments.Entitlement.t()) :: any()

after_purchase_fulfilled(t)

(optional)
@callback after_purchase_fulfilled(Gamend.Payments.Purchase.t()) :: any()

after_purchase_revoked(t)

(optional)
@callback after_purchase_revoked(Gamend.Payments.Purchase.t()) :: any()

before_purchase(t, product)

(optional)
@callback before_purchase(Gamend.Accounts.User.t(), product :: struct()) ::
  hook_result(term())

Veto a purchase before the player is charged.

Runs when a checkout starts and again when a receipt is validated, so a game can refuse to sell — an unlinked account whose entitlement would be stranded on one device, a region it does not ship to, a player it has banned. Return {:error, reason} to stop it; the money never moves.

Economy hooks

after_inventory_changed(map)

(optional)
@callback after_inventory_changed(map()) :: any()

after_wallet_changed(map)

(optional)
@callback after_wallet_changed(map()) :: any()

Push hooks

after_push_sent(t, map, map)

(optional)
@callback after_push_sent(String.t(), map(), map()) :: any()

before_push_send(t, map)

(optional)
@callback before_push_send(String.t(), map()) :: hook_result(map())

KV hooks

before_kv_get(t, kv_opts)

(optional)
@callback before_kv_get(String.t(), kv_opts()) :: kv_access_result()

Called before a KV get/2 is performed. Implementations should return one of these client KV API access decisions:

  • :public — any authenticated client can read.
  • :owner_only — only the caller matching the requested user_id can read.
  • :lobby_members_only — only callers in the requested lobby_id can read.
  • :owner_or_lobby_member — caller may match either requested user_id or lobby_id.
  • :admin_only — only admins can read through the client KV API.
  • :server_only — no client KV reads.

Server-side Gamend.KV.get/2 calls are unaffected.

Receives the key and an opts map/keyword (see kv_opts/0). Return either the bare atom (e.g. :public) or {:ok, :public}; return {:error, reason} to block the read.

Types

hook_result(attrs_or_user)

@type hook_result(attrs_or_user) :: {:ok, attrs_or_user} | {:error, term()}

kv_access()

@type kv_access() ::
  :public
  | :owner_only
  | :lobby_members_only
  | :owner_or_lobby_member
  | :admin_only
  | :server_only

kv_access_result()

@type kv_access_result() :: kv_access() | {:ok, kv_access()} | {:error, term()}

kv_opts()

@type kv_opts() :: map() | keyword()

Options passed to hooks that accept an options map/keyword list.

Common keys include :user_id, :lobby_id, and other domain-specific options. Hooks may accept either a map or keyword list for convenience.

veto_result()

@type veto_result() :: :ok | hook_result(term())

A veto-only hook: {:error, reason} rejects, anything else allows. The return never rewrites the args, so a bare :ok is the usual "allow".

Functions

call(name, args \\ [], opts \\ [])

Call an arbitrary function exported by the configured hooks module.

This is a safe wrapper that checks function existence, enforces an allow-list if configured and runs the call inside a short Task with a configurable timeout to avoid long-running user code.

Returns {:ok, result} | {:error, reason}

caller()

@spec caller() :: any() | nil

When a hooks function is executed via call/3 or internal_call/3, an optional :caller can be provided in the options. The caller will be injected into the spawned task's process dictionary and is accessible via Gamend.Hooks.caller/0 (the raw value) or caller_id/0 (the numeric id when the value is a user struct or map containing :id).

caller_id()

@spec caller_id() :: String.t() | nil

caller_user()

@spec caller_user() :: Gamend.Accounts.User.t() | nil

Return the user struct for the current caller when available. This will attempt to resolve the caller via Gamend.Accounts.get_user!/1 when the caller is a user id or a map containing an :id key. Returns nil when no caller or user is found.

exported_functions(mod \\ module())

Return a list of exported functions on the currently registered hooks module.

The result is a list of maps like: [%{name: "start_game", arities: [2,3]}, ...] This is useful for tooling and admin UI to display what RPCs are available.

internal_call(name, args \\ [], opts \\ [])

Call an internal lifecycle callback. When a callback is missing this returns a sensible default (eg. {:ok, attrs} for before callbacks) so domain code doesn't need to handle missing hooks specially in most cases.

internal_hooks()

@spec internal_hooks() :: MapSet.t(atom())

Returns the set of internal lifecycle hook names that are not callable through the public RPC interface.

invoke(name, args \\ [])

Invoke a dynamic hook function by name.

This is used by Gamend.Schedule to call scheduled job callbacks. Unlike internal_call/3, this is designed for user-defined functions that are not part of the core lifecycle callbacks.

Returns :ok on success, {:error, reason} on failure or if the function doesn't exist.

module()

Return the configured module that implements the hooks behaviour.

pipeline_hook?(name, arity)

True when the hook transforms its input (a before_* pipeline hook) rather than fanning out notifications. Exposed for the admin runtime page.