1pub mod v3 {
6 use std::{borrow::Cow, fmt, time::Duration};
11
12 use as_variant::as_variant;
13 use ruma_common::{
14 OwnedDeviceId, OwnedServerName, OwnedUserId,
15 api::{auth_scheme::AppserviceTokenOptional, request, response},
16 metadata,
17 serde::JsonObject,
18 };
19 use serde::{
20 Deserialize, Deserializer, Serialize,
21 de::{self, DeserializeOwned},
22 };
23 use serde_json::Value as JsonValue;
24
25 use crate::uiaa::UserIdentifier;
26
27 metadata! {
28 method: POST,
29 rate_limited: true,
30 authentication: AppserviceTokenOptional,
31 history: {
32 1.0 => "/_matrix/client/r0/login",
33 1.1 => "/_matrix/client/v3/login",
34 }
35 }
36
37 #[request]
39 pub struct Request {
40 #[serde(flatten)]
42 pub login_info: LoginInfo,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub device_id: Option<OwnedDeviceId>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
52 pub initial_device_display_name: Option<String>,
53
54 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
58 pub refresh_token: bool,
59 }
60
61 #[response]
63 pub struct Response {
64 pub user_id: OwnedUserId,
66
67 pub access_token: String,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 #[deprecated = "\
73 Since Matrix Client-Server API r0.4.0. Clients should instead use the \
74 `user_id.server_name()` method if they require it.\
75 "]
76 pub home_server: Option<OwnedServerName>,
77
78 pub device_id: OwnedDeviceId,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
88 pub well_known: Option<DiscoveryInfo>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
98 pub refresh_token: Option<String>,
99
100 #[serde(
108 with = "ruma_common::serde::duration::opt_ms",
109 default,
110 skip_serializing_if = "Option::is_none",
111 rename = "expires_in_ms"
112 )]
113 pub expires_in: Option<Duration>,
114 }
115 impl Request {
116 pub fn new(login_info: LoginInfo) -> Self {
118 Self {
119 login_info,
120 device_id: None,
121 initial_device_display_name: None,
122 refresh_token: false,
123 }
124 }
125 }
126
127 impl Response {
128 #[allow(deprecated)]
130 pub fn new(user_id: OwnedUserId, access_token: String, device_id: OwnedDeviceId) -> Self {
131 Self {
132 user_id,
133 access_token,
134 home_server: None,
135 device_id,
136 well_known: None,
137 refresh_token: None,
138 expires_in: None,
139 }
140 }
141 }
142
143 #[derive(Clone, Serialize)]
145 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
146 #[serde(untagged)]
147 pub enum LoginInfo {
148 Password(Password),
150
151 Token(Token),
153
154 ApplicationService(ApplicationService),
156
157 #[doc(hidden)]
158 _Custom(CustomLoginInfo),
159 }
160
161 impl LoginInfo {
162 pub fn new(login_type: &str, data: JsonObject) -> serde_json::Result<Self> {
173 Ok(match login_type {
174 "m.login.password" => {
175 Self::Password(serde_json::from_value(JsonValue::Object(data))?)
176 }
177 "m.login.token" => Self::Token(serde_json::from_value(JsonValue::Object(data))?),
178 "m.login.application_service" => {
179 Self::ApplicationService(serde_json::from_value(JsonValue::Object(data))?)
180 }
181 _ => Self::_Custom(CustomLoginInfo { login_type: login_type.into(), data }),
182 })
183 }
184
185 pub fn login_type(&self) -> &str {
187 match self {
188 LoginInfo::Password(_) => "m.login.password",
189 LoginInfo::Token(_) => "m.login.token",
190 LoginInfo::ApplicationService(_) => "m.login.application_service",
191 LoginInfo::_Custom(c) => &c.login_type,
192 }
193 }
194
195 pub fn data(&self) -> Cow<'_, JsonObject> {
200 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
201 match serde_json::to_value(obj).expect("login info serialization to succeed") {
202 JsonValue::Object(obj) => obj,
203 _ => panic!("all login info variants must serialize to objects"),
204 }
205 }
206
207 match self {
208 Self::Password(d) => Cow::Owned(serialize(d)),
209 Self::Token(d) => Cow::Owned(serialize(d)),
210 Self::ApplicationService(d) => Cow::Owned(serialize(d)),
211 Self::_Custom(c) => Cow::Borrowed(&c.data),
212 }
213 }
214 }
215
216 impl fmt::Debug for LoginInfo {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
220 Self::Password(inner) => inner.fmt(f),
221 Self::Token(inner) => inner.fmt(f),
222 Self::ApplicationService(inner) => inner.fmt(f),
223 Self::_Custom(inner) => inner.fmt(f),
224 }
225 }
226 }
227
228 impl<'de> Deserialize<'de> for LoginInfo {
229 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230 where
231 D: Deserializer<'de>,
232 {
233 fn from_json_value<T: DeserializeOwned, E: de::Error>(val: JsonValue) -> Result<T, E> {
234 serde_json::from_value(val).map_err(E::custom)
235 }
236
237 let mut data = JsonObject::deserialize(deserializer)?;
240
241 let login_type =
242 data["type"].as_str().ok_or_else(|| de::Error::missing_field("type"))?;
243 match login_type {
244 "m.login.password" => from_json_value(data.into()).map(Self::Password),
245 "m.login.token" => from_json_value(data.into()).map(Self::Token),
246 "m.login.application_service" => {
247 from_json_value(data.into()).map(Self::ApplicationService)
248 }
249 _ => {
250 let login_type = as_variant!(
251 data.remove("type")
252 .expect("we already checked that the object has a type field"),
253 JsonValue::String
254 )
255 .expect("we already checked that the type field is a string");
256 Ok(Self::_Custom(CustomLoginInfo { login_type, data }))
257 }
258 }
259 }
260 }
261
262 #[derive(Clone, Deserialize, Serialize)]
264 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
265 #[serde(tag = "type", rename = "m.login.password")]
266 pub struct Password {
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub identifier: Option<UserIdentifier>,
270
271 pub password: String,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 #[deprecated = "\
277 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
278 instead.\
279 "]
280 pub user: Option<String>,
281
282 #[serde(skip_serializing_if = "Option::is_none")]
284 #[deprecated = "\
285 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
286 instead.\
287 "]
288 pub address: Option<String>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
292 #[deprecated = "\
293 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
294 instead.\
295 "]
296 pub medium: Option<String>,
297 }
298
299 impl Password {
300 #[allow(deprecated)]
302 pub fn new(identifier: UserIdentifier, password: String) -> Self {
303 Self { identifier: Some(identifier), password, user: None, address: None, medium: None }
304 }
305 }
306
307 impl fmt::Debug for Password {
308 #[allow(deprecated)]
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 let Self { identifier, password: _, user, address, medium } = self;
311 f.debug_struct("Password")
312 .field("identifier", identifier)
313 .field("user", user)
314 .field("address", address)
315 .field("medium", medium)
316 .finish_non_exhaustive()
317 }
318 }
319
320 #[derive(Clone, Deserialize, Serialize)]
322 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
323 #[serde(tag = "type", rename = "m.login.token")]
324 pub struct Token {
325 pub token: String,
327 }
328
329 impl Token {
330 pub fn new(token: String) -> Self {
332 Self { token }
333 }
334 }
335
336 impl fmt::Debug for Token {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 let Self { token: _ } = self;
339 f.debug_struct("Token").finish_non_exhaustive()
340 }
341 }
342
343 #[derive(Clone, Debug, Deserialize, Serialize)]
345 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
346 #[serde(tag = "type", rename = "m.login.application_service")]
347 pub struct ApplicationService {
348 pub identifier: Option<UserIdentifier>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 #[deprecated = "\
354 Since Matrix Client-Server API r0.4.0, clients should use `identifier`\
355 instead.\
356 "]
357 pub user: Option<String>,
358 }
359
360 impl ApplicationService {
361 #[allow(deprecated)]
363 pub fn new(identifier: UserIdentifier) -> Self {
364 Self { identifier: Some(identifier), user: None }
365 }
366 }
367
368 #[doc(hidden)]
369 #[derive(Clone, Serialize)]
370 #[non_exhaustive]
371 pub struct CustomLoginInfo {
372 #[serde(rename = "type")]
373 login_type: String,
374 #[serde(flatten)]
375 data: JsonObject,
376 }
377
378 impl fmt::Debug for CustomLoginInfo {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 let Self { login_type, data: _ } = self;
381 f.debug_struct("CustomLoginInfo")
382 .field("login_type", login_type)
383 .finish_non_exhaustive()
384 }
385 }
386
387 #[derive(Clone, Debug, Deserialize, Serialize)]
389 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
390 pub struct DiscoveryInfo {
391 #[serde(rename = "m.homeserver")]
393 pub homeserver: HomeserverInfo,
394
395 #[serde(rename = "m.identity_server")]
397 pub identity_server: Option<IdentityServerInfo>,
398 }
399
400 impl DiscoveryInfo {
401 pub fn new(homeserver: HomeserverInfo) -> Self {
403 Self { homeserver, identity_server: None }
404 }
405 }
406
407 #[derive(Clone, Debug, Deserialize, Serialize)]
409 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
410 pub struct HomeserverInfo {
411 pub base_url: String,
413 }
414
415 impl HomeserverInfo {
416 pub fn new(base_url: String) -> Self {
418 Self { base_url }
419 }
420 }
421
422 #[derive(Clone, Debug, Deserialize, Serialize)]
424 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
425 pub struct IdentityServerInfo {
426 pub base_url: String,
428 }
429
430 impl IdentityServerInfo {
431 pub fn new(base_url: String) -> Self {
433 Self { base_url }
434 }
435 }
436
437 #[cfg(test)]
438 mod tests {
439 use assert_matches2::assert_matches;
440 use ruma_common::canonical_json::assert_to_canonical_json_eq;
441 use serde_json::{from_value as from_json_value, json};
442
443 use super::{LoginInfo, Token};
444 use crate::uiaa::UserIdentifier;
445
446 #[test]
447 fn deserialize_login_type() {
448 assert_matches!(
449 from_json_value(json!({
450 "type": "m.login.password",
451 "identifier": {
452 "type": "m.id.user",
453 "user": "cheeky_monkey"
454 },
455 "password": "ilovebananas"
456 }))
457 .unwrap(),
458 LoginInfo::Password(login)
459 );
460 assert_matches!(login.identifier, Some(UserIdentifier::Matrix(id)));
461 assert_eq!(id.user, "cheeky_monkey");
462 assert_eq!(login.password, "ilovebananas");
463
464 assert_matches!(
465 from_json_value(json!({
466 "type": "m.login.token",
467 "token": "1234567890abcdef"
468 }))
469 .unwrap(),
470 LoginInfo::Token(Token { token })
471 );
472 assert_eq!(token, "1234567890abcdef");
473 }
474
475 #[test]
476 fn login_info_serialize_roundtrip() {
477 let json = json!({
478 "type": "local.dev.custom",
479 "identifier": "dGhpcy5pcy5tZQ",
480 });
481
482 let login_info = from_json_value::<LoginInfo>(json.clone()).unwrap();
483
484 assert_eq!(login_info.login_type(), "local.dev.custom");
485 let data = login_info.data();
486 assert_eq!(data.len(), 1);
487 assert_eq!(data.get("identifier").unwrap().as_str(), Some("dGhpcy5pcy5tZQ"));
488
489 assert_to_canonical_json_eq!(login_info, json);
490 }
491
492 #[test]
493 #[cfg(feature = "client")]
494 fn serialize_login_request_body() {
495 use std::borrow::Cow;
496
497 use ruma_common::api::{
498 MatrixVersion, OutgoingRequest, SupportedVersions, auth_scheme::SendAccessToken,
499 };
500 use serde_json::Value as JsonValue;
501
502 use super::{LoginInfo, Password, Request, Token};
503 use crate::uiaa::{EmailUserIdentifier, UserIdentifier};
504
505 let supported = SupportedVersions {
506 versions: [MatrixVersion::V1_1].into(),
507 features: Default::default(),
508 };
509
510 let req: http::Request<Vec<u8>> = Request {
511 login_info: LoginInfo::Token(Token { token: "0xdeadbeef".to_owned() }),
512 device_id: None,
513 initial_device_display_name: Some("test".to_owned()),
514 refresh_token: false,
515 }
516 .try_into_http_request(
517 "https://homeserver.tld",
518 SendAccessToken::None,
519 Cow::Borrowed(&supported),
520 )
521 .unwrap();
522
523 let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
524 assert_eq!(
525 req_body_value,
526 json!({
527 "type": "m.login.token",
528 "token": "0xdeadbeef",
529 "initial_device_display_name": "test",
530 })
531 );
532
533 let req: http::Request<Vec<u8>> = Request {
534 #[allow(deprecated)]
535 login_info: LoginInfo::Password(Password {
536 identifier: Some(UserIdentifier::Email(EmailUserIdentifier::new(
537 "hello@example.com".to_owned(),
538 ))),
539 password: "deadbeef".to_owned(),
540 user: None,
541 address: None,
542 medium: None,
543 }),
544 device_id: None,
545 initial_device_display_name: Some("test".to_owned()),
546 refresh_token: false,
547 }
548 .try_into_http_request(
549 "https://homeserver.tld",
550 SendAccessToken::None,
551 Cow::Borrowed(&supported),
552 )
553 .unwrap();
554
555 let req_body_value: JsonValue = serde_json::from_slice(req.body()).unwrap();
556 assert_eq!(
557 req_body_value,
558 json!({
559 "identifier": {
560 "type": "m.id.thirdparty",
561 "medium": "email",
562 "address": "hello@example.com"
563 },
564 "type": "m.login.password",
565 "password": "deadbeef",
566 "initial_device_display_name": "test",
567 })
568 );
569 }
570 }
571}