4.0 KiB · text History 7a00c02
//! Friends: mutual, by request and accept. A `friendships` row exists per
//! pair in the direction it was asked; friends are the accepted rows with
//! you on either side.
use std::collections::HashSet;

use chrono::Utc;
use sea_orm::sea_query::{Condition, Expr};
use sea_orm::*;
use serde::Serialize;
use uuid::Uuid;

use crate::entities::friendships::{self, FriendStatus};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Link {
    None,
    Friends,
    /// You asked them.
    Requested,
    /// They asked you.
    Incoming,
}

fn between(a: Uuid, b: Uuid) -> Condition {
    Condition::any()
        .add(
            Condition::all()
                .add(friendships::Column::RequesterId.eq(a))
                .add(friendships::Column::AddresseeId.eq(b)),
        )
        .add(
            Condition::all()
                .add(friendships::Column::RequesterId.eq(b))
                .add(friendships::Column::AddresseeId.eq(a)),
        )
}

pub async fn link(db: &impl ConnectionTrait, me: Uuid, other: Uuid) -> Result<Link, DbErr> {
    Ok(match friendships::Entity::find().filter(between(me, other)).one(db).await? {
        None => Link::None,
        Some(f) if f.status == FriendStatus::Accepted => Link::Friends,
        Some(f) if f.requester_id == me => Link::Requested,
        Some(_) => Link::Incoming,
    })
}

/// Everyone `me` has any link with, and what it is.
pub async fn links(db: &impl ConnectionTrait, me: Uuid) -> Result<Vec<(Uuid, Link)>, DbErr> {
    Ok(friendships::Entity::find()
        .filter(
            Condition::any()
                .add(friendships::Column::RequesterId.eq(me))
                .add(friendships::Column::AddresseeId.eq(me)),
        )
        .all(db)
        .await?
        .into_iter()
        .map(|f| {
            let (other, link) = if f.status == FriendStatus::Accepted {
                (if f.requester_id == me { f.addressee_id } else { f.requester_id }, Link::Friends)
            } else if f.requester_id == me {
                (f.addressee_id, Link::Requested)
            } else {
                (f.requester_id, Link::Incoming)
            };
            (other, link)
        })
        .collect())
}

pub async fn friend_ids(db: &impl ConnectionTrait, me: Uuid) -> Result<HashSet<Uuid>, DbErr> {
    Ok(links(db, me).await?.into_iter().filter(|(_, l)| *l == Link::Friends).map(|(u, _)| u).collect())
}

/// Ask to be friends. If they'd already asked you, this accepts instead.
pub async fn request(db: &impl ConnectionTrait, me: Uuid, other: Uuid) -> Result<Link, DbErr> {
    if me == other {
        return Ok(Link::None);
    }
    match link(db, me, other).await? {
        Link::None => {
            friendships::ActiveModel {
                requester_id: Set(me),
                addressee_id: Set(other),
                status: Set(FriendStatus::Pending),
                created_at: Set(Utc::now().into()),
                accepted_at: Set(None),
            }
            .insert(db)
            .await?;
            Ok(Link::Requested)
        }
        Link::Incoming => accept(db, me, other).await,
        l => Ok(l),
    }
}

/// Accept `requester`'s request to `me`.
pub async fn accept(db: &impl ConnectionTrait, me: Uuid, requester: Uuid) -> Result<Link, DbErr> {
    let updated = friendships::Entity::update_many()
        .col_expr(friendships::Column::Status, Expr::value(FriendStatus::Accepted.to_value()))
        .col_expr(friendships::Column::AcceptedAt, Expr::value(Utc::now()))
        .filter(friendships::Column::RequesterId.eq(requester))
        .filter(friendships::Column::AddresseeId.eq(me))
        .filter(friendships::Column::Status.eq(FriendStatus::Pending))
        .exec(db)
        .await?;
    if updated.rows_affected == 0 { link(db, me, requester).await } else { Ok(Link::Friends) }
}

/// Decline, cancel, or unfriend — whichever applies, the link is gone.
pub async fn remove(db: &impl ConnectionTrait, me: Uuid, other: Uuid) -> Result<(), DbErr> {
    friendships::Entity::delete_many().filter(between(me, other)).exec(db).await?;
    Ok(())
}