actor(visibilityref): update function visibility and formatting across modules
This commit is contained in:
parent
9ffc7c9fb3
commit
f947c931cd
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2021,6 +2021,7 @@ dependencies = [
|
||||
"db",
|
||||
"futures",
|
||||
"hmac 0.13.0",
|
||||
"lazy_static",
|
||||
"model",
|
||||
"redis",
|
||||
"serde",
|
||||
|
||||
@ -7,13 +7,16 @@ use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::RigStreamChunk;
|
||||
use super::config::AgentConfig;
|
||||
use super::helpers::{build_input_string, check_token_budget, estimate_tokens};
|
||||
use super::hooks::{HookChain, HookLlmResponse, HookMessage, HookToolDef, ToolCallOutcome, ToolGuardrailDecision};
|
||||
use super::hooks::{
|
||||
HookChain, HookLlmResponse, HookMessage, HookToolDef, ToolCallOutcome,
|
||||
ToolGuardrailDecision,
|
||||
};
|
||||
use super::persistence::ActiveAgentRun;
|
||||
use super::request::{AgentRequest, AgentResult, AgentStep, ToolCallRecord};
|
||||
use super::subagent::run_experts;
|
||||
use super::RigStreamChunk;
|
||||
use crate::client::AiClient;
|
||||
use crate::error::{AiError, AiResult};
|
||||
|
||||
@ -48,9 +51,7 @@ impl RigAgent {
|
||||
tools: Vec<Box<dyn ToolDyn>>,
|
||||
) -> AiResult<String> {
|
||||
let (mut rx, handle) = self.run(request, tools);
|
||||
tokio::spawn(async move {
|
||||
while rx.recv().await.is_some() {}
|
||||
});
|
||||
tokio::spawn(async move { while rx.recv().await.is_some() {} });
|
||||
let result = handle.await.map_err(|_| {
|
||||
AiError::Response("agent task panicked".to_string())
|
||||
})?;
|
||||
@ -152,15 +153,24 @@ async fn execute_agent_run(
|
||||
// ---- SubAgent execution ----
|
||||
let expert_outputs = if !request.experts.is_empty() {
|
||||
let run = ActiveAgentRun {
|
||||
conversation_id: request.run_context.as_ref().and_then(|c| c.conversation_id),
|
||||
conversation_id: request
|
||||
.run_context
|
||||
.as_ref()
|
||||
.and_then(|c| c.conversation_id),
|
||||
message_id: None,
|
||||
invocation_id: request.run_context.as_ref().and_then(|c| c.invocation_id),
|
||||
invocation_id: request
|
||||
.run_context
|
||||
.as_ref()
|
||||
.and_then(|c| c.invocation_id),
|
||||
session_id: request.run_context.as_ref().and_then(|c| c.session_id),
|
||||
user_id: request.run_context.as_ref().and_then(|c| c.user_id),
|
||||
started_at: std::time::Instant::now(),
|
||||
current_step: 0,
|
||||
};
|
||||
let realtime = request.run_context.as_ref().and_then(|c| c.realtime.as_ref());
|
||||
let realtime = request
|
||||
.run_context
|
||||
.as_ref()
|
||||
.and_then(|c| c.realtime.as_ref());
|
||||
|
||||
// Notify frontend that subagents are starting.
|
||||
for expert in &request.experts {
|
||||
@ -173,7 +183,15 @@ async fn execute_agent_run(
|
||||
.await;
|
||||
}
|
||||
|
||||
match run_experts(&ai_client, &agent_config, &request.experts, realtime, &run).await {
|
||||
match run_experts(
|
||||
&ai_client,
|
||||
&agent_config,
|
||||
&request.experts,
|
||||
realtime,
|
||||
&run,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(outputs) => {
|
||||
for out in &outputs {
|
||||
let _ = tx
|
||||
@ -252,7 +270,10 @@ async fn execute_agent_run(
|
||||
Err(_elapsed) => {
|
||||
let _ = tx
|
||||
.send(RigStreamChunk::Failed {
|
||||
error: format!("agent timed out after {}s", dur.as_secs()),
|
||||
error: format!(
|
||||
"agent timed out after {}s",
|
||||
dur.as_secs()
|
||||
),
|
||||
})
|
||||
.await;
|
||||
return Err(AiError::Timeout {
|
||||
@ -284,7 +305,11 @@ async fn execute_agent_run(
|
||||
}
|
||||
|
||||
if let Some(limit) = max_total_tokens
|
||||
&& check_token_budget(estimated_input_tokens, accumulated_output_chars, limit)
|
||||
&& check_token_budget(
|
||||
estimated_input_tokens,
|
||||
accumulated_output_chars,
|
||||
limit,
|
||||
)
|
||||
{
|
||||
let _ = tx
|
||||
.send(RigStreamChunk::Failed {
|
||||
@ -317,7 +342,8 @@ async fn execute_agent_run(
|
||||
)) => {
|
||||
for part in &reasoning.content {
|
||||
if let rig::completion::message::ReasoningContent::Text {
|
||||
text, ..
|
||||
text,
|
||||
..
|
||||
} = part
|
||||
{
|
||||
accumulated_output_chars += text.chars().count();
|
||||
@ -334,7 +360,8 @@ async fn execute_agent_run(
|
||||
}
|
||||
Ok(rig::agent::MultiTurnStreamItem::StreamAssistantItem(
|
||||
rig::streaming::StreamedAssistantContent::ReasoningDelta {
|
||||
reasoning, ..
|
||||
reasoning,
|
||||
..
|
||||
},
|
||||
)) => {
|
||||
accumulated_output_chars += reasoning.chars().count();
|
||||
@ -363,7 +390,9 @@ async fn execute_agent_run(
|
||||
let tool_args: serde_json::Value =
|
||||
serde_json::from_str(&args).unwrap_or_default();
|
||||
|
||||
if let Ok(Some(decision)) = hooks.run_pre_tool_call(&tool_name, &tool_args).await {
|
||||
if let Ok(Some(decision)) =
|
||||
hooks.run_pre_tool_call(&tool_name, &tool_args).await
|
||||
{
|
||||
match decision {
|
||||
ToolGuardrailDecision::Allow => {}
|
||||
ToolGuardrailDecision::Block { reason } => {
|
||||
@ -390,7 +419,9 @@ async fn execute_agent_run(
|
||||
.send(RigStreamChunk::ToolCallFinished {
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
tool_name: tool_name.clone(),
|
||||
output: format!("awaiting approval: {message}"),
|
||||
output: format!(
|
||||
"awaiting approval: {message}"
|
||||
),
|
||||
error: None,
|
||||
})
|
||||
.await;
|
||||
@ -399,7 +430,9 @@ async fn execute_agent_run(
|
||||
name: tool_name.clone(),
|
||||
arguments: tool_args.clone(),
|
||||
output: None,
|
||||
error: Some(format!("requires approval: {message}")),
|
||||
error: Some(format!(
|
||||
"requires approval: {message}"
|
||||
)),
|
||||
elapsed_ms: None,
|
||||
});
|
||||
continue;
|
||||
@ -424,16 +457,22 @@ async fn execute_agent_run(
|
||||
});
|
||||
}
|
||||
Ok(rig::agent::MultiTurnStreamItem::StreamUserItem(
|
||||
rig::streaming::StreamedUserContent::ToolResult { tool_result, .. },
|
||||
rig::streaming::StreamedUserContent::ToolResult {
|
||||
tool_result,
|
||||
..
|
||||
},
|
||||
)) => {
|
||||
let content =
|
||||
super::helpers::tool_result_content_to_string(&tool_result.content);
|
||||
let content = super::helpers::tool_result_content_to_string(
|
||||
&tool_result.content,
|
||||
);
|
||||
accumulated_output_chars += content.chars().count();
|
||||
|
||||
if let Some(last) = current_step_tool_calls.last_mut()
|
||||
&& last.id == tool_result.id
|
||||
{
|
||||
last.output = Some(serde_json::from_str(&content).unwrap_or_default());
|
||||
last.output = Some(
|
||||
serde_json::from_str(&content).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
let tool_name = current_step_tool_calls
|
||||
@ -464,15 +503,21 @@ async fn execute_agent_run(
|
||||
Ok(rig::agent::MultiTurnStreamItem::FinalResponse(resp)) => {
|
||||
let usage = resp.usage();
|
||||
|
||||
if !current_step_tool_calls.is_empty() || !current_step_assistant.is_empty() {
|
||||
if !current_step_tool_calls.is_empty()
|
||||
|| !current_step_assistant.is_empty()
|
||||
{
|
||||
let reasoning = (!current_step_reasoning.is_empty())
|
||||
.then_some(std::mem::take(&mut current_step_reasoning));
|
||||
steps.push(AgentStep {
|
||||
index: steps.len(),
|
||||
assistant: (!current_step_assistant.is_empty())
|
||||
.then_some(std::mem::take(&mut current_step_assistant)),
|
||||
.then_some(std::mem::take(
|
||||
&mut current_step_assistant,
|
||||
)),
|
||||
reasoning_content: reasoning,
|
||||
tool_calls: std::mem::take(&mut current_step_tool_calls),
|
||||
tool_calls: std::mem::take(
|
||||
&mut current_step_tool_calls,
|
||||
),
|
||||
reflection: None,
|
||||
});
|
||||
}
|
||||
@ -533,7 +578,9 @@ async fn execute_agent_run(
|
||||
}
|
||||
}
|
||||
|
||||
Err(AiError::Response("agent stream ended without final response".to_string()))
|
||||
Err(AiError::Response(
|
||||
"agent stream ended without final response".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
impl Clone for HookChain {
|
||||
|
||||
@ -65,7 +65,10 @@ impl CompressionStrategy {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_custom_instructions(mut self, instructions: impl Into<String>) -> Self {
|
||||
pub fn with_custom_instructions(
|
||||
mut self,
|
||||
instructions: impl Into<String>,
|
||||
) -> Self {
|
||||
self.custom_instructions = Some(instructions.into());
|
||||
self
|
||||
}
|
||||
@ -91,7 +94,11 @@ pub struct CompactionResult {
|
||||
}
|
||||
|
||||
impl CompactionResult {
|
||||
pub fn new(summary: String, messages_compacted: usize, tokens_saved: i64) -> Self {
|
||||
pub fn new(
|
||||
summary: String,
|
||||
messages_compacted: usize,
|
||||
tokens_saved: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
summary,
|
||||
messages_compacted,
|
||||
@ -115,7 +122,12 @@ pub fn build_compression_prompt(
|
||||
existing_summary: Option<&str>,
|
||||
messages_text: &str,
|
||||
) -> String {
|
||||
build_compression_prompt_with_options(existing_summary, messages_text, None, 1500)
|
||||
build_compression_prompt_with_options(
|
||||
existing_summary,
|
||||
messages_text,
|
||||
None,
|
||||
1500,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the compaction prompt with custom instructions and word limit.
|
||||
|
||||
@ -132,7 +132,10 @@ impl AgentConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_completion_tokens(mut self, max_completion_tokens: Option<u64>) -> Self {
|
||||
pub fn with_max_completion_tokens(
|
||||
mut self,
|
||||
max_completion_tokens: Option<u64>,
|
||||
) -> Self {
|
||||
self.max_completion_tokens = max_completion_tokens;
|
||||
self
|
||||
}
|
||||
@ -142,19 +145,31 @@ impl AgentConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_toolset_policy(mut self, enabled: Vec<String>, disabled: Vec<String>) -> Self {
|
||||
pub fn with_toolset_policy(
|
||||
mut self,
|
||||
enabled: Vec<String>,
|
||||
disabled: Vec<String>,
|
||||
) -> Self {
|
||||
self.enabled_toolsets = enabled;
|
||||
self.disabled_toolsets = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tool_policy(mut self, allowed_tools: Vec<String>, denied_tools: Vec<String>) -> Self {
|
||||
pub fn with_tool_policy(
|
||||
mut self,
|
||||
allowed_tools: Vec<String>,
|
||||
denied_tools: Vec<String>,
|
||||
) -> Self {
|
||||
self.allowed_tools = allowed_tools;
|
||||
self.denied_tools = denied_tools;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_retry(mut self, max_attempts: usize, base_delay_ms: u64) -> Self {
|
||||
pub fn with_retry(
|
||||
mut self,
|
||||
max_attempts: usize,
|
||||
base_delay_ms: u64,
|
||||
) -> Self {
|
||||
self.retry_max_attempts = max_attempts;
|
||||
self.retry_base_delay_ms = base_delay_ms;
|
||||
self
|
||||
@ -165,7 +180,10 @@ impl AgentConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_fallback_model(mut self, fallback_model: impl Into<String>) -> Self {
|
||||
pub fn with_fallback_model(
|
||||
mut self,
|
||||
fallback_model: impl Into<String>,
|
||||
) -> Self {
|
||||
self.fallback_model = Some(fallback_model.into());
|
||||
self
|
||||
}
|
||||
|
||||
@ -44,7 +44,8 @@ impl RetryPolicy {
|
||||
let half = (ms as f64 * 0.25) as u64;
|
||||
let lo = ms.saturating_sub(half);
|
||||
let hi = ms.saturating_add(half);
|
||||
let mix = ((attempt as u64).wrapping_mul(1_103_515_245)) % (hi - lo + 1);
|
||||
let mix =
|
||||
((attempt as u64).wrapping_mul(1_103_515_245)) % (hi - lo + 1);
|
||||
lo + mix
|
||||
} else {
|
||||
ms
|
||||
@ -58,17 +59,26 @@ impl RetryPolicy {
|
||||
///
|
||||
/// Inspects both the HTTP status code (when available) and the error message
|
||||
/// content to determine the most appropriate category.
|
||||
pub fn classify_error(error: &AiError, http_status: Option<u16>) -> ErrorCategory {
|
||||
pub fn classify_error(
|
||||
error: &AiError,
|
||||
http_status: Option<u16>,
|
||||
) -> ErrorCategory {
|
||||
// HTTP status-based classification takes precedence
|
||||
let from_status = match http_status {
|
||||
Some(429) => Some(ErrorCategory::Retryable {
|
||||
reason: "rate limited (HTTP 429)".to_string(),
|
||||
}),
|
||||
Some(401) | Some(403) => Some(ErrorCategory::FallbackModel {
|
||||
reason: format!("authentication failed (HTTP {})", http_status.unwrap()),
|
||||
reason: format!(
|
||||
"authentication failed (HTTP {})",
|
||||
http_status.unwrap()
|
||||
),
|
||||
}),
|
||||
Some(502) | Some(503) => Some(ErrorCategory::Overloaded {
|
||||
reason: format!("provider unavailable (HTTP {})", http_status.unwrap()),
|
||||
reason: format!(
|
||||
"provider unavailable (HTTP {})",
|
||||
http_status.unwrap()
|
||||
),
|
||||
}),
|
||||
Some(504) => Some(ErrorCategory::Timeout),
|
||||
Some(413) => Some(ErrorCategory::ContextWindowExceeded {
|
||||
@ -90,7 +100,9 @@ pub fn classify_error(error: &AiError, http_status: Option<u16>) -> ErrorCategor
|
||||
// Message-based classification
|
||||
match error {
|
||||
AiError::Timeout { .. } => ErrorCategory::Timeout,
|
||||
AiError::TokenBudgetExceeded { .. } => ErrorCategory::TokenBudgetExceeded,
|
||||
AiError::TokenBudgetExceeded { .. } => {
|
||||
ErrorCategory::TokenBudgetExceeded
|
||||
}
|
||||
AiError::Api(msg) => classify_api_message(msg),
|
||||
AiError::Response(msg) => classify_response_message(msg),
|
||||
AiError::ModelRetriesExhausted { .. } => ErrorCategory::Fatal {
|
||||
@ -107,7 +119,10 @@ fn classify_api_message(msg: &str) -> ErrorCategory {
|
||||
let lower = msg.to_lowercase();
|
||||
|
||||
// Rate limiting
|
||||
if lower.contains("rate") || lower.contains("too many requests") || lower.contains("throttl") {
|
||||
if lower.contains("rate")
|
||||
|| lower.contains("too many requests")
|
||||
|| lower.contains("throttl")
|
||||
{
|
||||
return ErrorCategory::Retryable {
|
||||
reason: msg.to_string(),
|
||||
};
|
||||
@ -213,15 +228,15 @@ pub fn retry_policy_for(
|
||||
exponential: false,
|
||||
switch_to_fallback: false,
|
||||
},
|
||||
ErrorCategory::TokenBudgetExceeded | ErrorCategory::Cancelled | ErrorCategory::Fatal { .. } => {
|
||||
RetryPolicy {
|
||||
max_attempts: 0,
|
||||
base_delay: Duration::from_millis(0),
|
||||
jitter: false,
|
||||
exponential: false,
|
||||
switch_to_fallback: false,
|
||||
}
|
||||
}
|
||||
ErrorCategory::TokenBudgetExceeded
|
||||
| ErrorCategory::Cancelled
|
||||
| ErrorCategory::Fatal { .. } => RetryPolicy {
|
||||
max_attempts: 0,
|
||||
base_delay: Duration::from_millis(0),
|
||||
jitter: false,
|
||||
exponential: false,
|
||||
switch_to_fallback: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -140,7 +140,9 @@ impl EventSink {
|
||||
}
|
||||
|
||||
/// Subscribe to events, returns a receiver.
|
||||
pub fn subscribe(&mut self) -> tokio::sync::mpsc::UnboundedReceiver<AgentEvent> {
|
||||
pub fn subscribe(
|
||||
&mut self,
|
||||
) -> tokio::sync::mpsc::UnboundedReceiver<AgentEvent> {
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
self.senders.push(tx);
|
||||
rx
|
||||
|
||||
@ -69,7 +69,9 @@ where
|
||||
match f().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) if is_retryable(&e) && attempt + 1 < max_attempts => {
|
||||
let delay = Duration::from_millis(base_delay_ms * 2u64.pow(attempt as u32));
|
||||
let delay = Duration::from_millis(
|
||||
base_delay_ms * 2u64.pow(attempt as u32),
|
||||
);
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
attempt = attempt + 1,
|
||||
@ -94,12 +96,16 @@ where
|
||||
fn is_retryable(error: &AiError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
AiError::Api(_) | AiError::Response(_) | AiError::ModelRetriesExhausted { .. }
|
||||
AiError::Api(_)
|
||||
| AiError::Response(_)
|
||||
| AiError::ModelRetriesExhausted { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tool_result_content_to_string(
|
||||
content: &rig::one_or_many::OneOrMany<rig::completion::message::ToolResultContent>,
|
||||
content: &rig::one_or_many::OneOrMany<
|
||||
rig::completion::message::ToolResultContent,
|
||||
>,
|
||||
) -> String {
|
||||
use rig::completion::message::ToolResultContent;
|
||||
content
|
||||
|
||||
@ -51,11 +51,19 @@ pub trait AgentHook: Send + Sync {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_session_end(&self, _ctx: &AgentRunContext, _success: bool) -> AiResult<()> {
|
||||
async fn on_session_end(
|
||||
&self,
|
||||
_ctx: &AgentRunContext,
|
||||
_success: bool,
|
||||
) -> AiResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pre_llm_call(&self, _messages: &[HookMessage], _tools: &[HookToolDef]) -> AiResult<()> {
|
||||
async fn pre_llm_call(
|
||||
&self,
|
||||
_messages: &[HookMessage],
|
||||
_tools: &[HookToolDef],
|
||||
) -> AiResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -93,28 +101,42 @@ impl HookChain {
|
||||
self.hooks.is_empty()
|
||||
}
|
||||
|
||||
pub async fn run_session_start(&self, ctx: &AgentRunContext) -> AiResult<()> {
|
||||
pub async fn run_session_start(
|
||||
&self,
|
||||
ctx: &AgentRunContext,
|
||||
) -> AiResult<()> {
|
||||
for hook in &self.hooks {
|
||||
hook.on_session_start(ctx).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_session_end(&self, ctx: &AgentRunContext, success: bool) -> AiResult<()> {
|
||||
pub async fn run_session_end(
|
||||
&self,
|
||||
ctx: &AgentRunContext,
|
||||
success: bool,
|
||||
) -> AiResult<()> {
|
||||
for hook in &self.hooks {
|
||||
hook.on_session_end(ctx, success).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_pre_llm_call(&self, messages: &[HookMessage], tools: &[HookToolDef]) -> AiResult<()> {
|
||||
pub async fn run_pre_llm_call(
|
||||
&self,
|
||||
messages: &[HookMessage],
|
||||
tools: &[HookToolDef],
|
||||
) -> AiResult<()> {
|
||||
for hook in &self.hooks {
|
||||
hook.pre_llm_call(messages, tools).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_post_llm_call(&self, response: &HookLlmResponse) -> AiResult<()> {
|
||||
pub async fn run_post_llm_call(
|
||||
&self,
|
||||
response: &HookLlmResponse,
|
||||
) -> AiResult<()> {
|
||||
for hook in &self.hooks {
|
||||
hook.post_llm_call(response).await?;
|
||||
}
|
||||
@ -127,7 +149,9 @@ impl HookChain {
|
||||
arguments: &Value,
|
||||
) -> AiResult<Option<ToolGuardrailDecision>> {
|
||||
for hook in &self.hooks {
|
||||
if let Some(decision) = hook.pre_tool_call(tool_name, arguments).await? {
|
||||
if let Some(decision) =
|
||||
hook.pre_tool_call(tool_name, arguments).await?
|
||||
{
|
||||
if !matches!(decision, ToolGuardrailDecision::Allow) {
|
||||
return Ok(Some(decision));
|
||||
}
|
||||
@ -136,7 +160,10 @@ impl HookChain {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn run_post_tool_call(&self, outcome: &ToolCallOutcome) -> AiResult<()> {
|
||||
pub async fn run_post_tool_call(
|
||||
&self,
|
||||
outcome: &ToolCallOutcome,
|
||||
) -> AiResult<()> {
|
||||
for hook in &self.hooks {
|
||||
hook.post_tool_call(outcome).await?;
|
||||
}
|
||||
|
||||
@ -11,16 +11,19 @@ use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::RigStreamChunk;
|
||||
use super::config::AgentConfig;
|
||||
use super::error_classifier::{
|
||||
classify_error, retry_policy_for, should_switch_to_fallback,
|
||||
};
|
||||
use super::events::{AgentEvent, EventSink};
|
||||
use super::helpers::{build_input_string, estimate_tokens};
|
||||
use super::hooks::{HookChain, HookLlmResponse, HookMessage, ToolCallOutcome, ToolGuardrailDecision};
|
||||
use super::hooks::{
|
||||
HookChain, HookLlmResponse, HookMessage, ToolCallOutcome,
|
||||
ToolGuardrailDecision,
|
||||
};
|
||||
use super::iteration_budget::IterationBudget;
|
||||
use super::request::{AgentRequest, AgentResult, AgentStep, ToolCallRecord};
|
||||
use super::RigStreamChunk;
|
||||
use crate::client::AiClient;
|
||||
use crate::error::{AiError, AiResult};
|
||||
|
||||
@ -50,13 +53,13 @@ pub type FollowUpFn = Arc<
|
||||
>;
|
||||
|
||||
/// Callback to decide whether the agent should stop after a turn.
|
||||
pub type ShouldStopFn = Arc<
|
||||
dyn Fn(&TurnContext) -> bool + Send + Sync,
|
||||
>;
|
||||
pub type ShouldStopFn = Arc<dyn Fn(&TurnContext) -> bool + Send + Sync>;
|
||||
|
||||
/// Callback to prepare/modify state before the next turn.
|
||||
pub type PrepareNextTurnFn = Arc<
|
||||
dyn Fn(&TurnContext) -> Pin<Box<dyn Future<Output = Option<TurnUpdate>> + Send>>
|
||||
dyn Fn(
|
||||
&TurnContext,
|
||||
) -> Pin<Box<dyn Future<Output = Option<TurnUpdate>> + Send>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
@ -144,7 +147,10 @@ pub struct EnhancedAgent {
|
||||
}
|
||||
|
||||
impl EnhancedAgent {
|
||||
pub fn new(client: AiClient, loop_config: AgentLoopConfig) -> AiResult<Self> {
|
||||
pub fn new(
|
||||
client: AiClient,
|
||||
loop_config: AgentLoopConfig,
|
||||
) -> AiResult<Self> {
|
||||
loop_config.config.validate()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
@ -270,7 +276,11 @@ async fn run_enhanced_loop(
|
||||
loop {
|
||||
// Check cancellation
|
||||
if cancellation.as_ref().is_some_and(|ct| ct.is_cancelled()) {
|
||||
let _ = tx.send(RigStreamChunk::Failed { error: "cancelled".to_string() }).await;
|
||||
let _ = tx
|
||||
.send(RigStreamChunk::Failed {
|
||||
error: "cancelled".to_string(),
|
||||
})
|
||||
.await;
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::ErrorClassified {
|
||||
category: "cancelled".to_string(),
|
||||
@ -279,7 +289,9 @@ async fn run_enhanced_loop(
|
||||
retry_delay_ms: None,
|
||||
});
|
||||
}
|
||||
return Err(AiError::Response("agent run cancelled".to_string()));
|
||||
return Err(AiError::Response(
|
||||
"agent run cancelled".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Inject steering messages if any
|
||||
@ -298,10 +310,12 @@ async fn run_enhanced_loop(
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::TurnStart { turn_index });
|
||||
}
|
||||
let _ = tx.send(RigStreamChunk::TextDelta {
|
||||
index: 0,
|
||||
content: String::new(), // placeholder for turn boundary detection
|
||||
}).await;
|
||||
let _ = tx
|
||||
.send(RigStreamChunk::TextDelta {
|
||||
index: 0,
|
||||
content: String::new(), // placeholder for turn boundary detection
|
||||
})
|
||||
.await;
|
||||
|
||||
// Run one LLM turn with retry
|
||||
let turn_result = run_single_turn(
|
||||
@ -325,7 +339,9 @@ async fn run_enhanced_loop(
|
||||
|
||||
// Collect step
|
||||
let tool_call_count = turn_output.tool_calls.len();
|
||||
if !turn_output.tool_calls.is_empty() || !turn_output.assistant_text.is_empty() {
|
||||
if !turn_output.tool_calls.is_empty()
|
||||
|| !turn_output.assistant_text.is_empty()
|
||||
{
|
||||
all_steps.push(AgentStep {
|
||||
index: all_steps.len(),
|
||||
assistant: (!turn_output.assistant_text.is_empty())
|
||||
@ -340,7 +356,9 @@ async fn run_enhanced_loop(
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::TurnEnd {
|
||||
turn_index,
|
||||
assistant_text: Some(turn_output.assistant_text.clone()),
|
||||
assistant_text: Some(
|
||||
turn_output.assistant_text.clone(),
|
||||
),
|
||||
tool_call_count,
|
||||
});
|
||||
}
|
||||
@ -357,7 +375,10 @@ async fn run_enhanced_loop(
|
||||
|
||||
if let Some(stop_fn) = &should_stop {
|
||||
if stop_fn(&turn_ctx) {
|
||||
info!(turn_index, "agent stopped by should_stop callback");
|
||||
info!(
|
||||
turn_index,
|
||||
"agent stopped by should_stop callback"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -378,7 +399,8 @@ async fn run_enhanced_loop(
|
||||
if let Some(temp) = update.temperature {
|
||||
config.temperature = Some(temp);
|
||||
}
|
||||
if let Some(max_tok) = update.max_completion_tokens {
|
||||
if let Some(max_tok) = update.max_completion_tokens
|
||||
{
|
||||
config.max_completion_tokens = Some(max_tok);
|
||||
}
|
||||
}
|
||||
@ -397,14 +419,21 @@ async fn run_enhanced_loop(
|
||||
Err(e) => {
|
||||
// Error classification and retry with fallback
|
||||
let category = classify_error(&e, None);
|
||||
let policy = retry_policy_for(&category, config.retry_max_attempts, config.retry_base_delay_ms);
|
||||
let policy = retry_policy_for(
|
||||
&category,
|
||||
config.retry_max_attempts,
|
||||
config.retry_base_delay_ms,
|
||||
);
|
||||
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::ErrorClassified {
|
||||
category: format!("{category:?}"),
|
||||
message: e.to_string(),
|
||||
will_retry: policy.switch_to_fallback || policy.max_attempts > 0,
|
||||
retry_delay_ms: Some(policy.base_delay.as_millis() as u64),
|
||||
will_retry: policy.switch_to_fallback
|
||||
|| policy.max_attempts > 0,
|
||||
retry_delay_ms: Some(
|
||||
policy.base_delay.as_millis() as u64
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@ -443,16 +472,20 @@ async fn run_enhanced_loop(
|
||||
|
||||
match retry_result {
|
||||
Ok(turn_output) => {
|
||||
total_input_tokens += turn_output.input_tokens;
|
||||
total_output_tokens += turn_output.output_tokens;
|
||||
total_input_tokens +=
|
||||
turn_output.input_tokens;
|
||||
total_output_tokens +=
|
||||
turn_output.output_tokens;
|
||||
let tc_count = turn_output.tool_calls.len();
|
||||
let has_tools = tc_count > 0;
|
||||
let has_text = !turn_output.assistant_text.is_empty();
|
||||
let has_text =
|
||||
!turn_output.assistant_text.is_empty();
|
||||
let assistant = turn_output.assistant_text;
|
||||
if has_tools || has_text {
|
||||
all_steps.push(AgentStep {
|
||||
index: all_steps.len(),
|
||||
assistant: has_text.then_some(assistant.clone()),
|
||||
assistant: has_text
|
||||
.then_some(assistant.clone()),
|
||||
reasoning_content: None,
|
||||
tool_calls: turn_output.tool_calls,
|
||||
reflection: None,
|
||||
@ -472,7 +505,9 @@ async fn run_enhanced_loop(
|
||||
})
|
||||
.await;
|
||||
if let Some(ctx) = &request.run_context {
|
||||
let _ = hooks.run_session_end(ctx, false).await;
|
||||
let _ = hooks
|
||||
.run_session_end(ctx, false)
|
||||
.await;
|
||||
}
|
||||
return Err(retry_err);
|
||||
}
|
||||
@ -582,7 +617,9 @@ async fn run_single_turn(
|
||||
tx: &mpsc::Sender<RigStreamChunk>,
|
||||
) -> AiResult<TurnOutput> {
|
||||
if !budget.consume() {
|
||||
return Err(AiError::Response("iteration budget exhausted".to_string()));
|
||||
return Err(AiError::Response(
|
||||
"iteration budget exhausted".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let model = client.completion_model(&config.model);
|
||||
@ -674,7 +711,11 @@ async fn run_single_turn(
|
||||
rig::streaming::StreamedAssistantContent::Reasoning(reasoning),
|
||||
)) => {
|
||||
for part in &reasoning.content {
|
||||
if let rig::completion::message::ReasoningContent::Text { text, .. } = part {
|
||||
if let rig::completion::message::ReasoningContent::Text {
|
||||
text,
|
||||
..
|
||||
} = part
|
||||
{
|
||||
_accumulated_output_chars += text.chars().count();
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::MessageThinkingDelta {
|
||||
@ -693,7 +734,10 @@ async fn run_single_turn(
|
||||
}
|
||||
}
|
||||
Ok(rig::agent::MultiTurnStreamItem::StreamAssistantItem(
|
||||
rig::streaming::StreamedAssistantContent::ReasoningDelta { reasoning, .. },
|
||||
rig::streaming::StreamedAssistantContent::ReasoningDelta {
|
||||
reasoning,
|
||||
..
|
||||
},
|
||||
)) => {
|
||||
_accumulated_output_chars += reasoning.chars().count();
|
||||
if let Some(sink) = &event_sink {
|
||||
@ -711,7 +755,10 @@ async fn run_single_turn(
|
||||
delta_index += 1;
|
||||
}
|
||||
Ok(rig::agent::MultiTurnStreamItem::StreamAssistantItem(
|
||||
rig::streaming::StreamedAssistantContent::ToolCall { tool_call, .. },
|
||||
rig::streaming::StreamedAssistantContent::ToolCall {
|
||||
tool_call,
|
||||
..
|
||||
},
|
||||
)) => {
|
||||
let args = match &tool_call.function.arguments {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
@ -724,7 +771,9 @@ async fn run_single_turn(
|
||||
serde_json::from_str(&args).unwrap_or_default();
|
||||
|
||||
// Pre-tool-call guardrail hook
|
||||
if let Ok(Some(decision)) = hooks.run_pre_tool_call(&tool_name, &tool_args).await {
|
||||
if let Ok(Some(decision)) =
|
||||
hooks.run_pre_tool_call(&tool_name, &tool_args).await
|
||||
{
|
||||
match decision {
|
||||
ToolGuardrailDecision::Allow => {}
|
||||
ToolGuardrailDecision::Block { reason } => {
|
||||
@ -761,7 +810,9 @@ async fn run_single_turn(
|
||||
name: tool_name.clone(),
|
||||
arguments: tool_args,
|
||||
output: None,
|
||||
error: Some(format!("requires approval: {message}")),
|
||||
error: Some(format!(
|
||||
"requires approval: {message}"
|
||||
)),
|
||||
elapsed_ms: None,
|
||||
});
|
||||
continue;
|
||||
@ -794,10 +845,14 @@ async fn run_single_turn(
|
||||
});
|
||||
}
|
||||
Ok(rig::agent::MultiTurnStreamItem::StreamUserItem(
|
||||
rig::streaming::StreamedUserContent::ToolResult { tool_result, .. },
|
||||
rig::streaming::StreamedUserContent::ToolResult {
|
||||
tool_result,
|
||||
..
|
||||
},
|
||||
)) => {
|
||||
let content =
|
||||
super::helpers::tool_result_content_to_string(&tool_result.content);
|
||||
let content = super::helpers::tool_result_content_to_string(
|
||||
&tool_result.content,
|
||||
);
|
||||
_accumulated_output_chars += content.chars().count();
|
||||
|
||||
let tool_name = tool_calls
|
||||
@ -808,14 +863,18 @@ async fn run_single_turn(
|
||||
if let Some(last) = tool_calls.last_mut()
|
||||
&& last.id == tool_result.id
|
||||
{
|
||||
last.output = Some(serde_json::from_str(&content).unwrap_or_default());
|
||||
last.output = Some(
|
||||
serde_json::from_str(&content).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(sink) = &event_sink {
|
||||
sink.emit(AgentEvent::ToolExecutionEnd {
|
||||
tool_call_id: tool_result.id.clone(),
|
||||
tool_name: tool_name.clone(),
|
||||
output: Some(serde_json::Value::String(content.clone())),
|
||||
output: Some(serde_json::Value::String(
|
||||
content.clone(),
|
||||
)),
|
||||
error: None,
|
||||
elapsed_ms: 0,
|
||||
});
|
||||
@ -872,5 +931,3 @@ async fn run_single_turn(
|
||||
output_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -37,13 +37,12 @@ impl RigAgent {
|
||||
}
|
||||
let agent = builder.build();
|
||||
|
||||
let response = agent
|
||||
.prompt(&ui)
|
||||
.extended_details()
|
||||
.await
|
||||
.map_err(|e: rig::completion::PromptError| {
|
||||
AiError::Api(e.to_string())
|
||||
})?;
|
||||
let response =
|
||||
agent.prompt(&ui).extended_details().await.map_err(
|
||||
|e: rig::completion::PromptError| {
|
||||
AiError::Api(e.to_string())
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
response.output,
|
||||
|
||||
@ -63,8 +63,13 @@ impl SystemPromptBuilder {
|
||||
}
|
||||
|
||||
/// Add a one-line tool description snippet.
|
||||
pub fn tool_snippet(mut self, tool_name: impl Into<String>, description: impl Into<String>) -> Self {
|
||||
self.tool_snippets.push((tool_name.into(), description.into()));
|
||||
pub fn tool_snippet(
|
||||
mut self,
|
||||
tool_name: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
) -> Self {
|
||||
self.tool_snippets
|
||||
.push((tool_name.into(), description.into()));
|
||||
self
|
||||
}
|
||||
|
||||
@ -75,7 +80,11 @@ impl SystemPromptBuilder {
|
||||
}
|
||||
|
||||
/// Add a project context file (e.g., AGENTS.md content).
|
||||
pub fn project_context(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
pub fn project_context(
|
||||
mut self,
|
||||
path: impl Into<String>,
|
||||
content: impl Into<String>,
|
||||
) -> Self {
|
||||
self.project_contexts.push((path.into(), content.into()));
|
||||
self
|
||||
}
|
||||
@ -87,13 +96,20 @@ impl SystemPromptBuilder {
|
||||
}
|
||||
|
||||
/// Set a variable for {{key}} substitution.
|
||||
pub fn variable(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
pub fn variable(
|
||||
mut self,
|
||||
key: impl Into<String>,
|
||||
value: impl Into<String>,
|
||||
) -> Self {
|
||||
self.variables.insert(key.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple variables from an iterator.
|
||||
pub fn variables(mut self, vars: impl IntoIterator<Item = (String, String)>) -> Self {
|
||||
pub fn variables(
|
||||
mut self,
|
||||
vars: impl IntoIterator<Item = (String, String)>,
|
||||
) -> Self {
|
||||
self.variables.extend(vars);
|
||||
self
|
||||
}
|
||||
@ -105,7 +121,11 @@ impl SystemPromptBuilder {
|
||||
}
|
||||
|
||||
/// Add a custom named section to the prompt.
|
||||
pub fn custom_section(mut self, name: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
pub fn custom_section(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
content: impl Into<String>,
|
||||
) -> Self {
|
||||
self.custom_sections.push((name.into(), content.into()));
|
||||
self
|
||||
}
|
||||
@ -142,7 +162,8 @@ impl SystemPromptBuilder {
|
||||
// 4. Project context files
|
||||
if !self.project_contexts.is_empty() {
|
||||
let mut section = String::from("\n<project_context>\n\n");
|
||||
section.push_str("Project-specific instructions and guidelines:\n\n");
|
||||
section
|
||||
.push_str("Project-specific instructions and guidelines:\n\n");
|
||||
for (path, content) in &self.project_contexts {
|
||||
section.push_str(&format!("<project_instructions path=\"{path}\">\n{content}\n</project_instructions>\n\n"));
|
||||
}
|
||||
|
||||
@ -38,7 +38,9 @@ impl AgentRequest {
|
||||
|
||||
pub fn validate(&self) -> AiResult<()> {
|
||||
if self.input.trim().is_empty() {
|
||||
return Err(AiError::Config("agent request input is required".to_string()));
|
||||
return Err(AiError::Config(
|
||||
"agent request input is required".to_string(),
|
||||
));
|
||||
}
|
||||
if self.input.len() > 1_000_000 {
|
||||
return Err(AiError::Config(
|
||||
@ -83,12 +85,18 @@ impl AgentRequest {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_prefill_messages(mut self, prefill_messages: Vec<rig::completion::Message>) -> Self {
|
||||
pub fn with_prefill_messages(
|
||||
mut self,
|
||||
prefill_messages: Vec<rig::completion::Message>,
|
||||
) -> Self {
|
||||
self.prefill_messages = prefill_messages;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cancellation_token(mut self, cancellation_token: CancellationToken) -> Self {
|
||||
pub fn with_cancellation_token(
|
||||
mut self,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Self {
|
||||
self.cancellation_token = Some(cancellation_token);
|
||||
self
|
||||
}
|
||||
@ -119,7 +127,11 @@ pub struct AgentExpert {
|
||||
}
|
||||
|
||||
impl AgentExpert {
|
||||
pub fn new(id: impl Into<String>, role: impl Into<String>, task: impl Into<String>) -> Self {
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
role: impl Into<String>,
|
||||
task: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
role: role.into(),
|
||||
@ -131,7 +143,10 @@ impl AgentExpert {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
|
||||
pub fn with_system_prompt(
|
||||
mut self,
|
||||
system_prompt: impl Into<String>,
|
||||
) -> Self {
|
||||
self.system_prompt = Some(system_prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
@ -145,7 +145,10 @@ impl SessionEntry {
|
||||
}
|
||||
|
||||
/// Create a user message entry.
|
||||
pub fn user_message(parent_id: Option<Uuid>, content: impl Into<String>) -> Self {
|
||||
pub fn user_message(
|
||||
parent_id: Option<Uuid>,
|
||||
content: impl Into<String>,
|
||||
) -> Self {
|
||||
Self::Message {
|
||||
id: Uuid::new_v4(),
|
||||
parent_id,
|
||||
@ -328,7 +331,13 @@ impl Session {
|
||||
pub fn active_messages(&self) -> Vec<&SessionEntry> {
|
||||
self.active_branch()
|
||||
.into_iter()
|
||||
.filter(|e| matches!(e, SessionEntry::Message { .. } | SessionEntry::Compaction { .. }))
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e,
|
||||
SessionEntry::Message { .. }
|
||||
| SessionEntry::Compaction { .. }
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -342,11 +351,8 @@ impl Session {
|
||||
|
||||
/// Get all leaf entries (entries with no children).
|
||||
pub fn leaves(&self) -> Vec<&SessionEntry> {
|
||||
let parent_ids: std::collections::HashSet<Uuid> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|e| e.parent_id())
|
||||
.collect();
|
||||
let parent_ids: std::collections::HashSet<Uuid> =
|
||||
self.entries.iter().filter_map(|e| e.parent_id()).collect();
|
||||
|
||||
self.entries
|
||||
.iter()
|
||||
@ -367,7 +373,9 @@ impl Session {
|
||||
.iter()
|
||||
.position(|e| e.id() == fork_entry_id)
|
||||
.ok_or_else(|| {
|
||||
AiError::Config(format!("fork entry {fork_entry_id} not found in session"))
|
||||
AiError::Config(format!(
|
||||
"fork entry {fork_entry_id} not found in session"
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut new_session = Session::new();
|
||||
@ -445,9 +453,11 @@ fn iso_now() -> String {
|
||||
// Simple ISO 8601 format (UTC)
|
||||
let days = secs / 86400;
|
||||
let years = (days * 400) / 146097;
|
||||
let remaining_days = days - (years * 365 + years / 4 - years / 100 + years / 400);
|
||||
let remaining_days =
|
||||
days - (years * 365 + years / 4 - years / 100 + years / 400);
|
||||
let month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let is_leap = (years % 4 == 0 && years % 100 != 0) || years % 400 == 0;
|
||||
let is_leap =
|
||||
(years % 4 == 0 && years % 100 != 0) || years % 400 == 0;
|
||||
let mut month = 0usize;
|
||||
let mut day_acc = remaining_days as i64;
|
||||
for (i, &md) in month_days.iter().enumerate() {
|
||||
@ -487,7 +497,8 @@ mod tests {
|
||||
let msg1_id = msg1.id();
|
||||
session.push(msg1);
|
||||
|
||||
let msg2 = SessionEntry::assistant_message(Some(msg1_id), "Hi there!", None);
|
||||
let msg2 =
|
||||
SessionEntry::assistant_message(Some(msg1_id), "Hi there!", None);
|
||||
session.push(msg2);
|
||||
|
||||
assert_eq!(session.entry_count(), 2);
|
||||
@ -502,7 +513,8 @@ mod tests {
|
||||
let msg1_id = msg1.id();
|
||||
session.push(msg1);
|
||||
|
||||
let msg2 = SessionEntry::assistant_message(Some(msg1_id), "Reply 1", None);
|
||||
let msg2 =
|
||||
SessionEntry::assistant_message(Some(msg1_id), "Reply 1", None);
|
||||
let msg2_id = msg2.id();
|
||||
session.push(msg2);
|
||||
|
||||
@ -524,8 +536,10 @@ mod tests {
|
||||
session.push(msg1);
|
||||
|
||||
// Two children branching from root
|
||||
let msg2a = SessionEntry::assistant_message(Some(msg1_id), "Branch A", None);
|
||||
let msg2b = SessionEntry::assistant_message(Some(msg1_id), "Branch B", None);
|
||||
let msg2a =
|
||||
SessionEntry::assistant_message(Some(msg1_id), "Branch A", None);
|
||||
let msg2b =
|
||||
SessionEntry::assistant_message(Some(msg1_id), "Branch B", None);
|
||||
session.push(msg2a);
|
||||
session.push(msg2b);
|
||||
|
||||
|
||||
@ -31,13 +31,24 @@ pub async fn run_experts(
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(subagent_id = %expert.id, role = %expert.role, error = %error, "subagent failed");
|
||||
let _ = publish_subagent_failed(realtime, run, expert, &error.to_string()).await;
|
||||
let _ = publish_subagent_failed(
|
||||
realtime,
|
||||
run,
|
||||
expert,
|
||||
&error.to_string(),
|
||||
)
|
||||
.await;
|
||||
failed_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!(total = experts.len(), ok = outputs.len(), failed = failed_count, "experts done");
|
||||
debug!(
|
||||
total = experts.len(),
|
||||
ok = outputs.len(),
|
||||
failed = failed_count,
|
||||
"experts done"
|
||||
);
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
@ -53,7 +64,9 @@ async fn run_single(
|
||||
let rig_client = client.llm_client().clone();
|
||||
let model_name = config.model.clone();
|
||||
let temperature = expert.temperature.or(config.temperature);
|
||||
let max_completion_tokens = expert.max_completion_tokens.or(config.max_completion_tokens);
|
||||
let max_completion_tokens = expert
|
||||
.max_completion_tokens
|
||||
.or(config.max_completion_tokens);
|
||||
let retry_attempts = config.retry_max_attempts;
|
||||
let retry_delay_ms = config.retry_base_delay_ms;
|
||||
|
||||
@ -66,10 +79,8 @@ async fn run_single(
|
||||
|
||||
let task = build_expert_task(expert);
|
||||
|
||||
let (output, input_tokens_usage, output_tokens_usage) = with_retry(
|
||||
retry_attempts,
|
||||
retry_delay_ms,
|
||||
|| {
|
||||
let (output, input_tokens_usage, output_tokens_usage) =
|
||||
with_retry(retry_attempts, retry_delay_ms, || {
|
||||
let rig_client = rig_client.clone();
|
||||
let model_name = model_name.clone();
|
||||
let prompt = prompt.clone();
|
||||
@ -85,13 +96,12 @@ async fn run_single(
|
||||
}
|
||||
let agent = builder.build();
|
||||
|
||||
let response = agent
|
||||
.prompt(&task)
|
||||
.extended_details()
|
||||
.await
|
||||
.map_err(|e: rig::completion::PromptError| {
|
||||
AiError::Api(e.to_string())
|
||||
})?;
|
||||
let response =
|
||||
agent.prompt(&task).extended_details().await.map_err(
|
||||
|e: rig::completion::PromptError| {
|
||||
AiError::Api(e.to_string())
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
response.output,
|
||||
@ -99,9 +109,8 @@ async fn run_single(
|
||||
response.usage.output_tokens,
|
||||
))
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
let input_tokens = input_tokens_usage as i64;
|
||||
let output_tokens = if output_tokens_usage > 0 {
|
||||
@ -150,17 +159,19 @@ async fn publish_subagent_started(
|
||||
config: &AgentConfig,
|
||||
expert: &AgentExpert,
|
||||
) -> AiResult<()> {
|
||||
AgentRuntime::default().publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentStarted {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: expert.id.clone(),
|
||||
role: expert.role.clone(),
|
||||
task: expert.task.clone(),
|
||||
model: config.model.clone(),
|
||||
},
|
||||
).await
|
||||
AgentRuntime::default()
|
||||
.publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentStarted {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: expert.id.clone(),
|
||||
role: expert.role.clone(),
|
||||
task: expert.task.clone(),
|
||||
model: config.model.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn publish_subagent_completed(
|
||||
@ -169,20 +180,22 @@ async fn publish_subagent_completed(
|
||||
config: &AgentConfig,
|
||||
output: &AgentExpertOutput,
|
||||
) -> AiResult<()> {
|
||||
AgentRuntime::default().publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentCompleted {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: output.id.clone(),
|
||||
role: output.role.clone(),
|
||||
task: output.task.clone(),
|
||||
output: output.output.clone(),
|
||||
input_tokens: output.input_tokens,
|
||||
output_tokens: output.output_tokens,
|
||||
model: config.model.clone(),
|
||||
},
|
||||
).await
|
||||
AgentRuntime::default()
|
||||
.publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentCompleted {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: output.id.clone(),
|
||||
role: output.role.clone(),
|
||||
task: output.task.clone(),
|
||||
output: output.output.clone(),
|
||||
input_tokens: output.input_tokens,
|
||||
output_tokens: output.output_tokens,
|
||||
model: config.model.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn publish_subagent_failed(
|
||||
@ -191,13 +204,15 @@ async fn publish_subagent_failed(
|
||||
expert: &AgentExpert,
|
||||
error: &str,
|
||||
) -> AiResult<()> {
|
||||
AgentRuntime::default().publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentFailed {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: expert.id.clone(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
).await
|
||||
AgentRuntime::default()
|
||||
.publish(
|
||||
realtime,
|
||||
&AgentStreamEvent::SubagentFailed {
|
||||
conversation_id: run.conversation_id,
|
||||
message_id: run.message_id,
|
||||
subagent_id: expert.id.clone(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -23,7 +23,10 @@ impl<C> RigTool<C>
|
||||
where
|
||||
C: Clone + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(tool: Arc<dyn FunctionCall<Context = C>>, context: Arc<Mutex<C>>) -> Self {
|
||||
pub fn new(
|
||||
tool: Arc<dyn FunctionCall<Context = C>>,
|
||||
context: Arc<Mutex<C>>,
|
||||
) -> Self {
|
||||
let name = tool.name().to_string();
|
||||
let description = tool.description().to_string();
|
||||
let schema = tool.schema();
|
||||
@ -49,7 +52,8 @@ where
|
||||
fn definition<'a>(
|
||||
&'a self,
|
||||
_prompt: String,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = RigToolDefinition> + Send + 'a>> {
|
||||
) -> Pin<Box<dyn std::future::Future<Output = RigToolDefinition> + Send + 'a>>
|
||||
{
|
||||
let name = self.name.clone();
|
||||
let description = self.description.clone();
|
||||
let params = self.schema.clone();
|
||||
@ -67,23 +71,28 @@ where
|
||||
&'a self,
|
||||
args: String,
|
||||
) -> Pin<
|
||||
Box<dyn std::future::Future<Output = Result<String, rig::tool::ToolError>> + Send + 'a>,
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = Result<String, rig::tool::ToolError>,
|
||||
> + Send
|
||||
+ 'a,
|
||||
>,
|
||||
> {
|
||||
let tool = self.tool.clone();
|
||||
let context = self.context.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let args_value: Value =
|
||||
serde_json::from_str(&args).map_err(rig::tool::ToolError::JsonError)?;
|
||||
let args_value: Value = serde_json::from_str(&args)
|
||||
.map_err(rig::tool::ToolError::JsonError)?;
|
||||
|
||||
let mut ctx = context.lock().await;
|
||||
|
||||
match tool.call(&mut *ctx, args_value).await {
|
||||
Ok(value) => serde_json::to_string(&value)
|
||||
.map_err(rig::tool::ToolError::JsonError),
|
||||
Err(ai_err) => Err(rig::tool::ToolError::ToolCallError(Box::new(
|
||||
std::io::Error::other(ai_err.to_string()),
|
||||
))),
|
||||
Err(ai_err) => Err(rig::tool::ToolError::ToolCallError(
|
||||
Box::new(std::io::Error::other(ai_err.to_string())),
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -112,10 +121,14 @@ where
|
||||
register: &crate::tool::register::ToolRegister<C>,
|
||||
context: Arc<Mutex<C>>,
|
||||
) -> Self {
|
||||
let mut tools: Vec<Box<dyn ToolDyn + 'static>> = Vec::with_capacity(register.len());
|
||||
let mut tools: Vec<Box<dyn ToolDyn + 'static>> =
|
||||
Vec::with_capacity(register.len());
|
||||
|
||||
for tool_arc in ®ister.tools {
|
||||
tools.push(Box::new(RigTool::new(tool_arc.clone(), context.clone())));
|
||||
tools.push(Box::new(RigTool::new(
|
||||
tool_arc.clone(),
|
||||
context.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
Self {
|
||||
|
||||
@ -24,7 +24,10 @@ pub struct EndpointConfig {
|
||||
}
|
||||
|
||||
impl EndpointConfig {
|
||||
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> AiResult<Self> {
|
||||
pub fn new(
|
||||
base_url: impl Into<String>,
|
||||
api_key: impl Into<String>,
|
||||
) -> AiResult<Self> {
|
||||
let config = Self {
|
||||
base_url: base_url.into(),
|
||||
api_key: api_key.into(),
|
||||
@ -51,7 +54,11 @@ impl EndpointConfig {
|
||||
.api_key(&self.api_key)
|
||||
.base_url(self.base_url.trim())
|
||||
.build()
|
||||
.map_err(|e| AiError::Config(format!("failed to build rig OpenAI client: {e}")))
|
||||
.map_err(|e| {
|
||||
AiError::Config(format!(
|
||||
"failed to build rig OpenAI client: {e}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use rig::client::EmbeddingsClient;
|
||||
use rig::embeddings::EmbeddingModel;
|
||||
|
||||
use crate::{client::AiClient, error::{AiError, AiResult}};
|
||||
use crate::{
|
||||
client::AiClient,
|
||||
error::{AiError, AiResult},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EmbedClient {
|
||||
@ -23,23 +26,32 @@ impl EmbedClient {
|
||||
|
||||
pub async fn embed_text(&self, text: String) -> AiResult<Vec<f32>> {
|
||||
let model = self.embedding_model();
|
||||
let mut embeddings = model.embed_texts(vec![text])
|
||||
let mut embeddings = model
|
||||
.embed_texts(vec![text])
|
||||
.await
|
||||
.map_err(|e| AiError::Api(e.to_string()))?;
|
||||
embeddings.pop()
|
||||
embeddings
|
||||
.pop()
|
||||
.map(|e| e.vec.into_iter().map(|v| v as f32).collect())
|
||||
.ok_or_else(|| AiError::Response("no embedding returned".to_string()))
|
||||
.ok_or_else(|| {
|
||||
AiError::Response("no embedding returned".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn embed_texts(&self, texts: Vec<String>) -> AiResult<Vec<Vec<f32>>> {
|
||||
pub async fn embed_texts(
|
||||
&self,
|
||||
texts: Vec<String>,
|
||||
) -> AiResult<Vec<Vec<f32>>> {
|
||||
if texts.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let model = self.embedding_model();
|
||||
let embeddings = model.embed_texts(texts)
|
||||
let embeddings = model
|
||||
.embed_texts(texts)
|
||||
.await
|
||||
.map_err(|e| AiError::Api(e.to_string()))?;
|
||||
Ok(embeddings.into_iter()
|
||||
Ok(embeddings
|
||||
.into_iter()
|
||||
.map(|e| e.vec.into_iter().map(|v| v as f32).collect())
|
||||
.collect())
|
||||
}
|
||||
@ -55,11 +67,15 @@ impl EmbedClient {
|
||||
let mut embeddings: Vec<Vec<f32>> = Vec::with_capacity(texts.len());
|
||||
for chunk in texts.chunks(batch_size) {
|
||||
let model = self.embedding_model();
|
||||
let chunk_embeddings = model.embed_texts(chunk.to_vec())
|
||||
let chunk_embeddings = model
|
||||
.embed_texts(chunk.to_vec())
|
||||
.await
|
||||
.map_err(|e| AiError::Api(e.to_string()))?;
|
||||
embeddings.extend(chunk_embeddings.into_iter()
|
||||
.map(|e| e.vec.into_iter().map(|v| v as f32).collect()));
|
||||
embeddings.extend(
|
||||
chunk_embeddings
|
||||
.into_iter()
|
||||
.map(|e| e.vec.into_iter().map(|v| v as f32).collect()),
|
||||
);
|
||||
}
|
||||
Ok(embeddings)
|
||||
}
|
||||
|
||||
@ -24,10 +24,7 @@ pub enum AiError {
|
||||
Response(String),
|
||||
|
||||
#[error("model retries exhausted after {attempts} attempts: {last_error}")]
|
||||
ModelRetriesExhausted {
|
||||
attempts: usize,
|
||||
last_error: String,
|
||||
},
|
||||
ModelRetriesExhausted { attempts: usize, last_error: String },
|
||||
|
||||
#[error("agent timeout after {seconds}s")]
|
||||
Timeout { seconds: u64 },
|
||||
|
||||
@ -34,10 +34,7 @@ pub trait MemoryProvider: Send + Sync {
|
||||
) -> AiResult<Vec<MemoryEntry>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
async fn build_context_block(
|
||||
&self,
|
||||
_session_id: Uuid,
|
||||
) -> AiResult<String> {
|
||||
async fn build_context_block(&self, _session_id: Uuid) -> AiResult<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn setup(&self) -> AiResult<()> {
|
||||
|
||||
@ -42,10 +42,7 @@ impl RagClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect(
|
||||
ai_client: &AiClient,
|
||||
config: RagConfig,
|
||||
) -> AiResult<Self> {
|
||||
pub fn connect(ai_client: &AiClient, config: RagConfig) -> AiResult<Self> {
|
||||
config.validate()?;
|
||||
let mut builder =
|
||||
Qdrant::from_url(config.url.trim()).timeout(config.timeout);
|
||||
@ -132,10 +129,8 @@ impl RagClient {
|
||||
validate_session_id(session_id)?;
|
||||
validate_documents(&documents)?;
|
||||
|
||||
let texts: Vec<String> = documents
|
||||
.iter()
|
||||
.map(|d| d.content.clone())
|
||||
.collect();
|
||||
let texts: Vec<String> =
|
||||
documents.iter().map(|d| d.content.clone()).collect();
|
||||
let vectors = self
|
||||
.embedder
|
||||
.embed_texts_chunked(texts, self.config.upsert_batch_size)
|
||||
|
||||
@ -20,8 +20,8 @@ pub(super) fn point_id(session_id: &str, document_id: &str) -> u64 {
|
||||
let uuid = Uuid::new_v5(&ns, key.as_bytes());
|
||||
let bytes = uuid.as_bytes();
|
||||
u64::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
|
||||
bytes[7],
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,9 @@ static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
}
|
||||
}
|
||||
#[allow(clippy::expect_used)]
|
||||
builder.build().expect("failed to build reqwest HTTP client — check system TLS configuration")
|
||||
builder.build().expect(
|
||||
"failed to build reqwest HTTP client — check system TLS configuration",
|
||||
)
|
||||
});
|
||||
pub async fn list_models(
|
||||
config: &EndpointConfig,
|
||||
@ -102,12 +104,14 @@ pub async fn list_models(
|
||||
AiError::Response(format!("failed to list models: {}", e))
|
||||
})?;
|
||||
|
||||
let body = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| AiError::Response(format!("failed to read models body: {}", e)))?;
|
||||
let body = resp.text().await.map_err(|e| {
|
||||
AiError::Response(format!("failed to read models body: {}", e))
|
||||
})?;
|
||||
if let Ok(parsed) = serde_json::from_str::<ModelsListResponse>(&body) {
|
||||
debug!(count = parsed.data.len(), "parsed models in standard format");
|
||||
debug!(
|
||||
count = parsed.data.len(),
|
||||
"parsed models in standard format"
|
||||
);
|
||||
return Ok(parsed.data);
|
||||
}
|
||||
if let Ok(parsed) = serde_json::from_str::<Vec<UpstreamModel>>(&body) {
|
||||
|
||||
@ -27,7 +27,10 @@ impl Toolset {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tools(mut self, tool_names: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
pub fn with_tools(
|
||||
mut self,
|
||||
tool_names: impl IntoIterator<Item = impl Into<String>>,
|
||||
) -> Self {
|
||||
self.tools.extend(tool_names.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
@ -36,7 +39,8 @@ impl Toolset {
|
||||
mut self,
|
||||
env_vars: impl IntoIterator<Item = impl Into<String>>,
|
||||
) -> Self {
|
||||
self.requires_env.extend(env_vars.into_iter().map(Into::into));
|
||||
self.requires_env
|
||||
.extend(env_vars.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
pub fn is_available(&self) -> bool {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use actix_web::{HttpResponse, web, web::ServiceConfig};
|
||||
use service::AppService;
|
||||
use service::agent::conversation::{
|
||||
ConversationResponse, ConversationWithSessionResponse, CreateConversation, MessageResponse, UpdateConversation,
|
||||
ConversationResponse, ConversationWithSessionResponse, CreateConversation,
|
||||
MessageResponse, UpdateConversation,
|
||||
};
|
||||
use service::agent::types::{AgentRunRequest, AgentRunResponse};
|
||||
use session::Session;
|
||||
@ -53,8 +54,14 @@ pub async fn list_conversations(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_conversation_list(user_id, path.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_list(user_id, path.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -70,10 +77,16 @@ pub async fn create_conversation(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<CreateConversation>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_create(user_id, path.into_inner(), body.into_inner())
|
||||
.agent_conversation_create(
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
@ -94,7 +107,9 @@ pub async fn list_all_conversations(
|
||||
service: web::Data<AppService>,
|
||||
query: web::Query<ListAllConversationsQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_list_all(user_id, query.wk.as_deref())
|
||||
@ -113,8 +128,14 @@ pub async fn get_conversation(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_conversation_get(user_id, path.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_get(user_id, path.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -130,10 +151,16 @@ pub async fn update_conversation(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<UpdateConversation>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_update(user_id, path.into_inner(), body.into_inner())
|
||||
.agent_conversation_update(
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
@ -149,8 +176,12 @@ pub async fn delete_conversation(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service.agent_conversation_delete(user_id, path.into_inner()).await?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service
|
||||
.agent_conversation_delete(user_id, path.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({ "deleted": true })))
|
||||
}
|
||||
|
||||
@ -165,8 +196,14 @@ pub async fn archive_conversation(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_conversation_archive(user_id, path.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_archive(user_id, path.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -180,8 +217,14 @@ pub async fn unarchive_conversation(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_conversation_unarchive(user_id, path.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_unarchive(user_id, path.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/agent/conversations/{id}/messages",
|
||||
@ -195,10 +238,17 @@ pub async fn list_messages(
|
||||
path: web::Path<Uuid>,
|
||||
query: web::Query<MessageListQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_message_list(user_id, path.into_inner(), query.limit, query.before)
|
||||
.agent_message_list(
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
query.limit,
|
||||
query.before,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
@ -221,7 +271,9 @@ pub async fn send_message(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<AgentRunRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let conversation_id = path.into_inner();
|
||||
let mut req = body.into_inner();
|
||||
req.conversation_id = Some(conversation_id);
|
||||
@ -240,7 +292,9 @@ pub async fn stream_agent(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<AgentRunRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let conversation_id = path.into_inner();
|
||||
let mut req = body.into_inner();
|
||||
req.conversation_id = Some(conversation_id);
|
||||
@ -282,7 +336,9 @@ pub async fn fork_conversation(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<ForkConversationRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_conversation_fork(
|
||||
|
||||
@ -15,8 +15,7 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
.route(web::post().to(create_session)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/sessions/search")
|
||||
.route(web::get().to(search_sessions)),
|
||||
web::resource("/sessions/search").route(web::get().to(search_sessions)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/sessions/{id}")
|
||||
@ -38,7 +37,9 @@ pub async fn list_sessions(
|
||||
session: Session,
|
||||
service: web::Data<AppService>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_session_list(user_id).await?)
|
||||
}
|
||||
#[utoipa::path(
|
||||
@ -52,8 +53,14 @@ pub async fn create_session(
|
||||
service: web::Data<AppService>,
|
||||
body: web::Json<CreateAgentSession>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_session_create(user_id, body.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_session_create(user_id, body.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/agent/sessions/{id}",
|
||||
@ -66,8 +73,14 @@ pub async fn get_session(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_session_get(user_id, path.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_session_get(user_id, path.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
#[utoipa::path(
|
||||
patch, path = "/api/v1/agent/sessions/{id}",
|
||||
@ -82,8 +95,14 @@ pub async fn update_session(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<UpdateAgentSession>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.agent_session_update(user_id, path.into_inner(), body.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_session_update(user_id, path.into_inner(), body.into_inner())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
#[utoipa::path(
|
||||
delete, path = "/api/v1/agent/sessions/{id}",
|
||||
@ -96,8 +115,12 @@ pub async fn delete_session(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service.agent_session_delete(user_id, path.into_inner()).await?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service
|
||||
.agent_session_delete(user_id, path.into_inner())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({ "deleted": true })))
|
||||
}
|
||||
|
||||
@ -122,7 +145,9 @@ pub async fn search_sessions(
|
||||
service: web::Data<AppService>,
|
||||
query: web::Query<SearchQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_session_search(user_id, &query.q, query.limit)
|
||||
@ -148,7 +173,9 @@ pub async fn update_session_toolsets(
|
||||
path: web::Path<Uuid>,
|
||||
body: web::Json<UpdateToolsetsRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.agent_session_update_toolsets(
|
||||
|
||||
@ -37,7 +37,9 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
web::post().to(reset_pass::reset_password_verify),
|
||||
)),
|
||||
)
|
||||
.service(web::resource("/public-key").route(web::get().to(rsa::rsa)))
|
||||
.service(
|
||||
web::resource("/public-key").route(web::get().to(rsa::rsa)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/2fa")
|
||||
.service(
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
pub mod rest;
|
||||
pub mod rest_ai;
|
||||
pub mod rest_interact;
|
||||
pub mod rest_member;
|
||||
pub mod rest_message;
|
||||
pub mod rest_room;
|
||||
pub mod rest_user;
|
||||
pub mod rest_voice;
|
||||
pub mod token;
|
||||
|
||||
@ -65,8 +65,9 @@ pub fn configure(cfg: &mut ServiceConfig, bus: ChannelBus) {
|
||||
.route(actix_web::web::post().to(rest_room::access_grant)),
|
||||
)
|
||||
.service(
|
||||
actix_web::web::resource("/workspaces/{workspace_id}/members")
|
||||
.route(actix_web::web::get().to(rest_member::list_workspace_members)),
|
||||
actix_web::web::resource("/workspaces/{workspace_id}/members").route(
|
||||
actix_web::web::get().to(rest_member::list_workspace_members),
|
||||
),
|
||||
)
|
||||
.service(
|
||||
actix_web::web::resource("/rooms/{room_id}/members/{user_id}")
|
||||
@ -184,21 +185,8 @@ pub fn configure(cfg: &mut ServiceConfig, bus: ChannelBus) {
|
||||
.route(actix_web::web::post().to(rest_voice::screen_share)),
|
||||
);
|
||||
cfg.service(
|
||||
actix_web::web::resource("/rooms/{room_id}/ai/stop")
|
||||
.route(actix_web::web::post().to(rest_ai::ai_stop)),
|
||||
)
|
||||
.service(
|
||||
actix_web::web::resource("/rooms/{room_id}/ai")
|
||||
.route(actix_web::web::get().to(rest_ai::ai_list))
|
||||
.route(actix_web::web::post().to(rest_ai::ai_add)),
|
||||
)
|
||||
.service(
|
||||
actix_web::web::resource("/rooms/{room_id}/ai/{agent_session_id}")
|
||||
.route(actix_web::web::delete().to(rest_ai::ai_remove)),
|
||||
)
|
||||
.service(
|
||||
actix_web::web::resource("/users/summary/{username}")
|
||||
.route(actix_web::web::get().to(rest_ai::user_summary)),
|
||||
.route(actix_web::web::get().to(rest_user::user_summary)),
|
||||
);
|
||||
cfg.service(
|
||||
actix_web::web::resource("/token")
|
||||
|
||||
@ -6,13 +6,13 @@ use uuid::Uuid;
|
||||
|
||||
use crate::error::ApiError;
|
||||
|
||||
pub(crate) fn extract_user(req: &HttpRequest) -> Result<Uuid, ApiError> {
|
||||
pub fn extract_user(req: &HttpRequest) -> Result<Uuid, ApiError> {
|
||||
req.get_session()
|
||||
.user()
|
||||
.ok_or_else(|| ApiError(service::error::AppError::Unauthorized))
|
||||
}
|
||||
|
||||
pub(crate) fn channel_err(e: ChannelError) -> ApiError {
|
||||
pub fn channel_err(e: ChannelError) -> ApiError {
|
||||
ApiError(match e {
|
||||
ChannelError::Unauthorized | ChannelError::TokenInvalidOrExpired => {
|
||||
service::error::AppError::Unauthorized
|
||||
@ -61,14 +61,14 @@ pub(crate) fn channel_err(e: ChannelError) -> ApiError {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ok_json(event: Option<WsOutEvent>) -> HttpResponse {
|
||||
pub fn ok_json(event: Option<WsOutEvent>) -> HttpResponse {
|
||||
match event {
|
||||
Some(e) => HttpResponse::Ok().json(e),
|
||||
None => HttpResponse::NoContent().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn created_json(event: Option<WsOutEvent>) -> HttpResponse {
|
||||
pub fn created_json(event: Option<WsOutEvent>) -> HttpResponse {
|
||||
match event {
|
||||
Some(e) => HttpResponse::Created().json(e),
|
||||
None => HttpResponse::NoContent().finish(),
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use channel::ChannelBus;
|
||||
use channel::http::{WsHandler, WsInMessage};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::rest::{channel_err, created_json, extract_user, ok_json};
|
||||
use crate::error::ApiError;
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct AiAddRequest {
|
||||
pub agent_session: Uuid,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/ws/rooms/{room_id}/ai",
|
||||
responses((status = 200, description = "AI agents in room")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn ai_list(
|
||||
req: HttpRequest,
|
||||
room_id: web::Path<Uuid>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let msg = WsInMessage::AiList {
|
||||
room: room_id.into_inner(),
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(ok_json(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/ws/rooms/{room_id}/ai",
|
||||
request_body = AiAddRequest,
|
||||
responses((status = 201, description = "AI agent added to room")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn ai_add(
|
||||
req: HttpRequest,
|
||||
room_id: web::Path<Uuid>,
|
||||
body: web::Json<AiAddRequest>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let msg = WsInMessage::AiUpsert {
|
||||
room: room_id.into_inner(),
|
||||
model: body.agent_session,
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(created_json(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/ws/rooms/{room_id}/ai/{agent_session_id}",
|
||||
responses((status = 200, description = "AI agent removed from room")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn ai_remove(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(Uuid, Uuid)>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let (room, agent_id) = path.into_inner();
|
||||
let msg = WsInMessage::AiDelete { room, agent_id };
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(ok_json(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/ws/rooms/{room_id}/ai/stop",
|
||||
responses((status = 204, description = "AI agent stopped")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn ai_stop(
|
||||
req: HttpRequest,
|
||||
room_id: web::Path<Uuid>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let msg = WsInMessage::AiStop {
|
||||
room: room_id.into_inner(),
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(ok_json(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/ws/users/summary/{username}",
|
||||
responses((status = 200, description = "User summary")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn user_summary(
|
||||
req: HttpRequest,
|
||||
username: web::Path<String>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let msg = WsInMessage::UserSummary {
|
||||
username: username.into_inner(),
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(ok_json(result))
|
||||
}
|
||||
@ -360,7 +360,10 @@ pub async fn list_workspace_members(
|
||||
let _user_id = extract_user(&req)?;
|
||||
let workspace = workspace_id.into_inner();
|
||||
|
||||
let members = bus.list_workspace_members(workspace).await.map_err(channel_err)?;
|
||||
let members = bus
|
||||
.list_workspace_members(workspace)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
let result: Vec<RoomMember> = members
|
||||
.into_iter()
|
||||
.map(|(id, username, display_name, avatar_url)| RoomMember {
|
||||
|
||||
@ -13,6 +13,7 @@ pub struct RoomCreateRequest {
|
||||
pub room_name: String,
|
||||
pub public: bool,
|
||||
pub category: Option<Uuid>,
|
||||
pub ai_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
@ -20,6 +21,7 @@ pub struct RoomUpdateRequest {
|
||||
pub room_name: Option<String>,
|
||||
pub public: Option<bool>,
|
||||
pub category: Option<Uuid>,
|
||||
pub ai_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
@ -49,10 +51,9 @@ pub async fn list_rooms(
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = extract_user(&req)?;
|
||||
let rooms = bus.list_user_rooms(user_id)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
let categories = bus.list_user_categories(user_id)
|
||||
let rooms = bus.list_user_rooms(user_id).await.map_err(channel_err)?;
|
||||
let categories = bus
|
||||
.list_user_categories(user_id)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
let workspace_id = if let Some(r) = rooms.first() {
|
||||
@ -148,6 +149,7 @@ pub async fn room_create(
|
||||
room_name: body.room_name.clone(),
|
||||
public: body.public,
|
||||
category: body.category,
|
||||
ai_enabled: body.ai_enabled,
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
@ -174,6 +176,7 @@ pub async fn room_update(
|
||||
room_name: body.room_name.clone(),
|
||||
public: body.public,
|
||||
category: body.category,
|
||||
ai_enabled: body.ai_enabled,
|
||||
};
|
||||
let result = WsHandler::handle(&bus, user_id, msg)
|
||||
.await
|
||||
|
||||
27
lib/api/src/channel/rest_user.rs
Normal file
27
lib/api/src/channel/rest_user.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use channel::ChannelBus;
|
||||
use channel::http::{WsHandler, WsInMessage};
|
||||
|
||||
use super::rest::{channel_err, extract_user, ok_json};
|
||||
use crate::error::ApiError;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/ws/users/summary/{username}",
|
||||
responses((status = 200, description = "User summary")),
|
||||
tag = "channel",
|
||||
)]
|
||||
pub async fn user_summary(
|
||||
req: HttpRequest,
|
||||
username: web::Path<String>,
|
||||
bus: web::Data<ChannelBus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let _user_id = extract_user(&req)?;
|
||||
let msg = WsInMessage::UserSummary {
|
||||
username: username.into_inner(),
|
||||
};
|
||||
let result = WsHandler::handle(&bus, _user_id, msg)
|
||||
.await
|
||||
.map_err(channel_err)?;
|
||||
Ok(ok_json(result))
|
||||
}
|
||||
@ -41,7 +41,11 @@ pub async fn archive(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoPath { wk, repo } = path.into_inner();
|
||||
match query.format.as_str() {
|
||||
"zip" => ok_json(service.git_archive_zip(&session, &wk, &repo, None).await?),
|
||||
_ => ok_json(service.git_archive_tar(&session, &wk, &repo, None).await?),
|
||||
"zip" => {
|
||||
ok_json(service.git_archive_zip(&session, &wk, &repo, None).await?)
|
||||
}
|
||||
_ => {
|
||||
ok_json(service.git_archive_tar(&session, &wk, &repo, None).await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,8 +41,13 @@ pub async fn blame_file(
|
||||
(Some(start), Some(end)) => {
|
||||
let data: dto::BlameFileResponseDto = service
|
||||
.git_blame_hunk(
|
||||
&session, &wk, &repo, query.path.clone(),
|
||||
query.rev.clone(), start, end,
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
query.path.clone(),
|
||||
query.rev.clone(),
|
||||
start,
|
||||
end,
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
@ -51,8 +56,12 @@ pub async fn blame_file(
|
||||
_ => {
|
||||
let data: dto::BlameFileResponseDto = service
|
||||
.git_blame_file(
|
||||
&session, &wk, &repo, query.path.clone(),
|
||||
query.rev.clone(), None,
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
query.path.clone(),
|
||||
query.rev.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
|
||||
@ -65,10 +65,8 @@ pub async fn list_branches(
|
||||
return ok_json(data);
|
||||
}
|
||||
if query.default_only {
|
||||
let data: dto::BranchHeadResponseDto = service
|
||||
.git_branch_head(&session, &wk, &repo)
|
||||
.await?
|
||||
.into();
|
||||
let data: dto::BranchHeadResponseDto =
|
||||
service.git_branch_head(&session, &wk, &repo).await?.into();
|
||||
return ok_json(data);
|
||||
}
|
||||
let data: dto::BranchListResponseDto = service
|
||||
@ -180,7 +178,13 @@ pub async fn ahead_behind(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoBranchPath { wk, repo, name } = path.into_inner();
|
||||
let data: dto::BranchAheadBehindResponseDto = service
|
||||
.git_branch_ahead_behind(&session, &wk, &repo, name, query.remote_branch.clone())
|
||||
.git_branch_ahead_behind(
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
name,
|
||||
query.remote_branch.clone(),
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
ok_json(data)
|
||||
|
||||
@ -64,10 +64,8 @@ pub async fn list_commits(
|
||||
return ok_json(data);
|
||||
}
|
||||
if query.refs {
|
||||
let data: dto::CommitRefsResponseDto = service
|
||||
.git_commit_refs(&session, &wk, &repo)
|
||||
.await?
|
||||
.into();
|
||||
let data: dto::CommitRefsResponseDto =
|
||||
service.git_commit_refs(&session, &wk, &repo).await?.into();
|
||||
return ok_json(data);
|
||||
}
|
||||
if query.summary {
|
||||
@ -98,7 +96,9 @@ pub async fn commit_history(
|
||||
let WkRepoPath { wk, repo } = path.into_inner();
|
||||
let data: dto::CommitHistoryResponseDto = service
|
||||
.git_commit_history(
|
||||
&session, &wk, &repo,
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
query.limit.unwrap_or(20),
|
||||
query.skip.unwrap_or(0),
|
||||
query.sort.unwrap_or(0),
|
||||
|
||||
@ -40,9 +40,13 @@ pub async fn list_statuses(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<CommitShaPath>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
ok_json(service.git_commit_status_list_by_name(
|
||||
&session, &path.wk, &path.repo, &path.sha,
|
||||
).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_commit_status_list_by_name(
|
||||
&session, &path.wk, &path.repo, &path.sha,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -56,9 +60,13 @@ pub async fn combined_status(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<CommitShaPath>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
ok_json(service.git_commit_status_combined_by_name(
|
||||
&session, &path.wk, &path.repo, &path.sha,
|
||||
).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_commit_status_combined_by_name(
|
||||
&session, &path.wk, &path.repo, &path.sha,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -74,8 +82,19 @@ pub async fn create_status(
|
||||
path: web::Path<CommitShaPath>,
|
||||
body: web::Json<CreateCommitStatus>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_created(service.git_commit_status_create_by_name(
|
||||
&session, user_id, &path.wk, &path.repo, &path.sha, body.into_inner(),
|
||||
).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_created(
|
||||
service
|
||||
.git_commit_status_create_by_name(
|
||||
&session,
|
||||
user_id,
|
||||
&path.wk,
|
||||
&path.repo,
|
||||
&path.sha,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
@ -33,11 +33,15 @@ pub async fn compare(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, basehead) = path.into_inner();
|
||||
|
||||
let (base, head) = basehead
|
||||
.split_once("...")
|
||||
.ok_or_else(|| ApiError(service::error::AppError::BadRequest(
|
||||
let (base, head) = basehead.split_once("...").ok_or_else(|| {
|
||||
ApiError(service::error::AppError::BadRequest(
|
||||
"basehead must be in format 'base...head'".to_string(),
|
||||
)))?;
|
||||
))
|
||||
})?;
|
||||
|
||||
ok_json(service.git_compare(&session, &wk, &repo_name, base, head).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_compare(&session, &wk, &repo_name, base, head)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
@ -34,9 +34,17 @@ pub async fn get_contents(
|
||||
query: web::Query<ContentQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, file_path) = info.into_inner();
|
||||
ok_json(service.git_contents_get_by_name(
|
||||
&session, &wk, &repo_name, &file_path, query.r#ref.as_deref(),
|
||||
).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_contents_get_by_name(
|
||||
&session,
|
||||
&wk,
|
||||
&repo_name,
|
||||
&file_path,
|
||||
query.r#ref.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -53,9 +61,15 @@ pub async fn create_contents(
|
||||
body: web::Json<CreateContent>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, file_path) = info.into_inner();
|
||||
let resp = service.git_contents_create_by_name(
|
||||
&session, &wk, &repo_name, &file_path, body.into_inner(),
|
||||
).await?;
|
||||
let resp = service
|
||||
.git_contents_create_by_name(
|
||||
&session,
|
||||
&wk,
|
||||
&repo_name,
|
||||
&file_path,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::Created().json(resp))
|
||||
}
|
||||
|
||||
@ -73,9 +87,17 @@ pub async fn update_contents(
|
||||
body: web::Json<UpdateContent>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, file_path) = info.into_inner();
|
||||
ok_json(service.git_contents_update_by_name(
|
||||
&session, &wk, &repo_name, &file_path, body.into_inner(),
|
||||
).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_contents_update_by_name(
|
||||
&session,
|
||||
&wk,
|
||||
&repo_name,
|
||||
&file_path,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::IntoParams)]
|
||||
@ -98,8 +120,16 @@ pub async fn delete_contents(
|
||||
query: web::Query<DeleteContentQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, file_path) = info.into_inner();
|
||||
service.git_contents_delete_by_name(
|
||||
&session, &wk, &repo_name, &file_path, &query.message, &query.sha, query.branch.as_deref(),
|
||||
).await?;
|
||||
service
|
||||
.git_contents_delete_by_name(
|
||||
&session,
|
||||
&wk,
|
||||
&repo_name,
|
||||
&file_path,
|
||||
&query.message,
|
||||
&query.sha,
|
||||
query.branch.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
@ -44,18 +44,34 @@ pub async fn diff(
|
||||
query: web::Query<DiffQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoPath { wk, repo } = path.into_inner();
|
||||
if let (Some(old_tree), Some(new_tree)) = (&query.old_tree, &query.new_tree) {
|
||||
if let (Some(old_tree), Some(new_tree)) = (&query.old_tree, &query.new_tree)
|
||||
{
|
||||
let proto_resp = service
|
||||
.git_diff_tree_to_tree(&session, &wk, &repo, old_tree.clone(), new_tree.clone(), None)
|
||||
.git_diff_tree_to_tree(
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
old_tree.clone(),
|
||||
new_tree.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let data: dto::DiffResultDto = proto_resp.result.unwrap_or_default().into();
|
||||
let data: dto::DiffResultDto =
|
||||
proto_resp.result.unwrap_or_default().into();
|
||||
return ok_json(data);
|
||||
}
|
||||
if let Some(tree_oid) = &query.tree_oid {
|
||||
let proto_resp = service
|
||||
.git_diff_index_to_tree(&session, &wk, &repo, tree_oid.clone(), None)
|
||||
.git_diff_index_to_tree(
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
tree_oid.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let data: dto::DiffResultDto = proto_resp.result.unwrap_or_default().into();
|
||||
let data: dto::DiffResultDto =
|
||||
proto_resp.result.unwrap_or_default().into();
|
||||
return ok_json(data);
|
||||
}
|
||||
let old_oid = query.old_oid.clone().unwrap_or_default();
|
||||
@ -66,21 +82,29 @@ pub async fn diff(
|
||||
let proto_resp = service
|
||||
.git_diff_stats(&session, &wk, &repo, old_oid, new_oid, None)
|
||||
.await?;
|
||||
let data: dto::DiffStatsDto = proto_resp.result.and_then(|r| r.stats).unwrap_or_default().into();
|
||||
let data: dto::DiffStatsDto = proto_resp
|
||||
.result
|
||||
.and_then(|r| r.stats)
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
ok_json(data)
|
||||
}
|
||||
"side-by-side" => {
|
||||
let proto_resp = service
|
||||
.git_diff_patch_side_by_side(&session, &wk, &repo, old_oid, new_oid, None)
|
||||
.git_diff_patch_side_by_side(
|
||||
&session, &wk, &repo, old_oid, new_oid, None,
|
||||
)
|
||||
.await?;
|
||||
let data: dto::SideBySideDiffResultDto = proto_resp.result.unwrap_or_default().into();
|
||||
let data: dto::SideBySideDiffResultDto =
|
||||
proto_resp.result.unwrap_or_default().into();
|
||||
ok_json(data)
|
||||
}
|
||||
_ => {
|
||||
let proto_resp = service
|
||||
.git_diff_patch(&session, &wk, &repo, old_oid, new_oid, None)
|
||||
.await?;
|
||||
let data: dto::DiffResultDto = proto_resp.result.unwrap_or_default().into();
|
||||
let data: dto::DiffResultDto =
|
||||
proto_resp.result.unwrap_or_default().into();
|
||||
ok_json(data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use base64::Engine;
|
||||
use git::rpc::proto as p;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use git::rpc::proto as p;
|
||||
|
||||
fn oid_val(oid: Option<p::ObjectId>) -> String {
|
||||
oid.map(|o| o.value).unwrap_or_default()
|
||||
@ -430,7 +430,9 @@ impl From<p::BranchSummaryResponse> for BranchSummaryResponseDto {
|
||||
|
||||
impl From<p::BranchHeadResponse> for BranchHeadResponseDto {
|
||||
fn from(r: p::BranchHeadResponse) -> Self {
|
||||
BranchHeadResponseDto { head_name: r.head_name }
|
||||
BranchHeadResponseDto {
|
||||
head_name: r.head_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,7 +533,9 @@ impl From<p::CommitRefsResponse> for CommitRefsResponseDto {
|
||||
|
||||
impl From<p::CommitPrefixResponse> for CommitPrefixResponseDto {
|
||||
fn from(r: p::CommitPrefixResponse) -> Self {
|
||||
CommitPrefixResponseDto { oid: oid_opt(r.oid) }
|
||||
CommitPrefixResponseDto {
|
||||
oid: oid_opt(r.oid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,13 +547,17 @@ impl From<p::CommitExistsResponse> for CommitExistsResponseDto {
|
||||
|
||||
impl From<p::CherryPickResponse> for CherryPickResponseDto {
|
||||
fn from(r: p::CherryPickResponse) -> Self {
|
||||
CherryPickResponseDto { oid: oid_opt(r.oid) }
|
||||
CherryPickResponseDto {
|
||||
oid: oid_opt(r.oid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p::CherryPickSequenceResponse> for CherryPickResponseDto {
|
||||
fn from(r: p::CherryPickSequenceResponse) -> Self {
|
||||
CherryPickResponseDto { oid: oid_opt(r.oid) }
|
||||
CherryPickResponseDto {
|
||||
oid: oid_opt(r.oid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -646,7 +654,9 @@ impl From<p::BlobExistsResponse> for BlobExistsResponseDto {
|
||||
|
||||
impl From<p::BlobIsBinaryResponse> for BlobIsBinaryResponseDto {
|
||||
fn from(r: p::BlobIsBinaryResponse) -> Self {
|
||||
BlobIsBinaryResponseDto { is_binary: r.is_binary }
|
||||
BlobIsBinaryResponseDto {
|
||||
is_binary: r.is_binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -680,31 +690,41 @@ impl From<p::TagListResponse> for TagListResponseDto {
|
||||
|
||||
impl From<p::TagInfoResponse> for TagInfoResponseDto {
|
||||
fn from(r: p::TagInfoResponse) -> Self {
|
||||
TagInfoResponseDto { tag: r.tag.map(Into::into) }
|
||||
TagInfoResponseDto {
|
||||
tag: r.tag.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p::TagSummary> for TagSummaryDto {
|
||||
fn from(s: p::TagSummary) -> Self {
|
||||
TagSummaryDto { total_count: s.total_count }
|
||||
TagSummaryDto {
|
||||
total_count: s.total_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p::TagSummaryResponse> for TagSummaryResponseDto {
|
||||
fn from(r: p::TagSummaryResponse) -> Self {
|
||||
TagSummaryResponseDto { summary: r.summary.map(Into::into) }
|
||||
TagSummaryResponseDto {
|
||||
summary: r.summary.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p::TagInitResponse> for TagInitResponseDto {
|
||||
fn from(r: p::TagInitResponse) -> Self {
|
||||
TagInitResponseDto { oid: oid_opt(r.oid) }
|
||||
TagInitResponseDto {
|
||||
oid: oid_opt(r.oid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p::TagUpdateMessageResponse> for TagUpdateMessageResponseDto {
|
||||
fn from(r: p::TagUpdateMessageResponse) -> Self {
|
||||
TagUpdateMessageResponseDto { oid: oid_opt(r.oid) }
|
||||
TagUpdateMessageResponseDto {
|
||||
oid: oid_opt(r.oid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -843,10 +863,16 @@ impl From<p::DiffResult> for DiffResultDto {
|
||||
impl From<p::SideBySideChangeType> for SideBySideChangeTypeDto {
|
||||
fn from(t: p::SideBySideChangeType) -> Self {
|
||||
match t {
|
||||
p::SideBySideChangeType::Unchanged => SideBySideChangeTypeDto::Unchanged,
|
||||
p::SideBySideChangeType::Unchanged => {
|
||||
SideBySideChangeTypeDto::Unchanged
|
||||
}
|
||||
p::SideBySideChangeType::Added => SideBySideChangeTypeDto::Added,
|
||||
p::SideBySideChangeType::Removed => SideBySideChangeTypeDto::Removed,
|
||||
p::SideBySideChangeType::Modified => SideBySideChangeTypeDto::Modified,
|
||||
p::SideBySideChangeType::Removed => {
|
||||
SideBySideChangeTypeDto::Removed
|
||||
}
|
||||
p::SideBySideChangeType::Modified => {
|
||||
SideBySideChangeTypeDto::Modified
|
||||
}
|
||||
p::SideBySideChangeType::Empty => SideBySideChangeTypeDto::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use service::{AppService, git::init::{CloneRepo, CreateRepo}};
|
||||
use service::{
|
||||
AppService,
|
||||
git::init::{CloneRepo, CreateRepo},
|
||||
};
|
||||
use session::Session;
|
||||
|
||||
use crate::error::ApiError;
|
||||
|
||||
@ -31,8 +31,7 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
.route(web::get().to(repo::list_repos)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/clone")
|
||||
.route(web::post().to(init::clone_repo)),
|
||||
web::resource("/clone").route(web::post().to(init::clone_repo)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/{repo}")
|
||||
@ -132,8 +131,7 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
.route(web::get().to(blob::blob_info)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/blame")
|
||||
.route(web::get().to(blame::blame_file)),
|
||||
web::resource("/blame").route(web::get().to(blame::blame_file)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/trees/{oid}")
|
||||
@ -147,10 +145,7 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
web::resource("/commits/{oid}/tree")
|
||||
.route(web::get().to(tree::tree_entry_by_path_from_commit)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/diff")
|
||||
.route(web::get().to(diff::diff)),
|
||||
)
|
||||
.service(web::resource("/diff").route(web::get().to(diff::diff)))
|
||||
.service(
|
||||
web::resource("/diff/branches")
|
||||
.route(web::get().to(readme::diff_branches)),
|
||||
@ -195,8 +190,7 @@ pub fn configure(cfg: &mut ServiceConfig) {
|
||||
.route(web::get().to(readme::get_readme)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/refs")
|
||||
.route(web::get().to(refs::list_refs)),
|
||||
web::resource("/refs").route(web::get().to(refs::list_refs)),
|
||||
),
|
||||
);
|
||||
cfg.service(
|
||||
|
||||
@ -34,8 +34,14 @@ pub async fn list_refs(
|
||||
query: web::Query<RefQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(ref_name) = &query.r#ref {
|
||||
let r = service.git_ref_get_by_name(&session, &path.wk, &path.repo, ref_name).await?;
|
||||
let r = service
|
||||
.git_ref_get_by_name(&session, &path.wk, &path.repo, ref_name)
|
||||
.await?;
|
||||
return ok_json(vec![r]);
|
||||
}
|
||||
ok_json(service.git_ref_list_by_name(&session, &path.wk, &path.repo).await?)
|
||||
ok_json(
|
||||
service
|
||||
.git_ref_list_by_name(&session, &path.wk, &path.repo)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
@ -43,8 +43,14 @@ pub async fn list_releases(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<WkRepoPath>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.git_release_list_by_name(&session, user_id, &path.wk, &path.repo).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.git_release_list_by_name(&session, user_id, &path.wk, &path.repo)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -58,8 +64,16 @@ pub async fn get_release(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<ReleaseIdPath>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.git_release_get_by_name(&session, user_id, &path.wk, &path.repo, path.id).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.git_release_get_by_name(
|
||||
&session, user_id, &path.wk, &path.repo, path.id,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -74,8 +88,16 @@ pub async fn get_release_by_tag(
|
||||
path: web::Path<(String, String, String)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, tag) = path.into_inner();
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.git_release_get_by_tag_name(&session, user_id, &wk, &repo_name, &tag).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.git_release_get_by_tag_name(
|
||||
&session, user_id, &wk, &repo_name, &tag,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -91,8 +113,20 @@ pub async fn create_release(
|
||||
path: web::Path<WkRepoPath>,
|
||||
body: web::Json<CreateRelease>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_created(service.git_release_create_by_name(&session, user_id, &path.wk, &path.repo, body.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_created(
|
||||
service
|
||||
.git_release_create_by_name(
|
||||
&session,
|
||||
user_id,
|
||||
&path.wk,
|
||||
&path.repo,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -108,8 +142,21 @@ pub async fn update_release(
|
||||
path: web::Path<ReleaseIdPath>,
|
||||
body: web::Json<UpdateRelease>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(service.git_release_update_by_name(&session, user_id, &path.wk, &path.repo, path.id, body.into_inner()).await?)
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
ok_json(
|
||||
service
|
||||
.git_release_update_by_name(
|
||||
&session,
|
||||
user_id,
|
||||
&path.wk,
|
||||
&path.repo,
|
||||
path.id,
|
||||
body.into_inner(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@ -123,8 +170,14 @@ pub async fn delete_release(
|
||||
service: web::Data<AppService>,
|
||||
path: web::Path<ReleaseIdPath>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service.git_release_delete_by_name(&session, user_id, &path.wk, &path.repo, path.id).await?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service
|
||||
.git_release_delete_by_name(
|
||||
&session, user_id, &path.wk, &path.repo, path.id,
|
||||
)
|
||||
.await?;
|
||||
ok_empty()
|
||||
}
|
||||
|
||||
@ -140,7 +193,13 @@ pub async fn delete_release_by_tag(
|
||||
path: web::Path<(String, String, String)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (wk, repo_name, tag) = path.into_inner();
|
||||
let user_id = session.user().ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service.git_release_delete_by_tag_name(&session, user_id, &wk, &repo_name, &tag).await?;
|
||||
let user_id = session
|
||||
.user()
|
||||
.ok_or(ApiError(service::error::AppError::Unauthorized))?;
|
||||
service
|
||||
.git_release_delete_by_tag_name(
|
||||
&session, user_id, &wk, &repo_name, &tag,
|
||||
)
|
||||
.await?;
|
||||
ok_empty()
|
||||
}
|
||||
|
||||
@ -54,10 +54,8 @@ pub async fn list_tags(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoPath { wk, repo } = path.into_inner();
|
||||
if query.summary {
|
||||
let data: dto::TagSummaryResponseDto = service
|
||||
.git_tag_summary(&session, &wk, &repo)
|
||||
.await?
|
||||
.into();
|
||||
let data: dto::TagSummaryResponseDto =
|
||||
service.git_tag_summary(&session, &wk, &repo).await?.into();
|
||||
return ok_json(data);
|
||||
}
|
||||
let data: dto::TagListResponseDto = service
|
||||
@ -117,9 +115,7 @@ pub async fn delete_tag(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoTagPath { wk, repo, name } = path.into_inner();
|
||||
let params = git::rpc::proto::TagDeleteParams { name };
|
||||
let _ = service
|
||||
.git_tag_delete(&session, &wk, &repo, params)
|
||||
.await?;
|
||||
let _ = service.git_tag_delete(&session, &wk, &repo, params).await?;
|
||||
ok_json(serde_json::json!({}))
|
||||
}
|
||||
#[utoipa::path(
|
||||
@ -146,9 +142,7 @@ pub async fn update_tag(
|
||||
new_name: new_name.to_string(),
|
||||
force: false,
|
||||
};
|
||||
let _ = service
|
||||
.git_tag_rename(&session, &wk, &repo, params)
|
||||
.await?;
|
||||
let _ = service.git_tag_rename(&session, &wk, &repo, params).await?;
|
||||
return ok_json(serde_json::json!({}));
|
||||
}
|
||||
let message = body
|
||||
|
||||
@ -86,7 +86,13 @@ pub async fn tree_entry_by_path(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoTreeSubPath { wk, repo, tree_oid } = path.into_inner();
|
||||
let data: dto::TreeEntryByPathResponseDto = service
|
||||
.git_tree_entry_by_path(&session, &wk, &repo, tree_oid, query.path.clone())
|
||||
.git_tree_entry_by_path(
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
tree_oid,
|
||||
query.path.clone(),
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
ok_json(data)
|
||||
@ -112,7 +118,13 @@ pub async fn tree_entry_by_path_from_commit(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let WkRepoCommitPath { wk, repo, oid } = path.into_inner();
|
||||
let data: dto::TreeEntryByPathResponseDto = service
|
||||
.git_tree_entry_by_path_from_commit(&session, &wk, &repo, oid, query.path.clone())
|
||||
.git_tree_entry_by_path_from_commit(
|
||||
&session,
|
||||
&wk,
|
||||
&repo,
|
||||
oid,
|
||||
query.path.clone(),
|
||||
)
|
||||
.await?
|
||||
.into();
|
||||
ok_json(data)
|
||||
|
||||
@ -31,29 +31,28 @@ pub fn configure(cfg: &mut ServiceConfig, channel_bus: channel::ChannelBus) {
|
||||
.service(
|
||||
web::scope("/repos")
|
||||
.configure(git::configure)
|
||||
.configure(pull_request::configure)
|
||||
.configure(pull_request::configure),
|
||||
)
|
||||
.service(
|
||||
web::scope("/issues")
|
||||
.configure(issues::configure)
|
||||
.configure(issues::configure),
|
||||
)
|
||||
.service(
|
||||
web::scope("/labels")
|
||||
.configure(issues::configure_labels)
|
||||
.configure(issues::configure_labels),
|
||||
)
|
||||
.service(
|
||||
web::scope("/milestones")
|
||||
.configure(issues::configure_milestones)
|
||||
)
|
||||
)
|
||||
.configure(issues::configure_milestones),
|
||||
),
|
||||
),
|
||||
)
|
||||
.service(
|
||||
web::scope("/ws")
|
||||
.configure(|cfg| channel::configure(cfg, channel_bus)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/search")
|
||||
.route(web::get().to(search::search)),
|
||||
)
|
||||
web::resource("/search").route(web::get().to(search::search)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -297,11 +297,7 @@ use utoipa::openapi::security::{
|
||||
crate::channel::rest_voice::voice_mute,
|
||||
crate::channel::rest_voice::voice_deaf,
|
||||
crate::channel::rest_voice::screen_share,
|
||||
crate::channel::rest_ai::ai_list,
|
||||
crate::channel::rest_ai::ai_add,
|
||||
crate::channel::rest_ai::ai_remove,
|
||||
crate::channel::rest_ai::ai_stop,
|
||||
crate::channel::rest_ai::user_summary,
|
||||
crate::channel::rest_user::user_summary,
|
||||
crate::search::search,
|
||||
),
|
||||
modifiers(&SecurityAddon)
|
||||
|
||||
@ -2,8 +2,8 @@ use actix_web::{HttpResponse, web};
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::ApiError;
|
||||
use crate::channel::ChannelBus;
|
||||
use crate::error::ApiError;
|
||||
use service::AppService;
|
||||
use session::Session;
|
||||
|
||||
@ -177,14 +177,9 @@ async fn search_rooms(
|
||||
user_id: uuid::Uuid,
|
||||
q: &str,
|
||||
) -> Result<SearchGroup<RoomHit>, ApiError> {
|
||||
let rooms = bus
|
||||
.list_user_rooms(user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError(service::error::AppError::InternalServerError(
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
let rooms = bus.list_user_rooms(user_id).await.map_err(|e| {
|
||||
ApiError(service::error::AppError::InternalServerError(e.to_string()))
|
||||
})?;
|
||||
|
||||
let all: Vec<RoomHit> = rooms
|
||||
.into_iter()
|
||||
|
||||
@ -2,7 +2,9 @@ use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use serde::Serialize;
|
||||
use service::{
|
||||
AppService,
|
||||
user::profile::{AvatarUploadResponse, UpdateUserProfileConfig, UserProfileConfig},
|
||||
user::profile::{
|
||||
AvatarUploadResponse, UpdateUserProfileConfig, UserProfileConfig,
|
||||
},
|
||||
};
|
||||
use session::Session;
|
||||
|
||||
|
||||
@ -106,12 +106,7 @@ pub async fn update_group(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let GroupPath { wk, group_name } = path.into_inner();
|
||||
let data = service
|
||||
.workspace_update_group(
|
||||
&session,
|
||||
&wk,
|
||||
&group_name,
|
||||
params.into_inner(),
|
||||
)
|
||||
.workspace_update_group(&session, &wk, &group_name, params.into_inner())
|
||||
.await?;
|
||||
ok_json(data)
|
||||
}
|
||||
|
||||
@ -102,12 +102,7 @@ pub async fn update_member(
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let MemberPath { wk, username } = path.into_inner();
|
||||
let data = service
|
||||
.workspace_update_member(
|
||||
&session,
|
||||
&wk,
|
||||
&username,
|
||||
params.into_inner(),
|
||||
)
|
||||
.workspace_update_member(&session, &wk, &username, params.into_inner())
|
||||
.await?;
|
||||
ok_json(data)
|
||||
}
|
||||
|
||||
@ -6,12 +6,10 @@ pub mod workspace;
|
||||
use actix_web::{web, web::ServiceConfig};
|
||||
pub fn configure(cfg: &mut ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
.route(web::post().to(workspace::create_workspace)),
|
||||
web::resource("").route(web::post().to(workspace::create_workspace)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/my")
|
||||
.route(web::get().to(workspace::my_workspaces)),
|
||||
web::resource("/my").route(web::get().to(workspace::my_workspaces)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/join/my-applies")
|
||||
@ -64,12 +62,10 @@ pub fn configure_wk(cfg: &mut ServiceConfig) {
|
||||
.route(web::put().to(join::update_join_strategy)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/join/apply")
|
||||
.route(web::post().to(join::apply_join)),
|
||||
web::resource("/join/apply").route(web::post().to(join::apply_join)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/join/cancel")
|
||||
.route(web::post().to(join::cancel_join)),
|
||||
web::resource("/join/cancel").route(web::post().to(join::cancel_join)),
|
||||
);
|
||||
cfg.service(
|
||||
web::resource("/join/applies")
|
||||
|
||||
2
lib/cache/local.rs
vendored
2
lib/cache/local.rs
vendored
@ -26,7 +26,7 @@ impl Default for LocalCacheConfig {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MokaCache {
|
||||
pub(crate) inner: Cache<Arc<str>, Arc<[u8]>>,
|
||||
pub inner: Cache<Arc<str>, Arc<[u8]>>,
|
||||
}
|
||||
|
||||
impl MokaCache {
|
||||
|
||||
@ -35,5 +35,6 @@ tokio = { workspace = true, features = ["sync", "time"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||
lazy_static = "1.5.0"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@ -33,24 +33,31 @@ const ROOM_MESSAGE_EVENT: &str = "room.message";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChannelBus {
|
||||
pub(crate) inner: Arc<Inner>,
|
||||
pub inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
pub(crate) struct Inner {
|
||||
pub(crate) db: AppDatabase,
|
||||
pub(crate) cache: AppCache,
|
||||
pub(crate) io: SocketIo,
|
||||
pub(crate) config: ChannelBusConfig,
|
||||
pub(crate) online: RwLock<HashMap<Uuid, HashMap<String, Socket>>>,
|
||||
pub(crate) user_sync_locks: DashMap<Uuid, Arc<Mutex<()>>>,
|
||||
pub(crate) typing_states: DashMap<(Uuid, Uuid), (crate::event::UserInfo, crate::event::RoomInfo, tokio_util::sync::CancellationToken)>,
|
||||
pub(crate) seq: SeqAllocator,
|
||||
pub(crate) dedup: DeduplicationManager,
|
||||
pub(crate) metrics: ChannelMetrics,
|
||||
pub(crate) reconnect: ReconnectManager,
|
||||
pub(crate) rate_limiter: RateLimiter,
|
||||
pub(crate) csrf: CsrfProtection,
|
||||
pub(crate) circuit_breaker: CircuitBreaker,
|
||||
pub struct Inner {
|
||||
pub db: AppDatabase,
|
||||
pub cache: AppCache,
|
||||
pub io: SocketIo,
|
||||
pub config: ChannelBusConfig,
|
||||
pub online: RwLock<HashMap<Uuid, HashMap<String, Socket>>>,
|
||||
pub user_sync_locks: DashMap<Uuid, Arc<Mutex<()>>>,
|
||||
pub typing_states: DashMap<
|
||||
(Uuid, Uuid),
|
||||
(
|
||||
crate::event::UserInfo,
|
||||
crate::event::RoomInfo,
|
||||
tokio_util::sync::CancellationToken,
|
||||
),
|
||||
>,
|
||||
pub seq: SeqAllocator,
|
||||
pub dedup: DeduplicationManager,
|
||||
pub metrics: ChannelMetrics,
|
||||
pub reconnect: ReconnectManager,
|
||||
pub rate_limiter: RateLimiter,
|
||||
pub csrf: CsrfProtection,
|
||||
pub circuit_breaker: CircuitBreaker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -79,7 +86,7 @@ impl ChannelBus {
|
||||
&self,
|
||||
room: Uuid,
|
||||
) -> ChannelResult<crate::event::RoomInfo> {
|
||||
let row = db::sqlx::query_as::<_, (String,)>(
|
||||
let row = db::sqlx::query_as::<_, (String,)>(
|
||||
"SELECT name FROM room WHERE id = $1",
|
||||
)
|
||||
.bind(room)
|
||||
@ -132,7 +139,8 @@ impl ChannelBus {
|
||||
pub async fn lookup_users(
|
||||
&self,
|
||||
users: &[Uuid],
|
||||
) -> ChannelResult<std::collections::HashMap<Uuid, crate::event::UserInfo>> {
|
||||
) -> ChannelResult<std::collections::HashMap<Uuid, crate::event::UserInfo>>
|
||||
{
|
||||
if users.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
@ -585,7 +593,9 @@ impl ChannelBus {
|
||||
Err(_) => None,
|
||||
};
|
||||
let event = match sender {
|
||||
Some(s) => ChannelEvent::message_created_with_sender(message, s),
|
||||
Some(s) => {
|
||||
ChannelEvent::message_created_with_sender(message, s)
|
||||
}
|
||||
None => ChannelEvent::message_created(message),
|
||||
};
|
||||
socket.emit(ROOM_MESSAGE_EVENT, event).await?;
|
||||
|
||||
@ -71,17 +71,15 @@ impl CircuitBreaker {
|
||||
let slot_reserved = {
|
||||
let mut state = self.inner.state.lock().await;
|
||||
match state.status {
|
||||
STATUS_OPEN => {
|
||||
match state.last_failure_time {
|
||||
Some(t) if t.elapsed() > self.inner.config.timeout => {
|
||||
state.status = STATUS_HALF_OPEN;
|
||||
state.half_open_calls = 1;
|
||||
state.success_count = 0;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
STATUS_OPEN => match state.last_failure_time {
|
||||
Some(t) if t.elapsed() > self.inner.config.timeout => {
|
||||
state.status = STATUS_HALF_OPEN;
|
||||
state.half_open_calls = 1;
|
||||
state.success_count = 0;
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
STATUS_HALF_OPEN => {
|
||||
if state.half_open_calls
|
||||
< self.inner.config.half_open_max_calls
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{AgentInfo, RoomInfo};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiAgentJoinedService {
|
||||
pub room: RoomInfo,
|
||||
pub agent: AgentInfo,
|
||||
pub joined_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiAgentLeftService {
|
||||
pub room: RoomInfo,
|
||||
pub agent: AgentInfo,
|
||||
pub left_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoomAiEntry {
|
||||
pub agent_session: Uuid,
|
||||
pub name: String,
|
||||
pub agent_kind: String,
|
||||
pub model_version: Option<Uuid>,
|
||||
pub enabled: bool,
|
||||
pub auto_reply: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoomAiListService {
|
||||
pub room: RoomInfo,
|
||||
pub agents: Vec<RoomAiEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AiAgentStatusChangedService {
|
||||
pub room: RoomInfo,
|
||||
pub agent: AgentInfo,
|
||||
pub old_status: String,
|
||||
pub new_status: String,
|
||||
pub changed_at: DateTime<Utc>,
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::event::{RoomInfo, UserInfo};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::event::{UserInfo, WorkspaceInfo};
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use crate::event::{UserInfo, WorkspaceInfo};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@ -72,21 +72,3 @@ impl WorkspaceInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub agent_type: String,
|
||||
pub model_name: Option<String>,
|
||||
}
|
||||
|
||||
impl AgentInfo {
|
||||
pub fn unknown(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: String::new(),
|
||||
agent_type: String::new(),
|
||||
model_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{RoomInfo, UserInfo};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DmEventType {
|
||||
Created,
|
||||
Closed,
|
||||
Reopened,
|
||||
}
|
||||
|
||||
impl DmEventType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Created => "dm.created",
|
||||
Self::Closed => "dm.closed",
|
||||
Self::Reopened => "dm.reopened",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum DmEvent {
|
||||
#[serde(rename = "dm.created")]
|
||||
Created(DmCreatedService),
|
||||
#[serde(rename = "dm.closed")]
|
||||
Closed(DmClosedService),
|
||||
#[serde(rename = "dm.reopened")]
|
||||
Reopened(DmReopenedService),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DmCreatedService {
|
||||
pub room: RoomInfo,
|
||||
pub initiator: UserInfo,
|
||||
pub recipient: UserInfo,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DmClosedService {
|
||||
pub room: RoomInfo,
|
||||
pub closed_by: UserInfo,
|
||||
pub closed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DmReopenedService {
|
||||
pub room: RoomInfo,
|
||||
pub reopened_by: UserInfo,
|
||||
pub reopened_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DmCreateClient {
|
||||
pub recipient: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DmCloseClient {
|
||||
pub room: Uuid,
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
pub mod ai;
|
||||
pub mod attachment;
|
||||
pub mod ban;
|
||||
pub mod category;
|
||||
pub mod common;
|
||||
pub mod conversation;
|
||||
pub mod dm;
|
||||
pub mod draft;
|
||||
pub mod forward;
|
||||
pub mod invite;
|
||||
@ -22,7 +20,7 @@ pub mod thread;
|
||||
pub mod voice;
|
||||
pub mod workspace;
|
||||
|
||||
pub use common::{AgentInfo, RoomInfo, UserInfo, WorkspaceInfo};
|
||||
pub use common::{RoomInfo, UserInfo, WorkspaceInfo};
|
||||
|
||||
use model::room::RoomMessageModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -37,8 +35,6 @@ pub enum ChannelEventType {
|
||||
ReactionCreated,
|
||||
ReactionDeleted,
|
||||
MessageRead,
|
||||
DmCreated,
|
||||
DmClosed,
|
||||
ConversationUpdated,
|
||||
Custom(String),
|
||||
}
|
||||
@ -52,8 +48,6 @@ impl ChannelEventType {
|
||||
Self::ReactionCreated => "reaction.created",
|
||||
Self::ReactionDeleted => "reaction.deleted",
|
||||
Self::MessageRead => "message.read",
|
||||
Self::DmCreated => "dm.created",
|
||||
Self::DmClosed => "dm.closed",
|
||||
Self::ConversationUpdated => "conversation.updated",
|
||||
Self::Custom(value) => value,
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{RoomInfo, UserInfo};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{RoomInfo, UserInfo};
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ pub enum RoomEventType {
|
||||
TopicUpdated,
|
||||
SettingsUpdated,
|
||||
Moved,
|
||||
AiUpdated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -27,8 +26,6 @@ pub enum RoomEvent {
|
||||
Renamed(RoomRenamedService),
|
||||
#[serde(rename = "room.moved")]
|
||||
Moved(RoomMovedService),
|
||||
#[serde(rename = "room.ai_updated")]
|
||||
AiUpdated(RoomAiUpdatedService),
|
||||
#[serde(rename = "room.topic_updated")]
|
||||
TopicUpdated(RoomTopicUpdatedService),
|
||||
#[serde(rename = "room.settings_updated")]
|
||||
@ -73,18 +70,6 @@ pub struct RoomMovedService {
|
||||
pub moved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoomAiUpdatedService {
|
||||
pub room: RoomInfo,
|
||||
pub workspace: WorkspaceInfo,
|
||||
pub model: Uuid,
|
||||
pub model_name: String,
|
||||
pub version: i64,
|
||||
pub agent_type: String,
|
||||
pub updated_by: UserInfo,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoomTopicUpdatedService {
|
||||
pub room: RoomInfo,
|
||||
@ -112,6 +97,7 @@ pub struct RoomCreateClient {
|
||||
pub room_name: String,
|
||||
pub public: bool,
|
||||
pub category: Option<Uuid>,
|
||||
pub ai_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -123,6 +109,7 @@ pub struct RoomUpdateClient {
|
||||
pub slowmode_seconds: Option<i32>,
|
||||
pub nsfw: Option<bool>,
|
||||
pub default_auto_archive_duration: Option<i32>,
|
||||
pub ai_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{RoomInfo, UserInfo, message::MessageNewService};
|
||||
|
||||
|
||||
@ -1,159 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{AgentInfo, RoomInfo, ai};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn ai_list(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
room: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let rows = db::sqlx::query_as::<_, (Uuid, Option<String>, Option<String>, Option<Uuid>, bool, bool)>(
|
||||
"SELECT ra.agent_session, s.name, s.agent_kind, s.model_version, ra.enabled, ra.auto_reply \
|
||||
FROM room_ai ra \
|
||||
LEFT JOIN agent_session s ON s.id = ra.agent_session AND s.deleted_at IS NULL \
|
||||
WHERE ra.room = $1",
|
||||
)
|
||||
.bind(room)
|
||||
.fetch_all(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let agents = rows
|
||||
.into_iter()
|
||||
.filter_map(|(agent_session, name, agent_kind, model_version, enabled, auto_reply)| {
|
||||
name.map(|n| ai::RoomAiEntry {
|
||||
agent_session,
|
||||
name: n,
|
||||
agent_kind: agent_kind.unwrap_or_default(),
|
||||
model_version,
|
||||
enabled,
|
||||
auto_reply,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ai_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
Ok(Some(WsOutEvent::AiAgentList {
|
||||
room: ai_room.clone(),
|
||||
data: ai::RoomAiListService {
|
||||
room: ai_room,
|
||||
agents,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn ai_upsert(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
room: Uuid,
|
||||
model: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let session = db::sqlx::query_as::<_, model::agent::AgentSessionModel>(
|
||||
"SELECT id, \"user\", wk, name, description, agent_kind, model_version, \
|
||||
system_prompt, temperature, max_output_tokens, enabled, created_by, \
|
||||
created_at, updated_at, deleted_at \
|
||||
FROM agent_session WHERE id = $1 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(model)
|
||||
.fetch_one(bus.inner.db.reader())
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
db::sqlx::Error::RowNotFound => ChannelError::RoomNotFound,
|
||||
other => ChannelError::Database(other),
|
||||
})?;
|
||||
db::sqlx::query_as::<_, model::room::RoomAiModel>(
|
||||
"INSERT INTO room_ai (room, agent_session, enabled, auto_reply, created_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, true, false, $3, now(), now()) \
|
||||
ON CONFLICT (room, agent_session) DO UPDATE SET enabled = true, updated_at = now() \
|
||||
RETURNING room, agent_session, enabled, auto_reply, created_by, created_at, updated_at",
|
||||
)
|
||||
.bind(room)
|
||||
.bind(model)
|
||||
.bind(user_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let ai_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let data = ai::AiAgentJoinedService {
|
||||
room: ai_room,
|
||||
agent: AgentInfo {
|
||||
id: model,
|
||||
name: session.name.clone(),
|
||||
agent_type: session.agent_kind.clone(),
|
||||
model_name: None,
|
||||
},
|
||||
joined_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "ai.agent_joined", &data)
|
||||
.await?;
|
||||
|
||||
Ok(Some(WsOutEvent::AiAgentJoined { room: data.room.clone(), data }))
|
||||
}
|
||||
|
||||
pub(super) async fn ai_delete(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
room: Uuid,
|
||||
agent_id: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let session = db::sqlx::query_as::<_, model::agent::AgentSessionModel>(
|
||||
"SELECT id, \"user\", wk, name, description, agent_kind, model_version, \
|
||||
system_prompt, temperature, max_output_tokens, enabled, created_by, \
|
||||
created_at, updated_at, deleted_at \
|
||||
FROM agent_session WHERE id = $1 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(agent_id)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let result = db::sqlx::query(
|
||||
"DELETE FROM room_ai WHERE room = $1 AND agent_session = $2",
|
||||
)
|
||||
.bind(room)
|
||||
.bind(agent_id)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(ChannelError::RoomNotFound);
|
||||
}
|
||||
let ai_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let agent_info = session.map(|s| AgentInfo {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
agent_type: s.agent_kind,
|
||||
model_name: None,
|
||||
}).unwrap_or_else(|| AgentInfo::unknown(agent_id));
|
||||
|
||||
let data = ai::AiAgentLeftService {
|
||||
room: ai_room,
|
||||
agent: agent_info,
|
||||
left_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "ai.agent_left", &data).await?;
|
||||
|
||||
Ok(Some(WsOutEvent::AiAgentLeft { room: data.room.clone(), data }))
|
||||
}
|
||||
|
||||
pub(super) async fn ai_stop(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
room: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
bus.publish_room_event(
|
||||
room,
|
||||
"ai.stop",
|
||||
&serde_json::json!({"stopped_by": user_id}),
|
||||
)
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{UserInfo, WorkspaceInfo, ban};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn ban_create(
|
||||
@ -36,9 +36,18 @@ impl WsHandler {
|
||||
});
|
||||
bus.inner.cache.set(&ban_key, &ban_data).await?;
|
||||
let data = ban::BannedService {
|
||||
workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
user: bus.lookup_user(user).await.unwrap_or_else(|_| UserInfo::unknown(user)),
|
||||
banned_by: bus.lookup_user(_user_id).await.unwrap_or_else(|_| UserInfo::unknown(_user_id)),
|
||||
workspace: bus
|
||||
.lookup_workspace(workspace)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
user: bus
|
||||
.lookup_user(user)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user)),
|
||||
banned_by: bus
|
||||
.lookup_user(_user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(_user_id)),
|
||||
reason,
|
||||
expires_at: _expires_at,
|
||||
banned_at: Utc::now(),
|
||||
@ -64,9 +73,18 @@ impl WsHandler {
|
||||
let ban_key = format!("ban:{}:{}:{}", workspace, _user_id, user);
|
||||
bus.inner.cache.remove(&ban_key).await?;
|
||||
let data = ban::UnbannedService {
|
||||
workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
user: bus.lookup_user(user).await.unwrap_or_else(|_| UserInfo::unknown(user)),
|
||||
unbanned_by: bus.lookup_user(_user_id).await.unwrap_or_else(|_| UserInfo::unknown(_user_id)),
|
||||
workspace: bus
|
||||
.lookup_workspace(workspace)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
user: bus
|
||||
.lookup_user(user)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user)),
|
||||
unbanned_by: bus
|
||||
.lookup_user(_user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(_user_id)),
|
||||
unbanned_at: Utc::now(),
|
||||
};
|
||||
bus.workspace_changed(workspace).await?;
|
||||
|
||||
@ -5,8 +5,8 @@ use crate::event::{UserInfo, WorkspaceInfo, category};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::MAX_CATEGORY_NAME_LEN;
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn category_create(
|
||||
@ -17,7 +17,9 @@ impl WsHandler {
|
||||
position: Option<i32>,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
if name.is_empty() || name.len() > MAX_CATEGORY_NAME_LEN {
|
||||
return Err(ChannelError::Validation("invalid category name".into()));
|
||||
return Err(ChannelError::Validation(
|
||||
"invalid category name".into(),
|
||||
));
|
||||
}
|
||||
Self::ensure_workspace_member(bus, user_id, workspace).await?;
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomCategoryModel>(
|
||||
@ -95,7 +97,10 @@ impl WsHandler {
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
bus.workspace_changed(old.wk).await?;
|
||||
Ok(Some(WsOutEvent::CategoryUpdated { workspace: data.project.clone(), data }))
|
||||
Ok(Some(WsOutEvent::CategoryUpdated {
|
||||
workspace: data.project.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn category_delete(
|
||||
@ -133,6 +138,9 @@ impl WsHandler {
|
||||
deleted_at: Utc::now(),
|
||||
};
|
||||
bus.workspace_changed(row.wk).await?;
|
||||
Ok(Some(WsOutEvent::CategoryDeleted { workspace: cd_workspace, data }))
|
||||
Ok(Some(WsOutEvent::CategoryDeleted {
|
||||
workspace: cd_workspace,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, conversation};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn conversation_pin(
|
||||
@ -29,10 +29,14 @@ impl WsHandler {
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room_info = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
if pin {
|
||||
let data = conversation::ConversationPinnedService {
|
||||
@ -40,7 +44,8 @@ impl WsHandler {
|
||||
room: room_info.clone(),
|
||||
pinned_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "conversation.pinned", &data).await?;
|
||||
bus.emit_to_user(user_id, "conversation.pinned", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ConversationPinned {
|
||||
room: room_info,
|
||||
data,
|
||||
@ -51,7 +56,8 @@ impl WsHandler {
|
||||
room: room_info.clone(),
|
||||
unpinned_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "conversation.unpinned", &data).await?;
|
||||
bus.emit_to_user(user_id, "conversation.unpinned", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ConversationUnpinned {
|
||||
room: room_info,
|
||||
data,
|
||||
@ -80,10 +86,14 @@ impl WsHandler {
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room_info = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
if mute {
|
||||
let data = conversation::ConversationMutedService {
|
||||
@ -91,7 +101,8 @@ impl WsHandler {
|
||||
room: room_info.clone(),
|
||||
muted_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "conversation.muted", &data).await?;
|
||||
bus.emit_to_user(user_id, "conversation.muted", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ConversationMuted {
|
||||
room: room_info,
|
||||
data,
|
||||
@ -102,7 +113,8 @@ impl WsHandler {
|
||||
room: room_info.clone(),
|
||||
unmuted_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "conversation.unmuted", &data).await?;
|
||||
bus.emit_to_user(user_id, "conversation.unmuted", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ConversationUnmuted {
|
||||
room: room_info,
|
||||
data,
|
||||
@ -116,7 +128,8 @@ impl WsHandler {
|
||||
notify_level: String,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let valid = matches!(notify_level.as_str(), "all" | "mentions" | "none");
|
||||
let valid =
|
||||
matches!(notify_level.as_str(), "all" | "mentions" | "none");
|
||||
if !valid {
|
||||
return Err(crate::ChannelError::Internal(
|
||||
"notify_level must be 'all', 'mentions', or 'none'".to_string(),
|
||||
@ -147,10 +160,14 @@ impl WsHandler {
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room_info = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
let data = conversation::ConversationNotifyLevelChangedService {
|
||||
user: user_info,
|
||||
@ -208,7 +225,16 @@ impl WsHandler {
|
||||
let summaries: Vec<conversation::ConversationSummary> = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(id, name, room_type, is_pinned, is_muted, notify_level, last_read_seq, max_seq)| {
|
||||
|(
|
||||
id,
|
||||
name,
|
||||
room_type,
|
||||
is_pinned,
|
||||
is_muted,
|
||||
notify_level,
|
||||
last_read_seq,
|
||||
max_seq,
|
||||
)| {
|
||||
let unread = (max_seq - last_read_seq).max(0);
|
||||
conversation::ConversationSummary {
|
||||
room: id,
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{RoomInfo, UserInfo, dm};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn dm_create(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
recipient: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
if user_id == recipient {
|
||||
return Err(crate::ChannelError::Internal(
|
||||
"cannot create DM with yourself".to_string(),
|
||||
));
|
||||
}
|
||||
let recipient_exists: Option<(Uuid,)> = db::sqlx::query_as(
|
||||
"SELECT id FROM \"user\" WHERE id = $1",
|
||||
)
|
||||
.bind(recipient)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?;
|
||||
if recipient_exists.is_none() {
|
||||
return Err(crate::ChannelError::UserNotFound);
|
||||
}
|
||||
let (initiator, other) = if user_id < recipient {
|
||||
(user_id, recipient)
|
||||
} else {
|
||||
(recipient, user_id)
|
||||
};
|
||||
let existing: Option<(Uuid, Uuid, bool)> = db::sqlx::query_as(
|
||||
"SELECT room, initiator, is_closed FROM dm_conversation \
|
||||
WHERE initiator = $1 AND recipient = $2",
|
||||
)
|
||||
.bind(initiator)
|
||||
.bind(other)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let (room_id, is_reopen) = if let Some((room, _, is_closed)) = existing {
|
||||
if is_closed {
|
||||
db::sqlx::query(
|
||||
"UPDATE dm_conversation SET is_closed = false, closed_at = NULL, \
|
||||
updated_at = now() WHERE initiator = $1 AND recipient = $2",
|
||||
)
|
||||
.bind(initiator)
|
||||
.bind(other)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
db::sqlx::query(
|
||||
"UPDATE room SET is_archived = false, updated_at = now() WHERE id = $1",
|
||||
)
|
||||
.bind(room)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
(room, true)
|
||||
} else {
|
||||
(room, false)
|
||||
}
|
||||
} else {
|
||||
let shared_wk: Option<(Uuid,)> = db::sqlx::query_as(
|
||||
"SELECT wm1.wk FROM wk_member wm1 \
|
||||
INNER JOIN wk_member wm2 ON wm2.wk = wm1.wk \
|
||||
WHERE wm1.\"user\" = $1 AND wm1.leave_at IS NULL \
|
||||
AND wm2.\"user\" = $2 AND wm2.leave_at IS NULL \
|
||||
LIMIT 1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(recipient)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let wk = shared_wk.map(|r| r.0).unwrap_or_else(|| {
|
||||
Uuid::nil()
|
||||
});
|
||||
let room_id = Uuid::new_v4();
|
||||
db::sqlx::query(
|
||||
"INSERT INTO room (id, wk, name, topic, room_type, position, is_private, \
|
||||
created_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, NULL, 'DM', 0, true, $4, now(), now())",
|
||||
)
|
||||
.bind(room_id)
|
||||
.bind(wk)
|
||||
.bind(format!("dm-{}", &room_id.to_string()[..8]))
|
||||
.bind(user_id)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
db::sqlx::query(
|
||||
"INSERT INTO dm_conversation (room, initiator, recipient, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, now(), now()) \
|
||||
ON CONFLICT (initiator, recipient) DO NOTHING",
|
||||
)
|
||||
.bind(room_id)
|
||||
.bind(initiator)
|
||||
.bind(other)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
for uid in &[user_id, recipient] {
|
||||
db::sqlx::query(
|
||||
"INSERT INTO room_permission_overwrite \
|
||||
(room, target_type, target_id, allow_mask, deny_mask, created_at) \
|
||||
VALUES ($1, 'user', $2, 0, 0, now()) \
|
||||
ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(room_id)
|
||||
.bind(uid)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
}
|
||||
|
||||
(room_id, false)
|
||||
};
|
||||
let _ = crate::rooms::refresh_user_rooms_cache(
|
||||
&bus.inner.db,
|
||||
&bus.inner.cache,
|
||||
&bus.inner.config,
|
||||
user_id,
|
||||
)
|
||||
.await;
|
||||
let _ = crate::rooms::refresh_user_rooms_cache(
|
||||
&bus.inner.db,
|
||||
&bus.inner.cache,
|
||||
&bus.inner.config,
|
||||
recipient,
|
||||
)
|
||||
.await;
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room_id).await.unwrap_or_else(|_| RoomInfo::unknown(room_id));
|
||||
let initiator_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let recipient_info =
|
||||
bus.lookup_user(recipient).await.unwrap_or_else(|_| UserInfo::unknown(recipient));
|
||||
|
||||
if is_reopen {
|
||||
let data = dm::DmReopenedService {
|
||||
room: room_info.clone(),
|
||||
reopened_by: initiator_info,
|
||||
reopened_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "dm.reopened", &data).await?;
|
||||
bus.emit_to_user(recipient, "dm.reopened", &data).await?;
|
||||
Ok(Some(WsOutEvent::DmReopened {
|
||||
room: room_info,
|
||||
data,
|
||||
}))
|
||||
} else {
|
||||
let data = dm::DmCreatedService {
|
||||
room: room_info.clone(),
|
||||
initiator: initiator_info,
|
||||
recipient: recipient_info,
|
||||
created_at: now,
|
||||
};
|
||||
bus.emit_to_user(user_id, "dm.created", &data).await?;
|
||||
bus.emit_to_user(recipient, "dm.created", &data).await?;
|
||||
Ok(Some(WsOutEvent::DmCreated {
|
||||
room: room_info,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
pub(super) async fn dm_close(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
room: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let now = Utc::now();
|
||||
|
||||
let result = db::sqlx::query(
|
||||
"UPDATE dm_conversation SET is_closed = true, closed_at = $1, updated_at = $1 \
|
||||
WHERE room = $2 AND (initiator = $3 OR recipient = $3) AND is_closed = false",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(room)
|
||||
.bind(user_id)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let closed_by =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
let data = dm::DmClosedService {
|
||||
room: room_info.clone(),
|
||||
closed_by,
|
||||
closed_at: now,
|
||||
};
|
||||
bus.publish_room_event(room, "dm.closed", &data).await?;
|
||||
Ok(Some(WsOutEvent::DmClosed {
|
||||
room: room_info,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
pub(super) async fn dm_list(
|
||||
bus: &ChannelBus,
|
||||
user_id: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let rows = db::sqlx::query_as::<_, (Uuid, Uuid, Uuid, chrono::DateTime<Utc>)>(
|
||||
"SELECT dc.room, dc.initiator, dc.recipient, dc.created_at \
|
||||
FROM dm_conversation dc \
|
||||
INNER JOIN room r ON r.id = dc.room \
|
||||
WHERE (dc.initiator = $1 OR dc.recipient = $1) \
|
||||
AND dc.is_closed = false \
|
||||
AND r.deleted_at IS NULL \
|
||||
ORDER BY dc.updated_at DESC",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let mut results = Vec::with_capacity(rows.len());
|
||||
for (room_id, initiator_id, recipient_id, created_at) in rows {
|
||||
let room_info = bus
|
||||
.lookup_room(room_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room_id));
|
||||
let initiator_info = bus
|
||||
.lookup_user(initiator_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(initiator_id));
|
||||
let recipient_info = bus
|
||||
.lookup_user(recipient_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(recipient_id));
|
||||
|
||||
results.push(dm::DmCreatedService {
|
||||
room: room_info,
|
||||
initiator: initiator_info,
|
||||
recipient: recipient_info,
|
||||
created_at,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(WsOutEvent::DmList { data: results }))
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,9 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, draft};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::{MAX_TEXT_LEN};
|
||||
use super::WsOutEvent;
|
||||
use super::MAX_TEXT_LEN;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn draft_save(
|
||||
@ -21,15 +21,24 @@ impl WsHandler {
|
||||
}
|
||||
let key = format!("draft:{}:{}", user_id, room);
|
||||
bus.inner.cache.set(&key, &content).await?;
|
||||
let ds_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ds_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let ds_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ds_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = draft::DraftSavedService {
|
||||
user: ds_user,
|
||||
room: ds_room,
|
||||
content,
|
||||
saved_at: Utc::now(),
|
||||
};
|
||||
Ok(Some(WsOutEvent::DraftSaved { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::DraftSaved {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn draft_clear(
|
||||
@ -40,13 +49,22 @@ impl WsHandler {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let key = format!("draft:{}:{}", user_id, room);
|
||||
bus.inner.cache.remove(&key).await?;
|
||||
let dc_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let dc_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let dc_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let dc_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = draft::DraftClearedService {
|
||||
user: dc_user,
|
||||
room: dc_room,
|
||||
cleared_at: Utc::now(),
|
||||
};
|
||||
Ok(Some(WsOutEvent::DraftCleared { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::DraftCleared {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, forward};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn message_forward(
|
||||
@ -63,11 +63,8 @@ impl WsHandler {
|
||||
let fwd_content_type = row.content_type.clone();
|
||||
let fwd_created_at = row.created_at;
|
||||
|
||||
bus.publish_room_message(
|
||||
row,
|
||||
Some(bus.lookup_user(user_id).await?),
|
||||
)
|
||||
.await?;
|
||||
bus.publish_room_message(row, Some(bus.lookup_user(user_id).await?))
|
||||
.await?;
|
||||
|
||||
let data = forward::MessageForwardedService {
|
||||
id: fwd_id,
|
||||
|
||||
@ -59,10 +59,13 @@ impl WsHandler {
|
||||
.fetch_all(bus.inner.db.reader())
|
||||
.await?;
|
||||
|
||||
let user_ids: Vec<Uuid> = rows.iter().map(|(_, _, user)| *user).collect();
|
||||
let user_ids: Vec<Uuid> =
|
||||
rows.iter().map(|(_, _, user)| *user).collect();
|
||||
let users = bus.lookup_users(&user_ids).await.unwrap_or_default();
|
||||
let mut grouped: HashMap<Uuid, HashMap<String, reaction::ReactionGroup>> =
|
||||
HashMap::new();
|
||||
let mut grouped: HashMap<
|
||||
Uuid,
|
||||
HashMap<String, reaction::ReactionGroup>,
|
||||
> = HashMap::new();
|
||||
|
||||
for (message_id, emoji, reactor) in rows {
|
||||
let group = grouped
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, WorkspaceInfo, invite};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn invite_create(
|
||||
@ -29,16 +29,29 @@ impl WsHandler {
|
||||
"expires_at": _expires_at,
|
||||
});
|
||||
bus.inner.cache.set(&id_key, &meta.to_string()).await?;
|
||||
bus.inner.cache.set(&code_key, &invite_id.to_string()).await?;
|
||||
bus.inner
|
||||
.cache
|
||||
.set(&code_key, &invite_id.to_string())
|
||||
.await?;
|
||||
let inv_room = match _room {
|
||||
Some(r) => Some(bus.lookup_room(r).await.unwrap_or_else(|_| RoomInfo::unknown(r))),
|
||||
Some(r) => Some(
|
||||
bus.lookup_room(r)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(r)),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let data = invite::InviteCreatedService {
|
||||
id: invite_id,
|
||||
workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
workspace: bus
|
||||
.lookup_workspace(workspace)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
room: inv_room,
|
||||
inviter: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
inviter: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
invitee: None,
|
||||
code,
|
||||
max_uses: _max_uses,
|
||||
@ -54,7 +67,8 @@ impl WsHandler {
|
||||
code: String,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let code_key = format!("invite:code:{}", code);
|
||||
let invite_id_str: Option<String> = bus.inner.cache.get(&code_key).await?;
|
||||
let invite_id_str: Option<String> =
|
||||
bus.inner.cache.get(&code_key).await?;
|
||||
let invite_id = invite_id_str
|
||||
.as_deref()
|
||||
.and_then(|s| Uuid::parse_str(s).ok())
|
||||
@ -90,9 +104,15 @@ impl WsHandler {
|
||||
bus.inner.cache.remove(&id_key).await?;
|
||||
let data = invite::InviteAcceptedService {
|
||||
id: Uuid::now_v7(),
|
||||
workspace: bus.lookup_workspace(wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(wk)),
|
||||
workspace: bus
|
||||
.lookup_workspace(wk)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(wk)),
|
||||
room: None,
|
||||
user: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
user: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
accepted_at: Utc::now(),
|
||||
};
|
||||
bus.workspace_changed(wk).await?;
|
||||
|
||||
@ -7,9 +7,9 @@ use crate::{
|
||||
pagination::{MessagePagination, PaginationDirection, PaginationParams},
|
||||
};
|
||||
|
||||
use super::{MAX_MESSAGES_PER_REQUEST, MAX_TEXT_LEN};
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
use super::{MAX_MESSAGES_PER_REQUEST, MAX_TEXT_LEN};
|
||||
|
||||
impl WsHandler {
|
||||
/// Count non-deleted sibling replies to the same parent message.
|
||||
@ -124,16 +124,21 @@ impl WsHandler {
|
||||
|
||||
// ── Auto-thread logic ──────────────────────────────────────────
|
||||
let mut events: Vec<WsOutEvent> = Vec::new();
|
||||
let effective_thread: Option<Uuid> = if let Some(ref parent_id) = in_reply_to {
|
||||
let effective_thread: Option<Uuid> = if let Some(ref parent_id) =
|
||||
in_reply_to
|
||||
{
|
||||
if thread.is_some() {
|
||||
thread
|
||||
} else {
|
||||
let existing = Self::find_thread_in_chain(bus, *parent_id).await?;
|
||||
let existing =
|
||||
Self::find_thread_in_chain(bus, *parent_id).await?;
|
||||
if let Some(tid) = existing {
|
||||
Some(tid)
|
||||
} else {
|
||||
let sibling_count = Self::count_sibling_replies(bus, *parent_id).await?;
|
||||
let (root_id, root_seq, chain_depth) = Self::reply_chain_info(bus, *parent_id).await?;
|
||||
let sibling_count =
|
||||
Self::count_sibling_replies(bus, *parent_id).await?;
|
||||
let (root_id, root_seq, chain_depth) =
|
||||
Self::reply_chain_info(bus, *parent_id).await?;
|
||||
let should_create = sibling_count >= 3 || chain_depth >= 5;
|
||||
|
||||
if should_create {
|
||||
@ -152,11 +157,20 @@ impl WsHandler {
|
||||
.await?;
|
||||
|
||||
let new_thread_id = thread_row.id;
|
||||
Self::attach_chain_to_thread(bus, *parent_id, new_thread_id).await?;
|
||||
Self::attach_chain_to_thread(
|
||||
bus,
|
||||
*parent_id,
|
||||
new_thread_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tc_room = bus.lookup_room(room).await
|
||||
let tc_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let created_by = bus.lookup_user(user_id).await
|
||||
let created_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = thread::ThreadCreatedService {
|
||||
id: new_thread_id,
|
||||
@ -166,7 +180,8 @@ impl WsHandler {
|
||||
participants: serde_json::Value::Null,
|
||||
created_at: thread_row.created_at,
|
||||
};
|
||||
bus.publish_room_event(room, "thread.created", &data).await?;
|
||||
bus.publish_room_event(room, "thread.created", &data)
|
||||
.await?;
|
||||
events.push(WsOutEvent::ThreadCreated {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
@ -227,11 +242,11 @@ impl WsHandler {
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
bus.publish_room_message(
|
||||
row.clone(),
|
||||
Some(sender),
|
||||
).await?;
|
||||
let msg_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
bus.publish_room_message(row.clone(), Some(sender)).await?;
|
||||
let msg_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
events.push(WsOutEvent::MessageNew {
|
||||
room: msg_room.clone(),
|
||||
data: message::MessageNewService {
|
||||
@ -254,7 +269,9 @@ impl WsHandler {
|
||||
},
|
||||
});
|
||||
|
||||
Ok(events.into_iter().find(|e| matches!(e, WsOutEvent::MessageNew { .. })))
|
||||
Ok(events
|
||||
.into_iter()
|
||||
.find(|e| matches!(e, WsOutEvent::MessageNew { .. })))
|
||||
}
|
||||
|
||||
pub(super) async fn message_update(
|
||||
@ -302,8 +319,14 @@ impl WsHandler {
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
|
||||
let sender = bus.lookup_user(row.author).await.unwrap_or_else(|_| UserInfo::unknown(row.author));
|
||||
let room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let sender = bus
|
||||
.lookup_user(row.author)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(row.author));
|
||||
let room = bus
|
||||
.lookup_room(row.room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let data = message::MessageEditedService {
|
||||
id: row.id,
|
||||
seq: row.seq,
|
||||
@ -353,8 +376,14 @@ impl WsHandler {
|
||||
.bind(message_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let revoked_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let revoked_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room = bus
|
||||
.lookup_room(row.room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let data = message::MessageRevokedService {
|
||||
id: row.id,
|
||||
seq: row.seq,
|
||||
@ -418,20 +447,28 @@ impl WsHandler {
|
||||
.await?;
|
||||
|
||||
let mut page_messages = page.messages;
|
||||
if before_seq.is_some() || (before_seq.is_none() && after_seq.is_none()) {
|
||||
if before_seq.is_some() || (before_seq.is_none() && after_seq.is_none())
|
||||
{
|
||||
page_messages.reverse();
|
||||
}
|
||||
let message_ids: Vec<Uuid> = page_messages.iter().map(|m| m.id).collect();
|
||||
let reactions = Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let message_ids: Vec<Uuid> =
|
||||
page_messages.iter().map(|m| m.id).collect();
|
||||
let reactions =
|
||||
Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let list_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let list_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
|
||||
let mut messages: Vec<message::MessageNewService> =
|
||||
Vec::with_capacity(page_messages.len());
|
||||
for m in page_messages {
|
||||
let sender = bus.lookup_user(m.sender_id).await
|
||||
let sender = bus
|
||||
.lookup_user(m.sender_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(m.sender_id));
|
||||
messages.push(message::MessageNewService {
|
||||
id: m.id,
|
||||
@ -534,10 +571,14 @@ impl WsHandler {
|
||||
let author_ids: Vec<Uuid> = rows.iter().map(|r| r.author).collect();
|
||||
let user_map = bus.lookup_users(&author_ids).await.unwrap_or_default();
|
||||
let message_ids: Vec<Uuid> = rows.iter().map(|r| r.id).collect();
|
||||
let reactions = Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
let reactions =
|
||||
Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let around_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let around_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let messages = rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
@ -561,7 +602,10 @@ impl WsHandler {
|
||||
thinking_content: None,
|
||||
thinking_is_chunked: None,
|
||||
send_at: r.created_at,
|
||||
reactions: reactions.get(&r.id).cloned().unwrap_or_default(),
|
||||
reactions: reactions
|
||||
.get(&r.id)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@ -591,13 +635,19 @@ impl WsHandler {
|
||||
.reconnect
|
||||
.get_missed_messages(room, after_seq)
|
||||
.await?;
|
||||
let author_ids: Vec<Uuid> = messages.iter().map(|m| m.sender_id).collect();
|
||||
let message_ids: Vec<Uuid> = messages.iter().map(|m| m.message_id).collect();
|
||||
let author_ids: Vec<Uuid> =
|
||||
messages.iter().map(|m| m.sender_id).collect();
|
||||
let message_ids: Vec<Uuid> =
|
||||
messages.iter().map(|m| m.message_id).collect();
|
||||
let user_map = bus.lookup_users(&author_ids).await.unwrap_or_default();
|
||||
let reactions = Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
let reactions =
|
||||
Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let missed_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let missed_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let messages = messages
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
@ -622,7 +672,10 @@ impl WsHandler {
|
||||
thinking_content: None,
|
||||
thinking_is_chunked: None,
|
||||
send_at: m.send_at,
|
||||
reactions: reactions.get(&m.message_id).cloned().unwrap_or_default(),
|
||||
reactions: reactions
|
||||
.get(&m.message_id)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, message_read};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn message_mark_read(
|
||||
@ -60,10 +60,14 @@ impl WsHandler {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let reader_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room_info = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let reader_info = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
let data = message_read::MessageReadBatchService {
|
||||
room: room_info.clone(),
|
||||
@ -72,7 +76,8 @@ impl WsHandler {
|
||||
reader: reader_info,
|
||||
read_at: now,
|
||||
};
|
||||
bus.publish_room_event(room, "message.read_batch", &data).await?;
|
||||
bus.publish_room_event(room, "message.read_batch", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::MessageReadBatch {
|
||||
room: room_info,
|
||||
data,
|
||||
|
||||
@ -12,27 +12,25 @@ pub(crate) const MAX_CATEGORY_NAME_LEN: usize = 50;
|
||||
|
||||
mod helpers;
|
||||
|
||||
mod subscription;
|
||||
mod message;
|
||||
mod room;
|
||||
mod category;
|
||||
mod reaction;
|
||||
mod thread;
|
||||
mod pin;
|
||||
mod draft;
|
||||
mod notification;
|
||||
mod presence;
|
||||
mod invite;
|
||||
mod ban;
|
||||
mod voice;
|
||||
mod ai;
|
||||
mod search;
|
||||
mod user;
|
||||
mod category;
|
||||
mod conversation;
|
||||
mod dm;
|
||||
mod draft;
|
||||
mod forward;
|
||||
mod invite;
|
||||
mod message;
|
||||
mod message_read;
|
||||
mod notification;
|
||||
mod pin;
|
||||
mod presence;
|
||||
mod reaction;
|
||||
mod room;
|
||||
mod search;
|
||||
mod star;
|
||||
mod subscription;
|
||||
mod thread;
|
||||
mod user;
|
||||
mod voice;
|
||||
|
||||
pub struct WsHandler;
|
||||
|
||||
@ -73,8 +71,14 @@ impl WsHandler {
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::MessageAround { room, seq, limit, thread } => {
|
||||
Self::message_around(bus, user_id, room, seq, limit, thread).await
|
||||
WsInMessage::MessageAround {
|
||||
room,
|
||||
seq,
|
||||
limit,
|
||||
thread,
|
||||
} => {
|
||||
Self::message_around(bus, user_id, room, seq, limit, thread)
|
||||
.await
|
||||
}
|
||||
WsInMessage::MessageCreate {
|
||||
room,
|
||||
@ -108,9 +112,11 @@ impl WsHandler {
|
||||
room_name,
|
||||
public,
|
||||
category,
|
||||
ai_enabled,
|
||||
} => {
|
||||
Self::room_create(
|
||||
bus, user_id, workspace, room_name, public, category,
|
||||
ai_enabled,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -119,7 +125,13 @@ impl WsHandler {
|
||||
room_name,
|
||||
public,
|
||||
category,
|
||||
} => Self::room_update(bus, user_id, room, room_name, public, category).await,
|
||||
ai_enabled,
|
||||
} => {
|
||||
Self::room_update(
|
||||
bus, user_id, room, room_name, public, category, ai_enabled,
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::RoomDelete { room } => {
|
||||
Self::room_delete(bus, user_id, room).await
|
||||
}
|
||||
@ -127,7 +139,10 @@ impl WsHandler {
|
||||
workspace,
|
||||
name,
|
||||
position,
|
||||
} => Self::category_create(bus, user_id, workspace, name, position).await,
|
||||
} => {
|
||||
Self::category_create(bus, user_id, workspace, name, position)
|
||||
.await
|
||||
}
|
||||
WsInMessage::CategoryUpdate { id, name, position } => {
|
||||
Self::category_update(bus, user_id, id, name, position).await
|
||||
}
|
||||
@ -160,7 +175,11 @@ impl WsHandler {
|
||||
dnd_end_hour,
|
||||
} => {
|
||||
Self::dnd_update(
|
||||
bus, user_id, room, do_not_disturb, dnd_start_hour,
|
||||
bus,
|
||||
user_id,
|
||||
room,
|
||||
do_not_disturb,
|
||||
dnd_start_hour,
|
||||
dnd_end_hour,
|
||||
)
|
||||
.await
|
||||
@ -205,7 +224,8 @@ impl WsHandler {
|
||||
Self::notification_mark_read(bus, user_id, id).await
|
||||
}
|
||||
WsInMessage::NotificationMarkAllRead { workspace_id } => {
|
||||
Self::notification_mark_all_read(bus, user_id, workspace_id).await
|
||||
Self::notification_mark_all_read(bus, user_id, workspace_id)
|
||||
.await
|
||||
}
|
||||
WsInMessage::NotificationArchive { id } => {
|
||||
Self::notification_archive(bus, user_id, id).await
|
||||
@ -218,8 +238,10 @@ impl WsHandler {
|
||||
text,
|
||||
expires_at,
|
||||
} => {
|
||||
Self::custom_status_update(bus, user_id, emoji, text, expires_at)
|
||||
.await
|
||||
Self::custom_status_update(
|
||||
bus, user_id, emoji, text, expires_at,
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::InviteCreate {
|
||||
workspace,
|
||||
@ -227,8 +249,10 @@ impl WsHandler {
|
||||
max_uses,
|
||||
expires_at,
|
||||
} => {
|
||||
Self::invite_create(bus, user_id, workspace, room, max_uses, expires_at)
|
||||
.await
|
||||
Self::invite_create(
|
||||
bus, user_id, workspace, room, max_uses, expires_at,
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::InviteAccept { code } => {
|
||||
Self::invite_accept(bus, user_id, code).await
|
||||
@ -242,8 +266,10 @@ impl WsHandler {
|
||||
reason,
|
||||
expires_at,
|
||||
} => {
|
||||
Self::ban_create(bus, user_id, workspace, user, reason, expires_at)
|
||||
.await
|
||||
Self::ban_create(
|
||||
bus, user_id, workspace, user, reason, expires_at,
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::BanRemove { workspace, user } => {
|
||||
Self::ban_remove(bus, user_id, workspace, user).await
|
||||
@ -263,18 +289,6 @@ impl WsHandler {
|
||||
WsInMessage::ScreenShare { room, start } => {
|
||||
Self::screen_share(bus, user_id, room, start).await
|
||||
}
|
||||
WsInMessage::AiList { room } => {
|
||||
Self::ai_list(bus, user_id, room).await
|
||||
}
|
||||
WsInMessage::AiUpsert { room, model } => {
|
||||
Self::ai_upsert(bus, user_id, room, model).await
|
||||
}
|
||||
WsInMessage::AiDelete { room, agent_id } => {
|
||||
Self::ai_delete(bus, user_id, room, agent_id).await
|
||||
}
|
||||
WsInMessage::AiStop { room } => {
|
||||
Self::ai_stop(bus, user_id, room).await
|
||||
}
|
||||
WsInMessage::UserSummary { username } => {
|
||||
Self::user_summary(bus, username).await
|
||||
}
|
||||
@ -291,29 +305,20 @@ impl WsHandler {
|
||||
WsInMessage::ConversationMute { room, mute } => {
|
||||
Self::conversation_mute(bus, user_id, room, mute).await
|
||||
}
|
||||
WsInMessage::ConversationNotifyLevel {
|
||||
room,
|
||||
notify_level,
|
||||
} => {
|
||||
WsInMessage::ConversationNotifyLevel { room, notify_level } => {
|
||||
Self::conversation_notify_level(
|
||||
bus, user_id, room, notify_level,
|
||||
bus,
|
||||
user_id,
|
||||
room,
|
||||
notify_level,
|
||||
)
|
||||
.await
|
||||
}
|
||||
WsInMessage::ConversationList => {
|
||||
Self::conversation_list(bus, user_id).await
|
||||
}
|
||||
WsInMessage::DmCreate { recipient } => {
|
||||
Self::dm_create(bus, user_id, recipient).await
|
||||
}
|
||||
WsInMessage::DmClose { room } => {
|
||||
Self::dm_close(bus, user_id, room).await
|
||||
}
|
||||
WsInMessage::DmList => Self::dm_list(bus, user_id).await,
|
||||
WsInMessage::MessageMarkRead {
|
||||
room,
|
||||
message_ids,
|
||||
} => {
|
||||
|
||||
WsInMessage::MessageMarkRead { room, message_ids } => {
|
||||
Self::message_mark_read(bus, user_id, room, message_ids).await
|
||||
}
|
||||
WsInMessage::MessageGetReaders { message_id } => {
|
||||
@ -331,8 +336,13 @@ impl WsHandler {
|
||||
source_message_id,
|
||||
target_room,
|
||||
} => {
|
||||
Self::message_forward(bus, user_id, source_message_id, target_room)
|
||||
.await
|
||||
Self::message_forward(
|
||||
bus,
|
||||
user_id,
|
||||
source_message_id,
|
||||
target_room,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{UserInfo, notify};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn notification_mark_read(
|
||||
@ -24,7 +24,10 @@ impl WsHandler {
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(ChannelError::RoomNotFound);
|
||||
}
|
||||
let nr_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let nr_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = notify::NotifyReadService {
|
||||
id,
|
||||
user: nr_user,
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, pin};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn pin_add(
|
||||
@ -35,8 +35,14 @@ impl WsHandler {
|
||||
.bind(message)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
let pa_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let pinned_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let pa_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let pinned_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = pin::PinAddedService {
|
||||
room: pa_room,
|
||||
message,
|
||||
@ -44,7 +50,10 @@ impl WsHandler {
|
||||
pinned_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "pin.added", &data).await?;
|
||||
Ok(Some(WsOutEvent::PinAdded { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::PinAdded {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn pin_remove(
|
||||
@ -69,8 +78,14 @@ impl WsHandler {
|
||||
.bind(message)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
let pr_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let removed_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let pr_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let removed_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = pin::PinRemovedService {
|
||||
room: pr_room,
|
||||
message,
|
||||
@ -78,6 +93,9 @@ impl WsHandler {
|
||||
removed_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "pin.removed", &data).await?;
|
||||
Ok(Some(WsOutEvent::PinRemoved { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::PinRemoved {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, member, presence};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn dnd_update(
|
||||
@ -27,8 +27,14 @@ impl WsHandler {
|
||||
"dnd_end_hour": end_hour,
|
||||
});
|
||||
bus.inner.cache.set(&key, &dnd_data).await?;
|
||||
let dnd_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let dnd_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let dnd_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let dnd_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = member::DndUpdatedService {
|
||||
room: dnd_room,
|
||||
user: dnd_user,
|
||||
@ -36,7 +42,8 @@ impl WsHandler {
|
||||
dnd_start_hour: start_hour,
|
||||
dnd_end_hour: end_hour,
|
||||
};
|
||||
bus.publish_room_event(room, "member.dnd_updated", &data).await?;
|
||||
bus.publish_room_event(room, "member.dnd_updated", &data)
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@ -45,7 +52,10 @@ impl WsHandler {
|
||||
user_id: Uuid,
|
||||
status: presence::UserPresenceStatus,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let pc_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let pc_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = presence::PresenceChangedService {
|
||||
user: pc_user,
|
||||
project: None,
|
||||
@ -60,7 +70,8 @@ impl WsHandler {
|
||||
)
|
||||
.await?;
|
||||
for room in rooms {
|
||||
bus.publish_room_event(room, "presence.changed", &data).await?;
|
||||
bus.publish_room_event(room, "presence.changed", &data)
|
||||
.await?;
|
||||
}
|
||||
Ok(Some(WsOutEvent::PresenceChanged { data }))
|
||||
}
|
||||
@ -72,7 +83,10 @@ impl WsHandler {
|
||||
text: Option<String>,
|
||||
expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let cs_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let cs_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = presence::CustomStatusUpdatedService {
|
||||
user: cs_user,
|
||||
emoji,
|
||||
@ -87,7 +101,8 @@ impl WsHandler {
|
||||
)
|
||||
.await?;
|
||||
for room in rooms {
|
||||
bus.publish_room_event(room, "custom_status.updated", &data).await?;
|
||||
bus.publish_room_event(room, "custom_status.updated", &data)
|
||||
.await?;
|
||||
}
|
||||
Ok(Some(WsOutEvent::CustomStatusUpdated { data }))
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, reaction};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn reaction_add(
|
||||
@ -39,7 +39,10 @@ impl WsHandler {
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let rct_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let rct_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let data = reaction::ReactionAddedService {
|
||||
id: Uuid::now_v7(),
|
||||
room: rct_room,
|
||||
@ -48,8 +51,12 @@ impl WsHandler {
|
||||
emoji,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "reaction.added", &data).await?;
|
||||
Ok(Some(WsOutEvent::ReactionAdded { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(room, "reaction.added", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ReactionAdded {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn reaction_remove(
|
||||
@ -76,7 +83,10 @@ impl WsHandler {
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let rct_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let rct_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let data = reaction::ReactionRemovedService {
|
||||
id: Uuid::now_v7(),
|
||||
room: rct_room,
|
||||
@ -85,7 +95,11 @@ impl WsHandler {
|
||||
emoji,
|
||||
removed_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "reaction.removed", &data).await?;
|
||||
Ok(Some(WsOutEvent::ReactionRemoved { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(room, "reaction.removed", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ReactionRemoved {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, WorkspaceInfo, member, rooms};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::{MAX_ROOM_NAME_LEN};
|
||||
use super::WsOutEvent;
|
||||
use super::MAX_ROOM_NAME_LEN;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn room_get(
|
||||
@ -17,7 +17,7 @@ impl WsHandler {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
|
||||
"SELECT id, wk, parent, name, topic, room_type, position, \
|
||||
is_private, is_archived, created_by, created_at, updated_at, deleted_at \
|
||||
is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at \
|
||||
FROM room WHERE id = $1 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(room)
|
||||
@ -33,6 +33,7 @@ impl WsHandler {
|
||||
"room_type": row.room_type,
|
||||
"is_private": row.is_private,
|
||||
"is_archived": row.is_archived,
|
||||
"ai_enabled": row.ai_enabled,
|
||||
"parent": row.parent,
|
||||
"created_by": row.created_by,
|
||||
"created_at": row.created_at,
|
||||
@ -47,22 +48,25 @@ impl WsHandler {
|
||||
room_name: String,
|
||||
public: bool,
|
||||
category: Option<Uuid>,
|
||||
ai_enabled: Option<bool>,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
if room_name.is_empty() || room_name.len() > MAX_ROOM_NAME_LEN {
|
||||
return Err(ChannelError::Validation("invalid room name".into()));
|
||||
}
|
||||
Self::ensure_workspace_member(bus, user_id, workspace).await?;
|
||||
let is_private = !public;
|
||||
let ai = ai_enabled.unwrap_or(false);
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
|
||||
"INSERT INTO room (wk, parent, name, room_type, is_private, created_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, 'channel', $4, $5, now(), now()) \
|
||||
"INSERT INTO room (wk, parent, name, room_type, is_private, ai_enabled, created_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, 'channel', $4, $5, $6, now(), now()) \
|
||||
RETURNING id, wk, parent, name, topic, room_type, position, \
|
||||
is_private, is_archived, created_by, created_at, updated_at, deleted_at",
|
||||
is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at",
|
||||
)
|
||||
.bind(workspace)
|
||||
.bind(category)
|
||||
.bind(&room_name)
|
||||
.bind(is_private)
|
||||
.bind(ai)
|
||||
.bind(user_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
@ -77,15 +81,25 @@ impl WsHandler {
|
||||
.await?;
|
||||
let data = rooms::RoomCreatedService {
|
||||
room: RoomInfo::from_model(&row),
|
||||
workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
workspace: bus
|
||||
.lookup_workspace(workspace)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
|
||||
public,
|
||||
category,
|
||||
created_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
created_by: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
created_at: row.created_at,
|
||||
};
|
||||
bus.publish_room_event(row.id, "room.created", &data).await?;
|
||||
bus.publish_room_event(row.id, "room.created", &data)
|
||||
.await?;
|
||||
bus.room_changed(row.id).await?;
|
||||
Ok(Some(WsOutEvent::RoomCreated { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::RoomCreated {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn room_update(
|
||||
@ -95,56 +109,74 @@ impl WsHandler {
|
||||
room_name: Option<String>,
|
||||
public: Option<bool>,
|
||||
category: Option<Uuid>,
|
||||
ai_enabled: Option<bool>,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let old = db::sqlx::query_as::<_, model::room::RoomModel>(
|
||||
"SELECT id, wk, parent, name, topic, room_type, position, \
|
||||
is_private, is_archived, created_by, created_at, updated_at, deleted_at \
|
||||
is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at \
|
||||
FROM room WHERE id = $1 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(room)
|
||||
.fetch_one(bus.inner.db.reader())
|
||||
.await?;
|
||||
let new_name = room_name.unwrap_or(old.name.clone());
|
||||
let new_private =
|
||||
public.map(|p| !p).unwrap_or(old.is_private);
|
||||
let new_private = public.map(|p| !p).unwrap_or(old.is_private);
|
||||
let new_category = category.or(old.parent);
|
||||
let new_ai = ai_enabled.unwrap_or(old.ai_enabled);
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
|
||||
"UPDATE room SET name = $2, is_private = $3, parent = $4, updated_at = now() \
|
||||
"UPDATE room SET name = $2, is_private = $3, parent = $4, ai_enabled = $5, updated_at = now() \
|
||||
WHERE id = $1 AND deleted_at IS NULL \
|
||||
RETURNING id, wk, parent, name, topic, room_type, position, \
|
||||
is_private, is_archived, created_by, created_at, updated_at, deleted_at",
|
||||
is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at",
|
||||
)
|
||||
.bind(room)
|
||||
.bind(&new_name)
|
||||
.bind(new_private)
|
||||
.bind(new_category)
|
||||
.bind(new_ai)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let mut renamed = false;
|
||||
if new_name != old.name {
|
||||
let data = rooms::RoomRenamedService {
|
||||
room: RoomInfo::from_model(&row),
|
||||
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
workspace: bus
|
||||
.lookup_workspace(row.wk)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
old_name: old.name.clone(),
|
||||
new_name: new_name,
|
||||
renamed_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
renamed_by: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
renamed_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "room.renamed", &data).await?;
|
||||
renamed = true;
|
||||
}
|
||||
if new_private != old.is_private || new_category != old.parent {
|
||||
if new_private != old.is_private
|
||||
|| new_category != old.parent
|
||||
|| new_ai != old.ai_enabled
|
||||
{
|
||||
let data = rooms::RoomSettingsUpdatedService {
|
||||
room: RoomInfo::from_model(&row),
|
||||
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
workspace: bus
|
||||
.lookup_workspace(row.wk)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
slowmode_seconds: None,
|
||||
nsfw: false,
|
||||
default_auto_archive_duration: None,
|
||||
updated_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
updated_by: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "room.settings_updated", &data).await?;
|
||||
bus.publish_room_event(room, "room.settings_updated", &data)
|
||||
.await?;
|
||||
}
|
||||
bus.room_changed(room).await?;
|
||||
if renamed {
|
||||
@ -152,15 +184,30 @@ impl WsHandler {
|
||||
room: RoomInfo::from_model(&row),
|
||||
data: rooms::RoomRenamedService {
|
||||
room: RoomInfo::from_model(&row),
|
||||
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
workspace: bus
|
||||
.lookup_workspace(row.wk)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
old_name: old.name,
|
||||
new_name: row.name,
|
||||
renamed_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
renamed_by: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
renamed_at: Utc::now(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
Ok(Some(WsOutEvent::Response {
|
||||
request_id: Uuid::nil(),
|
||||
data: serde_json::json!({
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"is_private": row.is_private,
|
||||
"ai_enabled": row.ai_enabled,
|
||||
"parent": row.parent,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn room_delete(
|
||||
@ -191,13 +238,22 @@ impl WsHandler {
|
||||
.await?;
|
||||
let data = rooms::RoomDeletedService {
|
||||
room: RoomInfo::from_model(&row),
|
||||
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
deleted_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
workspace: bus
|
||||
.lookup_workspace(row.wk)
|
||||
.await
|
||||
.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
|
||||
deleted_by: bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id)),
|
||||
deleted_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "room.deleted", &data).await?;
|
||||
bus.room_changed(room).await?;
|
||||
Ok(Some(WsOutEvent::RoomDeleted { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::RoomDeleted {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn access_grant(
|
||||
@ -217,8 +273,14 @@ impl WsHandler {
|
||||
.bind(target_user)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
let mj_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let mj_user = bus.lookup_user(target_user).await.unwrap_or_else(|_| UserInfo::unknown(target_user));
|
||||
let mj_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let mj_user = bus
|
||||
.lookup_user(target_user)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(target_user));
|
||||
let data = member::MemberJoinedService {
|
||||
room: mj_room,
|
||||
user: mj_user,
|
||||
@ -227,7 +289,10 @@ impl WsHandler {
|
||||
};
|
||||
bus.publish_room_event(room, "member.joined", &data).await?;
|
||||
bus.room_changed(room).await?;
|
||||
Ok(Some(WsOutEvent::MemberJoined { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::MemberJoined {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn access_revoke(
|
||||
@ -245,17 +310,30 @@ impl WsHandler {
|
||||
.bind(target_user)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
let mr_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let mr_target = bus.lookup_user(target_user).await.unwrap_or_else(|_| UserInfo::unknown(target_user));
|
||||
let mr_remover = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let mr_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let mr_target = bus
|
||||
.lookup_user(target_user)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(target_user));
|
||||
let mr_remover = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = member::MemberRemovedService {
|
||||
room: mr_room,
|
||||
user: mr_target,
|
||||
removed_by: mr_remover,
|
||||
removed_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "member.removed", &data).await?;
|
||||
bus.publish_room_event(room, "member.removed", &data)
|
||||
.await?;
|
||||
bus.room_changed(room).await?;
|
||||
Ok(Some(WsOutEvent::MemberRemoved { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::MemberRemoved {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ use crate::{
|
||||
search::{SearchEngine, SearchQuery},
|
||||
};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn search(
|
||||
@ -36,18 +36,27 @@ impl WsHandler {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let author_ids: Vec<Uuid> = result.hits.iter().map(|h| h.sender_id).collect();
|
||||
let message_ids: Vec<Uuid> = result.hits.iter().map(|h| h.message_id).collect();
|
||||
let author_ids: Vec<Uuid> =
|
||||
result.hits.iter().map(|h| h.sender_id).collect();
|
||||
let message_ids: Vec<Uuid> =
|
||||
result.hits.iter().map(|h| h.message_id).collect();
|
||||
let user_map = bus.lookup_users(&author_ids).await.unwrap_or_default();
|
||||
let reactions = Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let reactions =
|
||||
Self::reaction_groups_for_messages(bus, user_id, &message_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let search_room = match room {
|
||||
Some(r) => Some(bus.lookup_room(r).await.unwrap_or_else(|_| RoomInfo::unknown(r))),
|
||||
Some(r) => Some(
|
||||
bus.lookup_room(r)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(r)),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let search_msg_room = search_room.clone().unwrap_or_else(|| RoomInfo::unknown(room.unwrap_or_default()));
|
||||
let search_msg_room = search_room
|
||||
.clone()
|
||||
.unwrap_or_else(|| RoomInfo::unknown(room.unwrap_or_default()));
|
||||
let data = crate::event::search::SearchResultService {
|
||||
q,
|
||||
room: search_room,
|
||||
@ -60,26 +69,30 @@ impl WsHandler {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| UserInfo::unknown(h.sender_id));
|
||||
crate::event::search::SearchMessageHitService {
|
||||
message: crate::event::message::MessageNewService {
|
||||
id: h.message_id,
|
||||
seq: 0,
|
||||
room: search_msg_room.clone(),
|
||||
sender_type: "user".to_string(),
|
||||
sender,
|
||||
thread: None,
|
||||
in_reply_to: None,
|
||||
content: h.content.clone(),
|
||||
content_type: "text".to_string(),
|
||||
pinned: false,
|
||||
system_type: None,
|
||||
metadata: serde_json::Value::Null,
|
||||
thinking_content: None,
|
||||
thinking_is_chunked: None,
|
||||
send_at: h.send_at,
|
||||
reactions: reactions.get(&h.message_id).cloned().unwrap_or_default(),
|
||||
},
|
||||
highlighted_content: h.highlighted,
|
||||
}})
|
||||
message: crate::event::message::MessageNewService {
|
||||
id: h.message_id,
|
||||
seq: 0,
|
||||
room: search_msg_room.clone(),
|
||||
sender_type: "user".to_string(),
|
||||
sender,
|
||||
thread: None,
|
||||
in_reply_to: None,
|
||||
content: h.content.clone(),
|
||||
content_type: "text".to_string(),
|
||||
pinned: false,
|
||||
system_type: None,
|
||||
metadata: serde_json::Value::Null,
|
||||
thinking_content: None,
|
||||
thinking_is_chunked: None,
|
||||
send_at: h.send_at,
|
||||
reactions: reactions
|
||||
.get(&h.message_id)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
highlighted_content: h.highlighted,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
total: result.total as i64,
|
||||
took_ms: 0,
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, star};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn message_star(
|
||||
@ -18,10 +18,14 @@ impl WsHandler {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
Self::ensure_message_in_room(bus, room, message).await?;
|
||||
|
||||
let room_info =
|
||||
bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info =
|
||||
bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let room_info = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let user_info = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
if do_star {
|
||||
let result = db::sqlx::query(
|
||||
@ -76,7 +80,8 @@ impl WsHandler {
|
||||
unstarred_by: user_info,
|
||||
unstarred_at: Utc::now(),
|
||||
};
|
||||
bus.emit_to_user(user_id, "message.unstarred", &data).await?;
|
||||
bus.emit_to_user(user_id, "message.unstarred", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::MessageUnstarred {
|
||||
room: room_info,
|
||||
data,
|
||||
@ -123,7 +128,17 @@ impl WsHandler {
|
||||
let user_map = bus.lookup_users(&author_ids).await.unwrap_or_default();
|
||||
|
||||
let mut entries = Vec::with_capacity(rows.len());
|
||||
for (_star_id, msg_id, seq, content, content_type, author_id, starred_at, sent_at) in rows {
|
||||
for (
|
||||
_star_id,
|
||||
msg_id,
|
||||
seq,
|
||||
content,
|
||||
content_type,
|
||||
author_id,
|
||||
starred_at,
|
||||
sent_at,
|
||||
) in rows
|
||||
{
|
||||
let msg_room_row: Option<(Uuid,)> = db::sqlx::query_as(
|
||||
"SELECT room FROM room_message WHERE id = $1",
|
||||
)
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, member};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn subscribe(
|
||||
@ -36,10 +36,18 @@ impl WsHandler {
|
||||
let key = (room, user_id);
|
||||
|
||||
if action == "start" {
|
||||
let ty_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ty_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let ty_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ty_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let already_typing = bus.inner.typing_states.contains_key(&key);
|
||||
if let Some((_, (_, _, old_cancel))) = bus.inner.typing_states.remove(&key) {
|
||||
if let Some((_, (_, _, old_cancel))) =
|
||||
bus.inner.typing_states.remove(&key)
|
||||
{
|
||||
old_cancel.cancel();
|
||||
}
|
||||
|
||||
@ -48,13 +56,18 @@ impl WsHandler {
|
||||
let bus_clone = bus.clone();
|
||||
let user_clone = ty_user.clone();
|
||||
let room_clone = ty_room.clone();
|
||||
bus.inner.typing_states.insert(key, (ty_user.clone(), ty_room.clone(), cancel));
|
||||
bus.inner
|
||||
.typing_states
|
||||
.insert(key, (ty_user.clone(), ty_room.clone(), cancel));
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
if cancel_clone.is_cancelled() {
|
||||
return;
|
||||
}
|
||||
bus_clone.inner.typing_states.remove(&(room_clone.id, user_clone.id));
|
||||
bus_clone
|
||||
.inner
|
||||
.typing_states
|
||||
.remove(&(room_clone.id, user_clone.id));
|
||||
let room_id = room_clone.id;
|
||||
let stop_data = member::TypingStopService {
|
||||
room: room_clone,
|
||||
@ -62,7 +75,9 @@ impl WsHandler {
|
||||
sender_type: "user".to_string(),
|
||||
stopped_at: Utc::now(),
|
||||
};
|
||||
let _ = bus_clone.publish_room_event(room_id, "typing.stop", &stop_data).await;
|
||||
let _ = bus_clone
|
||||
.publish_room_event(room_id, "typing.stop", &stop_data)
|
||||
.await;
|
||||
});
|
||||
if !already_typing {
|
||||
let data = member::TypingStartService {
|
||||
@ -72,16 +87,27 @@ impl WsHandler {
|
||||
started_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "typing.start", &data).await?;
|
||||
return Ok(Some(WsOutEvent::TypingStart { room: data.room.clone(), data }));
|
||||
return Ok(Some(WsOutEvent::TypingStart {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
if let Some((_, (_, _, cancel))) = bus.inner.typing_states.remove(&key) {
|
||||
if let Some((_, (_, _, cancel))) =
|
||||
bus.inner.typing_states.remove(&key)
|
||||
{
|
||||
cancel.cancel();
|
||||
}
|
||||
|
||||
let ty_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ty_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let ty_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let ty_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
|
||||
let data = member::TypingStopService {
|
||||
room: ty_room,
|
||||
@ -90,7 +116,10 @@ impl WsHandler {
|
||||
stopped_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "typing.stop", &data).await?;
|
||||
Ok(Some(WsOutEvent::TypingStop { room: data.room.clone(), data }))
|
||||
Ok(Some(WsOutEvent::TypingStop {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,8 +146,14 @@ impl WsHandler {
|
||||
.bind(last_read_seq)
|
||||
.execute(bus.inner.db.writer())
|
||||
.await?;
|
||||
let rr_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let rr_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let rr_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let rr_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = member::ReadReceiptService {
|
||||
room: rr_room.clone(),
|
||||
user: rr_user,
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, thread};
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
/// Helper struct for thread_list JOIN query result
|
||||
#[derive(db::sqlx::FromRow)]
|
||||
@ -48,9 +48,13 @@ impl WsHandler {
|
||||
|
||||
let mut items = Vec::new();
|
||||
for row in rows {
|
||||
let tc_room = bus.lookup_room(row.room).await
|
||||
let tc_room = bus
|
||||
.lookup_room(row.room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let created_by = bus.lookup_user(row.created_by).await
|
||||
let created_by = bus
|
||||
.lookup_user(row.created_by)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(row.created_by));
|
||||
// Get last message preview
|
||||
let preview: Option<(String,)> = db::sqlx::query_as(
|
||||
@ -110,8 +114,14 @@ impl WsHandler {
|
||||
.bind(user_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let tc_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let created_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let tc_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let created_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = thread::ThreadCreatedService {
|
||||
id: row.id,
|
||||
room: tc_room,
|
||||
@ -120,8 +130,12 @@ impl WsHandler {
|
||||
participants: serde_json::Value::Null,
|
||||
created_at: row.created_at,
|
||||
};
|
||||
bus.publish_room_event(room, "thread.created", &data).await?;
|
||||
Ok(Some(WsOutEvent::ThreadCreated { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(room, "thread.created", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ThreadCreated {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn thread_resolve(
|
||||
@ -129,13 +143,12 @@ impl WsHandler {
|
||||
user_id: Uuid,
|
||||
thread_id: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let existing: (Uuid,) = db::sqlx::query_as(
|
||||
"SELECT room FROM room_thread WHERE id = $1",
|
||||
)
|
||||
.bind(thread_id)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?
|
||||
.ok_or(ChannelError::RoomNotFound)?;
|
||||
let existing: (Uuid,) =
|
||||
db::sqlx::query_as("SELECT room FROM room_thread WHERE id = $1")
|
||||
.bind(thread_id)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?
|
||||
.ok_or(ChannelError::RoomNotFound)?;
|
||||
Self::ensure_room_access(bus, user_id, existing.0).await?;
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomThreadModel>(
|
||||
"UPDATE room_thread SET locked = true, updated_at = now() \
|
||||
@ -146,16 +159,26 @@ impl WsHandler {
|
||||
.bind(thread_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let tr_room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let resolved_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let tr_room = bus
|
||||
.lookup_room(row.room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let resolved_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = thread::ThreadResolvedService {
|
||||
id: row.id,
|
||||
room: tr_room,
|
||||
resolved_by,
|
||||
resolved_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(row.room, "thread.resolved", &data).await?;
|
||||
Ok(Some(WsOutEvent::ThreadResolved { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(row.room, "thread.resolved", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ThreadResolved {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn thread_archive(
|
||||
@ -163,13 +186,12 @@ impl WsHandler {
|
||||
user_id: Uuid,
|
||||
thread_id: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
let existing: (Uuid,) = db::sqlx::query_as(
|
||||
"SELECT room FROM room_thread WHERE id = $1",
|
||||
)
|
||||
.bind(thread_id)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?
|
||||
.ok_or(ChannelError::RoomNotFound)?;
|
||||
let existing: (Uuid,) =
|
||||
db::sqlx::query_as("SELECT room FROM room_thread WHERE id = $1")
|
||||
.bind(thread_id)
|
||||
.fetch_optional(bus.inner.db.reader())
|
||||
.await?
|
||||
.ok_or(ChannelError::RoomNotFound)?;
|
||||
Self::ensure_room_access(bus, user_id, existing.0).await?;
|
||||
let row = db::sqlx::query_as::<_, model::room::RoomThreadModel>(
|
||||
"UPDATE room_thread SET archived = true, archived_at = now(), updated_at = now() \
|
||||
@ -180,15 +202,25 @@ impl WsHandler {
|
||||
.bind(thread_id)
|
||||
.fetch_one(bus.inner.db.writer())
|
||||
.await?;
|
||||
let ta_room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let archived_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let ta_room = bus
|
||||
.lookup_room(row.room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
||||
let archived_by = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = thread::ThreadArchivedService {
|
||||
id: row.id,
|
||||
room: ta_room,
|
||||
archived_by,
|
||||
archived_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(row.room, "thread.archived", &data).await?;
|
||||
Ok(Some(WsOutEvent::ThreadArchived { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(row.room, "thread.archived", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::ThreadArchived {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{ChannelBus, ChannelError, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn user_summary(
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::event::{RoomInfo, UserInfo, voice};
|
||||
use crate::{ChannelBus, ChannelResult};
|
||||
|
||||
use super::WsOutEvent;
|
||||
use super::WsHandler;
|
||||
use super::WsOutEvent;
|
||||
|
||||
impl WsHandler {
|
||||
pub(super) async fn voice_join(
|
||||
@ -14,8 +14,14 @@ impl WsHandler {
|
||||
room: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let vj_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let vj_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let vj_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let vj_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = voice::VoiceChannelJoinedService {
|
||||
room: vj_room,
|
||||
workspace: None,
|
||||
@ -25,8 +31,12 @@ impl WsHandler {
|
||||
video: false,
|
||||
joined_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "voice.channel_joined", &data).await?;
|
||||
Ok(Some(WsOutEvent::VoiceChannelJoined { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(room, "voice.channel_joined", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::VoiceChannelJoined {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn voice_leave(
|
||||
@ -35,16 +45,26 @@ impl WsHandler {
|
||||
room: Uuid,
|
||||
) -> ChannelResult<Option<WsOutEvent>> {
|
||||
Self::ensure_room_access(bus, user_id, room).await?;
|
||||
let vl_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let vl_user = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let vl_room = bus
|
||||
.lookup_room(room)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
||||
let vl_user = bus
|
||||
.lookup_user(user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
||||
let data = voice::VoiceChannelLeftService {
|
||||
room: vl_room,
|
||||
workspace: None,
|
||||
user: vl_user,
|
||||
left_at: Utc::now(),
|
||||
};
|
||||
bus.publish_room_event(room, "voice.channel_left", &data).await?;
|
||||
Ok(Some(WsOutEvent::VoiceChannelLeft { room: data.room.clone(), data }))
|
||||
bus.publish_room_event(room, "voice.channel_left", &data)
|
||||
.await?;
|
||||
Ok(Some(WsOutEvent::VoiceChannelLeft {
|
||||
room: data.room.clone(),
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn voice_mute(
|
||||
|
||||
@ -2,10 +2,9 @@ use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event::{
|
||||
RoomInfo, WorkspaceInfo,
|
||||
ai, attachment, ban, category, conversation, dm, draft, forward, invite,
|
||||
member, message, message_read, notify, pin, presence, reaction, rooms,
|
||||
search, star, thread, voice, workspace,
|
||||
RoomInfo, WorkspaceInfo, attachment, ban, category, conversation, draft,
|
||||
forward, invite, member, message, message_read, notify, pin, presence,
|
||||
reaction, rooms, search, star, thread, voice, workspace,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@ -188,22 +187,6 @@ pub enum WsOutEvent {
|
||||
UserUnbanned {
|
||||
data: ban::UnbannedService,
|
||||
},
|
||||
AiAgentJoined {
|
||||
room: RoomInfo,
|
||||
data: ai::AiAgentJoinedService,
|
||||
},
|
||||
AiAgentLeft {
|
||||
room: RoomInfo,
|
||||
data: ai::AiAgentLeftService,
|
||||
},
|
||||
AiAgentList {
|
||||
room: RoomInfo,
|
||||
data: ai::RoomAiListService,
|
||||
},
|
||||
AiAgentStatusChanged {
|
||||
room: RoomInfo,
|
||||
data: ai::AiAgentStatusChangedService,
|
||||
},
|
||||
VoiceChannelJoined {
|
||||
room: RoomInfo,
|
||||
data: voice::VoiceChannelJoinedService,
|
||||
@ -235,21 +218,6 @@ pub enum WsOutEvent {
|
||||
ConversationList {
|
||||
data: Vec<conversation::ConversationSummary>,
|
||||
},
|
||||
DmCreated {
|
||||
room: RoomInfo,
|
||||
data: dm::DmCreatedService,
|
||||
},
|
||||
DmClosed {
|
||||
room: RoomInfo,
|
||||
data: dm::DmClosedService,
|
||||
},
|
||||
DmReopened {
|
||||
room: RoomInfo,
|
||||
data: dm::DmReopenedService,
|
||||
},
|
||||
DmList {
|
||||
data: Vec<dm::DmCreatedService>,
|
||||
},
|
||||
MessageRead {
|
||||
room: RoomInfo,
|
||||
data: message_read::MessageReadService,
|
||||
|
||||
@ -60,12 +60,14 @@ pub enum WsInMessage {
|
||||
room_name: String,
|
||||
public: bool,
|
||||
category: Option<Uuid>,
|
||||
ai_enabled: Option<bool>,
|
||||
},
|
||||
RoomUpdate {
|
||||
room: Uuid,
|
||||
room_name: Option<String>,
|
||||
public: Option<bool>,
|
||||
category: Option<Uuid>,
|
||||
ai_enabled: Option<bool>,
|
||||
},
|
||||
RoomDelete {
|
||||
room: Uuid,
|
||||
@ -212,20 +214,6 @@ pub enum WsInMessage {
|
||||
room: Uuid,
|
||||
start: bool,
|
||||
},
|
||||
AiList {
|
||||
room: Uuid,
|
||||
},
|
||||
AiUpsert {
|
||||
room: Uuid,
|
||||
model: Uuid,
|
||||
},
|
||||
AiDelete {
|
||||
room: Uuid,
|
||||
agent_id: Uuid,
|
||||
},
|
||||
AiStop {
|
||||
room: Uuid,
|
||||
},
|
||||
UserSummary {
|
||||
username: String,
|
||||
},
|
||||
@ -242,13 +230,6 @@ pub enum WsInMessage {
|
||||
notify_level: String,
|
||||
},
|
||||
ConversationList,
|
||||
DmCreate {
|
||||
recipient: Uuid,
|
||||
},
|
||||
DmClose {
|
||||
room: Uuid,
|
||||
},
|
||||
DmList,
|
||||
MessageMarkRead {
|
||||
room: Uuid,
|
||||
message_ids: Vec<Uuid>,
|
||||
@ -312,14 +293,9 @@ impl WsInMessage {
|
||||
VoiceMute,
|
||||
VoiceDeaf,
|
||||
ScreenShare,
|
||||
AiList,
|
||||
AiUpsert,
|
||||
AiDelete,
|
||||
AiStop,
|
||||
ConversationPin,
|
||||
ConversationMute,
|
||||
ConversationNotifyLevel,
|
||||
DmClose,
|
||||
MessageMarkRead,
|
||||
MessageStar,
|
||||
)
|
||||
|
||||
@ -47,11 +47,7 @@ async fn handle_inbound(bus: &ChannelBus, socket: &Socket, data: EventPayload) {
|
||||
let parsed = payload;
|
||||
|
||||
let text = serde_json::to_string(payload).unwrap_or_default();
|
||||
if parsed
|
||||
.get("type")
|
||||
.and_then(|t| t.as_str())
|
||||
== Some("ping")
|
||||
{
|
||||
if parsed.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
||||
let pong = WsOutEvent::Pong {
|
||||
protocol_version: super::types::WS_PROTOCOL_VERSION,
|
||||
};
|
||||
@ -115,7 +111,8 @@ async fn handle_inbound(bus: &ChannelBus, socket: &Socket, data: EventPayload) {
|
||||
code: 400,
|
||||
error: "parse_error".to_string(),
|
||||
message: e.to_string(),
|
||||
}).unwrap_or_default(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
send_event(socket, &err_resp).await;
|
||||
} else {
|
||||
@ -126,7 +123,8 @@ async fn handle_inbound(bus: &ChannelBus, socket: &Socket, data: EventPayload) {
|
||||
error: "parse_error".to_string(),
|
||||
message: e.to_string(),
|
||||
},
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ mod security;
|
||||
mod seq;
|
||||
mod token;
|
||||
|
||||
use crate::event::UserInfo;
|
||||
pub use ack::{AckRequest, AckResponse, AckStatus, AckTracker, MessageAck};
|
||||
pub use bus::ChannelBus;
|
||||
pub use cdn::{CdnManager, CdnStoredFile};
|
||||
@ -37,3 +38,14 @@ pub use seq::SeqAllocator;
|
||||
pub use token::{
|
||||
ChannelAccessToken, ChannelTokenApply, ChannelTokenContext, TOKEN_TTL_SECS,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REDPADA: UserInfo = UserInfo {
|
||||
id: Uuid::nil(),
|
||||
username: "RedPanda".to_string(),
|
||||
display_name: "RedPanda".to_string(),
|
||||
avatar_url: "".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use model::room::RoomMessageModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::rooms::RM_COLUMNS;
|
||||
use crate::ChannelResult;
|
||||
use crate::rooms::RM_COLUMNS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
|
||||
@ -6,8 +6,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{ChannelBusConfig, ChannelResult};
|
||||
|
||||
pub(crate) const RM_COLUMNS: &str =
|
||||
"id, room, seq, thread, parent, author, content, content_type, pinned, \
|
||||
pub(crate) const RM_COLUMNS: &str = "id, room, seq, thread, parent, author, content, content_type, pinned, \
|
||||
system_type, metadata, edited_at, created_at, updated_at, deleted_at";
|
||||
|
||||
pub(crate) fn room_socket_name(room: Uuid) -> String {
|
||||
@ -24,6 +23,7 @@ pub struct RoomListItem {
|
||||
pub topic: Option<String>,
|
||||
pub room_type: String,
|
||||
pub is_private: bool,
|
||||
pub ai_enabled: bool,
|
||||
pub category: Option<Uuid>,
|
||||
pub workspace_id: Uuid,
|
||||
}
|
||||
@ -44,8 +44,20 @@ pub async fn user_rooms_for_api(
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as::<_, (Uuid, String, Option<String>, String, bool, Option<Uuid>, Uuid)>(
|
||||
"SELECT id, name, topic, room_type, is_private, parent, wk \
|
||||
let rows = sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
Uuid,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
Option<Uuid>,
|
||||
Uuid,
|
||||
),
|
||||
>(
|
||||
"SELECT id, name, topic, room_type, is_private, ai_enabled, parent, wk \
|
||||
FROM room \
|
||||
WHERE id = ANY($1) AND deleted_at IS NULL AND is_archived = false \
|
||||
ORDER BY name",
|
||||
@ -56,15 +68,27 @@ pub async fn user_rooms_for_api(
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|(id, name, topic, room_type, is_private, category, workspace_id)| RoomListItem {
|
||||
id,
|
||||
name,
|
||||
topic,
|
||||
room_type,
|
||||
is_private,
|
||||
category,
|
||||
workspace_id,
|
||||
})
|
||||
.map(
|
||||
|(
|
||||
id,
|
||||
name,
|
||||
topic,
|
||||
room_type,
|
||||
is_private,
|
||||
ai_enabled,
|
||||
category,
|
||||
workspace_id,
|
||||
)| RoomListItem {
|
||||
id,
|
||||
name,
|
||||
topic,
|
||||
room_type,
|
||||
is_private,
|
||||
ai_enabled,
|
||||
category,
|
||||
workspace_id,
|
||||
},
|
||||
)
|
||||
.collect())
|
||||
}
|
||||
pub async fn user_categories_for_api(
|
||||
@ -179,14 +203,14 @@ pub(crate) async fn catchup_messages(
|
||||
room: Uuid,
|
||||
after_seq: i64,
|
||||
) -> ChannelResult<Vec<RoomMessageModel>> {
|
||||
let rows = sqlx::query_as::<_, RoomMessageModel>(
|
||||
db::sqlx::AssertSqlSafe(format!(
|
||||
let rows = sqlx::query_as::<_, RoomMessageModel>(db::sqlx::AssertSqlSafe(
|
||||
format!(
|
||||
"SELECT {RM_COLUMNS} FROM room_message \
|
||||
WHERE room = $1 AND seq > $2 AND deleted_at IS NULL \
|
||||
ORDER BY seq ASC \
|
||||
LIMIT $3"
|
||||
)),
|
||||
)
|
||||
),
|
||||
))
|
||||
.bind(room)
|
||||
.bind(after_seq)
|
||||
.bind(config.catchup_limit)
|
||||
|
||||
@ -142,7 +142,7 @@ return 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn require_cluster(
|
||||
pub fn require_cluster(
|
||||
cache: &cache::AppCache,
|
||||
) -> ChannelResult<&cache::ClusterCache> {
|
||||
cache
|
||||
|
||||
@ -128,7 +128,12 @@ impl SeqAllocator {
|
||||
}
|
||||
if state
|
||||
.next
|
||||
.compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Acquire)
|
||||
.compare_exchange_weak(
|
||||
current,
|
||||
current + 1,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Acquire,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some(current);
|
||||
|
||||
@ -184,7 +184,9 @@ impl ChannelBus {
|
||||
.arg(&session_key)
|
||||
.query_async(&mut conn)
|
||||
.await
|
||||
.map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?;
|
||||
.map_err(|e| {
|
||||
ChannelError::Cache(cache::CacheError::Redis(e))
|
||||
})?;
|
||||
|
||||
let device_id = hash_data
|
||||
.get("device_id")
|
||||
@ -252,9 +254,8 @@ impl ChannelBus {
|
||||
created_at,
|
||||
};
|
||||
let new_token_bytes = new_payload.encode(&signing_key)?;
|
||||
let new_access_token =
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(&new_token_bytes);
|
||||
let new_access_token = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(&new_token_bytes);
|
||||
|
||||
let new_session_key =
|
||||
self.session_hash_key(&payload.user_id, created_at);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user