mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2693,6 +2693,8 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
"ical",
|
||||||
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rustical_dav",
|
"rustical_dav",
|
||||||
"rustical_dav_push",
|
"rustical_dav_push",
|
||||||
@@ -2722,6 +2724,7 @@ dependencies = [
|
|||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rustical_dav",
|
"rustical_dav",
|
||||||
"rustical_dav_push",
|
"rustical_dav_push",
|
||||||
@@ -2797,6 +2800,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"http",
|
"http",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rustical_ical",
|
"rustical_ical",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ tower-http = { version = "0.6", features = [
|
|||||||
"normalize-path",
|
"normalize-path",
|
||||||
"catch-panic",
|
"catch-panic",
|
||||||
] }
|
] }
|
||||||
|
percent-encoding = "2.3"
|
||||||
rustical_dav = { path = "./crates/dav/" }
|
rustical_dav = { path = "./crates/dav/" }
|
||||||
rustical_dav_push = { path = "./crates/dav_push/" }
|
rustical_dav_push = { path = "./crates/dav_push/" }
|
||||||
rustical_store = { path = "./crates/store/" }
|
rustical_store = { path = "./crates/store/" }
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ rustical_store = { workspace = true }
|
|||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
|
ical.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
rustical_dav_push.workspace = true
|
rustical_dav_push.workspace = true
|
||||||
|
|||||||
96
crates/caldav/src/calendar/methods/get.rs
Normal file
96
crates/caldav/src/calendar/methods/get.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use crate::Error;
|
||||||
|
use crate::calendar::CalendarResourceService;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::{extract::Path, response::Response};
|
||||||
|
use headers::{ContentType, HeaderMapExt};
|
||||||
|
use http::{HeaderValue, StatusCode, header};
|
||||||
|
use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||||
|
use ical::property::Property;
|
||||||
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
|
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
|
||||||
|
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip(cal_store))]
|
||||||
|
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
||||||
|
Path((principal, calendar_id)): Path<(String, String)>,
|
||||||
|
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
|
if !user.is_principal(&principal) {
|
||||||
|
return Err(crate::Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
||||||
|
if !user.is_principal(&calendar.principal) {
|
||||||
|
return Err(crate::Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
||||||
|
|
||||||
|
let mut timezones = HashMap::new();
|
||||||
|
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
||||||
|
|
||||||
|
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
||||||
|
.gregorian()
|
||||||
|
.prodid("RustiCal");
|
||||||
|
if calendar.displayname.is_some() {
|
||||||
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
|
name: "X-WR-CALNAME".to_owned(),
|
||||||
|
value: calendar.displayname,
|
||||||
|
params: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if calendar.description.is_some() {
|
||||||
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
|
name: "X-WR-CALDESC".to_owned(),
|
||||||
|
value: calendar.description,
|
||||||
|
params: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if calendar.timezone_id.is_some() {
|
||||||
|
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||||
|
name: "X-WR-TIMEZONE".to_owned(),
|
||||||
|
value: calendar.timezone_id,
|
||||||
|
params: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut ical_calendar = ical_calendar_builder.build();
|
||||||
|
|
||||||
|
for object in &objects {
|
||||||
|
match object.get_data() {
|
||||||
|
CalendarObjectComponent::Event(EventObject {
|
||||||
|
event,
|
||||||
|
timezones: object_timezones,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
timezones.extend(object_timezones);
|
||||||
|
ical_calendar.events.push(event.clone());
|
||||||
|
}
|
||||||
|
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
|
||||||
|
ical_calendar.todos.push(todo.clone());
|
||||||
|
}
|
||||||
|
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
|
||||||
|
ical_calendar.journals.push(journal.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
|
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||||
|
|
||||||
|
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
|
||||||
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
|
hdrs.insert(
|
||||||
|
header::CONTENT_DISPOSITION,
|
||||||
|
HeaderValue::from_str(&format!(
|
||||||
|
"attachement; filename*=UTF-8''{filename}; filename={filename}",
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod mkcalendar;
|
pub mod mkcalendar;
|
||||||
// pub mod post;
|
// pub mod post;
|
||||||
|
pub mod get;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::calendar::methods::get::route_get;
|
||||||
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
||||||
use crate::calendar::methods::report::route_report_calendar;
|
use crate::calendar::methods::report::route_report_calendar;
|
||||||
use crate::calendar::resource::CalendarResource;
|
use crate::calendar::resource::CalendarResource;
|
||||||
@@ -118,6 +119,13 @@ impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceSer
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_get::<C, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
||||||
{
|
{
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ rustical_dav_push.workspace = true
|
|||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
|
|||||||
59
crates/carddav/src/addressbook/methods/get.rs
Normal file
59
crates/carddav/src/addressbook/methods/get.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::Error;
|
||||||
|
use crate::addressbook::AddressbookResourceService;
|
||||||
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum_extra::headers::{ContentType, HeaderMapExt};
|
||||||
|
use http::{HeaderValue, StatusCode, header};
|
||||||
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
|
use rustical_dav::resource::Resource;
|
||||||
|
use rustical_ical::AddressObject;
|
||||||
|
use rustical_store::auth::User;
|
||||||
|
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip(addr_store))]
|
||||||
|
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
|
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
|
if !user.is_principal(&principal) {
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let addressbook = addr_store
|
||||||
|
.get_addressbook(&principal, &addressbook_id, false)
|
||||||
|
.await?;
|
||||||
|
let addressbook_resource = AddressbookResource(addressbook);
|
||||||
|
if !addressbook_resource
|
||||||
|
.get_user_privileges(&user)?
|
||||||
|
.has(&UserPrivilege::Read)
|
||||||
|
{
|
||||||
|
return Err(Error::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let objects = addr_store.get_objects(&principal, &addressbook_id).await?;
|
||||||
|
let vcf = objects
|
||||||
|
.iter()
|
||||||
|
.map(AddressObject::get_vcf)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\r\n");
|
||||||
|
|
||||||
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
|
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||||
|
let filename = format!("{}_{}.vcf", principal, addressbook_id);
|
||||||
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
|
hdrs.insert(
|
||||||
|
header::CONTENT_DISPOSITION,
|
||||||
|
HeaderValue::from_str(&format!(
|
||||||
|
"attachement; filename*=UTF-8''{filename}; filename={filename}",
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
Ok(resp.body(Body::new(vcf)).unwrap())
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod mkcol;
|
pub mod mkcol;
|
||||||
// pub mod post;
|
// pub mod post;
|
||||||
|
pub mod get;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::methods::mkcol::route_mkcol;
|
|||||||
use super::methods::report::route_report_addressbook;
|
use super::methods::report::route_report_addressbook;
|
||||||
use crate::address_object::AddressObjectResourceService;
|
use crate::address_object::AddressObjectResourceService;
|
||||||
use crate::address_object::resource::AddressObjectResource;
|
use crate::address_object::resource::AddressObjectResource;
|
||||||
|
use crate::addressbook::methods::get::route_get;
|
||||||
use crate::addressbook::resource::AddressbookResource;
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -121,6 +122,13 @@ impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookReso
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_get::<AS, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
Some(|state, req| {
|
Some(|state, req| {
|
||||||
let mut service = Handler::with_state(route_mkcol::<AS, S>, state);
|
let mut service = Handler::with_state(route_mkcol::<AS, S>, state);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ uuid.workspace = true
|
|||||||
url.workspace = true
|
url.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
rustical_oidc.workspace = true
|
rustical_oidc.workspace = true
|
||||||
axum-extra.workspace= true
|
axum-extra.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
tower-sessions = "0.14"
|
tower-sessions.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ html {
|
|||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button,
|
||||||
|
.button {
|
||||||
border: none;
|
border: none;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -163,9 +164,9 @@ table {
|
|||||||
"title comps color-chip"
|
"title comps color-chip"
|
||||||
"description . color-chip"
|
"description . color-chip"
|
||||||
"subscription-url . color-chip"
|
"subscription-url . color-chip"
|
||||||
"restore . color-chip"
|
"actions . color-chip"
|
||||||
". . color-chip";
|
". . color-chip";
|
||||||
grid-template-rows: 12px auto auto auto 12px;
|
grid-template-rows: 12px auto auto auto auto 12px;
|
||||||
grid-template-columns: min-content auto 80px;
|
grid-template-columns: min-content auto 80px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -184,6 +185,10 @@ table {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: 8px initial;
|
||||||
|
}
|
||||||
|
|
||||||
.comps {
|
.comps {
|
||||||
grid-area: comps;
|
grid-area: comps;
|
||||||
|
|
||||||
@@ -212,8 +217,9 @@ table {
|
|||||||
grid-area: color-chip;
|
grid-area: color-chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.restore-form {
|
.actions {
|
||||||
grid-area: restore;
|
grid-area: actions;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -78,6 +78,11 @@
|
|||||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||||
<span class="subscription-url">{{ subscription_url }}</span>
|
<span class="subscription-url">{{ subscription_url }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET">
|
||||||
|
<button type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="color-chip"></div>
|
<div class="color-chip"></div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -101,9 +106,11 @@
|
|||||||
<span class="description">
|
<span class="description">
|
||||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
<div class="actions">
|
||||||
<button type="submit">Restore</button>
|
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
||||||
</form>
|
<button type="submit">Restore</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="color-chip"></div>
|
<div class="color-chip"></div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -168,6 +175,11 @@
|
|||||||
<span class="description">
|
<span class="description">
|
||||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="actions">
|
||||||
|
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
|
||||||
|
<button type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -184,9 +196,11 @@
|
|||||||
<span class="description">
|
<span class="description">
|
||||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
<div class="actions">
|
||||||
<button type="submit">Restore</button>
|
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
||||||
</form>
|
<button type="submit">Restore</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use axum::{
|
|||||||
use axum_extra::extract::Host;
|
use axum_extra::extract::Host;
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt};
|
||||||
use http::{HeaderValue, StatusCode, header};
|
use http::{HeaderValue, StatusCode, header};
|
||||||
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
use rand::{Rng, distr::Alphanumeric};
|
use rand::{Rng, distr::Alphanumeric};
|
||||||
use rustical_store::auth::{AuthenticationProvider, User};
|
use rustical_store::auth::{AuthenticationProvider, User};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -79,6 +80,7 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
|||||||
ContentType::from_str("application/x-apple-aspen-config; charset=utf-8").unwrap(),
|
ContentType::from_str("application/x-apple-aspen-config; charset=utf-8").unwrap(),
|
||||||
);
|
);
|
||||||
let filename = format!("rustical-{}.mobileconfig", user_id);
|
let filename = format!("rustical-{}.mobileconfig", user_id);
|
||||||
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
hdrs.insert(
|
hdrs.insert(
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_DISPOSITION,
|
||||||
HeaderValue::from_str(&format!(
|
HeaderValue::from_str(&format!(
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use std::{collections::HashMap, str::FromStr};
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EventObject {
|
pub struct EventObject {
|
||||||
pub(crate) event: IcalEvent,
|
pub event: IcalEvent,
|
||||||
pub(crate) timezones: HashMap<String, IcalTimeZone>,
|
pub timezones: HashMap<String, IcalTimeZone>,
|
||||||
pub(crate) ics: String,
|
pub(crate) ics: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ impl CalendarObject {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_data(&self) -> &CalendarObjectComponent {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_id(&self) -> &str {
|
pub fn get_id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user