1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use language_tags::LanguageTag;
use ruma::{api::client::profile::get_profile, DeviceId, RoomId, UserId};
use url::Url;
use crate::Room;
mod element_call;
mod url_params;
pub use self::element_call::{EncryptionSystem, VirtualElementCallWidgetOptions};
/// Settings of the widget.
#[derive(Debug, Clone)]
pub struct WidgetSettings {
widget_id: String,
init_on_content_load: bool,
raw_url: Url,
}
impl WidgetSettings {
/// Create a new WidgetSettings instance
pub fn new(
id: String,
init_on_content_load: bool,
raw_url: &str,
) -> Result<Self, url::ParseError> {
Ok(Self { widget_id: id, init_on_content_load, raw_url: Url::parse(raw_url)? })
}
/// Widget's unique identifier.
pub fn widget_id(&self) -> &str {
&self.widget_id
}
/// Whether or not the widget should be initialized on load message
/// (`ContentLoad` message), or upon creation/attaching of the widget to
/// the SDK's state machine that drives the API.
pub fn init_on_content_load(&self) -> bool {
self.init_on_content_load
}
/// This contains the url from the widget state event.
/// In this url placeholders can be used to pass information from the client
/// to the widget. Possible values are: `$matrix_widget_id`,
/// `$matrix_display_name`, etc.
///
/// # Examples
///
/// `http://widget.domain?username=$userId` will become
/// `http://widget.domain?username=@user_matrix_id:server.domain`.
pub fn raw_url(&self) -> &Url {
&self.raw_url
}
/// Get the base url of the widget. Used as the target for PostMessages. In
/// case the widget is in a webview and not an IFrame. It contains the
/// schema and the authority e.g. `https://my.domain.org`. A postmessage would
/// be sent using: `postMessage(myMessage, widget_base_url)`.
pub fn base_url(&self) -> Option<Url> {
base_url(&self.raw_url)
}
/// Create the actual [`Url`] that can be used to setup the WebView or
/// IFrame that contains the widget.
///
/// # Arguments
///
/// * `room` - A matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to
/// adapt to the client. e.g. language, font-scale...
//
// TODO: add `From<WidgetStateEvent>`, so that `WidgetSettings` can be built
// by using the room state.
pub async fn generate_webview_url(
&self,
room: &Room,
props: ClientProperties,
) -> Result<Url, url::ParseError> {
self._generate_webview_url(
room.client().account().fetch_user_profile().await.unwrap_or_default(),
room.own_user_id(),
room.room_id(),
room.client().device_id().unwrap_or("UNKNOWN".into()),
room.client().homeserver(),
props,
)
}
// Using a separate function (without Room as a param) for tests.
fn _generate_webview_url(
&self,
profile: get_profile::v3::Response,
user_id: &UserId,
room_id: &RoomId,
device_id: &DeviceId,
homeserver_url: Url,
client_props: ClientProperties,
) -> Result<Url, url::ParseError> {
let avatar_url = profile.avatar_url.map(|url| url.to_string()).unwrap_or_default();
let query_props = url_params::QueryProperties {
widget_id: self.widget_id.clone(),
avatar_url,
display_name: profile.displayname.unwrap_or_default(),
user_id: user_id.into(),
room_id: room_id.into(),
language: client_props.language.to_string(),
client_theme: client_props.theme,
client_id: client_props.client_id,
device_id: device_id.into(),
homeserver_url: homeserver_url.into(),
};
let mut generated_url = self.raw_url.clone();
url_params::replace_properties(&mut generated_url, query_props);
Ok(generated_url)
}
}
/// The set of settings and properties for the widget based on the client
/// configuration. Those values are used generate the widget url.
#[derive(Debug)]
pub struct ClientProperties {
/// The client_id provides the widget with the option to behave differently
/// for different clients. e.g org.example.ios.
client_id: String,
/// The language the client is set to e.g. en-us.
language: LanguageTag,
/// A string describing the theme (dark, light) or org.example.dark.
theme: String,
}
impl ClientProperties {
/// Creates client properties. If a malformatted language tag is provided,
/// the default one (en-US) will be used.
///
/// # Arguments
/// * `client_id` - client identifier. This allows widgets to adapt to
/// specific clients (e.g. `io.element.web`).
/// * `language` - language that is used in the client (default: `en-US`).
/// * `theme` - theme (dark, light) or org.example.dark (default: `light`).
pub fn new(client_id: &str, language: Option<LanguageTag>, theme: Option<String>) -> Self {
// It is safe to unwrap "en-us".
let default_language = LanguageTag::parse("en-us").unwrap();
let default_theme = "light".to_owned();
Self {
language: language.unwrap_or(default_language),
client_id: client_id.to_owned(),
theme: theme.unwrap_or(default_theme),
}
}
}
fn base_url(url: &Url) -> Option<Url> {
let mut url = url.clone();
url.path_segments_mut().ok()?.clear();
url.set_query(None);
url.set_fragment(None);
Some(url)
}