1pub mod v3 {
6 use std::{fmt, time::Duration};
11
12 use ruma_common::{
13 api::{request, response, Metadata},
14 metadata,
15 serde::JsonObject,
16 OwnedDeviceId, OwnedServerName, OwnedUserId,
17 };
18 use serde::{
19 de::{self, DeserializeOwned},
20 Deserialize, Deserializer, Serialize,
21 };
22 use serde_json::Value as JsonValue;
23
24 use crate::uiaa::UserIdentifier;
25
26 const METADATA: Metadata = metadata! {
27 method: POST,
28 rate_limited: true,
29 authentication: AppserviceTokenOptional,
30 history: {
31 1.0 => "/_matrix/client/r0/login",
32 1.1 => "/_matrix/client/v3/login",
33 }
34 };
35
36 #[request(error = crate::Error)]
38 pub struct Request {
39 #[serde(flatten)]
41 pub login_info: LoginInfo,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub device_id: Option<OwnedDeviceId>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
51 pub initial_device_display_name: Option<String>,
52
53 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
57 pub refresh_token: bool,
58 }
59
60 #[response(error = crate::Error)]
62 pub struct Response {
63 pub user_id: OwnedUserId,
65
66 pub access_token: String,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 #[deprecated = "\
72 Since Matrix Client-Server API r0.4.0. Clients should instead use the \
73 `user_id.server_name()` method if they require it.\
74 "]
75 pub home_server: Option<OwnedServerName>,
76
77 pub device_id: OwnedDeviceId,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
87 pub well_known: Option<DiscoveryInfo>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
97 pub refresh_token: Option<String>,
98
99 #[serde(
107 with = "ruma_common::serde::duration::opt_ms",
108 default,
109 skip_serializing_if = "Option::is_none",
110 rename = "expires_in_ms"
111 )]
112 pub expires_in: Option<Duration>,
113 }
114 impl Request {
115 pub fn new(login_info: LoginInfo) -> Self {
117 Self {
118 login_info,
119 device_id: None,
120 initial_device_display_name: None,
121 refresh_token: false,
122 }
123 }
124 }
125
126 impl Response {
127 #[allow(deprecated)]
129 pub fn new(user_id: OwnedUserId, access_token: String, device_id: OwnedDeviceId) -> Self {
130 Self {
131 user_id,
132 access_token,
133 home_server: None,
134 device_id,
135 well_known: None,
136 refresh_token: None,
137 expires_in: None,
138 }
139 }
140 }
141
142 #[derive(Clone, Serialize)]
144 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
145 #[serde(untagged)]
146 pub enum LoginInfo {
147 Password(Password),
149
150 Token(Token),
152
153 ApplicationService(ApplicationService),
155
156 #[doc(hidden)]
157 _Custom(CustomLoginInfo),
158 }
159
160 impl LoginInfo {
161 pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
172 Ok(match login_type {
173 "m.login.password" => {
174 Self::Password(serde_json::from_value(JsonValue::Object(data))?)
175 }
176 "m.login.token" => Self::Token(serde_json::from_value(JsonValue::Object(data))?),
177 "m.login.application_service" => {
178 Self::ApplicationService(serde_json::from_value(JsonValue::Object(data))?)
179 }
180 _ => Self::_Custom(CustomLoginInfo { login_type: login_type.into(), extra: data }),
181 })
182 }
183 }
184
185 impl fmt::Debug for LoginInfo {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
189 Self::Password(inner) => inner.fmt(f),
190 Self::Token(inner) => inner.fmt(f),
191 Self::ApplicationService(inner) => inner.fmt(f),
192 Self::_Custom(inner) => inner.fmt(f),
193 }
194 }
195 }
196
197 impl<'de> Deserialize<'de> for LoginInfo {
198 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
199 where
200 D: Deserializer<'de>,
201 {
202 fn from_json_value<T: DeserializeOwned, E: de::Error>(val: JsonValue) -> Result<T, E> {
203 serde_json::from_value(val).map_err(E::custom)
204 }
205
206 let json = JsonValue::deserialize(deserializer)?;
209
210 let login_type =
211 json["type"].as_str().ok_or_else(|| de::Error::missing_field("type"))?;
212 match login_type {
213 "m.login.password" => from_json_value(json).map(Self::Password),
214 "m.login.token" => from_json_value(json).map(Self::Token),
215 "m.login.application_service" => {
216 from_json_value(json).map(Self::ApplicationService)
217 }
218 _ => from_json_value(json).map(Self::_Custom),
219 }
220 }
221 }
222
223 #[derive(Clone, Deserialize, Serialize)]
225 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
226 #[serde(tag = "type", rename = "m.login.password")]
227 pub struct Password {
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub identifier: Option<UserIdentifier>,
231
232 pub password: String,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 #[deprecated = "\
238 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
239 instead.\
240 "]
241 pub user: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 #[deprecated = "\
246 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
247 instead.\
248 "]
249 pub address: Option<String>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 #[deprecated = "\
254 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
255 instead.\
256 "]
257 pub medium: Option<String>,
258 }
259
260 impl Password {
261 #[allow(deprecated)]
263 pub fn new(identifier: UserIdentifier, password: String) -> Self {
264 Self { identifier: Some(identifier), password, user: None, address: None, medium: None }
265 }
266 }
267
268 impl fmt::Debug for Password {
269 #[allow(deprecated)]
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 let Self { identifier, password: _, user, address, medium } = self;
272 f.debug_struct("Password")
273 .field("identifier", identifier)
274 .field("user", user)
275 .field("address", address)
276 .field("medium", medium)
277 .finish_non_exhaustive()
278 }
279 }
280
281 #[derive(Clone, Deserialize, Serialize)]
283 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
284 #[serde(tag = "type", rename = "m.login.token")]
285 pub struct Token {
286 pub token: String,
288 }
289
290 impl Token {
291 pub fn new(token: String) -> Self {
293 Self { token }
294 }
295 }
296
297 impl fmt::Debug for Token {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 let Self { token: _ } = self;
300 f.debug_struct("Token").finish_non_exhaustive()
301 }
302 }
303
304 #[derive(Clone, Debug, Deserialize, Serialize)]
306 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
307 #[serde(tag = "type", rename = "m.login.application_service")]
308 pub struct ApplicationService {
309 pub identifier: Option<UserIdentifier>,
311
312 #[serde(skip_serializing_if = "Option::is_none")]
314 #[deprecated = "\
315 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
316 instead.\
317 "]
318 pub user: Option<String>,
319 }
320
321 impl ApplicationService {
322 #[allow(deprecated)]
324 pub fn new(identifier: UserIdentifier) -> Self {
325 Self { identifier: Some(identifier), user: None }
326 }
327 }
328
329 #[doc(hidden)]
330 #[derive(Clone, Deserialize, Serialize)]
331 #[non_exhaustive]
332 pub struct CustomLoginInfo {
333 #[serde(rename = "type")]
334 login_type: String,
335 #[serde(flatten)]
336 extra: JsonObject,
337 }
338
339 impl fmt::Debug for CustomLoginInfo {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 let Self { login_type, extra: _ } = self;
342 f.debug_struct("CustomLoginInfo")
343 .field("login_type", login_type)
344 .finish_non_exhaustive()
345 }
346 }
347
348 #[derive(Clone, Debug, Deserialize, Serialize)]
350 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
351 pub struct DiscoveryInfo {
352 #[serde(rename = "m.homeserver")]
354 pub homeserver: HomeserverInfo,
355
356 #[serde(rename = "m.identity_server")]
358 pub identity_server: Option<IdentityServerInfo>,
359 }
360
361 impl DiscoveryInfo {
362 pub fn new(homeserver: HomeserverInfo) -> Self {
364 Self { homeserver, identity_server: None }
365 }
366 }
367
368 #[derive(Clone, Debug, Deserialize, Serialize)]
370 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
371 pub struct HomeserverInfo {
372 pub base_url: String,
374 }
375
376 impl HomeserverInfo {
377 pub fn new(base_url: String) -> Self {
379 Self { base_url }
380 }
381 }
382
383 #[derive(Clone, Debug, Deserialize, Serialize)]
385 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
386 pub struct IdentityServerInfo {
387 pub base_url: String,
389 }
390
391 impl IdentityServerInfo {
392 pub fn new(base_url: String) -> Self {
394 Self { base_url }
395 }
396 }
397
398 #[cfg(test)]
399 mod tests {
400 use assert_matches2::assert_matches;
401 use serde_json::{from_value as from_json_value, json};
402
403 use super::{LoginInfo, Token};
404 use crate::uiaa::UserIdentifier;
405
406 #[test]
407 fn deserialize_login_type() {
408 assert_matches!(
409 from_json_value(json!({
410 "type": "m.login.password",
411 "identifier": {
412 "type": "m.id.user",
413 "user": "cheeky_monkey"
414 },
415 "password": "ilovebananas"
416 }))
417 .unwrap(),
418 LoginInfo::Password(login)
419 );
420 assert_matches!(login.identifier, Some(UserIdentifier::UserIdOrLocalpart(user)));
421 assert_eq!(user, "cheeky_monkey");
422 assert_eq!(login.password, "ilovebananas");
423
424 assert_matches!(
425 from_json_value(json!({
426 "type": "m.login.token",
427 "token": "1234567890abcdef"
428 }))
429 .unwrap(),
430 LoginInfo::Token(Token { token })
431 );
432 assert_eq!(token, "1234567890abcdef");
433 }
434
435 #[test]
436 #[cfg(feature = "client")]
437 fn serialize_login_request_body() {
438 use ruma_common::api::{MatrixVersion, OutgoingRequest, SendAccessToken};
439 use serde_json::Value as JsonValue;
440
441 use super::{LoginInfo, Password, Request, Token};
442 use crate::uiaa::UserIdentifier;
443
444 let req: http::Request<Vec<u8>> = Request {
445 login_info: LoginInfo::Token(Token { token: "0xdeadbeef".to_owned() }),
446 device_id: None,
447 initial_device_display_name: Some("test".to_owned()),
448 refresh_token: false,
449 }
450 .try_into_http_request(
451 "https://homeserver.tld",
452 SendAccessToken::None,
453 &[MatrixVersion::V1_1],
454 )
455 .unwrap();
456
457 let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
458 assert_eq!(
459 req_body_value,
460 json!({
461 "type": "m.login.token",
462 "token": "0xdeadbeef",
463 "initial_device_display_name": "test",
464 })
465 );
466
467 let req: http::Request<Vec<u8>> = Request {
468 #[allow(deprecated)]
469 login_info: LoginInfo::Password(Password {
470 identifier: Some(UserIdentifier::Email {
471 address: "hello@example.com".to_owned(),
472 }),
473 password: "deadbeef".to_owned(),
474 user: None,
475 address: None,
476 medium: None,
477 }),
478 device_id: None,
479 initial_device_display_name: Some("test".to_owned()),
480 refresh_token: false,
481 }
482 .try_into_http_request(
483 "https://homeserver.tld",
484 SendAccessToken::None,
485 &[MatrixVersion::V1_1],
486 )
487 .unwrap();
488
489 let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
490 assert_eq!(
491 req_body_value,
492 json!({
493 "identifier": {
494 "type": "m.id.thirdparty",
495 "medium": "email",
496 "address": "hello@example.com"
497 },
498 "type": "m.login.password",
499 "password": "deadbeef",
500 "initial_device_display_name": "test",
501 })
502 );
503 }
504 }
505}