use crate::config::Color;
use crate::permissions::Perm;
use crate::{fallback, get_user_login, get_user_login_email, get_user_login_email_strict, Config};
use axum::{
debug_handler,
extract::{ConnectInfo, Path, Query, State},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Form, Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use axum_extra::TypedHeader;
use chrono::{DateTime, Utc};
use diesel::{
BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods,
OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncPgConnection;
use diesel_async::{AsyncConnection, RunQueryDsl};
use crate::repository::TreePath;
use http::StatusCode;
use libpijul::changestore::ChangeStore;
use libpijul::Base32;
use serde_derive::*;
use std::net::SocketAddr;
use tracing::*;
pub fn router() -> Router<Config> {
Router::new()
.route("/{owner}/{repo}", get(list))
.route("/{owner}/{repo}/new", get(new).post(new_post))
.route(
"/{owner}/{repo}/{disc}",
get(discussion).post(discussion_post),
)
.route("/{owner}/{repo}/{disc}/remove_change", post(remove_change))
.route("/{owner}/{repo}/{disc}/add_change", post(add_change))
.fallback(fallback)
}
#[derive(Debug, Deserialize)]
pub struct DiscQuery {
from: Option<u64>,
closed: Option<bool>,
q: Option<String>,
}
pub async fn list(
State(config): State<Config>,
jar: SignedCookieJar,
Path(tree): Path<TreePath>,
Query(q): Query<DiscQuery>,
) -> Result<Response, crate::Error> {
debug!("list {:?} {:?}", tree, q);
let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
(Some(a), Some(b))
} else {
(None, None)
};
let mut db = config.db.get().await?;
let (id, _) =
crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
if let Some(_) = q.q {
unimplemented!()
} else {
use crate::db::discussion_tags::dsl as dt;
use crate::db::discussions::dsl as d;
use crate::db::tags::dsl as tags;
use crate::db::users::dsl as users;
use diesel::dsl::count;
let n = d::discussions
.filter(d::repository_id.eq(id))
.select(count(d::id))
.get_result::<i64>(&mut db)
.await?;
let mut max_number = 0;
let mut disc: Vec<_> = (if let Some(true) = q.closed {
d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.select((
d::id,
d::number,
d::title,
users::login,
d::creation_date,
d::changes,
d::closed,
))
.order_by(d::number)
.offset(q.from.unwrap_or(0) as i64)
.limit(LIMIT)
.get_results::<(
uuid::Uuid,
i32,
String,
String,
DateTime<Utc>,
i32,
Option<DateTime<Utc>>,
)>(&mut db)
.await?
} else {
d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.filter(d::closed.is_null())
.select((
d::id,
d::number,
d::title,
users::login,
d::creation_date,
d::changes,
d::closed,
))
.order_by(d::number)
.offset(q.from.unwrap_or(0) as i64)
.limit(LIMIT)
.get_results::<(
uuid::Uuid,
i32,
String,
String,
DateTime<Utc>,
i32,
Option<DateTime<Utc>>,
)>(&mut db)
.await?
})
.into_iter()
.map(|(id, num, title, author, opened, changes, closed)| {
max_number = max_number.max(num);
DiscussionListItem {
id,
num,
title,
author,
opened,
changes,
closed,
..DiscussionListItem::default()
}
})
.collect();
let mut i = 0;
for (num, tag) in d::discussions
.inner_join(dt::discussion_tags)
.inner_join(tags::tags.on(dt::tag.eq(tags::id.nullable())))
.filter(d::number.ge(q.from.unwrap_or(0) as i32))
.filter(d::number.le(max_number))
.filter(tags::id.is_not_null())
.select((d::number, tags::id))
.get_results::<(i32, uuid::Uuid)>(&mut db)
.await?
{
while i < disc.len() && num < disc[i].num {
i += 1
}
if i >= disc.len() {
break;
}
disc[i].tags.push(tag)
}
Ok(Json(Discussions {
owner: tree.owner,
repo: tree.repo,
login,
discussions: disc,
n,
})
.into_response())
}
}
#[derive(Debug, Serialize)]
struct Discussions {
owner: String,
repo: String,
n: i64,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
discussions: Vec<DiscussionListItem>,
}
#[derive(Debug, Deserialize)]
pub struct DiscPath {
owner: String,
repo: String,
disc: i32,
}
const LIMIT: i64 = 20;
#[derive(Debug, Default, Serialize)]
struct DiscussionListItem {
id: uuid::Uuid,
num: i32,
title: String,
author: String,
opened: DateTime<Utc>,
changes: i32,
tags: Vec<uuid::Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
closed: Option<DateTime<Utc>>,
}
#[debug_handler]
pub async fn new(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<TreePath>,
) -> Result<Response, crate::Error> {
debug!("new {:?}", tree);
let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
(Some(a), Some(b))
} else {
(None, None)
};
let mut db = config.db.get().await?;
crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
uid,
Perm::CREATE_DISCUSSION,
)
.await?;
let auth_token = token.authenticity_token().ok();
Ok((
token,
Json(NewDiscussion {
owner: tree.owner,
repo: tree.repo,
login,
unique: rand::random(),
token: auth_token,
}),
)
.into_response())
}
#[derive(Debug, Serialize)]
struct NewDiscussion {
owner: String,
repo: String,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
unique: i64,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct NewDiscussionForm {
new_discussion_name: String,
new_discussion_comment: String,
uniq: i64,
token: String,
}
#[debug_handler]
pub async fn new_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<TreePath>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(form): Form<NewDiscussionForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
debug!("new {:?}", tree);
let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let (id, _) = crate::repository::repository_id(
&mut db,
&tree.owner,
&tree.repo,
Some(uid),
Perm::CREATE_DISCUSSION,
)
.await?;
use crate::db::repositories::dsl as r;
db.transaction(move |mut txn| {
(async move {
let number: i32 = diesel::update(r::repositories.find(id))
.set(r::next_discussion_number.eq(r::next_discussion_number + 1))
.returning(r::next_discussion_number)
.get_result(&mut txn)
.await?;
use crate::db::discussions::dsl as d;
let ip_sql =
ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
.unwrap();
let disc_id: uuid::Uuid = diesel::insert_into(d::discussions)
.values((
d::title.eq(&form.new_discussion_name.trim()),
d::author.eq(uid),
d::creation_ip.eq(&ip_sql),
d::repository_id.eq(id),
d::number.eq(number),
d::uniq.eq(&form.uniq),
))
.returning(d::id)
.get_result(&mut txn)
.await?;
use crate::db::comments::dsl as c;
let comment = form.new_discussion_comment.trim();
if !comment.is_empty() {
let comment_html =
crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
diesel::insert_into(c::comments)
.values((
c::discussion_id.eq(disc_id),
c::author.eq(uid),
c::creation_ip.eq(&ip_sql),
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
c::uniq.eq(&form.uniq),
))
.execute(&mut txn)
.await?;
}
use crate::db::discussion_subscriptions::dsl as ds;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
.execute(&mut txn)
.await?;
if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
Ok::<_, crate::Error>(
(
token.clone(),
Json(
discussion_json(
&config,
&mut txn,
id,
token,
Path(DiscPath {
owner: tree.owner,
repo: tree.repo,
disc: number,
}),
Some(login),
Some(email),
)
.await?,
),
)
.into_response(),
)
} else {
Ok::<_, crate::Error>(
Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, number
))
.into_response(),
)
}
})
.scope_boxed()
})
.await
}
#[debug_handler]
pub async fn discussion(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
) -> Result<Response, crate::Error> {
debug!("discussion {:?}", tree);
let (uid, login, email) = if let Some((a, b, c)) = get_user_login_email(&jar, &config).await? {
(Some(a), Some(b), Some(c))
} else {
(None, None, None)
};
let mut db = config.db.get().await?;
let (repo, _) =
crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
Ok((
token.clone(),
Json(discussion_json(&config, &mut db, repo, token, Path(tree), login, email).await?),
)
.into_response())
}
async fn discussion_json(
config: &Config,
db: &mut AsyncPgConnection,
id: uuid::Uuid,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
login: Option<String>,
email: Option<String>,
) -> Result<Discussion, crate::Error> {
use crate::db::discussions::dsl as d;
use crate::db::users::dsl as users;
let (disc_id, title, author, opened, closed) = d::discussions
.inner_join(users::users)
.filter(d::repository_id.eq(id))
.filter(d::number.eq(tree.disc))
.select((d::id, d::title, users::login, d::creation_date, d::closed))
.get_result::<(_, _, _, DateTime<Utc>, Option<DateTime<Utc>>)>(db)
.await?;
use crate::db::comments::dsl as comments;
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussion_tags::dsl as dt;
use crate::db::tags::dsl as tags;
let mut comments: Vec<_> = comments::comments
.inner_join(users::users)
.filter(comments::discussion_id.eq(disc_id))
.select((
comments::id,
users::login,
comments::creation_date,
comments::contents,
comments::cached_html,
))
.order_by(comments::creation_date)
.get_results::<(uuid::Uuid, String, DateTime<Utc>, String, String)>(db)
.await?
.into_iter()
.map(
|(id, author, date, content, content_html)| DiscussionItem::Comment {
comment: Comment {
author,
id,
timestamp: date.timestamp(),
content,
content_html,
},
},
)
.collect();
for (id, name, color, date, active, author, dt_id, added) in dt::discussion_tags
.inner_join(users::users)
.left_join(tags::tags)
.filter(dt::discussion.eq(disc_id))
.select((
tags::id.nullable(),
tags::name.nullable(),
tags::color.nullable(),
dt::date,
dt::active,
users::login,
dt::id,
dt::addition,
))
.order_by(dt::date)
.get_results::<(
Option<uuid::Uuid>,
Option<String>,
Option<i32>,
DateTime<Utc>,
bool,
String,
uuid::Uuid,
bool,
)>(db)
.await?
{
let tag = if let (Some(id), Some(name), Some(color)) = (id, name, color) {
let color = Color(color);
Some(TagT {
name,
id,
color: color.to_string(),
fg: color.fg().to_string(),
active,
})
} else {
None
};
comments.push(DiscussionItem::Tag {
tag: Tag {
author,
id: dt_id,
timestamp: date.timestamp(),
tag,
added,
},
})
}
for (pushed_by, hash, timestamp, change_id, removed) in dc::discussion_changes
.inner_join(users::users)
.select((users::login, dc::change, dc::added, dc::id, dc::removed))
.get_results::<(
String,
String,
DateTime<Utc>,
uuid::Uuid,
Option<DateTime<Utc>>,
)>(db)
.await?
{
let locks = config.repo_locks.clone();
let repo = locks.get(&id).await?;
let mut header = repo
.changes
.get_header(&libpijul::Hash::from_base32(hash.as_bytes()).unwrap())?;
let hash_ = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();
comments.push(DiscussionItem::Patch {
patch: Patch {
can_add: if removed.is_some() {
can_add(&config, db, id, disc_id, hash_).await.is_ok()
} else {
false
},
can_remove: if removed.is_none() {
can_remove(&config, db, id, disc_id, hash_).await.is_ok()
} else {
false
},
authors: crate::change::get_authors(db, &mut header.authors).await?,
header,
pushed_by,
hash,
timestamp: timestamp.timestamp(),
removed: removed.as_ref().map(DateTime::timestamp),
id: change_id,
},
})
}
comments.sort_by_key(DiscussionItem::timestamp);
let auth_token = token.authenticity_token().ok();
Ok(Discussion {
owner: tree.owner,
repo: tree.repo,
login,
email,
token: auth_token,
uniq: rand::random(),
d: DiscussionT {
n: tree.disc,
id: disc_id,
title,
author,
closed: closed.map(|x| x.timestamp()),
opened: opened.timestamp(),
},
comments,
})
}
#[derive(Debug, Serialize)]
struct Discussion {
owner: String,
repo: String,
#[serde(skip_serializing_if = "Option::is_none")]
login: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
uniq: i64,
d: DiscussionT,
comments: Vec<DiscussionItem>,
}
#[derive(Debug, Serialize)]
struct DiscussionT {
n: i32,
id: uuid::Uuid,
title: String,
author: String,
#[serde(skip_serializing_if = "Option::is_none")]
closed: Option<i64>,
opened: i64,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum DiscussionItem {
Comment { comment: Comment },
Patch { patch: Patch },
Tag { tag: Tag },
}
impl DiscussionItem {
fn timestamp(&self) -> i64 {
match self {
DiscussionItem::Comment { comment } => comment.timestamp,
DiscussionItem::Patch { patch } => patch.timestamp,
DiscussionItem::Tag { tag } => tag.timestamp,
}
}
}
#[derive(Debug, Serialize)]
struct Comment {
author: String,
timestamp: i64,
id: uuid::Uuid,
content: String,
content_html: String,
}
#[derive(Debug, Serialize)]
struct Tag {
author: String,
timestamp: i64,
added: bool,
id: uuid::Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<TagT>,
}
#[derive(Debug, Serialize)]
struct TagT {
id: uuid::Uuid,
name: String,
fg: String,
color: String,
active: bool,
}
#[derive(Debug, Serialize)]
struct Patch {
can_add: bool,
can_remove: bool,
hash: String,
timestamp: i64,
pushed_by: String,
#[serde(skip_serializing_if = "Option::is_none")]
removed: Option<i64>,
header: libpijul::change::ChangeHeader,
authors: Vec<crate::change::Author>,
id: uuid::Uuid,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
enum Action {
#[serde(rename = "close")]
Close,
#[serde(rename = "reopen")]
Reopen,
#[serde(rename = "apply")]
Apply,
#[serde(rename = "unrecord")]
Unrecord,
}
#[derive(Debug, Deserialize)]
pub struct RemoveChangeForm {
discussion_change: uuid::Uuid,
token: String,
}
#[axum::debug_handler]
async fn add_change(
config: State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
tree: Path<DiscPath>,
form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
edit_change(true, config, jar, token, tree, form).await
}
#[axum::debug_handler]
async fn remove_change(
config: State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
tree: Path<DiscPath>,
form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
edit_change(false, config, jar, token, tree, form).await
}
async fn edit_change(
add: bool,
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Path(tree): Path<DiscPath>,
Form(form): Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
token.verify(&form.token)?;
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
use crate::db::repositories::dsl as r;
let uid = crate::get_user_id_strict(&jar)?;
config
.db
.get()
.await?
.transaction(|mut txn| {
(async move {
let repo_disc = dc::discussion_changes
.find(form.discussion_change)
.inner_join(d::discussions)
.inner_join(r::repositories.on(d::repository_id.eq(r::id)))
.filter(dc::pushed_by.eq(uid).or(crate::has_permissions!(
uid,
r::id,
Perm::EDIT_DISCUSSION.bits()
)))
.filter(r::is_active)
.select((r::id, d::id, dc::change))
.get_result::<(uuid::Uuid, uuid::Uuid, String)>(&mut txn)
.await
.optional()?;
let (repo_id, disc_id, hash) = if let Some(disc_id) = repo_disc {
disc_id
} else {
return Ok(StatusCode::FORBIDDEN.into_response());
};
let hash = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();
if add {
can_add(&config, &mut txn, repo_id, disc_id, hash).await?;
diesel::update(d::discussions.find(disc_id))
.set(d::changes.eq(d::changes + 1))
.execute(&mut txn)
.await?;
} else {
can_remove(&config, &mut txn, repo_id, disc_id, hash).await?;
diesel::update(d::discussions.find(disc_id))
.set(d::changes.eq(d::changes - 1))
.execute(&mut txn)
.await?;
}
diesel::update(dc::discussion_changes.find(form.discussion_change))
.set(dc::removed.eq(if add { None } else { Some(Utc::now()) }))
.execute(&mut txn)
.await?;
Ok(Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, tree.disc
))
.into_response())
})
.scope_boxed()
})
.await
}
async fn can_add(
config: &Config,
txn: &mut AsyncPgConnection,
repo_id: uuid::Uuid,
disc_id: uuid::Uuid,
hash: libpijul::Hash,
) -> Result<(), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
let other_changes = dc::discussion_changes
.filter(dc::discussion.eq(disc_id))
.select((dc::change, dc::removed))
.get_results::<(String, Option<DateTime<Utc>>)>(txn)
.await?;
let repo_locks = config.repo_locks.clone();
let repo = repo_locks.get(&repo_id).await.unwrap();
let change =
tokio::task::spawn_blocking(move || repo.changes.get_change(&hash).unwrap()).await?;
for (o, removed) in other_changes {
if removed.is_some() {
let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
if change.dependencies.contains(&o) {
return Err(crate::Error::DependedUpon);
}
}
}
Ok(())
}
async fn can_remove(
config: &Config,
txn: &mut AsyncPgConnection,
repo_id: uuid::Uuid,
disc_id: uuid::Uuid,
hash: libpijul::Hash,
) -> Result<(), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
let other_changes = dc::discussion_changes
.filter(dc::discussion.eq(disc_id))
.select((dc::change, dc::removed))
.get_results::<(String, Option<DateTime<Utc>>)>(txn)
.await?;
let repo_locks = config.repo_locks.clone();
let repo = repo_locks.get(&repo_id).await.unwrap();
tokio::task::spawn_blocking(move || {
for (o, removed) in other_changes {
if removed.is_none() {
let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
let change = repo.changes.get_change(&o).unwrap();
if change.dependencies.contains(&hash) {
return Err(crate::Error::DependedUpon);
}
}
}
Ok(())
})
.await?
}
#[derive(Debug, Deserialize)]
pub struct NewCommentForm {
#[serde(default)]
edit_comment: Option<uuid::Uuid>,
new_discussion_comment: String,
uniq: i64,
token: String,
#[serde(default)]
action: Option<Action>,
}
#[debug_handler]
pub async fn discussion_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
Path(tree): Path<DiscPath>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(form): Form<NewCommentForm>,
) -> Result<Response, crate::Error> {
debug!("discussion_post {:?}", form);
token.verify(&form.token)?;
debug!("new comment {:?}", tree);
let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let repo = crate::repository::repository(
&mut db,
&tree.owner,
&tree.repo,
uid,
Perm::CREATE_DISCUSSION,
)
.await?;
db.transaction(move |mut txn| {
(async move {
use crate::db::discussions::dsl as d;
let ip_sql =
ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
.unwrap();
let (disc_id, author): (uuid::Uuid, Option<uuid::Uuid>) = d::discussions
.filter(d::repository_id.eq(repo.id))
.filter(d::number.eq(tree.disc))
.select((d::id, d::author))
.get_result(&mut txn)
.await
.unwrap();
match form.action {
Some(Action::Close) | Some(Action::Reopen) => {
if author == Some(uid) || repo.permissions.contains(Perm::EDIT_DISCUSSION) {
use crate::db::discussion_tags::dsl as dt;
diesel::update(dt::discussion_tags)
.filter(dt::discussion.eq(disc_id))
.filter(dt::tag.is_null())
.set(dt::active.eq(false))
.execute(&mut txn)
.await?;
let n = diesel::insert_into(dt::discussion_tags)
.values((
dt::discussion.eq(disc_id),
dt::addition.eq(form.action == Some(Action::Reopen)),
dt::author.eq(uid),
))
.execute(&mut txn)
.await?;
if n > 0 {
if let Some(Action::Close) = form.action {
diesel::update(d::discussions.find(disc_id))
.set(d::closed.eq(diesel::dsl::now))
.execute(&mut txn)
.await?;
} else {
diesel::update(d::discussions.find(disc_id))
.set(d::closed.eq(None::<chrono::DateTime<chrono::Utc>>))
.execute(&mut txn)
.await?;
}
}
}
}
_ => {}
}
use crate::db::comments::dsl as c;
let comment = form.new_discussion_comment.trim();
if !comment.is_empty() {
let comment_html =
crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
if let Some(cid) = form.edit_comment {
diesel::update(c::comments.find(cid))
.filter(c::author.eq(uid))
.set((
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
))
.execute(&mut txn)
.await?;
} else {
diesel::insert_into(c::comments)
.values((
c::discussion_id.eq(disc_id),
c::author.eq(uid),
c::creation_ip.eq(&ip_sql),
c::contents.eq(comment),
c::cached_html.eq(&comment_html.rendered),
c::uniq.eq(&form.uniq),
))
.execute(&mut txn)
.await?;
}
}
use crate::db::discussion_subscriptions::dsl as ds;
diesel::insert_into(ds::discussion_subscriptions)
.values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
.execute(&mut txn)
.await?;
if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
Ok::<_, crate::Error>(
(
token.clone(),
Json(
discussion_json(
&config,
&mut txn,
repo.id,
token,
Path(tree),
Some(login),
Some(email),
)
.await?,
),
)
.into_response(),
)
} else {
Ok::<_, crate::Error>(
Redirect::to(&format!(
"/{}/{}/discussion/{}",
tree.owner, tree.repo, tree.disc
))
.into_response(),
)
}
})
.scope_boxed()
})
.await
}
pub async fn discussion_changelist(
db: &mut diesel_async::AsyncPgConnection,
changes: &crate::repository::changestore::FileSystem,
repo: uuid::Uuid,
disc: i32,
) -> Result<Vec<u8>, crate::Error> {
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
let mut n: usize = 0;
let mut m = libpijul::pristine::Merkle::zero();
let mut data = Vec::new();
use libpijul::pristine::Base32;
use std::io::Write;
for h in dc::discussion_changes
.inner_join(d::discussions)
.filter(d::repository_id.eq(repo))
.filter(d::number.eq(disc))
.filter(dc::removed.is_null())
.order_by(dc::added)
.select(dc::change)
.get_results::<String>(db)
.await?
{
let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
if changes.has_change(&hh) {
m = m.next(&hh);
writeln!(data, "{}.{}.{}", n, h, m.to_base32())?;
n += 1
}
}
data.push(b'\n');
Ok(data)
}
pub async fn discussion_state(
db: &mut diesel_async::AsyncPgConnection,
repo: uuid::Uuid,
disc: i32,
at: Option<u64>,
) -> Result<(usize, libpijul::pristine::Merkle), crate::Error> {
use crate::db::discussion_changes::dsl as dc;
use crate::db::discussions::dsl as d;
let q = dc::discussion_changes
.inner_join(d::discussions)
.filter(d::repository_id.eq(repo))
.filter(d::number.eq(disc))
.filter(dc::removed.is_null())
.order_by(dc::added)
.select(dc::change);
let it = if let Some(at) = at {
q.limit(at as i64 + 1).get_results::<String>(db).await?
} else {
q.get_results::<String>(db).await?
};
let mut n = 0;
let mut m = libpijul::pristine::Merkle::zero();
use libpijul::pristine::Base32;
for h in it {
let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
m = m.next(&hh);
n += 1
}
Ok((n, m))
}