defmodule Clacks.Timeline do alias Clacks.{Repo, Actor, Activity, Object, User, Notification} import Clacks.Paginator import Ecto.Query @public "https://www.w3.org/ns/activitystreams#Public" @timeline_types ["Create", "Announce"] @notification_types ["Create", "Announce", "Like", "Follow"] @spec actor_timeline( actor :: Actor.t(), params :: map(), only_public :: boolean() ) :: [ {Activity.t(), Actor.t(), {Activity.t() | nil, Actor.t() | nil}} ] def actor_timeline(actor, params, only_public \\ true) do Activity |> restrict_to_actor(actor.ap_id) |> restrict_to_types(@timeline_types) |> restrict_to_public(only_public) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) |> Activity.preload_object() |> join_with_announced_or_liked() |> select( [activity, announced, announced_actor], {activity, {announced, announced_actor}} ) |> Repo.all() end @spec home_timeline(user :: User.t(), params :: map()) :: [ {Activity.t(), Actor.t(), {Activity.t() | nil, Actor.t() | nil}} ] def home_timeline(user, params) do user = case user.actor do %Ecto.Association.NotLoaded{} -> Repo.preload(user, :actor) _ -> user end Activity |> join_with_actors() |> where( [activity, actor], fragment("?->>'actor'", activity.data) == ^user.actor.ap_id or ^user.actor.ap_id in actor.followers ) |> restrict_to_types(@timeline_types) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) |> Activity.preload_object() |> join_with_announced_or_liked() |> select( [activity, actor, announced, announced_actor], {activity, actor, {announced, announced_actor}} ) |> Repo.all() end @spec local_timeline(params :: map()) :: [ {Activity.t(), Actor.t(), {Activity.t() | nil, Actor.t() | nil}} ] def local_timeline(params) do Activity |> where([a], a.local) |> restrict_to_public(true) |> restrict_to_types(@timeline_types) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) |> Activity.preload_object() |> join_with_actors() |> join_with_announced_or_liked() |> select( [activity, _object, actor, announced, announced_actor], {activity, actor, {announced, announced_actor}} ) |> Repo.all() end @spec notifications(Actor.t(), map()) :: [Notification.t()] def notifications(actor, params) do Notification |> where([n], n.user_id == ^actor.user_id) |> paginate(params) |> limit(^Map.get(params, "limit", 20)) |> join(:left, [n], a in Activity, as: :activity, on: a.id == n.activity_id ) |> join(:left, [n], a in Activity, as: :referenced_activity, on: a.id == n.referenced_activity_id ) |> join(:left, [n, activity: a], o in Object, as: :object, on: fragment("?->>'id'", o.data) == fragment("COALESCE(?->'object'->>'id', ?->>'object')", a.data, a.data) ) |> join(:left, [n, referenced_activity: a], o in Object, as: :referenced_object, on: fragment("?->>'id'", o.data) == fragment("COALESCE(?->'object'->>'id', ?->>'object')", a.data, a.data) ) |> join(:left, [n, activity: activity], actor in Actor, as: :activity_actor, on: activity.actor_ap_id == actor.ap_id ) # note: we shouldn't need to load the actor for the referenced_activity here, # because notifications for a given actor always reference activities from that same actor |> select( [ n, activity: activity, referenced_activity: referenced_activity, object: object, referenced_object: referenced_object, activity_actor: activity_actor ], %Notification{ n | activity: %Activity{activity | object: object, actor: activity_actor}, referenced_activity: %Activity{referenced_activity | object: referenced_object} } ) |> Repo.all() end defp restrict_to_actor(query, actor_id) do where(query, [a], fragment("?->>'actor'", a.data) == ^actor_id) end defp restrict_to_types(query, types) do where(query, [a], fragment("?->>'type'", a.data) in ^types) end defp restrict_to_public(query, true) do where( query, [a], fragment("?->'to' \\? ?", a.data, @public) or fragment("?->'cc' \\? ?", a.data, @public) ) end defp restrict_to_public(query, false), do: query defp join_with_actors(query) do query |> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data)) end defp join_with_announced_or_liked(query) do query |> join(:left, [a], other in Activity, on: fragment("?->>'type'", a.data) in ["Announce", "Like"] and fragment("?->>'type'", other.data) == "Create" and fragment("?->>'object'", a.data) == fragment("?->'object'->>'id'", other.data) ) |> join(:left, [a, ..., announced], actor in Actor, on: actor.ap_id == fragment("?->>'actor'", announced.data) ) end end