Packages

SDK for GameServer hooks development. Provides type specs, documentation, and IDE autocomplete for GameServer modules without requiring the full server.

Current section

Files

Jump to
game_server_sdk lib game_server push.ex
Raw

lib/game_server/push.ex

defmodule GameServer.Push do
@moduledoc ~S"""
Push context – device push-token registry and (see `send_to_user/3`)
server-authoritative delivery of push notifications.
Devices register their FCM registration token or APNs device token against
the authenticated user; a user has many devices. Delivery routes **per
token** off the `provider` column (`"fcm"` | `"apns"`), falling back to the
zero-config `Log` provider when nothing is configured
(see `docs/specs/push.md`).
## Usage
# Register a device (typically via POST /me/push_tokens)
{:ok, token} = Push.register_token(user_id, %{
"token" => "fcm-registration-token",
"platform" => "android",
"device_id" => "stable-device-key"
})
# List a user's devices
tokens = Push.list_tokens(user_id, page: 1, page_size: 25)
# Remove one (DELETE /me/push_tokens/:id)
{:ok, _} = Push.delete_token(user_id, token.id)
**Note:** This is an SDK stub. Calling these functions will raise an error.
The actual implementation runs on the GameServer.
"""
@type user_id() :: Ecto.UUID.t()
@doc ~S"""
Remove any token row by id (admin). Returns `{:ok, %PushToken{}}` or
`{:error, :not_found}`.
"""
@spec admin_delete_token(Ecto.UUID.t()) :: {:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
def admin_delete_token(_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Push.PushToken{id: "", user_id: "", token: "", platform: "android", provider: "fcm", device_id: nil, disabled_at: nil, last_used_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Push.admin_delete_token/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count for `list_all_tokens/2` (same filters).
"""
@spec count_all_tokens(map()) :: non_neg_integer()
def count_all_tokens(_filters) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Push.count_all_tokens/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Count a user's registered tokens (including disabled).
"""
@spec count_tokens(user_id()) :: non_neg_integer()
def count_tokens(_user_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
0
_ ->
raise "GameServer.Push.count_tokens/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Remove a token row by id, scoped to `user_id` (the `DELETE /me/push_tokens/:id`
path). Returns `{:ok, %PushToken{}}` or `{:error, :not_found}`.
"""
@spec delete_token(user_id(), Ecto.UUID.t()) ::
{:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
def delete_token(_user_id, _id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Push.PushToken{id: "", user_id: "", token: "", platform: "android", provider: "fcm", device_id: nil, disabled_at: nil, last_used_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Push.delete_token/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Soft-disable a token the provider reported dead. The row is kept —
re-registration re-enables it — so a token bouncing between valid and
invalid never loses its device association.
"""
@spec disable_token(String.t()) :: :ok
def disable_token(_token) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
:ok
_ ->
raise "GameServer.Push.disable_token/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Whether every delivery is forced to the Log provider (`PUSH_ADAPTER=log`).
"""
@spec force_log?() :: boolean()
def force_log?() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
false
_ ->
raise "GameServer.Push.force_log?/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Admin listing across all users. Supported `filters` keys (atom or string):
`:user_id`, `:platform`, `:provider`, and `:status` (`"live"` | `"disabled"`).
Supports pagination via `:page` and `:page_size` options; preloads `:user`
so the admin UI can show names, not UUIDs.
"""
@spec list_all_tokens(
map(),
keyword()
) :: [GameServer.Push.PushToken.t()]
def list_all_tokens(_filters, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Push.list_all_tokens/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
List a user's registered tokens, newest first. Includes disabled rows (they
are the user's devices; clients can show them greyed out).
Supports pagination via `:page` and `:page_size` options.
"""
@spec list_tokens(
user_id(),
keyword()
) :: [GameServer.Push.PushToken.t()]
def list_tokens(_user_id, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Push.list_tokens/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
The user's live (non-disabled) tokens — the delivery fan-out set. Unpaginated
by design: bounded by `max_push_tokens_per_user`.
"""
@spec live_tokens(user_id()) :: [GameServer.Push.PushToken.t()]
def live_tokens(_user_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
[]
_ ->
raise "GameServer.Push.live_tokens/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Bump `last_used_at` after a successful delivery.
"""
@spec mark_token_used(String.t()) :: :ok
def mark_token_used(_token) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
:ok
_ ->
raise "GameServer.Push.mark_token_used/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Resolve the delivery provider for a token: its `provider` column's module
when that module's `configured?/0` says it can deliver, else the zero-config
`Log` provider. `PUSH_ADAPTER=log` short-circuits everything to `Log`.
"""
@spec provider_for(GameServer.Push.PushToken.t()) :: module()
def provider_for(_push_token) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
nil
_ ->
raise "GameServer.Push.provider_for/1 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Register (or refresh) a device push token for `user_id`.
Upsert semantics, serialized under the `:push_tokens` advisory lock:
- a row with the same `token` already exists → it is claimed for this
user/device (a device that logged into another account must not keep
receiving the old account's pushes) and re-enabled;
- else a row with the same `(user_id, device_id)` exists → its token is
rotated in place and the row re-enabled;
- else a new row is inserted, subject to `max_push_tokens_per_user`
(counting live tokens only).
`provider` defaults from the platform when omitted: `"ios"` → `"apns"`,
anything else → `"fcm"`.
Returns `{:ok, %PushToken{}}`, `{:error, :too_many_tokens}`, or
`{:error, changeset}`.
"""
@spec register_token(user_id(), map()) ::
{:ok, GameServer.Push.PushToken.t()} | {:error, :too_many_tokens | Ecto.Changeset.t()}
def register_token(_user_id, _attrs) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Push.PushToken{id: "", user_id: "", token: "", platform: "android", provider: "fcm", device_id: nil, disabled_at: nil, last_used_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Push.register_token/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Queue a push message to all of `user_id`'s live devices.
Server-authoritative: exposed to plugins through the SDK and to admins —
never as a public client endpoint. Best-effort by design: no live devices
means no jobs, and delivery failures never propagate back to the caller.
Returns `:ok` or `{:error, errors}` when the message fails validation
(see `GameServer.Push.Message.new/1`).
"""
@spec send_to_user(user_id(), map(), keyword()) :: :ok | {:error, map()}
def send_to_user(_user_id, _message_attrs, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
:ok
_ ->
raise "GameServer.Push.send_to_user/3 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Queue a push message to every live device of `user_ids`.
Small audiences enqueue delivery jobs inline; past 100
recipients the expansion itself becomes a `FanoutWorker` job (chunked,
restart-safe, deduped against identical double-broadcasts).
"""
@spec send_to_users([user_id()], map(), keyword()) :: :ok | {:error, map() | term()}
def send_to_users(_user_ids, _message_attrs, _opts) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
:ok
_ ->
raise "GameServer.Push.send_to_users/3 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Aggregate token counts for the admin stat card and runtime introspection:
`%{total: n, live: n, disabled: n, by_platform: %{...}, by_provider: %{...}}`
(platform/provider maps count live tokens only).
"""
@spec token_stats() :: map()
def token_stats() do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
%{}
_ ->
raise "GameServer.Push.token_stats/0 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Remove a token row by its raw `token` value, scoped to `user_id`.
Returns `{:ok, %PushToken{}}` or `{:error, :not_found}`.
"""
@spec unregister_token(user_id(), String.t()) ::
{:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
def unregister_token(_user_id, _token) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
{:ok, %GameServer.Push.PushToken{id: "", user_id: "", token: "", platform: "android", provider: "fcm", device_id: nil, disabled_at: nil, last_used_at: nil, metadata: %{}, inserted_at: ~U[1970-01-01 00:00:00Z], updated_at: ~U[1970-01-01 00:00:00Z]}}
_ ->
raise "GameServer.Push.unregister_token/2 is a stub - only available at runtime on GameServer"
end
end
@doc ~S"""
Whether the user has any live device. Cached (60s TTL + version bump on
register/remove/disable): `Notifications` asks this on every insert, and the
common no-device answer must not cost a query.
"""
@spec user_has_live_tokens?(user_id()) :: boolean()
def user_has_live_tokens?(_user_id) do
case Application.get_env(:game_server_sdk, :stub_mode, :raise) do
:placeholder ->
false
_ ->
raise "GameServer.Push.user_has_live_tokens?/1 is a stub - only available at runtime on GameServer"
end
end
end