feat(gateway): list bot ready guilds as unavailable (#2774)

This commit is contained in:
Hampus
2026-09-14 20:08:26 +02:00
committed by GitHub
parent 5d1dddc093
commit 7c9274847f
8 changed files with 358 additions and 52 deletions
@@ -146,7 +146,7 @@ The initial session state. Sent once after a successful [Identify](/gateway/comm
| _timings? | object | HTTP-side timing breakdown, present only for a staff account |
| _timings_gw? | object | Gateway-side timing breakdown, present only for a staff account |
<sup>1</sup> A bot session always receives an empty array here and the guilds arrive as the [Guild Create](#guild-create) burst described below
<sup>1</sup> On a bot session every entry is an unavailable guild with `id` and `unavailable: true` alone. The burst described below then sends a [Guild Create](#guild-create) with the full state of each available guild, and a [Guild Delete](#guild-delete) for each unavailable one
<sup>2</sup> Each entry has its `user` field removed and the removed accounts appear in `users` instead, so a client resolves a relationship through the entry's `id`
@@ -156,7 +156,7 @@ The initial session state. Sent once after a successful [Identify](/gateway/comm
Ready is sent outside the replay buffer, so a [Resume](/gateway/commands/#resume) never replays it.
A bot session receives one [Guild Create](#guild-create) per available guild immediately after Ready, and one [Guild Delete](#guild-delete) per unavailable guild. Those Dispatches are also sent outside the replay buffer.
A bot session receives one [Guild Create](#guild-create) per available guild immediately after Ready, and one [Guild Delete](#guild-delete) per unavailable guild. Together they resolve the entries of the `guilds` array. Those Dispatches are also sent outside the replay buffer.
Shortly after Ready, every session receives one [Call Create](#call-create) for each of its private channels that has an active call.
@@ -187,9 +187,9 @@ The same structure appears in Ready, [Guild Create](#guild-create), and [Guild S
<sup>3</sup> Guild presences arrive as separate [Presence Update](#presence-update) and [Presence Update Bulk](#presence-update-bulk) Dispatches
An unavailable guild is reduced to `id` and `unavailable: true`, plus `unavailable_hidden: true` when the guild is hidden. It has none of the other fields.
An unavailable guild is reduced to `id` and `unavailable: true`, plus `unavailable_hidden: true` when the guild is hidden. It has none of the other fields. Every entry in a bot session's [Ready](#ready) `guilds` array has this form, with `id` and `unavailable: true` alone.
Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot receives immediately after Ready, each entry in `members` has its `user` replaced by `{"id": "..."}`. On a user session the removed accounts appear in the Ready payload's `users` array. A bot's `users` array is empty, so a bot pulls those accounts with [Request Guild Members](/gateway/commands/#request-guild-members). A [Guild Create](#guild-create) sent later in the session, and every [Guild Sync](#guild-sync), have the members with `user` intact.
Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot receives immediately after Ready, each entry in `members` has its `user` replaced by `{"id": "..."}`. A bot session's Ready has no `members` at all, because each of its guilds is an unavailable guild, so a bot sees the reduced form in the burst alone. On a user session the removed accounts appear in the Ready payload's `users` array. A bot's `users` array is empty, so a bot pulls those accounts with [Request Guild Members](/gateway/commands/#request-guild-members). A [Guild Create](#guild-create) sent later in the session, and every [Guild Sync](#guild-sync), have the members with `user` intact.
#### Session presence object
@@ -380,7 +380,9 @@ A guild became available to the session. The payload is a [guild ready object](#
`roles`, `channels`, `emojis`, `stickers`, and `voice_states` are always complete. A client that stores any of them for the guild replaces its stored list with the new array. `members` is a partial list. A client adds or updates those members and keeps every other member it already stores.
A user session receives Guild Create when a guild becomes available after Ready, for example after joining one or after an unavailable guild recovers. A bot session receives one for every guild in the burst that follows Ready.
Every session receives Guild Create when a guild becomes available after Ready, for example after joining one or after an unavailable guild recovers. A bot session also receives one for each available guild in the burst that follows Ready.
A bot session's Guild Create has `unavailable: false` unless it is the first Guild Create for a guild the bot joined during the session and [Ready](#ready) did not list. That Dispatch has no `unavailable` field, even when a [Guild Delete](#guild-delete) with `unavailable: true` for the guild came before it. A user session never receives the field on Guild Create.
### <span id="guild-sync"></span>GUILD_SYNC
@@ -293,7 +293,7 @@ A guild belongs to `((guild_id >> 22) % shard_count)`, computed on the integer v
The pair selects the session's guild membership. At Identify, Fluxer filters the account's guild list to the guilds the shard owns, and the session connects only to those.
For a user session, the filtered set is also the [Ready](/gateway/events/#ready) `guilds` array. For a bot session, it is the guild burst of [Guild Create](/gateway/events/#guild-create) and [Guild Delete](/gateway/events/#guild-delete) Dispatches after Ready. Ready echoes the accepted pair back as `shard`.
On every session, the filtered set is also the [Ready](/gateway/events/#ready) `guilds` array. A bot session has each of those guilds as an unavailable guild, and one [Guild Create](/gateway/events/#guild-create) or [Guild Delete](/gateway/events/#guild-delete) per guild follows Ready. Ready echoes the accepted pair back as `shard`.
Fluxer checks only a bot session against the guild ceiling. A bot whose shard owns more than 2,500 guilds closes with `4011` and reason `Sharding required`. A bot that supplies no pair is checked against its whole guild list. A user session is bounded by the 100-session-per-user limit alone, whatever its guild count.
+2 -1
View File
@@ -66,6 +66,7 @@
guild_connect_inflight => #{guild_id() => non_neg_integer()},
guild_connect_workers => #{reference() => {guild_id(), non_neg_integer(), pid()}},
guild_connect_timers => #{guild_id() => {reference(), reference()}},
pending_guild_joins => #{guild_id() => true},
voice_queue => queue:queue(map()),
voice_queue_timer => reference() | undefined,
debounce_reactions => boolean(),
@@ -168,7 +169,7 @@ handle_cast_guild_or_lifecycle({reconnect_drain, SocketPid}, State) when
session_lifecycle:handle_reconnect_drain(SocketPid, State);
handle_cast_guild_or_lifecycle({guild_join, GuildId}, State) when is_integer(GuildId) ->
self() ! {guild_connect, GuildId, 0},
{noreply, State};
{noreply, session_bot_guilds:track_join(GuildId, State)};
handle_cast_guild_or_lifecycle({store_guild_subscriptions, Data}, State) when is_map(Data) ->
{noreply, session_guilds:store_guild_subscriptions(Data, State)};
handle_cast_guild_or_lifecycle({guild_leave, GuildId, forced_unavailable, true}, State) when
@@ -0,0 +1,56 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(session_bot_guilds).
-typing([eqwalizer]).
-export([
guild_event/3,
track_join/2,
forget_joins/2
]).
-export_type([session_state/0, guild_id/0, event/0]).
-type session_state() :: session:session_state().
-type guild_id() :: session:guild_id().
-type event() :: session_dispatch:event().
-spec guild_event(event(), map(), session_state()) -> {map(), session_state()}.
guild_event(guild_create, Data, #{bot := true} = State) ->
guild_create(type_conv:extract_id(Data, <<"id">>), Data, State);
guild_event(guild_delete, #{<<"unavailable">> := true} = Data, State) ->
{Data, State};
guild_event(guild_delete, Data, #{bot := true} = State) ->
{Data, forget_join(type_conv:extract_id(Data, <<"id">>), State)};
guild_event(_Event, Data, State) ->
{Data, State}.
-spec guild_create(guild_id() | undefined, map(), session_state()) -> {map(), session_state()}.
guild_create(undefined, Data, State) ->
{Data#{<<"unavailable">> => false}, State};
guild_create(GuildId, Data, State) ->
Pending = pending_joins(State),
case maps:is_key(GuildId, Pending) of
true -> {Data, State#{pending_guild_joins => maps:remove(GuildId, Pending)}};
false -> {Data#{<<"unavailable">> => false}, State}
end.
-spec track_join(guild_id(), session_state()) -> session_state().
track_join(GuildId, #{bot := true} = State) ->
State#{pending_guild_joins => (pending_joins(State))#{GuildId => true}};
track_join(_GuildId, State) ->
State.
-spec forget_join(guild_id() | undefined, session_state()) -> session_state().
forget_join(undefined, State) ->
State;
forget_join(GuildId, State) ->
forget_joins([GuildId], State).
-spec forget_joins([guild_id()], session_state()) -> session_state().
forget_joins(GuildIds, State) ->
State#{pending_guild_joins => maps:without(GuildIds, pending_joins(State))}.
-spec pending_joins(session_state()) -> #{guild_id() => true}.
pending_joins(State) ->
maps:get(pending_guild_joins, State, #{}).
@@ -33,9 +33,17 @@ handle_dispatch(Event, Data, State) ->
should_skip_for_shard(Event, Data, State) orelse should_ignore_event(Event, Data, State)
of
true -> {noreply, State};
false -> route_dispatch(Event, Data, State)
false -> route_bot_guild_event(Event, Data, State)
end.
-spec route_bot_guild_event(event(), map() | list(), session_state()) ->
{noreply, session_state()}.
route_bot_guild_event(Event, Data, State) when is_map(Data) ->
{BotData, BotState} = session_bot_guilds:guild_event(Event, Data, State),
route_dispatch(Event, BotData, BotState);
route_bot_guild_event(Event, Data, State) ->
route_dispatch(Event, Data, State).
-spec should_skip_for_shard(
event(), map() | list() | {pre_encoded, binary()}, session_state()
) ->
+10 -13
View File
@@ -12,7 +12,6 @@
build_ignored_events_map/1,
load_private_channels/1,
load_relationships/1,
ensure_bot_ready_map/1,
build_state/1,
schedule_timers/1
]).
@@ -153,11 +152,6 @@ load_relationships(Ready) when is_map(Ready) ->
load_relationships(_) ->
#{}.
-spec ensure_bot_ready_map(term()) -> map().
ensure_bot_ready_map(undefined) -> #{<<"guilds">> => []};
ensure_bot_ready_map(Ready) when is_map(Ready) -> Ready#{<<"guilds">> => []};
ensure_bot_ready_map(_) -> #{<<"guilds">> => []}.
-spec build_state(map()) -> session_state().
build_state(SessionData) ->
BuildStartedAt = gateway_timings:start(),
@@ -307,7 +301,8 @@ extract_is_staff(_) ->
false.
-spec init_ready(boolean(), map() | undefined) -> map() | undefined.
init_ready(true, Ready) -> ensure_bot_ready_map(Ready);
init_ready(true, Ready) when is_map(Ready) -> Ready;
init_ready(true, _Ready) -> undefined;
init_ready(false, Ready) -> Ready.
-spec init_ack_seq(seq(), seq()) -> seq().
@@ -502,14 +497,16 @@ base_session_data(Ready) ->
ready => Ready
}.
ensure_bot_ready_map_test() ->
?assertEqual(#{<<"guilds">> => []}, ensure_bot_ready_map(undefined)),
?assertEqual(
#{<<"guilds">> => [], <<"user">> => #{}}, ensure_bot_ready_map(#{<<"user">> => #{}})
),
?assertEqual(#{<<"guilds">> => []}, ensure_bot_ready_map(not_a_map)),
init_ready_keeps_transferred_bot_ready_undefined_test() ->
?assertEqual(undefined, init_ready(true, undefined)),
?assertEqual(#{<<"user">> => #{}}, init_ready(true, #{<<"user">> => #{}})),
?assertEqual(undefined, init_ready(false, undefined)),
ok.
build_state_keeps_transferred_bot_ready_undefined_test() ->
Data = (base_session_data(undefined))#{bot => true},
?assertEqual(undefined, maps:get(ready, build_state(Data))).
normalize_guild_ids_filters_invalid_values_test() ->
?assertEqual([1, 2], normalize_guild_ids([1, 0, -1, 2, <<"3">>])),
?assertEqual([10], normalize_guild_ids(#{10 => undefined, <<"bad">> => undefined})),
@@ -173,9 +173,8 @@ build_final_ready_data(
SessionId,
IsBot
) ->
ReadyData = prepare_ready_base(State, IsBot),
AllGuildStates = build_all_guild_states(CollectedGuilds, Guilds),
GuildsForReady = guilds_for_ready_payload(IsBot, AllGuildStates),
ReadyData = prepare_ready_base(State),
GuildsForReady = guilds_for_ready_payload(IsBot, CollectedGuilds, Guilds),
FinalReadyData = ReadyData#{
<<"guilds">> => GuildsForReady,
<<"sessions">> => CollectedSessions,
@@ -186,15 +185,11 @@ build_final_ready_data(
},
gateway_sharding:maybe_put_ready_shard(FinalReadyData, maps:get(shard, State, undefined)).
-spec prepare_ready_base(session_state(), boolean()) -> map().
prepare_ready_base(#{ready := undefined}, _IsBot) ->
-spec prepare_ready_base(session_state()) -> map().
prepare_ready_base(#{ready := undefined}) ->
#{<<"guilds">> => []};
prepare_ready_base(#{ready := Ready}, IsBot) ->
Stripped = session_ready_collect:strip_user_from_relationships(Ready),
case IsBot of
true -> Stripped#{<<"guilds">> => []};
false -> Stripped
end.
prepare_ready_base(#{ready := Ready}) ->
session_ready_collect:strip_user_from_relationships(Ready).
-spec build_all_guild_states([map()], map()) -> [map()].
build_all_guild_states(CollectedGuilds, Guilds) ->
@@ -225,9 +220,13 @@ collect_unavailable_placeholder(GuildId, _Value, StrippedGuildIds, Acc) ->
maybe_add_unavailable_guild(GuildId, ExistingGuildIds, Acc) ->
case maps:is_key(GuildId, ExistingGuildIds) of
true -> Acc;
false -> [#{<<"id">> => integer_to_binary(GuildId), <<"unavailable">> => true} | Acc]
false -> [unavailable_guild_placeholder(GuildId) | Acc]
end.
-spec unavailable_guild_placeholder(guild_id()) -> map().
unavailable_guild_placeholder(GuildId) ->
#{<<"id">> => integer_to_binary(GuildId), <<"unavailable">> => true}.
-spec collected_guild_id_map([map()]) -> #{guild_id() => true}.
collected_guild_id_map(GuildStates) ->
lists:foldl(fun accumulate_guild_id/2, #{}, GuildStates).
@@ -255,22 +254,25 @@ parse_guild_id_binary(GuildIdBin) ->
error:badarg -> error
end.
-spec guilds_for_ready_payload(boolean(), [map()]) -> [map()].
guilds_for_ready_payload(true, _AllGuildStates) -> [];
guilds_for_ready_payload(false, AllGuildStates) -> AllGuildStates.
-spec guilds_for_ready_payload(boolean(), [map()], map()) -> [map()].
guilds_for_ready_payload(true, _CollectedGuilds, Guilds) ->
[unavailable_guild_placeholder(GuildId) || GuildId <- maps:keys(Guilds)];
guilds_for_ready_payload(false, CollectedGuilds, Guilds) ->
build_all_guild_states(CollectedGuilds, Guilds).
-spec dispatch_bot_guild_creates(boolean(), [map()], map(), session_state()) -> session_state().
dispatch_bot_guild_creates(false, _CollectedGuilds, _Guilds, State) ->
State;
dispatch_bot_guild_creates(true, CollectedGuilds, _Guilds, State) ->
dispatch_bot_guild_creates(true, CollectedGuilds, Guilds, State) ->
AllGuildStates = strip_collected_guild_states(CollectedGuilds),
lists:foldl(
fun(GuildState, AccState) ->
dispatch_event(guild_state_event(GuildState), GuildState, AccState)
end,
State,
AllGuildStates
).
AnnouncedState = session_bot_guilds:forget_joins(maps:keys(Guilds), State),
lists:foldl(fun dispatch_bot_guild_state/2, AnnouncedState, AllGuildStates).
-spec dispatch_bot_guild_state(map(), session_state()) -> session_state().
dispatch_bot_guild_state(GuildState, State) ->
Event = guild_state_event(GuildState),
{Data, NextState} = session_bot_guilds:guild_event(Event, GuildState, State),
dispatch_event(Event, Data, NextState).
-spec schedule_call_creates(session_state(), binary()) -> ok.
schedule_call_creates(State, SessionId) ->
+248 -8
View File
@@ -32,6 +32,118 @@ process_guild_state_available_dispatches_guild_create_test() ->
?assertEqual(guild_create, FirstEvent),
ok.
process_guild_state_bot_guild_create_marks_guild_available_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()},
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State1} = session_ready:process_guild_state(GuildState, State0),
receive
{dispatch, guild_create, GuildCreateData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)),
?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end.
process_guild_state_bot_guild_create_after_join_omits_unavailable_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()},
{noreply, State1} = session:handle_cast({guild_join, 123}, State0),
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, State2} = session_ready:process_guild_state(GuildState, State1),
receive
{dispatch, guild_create, JoinCreateData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, JoinCreateData)),
?assertNot(maps:is_key(<<"unavailable">>, JoinCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end,
{noreply, _State3} = session_ready:process_guild_state(GuildState, State2),
receive
{dispatch, guild_create, LaterCreateData, 2} ->
?assertEqual(false, maps:get(<<"unavailable">>, LaterCreateData))
after 1000 ->
?assert(false, second_guild_create_not_dispatched)
end,
drain_mailbox().
process_guild_state_bot_guild_create_after_join_and_outage_omits_unavailable_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()},
{noreply, State1} = session:handle_cast({guild_join, 123}, State0),
{noreply, State2} = session_ready:mark_guild_unavailable(123, State1),
receive
{dispatch, guild_delete, GuildDeleteData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildDeleteData)),
?assertEqual(true, maps:get(<<"unavailable">>, GuildDeleteData))
after 1000 ->
?assert(false, guild_delete_not_dispatched)
end,
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State3} = session_ready:process_guild_state(GuildState, State2),
receive
{dispatch, guild_create, GuildCreateData, 2} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)),
?assertNot(maps:is_key(<<"unavailable">>, GuildCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end,
drain_mailbox().
process_guild_state_bot_guild_create_after_join_and_removal_marks_guild_available_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{
bot => true,
socket_pid => self(),
user_id => 1
},
{noreply, State1} = session:handle_cast({guild_join, 123}, State0),
{noreply, State2} = session_connection_guild:handle_result_internal(
123, 3, {error, not_member}, State1
),
receive
{dispatch, guild_delete, GuildDeleteData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildDeleteData)),
?assertNot(maps:is_key(<<"unavailable">>, GuildDeleteData))
after 1000 ->
?assert(false, guild_delete_not_dispatched)
end,
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State3} = session_ready:process_guild_state(GuildState, State2),
receive
{dispatch, guild_create, GuildCreateData, 2} ->
?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end,
drain_mailbox().
relayed_guild_create_to_bot_marks_guild_available_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{bot => true, socket_pid => self()},
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State1} = session:handle_cast({dispatch, guild_create, GuildState}, State0),
receive
{dispatch, guild_create, GuildCreateData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)),
?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end.
process_guild_state_user_guild_create_omits_unavailable_test() ->
drain_mailbox(),
State0 = (base_state_for_guild_dispatch_test())#{bot => false, socket_pid => self()},
GuildState = #{<<"id">> => <<"123">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State1} = session_ready:process_guild_state(GuildState, State0),
receive
{dispatch, guild_create, GuildCreateData, 1} ->
?assertEqual(<<"123">>, maps:get(<<"id">>, GuildCreateData)),
?assertNot(maps:is_key(<<"unavailable">>, GuildCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end.
mark_guild_unavailable_hidden_includes_flag_test() ->
State0 = #{
collected_guild_states => [],
@@ -88,15 +200,20 @@ dispatch_ready_data_bot_unavailable_dispatches_guild_delete_test() ->
),
{noreply, _State1} = session_ready:dispatch_ready_data(State0),
receive
{dispatch, ready, ReadyData, _ReadySeq} ->
?assertEqual([], maps:get(<<"guilds">>, ReadyData, []));
{dispatch, ready, ReadyData, ReadySeq} ->
?assertEqual(1, ReadySeq),
?assertEqual(
[#{<<"id">> => <<"987">>, <<"unavailable">> => true}],
maps:get(<<"guilds">>, ReadyData)
);
OtherReady ->
?assert(false, {unexpected_ready_message, OtherReady})
after 1000 ->
?assert(false, ready_not_dispatched)
end,
receive
{dispatch, guild_delete, GuildDeleteData, _GuildDeleteSeq} ->
{dispatch, guild_delete, GuildDeleteData, GuildDeleteSeq} ->
?assertEqual(2, GuildDeleteSeq),
?assertEqual(<<"987">>, maps:get(<<"id">>, GuildDeleteData)),
?assertEqual(true, maps:get(<<"unavailable">>, GuildDeleteData));
OtherDelete ->
@@ -206,31 +323,154 @@ dispatch_ready_data_nonbot_replaces_unavailable_placeholder_with_collected_guild
?assert(false, ready_not_dispatched)
end.
dispatch_ready_data_bot_does_not_synthesize_unavailable_placeholders_test() ->
dispatch_ready_data_bot_lists_every_guild_as_unavailable_test() ->
drain_mailbox(),
State0 = base_ready_state(
<<"session-ready-bot-placeholder-test">>,
47,
true,
#{111 => undefined, 222 => cached_unavailable},
#{
111 => undefined,
222 => cached_unavailable,
333 => unavailable,
444 => {self(), make_ref()}
},
[]
),
{noreply, _State1} = session_ready:dispatch_ready_data(State0),
receive
{dispatch, ready, ReadyData, _ReadySeq} ->
?assertEqual([], maps:get(<<"guilds">>, ReadyData, []));
{dispatch, ready, ReadyData, ReadySeq} ->
?assertEqual(1, ReadySeq),
?assertEqual(
[
#{<<"id">> => <<"111">>, <<"unavailable">> => true},
#{<<"id">> => <<"222">>, <<"unavailable">> => true},
#{<<"id">> => <<"333">>, <<"unavailable">> => true},
#{<<"id">> => <<"444">>, <<"unavailable">> => true}
],
lists:sort(maps:get(<<"guilds">>, ReadyData))
);
OtherReady ->
?assert(false, {unexpected_ready_message, OtherReady})
after 1000 ->
?assert(false, ready_not_dispatched)
end,
receive
{dispatch, guild_create, _GuildCreateData, _GuildCreateSeq} ->
?assert(false, unexpected_guild_create_for_bot_ready);
{dispatch, guild_delete, _GuildDeleteData, _GuildDeleteSeq} ->
?assert(false, unexpected_synthesized_unavailable_guild)
?assert(false, unexpected_guild_delete_for_bot_ready)
after 100 ->
ok
end.
dispatch_ready_data_bot_lists_collected_guild_as_unavailable_stub_test() ->
drain_mailbox(),
CollectedGuild = #{
<<"id">> => <<"111">>,
<<"name">> => <<"guild-111">>,
<<"members">> => [
#{<<"user">> => #{<<"id">> => <<"1">>, <<"username">> => <<"member-1">>}}
],
<<"channels">> => []
},
State0 = base_ready_state(
<<"session-ready-bot-collected-test">>,
50,
true,
#{111 => undefined, 222 => undefined},
[CollectedGuild]
),
{noreply, _State1} = session_ready:dispatch_ready_data(State0),
receive
{dispatch, ready, ReadyData, ReadySeq} ->
?assertEqual(1, ReadySeq),
?assertEqual(
[
#{<<"id">> => <<"111">>, <<"unavailable">> => true},
#{<<"id">> => <<"222">>, <<"unavailable">> => true}
],
lists:sort(maps:get(<<"guilds">>, ReadyData))
);
OtherReady ->
?assert(false, {unexpected_ready_message, OtherReady})
after 1000 ->
?assert(false, ready_not_dispatched)
end,
receive
{dispatch, guild_create, GuildCreateData, GuildCreateSeq} ->
?assertEqual(2, GuildCreateSeq),
?assertEqual(<<"111">>, maps:get(<<"id">>, GuildCreateData)),
?assertEqual(<<"guild-111">>, maps:get(<<"name">>, GuildCreateData)),
?assertEqual(false, maps:get(<<"unavailable">>, GuildCreateData)),
?assertEqual(
[#{<<"user">> => #{<<"id">> => <<"1">>}}],
maps:get(<<"members">>, GuildCreateData)
);
OtherGuild ->
?assert(false, {unexpected_guild_event, OtherGuild})
after 1000 ->
?assert(false, guild_create_not_dispatched)
end,
receive
{dispatch, OtherEvent, _OtherData, _OtherSeq} ->
?assert(false, {unexpected_dispatch_after_burst, OtherEvent})
after 100 ->
ok
end.
dispatch_ready_data_bot_marks_guilds_joined_before_ready_available_test() ->
drain_mailbox(),
CollectedGuild = #{<<"id">> => <<"111">>, <<"channels">> => [], <<"members">> => []},
UnavailableGuild = #{<<"id">> => <<"222">>, <<"unavailable">> => true},
State0 = base_ready_state(
<<"session-ready-bot-join-test">>,
51,
true,
#{111 => {self(), make_ref()}, 222 => unavailable},
[UnavailableGuild, CollectedGuild]
),
{noreply, State1} = session:handle_cast({guild_join, 111}, State0),
{noreply, State2} = session:handle_cast({guild_join, 222}, State1),
drain_mailbox(),
{noreply, State3} = session_ready:dispatch_ready_data(State2),
receive
{dispatch, ready, ReadyData, 1} ->
?assertEqual(
[
#{<<"id">> => <<"111">>, <<"unavailable">> => true},
#{<<"id">> => <<"222">>, <<"unavailable">> => true}
],
lists:sort(maps:get(<<"guilds">>, ReadyData))
)
after 1000 ->
?assert(false, ready_not_dispatched)
end,
receive
{dispatch, guild_create, BurstCreateData, 2} ->
?assertEqual(<<"111">>, maps:get(<<"id">>, BurstCreateData)),
?assertEqual(false, maps:get(<<"unavailable">>, BurstCreateData))
after 1000 ->
?assert(false, guild_create_not_dispatched)
end,
receive
{dispatch, guild_delete, BurstDeleteData, 3} ->
?assertEqual(<<"222">>, maps:get(<<"id">>, BurstDeleteData)),
?assertEqual(true, maps:get(<<"unavailable">>, BurstDeleteData))
after 1000 ->
?assert(false, guild_delete_not_dispatched)
end,
RecoveredGuild = #{<<"id">> => <<"222">>, <<"channels">> => [], <<"members">> => []},
{noreply, _State4} = session_ready:process_guild_state(RecoveredGuild, State3),
receive
{dispatch, guild_create, RecoveredCreateData, 4} ->
?assertEqual(<<"222">>, maps:get(<<"id">>, RecoveredCreateData)),
?assertEqual(false, maps:get(<<"unavailable">>, RecoveredCreateData))
after 1000 ->
?assert(false, recovered_guild_create_not_dispatched)
end,
drain_mailbox().
dispatch_ready_data_includes_shard_metadata_test() ->
drain_mailbox(),
State0 = (base_ready_state(