Add json export
This commit is contained in:
parent
ed181a6518
commit
186ca5ea5a
4 changed files with 139 additions and 4 deletions
|
|
@ -39,9 +39,11 @@ pub struct Message {
|
||||||
/// The id of the message (unique within a room).
|
/// The id of the message (unique within a room).
|
||||||
pub id: Snowflake,
|
pub id: Snowflake,
|
||||||
/// The id of the message's parent, or null if top-level.
|
/// The id of the message's parent, or null if top-level.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub parent: Option<Snowflake>,
|
pub parent: Option<Snowflake>,
|
||||||
/// The edit id of the most recent edit of this message, or null if it's
|
/// The edit id of the most recent edit of this message, or null if it's
|
||||||
/// never been edited.
|
/// never been edited.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub previous_edit_id: Option<Snowflake>,
|
pub previous_edit_id: Option<Snowflake>,
|
||||||
/// The unix timestamp of when the message was posted.
|
/// The unix timestamp of when the message was posted.
|
||||||
pub time: Time,
|
pub time: Time,
|
||||||
|
|
@ -50,15 +52,18 @@ pub struct Message {
|
||||||
/// The content of the message (client-defined).
|
/// The content of the message (client-defined).
|
||||||
pub content: String,
|
pub content: String,
|
||||||
/// The id of the key that encrypts the message in storage.
|
/// The id of the key that encrypts the message in storage.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub encryption_key_id: Option<String>,
|
pub encryption_key_id: Option<String>,
|
||||||
/// The unix timestamp of when the message was last edited.
|
/// The unix timestamp of when the message was last edited.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub edited: Option<Time>,
|
pub edited: Option<Time>,
|
||||||
/// The unix timestamp of when the message was deleted.
|
/// The unix timestamp of when the message was deleted.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub deleted: Option<Time>,
|
pub deleted: Option<Time>,
|
||||||
/// If true, then the full content of this message is not included (see
|
/// If true, then the full content of this message is not included (see
|
||||||
/// [`GetMessage`](super::GetMessage) to obtain the message with full
|
/// [`GetMessage`](super::GetMessage) to obtain the message with full
|
||||||
/// content).
|
/// content).
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub truncated: bool,
|
pub truncated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,14 +272,16 @@ pub struct SessionView {
|
||||||
/// Id of the session, unique across all sessions globally.
|
/// Id of the session, unique across all sessions globally.
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
/// If true, this session belongs to a member of staff.
|
/// If true, this session belongs to a member of staff.
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub is_staff: bool,
|
pub is_staff: bool,
|
||||||
/// If true, this session belongs to a manager of the room.
|
/// If true, this session belongs to a manager of the room.
|
||||||
#[serde(default)]
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub is_manager: bool,
|
pub is_manager: bool,
|
||||||
/// For hosts and staff, the virtual address of the client.
|
/// For hosts and staff, the virtual address of the client.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub client_address: Option<String>,
|
pub client_address: Option<String>,
|
||||||
/// For staff, the real address of the client.
|
/// For staff, the real address of the client.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub real_client_address: Option<String>,
|
pub real_client_address: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Export logs from the vault to plain text files.
|
//! Export logs from the vault to plain text files.
|
||||||
|
|
||||||
|
mod json;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
@ -11,18 +12,22 @@ use crate::vault::Vault;
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
/// Human-readable tree-structured messages.
|
/// Human-readable tree-structured messages.
|
||||||
Text,
|
Text,
|
||||||
|
/// List of message objects in the same format as the euphoria API uses.
|
||||||
|
Json,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format {
|
impl Format {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Text => "text",
|
Self::Text => "text",
|
||||||
|
Self::Json => "json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extension(&self) -> &'static str {
|
fn extension(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Text => "txt",
|
Self::Text => "txt",
|
||||||
|
Self::Json => "json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +86,7 @@ pub async fn export(vault: &Vault, mut args: Args) -> anyhow::Result<()> {
|
||||||
let mut file = BufWriter::new(File::create(out)?);
|
let mut file = BufWriter::new(File::create(out)?);
|
||||||
match args.format {
|
match args.format {
|
||||||
Format::Text => text::export_to_file(vault, room, &mut file).await?,
|
Format::Text => text::export_to_file(vault, room, &mut file).await?,
|
||||||
|
Format::Json => json::export_to_file(vault, room, &mut file).await?,
|
||||||
}
|
}
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/export/json.rs
Normal file
47
src/export/json.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
|
|
||||||
|
use crate::vault::Vault;
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 10000;
|
||||||
|
|
||||||
|
pub async fn export_to_file(
|
||||||
|
vault: &Vault,
|
||||||
|
room: String,
|
||||||
|
file: &mut BufWriter<File>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let vault = vault.euph(room);
|
||||||
|
|
||||||
|
write!(file, "[")?;
|
||||||
|
|
||||||
|
let mut total = 0;
|
||||||
|
let mut offset = 0;
|
||||||
|
loop {
|
||||||
|
let messages = vault.chunk_at_offset(CHUNK_SIZE, offset).await;
|
||||||
|
offset += messages.len();
|
||||||
|
|
||||||
|
if messages.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
if total == 0 {
|
||||||
|
writeln!(file)?;
|
||||||
|
} else {
|
||||||
|
writeln!(file, ",")?;
|
||||||
|
}
|
||||||
|
serde_json::to_writer(&mut *file, &message)?; // Fancy reborrow! :D
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if total % 100000 == 0 {
|
||||||
|
println!(" {total} messages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(file, "\n]")?;
|
||||||
|
|
||||||
|
println!(" {total} messages in total");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use rusqlite::{named_params, params, Connection, OptionalExtension, ToSql, Trans
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::euph::api::{Message, Snowflake, Time, UserId};
|
use crate::euph::api::{Message, SessionView, Snowflake, Time, UserId};
|
||||||
use crate::euph::SmallMessage;
|
use crate::euph::SmallMessage;
|
||||||
use crate::store::{MsgStore, Path, Tree};
|
use crate::store::{MsgStore, Path, Tree};
|
||||||
|
|
||||||
|
|
@ -139,6 +139,19 @@ impl EuphVault {
|
||||||
let _ = self.vault.tx.send(request.into());
|
let _ = self.vault.tx.send(request.into());
|
||||||
rx.await.unwrap()
|
rx.await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn chunk_at_offset(&self, amount: usize, offset: usize) -> Vec<Message> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::GetChunkAtOffset {
|
||||||
|
room: self.room.clone(),
|
||||||
|
amount,
|
||||||
|
offset,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.vault.tx.send(request.into());
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -457,6 +470,12 @@ pub(super) enum EuphRequest {
|
||||||
id: Snowflake,
|
id: Snowflake,
|
||||||
seen: bool,
|
seen: bool,
|
||||||
},
|
},
|
||||||
|
GetChunkAtOffset {
|
||||||
|
room: String,
|
||||||
|
amount: usize,
|
||||||
|
offset: usize,
|
||||||
|
result: oneshot::Sender<Vec<Message>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EuphRequest {
|
impl EuphRequest {
|
||||||
|
|
@ -525,6 +544,12 @@ impl EuphRequest {
|
||||||
EuphRequest::SetOlderSeen { room, id, seen } => {
|
EuphRequest::SetOlderSeen { room, id, seen } => {
|
||||||
Self::set_older_seen(conn, room, id, seen)
|
Self::set_older_seen(conn, room, id, seen)
|
||||||
}
|
}
|
||||||
|
EuphRequest::GetChunkAtOffset {
|
||||||
|
room,
|
||||||
|
amount,
|
||||||
|
offset,
|
||||||
|
result,
|
||||||
|
} => Self::get_chunk_at_offset(conn, room, amount, offset, result),
|
||||||
};
|
};
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
// If an error occurs here, the rest of the UI will likely panic and
|
// If an error occurs here, the rest of the UI will likely panic and
|
||||||
|
|
@ -1235,4 +1260,54 @@ impl EuphRequest {
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_chunk_at_offset(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
amount: usize,
|
||||||
|
offset: usize,
|
||||||
|
result: oneshot::Sender<Vec<Message>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let mut query = conn.prepare(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
||||||
|
FROM euph_msgs
|
||||||
|
WHERE room = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT ?
|
||||||
|
OFFSET ?
|
||||||
|
",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let messages = query
|
||||||
|
.query_map(params![room, amount, offset], |row| {
|
||||||
|
Ok(Message {
|
||||||
|
id: row.get(0)?,
|
||||||
|
parent: row.get(1)?,
|
||||||
|
previous_edit_id: row.get(2)?,
|
||||||
|
time: row.get(3)?,
|
||||||
|
content: row.get(4)?,
|
||||||
|
encryption_key_id: row.get(5)?,
|
||||||
|
edited: row.get(6)?,
|
||||||
|
deleted: row.get(7)?,
|
||||||
|
truncated: row.get(8)?,
|
||||||
|
sender: SessionView {
|
||||||
|
id: UserId(row.get(9)?),
|
||||||
|
name: row.get(10)?,
|
||||||
|
server_id: row.get(11)?,
|
||||||
|
server_era: row.get(12)?,
|
||||||
|
session_id: row.get(13)?,
|
||||||
|
is_staff: row.get(14)?,
|
||||||
|
is_manager: row.get(15)?,
|
||||||
|
client_address: row.get(16)?,
|
||||||
|
real_client_address: row.get(17)?,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<rusqlite::Result<_>>()?;
|
||||||
|
let _ = result.send(messages);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue