Skip to content

Instantly share code, notes, and snippets.

@mayel
Last active December 22, 2024 18:40
Show Gist options
  • Save mayel/880d98c7d70343f45515260267db6f76 to your computer and use it in GitHub Desktop.
Save mayel/880d98c7d70343f45515260267db6f76 to your computer and use it in GitHub Desktop.
defmodule Bonfire.Social.Feeds.Presets do
@moduledoc """
Preset feed definitions
"""
use Bonfire.Common.E
import Bonfire.Common.Utils
alias Bonfire.Common.Config
alias Bonfire.Common.Types
@type filter_params :: %{
feed_name: String.t() | nil,
feed_ids: String.t() | nil,
activity_types: list(String.t()) | nil,
#  TODO: rename to exclude_activity_types
exclude_verbs: list(String.t()) | nil,
object_types: list(String.t()) | nil,
media_types: list(String.t()) | nil,
subjects: list(String.t()) | nil,
objects: list(String.t()) | nil,
creators: list(String.t()) | nil,
tags: list(String.t()) | nil,
time_limit: integer() | nil,
sort_by: atom() | nil,
sort_order: :asc | :desc | nil
}
def feed_presets do
Config.get([__MODULE__, :preload_presets], %{
my: %{
description: "Activities of people I follow",
filters: %{feed_name: :my},
current_user_required: true
},
explore: %{
description: "All activities",
filters: %{feed_name: :explore},
exclude_verbs: [:like]
},
local: %{
description: "Local instance activities",
filters: %{feed_name: :local},
exclude_verbs: [:like]
},
remote: %{
description: "Remote/Fediverse activities",
filters: %{feed_name: :remote},
exclude_verbs: [:like]
},
notifications: %{
description: "Notifications for me",
filters: %{feed_name: :notifications},
current_user_required: true
},
messages: %{
description: "Messages for me",
filters: %{feed_name: :messages},
current_user_required: true
},
# User interaction feeds
liked_by_me: %{
description: "Posts I've liked",
filters: %{activity_types: :like, subjects: :me},
parameterized: true
},
my_bookmarks: %{
description: "Posts I've bookmarked",
filters: %{activity_types: :bookmark, subjects: :me},
current_user_required: true,
parameterized: true
},
my_requests: %{
description: "Pending requests for me",
filters: %{feed_name: :notifications, activity_types: :request},
current_user_required: true
},
# User-specific feeds
user_activities: %{
description: "A specific user's activities",
# $username is replaced at runtime
filters: %{subjects: :by},
parameterized: true
},
user_followers: %{
description: "Followers of a specific user",
filters: %{object_types: :follow, objects: :by},
parameterized: true
},
user_following: %{
description: "Users followed by a specific user",
filters: %{activity_types: :follow, subjects: :by},
parameterized: true
},
user_posts: %{
description: "Posts by a specific user",
filters: %{creators: :by, object_types: :post},
parameterized: true
},
# user_publications: %{
# description: "Publications by a specific user",
# filters: %{creators: :by, media_types: :publication},
# parameterized: true
# },
# Content type feeds
# publications: %{
# description: "All known publications",
# filters: %{media_types: :publication]}
# },
images: %{
description: "All known images",
filters: %{media_types: "image"}
},
# Hashtag feeds
hashtag: %{
description: "Activities with a specific hashtag",
filters: %{tags: :hashtag},
parameterized: true
},
mentions: %{
description: "Activities with a specific @ mention",
filters: %{tags: :mentioned},
parameterized: true
},
# Moderation feeds
flagged_by_me: %{
description: "Content I've flagged",
filters: %{activity_types: :flag, subjects: :me},
parameterized: true,
current_user_required: true
},
flagged_content: %{
description: "Content flagged by anyone (mods only)",
filters: %{activity_types: :flag},
current_user_required: true,
mod_required: true
},
# Combined filters examples
trending_discussions: %{
description: "Popular discussions from the last 7 days",
filters: %{
time_limit: 7,
sort_by: :num_replies,
sort_order: :desc
}
},
local_media: %{
description: "Media from local instance",
filters: %{
feed_name: :local,
media_types: "*"
}
}
})
end
@doc """
Gets an aliased feed's filters by name, with optional parameters.
## Examples
# 1: Retrieve a preset feed without parameters
iex> preset_feed_filters("local", [])
{:ok, %{feed_name: :local}}
# 1: Retrieve a preset feed without parameters
iex> preset_feed_filters(:local, [])
{:ok, %{feed_name: :local}}
# 2: Retrieve a preset feed with parameters
iex> preset_feed_filters("user_activities", [by: "alice"])
{:ok, %{subjects: "alice"}}
# 3: Feed not found (error case)
iex> preset_feed_filters("unknown_feed", [])
{:error, :not_found}
# 4: Preset feed with parameterized filters
iex> preset_feed_filters("liked_by_me", current_user: %{id: "alice"})
{:ok, %{activity_types: :like, subjects: %{id: "alice"}}}
# 5: Feed with `current_user_required` should check for current user
iex> preset_feed_filters("messages", current_user: %{id: "alice"})
{:ok, %{feed_name: :messages}}
# 6: Feed with `current_user_required` and no current user
iex> preset_feed_filters("messages", [])
** (Bonfire.Fail.Auth) You need to log in first.
# 7: Custom feed with additional parameters
iex> preset_feed_filters("user_followers", [by: "alice"])
{:ok, %{object_types: :follow, objects: "alice"}}
"""
@spec preset_feed_filters(String.t(), map()) :: {:ok, filter_params()} | {:error, atom()}
def preset_feed_filters(name, opts \\ []) do
case feed_definition_if_permitted(name, opts) do
{:error, e} ->
{:error, e}
{:ok, %{parameterized: true, filters: filters}} ->
{:ok, parameterize_filters(filters, opts)}
{:ok, %{filters: filters}} ->
{:ok, filters}
end
end
defp feed_definition_if_permitted(name, opts) when is_atom(name) do
case feed_presets()[name] do
nil ->
{:error, :not_found}
# %{admin_required: true} = alias when not user.is_admin ->
# {:error, :unauthorized} # TODO
# %{mod_required: true} = alias when not user.is_moderator ->
# {:error, :unauthorized} # TODO
%{current_user_required: true} = feed_def ->
if current_user_required!(opts), do: {:ok, feed_def}
feed_def ->
{:ok, feed_def}
end
end
defp feed_definition_if_permitted(name, opts) do
case Types.maybe_to_atom!(name) do
nil ->
{:error, :not_found}
name ->
feed_definition_if_permitted(name, opts)
end
end
@doc """
Parameterizes the filters by replacing parameterized values with values from `opts`.
## Examples
# 1: Parameterizing a simple filter
iex> parameterize_filters(%{subjects: [:me]}, current_user: %{id: "alice"})
%{subjects: [%{id: "alice"}]}
# 2: Parameterizing multiple filters
iex> parameterize_filters(%{subjects: :me, tags: [:hashtag]}, current_user: %{id: "alice"}, hashtag: "elixir")
%{subjects: %{id: "alice"}, tags: ["elixir"]}
# 3: Parameterizing with undefined options
iex> parameterize_filters(%{subjects: :me}, current_user: nil)
%{subjects: nil}
# 4: Handling filters that don't require parameterization
iex> parameterize_filters(%{activity_types: ["like"]}, current_user: "bob")
%{activity_types: ["like"]}
"""
def parameterize_filters(filters, opts) do
filters
|> Enum.map(fn
{k, v} when is_list(v) ->
{k, Enum.map(v, &replace_parameters(&1, opts))}
{k, v} ->
{k, replace_parameters(v, opts)}
end)
|> Enum.into(%{})
end
@doc """
Replaces parameters in the filter value with the actual values from `opts`.
## Examples
# 1: Replacing a `me` parameter with the current user
iex> replace_parameters(:me, current_user: %{id: "alice"})
%{id: "alice"}
# 2: Replacing a `:current_user` parameter with the current user only if available
iex> replace_parameters(:current_user, current_user: nil)
nil
# 3: Failing with `:current_user_required` parameter if we have no current user
iex> replace_parameters(:current_user_required, current_user: nil)
** (Bonfire.Fail.Auth) You need to log in first.
# 4: Handling a parameter that is not in the opts
iex> replace_parameters(:unknown, current_user: "bob")
:unknown
"""
def replace_parameters(:current_user, opts) do
current_user(opts)
end
def replace_parameters(:current_user_required, opts) do
current_user_required!(opts)
end
def replace_parameters(:me, opts) do
current_user(opts)
end
def replace_parameters(value, opts) do
ed(opts, value, value)
end
def replace_parameters(value, _params), do: value
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment