//! The friends page: your friends, requests both ways, and people you
//! share an area with as suggestions. Friends shape the "who's going"
//! pickers and whom admins are offered to invite. See `crate::friends`.
use std::collections::{HashMap, HashSet};
use axum::extract::{Form, Path, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, Redirect};
use minijinja::context;
use sea_orm::*;
use serde::Deserialize;
use uuid::Uuid;
use super::{internal, non_empty, render};
use crate::entities::{area_members, users};
use crate::friends::{self, Link};
use crate::middleware::UserContext;
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct Notice {
pub notice: Option<String>,
}
pub async fn show_friends(
State(state): State<AppState>,
user: UserContext,
Query(q): Query<Notice>,
) -> Result<Html<String>, StatusCode> {
let db = state.db.as_ref();
let me = user.user.id;
let links: HashMap<Uuid, Link> =
friends::links(db, me).await.map_err(internal("load friends failed"))?.into_iter().collect();
// People you share at least one area with, not yet linked: the likeliest
// friends, and the only strangers this page will name.
let my_areas: Vec<Uuid> = area_members::Entity::find()
.filter(area_members::Column::UserId.eq(me))
.all(db)
.await
.map_err(internal("load areas failed"))?
.into_iter()
.map(|m| m.area_id)
.collect();
let mut shared: HashMap<Uuid, usize> = HashMap::new();
for m in area_members::Entity::find()
.filter(area_members::Column::AreaId.is_in(my_areas))
.filter(area_members::Column::UserId.ne(me))
.all(db)
.await
.map_err(internal("load co-members failed"))?
{
*shared.entry(m.user_id).or_default() += 1;
}
let ids: HashSet<Uuid> = links.keys().chain(shared.keys()).copied().collect();
let people = users::Entity::find()
.filter(users::Column::Id.is_in(ids))
.filter(users::Column::IsActive.eq(true))
.order_by_asc(users::Column::Username)
.all(db)
.await
.map_err(internal("load users failed"))?;
let pick = |want: Link| -> Vec<minijinja::Value> {
people
.iter()
.filter(|u| links.get(&u.id).copied().unwrap_or(Link::None) == want)
.filter(|u| want != Link::None || shared.contains_key(&u.id))
.map(|u| context! { id => u.id, username => u.username, shared_areas => shared.get(&u.id).copied().unwrap_or(0) })
.collect()
};
render(
&state,
"friends.html",
context! {
title => "Friends",
user => user.user,
friends => pick(Link::Friends),
incoming => pick(Link::Incoming),
outgoing => pick(Link::Requested),
suggestions => pick(Link::None),
notice => q.notice,
},
)
}
#[derive(Debug, Deserialize)]
pub struct RequestForm {
pub username: Option<String>,
pub user_id: Option<Uuid>,
}
pub async fn request(
State(state): State<AppState>,
user: UserContext,
Form(form): Form<RequestForm>,
) -> Result<Redirect, StatusCode> {
let db = state.db.as_ref();
let mut q = users::Entity::find().filter(users::Column::IsActive.eq(true));
q = match (form.user_id, non_empty(form.username)) {
(Some(id), _) => q.filter(users::Column::Id.eq(id)),
(None, Some(name)) => q.filter(users::Column::Username.eq(name)),
(None, None) => return Err(StatusCode::BAD_REQUEST),
};
let Some(other) = q.one(db).await.map_err(internal("load user failed"))? else {
return Ok(Redirect::to("/friends?notice=no-such-user"));
};
if other.id == user.user.id {
return Ok(Redirect::to("/friends?notice=self"));
}
let link = friends::request(db, user.user.id, other.id).await.map_err(internal("friend request failed"))?;
Ok(Redirect::to(match link {
Link::Friends => "/friends?notice=friends",
_ => "/friends?notice=requested",
}))
}
/// `accept` a request from them, or `remove` the link (decline, cancel,
/// unfriend).
pub async fn answer(
State(state): State<AppState>,
user: UserContext,
Path((other, action)): Path<(Uuid, String)>,
) -> Result<Redirect, StatusCode> {
let db = state.db.as_ref();
match action.as_str() {
"accept" => {
friends::accept(db, user.user.id, other).await.map_err(internal("accept failed"))?;
}
"remove" => friends::remove(db, user.user.id, other).await.map_err(internal("remove failed"))?,
_ => return Err(StatusCode::NOT_FOUND),
}
Ok(Redirect::to("/friends"))
}
/// Incoming requests waiting on `user_id`, for the home page.
pub async fn incoming_count(db: &DatabaseConnection, user_id: Uuid) -> Result<usize, StatusCode> {
Ok(friends::links(db, user_id)
.await
.map_err(internal("load friends failed"))?
.into_iter()
.filter(|(_, l)| *l == Link::Incoming)
.count())
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.0 KiB · text
History
aecd85b