Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ build:
./build.sh bucket
./build.sh taggr

check:
cargo check --tests
cargo fmt --check
npx tsc --noEmit
npx prettier --check src/frontend/

format:
cargo fmt
npx prettier --write src/frontend/

test:
make e2e_build
make local_deploy
Expand Down
11 changes: 11 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,16 @@ pkgs.mkShell {
buildInputs = [
pkgs.podman
pkgs.gnumake
pkgs.cargo
pkgs.rustc
pkgs.rustfmt
pkgs.gcc
pkgs.nodejs
];

# Bypass the repo's rust-toolchain.toml so the nixpkgs-pinned
# cargo/rustc are used instead of trying to invoke rustup.
shellHook = ''
export RUSTUP_TOOLCHAIN=
'';
}
12 changes: 0 additions & 12 deletions src/backend/env/canisters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use ic_cdk_management_canister::{
};
use ic_ledger_types::{Tokens, MAINNET_GOVERNANCE_CANISTER_ID};
use ic_xrc_types::{Asset, GetExchangeRateRequest, GetExchangeRateResult};
use icrc_ledger_types::icrc3::transactions::{GetTransactionsRequest, GetTransactionsResponse};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::HashMap;
Expand Down Expand Up @@ -366,14 +365,3 @@ pub async fn top_up() {
}
}
}

pub async fn get_transactions(
canister_id: Principal,
args: GetTransactionsRequest,
) -> Result<GetTransactionsResponse, String> {
let (response,): (GetTransactionsResponse,) =
call_canister(canister_id, "get_transactions", (args,))
.await
.map_err(|e| format!("failed to call ledger: {:?}", e))?;
Ok(response)
}
4 changes: 2 additions & 2 deletions src/backend/env/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub struct Config {
pub default_max_downvotes: u32,

pub domain_cost: Credits,
pub feature_cost: Credits,
pub account_activation_cost: Credits,
pub post_cost: Credits,
pub blob_cost: Credits,
pub poll_cost: Credits,
Expand Down Expand Up @@ -270,7 +270,7 @@ pub const CONFIG: &Config = &Config {
default_max_downvotes: 15,

domain_cost: 1000,
feature_cost: 1000,
account_activation_cost: 1000,
post_cost: 2,
blob_cost: 20,
poll_cost: 3,
Expand Down
126 changes: 4 additions & 122 deletions src/backend/env/features.rs
Original file line number Diff line number Diff line change
@@ -1,134 +1,16 @@
use std::collections::HashSet;

use candid::Principal;
use serde::{Deserialize, Serialize};

use crate::{
env::{post::Meta, Time, YEAR},
mutate,
};

use super::{
config::CONFIG,
post::{Extension, Post, PostId},
token::Token,
user::UserId,
State,
};

const STATUS_IMPLEMENTED: u8 = 1;
const STATUS_OPEN: u8 = 0;
use super::{user::UserId, Time};

// Retained only so the `Memory::features` index can still be deserialized.
// The next release drops the field; serde silently ignores it once the migration
// has drained the index.
#[derive(Default, Serialize, Deserialize)]
pub struct Feature {
// Users who voted for increased priority
pub supporters: HashSet<UserId>,
// 0: requested, 1: implemented
pub status: u8,
#[serde(default)]
pub last_activity: Time,
}

/// Returns a list of all feature ids and current collective voting power of all supporters.
pub fn features<'a>(
state: &'a State,
ids: &'a [PostId],
now: Time,
) -> Box<dyn DoubleEndedIterator<Item = ((&'a Post, Meta<'a>), Token, Feature)> + 'a> {
let transform_feature = move |(post_id, feature): (&PostId, Feature)| {
if feature.status == STATUS_OPEN && feature.last_activity + YEAR <= now {
return None;
}
let tokens = feature
.supporters
.iter()
.map(|user_id| {
state
.users
.get(user_id)
.map(|user| user.total_balance())
.unwrap_or_default()
})
.sum::<Token>();
Post::get(state, post_id).map(|post| (post.with_meta(state), tokens, feature))
};

if !ids.is_empty() {
return Box::new(
ids.iter()
.filter_map(move |id| state.memory.features.get(id).map(|feature| (id, feature)))
.filter_map(transform_feature),
);
}
Box::new(state.memory.features.iter().filter_map(transform_feature))
}

pub fn toggle_feature_support(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> {
mutate(|state| {
let user_id = state.principal_to_user(caller).ok_or("no user found")?.id;
let mut feature = state.memory.features.remove(&post_id)?;
if feature.supporters.contains(&user_id) {
feature.supporters.remove(&user_id);
} else {
feature.last_activity = now;
feature.supporters.insert(user_id);
}
state
.memory
.features
.insert(post_id, feature)
.expect("couldn't re-insert feature");

Ok(())
})
}

pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> {
mutate(|state| {
let user = state.principal_to_user(caller).ok_or("no user found")?;
let user_name = user.name.clone();

let post = Post::get(state, &post_id).ok_or("post not found")?;
if post.user != user.id || !matches!(post.extension.as_ref(), Some(&Extension::Feature)) {
return Err("no post with a feature found".into());
}

if state.memory.features.get(&post_id).is_some() {
return Err("feature already exists".into());
}

state
.memory
.features
.insert(
post_id,
Feature {
supporters: Default::default(),
status: STATUS_OPEN,
last_activity: now,
},
)
.expect("couldn't persist feature");

let _ = state.system_message(
format!(
"A [new feature](#/post/{}) was created by @{}",
post_id, user_name
),
CONFIG.dao_realm.into(),
);

Ok(())
})
}

pub fn close_feature(state: &mut State, post_id: PostId) -> Result<(), String> {
let mut feature = state.memory.features.remove(&post_id)?;
feature.status = STATUS_IMPLEMENTED;
state
.memory
.features
.insert(post_id, feature)
.expect("couldn't re-insert feature");
Ok(())
}
1 change: 1 addition & 0 deletions src/backend/env/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Api {
pub struct Memory {
api: Api,
pub posts: ObjectManager<PostId, Post>,
// TODO: remove
pub features: ObjectManager<PostId, Feature>,
#[serde(default)]
pub ledger: ObjectManager<u32, Transaction>,
Expand Down
15 changes: 1 addition & 14 deletions src/backend/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ pub mod realms;
pub mod reports;
pub mod search;
pub mod storage;
pub mod tip;
pub mod token;
pub mod user;

Expand All @@ -67,7 +66,6 @@ pub const HOUR: u64 = 60 * MINUTE;
pub const DAY: u64 = 24 * HOUR;
pub const WEEK: u64 = 7 * DAY;
pub const MONTH: u64 = 30 * DAY;
pub const YEAR: u64 = 52 * WEEK;

pub const MAX_USER_ID: UserId = 9_007_199_254_740_991; // Number.MAX_SAFE_INTEGER in JS

Expand Down Expand Up @@ -316,7 +314,7 @@ impl State {
let (username, len, is_deactivated) = {
let user = self.principal_to_user_mut(caller).ok_or("user not found")?;
user.change_credits(
CONFIG.feature_cost,
CONFIG.account_activation_cost,
CreditsDelta::Minus,
"profile privacy change",
)?;
Expand Down Expand Up @@ -746,7 +744,6 @@ impl State {
cleanup_penalty,
adult_content,
comments_filtering,
tokens,
max_downvotes,
..
} = realm;
Expand All @@ -763,9 +760,6 @@ impl State {
if !logo.is_empty() {
realm.logo = logo;
}
if tokens.as_ref().is_some_and(|t| t.len() > 50) {
return Err("tokens count above 50".into());
}
let description_change = realm.description != description;
realm.description = description;
if realm.controllers != controllers {
Expand Down Expand Up @@ -796,7 +790,6 @@ impl State {
realm.last_setting_update = time();
realm.adult_content = adult_content;
realm.comments_filtering = comments_filtering;
realm.tokens = tokens;
if description_change {
self.notify_with_filter(
&|user| user.realms.contains(&realm_id),
Expand Down Expand Up @@ -2731,12 +2724,6 @@ impl State {
Some(Extension::Poll(_)) => {
self.pending_polls.remove(&post_id);
}
Some(Extension::Feature) => {
self.memory
.features
.remove(&post_id)
.expect("couldn't delete feature");
}
_ => {}
};

Expand Down
43 changes: 7 additions & 36 deletions src/backend/env/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::cmp::{Ordering, PartialOrd};
use super::config::DOWNVOTE_REACTION_ID;
use super::*;
use super::{storage::Storage, user::UserId};
use crate::env::tip::Tip;
use crate::mutate;
use ic_cdk::api::msg_caller as caller;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -49,6 +48,8 @@ pub enum Extension {
Poll(Poll),
Proposal(u32),
Repost(PostId),
// Retained so cold-stored posts that reference the old feature extension still
// deserialize. No new posts use it and no behavior is attached to it anymore.
Feature,
}

Expand Down Expand Up @@ -94,9 +95,6 @@ pub struct Post {
#[serde(default)]
pub encrypted: bool,

#[serde(default)]
pub external_tips: Vec<Tip>,

#[serde(default)]
pub hidden_for: Vec<UserId>,
}
Expand Down Expand Up @@ -161,7 +159,6 @@ impl Post {
encrypted: false,
realm,
heat,
external_tips: Default::default(),
hidden_for: Default::default(),
}
}
Expand Down Expand Up @@ -352,8 +349,6 @@ impl Post {
+ blobs as Credits * CONFIG.blob_cost
+ if matches!(self.extension, Some(Extension::Poll(_))) {
CONFIG.poll_cost
} else if matches!(self.extension, Some(Extension::Feature)) {
CONFIG.feature_cost
} else {
0
}
Expand Down Expand Up @@ -771,11 +766,8 @@ impl Post {
return;
};

// We never encrypt feature requests and proposals, as they are supposed to be public.
let skip = matches!(
post.extension,
Some(Extension::Feature) | Some(Extension::Proposal(_))
);
// We never encrypt proposals, as they are supposed to be public.
let skip = matches!(post.extension, Some(Extension::Proposal(_)));
if !skip {
let encrypt = !post.encrypted;
post.body = xor(&post.body, seed, encrypt);
Expand Down Expand Up @@ -1093,7 +1085,7 @@ mod tests {
assert_eq!(state.posts.len(), 10);
// Trigger post archiving
archive_cold_posts(state, 5).unwrap();
assert!(state.memory.health("B").starts_with("boundary=1044B"));
assert!(state.memory.health("B").starts_with("boundary=969B"));
assert!(state.memory.health("B").ends_with("segments=0"));

// Make sure we have the right numbers in cold and hot memories
Expand Down Expand Up @@ -1135,7 +1127,7 @@ mod tests {
assert!(!Post::get(state, &3).unwrap().archived);
assert_eq!(state.posts.len(), 8);
assert_eq!(state.memory.posts.len(), 3);
assert!(state.memory.health("B").starts_with("boundary=1044B"));
assert!(state.memory.health("B").starts_with("boundary=969B"));
assert!(state.memory.health("B").ends_with("segments=2"));

// Archive posts again
Expand All @@ -1144,7 +1136,7 @@ mod tests {
assert_eq!(state.memory.posts.len(), 6);
// Segments were reduced, becasue the new post 10 fits into a gap left from one of the
// old posts
assert!(state.memory.health("B").starts_with("boundary=1460B"));
assert!(state.memory.health("B").starts_with("boundary=1355B"));
assert!(state.memory.health("B").ends_with("segments=1"));
});
}
Expand Down Expand Up @@ -1326,27 +1318,6 @@ mod tests {
assert_eq!(post.body, original_body);
assert_eq!(post.patches[0].1, original_patch);

// Test feature post is not encrypted
let feature_id = Post::create(
state,
"Feature request".into(),
&[],
p,
1,
None,
None,
Some(Extension::Feature),
)
.unwrap();

let feature_body = Post::get(state, &feature_id).unwrap().body.clone();
assert!(!Post::get(state, &feature_id).unwrap().encrypted);

Post::crypt(state, feature_id, "test_seed");
let post = Post::get(state, &feature_id).unwrap();
assert!(!post.encrypted);
assert_eq!(post.body, feature_body);

// Test proposal post is not encrypted
let proposal_id = Post::create(
state,
Expand Down
Loading
Loading