mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-12 23:43:22 -05:00
Merge branch 'master' into auth
This commit is contained in:
commit
4f87c16c31
451
Cargo.lock
generated
451
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -38,11 +38,11 @@ moonfire-db = { path = "db" }
|
||||
moonfire-ffmpeg = { path = "ffmpeg" }
|
||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||
openssl = "0.10"
|
||||
parking_lot = { version = "0.6", features = [] }
|
||||
parking_lot = { version = "0.7", features = [] }
|
||||
reffers = "0.5.1"
|
||||
regex = "1.0"
|
||||
ring = "0.12.1"
|
||||
rusqlite = "0.14"
|
||||
rusqlite = "0.15"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
@ -51,14 +51,14 @@ time = "0.1"
|
||||
tokio = "0.1.8"
|
||||
tokio-signal = "0.2"
|
||||
url = "1.4"
|
||||
uuid = { version = "0.6", features = ["serde", "std", "v4"] }
|
||||
uuid = { version = "0.7", features = ["serde", "std", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = "0.9.5"
|
||||
tempdir = "0.3"
|
||||
|
||||
[dependencies.cursive]
|
||||
version = "0.9"
|
||||
version = "0.10"
|
||||
#default-features = false
|
||||
#features = ["termion-backend"]
|
||||
|
||||
|
@ -14,5 +14,5 @@ path = "lib.rs"
|
||||
failure = "0.1.1"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
parking_lot = { version = "0.6", features = [] }
|
||||
parking_lot = { version = "0.7", features = [] }
|
||||
time = "0.1"
|
||||
|
@ -23,10 +23,10 @@ lru-cache = "0.1"
|
||||
moonfire-base = { path = "../base" }
|
||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||
openssl = "0.10"
|
||||
parking_lot = { version = "0.6", features = [] }
|
||||
parking_lot = { version = "0.7", features = [] }
|
||||
protobuf = "2.0"
|
||||
regex = "1.0"
|
||||
rusqlite = "0.14"
|
||||
rusqlite = "0.15"
|
||||
tempdir = "0.3"
|
||||
time = "0.1"
|
||||
uuid = { version = "0.6", features = ["std", "v4"] }
|
||||
uuid = { version = "0.7", features = ["std", "v4"] }
|
||||
|
@ -34,7 +34,7 @@ use failure::Error;
|
||||
use fnv::FnvHashMap;
|
||||
use libpasta;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::{self, Connection, Transaction};
|
||||
use rusqlite::{self, Connection, Transaction, types::ToSql};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
@ -343,7 +343,7 @@ impl State {
|
||||
from
|
||||
user
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = row.get_checked(0)?;
|
||||
@ -594,7 +594,7 @@ impl State {
|
||||
let addr = req.addr_buf();
|
||||
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
|
||||
stmt.execute(&[
|
||||
&req.when_sec,
|
||||
&req.when_sec as &ToSql,
|
||||
&req.user_agent,
|
||||
&addr,
|
||||
&(reason as i32),
|
||||
|
@ -36,7 +36,7 @@ use failure::Error;
|
||||
use fnv::FnvHashMap;
|
||||
use raw;
|
||||
use recording;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use schema;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::fs;
|
||||
@ -57,7 +57,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<(), Error> {
|
||||
"#)?;
|
||||
let mut garbage_stmt = conn.prepare_cached(
|
||||
"select composite_id from garbage where sample_file_dir_id = ?")?;
|
||||
let mut rows = dir_stmt.query(&[])?;
|
||||
let mut rows = dir_stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let mut meta = schema::DirMeta::default();
|
||||
@ -93,7 +93,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<(), Error> {
|
||||
let mut stmt = conn.prepare(r#"
|
||||
select id, sample_file_dir_id from stream where sample_file_dir_id is not null
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let stream_id = row.get_checked(0)?;
|
||||
|
29
db/db.rs
29
db/db.rs
@ -62,7 +62,7 @@ use openssl::hash;
|
||||
use parking_lot::{Mutex,MutexGuard};
|
||||
use raw;
|
||||
use recording::{self, TIME_UNITS_PER_SEC};
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use schema;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::cell::RefCell;
|
||||
@ -101,7 +101,7 @@ pub struct FromSqlUuid(pub Uuid);
|
||||
|
||||
impl rusqlite::types::FromSql for FromSqlUuid {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let uuid = Uuid::from_bytes(value.as_blob()?)
|
||||
let uuid = Uuid::from_slice(value.as_blob()?)
|
||||
.map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))?;
|
||||
Ok(FromSqlUuid(uuid))
|
||||
}
|
||||
@ -881,7 +881,7 @@ impl LockedDatabase {
|
||||
let mut stmt = tx.prepare_cached(
|
||||
r"update open set duration_90k = ?, end_time_90k = ? where id = ?")?;
|
||||
let rows = stmt.execute(&[
|
||||
&(recording::Time::new(clocks.monotonic()) - self.open_monotonic).0,
|
||||
&(recording::Time::new(clocks.monotonic()) - self.open_monotonic).0 as &ToSql,
|
||||
&recording::Time::new(clocks.realtime()).0,
|
||||
&o.id,
|
||||
])?;
|
||||
@ -1002,7 +1002,7 @@ impl LockedDatabase {
|
||||
update sample_file_dir set last_complete_open_id = ? where id = ?
|
||||
"#)?;
|
||||
for &id in in_progress.keys() {
|
||||
if stmt.execute(&[&o.id, &id])? != 1 {
|
||||
if stmt.execute(&[&o.id as &ToSql, &id])? != 1 {
|
||||
bail!("unable to update dir {}", id);
|
||||
}
|
||||
}
|
||||
@ -1260,7 +1260,7 @@ impl LockedDatabase {
|
||||
from
|
||||
video_sample_entry
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = row.get_checked(0)?;
|
||||
@ -1300,7 +1300,7 @@ impl LockedDatabase {
|
||||
from
|
||||
sample_file_dir d left join open o on (d.last_complete_open_id = o.id);
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = row.get_checked(0)?;
|
||||
@ -1342,7 +1342,7 @@ impl LockedDatabase {
|
||||
from
|
||||
camera;
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = row.get_checked(0)?;
|
||||
@ -1381,7 +1381,7 @@ impl LockedDatabase {
|
||||
from
|
||||
stream;
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = row.get_checked(0)?;
|
||||
@ -1486,7 +1486,7 @@ impl LockedDatabase {
|
||||
self.conn.execute(r#"
|
||||
insert into sample_file_dir (path, uuid, last_complete_open_id)
|
||||
values (?, ?, ?)
|
||||
"#, &[&path, &uuid_bytes, &o.id])?;
|
||||
"#, &[&path as &ToSql, &uuid_bytes, &o.id])?;
|
||||
let id = self.conn.last_insert_rowid() as i32;
|
||||
use ::std::collections::btree_map::Entry;
|
||||
let e = self.sample_file_dirs_by_id.entry(id);
|
||||
@ -1731,7 +1731,7 @@ impl LockedDatabase {
|
||||
/// Note this doesn't set journal options, so that it can be used on in-memory databases for
|
||||
/// test code.
|
||||
pub fn init(conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
conn.execute("pragma foreign_keys = on", &[])?;
|
||||
conn.execute("pragma foreign_keys = on", &[] as &[&ToSql])?;
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute_batch(include_str!("schema.sql"))?;
|
||||
{
|
||||
@ -1750,11 +1750,12 @@ pub fn init(conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
pub fn get_schema_version(conn: &rusqlite::Connection) -> Result<Option<i32>, Error> {
|
||||
let ver_tables: i32 = conn.query_row_and_then(
|
||||
"select count(*) from sqlite_master where name = 'version'",
|
||||
&[], |row| row.get_checked(0))?;
|
||||
&[] as &[&ToSql], |row| row.get_checked(0))?;
|
||||
if ver_tables == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(conn.query_row_and_then("select max(id) from version", &[], |row| row.get_checked(0))?))
|
||||
Ok(Some(conn.query_row_and_then("select max(id) from version", &[] as &[&ToSql],
|
||||
|row| row.get_checked(0))?))
|
||||
}
|
||||
|
||||
/// The recording database. Abstracts away SQLite queries. Also maintains in-memory state
|
||||
@ -1791,7 +1792,7 @@ impl<C: Clocks + Clone> Database<C> {
|
||||
/// Creates the database from a caller-supplied SQLite connection.
|
||||
pub fn new(clocks: C, conn: rusqlite::Connection,
|
||||
read_write: bool) -> Result<Database<C>, Error> {
|
||||
conn.execute("pragma foreign_keys = on", &[])?;
|
||||
conn.execute("pragma foreign_keys = on", &[] as &[&ToSql])?;
|
||||
{
|
||||
let ver = get_schema_version(&conn)?.ok_or_else(|| format_err!(
|
||||
"no such table: version. \
|
||||
@ -1821,7 +1822,7 @@ impl<C: Clocks + Clone> Database<C> {
|
||||
let mut stmt = conn.prepare(" insert into open (uuid, start_time_90k) values (?, ?)")?;
|
||||
let uuid = Uuid::new_v4();
|
||||
let uuid_bytes = &uuid.as_bytes()[..];
|
||||
stmt.execute(&[&uuid_bytes, &real.0])?;
|
||||
stmt.execute(&[&uuid_bytes as &ToSql, &real.0])?;
|
||||
Some(Open {
|
||||
id: conn.last_insert_rowid() as u32,
|
||||
uuid,
|
||||
|
@ -34,7 +34,7 @@ use db::{self, CompositeId, FromSqlUuid};
|
||||
use failure::{Error, ResultExt};
|
||||
use fnv::FnvHashSet;
|
||||
use recording;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use std::ops::Range;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -166,7 +166,7 @@ fn list_recordings_inner(mut rows: rusqlite::Rows,
|
||||
}
|
||||
|
||||
pub(crate) fn get_db_uuid(conn: &rusqlite::Connection) -> Result<Uuid, Error> {
|
||||
conn.query_row("select uuid from meta", &[], |row| -> Result<Uuid, Error> {
|
||||
conn.query_row("select uuid from meta", &[] as &[&ToSql], |row| -> Result<Uuid, Error> {
|
||||
let uuid: FromSqlUuid = row.get_checked(0)?;
|
||||
Ok(uuid.0)
|
||||
})?
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
use db;
|
||||
use failure::Error;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
|
||||
mod v0_to_v1;
|
||||
mod v1_to_v2;
|
||||
@ -52,7 +52,7 @@ pub struct Args<'a> {
|
||||
|
||||
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
||||
assert!(!requested.contains(';')); // quick check for accidental sql injection.
|
||||
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[],
|
||||
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[] as &[&ToSql],
|
||||
|row| row.get_checked::<_, String>(0))??;
|
||||
info!("...database now in journal_mode {} (requested {}).", actual, requested);
|
||||
Ok(())
|
||||
@ -68,7 +68,8 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
{
|
||||
assert_eq!(upgraders.len(), db::EXPECTED_VERSION as usize);
|
||||
let old_ver =
|
||||
conn.query_row("select max(id) from version", &[], |row| row.get_checked(0))??;
|
||||
conn.query_row("select max(id) from version", &[] as &[&ToSql],
|
||||
|row| row.get_checked(0))??;
|
||||
if old_ver > db::EXPECTED_VERSION {
|
||||
bail!("Database is at version {}, later than expected {}",
|
||||
old_ver, db::EXPECTED_VERSION);
|
||||
@ -84,7 +85,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
tx.execute(r#"
|
||||
insert into version (id, unix_time, notes)
|
||||
values (?, cast(strftime('%s', 'now') as int32), ?)
|
||||
"#, &[&(ver + 1), &UPGRADE_NOTES])?;
|
||||
"#, &[&(ver + 1) as &ToSql, &UPGRADE_NOTES])?;
|
||||
tx.commit()?;
|
||||
}
|
||||
}
|
||||
@ -93,7 +94,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
// compiles the SQLite3 amalgamation with -DSQLITE_DEFAULT_FOREIGN_KEYS=1). Ensure it's
|
||||
// always on. Note that our foreign keys are immediate rather than deferred, so we have to
|
||||
// be careful about the order of operations during the upgrade.
|
||||
conn.execute("pragma foreign_keys = on", &[])?;
|
||||
conn.execute("pragma foreign_keys = on", &[] as &[&ToSql])?;
|
||||
|
||||
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
||||
// without compromising safety.
|
||||
|
@ -33,7 +33,7 @@
|
||||
use db;
|
||||
use failure::Error;
|
||||
use recording;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
@ -141,7 +141,7 @@ fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState
|
||||
insert into recording_playback values (:composite_id, :sample_file_uuid, :sample_file_sha1,
|
||||
:video_index)
|
||||
"#)?;
|
||||
let mut rows = select.query(&[])?;
|
||||
let mut rows = select.query(&[] as &[&ToSql])?;
|
||||
let mut camera_state: HashMap<i32, CameraState> = HashMap::new();
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
@ -216,7 +216,7 @@ fn fill_camera(tx: &rusqlite::Transaction, camera_state: HashMap<i32, CameraStat
|
||||
insert into camera values (:id, :uuid, :short_name, :description, :host, :username, :password,
|
||||
:main_rtsp_path, :sub_rtsp_path, :retain_bytes, :next_recording_id)
|
||||
"#)?;
|
||||
let mut rows = select.query(&[])?;
|
||||
let mut rows = select.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id: i32 = row.get_checked(0)?;
|
||||
|
@ -33,7 +33,7 @@
|
||||
use dir;
|
||||
use failure::Error;
|
||||
use libc;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use schema::DirMeta;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
@ -122,7 +122,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
||||
tx.execute(r#"
|
||||
insert into sample_file_dir (path, uuid, last_complete_open_id)
|
||||
values (?, ?, ?)
|
||||
"#, &[&sample_file_path, &dir_uuid_bytes, &open_id])?;
|
||||
"#, &[&sample_file_path as &ToSql, &dir_uuid_bytes, &open_id])?;
|
||||
|
||||
tx.execute_batch(r#"
|
||||
drop table reserved_sample_files;
|
||||
@ -298,7 +298,7 @@ fn verify_dir_contents(sample_file_path: &str, tx: &rusqlite::Transaction) -> Re
|
||||
from
|
||||
(select count(*) as c from recording) a,
|
||||
(select count(*) as c from reserved_sample_files) b;
|
||||
"#, &[], |r| r.get_checked(0))??;
|
||||
"#, &[] as &[&ToSql], |r| r.get_checked(0))??;
|
||||
let mut files = ::fnv::FnvHashSet::with_capacity_and_hasher(n as usize, Default::default());
|
||||
for e in fs::read_dir(sample_file_path)? {
|
||||
let e = e?;
|
||||
@ -320,7 +320,7 @@ fn verify_dir_contents(sample_file_path: &str, tx: &rusqlite::Transaction) -> Re
|
||||
Ok(u) => u,
|
||||
Err(_) => bail!("unexpected file {:?} in {:?}", f, sample_file_path),
|
||||
};
|
||||
if s != uuid.hyphenated().to_string() { // non-canonical form.
|
||||
if s != uuid.to_hyphenated_ref().to_string() { // non-canonical form.
|
||||
bail!("unexpected file {:?} in {:?}", f, sample_file_path);
|
||||
}
|
||||
files.insert(uuid);
|
||||
@ -329,7 +329,7 @@ fn verify_dir_contents(sample_file_path: &str, tx: &rusqlite::Transaction) -> Re
|
||||
// Iterate through the database and check that everything has a matching file.
|
||||
{
|
||||
let mut stmt = tx.prepare(r"select sample_file_uuid from recording_playback")?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let uuid: ::db::FromSqlUuid = row.get_checked(0)?;
|
||||
@ -340,7 +340,7 @@ fn verify_dir_contents(sample_file_path: &str, tx: &rusqlite::Transaction) -> Re
|
||||
}
|
||||
|
||||
let mut stmt = tx.prepare(r"select uuid from reserved_sample_files")?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let uuid: ::db::FromSqlUuid = row.get_checked(0)?;
|
||||
@ -368,7 +368,7 @@ fn fix_video_sample_entry(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
let mut insert = tx.prepare(r#"
|
||||
insert into video_sample_entry values (:id, :sha1, :width, :height, :rfc6381_codec, :data)
|
||||
"#)?;
|
||||
let mut rows = select.query(&[])?;
|
||||
let mut rows = select.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let data: Vec<u8> = row.get_checked(4)?;
|
||||
|
@ -40,7 +40,7 @@ use schema;
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use rusqlite;
|
||||
use rusqlite::{self, types::ToSql};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Opens the sample file dir.
|
||||
@ -57,7 +57,7 @@ fn open_sample_file_dir(tx: &rusqlite::Transaction) -> Result<Arc<dir::SampleFil
|
||||
sample_file_dir s
|
||||
join open o on (s.last_complete_open_id = o.id)
|
||||
cross join meta m
|
||||
"#, &[], |row| {
|
||||
"#, &[] as &[&ToSql], |row| {
|
||||
(row.get_checked(0).unwrap(),
|
||||
row.get_checked(1).unwrap(),
|
||||
row.get_checked(2).unwrap(),
|
||||
@ -84,7 +84,7 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
||||
from
|
||||
recording_playback
|
||||
"#)?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
let mut rows = stmt.query(&[] as &[&ToSql])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id = db::CompositeId(row.get_checked(0)?);
|
||||
@ -124,7 +124,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
||||
/// Gets a pathname for a sample file suitable for passing to open or unlink.
|
||||
fn get_uuid_pathname(uuid: Uuid) -> [libc::c_char; 37] {
|
||||
let mut buf = [0u8; 37];
|
||||
write!(&mut buf[..36], "{}", uuid.hyphenated()).expect("can't format uuid to pathname buf");
|
||||
write!(&mut buf[..36], "{}", uuid.to_hyphenated_ref())
|
||||
.expect("can't format uuid to pathname buf");
|
||||
|
||||
// libc::c_char seems to be i8 on some platforms (Linux/arm) and u8 on others (Linux/amd64).
|
||||
unsafe { mem::transmute::<[u8; 37], [libc::c_char; 37]>(buf) }
|
||||
|
@ -142,7 +142,7 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
|
||||
format!("{} stream at {}:\n\n{}", t.as_str(), url, description))
|
||||
.title("Stream test succeeded")
|
||||
.dismiss_button("Back"));
|
||||
}));
|
||||
})).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user