Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chainmail: implement save/unsave message #855 #856

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3ade844
archive implemented
cool-ant Sep 19, 2024
981aac6
adding plugin functionality
cool-ant Sep 19, 2024
5b8640e
save and unsave largely fleshed out; commiting before trying to move …
cool-ant Sep 20, 2024
c43cf0a
code restructured to separate out table defs and compiling; before RE…
cool-ant Sep 20, 2024
783ba90
few more doc additions
cool-ant Sep 23, 2024
a9f6c87
final minor tweak to REST API for message-by-id
cool-ant Sep 23, 2024
7ede9c5
Merge branch 'mm/chainmail-implement-archive' into mm/chainmail-impl-…
cool-ant Oct 7, 2024
8627a21
now compiles post merge
cool-ant Oct 7, 2024
a360433
Merge branch 'main' into mm/chainmail-impl-save
cool-ant Oct 9, 2024
bb46c0e
fixing a compile error
cool-ant Oct 9, 2024
2c5dba8
fixing serveSys
cool-ant Oct 10, 2024
21dcb00
stashing
cool-ant Oct 10, 2024
3914e24
save now executing
cool-ant Oct 11, 2024
14ad7b1
save/unsave largely working; save/unsave/archive interactions impleme…
cool-ant Oct 15, 2024
4dd13d5
get_msg_by_id works; save/unsave working
cool-ant Oct 15, 2024
846676c
Merge branch 'main' into mm/chainmail-impl-save
cool-ant Oct 15, 2024
87959fe
removed comments
cool-ant Oct 15, 2024
239cec2
chicken scratch doc removed; deserialization for msg_id working; comm…
cool-ant Oct 17, 2024
f75afe8
simplifying deserialization
cool-ant Oct 17, 2024
69419f4
spliting code betweenf files
cool-ant Oct 17, 2024
cecd7d2
separating out another query func
cool-ant Oct 17, 2024
52f444e
minor cleanup
cool-ant Oct 17, 2024
6b15ebb
removing final awkwardness and printlns
cool-ant Oct 17, 2024
0e1ea52
renaming a file
cool-ant Oct 17, 2024
f13c8fd
adding documentation to plugin and service
cool-ant Oct 17, 2024
1676b6f
Merge branch 'main' into mm/chainmail-impl-save
cool-ant Oct 17, 2024
228c0f5
removing unnecessary use of Wrapper for self-call
cool-ant Oct 18, 2024
5435efb
removing now-unnecessary recursive option on service
cool-ant Oct 21, 2024
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
1 change: 1 addition & 0 deletions services/user/Chainmail/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions services/user/Chainmail/plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ psibase = { path = "../../../../rust/psibase/" }
chainmail = { path = "../service" }
serde_json = "1.0.128"
serde = "1.0.210"
serde-aux = "4.5.0"

[dev-dependencies]
chainmail_package = { path = "..", version = "0.13.0" }
Expand Down
8 changes: 7 additions & 1 deletion services/user/Chainmail/plugin/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use crate::bindings::host::common::types::{Error, PluginId};
#[derive(PartialEq, Eq, Hash)]
pub enum ErrorType {
QueryResponseParseError,
InvalidMsgId,
}

fn my_plugin_id() -> PluginId {
return PluginId {
service: "identity".to_string(),
service: "chainmail".to_string(),
plugin: "plugin".to_string(),
};
}
Expand All @@ -20,6 +21,11 @@ impl ErrorType {
producer: my_plugin_id(),
message: format!("Query response parsing error: {}", msg),
},
ErrorType::InvalidMsgId => Error {
code: self as u32,
producer: my_plugin_id(),
message: format!("No message found with msg_id {}", msg),
},
}
}
}
116 changes: 60 additions & 56 deletions services/user/Chainmail/plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
//! Chainmail Plugin: Basic Chainmail API
//!
//! Provides a convenient API for interacting with Chainmail, a simple email-like client.

#[allow(warnings)]
mod bindings;
mod errors;
mod queries;
mod serde_structs;

use bindings::exports::chainmail::plugin::api::{Error, Guest as Api};
use bindings::exports::chainmail::plugin::queries::{Guest as Query, Message};
use bindings::host::common::server as CommonServer;
use bindings::exports::chainmail::plugin::{
api::{Error, Guest as Api},
queries::{Guest as Query, Message},
};
use bindings::transact::plugin::intf as Transact;
use errors::ErrorType::QueryResponseParseError;
use psibase::fracpack::Pack;
use psibase::AccountNumber;
use serde::Deserialize;
use queries::{get_msg_by_id, query_messages_endpoint};

/// Struct that implements the Api as well as the Query interfaces
struct ChainmailPlugin;

#[derive(Debug, Deserialize)]
struct MessageSerde {
msg_id: String,
receiver: String,
sender: String,
subject: String,
body: String,
}

impl Api for ChainmailPlugin {
/// Send a message
fn send(receiver: String, subject: String, body: String) -> Result<(), Error> {
Transact::add_action_to_transaction(
"send",
Expand All @@ -36,66 +35,71 @@ impl Api for ChainmailPlugin {
Ok(())
}

/// Archive a message
/// - msg_id: the message's rowid (from the events table)
///
/// Archiving is equivalent to deleting, given message can't be truly deleted.
/// Archiving is a proactive action equivalent to a node pruning an message (an message's historical event)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be better if these comments were in the wit document. Docs in wit are actually extractable from the raw wasm, and therefore can be used to display docs for a plugin without source code access

fn archive(msg_id: u64) -> Result<(), Error> {
Transact::add_action_to_transaction(
"archive",
&chainmail::action_structs::archive { msg_id }.packed(),
)?;
Ok(())
}
}

fn query_messages_endpoint(
sender: Option<String>,
receiver: Option<String>,
archived_requested: bool,
) -> Result<Vec<Message>, Error> {
let mut endpoint = String::from("/api/messages?");
if archived_requested {
endpoint += "archived=true&";
}
if sender.is_some() {
endpoint += &format!("sender={}", sender.clone().unwrap());
}
if sender.is_some() && receiver.is_some() {
endpoint += "&";
}
if receiver.is_some() {
endpoint += &format!("receiver={}", receiver.unwrap());
}
/// Save the message
/// - msg_id: the message's rowid (from the events table)
///
/// Saving a message moves it to state, making it long-lasting (immune to pruning)
fn save(msg_id: u64) -> Result<(), Error> {
let msg = get_msg_by_id(msg_id)?;

let resp = serde_json::from_str::<Vec<MessageSerde>>(&CommonServer::get_json(&endpoint)?);
let mut resp_val: Vec<MessageSerde>;
if resp.is_err() {
return Err(errors::ErrorType::QueryResponseParseError.err(&resp.unwrap_err().to_string()));
} else {
resp_val = resp.unwrap();
Transact::add_action_to_transaction(
"save",
&chainmail::action_structs::save {
subject: msg.subject.clone(),
body: msg.body.clone(),
Comment on lines +61 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is clone needed here? msg goes out of scope right after this, so you should be able to move from it.

receiver: AccountNumber::from(msg.receiver.as_str()),
msg_id: msg.msg_id,
sender: AccountNumber::from(msg.sender.as_str()),
datetime: msg.datetime,
}
.packed(),
)?;
Ok(())
}

// There's a way to tell the bindgen to generate the rust types with custom attributes. Goes in cargo.toml.
// Somewhere in the codebase is an example of doing this with serde serialize and deserialize attributes
let messages: Vec<Message> = resp_val
.into_iter()
.map(|m| Message {
msg_id: m
.msg_id
.parse::<u64>()
.map_err(|err| QueryResponseParseError.err(&err.to_string()))
.unwrap(),
receiver: m.receiver,
sender: m.sender,
subject: m.subject,
body: m.body,
})
.collect();
Ok(messages)
/// Unsave the message
/// - msg_id: the message's rowid (from the events table)
///
/// Unsaving a message removing it from state, exposing it to pruning
fn unsave(msg_id: u64) -> Result<(), Error> {
let msg = get_msg_by_id(msg_id).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why call unwrap instead of ??

Transact::add_action_to_transaction(
"save",
&chainmail::action_structs::unsave {
subject: msg.subject.clone(),
body: msg.body.clone(),
msg_id: msg.msg_id,
sender: AccountNumber::from(msg.sender.as_str()),
datetime: msg.datetime,
}
.packed(),
)?;
Ok(())
}
}

impl Query for ChainmailPlugin {
/// Retrieve non-archived messages
/// - Messages can be filtered by one or both of sender and/or receiver
fn get_msgs(sender: Option<String>, receiver: Option<String>) -> Result<Vec<Message>, Error> {
Ok(query_messages_endpoint(sender, receiver, false)?)
}

/// Retrieve archived messages
/// - Messages can be filtered by one or both of sender and/or receiver
fn get_archived_msgs(
sender: Option<String>,
receiver: Option<String>,
Expand Down
49 changes: 49 additions & 0 deletions services/user/Chainmail/plugin/src/queries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::{
bindings::exports::chainmail::plugin::queries::Message,
bindings::host::common::{server as CommonServer, types::Error},
errors::ErrorType,
serde_structs::TempMessageForDeserialization,
};

pub fn get_msg_by_id(msg_id: u64) -> Result<TempMessageForDeserialization, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the struct is called TempMessageForDeserialization, I would use it only for deserialization and immediately convert it to Message for use everywhere else.

let api_root = String::from("/api");

let res = CommonServer::get_json(&format!("{}/messages?id={}", api_root, &msg_id.to_string()))?;

let msg = serde_json::from_str::<Vec<TempMessageForDeserialization>>(&res)
.map_err(|err| ErrorType::QueryResponseParseError.err(err.to_string().as_str()))?;

if msg.len() == 1 {
let msg = msg.get(0).unwrap();
return Ok(msg.clone());
Comment on lines +16 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary clone. pop or remove can extract an item from a Vec without copying it.

} else {
return Err(ErrorType::InvalidMsgId.err(&msg_id.to_string()));
}
}

pub fn query_messages_endpoint(
sender: Option<String>,
receiver: Option<String>,
archived_requested: bool,
) -> Result<Vec<Message>, Error> {
let mut endpoint = String::from("/api/messages?");
if archived_requested {
endpoint += "archived=true&";
}
if sender.is_some() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if let Some(sender) = &sender

is better if you need to both test and unwrap.

endpoint += &format!("sender={}", sender.clone().unwrap());
}
if sender.is_some() && receiver.is_some() {
endpoint += "&";
}
if receiver.is_some() {
endpoint += &format!("receiver={}", receiver.unwrap());
}

let resp = serde_json::from_str::<Vec<TempMessageForDeserialization>>(&CommonServer::get_json(
&endpoint,
)?)
.map_err(|err| ErrorType::QueryResponseParseError.err(err.to_string().as_str()))?;

Ok(resp.into_iter().map(|m| m.into()).collect())
}
37 changes: 37 additions & 0 deletions services/user/Chainmail/plugin/src/serde_structs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use psibase::TimePointSec;
use serde::{de, Deserialize};
use serde_aux::prelude::*;

use crate::bindings::exports::chainmail::plugin::queries::Message;

#[derive(Clone, Debug, Deserialize)]
pub struct TempMessageForDeserialization {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub msg_id: u64,
pub receiver: String,
pub sender: String,
pub subject: String,
pub body: String,
#[serde(deserialize_with = "deserialize_timepoint")]
pub datetime: u32,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it possible to deserialize with either:

  • The SavedMessage struct in the service, or
  • The generated Message object from your bindings?

Just seems a bit strange to essentially have three definitions of a Message type... Maybe it's needed though?


fn deserialize_timepoint<'d, D>(deserializer: D) -> Result<u32, D::Error>
where
D: de::Deserializer<'d>,
{
Ok(TimePointSec::deserialize(deserializer)?.seconds)
}

impl Into<Message> for TempMessageForDeserialization {
fn into(self) -> Message {
Message {
msg_id: self.msg_id,
receiver: self.receiver,
sender: self.sender,
subject: self.subject,
body: self.body,
datetime: self.datetime,
}
}
}
9 changes: 8 additions & 1 deletion services/user/Chainmail/plugin/wit/world.wit
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ interface types {
receiver: string,
sender: string,
subject: string,
body: string
body: string,
datetime: u32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This datetime should be converted to a utc string before returning it to the UI, so the UI can easily construct a Date with it. You can check the invite plugin to see how I already did this over there:

let expiry = DateTime::from_timestamp(invite.expiry as i64, 0)
            .ok_or(DatetimeError.err("decode_invite"))?
            .to_string();

}
}

Expand All @@ -26,6 +27,12 @@ interface api {

// Archive an email
archive: func(msg-id: u64) -> result<_, error>;

// Save an email
save: func(event-id: u64) -> result<_, error>;
James-Mart marked this conversation as resolved.
Show resolved Hide resolved

// Unsave an email (remove it from state)
unsave: func(event-id: u64) -> result<_, error>;
}

world imports {
Expand Down
Loading
Loading