-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: main
Are you sure you want to change the base?
Changes from all commits
3ade844
981aac6
5b8640e
c43cf0a
783ba90
a9f6c87
7ede9c5
8627a21
a360433
bb46c0e
2c5dba8
21dcb00
3914e24
14ad7b1
4dd13d5
846676c
87959fe
239cec2
f75afe8
69419f4
cecd7d2
52f444e
6b15ebb
0e1ea52
f13c8fd
1676b6f
228c0f5
5435efb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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", | ||
|
@@ -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) | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is clone needed here? |
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why call |
||
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>, | ||
|
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the struct is called |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary clone. |
||
} 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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
} |
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, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, is it possible to deserialize with either:
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, | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,8 @@ interface types { | |
receiver: string, | ||
sender: string, | ||
subject: string, | ||
body: string | ||
body: string, | ||
datetime: u32 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
|
||
} | ||
} | ||
|
||
|
@@ -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 { | ||
|
There was a problem hiding this comment.
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