use crate::module::ImModule;
use crate::send::upload_props::{self, PendingMediaPut, PlannedMedia, PreparedMedia};
use crate::state::{ChannelId, TemporaryId};
use crate::ImError;
use helix_core::effect::TimerId;
use helix_core::tick::{AppCommand, PortOutcome};
use helix_core::{Correlation, Effect, EffectSink, Module, Tick};
use serde_json::Value;
use std::collections::VecDeque;
#[derive(Debug)]
pub(crate) struct PendingScheduleMedia {
payload: Value,
remaining: VecDeque<PlannedMedia>,
current: PlannedMedia,
prepared: Option<PreparedMedia>,
timer: TimerId,
}
impl ImModule {
pub(crate) fn begin_schedule_media(
&mut self,
bytes: &[u8],
now_ms: u64,
out: &mut EffectSink,
) -> Result<bool, ImError> {
let mut payload: Value =
serde_json::from_slice(bytes).map_err(|e| ImError::Parse(e.to_string()))?;
crate::commands::handle_outbound(
"im_create_schedule",
bytes,
&self.config.api_base_url,
&self.config.default_api_base_url,
self.state.connection_id.as_deref(),
Correlation::from_raw(0),
)?;
let plan = upload_props::build_upload_plan(
payload["type"].as_str().unwrap_or("TEXT"),
payload["message"].as_str().unwrap_or(""),
payload
.get("props")
.cloned()
.unwrap_or_else(|| serde_json::json!({})),
)?;
if plan.media.is_empty() {
return Ok(false);
}
if self.state.pending_schedule_media.len() >= 32 {
return Err(ImError::Parse("too many pending schedule uploads".into()));
}
upload_props::validate_java_api_base_url(&self.config.default_api_base_url)?;
payload["props"] = plan.props;
let mut remaining: VecDeque<_> = plan.media.into();
let current = remaining
.pop_front()
.ok_or_else(|| ImError::Parse("missing schedule media".into()))?;
let timer = self.alloc_timer();
self.dispatch_schedule_media(
PendingScheduleMedia {
payload,
remaining,
current,
prepared: None,
timer,
},
now_ms,
out,
)?;
Ok(true)
}
fn dispatch_schedule_media(
&mut self,
pending: PendingScheduleMedia,
_now_ms: u64,
out: &mut EffectSink,
) -> Result<(), ImError> {
let corr = self.alloc_corr_internal();
let effect = if let Some(prepared) = &pending.prepared {
Effect::UploadFile {
corr,
req: upload_props::media_upload_request(
&pending.current.input,
prepared,
&pending.current.target,
)?,
}
} else {
let upload_id = format!(
"schedule-{}-{}",
pending.payload["req_id"].as_str().unwrap_or("local"),
corr.raw()
);
Effect::Http {
corr,
req: upload_props::prepare_upload_request(
&self.config.default_api_base_url,
&upload_id,
&pending.current.input,
&pending.current.target,
crate::acl::sync_http_effects::session_auth_headers(
self.state.connection_id.as_deref(),
),
)?,
}
};
out.push(Effect::ScheduleTimer {
id: pending.timer,
after_ms: 120_000,
});
self.state.pending_schedule_media.insert(corr, pending);
out.push(effect);
Ok(())
}
pub(crate) fn schedule_media_reply(
&mut self,
mut pending: PendingScheduleMedia,
outcome: &PortOutcome,
now_ms: u64,
out: &mut EffectSink,
) -> Result<(), ImError> {
out.push(Effect::CancelTimer { id: pending.timer });
let req_id = pending.payload["req_id"]
.as_str()
.unwrap_or_default()
.to_owned();
let result = (|| -> Result<(), ImError> {
let reply = match outcome {
PortOutcome::Ok(reply) => reply,
PortOutcome::Err(_) => {
return Err(ImError::Parse("schedule media upload failed".into()))
}
};
if let Some(prepared) = pending.prepared.take() {
let channel_id = pending.payload["channel_id"]
.as_str()
.and_then(ChannelId::from_str)
.ok_or_else(|| ImError::Parse("invalid schedule channel".into()))?;
let put = PendingMediaPut {
temporary_id: TemporaryId(req_id.clone()),
channel_id,
target: pending.current.target,
input: pending.current.input,
prepared,
};
upload_props::mark_media_complete(&mut pending.payload["props"], &put)?;
if let Some(next) = pending.remaining.pop_front() {
pending.current = next;
return self.dispatch_schedule_media(pending, now_ms, out);
}
let bytes = serde_json::to_vec(&pending.payload)
.map_err(|e| ImError::Serialize(e.to_string()))?;
self.handle(
&Tick::Command(AppCommand::new("im_create_schedule", bytes)),
now_ms,
out,
)
.map_err(|e| ImError::Parse(e.to_string()))
} else {
pending.prepared = Some(
upload_props::parse_prepare_reply(
reply.0.as_ref(),
&pending.current.input,
&pending.current.target,
)
.map_err(ImError::Parse)?,
);
self.dispatch_schedule_media(pending, now_ms, out)
}
})();
if result.is_err() {
out.push(crate::read_relay::emit_read_error(
&req_id,
"SCHEDULE_MEDIA_UPLOAD_FAILED",
));
}
Ok(())
}
pub(crate) fn stop_schedule_media(&mut self, out: &mut EffectSink) {
for (_, pending) in self.state.pending_schedule_media.drain() {
out.push(Effect::CancelTimer { id: pending.timer });
out.push(crate::read_relay::emit_read_error(
pending.payload["req_id"].as_str().unwrap_or_default(),
"SCHEDULE_MEDIA_INTERRUPTED",
));
}
}
pub(crate) fn schedule_media_timeout(&mut self, id: TimerId, out: &mut EffectSink) -> bool {
let corr = self
.state
.pending_schedule_media
.iter()
.find_map(|(corr, pending)| (pending.timer == id).then_some(*corr));
if let Some(pending) = corr.and_then(|corr| self.state.pending_schedule_media.remove(&corr))
{
out.push(crate::read_relay::emit_read_error(
pending.payload["req_id"].as_str().unwrap_or_default(),
"SCHEDULE_MEDIA_UPLOAD_TIMEOUT",
));
return true;
}
false
}
}