use std::sync::Arc;
use rainier_auth::AuthenticatedUser;
use rainier_broadcast::{Broadcasting, Channel, ChannelRegistry};
use rainier_container::Application;
use rainier_http::{Request, Response, StatusCode};
use rainier_notify::{Channel as NotificationChannel, Delivery};
use rainier_routing::Req;
use rainier_support::{BoxFuture, Error, Result};
pub const SOCKET_ID_HEADER: &str = "X-Socket-ID";
pub fn socket_id(request: &Request) -> Option<String> {
request.header(SOCKET_ID_HEADER).map(str::to_string)
}
pub async fn authorize<U>(request: Req) -> Result<Response>
where
U: Send + Sync + 'static,
{
let socket = required(&request, "socket_id")?;
let wire_name = required(&request, "channel_name")?;
let channel = Channel::from_wire_name(&wire_name);
let app = rainier_container::facade_application();
let registry = app.resolve::<ChannelRegistry<U>>()?;
let broadcasting = app.resolve::<Broadcasting>()?;
let Some(user) = request.extension::<AuthenticatedUser<U>>() else {
return Err(Error::unauthenticated("Unauthenticated."));
};
let access = registry.authorize(user.get(), &channel).await?;
if !access.is_allowed() {
return Ok(Response::new(StatusCode::FORBIDDEN));
}
let body = broadcasting.driver().auth_response(&socket, &channel, access.member())?;
Ok(Response::json(&body))
}
fn required(request: &Request, name: &str) -> Result<String> {
request
.input(name)
.map(|value| value.to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| Error::bad_request(format!("`{name}` is required.")))
}
pub struct BroadcastChannel {
broadcasting: Arc<Broadcasting>,
}
impl BroadcastChannel {
pub fn new(broadcasting: Arc<Broadcasting>) -> Self {
Self { broadcasting }
}
pub fn channel_for(notifiable_type: &str, notifiable_id: &str) -> Channel {
Channel::private(format!("notifications.{notifiable_type}.{notifiable_id}"))
}
}
impl NotificationChannel for BroadcastChannel {
fn name(&self) -> &'static str {
"broadcast"
}
fn send<'a>(&'a self, delivery: &'a Delivery) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let Some(payload) = delivery.data_or_text() else {
return Err(delivery.nothing_to_send("broadcast", "data or text"));
};
let channel = Self::channel_for(delivery.recipient_type, &delivery.recipient_id);
self.broadcasting.to(vec![channel], delivery.notification, payload).await
})
}
}
pub fn authorize_notifications<U, F>(
registry: &mut ChannelRegistry<U>,
notifiable_type: &'static str,
identify: F,
) where
U: Send + Sync + 'static,
F: Fn(&U) -> String + Send + Sync + 'static,
{
registry.channel("notifications.{type}.{id}", move |user: &U, params| {
let allowed = params.get("type") == Some(notifiable_type)
&& params.get("id") == Some(identify(user).as_str());
Box::pin(async move { Ok(rainier_broadcast::ChannelAccess::allowed_if(allowed)) })
});
}
pub fn bind_defaults<U: Send + Sync + 'static>(app: &Application) {
app.instance(Broadcasting::log());
app.instance(ChannelRegistry::<U>::new());
}
#[cfg(test)]
mod tests {
use super::*;
use rainier_broadcast::{ChannelAccess, MemoryBroadcaster};
use rainier_notify::{Channels, Notifiable, Notification, Notifier};
struct User(u64);
impl Notifiable for User {
fn notifiable_id(&self) -> String {
self.0.to_string()
}
fn notifiable_type(&self) -> &'static str {
"User"
}
fn route_for(&self, _: &str) -> Option<String> {
None
}
}
struct Mentioned;
impl Notification<User> for Mentioned {
fn notification_name(&self) -> &'static str {
"post.mentioned"
}
fn via(&self, _: &User) -> Channels {
Channels::new().with::<BroadcastChannel>()
}
fn to_text(&self, _: &User) -> Option<String> {
Some("someone mentioned you".into())
}
}
#[tokio::test]
async fn a_notification_broadcasts_to_the_recipients_own_channel() {
let driver = Arc::new(MemoryBroadcaster::new());
let broadcasting = Arc::new(Broadcasting::new(driver.clone()));
let notifier = Notifier::new().with(BroadcastChannel::new(broadcasting));
notifier.send(&User(7), &Mentioned).await.unwrap();
driver.assert_broadcast("post.mentioned", "private-notifications.User.7");
assert_eq!(driver.sent()[0].payload["message"], "someone mentioned you");
}
#[tokio::test]
async fn a_recipient_may_listen_to_their_own_and_nobody_elses() {
let mut registry = ChannelRegistry::<User>::new();
authorize_notifications(&mut registry, "User", |user| user.notifiable_id());
let mine = BroadcastChannel::channel_for("User", "7");
assert_eq!(registry.authorize(&User(7), &mine).await.unwrap(), ChannelAccess::Allowed);
assert_eq!(registry.authorize(&User(8), &mine).await.unwrap(), ChannelAccess::Denied);
}
#[tokio::test]
async fn another_notifiable_type_with_the_same_id_is_denied() {
let mut registry = ChannelRegistry::<User>::new();
authorize_notifications(&mut registry, "User", |user| user.notifiable_id());
let theirs = BroadcastChannel::channel_for("Team", "7");
assert_eq!(registry.authorize(&User(7), &theirs).await.unwrap(), ChannelAccess::Denied);
}
}