mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-09 21:13:09 +00:00
435 lines
13 KiB
Rust
435 lines
13 KiB
Rust
use async_trait::async_trait;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
use sqlx::PgPool;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::plugins::{
|
|
hooks::HookContext, manager::PluginSystem, Plugin, PluginError, PluginMetadata, PluginScope,
|
|
};
|
|
|
|
pub struct StructuredDeliberationPlugin;
|
|
|
|
impl StructuredDeliberationPlugin {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Plugin for StructuredDeliberationPlugin {
|
|
fn metadata(&self) -> PluginMetadata {
|
|
PluginMetadata {
|
|
name: "structured_deliberation",
|
|
version: "1.0.0",
|
|
description: "Pro/con arguments and collaborative summaries",
|
|
is_core: false,
|
|
scope: PluginScope::Community,
|
|
default_enabled: true,
|
|
settings_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"require_reading": {"type": "boolean", "default": true},
|
|
"enable_summaries": {"type": "boolean", "default": true}
|
|
}
|
|
})),
|
|
}
|
|
}
|
|
|
|
fn register(&self, system: &mut PluginSystem) {
|
|
// Hook: Check reading requirement before comment
|
|
system.add_action(
|
|
"comment.pre_create",
|
|
"structured_deliberation".to_string(),
|
|
5,
|
|
Arc::new(|ctx: HookContext, payload: Value| {
|
|
Box::pin(async move {
|
|
if let (Some(proposal_id), Some(user_id)) = (
|
|
payload
|
|
.get("proposal_id")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| Uuid::parse_str(s).ok()),
|
|
ctx.actor_user_id,
|
|
) {
|
|
let can_comment = DeliberationService::check_can_participate(
|
|
&ctx.pool,
|
|
proposal_id,
|
|
user_id,
|
|
"comment",
|
|
)
|
|
.await?;
|
|
|
|
if !can_comment {
|
|
return Err(PluginError::Message(
|
|
"Please read the proposal before commenting".to_string(),
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
})
|
|
}),
|
|
);
|
|
|
|
// Hook: Calculate metrics periodically
|
|
system.add_action(
|
|
"cron.hourly",
|
|
"structured_deliberation".to_string(),
|
|
50,
|
|
Arc::new(|ctx: HookContext, _payload: Value| {
|
|
Box::pin(async move {
|
|
DeliberationService::update_all_metrics(&ctx.pool).await?;
|
|
Ok(())
|
|
})
|
|
}),
|
|
);
|
|
|
|
// Hook: Track argument creation
|
|
system.add_action(
|
|
"deliberation.argument.created",
|
|
"structured_deliberation".to_string(),
|
|
50,
|
|
Arc::new(|ctx: HookContext, payload: Value| {
|
|
Box::pin(async move {
|
|
ctx.emit_public_event(
|
|
Some("structured_deliberation"),
|
|
"argument.created",
|
|
payload.clone(),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
})
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Argument {
|
|
pub id: Uuid,
|
|
pub proposal_id: Uuid,
|
|
pub stance: String,
|
|
pub title: String,
|
|
pub content: String,
|
|
pub author_id: Uuid,
|
|
pub upvotes: i32,
|
|
pub downvotes: i32,
|
|
pub quality_score: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Summary {
|
|
pub id: Uuid,
|
|
pub proposal_id: Uuid,
|
|
pub summary_type: String,
|
|
pub content: String,
|
|
pub version: i32,
|
|
pub is_approved: bool,
|
|
}
|
|
|
|
pub struct DeliberationService;
|
|
|
|
impl DeliberationService {
|
|
/// Check if user can participate based on reading requirements
|
|
pub async fn check_can_participate(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
user_id: Uuid,
|
|
action: &str,
|
|
) -> Result<bool, PluginError> {
|
|
let reading = sqlx::query!(
|
|
r#"SELECT can_comment, can_vote FROM deliberation_reading_log
|
|
WHERE proposal_id = $1 AND user_id = $2"#,
|
|
proposal_id,
|
|
user_id
|
|
)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
|
|
match reading {
|
|
Some(r) => Ok(match action {
|
|
"comment" => r.can_comment.unwrap_or(false),
|
|
"vote" => r.can_vote.unwrap_or(false),
|
|
_ => true,
|
|
}),
|
|
None => Ok(false), // No reading record
|
|
}
|
|
}
|
|
|
|
/// Record that user has read content
|
|
pub async fn record_reading(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
user_id: Uuid,
|
|
read_type: &str,
|
|
time_seconds: i32,
|
|
) -> Result<(), PluginError> {
|
|
sqlx::query!(
|
|
r#"INSERT INTO deliberation_reading_log (proposal_id, user_id, first_read_at, reading_time_seconds)
|
|
VALUES ($1, $2, NOW(), $3)
|
|
ON CONFLICT (proposal_id, user_id) DO UPDATE SET
|
|
read_proposal = CASE WHEN $4 = 'proposal' THEN true ELSE deliberation_reading_log.read_proposal END,
|
|
read_summaries = CASE WHEN $4 = 'summaries' THEN true ELSE deliberation_reading_log.read_summaries END,
|
|
read_top_arguments = CASE WHEN $4 = 'arguments' THEN true ELSE deliberation_reading_log.read_top_arguments END,
|
|
reading_time_seconds = deliberation_reading_log.reading_time_seconds + $3,
|
|
updated_at = NOW()"#,
|
|
proposal_id,
|
|
user_id,
|
|
time_seconds,
|
|
read_type
|
|
)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add an argument
|
|
pub async fn add_argument(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
parent_id: Option<Uuid>,
|
|
stance: &str,
|
|
title: &str,
|
|
content: &str,
|
|
author_id: Uuid,
|
|
) -> Result<Uuid, PluginError> {
|
|
let argument_id: Uuid = sqlx::query_scalar(
|
|
"SELECT add_deliberation_argument($1, $2, $3::argument_stance, $4, $5, $6)",
|
|
)
|
|
.bind(proposal_id)
|
|
.bind(parent_id)
|
|
.bind(stance)
|
|
.bind(title)
|
|
.bind(content)
|
|
.bind(author_id)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
Ok(argument_id)
|
|
}
|
|
|
|
/// Vote on an argument
|
|
pub async fn vote_argument(
|
|
pool: &PgPool,
|
|
argument_id: Uuid,
|
|
user_id: Uuid,
|
|
vote_type: &str,
|
|
) -> Result<(), PluginError> {
|
|
sqlx::query("SELECT vote_on_argument($1, $2, $3)")
|
|
.bind(argument_id)
|
|
.bind(user_id)
|
|
.bind(vote_type)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get arguments for a proposal
|
|
pub async fn get_arguments(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
stance: Option<&str>,
|
|
limit: i64,
|
|
) -> Result<Vec<Argument>, PluginError> {
|
|
let arguments = if let Some(s) = stance {
|
|
sqlx::query_as!(
|
|
Argument,
|
|
r#"SELECT
|
|
id, proposal_id, stance::text AS "stance!",
|
|
title, content, author_id,
|
|
upvotes, downvotes, quality_score::float8 AS quality_score
|
|
FROM deliberation_arguments
|
|
WHERE proposal_id = $1 AND stance::text = $2 AND NOT is_hidden AND parent_id IS NULL
|
|
ORDER BY quality_score DESC NULLS LAST
|
|
LIMIT $3"#,
|
|
proposal_id,
|
|
s,
|
|
limit
|
|
)
|
|
.fetch_all(pool)
|
|
.await?
|
|
} else {
|
|
sqlx::query_as!(
|
|
Argument,
|
|
r#"SELECT
|
|
id, proposal_id, stance::text AS "stance!",
|
|
title, content, author_id,
|
|
upvotes, downvotes, quality_score::float8 AS quality_score
|
|
FROM deliberation_arguments
|
|
WHERE proposal_id = $1 AND NOT is_hidden AND parent_id IS NULL
|
|
ORDER BY quality_score DESC NULLS LAST
|
|
LIMIT $2"#,
|
|
proposal_id,
|
|
limit
|
|
)
|
|
.fetch_all(pool)
|
|
.await?
|
|
};
|
|
|
|
Ok(arguments)
|
|
}
|
|
|
|
/// Create or update a summary
|
|
pub async fn upsert_summary(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
summary_type: &str,
|
|
content: &str,
|
|
key_points: Value,
|
|
editor_id: Uuid,
|
|
) -> Result<Uuid, PluginError> {
|
|
// Check if summary exists
|
|
let existing = sqlx::query_scalar!(
|
|
"SELECT id FROM deliberation_summaries WHERE proposal_id = $1 AND summary_type = $2::summary_type",
|
|
proposal_id,
|
|
summary_type as _
|
|
)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
|
|
if let Some(summary_id) = existing {
|
|
// Save to history
|
|
sqlx::query!(
|
|
r#"INSERT INTO summary_edit_history (summary_id, version, content, key_points, editor_id)
|
|
SELECT id, version, content, key_points, last_editor_id
|
|
FROM deliberation_summaries WHERE id = $1"#,
|
|
summary_id
|
|
)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
// Update
|
|
sqlx::query!(
|
|
r#"UPDATE deliberation_summaries SET
|
|
content = $2, key_points = $3, last_editor_id = $4,
|
|
version = version + 1, edit_count = edit_count + 1,
|
|
is_approved = false, updated_at = NOW()
|
|
WHERE id = $1"#,
|
|
summary_id,
|
|
content,
|
|
key_points,
|
|
editor_id
|
|
)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(summary_id)
|
|
} else {
|
|
// Create new
|
|
let summary_id = sqlx::query_scalar!(
|
|
r#"INSERT INTO deliberation_summaries (
|
|
proposal_id, summary_type, content, key_points, last_editor_id
|
|
) VALUES ($1, $2::summary_type, $3, $4, $5)
|
|
RETURNING id"#,
|
|
proposal_id,
|
|
summary_type as _,
|
|
content,
|
|
key_points,
|
|
editor_id
|
|
)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
|
|
Ok(summary_id)
|
|
}
|
|
}
|
|
|
|
/// Get summaries for a proposal
|
|
pub async fn get_summaries(
|
|
pool: &PgPool,
|
|
proposal_id: Uuid,
|
|
) -> Result<Vec<Summary>, PluginError> {
|
|
let summaries = sqlx::query_as!(
|
|
Summary,
|
|
r#"SELECT
|
|
id, proposal_id, summary_type::text AS "summary_type!",
|
|
content, version, is_approved
|
|
FROM deliberation_summaries
|
|
WHERE proposal_id = $1
|
|
ORDER BY summary_type"#,
|
|
proposal_id
|
|
)
|
|
.fetch_all(pool)
|
|
.await?;
|
|
|
|
Ok(summaries)
|
|
}
|
|
|
|
/// Approve a summary
|
|
pub async fn approve_summary(
|
|
pool: &PgPool,
|
|
summary_id: Uuid,
|
|
approver_id: Uuid,
|
|
) -> Result<(), PluginError> {
|
|
sqlx::query!(
|
|
r#"UPDATE deliberation_summaries SET
|
|
is_approved = true, approved_by = $2, approved_at = NOW()
|
|
WHERE id = $1"#,
|
|
summary_id,
|
|
approver_id
|
|
)
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Update metrics for all active proposals
|
|
pub async fn update_all_metrics(pool: &PgPool) -> Result<i32, PluginError> {
|
|
let proposals = sqlx::query_scalar!(
|
|
r#"SELECT DISTINCT proposal_id FROM deliberation_arguments
|
|
WHERE created_at > NOW() - INTERVAL '1 day'"#
|
|
)
|
|
.fetch_all(pool)
|
|
.await?;
|
|
|
|
let mut count = 0;
|
|
for proposal_id in proposals {
|
|
sqlx::query("SELECT calculate_deliberation_metrics($1)")
|
|
.bind(proposal_id)
|
|
.execute(pool)
|
|
.await?;
|
|
count += 1;
|
|
}
|
|
|
|
Ok(count)
|
|
}
|
|
|
|
/// Get deliberation overview
|
|
pub async fn get_overview(pool: &PgPool, proposal_id: Uuid) -> Result<Value, PluginError> {
|
|
let metrics = sqlx::query!(
|
|
r#"SELECT
|
|
total_arguments, pro_arguments, con_arguments, neutral_arguments,
|
|
unique_participants, substantive_ratio::float8 AS substantive_ratio,
|
|
balance_score::float8 AS balance_score
|
|
FROM deliberation_metrics
|
|
WHERE proposal_id = $1
|
|
ORDER BY calculated_at DESC
|
|
LIMIT 1"#,
|
|
proposal_id
|
|
)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
|
|
let top_pro = Self::get_arguments(pool, proposal_id, Some("pro"), 3).await?;
|
|
let top_con = Self::get_arguments(pool, proposal_id, Some("con"), 3).await?;
|
|
let summaries = Self::get_summaries(pool, proposal_id).await?;
|
|
|
|
Ok(json!({
|
|
"metrics": metrics.map(|m| json!({
|
|
"total_arguments": m.total_arguments,
|
|
"pro": m.pro_arguments,
|
|
"con": m.con_arguments,
|
|
"neutral": m.neutral_arguments,
|
|
"participants": m.unique_participants,
|
|
"substantive_ratio": m.substantive_ratio,
|
|
"balance_score": m.balance_score
|
|
})),
|
|
"top_pro_arguments": top_pro,
|
|
"top_con_arguments": top_con,
|
|
"summaries": summaries
|
|
}))
|
|
}
|
|
}
|