mirror of
				https://github.com/Blah-IM/blahrs.git
				synced 2025-10-29 18:11:36 +00:00 
			
		
		
		
	Impl global room listing at /room
				
					
				
			This commit is contained in:
		
							parent
							
								
									a9d5eb9631
								
							
						
					
					
						commit
						2b6fbe8794
					
				
					 4 changed files with 180 additions and 8 deletions
				
			
		|  | @ -1,3 +1,4 @@ | |||
| use std::num::NonZeroUsize; | ||||
| use std::path::PathBuf; | ||||
| use std::time::Duration; | ||||
| 
 | ||||
|  | @ -28,8 +29,8 @@ pub struct ServerConfig { | |||
|     pub listen: String, | ||||
|     pub base_url: Url, | ||||
| 
 | ||||
|     #[serde_inline_default(1024)] | ||||
|     pub max_page_len: usize, | ||||
|     #[serde_inline_default(1024.try_into().unwrap())] | ||||
|     pub max_page_len: NonZeroUsize, | ||||
|     #[serde_inline_default(4096)] // 4KiB
 | ||||
|     pub max_request_len: usize, | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ use blah::types::{ | |||
| use config::Config; | ||||
| use ed25519_dalek::SIGNATURE_LENGTH; | ||||
| use middleware::{ApiError, OptionalAuth, SignedJson}; | ||||
| use rusqlite::{named_params, params, Connection, OptionalExtension, Row}; | ||||
| use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use url::Url; | ||||
| use utils::ExpiringSet; | ||||
|  | @ -149,6 +149,7 @@ async fn main_async(st: AppState) -> Result<()> { | |||
| 
 | ||||
|     let app = Router::new() | ||||
|         .route("/ws", get(handle_ws)) | ||||
|         .route("/room", get(room_list)) | ||||
|         .route("/room/create", post(room_create)) | ||||
|         .route("/room/:ruuid", get(room_get_metadata)) | ||||
|         // NB. Sync with `feed_url` and `next_url` generation.
 | ||||
|  | @ -196,6 +197,108 @@ async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response { | |||
|     }) | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| struct RoomList { | ||||
|     rooms: Vec<RoomMetadata>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     skip_token: Option<u64>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(deny_unknown_fields, rename_all = "camelCase")] | ||||
| struct ListRoomParams { | ||||
|     filter: ListRoomFilter, | ||||
|     // Workaround: serde(flatten) breaks deserialization
 | ||||
|     // See: https://github.com/nox/serde_urlencoded/issues/33
 | ||||
|     skip_token: Option<u64>, | ||||
|     top: Option<NonZeroUsize>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "snake_case")] | ||||
| enum ListRoomFilter { | ||||
|     Public, | ||||
|     Joined, | ||||
| } | ||||
| 
 | ||||
| async fn room_list( | ||||
|     st: ArcState, | ||||
|     WithRejection(params, _): WithRejection<Query<ListRoomParams>, ApiError>, | ||||
|     OptionalAuth(user): OptionalAuth, | ||||
| ) -> Result<Json<RoomList>, ApiError> { | ||||
|     let pagination = Pagination { | ||||
|         skip_token: params.skip_token, | ||||
|         top: params.top, | ||||
|     }; | ||||
|     let page_len = pagination.effective_page_len(&st); | ||||
|     let start_rid = pagination.skip_token.unwrap_or(0); | ||||
| 
 | ||||
|     let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> { | ||||
|         let mut last_rid = None; | ||||
|         let rooms = st | ||||
|             .conn | ||||
|             .lock() | ||||
|             .unwrap() | ||||
|             .prepare(sql)? | ||||
|             .query_map(params, |row| { | ||||
|                 last_rid = Some(row.get::<_, u64>("rid")?); | ||||
|                 Ok(RoomMetadata { | ||||
|                     ruuid: row.get("ruuid")?, | ||||
|                     title: row.get("title")?, | ||||
|                     attrs: row.get("attrs")?, | ||||
|                 }) | ||||
|             })? | ||||
|             .collect::<Result<Vec<_>, _>>()?; | ||||
|         let skip_token = (rooms.len() == page_len).then_some(()).and(last_rid); | ||||
|         Ok(RoomList { rooms, skip_token }) | ||||
|     }; | ||||
| 
 | ||||
|     match params.filter { | ||||
|         ListRoomFilter::Public => query( | ||||
|             r" | ||||
|             SELECT `rid`, `ruuid`, `title`, `attrs` | ||||
|             FROM `room` | ||||
|             WHERE `rid` > :start_rid AND | ||||
|                 (`attrs` & :perm) = :perm | ||||
|             ORDER BY `rid` ASC | ||||
|             LIMIT :page_len | ||||
|             ",
 | ||||
|             named_params! { | ||||
|                 ":start_rid": start_rid, | ||||
|                 ":page_len": page_len, | ||||
|                 ":perm": RoomAttrs::PUBLIC_READABLE, | ||||
|             }, | ||||
|         ), | ||||
|         ListRoomFilter::Joined => { | ||||
|             let Some(user) = user else { | ||||
|                 return Err(error_response!( | ||||
|                     StatusCode::UNAUTHORIZED, | ||||
|                     "unauthorized", | ||||
|                     "missing Authorization header for listing joined rooms", | ||||
|                 )); | ||||
|             }; | ||||
|             query( | ||||
|                 r" | ||||
|                 SELECT `rid`, `ruuid`, `title`, `attrs` | ||||
|                 FROM `user` | ||||
|                 JOIN `room_member` USING (`uid`) | ||||
|                 JOIN `room` USING (`rid`) | ||||
|                 WHERE `userkey` = :userkey AND | ||||
|                     `rid` > :start_rid | ||||
|                 ORDER BY `rid` ASC | ||||
|                 LIMIT :page_len | ||||
|                 ",
 | ||||
|                 named_params! { | ||||
|                     ":start_rid": start_rid, | ||||
|                     ":page_len": page_len, | ||||
|                     ":userkey": user, | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     .map(Json) | ||||
| } | ||||
| 
 | ||||
| async fn room_create( | ||||
|     st: ArcState, | ||||
|     SignedJson(params): SignedJson<CreateRoomPayload>, | ||||
|  | @ -293,6 +396,15 @@ struct Pagination { | |||
|     top: Option<NonZeroUsize>, | ||||
| } | ||||
| 
 | ||||
| impl Pagination { | ||||
|     fn effective_page_len(&self, st: &AppState) -> usize { | ||||
|         self.top | ||||
|             .unwrap_or(usize::MAX.try_into().unwrap()) | ||||
|             .min(st.config.server.max_page_len) | ||||
|             .get() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| struct RoomItems { | ||||
|     items: Vec<ChatItem>, | ||||
|  | @ -331,7 +443,11 @@ async fn room_get_metadata( | |||
|             )) | ||||
|         })?; | ||||
| 
 | ||||
|     Ok(Json(RoomMetadata { title, attrs })) | ||||
|     Ok(Json(RoomMetadata { | ||||
|         ruuid, | ||||
|         title, | ||||
|         attrs, | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| async fn room_get_feed( | ||||
|  | @ -436,6 +552,7 @@ struct FeedItemExtra { | |||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct RoomMetadata { | ||||
|     pub ruuid: Uuid, | ||||
|     pub title: String, | ||||
|     pub attrs: RoomAttrs, | ||||
| } | ||||
|  | @ -479,10 +596,7 @@ fn query_room_items( | |||
|     ruuid: Uuid, | ||||
|     pagination: Pagination, | ||||
| ) -> Result<(Vec<ChatItemWithId>, Option<u64>), ApiError> { | ||||
|     let page_len = pagination | ||||
|         .top | ||||
|         .map_or(usize::MAX, |n| n.get()) | ||||
|         .min(st.config.server.max_page_len); | ||||
|     let page_len = pagination.effective_page_len(st); | ||||
|     let mut stmt = conn.prepare( | ||||
|         r" | ||||
|         SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text` | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 oxalica
						oxalica