Packages

phoenix_kit

1.7.21
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules connections connections.ex
Raw

lib/modules/connections/connections.ex

defmodule PhoenixKit.Modules.Connections do
@moduledoc """
Connections module for PhoenixKit - Social Relationships System.
Provides a complete social relationships system with two types of relationships:
1. **Follows** - One-way relationships (User A follows User B, no consent needed)
2. **Connections** - Two-way mutual relationships (both users must accept)
Plus **blocking** functionality to prevent unwanted interactions.
## Public API
This is a **PUBLIC API** - all functions are available to parent applications
for use in their own views, components, and logic.
## Usage Examples
### In a User Profile Page
alias PhoenixKit.Modules.Connections
# Get relationship for rendering follow/connect buttons
relationship = Connections.get_relationship(current_user, profile_user)
# Display counts
followers = Connections.followers_count(profile_user)
following = Connections.following_count(profile_user)
connections = Connections.connections_count(profile_user)
### In a LiveView
def handle_event("follow", %{"user_id" => user_id}, socket) do
target_user = get_user(user_id)
case Connections.follow(socket.assigns.current_user, target_user) do
{:ok, _follow} -> {:noreply, put_flash(socket, :info, "Now following!")}
{:error, reason} -> {:noreply, put_flash(socket, :error, reason)}
end
end
## Business Rules
### Following
- Cannot follow yourself
- Cannot follow if blocked (either direction)
- Instant, no approval needed
### Connections
- Cannot connect with yourself
- Cannot connect if blocked
- Requires acceptance from recipient
- If A requests B while B has pending request to A → auto-accept both
### Blocking
- Blocking removes any existing follow/connection between the users
- Blocked user cannot follow, connect, or view profile
- Blocking is one-way (A blocks B doesn't mean B blocks A)
"""
import Ecto.Query, warn: false
alias PhoenixKit.Modules.Connections.Block
alias PhoenixKit.Modules.Connections.BlockHistory
alias PhoenixKit.Modules.Connections.Connection
alias PhoenixKit.Modules.Connections.ConnectionHistory
alias PhoenixKit.Modules.Connections.Follow
alias PhoenixKit.Modules.Connections.FollowHistory
alias PhoenixKit.Settings
# ===== MODULE STATUS =====
@doc """
Checks if the Connections module is enabled.
## Examples
iex> PhoenixKit.Modules.Connections.enabled?()
true
"""
def enabled? do
Settings.get_boolean_setting("connections_enabled", false)
end
@doc """
Enables the Connections module.
"""
def enable_system do
Settings.update_boolean_setting("connections_enabled", true)
end
@doc """
Disables the Connections module.
"""
def disable_system do
Settings.update_boolean_setting("connections_enabled", false)
end
@doc """
Returns the Connections module configuration.
Used by the Modules admin page to display module status and statistics.
## Returns
A map containing:
- `:enabled` - Whether the module is enabled
- `:follows_count` - Total number of follows
- `:connections_count` - Total number of accepted connections
- `:pending_count` - Total number of pending connection requests
- `:blocks_count` - Total number of blocks
## Examples
iex> Connections.get_config()
%{
enabled: true,
follows_count: 100,
connections_count: 50,
pending_count: 5,
blocks_count: 3
}
"""
def get_config do
%{
enabled: enabled?(),
follows_count: get_total_follows_count(),
connections_count: get_total_connections_count(),
pending_count: get_total_pending_count(),
blocks_count: get_total_blocks_count()
}
end
@doc """
Returns statistics for the admin overview page.
## Returns
A map containing:
- `:follows` - Total follows across all users
- `:connections` - Total accepted connections
- `:pending` - Total pending connection requests
- `:blocks` - Total blocks
## Examples
iex> Connections.get_stats()
%{follows: 100, connections: 50, pending: 5, blocks: 3}
"""
def get_stats do
%{
follows: get_total_follows_count(),
connections: get_total_connections_count(),
pending: get_total_pending_count(),
blocks: get_total_blocks_count()
}
end
# ===== FOLLOWS =====
@doc """
Creates a follow relationship.
User A follows User B. No consent is required from User B.
## Parameters
- `follower` - The user who is following (struct with id)
- `followed` - The user being followed (struct with id)
## Returns
- `{:ok, %Follow{}}` - Follow created successfully
- `{:error, :blocked}` - Cannot follow due to block
- `{:error, :self_follow}` - Cannot follow yourself
- `{:error, %Ecto.Changeset{}}` - Validation error
## Examples
iex> Connections.follow(current_user, target_user)
{:ok, %Follow{}}
iex> Connections.follow(user, user)
{:error, :self_follow}
"""
def follow(follower, followed) do
follower_id = get_user_id(follower)
followed_id = get_user_id(followed)
cond do
follower_id == followed_id ->
{:error, :self_follow}
blocked?(followed_id, follower_id) || blocked?(follower_id, followed_id) ->
{:error, :blocked}
following?(follower_id, followed_id) ->
{:error, :already_following}
true ->
repo().transaction(fn ->
case %Follow{}
|> Follow.changeset(%{follower_id: follower_id, followed_id: followed_id})
|> repo().insert() do
{:ok, follow} ->
log_follow_history(follower_id, followed_id, "follow")
follow
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
end
@doc """
Removes a follow relationship.
## Parameters
- `follower` - The user who is following
- `followed` - The user being followed
## Returns
- `{:ok, %Follow{}}` - Follow removed successfully
- `{:error, :not_following}` - No follow relationship exists
"""
def unfollow(follower, followed) do
follower_id = get_user_id(follower)
followed_id = get_user_id(followed)
case get_follow(follower_id, followed_id) do
nil ->
{:error, :not_following}
follow ->
repo().transaction(fn ->
case repo().delete(follow) do
{:ok, deleted} ->
log_follow_history(follower_id, followed_id, "unfollow")
deleted
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
end
@doc """
Checks if user A is following user B.
## Examples
iex> Connections.following?(user_a, user_b)
true
"""
def following?(follower, followed) do
follower_id = get_user_id(follower)
followed_id = get_user_id(followed)
Follow
|> where([f], f.follower_id == ^follower_id and f.followed_id == ^followed_id)
|> repo().exists?()
end
@doc """
Returns all followers of a user.
## Options
- `:preload` - Preload the follower user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
## Examples
iex> Connections.list_followers(user)
[%Follow{follower: %User{}}]
"""
def list_followers(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Follow
|> where([f], f.followed_id == ^user_id)
|> order_by([f], desc: f.inserted_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:follower]), else: query
repo().all(query)
end
@doc """
Returns all users that a user is following.
## Options
- `:preload` - Preload the followed user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
## Examples
iex> Connections.list_following(user)
[%Follow{followed: %User{}}]
"""
def list_following(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Follow
|> where([f], f.follower_id == ^user_id)
|> order_by([f], desc: f.inserted_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:followed]), else: query
repo().all(query)
end
@doc """
Returns the count of followers for a user.
## Examples
iex> Connections.followers_count(user)
42
"""
def followers_count(user) do
user_id = get_user_id(user)
Follow
|> where([f], f.followed_id == ^user_id)
|> repo().aggregate(:count, :id)
end
@doc """
Returns the count of users that a user is following.
## Examples
iex> Connections.following_count(user)
100
"""
def following_count(user) do
user_id = get_user_id(user)
Follow
|> where([f], f.follower_id == ^user_id)
|> repo().aggregate(:count, :id)
end
# ===== CONNECTIONS =====
@doc """
Sends a connection request from requester to recipient.
If recipient already has a pending request to requester, both requests
are automatically accepted.
## Parameters
- `requester` - The user sending the request
- `recipient` - The user receiving the request
## Returns
- `{:ok, %Connection{status: "pending"}}` - Request sent
- `{:ok, %Connection{status: "accepted"}}` - Auto-accepted (mutual request)
- `{:error, :blocked}` - Cannot connect due to block
- `{:error, :self_connection}` - Cannot connect with yourself
- `{:error, :already_connected}` - Already connected
- `{:error, :pending_request}` - Already has pending request
"""
def request_connection(requester, recipient) do
requester_id = get_user_id(requester)
recipient_id = get_user_id(recipient)
cond do
requester_id == recipient_id ->
{:error, :self_connection}
blocked?(requester_id, recipient_id) || blocked?(recipient_id, requester_id) ->
{:error, :blocked}
connected?(requester_id, recipient_id) ->
{:error, :already_connected}
true ->
# Check if there's a pending request from recipient to requester
case get_pending_request_between(recipient_id, requester_id) do
%Connection{} = existing ->
# Auto-accept the existing request (mutual request)
# The accept_connection will log the "accepted" history entry
accept_connection_with_actor(existing, requester_id)
nil ->
# Check if there's already a pending request from requester to recipient
case get_pending_request_between(requester_id, recipient_id) do
%Connection{} ->
{:error, :pending_request}
nil ->
# Create new pending request
create_pending_connection(requester_id, recipient_id)
end
end
end
end
@doc """
Accepts a pending connection request.
## Parameters
- `connection_or_id` - Connection struct or connection ID
## Returns
- `{:ok, %Connection{status: "accepted"}}` - Request accepted
- `{:error, :not_found}` - Connection not found
- `{:error, :not_pending}` - Connection is not pending
"""
def accept_connection(%Connection{status: "pending"} = connection) do
# When called directly, the actor is the recipient (who accepts)
accept_connection_with_actor(connection, connection.recipient_id)
end
def accept_connection(%Connection{}), do: {:error, :not_pending}
def accept_connection(connection_id) when is_binary(connection_id) do
case repo().get(Connection, connection_id) do
nil -> {:error, :not_found}
connection -> accept_connection(connection)
end
end
# Internal function that tracks the actor for history
defp accept_connection_with_actor(%Connection{status: "pending"} = connection, actor_id) do
repo().transaction(fn ->
case connection
|> Connection.status_changeset(%{status: "accepted"})
|> repo().update() do
{:ok, updated} ->
log_connection_history(
connection.requester_id,
connection.recipient_id,
actor_id,
"accepted"
)
updated
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
defp accept_connection_with_actor(%Connection{}, _actor_id), do: {:error, :not_pending}
@doc """
Rejects a pending connection request.
## Parameters
- `connection_or_id` - Connection struct or connection ID
## Returns
- `{:ok, %Connection{status: "rejected"}}` - Request rejected
- `{:error, :not_found}` - Connection not found
- `{:error, :not_pending}` - Connection is not pending
"""
def reject_connection(%Connection{status: "pending"} = connection) do
repo().transaction(fn ->
# Log history before deleting (rejected connections are removed from main table)
log_connection_history(
connection.requester_id,
connection.recipient_id,
connection.recipient_id,
"rejected"
)
# Delete instead of updating to rejected status
case repo().delete(connection) do
{:ok, deleted} -> deleted
{:error, changeset} -> repo().rollback(changeset)
end
end)
end
def reject_connection(%Connection{}), do: {:error, :not_pending}
def reject_connection(connection_id) when is_binary(connection_id) do
case repo().get(Connection, connection_id) do
nil -> {:error, :not_found}
connection -> reject_connection(connection)
end
end
@doc """
Removes an existing connection between two users.
Either user can remove the connection.
## Parameters
- `user_a` - First user
- `user_b` - Second user
## Returns
- `{:ok, %Connection{}}` - Connection removed
- `{:error, :not_connected}` - No connection exists
"""
def remove_connection(user_a, user_b) do
user_a_id = get_user_id(user_a)
user_b_id = get_user_id(user_b)
case get_accepted_connection(user_a_id, user_b_id) do
nil ->
{:error, :not_connected}
connection ->
repo().transaction(fn ->
# Log history - user_a is the actor (the one removing)
log_connection_history(
connection.requester_id,
connection.recipient_id,
user_a_id,
"removed"
)
case repo().delete(connection) do
{:ok, deleted} -> deleted
{:error, changeset} -> repo().rollback(changeset)
end
end)
end
end
@doc """
Checks if two users are connected (mutual connection exists).
## Examples
iex> Connections.connected?(user_a, user_b)
true
"""
def connected?(user_a, user_b) do
user_a_id = get_user_id(user_a)
user_b_id = get_user_id(user_b)
Connection
|> where([c], c.status == "accepted")
|> where(
[c],
(c.requester_id == ^user_a_id and c.recipient_id == ^user_b_id) or
(c.requester_id == ^user_b_id and c.recipient_id == ^user_a_id)
)
|> repo().exists?()
end
@doc """
Returns all connections for a user.
## Options
- `:preload` - Preload the other user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
## Examples
iex> Connections.list_connections(user)
[%Connection{requester: %User{}, recipient: %User{}}]
"""
def list_connections(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Connection
|> where([c], c.status == "accepted")
|> where([c], c.requester_id == ^user_id or c.recipient_id == ^user_id)
|> order_by([c], desc: c.responded_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:requester, :recipient]), else: query
repo().all(query)
end
@doc """
Returns pending incoming connection requests for a user.
## Options
- `:preload` - Preload the requester user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
"""
def list_pending_requests(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Connection
|> where([c], c.recipient_id == ^user_id and c.status == "pending")
|> order_by([c], desc: c.requested_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:requester]), else: query
repo().all(query)
end
@doc """
Returns pending outgoing connection requests sent by a user.
## Options
- `:preload` - Preload the recipient user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
"""
def list_sent_requests(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Connection
|> where([c], c.requester_id == ^user_id and c.status == "pending")
|> order_by([c], desc: c.requested_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:recipient]), else: query
repo().all(query)
end
@doc """
Returns the count of connections for a user.
## Examples
iex> Connections.connections_count(user)
50
"""
def connections_count(user) do
user_id = get_user_id(user)
Connection
|> where([c], c.status == "accepted")
|> where([c], c.requester_id == ^user_id or c.recipient_id == ^user_id)
|> repo().aggregate(:count, :id)
end
@doc """
Returns the count of pending incoming connection requests for a user.
## Examples
iex> Connections.pending_requests_count(user)
5
"""
def pending_requests_count(user) do
user_id = get_user_id(user)
Connection
|> where([c], c.recipient_id == ^user_id and c.status == "pending")
|> repo().aggregate(:count, :id)
end
# ===== BLOCKS =====
@doc """
Blocks a user.
Blocking removes any existing follows and connections between the users.
## Parameters
- `blocker` - The user who is blocking
- `blocked` - The user being blocked
- `reason` - Optional reason for the block
## Returns
- `{:ok, %Block{}}` - Block created successfully
- `{:error, :self_block}` - Cannot block yourself
- `{:error, :already_blocked}` - User is already blocked
"""
def block(blocker, blocked, reason \\ nil) do
blocker_id = get_user_id(blocker)
blocked_id = get_user_id(blocked)
cond do
blocker_id == blocked_id ->
{:error, :self_block}
blocked?(blocker_id, blocked_id) ->
{:error, :already_blocked}
true ->
repo().transaction(fn ->
# Remove any existing follows (both directions) - log history for each
remove_follows_between_with_history(blocker_id, blocked_id)
# Remove any existing connections - log history
remove_connections_between_with_history(blocker_id, blocked_id)
# Create the block
attrs = %{blocker_id: blocker_id, blocked_id: blocked_id, reason: reason}
case %Block{} |> Block.changeset(attrs) |> repo().insert() do
{:ok, block} ->
log_block_history(blocker_id, blocked_id, "block", reason)
block
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
end
@doc """
Removes a block.
## Parameters
- `blocker` - The user who blocked
- `blocked` - The user who was blocked
## Returns
- `{:ok, %Block{}}` - Block removed
- `{:error, :not_blocked}` - No block exists
"""
def unblock(blocker, blocked) do
blocker_id = get_user_id(blocker)
blocked_id = get_user_id(blocked)
case get_block(blocker_id, blocked_id) do
nil ->
{:error, :not_blocked}
block ->
repo().transaction(fn ->
case repo().delete(block) do
{:ok, deleted} ->
log_block_history(blocker_id, blocked_id, "unblock", nil)
deleted
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
end
@doc """
Checks if user A has blocked user B.
## Examples
iex> Connections.blocked?(user_a, user_b)
true
"""
def blocked?(blocker, blocked) do
blocker_id = get_user_id(blocker)
blocked_id = get_user_id(blocked)
Block
|> where([b], b.blocker_id == ^blocker_id and b.blocked_id == ^blocked_id)
|> repo().exists?()
end
@doc """
Checks if user is blocked by other user.
## Examples
iex> Connections.blocked_by?(user, other)
true
"""
def blocked_by?(user, other) do
blocked?(other, user)
end
@doc """
Returns all users blocked by a user.
## Options
- `:preload` - Preload the blocked user (default: true)
- `:limit` - Maximum number of results
- `:offset` - Number of results to skip
"""
def list_blocked(user, opts \\ []) do
user_id = get_user_id(user)
preload = Keyword.get(opts, :preload, true)
query =
Block
|> where([b], b.blocker_id == ^user_id)
|> order_by([b], desc: b.inserted_at)
|> maybe_limit(opts[:limit])
|> maybe_offset(opts[:offset])
query = if preload, do: preload(query, [:blocked]), else: query
repo().all(query)
end
@doc """
Checks if two users can interact (neither has blocked the other).
## Examples
iex> Connections.can_interact?(user_a, user_b)
true
"""
def can_interact?(user_a, user_b) do
user_a_id = get_user_id(user_a)
user_b_id = get_user_id(user_b)
not (blocked?(user_a_id, user_b_id) or blocked?(user_b_id, user_a_id))
end
# ===== RELATIONSHIP STATUS =====
@doc """
Gets the full relationship status between two users in one call.
## Parameters
- `user_a` - First user
- `user_b` - Second user
## Returns
A map containing:
- `:following` - Whether A follows B
- `:followed_by` - Whether B follows A
- `:connected` - Whether they have a mutual connection
- `:connection_pending` - `:sent`, `:received`, or `nil`
- `:blocked` - Whether A blocked B
- `:blocked_by` - Whether B blocked A
## Examples
iex> Connections.get_relationship(user_a, user_b)
%{
following: true,
followed_by: false,
connected: false,
connection_pending: :sent,
blocked: false,
blocked_by: false
}
"""
def get_relationship(user_a, user_b) do
user_a_id = get_user_id(user_a)
user_b_id = get_user_id(user_b)
%{
following: following?(user_a_id, user_b_id),
followed_by: following?(user_b_id, user_a_id),
connected: connected?(user_a_id, user_b_id),
connection_pending: get_connection_pending_status(user_a_id, user_b_id),
blocked: blocked?(user_a_id, user_b_id),
blocked_by: blocked?(user_b_id, user_a_id)
}
end
# ===== PRIVATE HELPERS =====
defp repo do
PhoenixKit.Config.get_repo()
end
defp get_user_id(%{id: id}), do: id
defp get_user_id(id) when is_integer(id), do: id
defp get_user_id(id) when is_binary(id), do: String.to_integer(id)
defp get_follow(follower_id, followed_id) do
Follow
|> where([f], f.follower_id == ^follower_id and f.followed_id == ^followed_id)
|> repo().one()
end
defp get_block(blocker_id, blocked_id) do
Block
|> where([b], b.blocker_id == ^blocker_id and b.blocked_id == ^blocked_id)
|> repo().one()
end
defp get_pending_request_between(requester_id, recipient_id) do
Connection
|> where([c], c.requester_id == ^requester_id and c.recipient_id == ^recipient_id)
|> where([c], c.status == "pending")
|> repo().one()
end
defp get_accepted_connection(user_a_id, user_b_id) do
Connection
|> where([c], c.status == "accepted")
|> where(
[c],
(c.requester_id == ^user_a_id and c.recipient_id == ^user_b_id) or
(c.requester_id == ^user_b_id and c.recipient_id == ^user_a_id)
)
|> repo().one()
end
defp get_connection_pending_status(user_a_id, user_b_id) do
# Check if user_a sent a pending request to user_b
sent =
Connection
|> where([c], c.requester_id == ^user_a_id and c.recipient_id == ^user_b_id)
|> where([c], c.status == "pending")
|> repo().exists?()
if sent do
:sent
else
# Check if user_a received a pending request from user_b
received =
Connection
|> where([c], c.requester_id == ^user_b_id and c.recipient_id == ^user_a_id)
|> where([c], c.status == "pending")
|> repo().exists?()
if received, do: :received, else: nil
end
end
defp maybe_limit(query, nil), do: query
defp maybe_limit(query, limit), do: limit(query, ^limit)
defp maybe_offset(query, nil), do: query
defp maybe_offset(query, offset), do: offset(query, ^offset)
# Total counts for statistics
defp get_total_follows_count do
Follow
|> repo().aggregate(:count, :id)
end
defp get_total_connections_count do
Connection
|> where([c], c.status == "accepted")
|> repo().aggregate(:count, :id)
end
defp get_total_pending_count do
Connection
|> where([c], c.status == "pending")
|> repo().aggregate(:count, :id)
end
defp get_total_blocks_count do
Block
|> repo().aggregate(:count, :id)
end
# ===== HISTORY LOGGING =====
defp log_follow_history(follower_id, followed_id, action) do
%FollowHistory{}
|> FollowHistory.changeset(%{
follower_id: follower_id,
followed_id: followed_id,
action: action
})
|> repo().insert!()
end
defp log_connection_history(user_a_id, user_b_id, actor_id, action) do
%ConnectionHistory{}
|> ConnectionHistory.changeset(%{
user_a_id: user_a_id,
user_b_id: user_b_id,
actor_id: actor_id,
action: action
})
|> repo().insert!()
end
# Create a new pending connection request with history logging
defp create_pending_connection(requester_id, recipient_id) do
repo().transaction(fn ->
case %Connection{}
|> Connection.changeset(%{
requester_id: requester_id,
recipient_id: recipient_id
})
|> repo().insert() do
{:ok, connection} ->
log_connection_history(requester_id, recipient_id, requester_id, "requested")
connection
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
defp log_block_history(blocker_id, blocked_id, action, reason) do
%BlockHistory{}
|> BlockHistory.changeset(%{
blocker_id: blocker_id,
blocked_id: blocked_id,
action: action,
reason: reason
})
|> repo().insert!()
end
# Remove follows between users with history logging
defp remove_follows_between_with_history(user_a_id, user_b_id) do
# Get follows in both directions
follows =
Follow
|> where(
[f],
(f.follower_id == ^user_a_id and f.followed_id == ^user_b_id) or
(f.follower_id == ^user_b_id and f.followed_id == ^user_a_id)
)
|> repo().all()
# Log history for each and delete
Enum.each(follows, fn follow ->
log_follow_history(follow.follower_id, follow.followed_id, "unfollow")
repo().delete!(follow)
end)
end
# Remove connections between users with history logging
defp remove_connections_between_with_history(user_a_id, user_b_id) do
# Get connection between users
connections =
Connection
|> where(
[c],
(c.requester_id == ^user_a_id and c.recipient_id == ^user_b_id) or
(c.requester_id == ^user_b_id and c.recipient_id == ^user_a_id)
)
|> repo().all()
# Log history for each and delete
Enum.each(connections, fn connection ->
log_connection_history(
connection.requester_id,
connection.recipient_id,
user_a_id,
"removed"
)
repo().delete!(connection)
end)
end
end