fluxer/fluxer_gateway/src/push/push_eligibility.erl
2026-01-01 21:05:54 +00:00

313 lines
11 KiB
Erlang

%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
-module(push_eligibility).
-export([is_eligible_for_push/8]).
-export([is_user_blocked/3]).
-export([check_user_guild_settings/7]).
-export([should_allow_notification/5]).
-export([is_user_mentioned/4]).
-define(LARGE_GUILD_THRESHOLD, 250).
-define(LARGE_GUILD_OVERRIDE_FEATURE, <<"LARGE_GUILD_OVERRIDE">>).
-define(MESSAGE_NOTIFICATIONS_NULL, -1).
-define(MESSAGE_NOTIFICATIONS_ALL, 0).
-define(MESSAGE_NOTIFICATIONS_ONLY_MENTIONS, 1).
-define(MESSAGE_NOTIFICATIONS_NO_MESSAGES, 2).
-define(MESSAGE_NOTIFICATIONS_INHERIT, 3).
-define(CHANNEL_TYPE_DM, 1).
-define(CHANNEL_TYPE_GROUP_DM, 3).
is_eligible_for_push(
UserId, UserId, _GuildId, _ChannelId, _MessageData, _GuildDefaultNotifications, _UserRoles, _State
) ->
false;
is_eligible_for_push(
UserId,
AuthorId,
GuildId,
ChannelId,
MessageData,
GuildDefaultNotifications,
UserRolesMap,
State
) ->
Blocked = is_user_blocked(UserId, AuthorId, State),
SettingsOk = check_user_guild_settings(
UserId,
GuildId,
ChannelId,
MessageData,
GuildDefaultNotifications,
UserRolesMap,
State
),
not Blocked andalso SettingsOk.
is_user_blocked(UserId, AuthorId, State) ->
BlockedIdsCache = maps:get(blocked_ids_cache, State, #{}),
case maps:get({blocked, UserId}, BlockedIdsCache, undefined) of
undefined ->
false;
BlockedIds ->
Blocked = lists:member(AuthorId, BlockedIds),
Blocked
end.
check_user_guild_settings(
_UserId, 0, _ChannelId, _MessageData, _GuildDefaultNotifications, _UserRolesMap, _State
) ->
true;
check_user_guild_settings(
UserId,
GuildId,
ChannelId,
MessageData,
GuildDefaultNotifications,
UserRolesMap,
State
) ->
UserGuildSettingsCache = maps:get(user_guild_settings_cache, State, #{}),
Settings =
case maps:get({settings, UserId, GuildId}, UserGuildSettingsCache, undefined) of
undefined ->
FetchedSettings = push_subscriptions:fetch_and_cache_user_guild_settings(
UserId, GuildId, State
),
case FetchedSettings of
null -> #{};
S -> S
end;
S ->
S
end,
MobilePush = maps:get(mobile_push, Settings, true),
case MobilePush of
false ->
false;
true ->
Muted = maps:get(muted, Settings, false),
ChannelOverrides = maps:get(channel_overrides, Settings, #{}),
ChannelKey = integer_to_binary(ChannelId),
ChannelOverride = maps:get(ChannelKey, ChannelOverrides, #{}),
ChannelMuted = maps:get(muted, ChannelOverride, undefined),
ActualMuted =
case ChannelMuted of
undefined -> Muted;
_ -> ChannelMuted
end,
MuteConfig = maps:get(mute_config, Settings, undefined),
IsTempMuted =
case MuteConfig of
undefined ->
false;
#{<<"end_time">> := EndTimeStr} ->
case push_utils:parse_timestamp(EndTimeStr) of
undefined ->
false;
EndTime ->
Now = erlang:system_time(millisecond),
Now < EndTime
end;
_ ->
false
end,
case ActualMuted orelse IsTempMuted of
true ->
false;
false ->
Level = resolve_message_notifications(
ChannelId,
Settings,
GuildDefaultNotifications
),
EffectiveLevel = override_for_large_guild(GuildId, Level, State),
should_allow_notification(
EffectiveLevel,
MessageData,
UserId,
Settings,
UserRolesMap
)
end
end.
should_allow_notification(Level, MessageData, UserId, Settings, UserRolesMap) ->
case Level of
?MESSAGE_NOTIFICATIONS_NO_MESSAGES ->
false;
?MESSAGE_NOTIFICATIONS_ONLY_MENTIONS ->
case is_private_channel(MessageData) of
true ->
true;
false ->
is_user_mentioned(UserId, MessageData, Settings, UserRolesMap)
end;
_ ->
true
end.
is_private_channel(MessageData) ->
ChannelType = maps:get(<<"channel_type">>, MessageData, ?CHANNEL_TYPE_DM),
ChannelType =:= ?CHANNEL_TYPE_DM orelse ChannelType =:= ?CHANNEL_TYPE_GROUP_DM.
is_user_mentioned(UserId, MessageData, Settings, UserRolesMap) ->
MentionEveryone = maps:get(<<"mention_everyone">>, MessageData, false),
SuppressEveryone = maps:get(suppress_everyone, Settings, false),
SuppressRoles = maps:get(suppress_roles, Settings, false),
case {MentionEveryone, SuppressEveryone} of
{true, false} ->
true;
{true, true} ->
false;
_ ->
Mentions = maps:get(<<"mentions">>, MessageData, []),
MentionRoles = maps:get(<<"mention_roles">>, MessageData, []),
UserRoles = maps:get(UserId, UserRolesMap, []),
is_user_in_mentions(UserId, Mentions) orelse
case SuppressRoles of
true -> false;
false -> has_mentioned_role(UserRoles, MentionRoles)
end
end.
is_user_in_mentions(UserId, Mentions) ->
lists:any(fun(Mention) -> mention_matches_user(UserId, Mention) end, Mentions).
mention_matches_user(UserId, Mention) ->
case maps:get(<<"id">>, Mention, undefined) of
undefined ->
false;
Id when is_integer(Id) ->
Id =:= UserId;
Id when is_binary(Id) ->
case validation:validate_snowflake(<<"mention.id">>, Id) of
{ok, ParsedId} -> ParsedId =:= UserId;
_ -> false
end;
_ -> false
end.
has_mentioned_role([], _) ->
false;
has_mentioned_role([RoleId | Rest], MentionRoles) ->
case role_in_mentions(RoleId, MentionRoles) of
true -> true;
false -> has_mentioned_role(Rest, MentionRoles)
end.
role_in_mentions(RoleId, MentionRoles) ->
RoleBin = integer_to_binary(RoleId),
lists:any(
fun(MentionRole) ->
case MentionRole of
Value when is_integer(Value) ->
Value =:= RoleId;
Value when is_binary(Value) ->
Value =:= RoleBin;
_ ->
false
end
end,
MentionRoles
).
resolve_message_notifications(ChannelId, Settings, GuildDefaultNotifications) ->
ChannelOverrides = maps:get(channel_overrides, Settings, #{}),
ChannelKey = integer_to_binary(ChannelId),
Level =
case maps:get(ChannelKey, ChannelOverrides, undefined) of
undefined ->
undefined;
Override ->
maps:get(message_notifications, Override, ?MESSAGE_NOTIFICATIONS_NULL)
end,
case Level of
?MESSAGE_NOTIFICATIONS_NULL -> resolve_guild_notification(Settings, GuildDefaultNotifications);
?MESSAGE_NOTIFICATIONS_INHERIT -> resolve_guild_notification(Settings, GuildDefaultNotifications);
undefined -> resolve_guild_notification(Settings, GuildDefaultNotifications);
Valid -> normalize_notification_level(Valid)
end.
resolve_guild_notification(Settings, GuildDefaultNotifications) ->
Level = maps:get(message_notifications, Settings, ?MESSAGE_NOTIFICATIONS_NULL),
case Level of
?MESSAGE_NOTIFICATIONS_NULL -> normalize_notification_level(GuildDefaultNotifications);
?MESSAGE_NOTIFICATIONS_INHERIT -> normalize_notification_level(GuildDefaultNotifications);
Valid -> normalize_notification_level(Valid)
end.
normalize_notification_level(Level) when Level == ?MESSAGE_NOTIFICATIONS_ALL ->
Level;
normalize_notification_level(Level) when Level == ?MESSAGE_NOTIFICATIONS_ONLY_MENTIONS ->
Level;
normalize_notification_level(Level) when Level == ?MESSAGE_NOTIFICATIONS_NO_MESSAGES ->
Level;
normalize_notification_level(_) ->
?MESSAGE_NOTIFICATIONS_ALL.
override_for_large_guild(GuildId, CurrentLevel, _State) ->
case get_guild_large_metadata(GuildId) of
undefined ->
CurrentLevel;
#{member_count := Count, features := Features} ->
case is_large_guild(Count, Features) of
true -> enforce_only_mentions(CurrentLevel);
false -> CurrentLevel
end
end.
enforce_only_mentions(CurrentLevel) ->
case CurrentLevel of
0 -> 1;
_ -> CurrentLevel
end.
is_large_guild(Count, Features) when is_integer(Count) ->
Count > ?LARGE_GUILD_THRESHOLD orelse has_large_guild_override(Features);
is_large_guild(_, Features) ->
has_large_guild_override(Features).
has_large_guild_override(Features) when is_list(Features) ->
lists:member(?LARGE_GUILD_OVERRIDE_FEATURE, Features);
has_large_guild_override(_) ->
false.
get_guild_large_metadata(GuildId) ->
GuildName = process_registry:build_process_name(guild, GuildId),
try
case whereis(GuildName) of
undefined ->
undefined;
Pid when is_pid(Pid) ->
case gen_server:call(Pid, {get_large_guild_metadata}, 500) of
#{member_count := Count, features := Features} ->
#{member_count => Count, features => Features};
_ ->
undefined
end
end
catch
_:_ -> undefined
end.