mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
feat(blahd): impl ETag for feed
This commit is contained in:
parent
fac146e859
commit
70481e6c74
4 changed files with 114 additions and 23 deletions
|
@ -1,5 +1,7 @@
|
||||||
//! Core message subtypes.
|
//! Core message subtypes.
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitflags_serde_shim::impl_serde_for_bitflags;
|
use bitflags_serde_shim::impl_serde_for_bitflags;
|
||||||
use serde::{de, ser, Deserialize, Serialize};
|
use serde::{de, ser, Deserialize, Serialize};
|
||||||
|
@ -22,6 +24,14 @@ impl fmt::Display for Id {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for Id {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
i64::from_str(s).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
pub const MIN: Self = Id(i64::MIN);
|
pub const MIN: Self = Id(i64::MIN);
|
||||||
pub const MAX: Self = Id(i64::MAX);
|
pub const MAX: Self = Id(i64::MAX);
|
||||||
|
|
|
@ -23,7 +23,7 @@ use blah_types::{get_timestamp, Id, Signed, UserKey};
|
||||||
use database::{Transaction, TransactionOps};
|
use database::{Transaction, TransactionOps};
|
||||||
use feed::FeedData;
|
use feed::FeedData;
|
||||||
use id::IdExt;
|
use id::IdExt;
|
||||||
use middleware::{Auth, MaybeAuth, ResultExt as _, SignedJson};
|
use middleware::{Auth, ETag, MaybeAuth, ResultExt as _, SignedJson};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_inline_default::serde_inline_default;
|
use serde_inline_default::serde_inline_default;
|
||||||
|
@ -434,11 +434,11 @@ async fn room_get_metadata(
|
||||||
|
|
||||||
async fn room_get_feed<FT: feed::FeedType>(
|
async fn room_get_feed<FT: feed::FeedType>(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
|
ETag(etag): ETag<Id>,
|
||||||
R(OriginalUri(req_uri), _): RE<OriginalUri>,
|
R(OriginalUri(req_uri), _): RE<OriginalUri>,
|
||||||
R(Path(rid), _): RE<Path<Id>>,
|
R(Path(rid), _): RE<Path<Id>>,
|
||||||
R(Query(mut pagination), _): RE<Query<Pagination>>,
|
R(Query(mut pagination), _): RE<Query<Pagination>>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
// TODO: If-None-Match.
|
|
||||||
let self_url = st
|
let self_url = st
|
||||||
.config
|
.config
|
||||||
.base_url
|
.base_url
|
||||||
|
@ -460,6 +460,12 @@ async fn room_get_feed<FT: feed::FeedType>(
|
||||||
Ok((title, msgs, skip_token))
|
Ok((title, msgs, skip_token))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Use `Id(0)` as the tag for an empty list.
|
||||||
|
let ret_etag = msgs.first().map_or(Id(0), |msg| msg.cid);
|
||||||
|
if etag == Some(ret_etag) {
|
||||||
|
return Ok(StatusCode::NOT_MODIFIED.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
let next_url = skip_token.map(|skip_token| {
|
let next_url = skip_token.map(|skip_token| {
|
||||||
let next_params = Pagination {
|
let next_params = Pagination {
|
||||||
skip_token: Some(skip_token),
|
skip_token: Some(skip_token),
|
||||||
|
@ -478,13 +484,14 @@ async fn room_get_feed<FT: feed::FeedType>(
|
||||||
next_url
|
next_url
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(FT::to_feed_response(FeedData {
|
let resp = FT::to_feed_response(FeedData {
|
||||||
rid,
|
rid,
|
||||||
title,
|
title,
|
||||||
msgs,
|
msgs,
|
||||||
self_url,
|
self_url,
|
||||||
next_url,
|
next_url,
|
||||||
}))
|
});
|
||||||
|
Ok((ETag(Some(ret_etag)), resp).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get room messages with pagination parameters,
|
/// Get room messages with pagination parameters,
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::backtrace::Backtrace;
|
use std::backtrace::Backtrace;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection};
|
use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection};
|
||||||
use axum::extract::{FromRef, FromRequest, FromRequestParts, Request};
|
use axum::extract::{FromRef, FromRequest, FromRequestParts, Request};
|
||||||
use axum::http::{header, request, StatusCode};
|
use axum::http::{header, request, HeaderValue, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
|
||||||
use axum::{async_trait, Json};
|
use axum::{async_trait, Json};
|
||||||
use blah_types::msg::AuthPayload;
|
use blah_types::msg::AuthPayload;
|
||||||
use blah_types::{Signed, UserKey};
|
use blah_types::{Signed, UserKey};
|
||||||
|
@ -244,3 +245,42 @@ where
|
||||||
Ok(Self(data.signee.user))
|
Ok(Self(data.signee.user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ETag<T>(pub Option<T>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, T: FromStr> FromRequestParts<S> for ETag<T>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Infallible;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut request::Parts,
|
||||||
|
_state: &S,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let tag = parts
|
||||||
|
.headers
|
||||||
|
.get(header::IF_NONE_MATCH)
|
||||||
|
.and_then(|v| v.to_str().ok()?.strip_prefix('"')?.strip_suffix('"'))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.and_then(|s| s.parse::<T>().ok());
|
||||||
|
Ok(Self(tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: fmt::Display> IntoResponseParts for ETag<T> {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
if let Some(tag) = &self.0 {
|
||||||
|
res.headers_mut().insert(
|
||||||
|
header::ETAG,
|
||||||
|
HeaderValue::from_str(&format!("\"{tag}\""))
|
||||||
|
.expect("ETag must be a valid header value"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -790,15 +790,60 @@ async fn room_feed(server: Server, #[case] typ: &'static str) {
|
||||||
.join_room(rid, &BOB, MemberPermission::POST_CHAT)
|
.join_room(rid, &BOB, MemberPermission::POST_CHAT)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
server.post_chat(rid, &ALICE, "a").await.unwrap();
|
let feed_url = server.url(format!("/room/{rid}/feed.{typ}"));
|
||||||
|
let get_feed = |etag: Option<&str>| {
|
||||||
|
let mut req = server.client.get(&feed_url);
|
||||||
|
if let Some(etag) = etag {
|
||||||
|
req = req.header(header::IF_NONE_MATCH, etag);
|
||||||
|
}
|
||||||
|
async move {
|
||||||
|
let resp = req.send().await.unwrap().error_for_status().unwrap();
|
||||||
|
if resp.status() == StatusCode::NOT_MODIFIED {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let etag = resp.headers()[header::ETAG].to_str().unwrap().to_owned();
|
||||||
|
Some((etag, resp.text().await.unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty yet.
|
||||||
|
let etag_zero = "\"0\"";
|
||||||
|
assert_eq!(get_feed(None).await.unwrap().0, etag_zero);
|
||||||
|
// ETag should track from empty -> empty.
|
||||||
|
assert_eq!(get_feed(Some(etag_zero)).await, None);
|
||||||
|
|
||||||
|
// Post some chats.
|
||||||
|
let cid1 = server.post_chat(rid, &ALICE, "a").await.unwrap().cid;
|
||||||
|
// Got some response.
|
||||||
|
let etag_one = format!("\"{cid1}\"");
|
||||||
|
{
|
||||||
|
let resp1 = get_feed(None).await.unwrap();
|
||||||
|
// ETag should track from empty -> non-empty.
|
||||||
|
let resp2 = get_feed(Some(etag_zero)).await.unwrap();
|
||||||
|
// Idempotent.
|
||||||
|
assert_eq!(resp1, resp2);
|
||||||
|
assert_eq!(resp1.0, etag_one);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post more chats.
|
||||||
let cid2 = server.post_chat(rid, &BOB, "b1").await.unwrap().cid;
|
let cid2 = server.post_chat(rid, &BOB, "b1").await.unwrap().cid;
|
||||||
server.post_chat(rid, &BOB, "b2").await.unwrap();
|
let cid3 = server.post_chat(rid, &BOB, "b2").await.unwrap().cid;
|
||||||
|
|
||||||
|
let etag_last = format!("\"{cid3}\"");
|
||||||
|
let resp = {
|
||||||
|
let resp1 = get_feed(None).await.unwrap();
|
||||||
|
// ETag should track from non-empty -> non-empty.
|
||||||
|
let resp2 = get_feed(Some(&etag_one)).await.unwrap();
|
||||||
|
// Idempotent.
|
||||||
|
assert_eq!(resp1, resp2);
|
||||||
|
assert_eq!(resp1.0, etag_last);
|
||||||
|
assert_eq!(get_feed(Some(&etag_last)).await, None);
|
||||||
|
resp1.1
|
||||||
|
};
|
||||||
|
|
||||||
if typ == "json" {
|
if typ == "json" {
|
||||||
let feed = server
|
let feed = serde_json::from_str::<serde_json::Value>(&resp).unwrap();
|
||||||
.get::<serde_json::Value>(&format!("/room/{rid}/feed.json"), None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
// TODO: Ideally we should assert on the result, but it contains time and random id currently.
|
// TODO: Ideally we should assert on the result, but it contains time and random id currently.
|
||||||
assert_eq!(feed["title"].as_str().unwrap(), "public");
|
assert_eq!(feed["title"].as_str().unwrap(), "public");
|
||||||
assert_eq!(feed["items"].as_array().unwrap().len(), 2);
|
assert_eq!(feed["items"].as_array().unwrap().len(), 2);
|
||||||
|
@ -820,17 +865,6 @@ async fn room_feed(server: Server, #[case] typ: &'static str) {
|
||||||
assert_eq!(items.len(), 1);
|
assert_eq!(items.len(), 1);
|
||||||
assert_eq!(items[0]["content_html"].as_str().unwrap(), "a");
|
assert_eq!(items[0]["content_html"].as_str().unwrap(), "a");
|
||||||
} else {
|
} else {
|
||||||
let resp = server
|
|
||||||
.client
|
|
||||||
.get(server.url(format!("/room/{rid}/feed.atom")))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.error_for_status()
|
|
||||||
.unwrap()
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(resp.starts_with(r#"<?xml version="1.0" encoding="utf-8"?>"#));
|
assert!(resp.starts_with(r#"<?xml version="1.0" encoding="utf-8"?>"#));
|
||||||
assert_eq!(resp.matches("<entry>").count(), 2);
|
assert_eq!(resp.matches("<entry>").count(), 2);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue