|
|
@@ -0,0 +1,341 @@ |
|
|
|
#![no_std] |
|
|
|
|
|
|
|
mod test; |
|
|
|
|
|
|
|
use core::cmp::PartialEq; |
|
|
|
use soroban_sdk::{contract, contracttype, contractimpl, Env, String, Vec, BytesN, Map}; |
|
|
|
|
|
|
|
macro_rules! if_let_some { |
|
|
|
($opt:expr, $body:block) => { |
|
|
|
if let Some(_) = $opt { |
|
|
|
$body |
|
|
|
} |
|
|
|
}; |
|
|
|
($opt:expr, mut $var:ident, $body:block) => { |
|
|
|
if let Some(mut $var) = $opt { |
|
|
|
$body |
|
|
|
} |
|
|
|
}; |
|
|
|
($opt:expr, $var:ident, $body:block) => { |
|
|
|
if let Some($var) = $opt { |
|
|
|
$body |
|
|
|
} |
|
|
|
}; |
|
|
|
($opt:expr, mut $var:ident, $some_case:block, $none_case:block) => { |
|
|
|
if let Some(mut $var) = $opt { |
|
|
|
$some_case |
|
|
|
} else { |
|
|
|
$none_case |
|
|
|
} |
|
|
|
}; |
|
|
|
($opt:expr, $var:ident, $some_case:block, $none_case:block) => { |
|
|
|
if let Some($var) = $opt { |
|
|
|
$some_case |
|
|
|
} else { |
|
|
|
$none_case |
|
|
|
} |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
#[contracttype] |
|
|
|
pub enum DataKey { |
|
|
|
IssueRecord, |
|
|
|
CommentRecord, |
|
|
|
ParagraphRecord(BytesN<16>, u32), |
|
|
|
PositiveVoteRecord, |
|
|
|
NegativeVoteRecord, |
|
|
|
} |
|
|
|
|
|
|
|
pub enum Error { |
|
|
|
NoIssuesFound, |
|
|
|
NoCommentsFound, |
|
|
|
} |
|
|
|
|
|
|
|
#[contract] |
|
|
|
pub struct PuffPastry; |
|
|
|
|
|
|
|
#[contracttype] |
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)] |
|
|
|
pub struct IssuePost { |
|
|
|
id: BytesN<16>, |
|
|
|
title: String, |
|
|
|
summary: String, |
|
|
|
paragraph_count: u32, |
|
|
|
positive_votes: u64, |
|
|
|
negative_votes: u64, |
|
|
|
telegram_handle: String, |
|
|
|
created_at: u64, |
|
|
|
} |
|
|
|
|
|
|
|
#[contracttype] |
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)] |
|
|
|
pub struct Comment { |
|
|
|
id: BytesN<16>, |
|
|
|
issue_id: BytesN<16>, |
|
|
|
text: String, |
|
|
|
positive_votes: u64, |
|
|
|
negative_votes: u64, |
|
|
|
created_at: u64, |
|
|
|
} |
|
|
|
|
|
|
|
impl IssuePost { |
|
|
|
pub fn default(env: &Env) -> Self { |
|
|
|
Self { |
|
|
|
id: BytesN::from_array(env, &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), |
|
|
|
title: String::from_str(env, ""), |
|
|
|
summary: String::from_str(&env, ""), |
|
|
|
paragraph_count: 0, |
|
|
|
positive_votes: 0, |
|
|
|
negative_votes: 0, |
|
|
|
telegram_handle: String::from_str(env, ""), |
|
|
|
created_at: 0 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[contractimpl] |
|
|
|
impl PuffPastry { |
|
|
|
pub fn add_issue(env: Env, id: BytesN<16>, title: String, paragraphs: Vec<String>, telegram_handle: String) { |
|
|
|
let metadata = IssuePost { |
|
|
|
id: id.clone(), |
|
|
|
title, |
|
|
|
summary: String::from_str(&env, ""), |
|
|
|
paragraph_count: paragraphs.len(), |
|
|
|
positive_votes: 0, |
|
|
|
negative_votes: 0, |
|
|
|
telegram_handle, |
|
|
|
created_at: env.ledger().timestamp(), |
|
|
|
}; |
|
|
|
|
|
|
|
let mut issues = Self::get_issues(&env); |
|
|
|
issues.push_back(metadata); |
|
|
|
env.storage().persistent().set(&DataKey::IssueRecord, &issues); |
|
|
|
|
|
|
|
for (i, paragraph) in paragraphs.iter().enumerate() { |
|
|
|
env.storage().persistent().set(&DataKey::ParagraphRecord(id.clone(), i as u32), ¶graph); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn add_comment(env: Env, id: BytesN<16>, issue_id: BytesN<16>, text: String) -> bool { |
|
|
|
let comment = Comment { |
|
|
|
id, |
|
|
|
issue_id: issue_id.clone(), |
|
|
|
text, |
|
|
|
positive_votes: 0, |
|
|
|
negative_votes: 0, |
|
|
|
created_at: env.ledger().timestamp(), |
|
|
|
}; |
|
|
|
|
|
|
|
if_let_some!( |
|
|
|
Self::get_issue(env.clone(), issue_id), |
|
|
|
{ |
|
|
|
let mut comments = Self::get_comments(&env); |
|
|
|
comments.push_back(comment); |
|
|
|
env.storage().persistent().set(&DataKey::CommentRecord, &comments); |
|
|
|
return true |
|
|
|
} |
|
|
|
); |
|
|
|
false |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_comments_for_issue(env: &Env, issue_id: BytesN<16>) -> Vec<Comment> { |
|
|
|
let comments = Self::get_comments(&env); |
|
|
|
let mut issue_comments = Vec::new(env); |
|
|
|
|
|
|
|
for comment in comments.iter().filter(|c| c.issue_id == issue_id) { |
|
|
|
issue_comments.push_back(comment); |
|
|
|
} |
|
|
|
|
|
|
|
issue_comments |
|
|
|
} |
|
|
|
|
|
|
|
pub fn increase_positive_comment_vote(env: &Env, comment_id: BytesN<16>) { |
|
|
|
let mut comments = Self::get_comments(&env); |
|
|
|
|
|
|
|
if_let_some!( |
|
|
|
comments.iter().position(|c| c.id == comment_id), |
|
|
|
index, |
|
|
|
{ |
|
|
|
let mut comment = comments.get(index as u32).unwrap().clone(); |
|
|
|
comment.positive_votes = comment.positive_votes.checked_add(1).unwrap_or_else(|| 0); |
|
|
|
comments.set(index as u32, comment.clone()); |
|
|
|
|
|
|
|
env.storage().persistent().set(&DataKey::CommentRecord, &comments); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
pub fn increase_negative_comment_vote(env: &Env, comment_id: BytesN<16>) { |
|
|
|
let mut comments = Self::get_comments(&env); |
|
|
|
|
|
|
|
if_let_some!( |
|
|
|
comments.iter().position(|c| c.id == comment_id), |
|
|
|
index, |
|
|
|
{ |
|
|
|
let mut comment = comments.get(index as u32).unwrap().clone(); |
|
|
|
comment.negative_votes = comment.negative_votes.checked_add(1).unwrap_or_else(|| 0); |
|
|
|
comments.set(index as u32, comment.clone()); |
|
|
|
|
|
|
|
env.storage().persistent().set(&DataKey::CommentRecord, &comments); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
pub fn increase_positive_vote(env: &Env, user_id: u64, issue_id: BytesN<16>) -> bool { |
|
|
|
let mut issues = Self::get_issues(env); |
|
|
|
|
|
|
|
if_let_some!( |
|
|
|
issues.iter().position(|i| i.id == issue_id), |
|
|
|
index, |
|
|
|
{ |
|
|
|
let mut issue = issues.get(index as u32).unwrap().clone(); |
|
|
|
issue.positive_votes = issue.positive_votes.checked_add(1).unwrap_or_else(|| 0); |
|
|
|
issues.set(index as u32, issue.clone()); |
|
|
|
|
|
|
|
let votes = Self::record_user_positive_vote(&env, user_id, issue_id); |
|
|
|
env.storage().persistent().set(&DataKey::PositiveVoteRecord, &votes); |
|
|
|
|
|
|
|
env.storage().persistent().set(&DataKey::IssueRecord, &issues); |
|
|
|
return true |
|
|
|
} |
|
|
|
); |
|
|
|
false |
|
|
|
} |
|
|
|
|
|
|
|
pub fn increase_negative_vote(env: &Env, user_id: u64, issue_id: BytesN<16>) -> bool { |
|
|
|
let mut issues = Self::get_issues(env); |
|
|
|
|
|
|
|
if_let_some!( |
|
|
|
issues.iter().position(|i| i.id == issue_id), |
|
|
|
index, |
|
|
|
{ |
|
|
|
let mut issue = issues.get(index as u32).unwrap().clone(); |
|
|
|
issue.positive_votes = issue.negative_votes.checked_add(1).unwrap_or_else(|| 0); |
|
|
|
issues.set(index as u32, issue.clone()); |
|
|
|
|
|
|
|
let votes = Self::record_user_negative_vote(&env, user_id, issue_id); |
|
|
|
env.storage().persistent().set(&DataKey::NegativeVoteRecord, &votes); |
|
|
|
|
|
|
|
env.storage().persistent().set(&DataKey::IssueRecord, &issues); |
|
|
|
return true |
|
|
|
} |
|
|
|
); |
|
|
|
false |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_positive_votes_for_user(env: &Env, user_id: u64) -> Option<Vec<BytesN<16>>> { |
|
|
|
let votes = Self::get_positive_votes(&env); |
|
|
|
votes.get(user_id) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_negative_votes_for_user(env: &Env, user_id: u64) -> Option<Vec<BytesN<16>>> { |
|
|
|
let votes = Self::get_negative_votes(&env); |
|
|
|
votes.get(user_id) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_issue(env: Env, id: BytesN<16>) -> Option<IssuePost> { |
|
|
|
let issues = Self::get_issues(&env); |
|
|
|
issues.into_iter().find(|i| i.id == id).or_else(|| None) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_paragraphs_for_issue(env: &Env, id: BytesN<16>) -> Option<Vec<String>> { |
|
|
|
// This method is here to allow for "chunking", so that we don't store values that exceed the maximum size in Soroban |
|
|
|
let mut content = Vec::new(&env); |
|
|
|
if_let_some!( |
|
|
|
Self::get_issue(env.clone(), id.clone()), |
|
|
|
metadata, |
|
|
|
{ |
|
|
|
for i in 0..metadata.paragraph_count { |
|
|
|
let paragraph: String = env.storage().persistent().get(&DataKey::ParagraphRecord(id.clone(), i)).expect("Paragraph not found"); |
|
|
|
content.push_back(paragraph); |
|
|
|
} |
|
|
|
return Some(content) |
|
|
|
} |
|
|
|
); |
|
|
|
None |
|
|
|
} |
|
|
|
|
|
|
|
pub fn list_issues(env: &Env) -> Vec<IssuePost> { |
|
|
|
let original_issues = Self::get_issues(env); |
|
|
|
let mut updated_issues = Vec::new(env); |
|
|
|
|
|
|
|
for issue in original_issues.iter() { |
|
|
|
if_let_some!( |
|
|
|
Self::get_paragraphs_for_issue(env, issue.id.clone()), |
|
|
|
paragraphs, |
|
|
|
{ |
|
|
|
let summary = paragraphs.first() |
|
|
|
.map(|i| i.clone()) |
|
|
|
.unwrap_or_else(|| String::from_str(env, "Unknown summary")); |
|
|
|
|
|
|
|
let updated_issue = IssuePost { |
|
|
|
id: issue.id.clone(), |
|
|
|
title: issue.title.clone(), |
|
|
|
summary, |
|
|
|
paragraph_count: issue.paragraph_count, |
|
|
|
positive_votes: issue.positive_votes, |
|
|
|
negative_votes: issue.negative_votes, |
|
|
|
telegram_handle: issue.telegram_handle, |
|
|
|
created_at: issue.created_at, |
|
|
|
}; |
|
|
|
|
|
|
|
updated_issues.push_back(updated_issue); |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
updated_issues |
|
|
|
} |
|
|
|
|
|
|
|
fn get_issues(env: &Env) -> Vec<IssuePost> { |
|
|
|
env.storage().persistent().get(&DataKey::IssueRecord).unwrap_or_else(|| Vec::new(&env)) |
|
|
|
} |
|
|
|
|
|
|
|
fn get_comments(env: &Env) -> Vec<Comment> { |
|
|
|
env.storage().persistent().get(&DataKey::CommentRecord).unwrap_or_else(|| Vec::new(&env)) |
|
|
|
} |
|
|
|
|
|
|
|
fn get_positive_votes(env: &Env) -> Map<u64, Vec<BytesN<16>>> { |
|
|
|
env.storage().persistent().get(&DataKey::PositiveVoteRecord).unwrap_or_else(|| Map::new(&env)) |
|
|
|
} |
|
|
|
|
|
|
|
fn get_negative_votes(env: &Env) -> Map<u64, Vec<BytesN<16>>> { |
|
|
|
env.storage().persistent().get(&DataKey::NegativeVoteRecord).unwrap_or_else(|| Map::new(&env)) |
|
|
|
} |
|
|
|
|
|
|
|
fn record_user_positive_vote(env: &&Env, user_id: u64, issue_id: BytesN<16>) -> Map<u64, Vec<BytesN<16>>> { |
|
|
|
let mut votes = Self::get_positive_votes(env); |
|
|
|
if_let_some!( |
|
|
|
votes.get(user_id), |
|
|
|
mut user_positive_votes, |
|
|
|
{ |
|
|
|
if !user_positive_votes.contains(&issue_id) { |
|
|
|
user_positive_votes.push_back(issue_id); |
|
|
|
votes.set(user_id, user_positive_votes); |
|
|
|
} |
|
|
|
}, |
|
|
|
{ |
|
|
|
votes.set(user_id, Vec::from_array(&env, [issue_id])); |
|
|
|
} |
|
|
|
); |
|
|
|
votes |
|
|
|
} |
|
|
|
|
|
|
|
fn record_user_negative_vote(env: &&Env, user_id: u64, issue_id: BytesN<16>) -> Map<u64, Vec<BytesN<16>>> { |
|
|
|
let mut votes = Self::get_negative_votes(env); |
|
|
|
if_let_some!( |
|
|
|
votes.get(user_id), |
|
|
|
mut user_negative_votes, |
|
|
|
{ |
|
|
|
if !user_negative_votes.contains(&issue_id) { |
|
|
|
user_negative_votes.push_back(issue_id); |
|
|
|
votes.set(user_id, user_negative_votes); |
|
|
|
} |
|
|
|
}, |
|
|
|
{ |
|
|
|
votes.set(user_id, Vec::from_array(&env, [issue_id])); |
|
|
|
} |
|
|
|
); |
|
|
|
votes |
|
|
|
} |
|
|
|
} |