use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct RawMessage {
pub participant: String,
pub timestamp: String,
pub text: String,
pub source: String,
}
fn extract_text_parts(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Array(blocks) => {
let mut out = Vec::new();
for block in blocks {
if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
out.push(text.to_string());
}
}
out.join("\n")
}
Value::Object(map) => {
if let Some(parts) = map.get("parts").and_then(|p| p.as_array()) {
let mut out = Vec::new();
for part in parts {
if let Some(s) = part.as_str() {
out.push(s.to_string());
}
}
return out.join("\n");
}
map.get("text")
.and_then(|t| t.as_str())
.unwrap_or("")
.to_string()
}
_ => String::new(),
}
}
#[derive(Deserialize)]
struct ClaudeExport {
conversations: Vec<ClaudeConversation>,
}
#[derive(Deserialize)]
struct ClaudeConversation {
messages: Vec<ClaudeMessage>,
}
#[derive(Deserialize)]
struct ClaudeMessage {
role: Option<String>,
text: Option<String>,
content: Option<Value>,
}
pub fn parse_claude_export(content: &str) -> Result<Vec<RawMessage>, String> {
let export: ClaudeExport =
serde_json::from_str(content).map_err(|e| format!("not a Claude export: {e}"))?;
let mut out = Vec::new();
for conv in export.conversations {
for msg in conv.messages {
let text = match (&msg.text, &msg.content) {
(Some(t), _) => t.clone(),
(None, Some(c)) => extract_text_parts(c),
(None, None) => String::new(),
};
out.push(RawMessage {
participant: msg.role.unwrap_or_else(|| "user".to_string()),
timestamp: String::new(),
text,
source: "claude".to_string(),
});
}
}
Ok(out)
}
#[derive(Deserialize)]
struct ChatGptExport {
mapping: std::collections::BTreeMap<String, ChatGptNode>,
}
#[derive(Deserialize)]
struct ChatGptNode {
#[serde(default)]
message: Option<ChatGptMessage>,
#[serde(default)]
mapping: Option<std::collections::BTreeMap<String, ChatGptNode>>,
}
#[derive(Deserialize)]
struct ChatGptMessage {
author: Option<ChatGptAuthor>,
#[serde(default)]
content: Option<Value>,
}
#[derive(Deserialize)]
struct ChatGptAuthor {
#[serde(default)]
role: Option<String>,
}
pub fn parse_chatgpt_export(content: &str) -> Result<Vec<RawMessage>, String> {
let export: ChatGptExport =
serde_json::from_str(content).map_err(|e| format!("not a ChatGPT export: {e}"))?;
let mut out = Vec::new();
collect_chatgpt_nodes(&export.mapping, &mut out);
Ok(out)
}
fn collect_chatgpt_nodes(
nodes: &std::collections::BTreeMap<String, ChatGptNode>,
out: &mut Vec<RawMessage>,
) {
for node in nodes.values() {
if let Some(msg) = &node.message {
if let Some(content) = msg.content.as_ref() {
let text = extract_text_parts(content);
if !text.trim().is_empty() {
out.push(RawMessage {
participant: msg
.author
.as_ref()
.and_then(|a| a.role.clone())
.unwrap_or_else(|| "user".to_string()),
timestamp: String::new(),
text,
source: "chatgpt".to_string(),
});
}
}
}
if let Some(nested) = &node.mapping {
collect_chatgpt_nodes(nested, out);
}
}
}
#[derive(Deserialize)]
struct SlackExport {
#[serde(default)]
#[allow(dead_code)] channel: Option<Value>,
messages: Vec<SlackMessage>,
}
#[derive(Deserialize)]
struct SlackMessage {
#[serde(default)]
r#type: Option<String>,
#[serde(default)]
user: Option<String>,
#[serde(default)]
text: Option<String>,
}
pub fn parse_slack_export(content: &str) -> Result<Vec<RawMessage>, String> {
let export: SlackExport =
serde_json::from_str(content).map_err(|e| format!("not a Slack export: {e}"))?;
let mut out = Vec::new();
for msg in export.messages {
if msg.r#type.as_deref() == Some("message") || msg.text.is_some() {
out.push(RawMessage {
participant: msg.user.unwrap_or_else(|| "unknown".to_string()),
timestamp: String::new(),
text: msg.text.unwrap_or_default(),
source: "slack".to_string(),
});
}
}
Ok(out)
}