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
27 changes: 11 additions & 16 deletions src/features/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
use crate::client::Client;
use crate::features::groups::GroupMetadata;
use crate::features::groups::GroupParticipant;
use crate::features::mex::{MexError, MexRequest};
use crate::features::mex::{MexError, mex_request};
use log::warn;
use serde_json::json;
use wacore::iq::groups::{
DeleteCommunityIq, GetLinkedGroupsParticipantsIq, GroupCreateIq, GroupCreateOptions,
JoinLinkedGroupIq, LinkSubgroupsIq, QueryLinkedGroupIq, UnlinkSubgroupsIq,
};
use wacore::iq::mex_ids::community as community_docs;
use wacore::iq::mex_operations::{fetch_all_subgroups, query_subgroup_participant_count};
use wacore_binary::Jid;

// Types
Expand Down Expand Up @@ -230,12 +229,10 @@ impl<'a> Community<'a> {
let response = self
.client
.mex()
.query(MexRequest {
doc: community_docs::FETCH_ALL_SUBGROUPS,
variables: json!({
"group_id": community_jid.to_string()
}),
})
.query(mex_request!(fetch_all_subgroups {
group_id: Some(community_jid.to_string()),
..Default::default()
}))
.await?;

let data = response
Expand Down Expand Up @@ -277,14 +274,12 @@ impl<'a> Community<'a> {
let response = self
.client
.mex()
.query(MexRequest {
doc: community_docs::FETCH_SUBGROUP_PARTICIPANT_COUNT,
variables: json!({
"input": {
"group_jid": community_jid.to_string()
}
.query(mex_request!(query_subgroup_participant_count {
input: Some(query_subgroup_participant_count::Input {
group_jid: Some(community_jid.to_string()),
..Default::default()
}),
})
}))
.await?;

let data = response
Expand Down
96 changes: 76 additions & 20 deletions src/features/groups.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::client::Client;
use crate::features::mex::{MexError, MexRequest};
use crate::features::mex::{MexError, mex_request};
use std::collections::HashMap;
use std::sync::Arc;
use wacore::client::context::GroupInfo;
Expand All @@ -14,6 +14,7 @@ use wacore::iq::groups::{
SetGroupMembershipApprovalIq, SetGroupSubjectIq, SetMemberAddModeIq,
SetNoFrequentlyForwardedIq, normalize_participants,
};
use wacore::iq::mex_operations::update_group_property;
use wacore::types::message::AddressingMode;
use wacore_binary::{Jid, JidExt as _};

Expand All @@ -25,6 +26,31 @@ pub use wacore::iq::groups::{
MembershipRequest, ParticipantChangeResponse, ParticipantType, PictureType,
};

/// Typed `update` payload for the `update_group_property` mex mutation. The
/// generated mirror types this op's `update` as a `String`, but it is a one-of
/// object; this enum's `#[serde(rename_all = "snake_case")]` emits the exact
/// wire keys with no `serde_json::Value`. Leaf values use the mex (uppercase)
/// vocabulary, which differs from the lower-case `WireEnum` IQ values.
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum GroupPropertyUpdate {
MemberLinkMode(&'static str),
MemberShareGroupHistoryMode(&'static str),
LimitSharing(LimitSharingUpdate),
}

#[derive(serde::Serialize)]
struct LimitSharingUpdate {
limit_sharing_enabled: bool,
limit_sharing_trigger: &'static str,
}

#[derive(serde::Serialize)]
struct UpdateGroupPropertyVars {
group_id: String,
update: GroupPropertyUpdate,
}

/// Result for a single group in a batch query.
#[derive(Debug, Clone)]
pub enum BatchGroupResult {
Expand Down Expand Up @@ -655,7 +681,7 @@ impl<'a> Groups<'a> {
MemberLinkMode::AdminLink => "ADMIN_LINK",
MemberLinkMode::AllMemberLink => "ALL_MEMBER_LINK",
};
self.mex_update_group_property(jid, serde_json::json!({ "member_link_mode": value }))
self.mex_update_group_property(jid, GroupPropertyUpdate::MemberLinkMode(value))
.await
}

Expand All @@ -669,22 +695,17 @@ impl<'a> Groups<'a> {
MemberShareHistoryMode::AdminShare => "ADMIN_SHARE",
MemberShareHistoryMode::AllMemberShare => "ALL_MEMBER_SHARE",
};
self.mex_update_group_property(
jid,
serde_json::json!({ "member_share_group_history_mode": value }),
)
.await
self.mex_update_group_property(jid, GroupPropertyUpdate::MemberShareGroupHistoryMode(value))
.await
}

/// Enable or disable limit sharing in the group (via MEX).
pub async fn set_limit_sharing(&self, jid: &Jid, enabled: bool) -> Result<(), MexError> {
self.mex_update_group_property(
jid,
serde_json::json!({
"limit_sharing": {
"limit_sharing_enabled": enabled,
"limit_sharing_trigger": "CHAT_SETTING"
}
GroupPropertyUpdate::LimitSharing(LimitSharingUpdate {
limit_sharing_enabled: enabled,
limit_sharing_trigger: "CHAT_SETTING",
}),
)
.await
Expand Down Expand Up @@ -769,18 +790,18 @@ impl<'a> Groups<'a> {
async fn mex_update_group_property(
&self,
jid: &Jid,
update: serde_json::Value,
update: GroupPropertyUpdate,
) -> Result<(), MexError> {
let resp = self
.client
.mex()
.mutate(MexRequest {
doc: wacore::iq::mex_ids::groups::UPDATE_GROUP_PROPERTY,
variables: serde_json::json!({
"group_id": jid.to_string(),
"update": update,
}),
})
.mutate(mex_request!(
update_group_property,
UpdateGroupPropertyVars {
group_id: jid.to_string(),
update,
}
))
.await?;

let state = resp
Expand Down Expand Up @@ -1113,4 +1134,39 @@ mod tests {
}

// Protocol-level tests (node building, parsing, validation) are in wacore/src/iq/groups.rs

#[test]
fn group_property_update_serializes_to_wire() {
assert_eq!(
serde_json::to_value(UpdateGroupPropertyVars {
group_id: "123@g.us".to_string(),
update: GroupPropertyUpdate::MemberLinkMode("ADMIN_LINK"),
})
.unwrap(),
serde_json::json!({
"group_id": "123@g.us",
"update": { "member_link_mode": "ADMIN_LINK" }
})
);
assert_eq!(
serde_json::to_value(GroupPropertyUpdate::MemberShareGroupHistoryMode(
"ALL_MEMBER_SHARE"
))
.unwrap(),
serde_json::json!({ "member_share_group_history_mode": "ALL_MEMBER_SHARE" })
);
assert_eq!(
serde_json::to_value(GroupPropertyUpdate::LimitSharing(LimitSharingUpdate {
limit_sharing_enabled: true,
limit_sharing_trigger: "CHAT_SETTING",
}))
.unwrap(),
serde_json::json!({
"limit_sharing": {
"limit_sharing_enabled": true,
"limit_sharing_trigger": "CHAT_SETTING"
}
})
);
}
}
74 changes: 63 additions & 11 deletions src/features/mex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use crate::client::Client;
use crate::request::IqError;
use serde_json::Value;
use serde::Serialize;
use thiserror::Error;
use wacore::iq::mex::MexQuerySpec;
use wacore_binary::jid::JidError;
Expand Down Expand Up @@ -33,16 +33,57 @@ pub enum MexError {
Json(#[from] serde_json::Error),
}

/// MEX request with persisted-query descriptor and variables.
/// MEX request: a persisted-query descriptor plus its typed variables.
///
/// Variables are serialized straight to the wire in the IQ spec (no intermediate
/// `serde_json::Value`). Build one with the [`mex_request!`] macro, which pulls
/// `NAME`/`DOC_ID` from a generated [`wacore::iq::mex_operations`] module so the
/// op is named once.
#[derive(Debug, Clone)]
pub struct MexRequest {
/// GraphQL persisted-query descriptor (name + id), from
/// [`wacore::iq::mex_ids`].
pub struct MexRequest<V> {
/// GraphQL persisted-query descriptor (name + id).
pub doc: MexDoc,
/// Query variables.
pub variables: Value,
/// Typed query variables: a generated `Variables`, or any `Serialize` value
/// (e.g. a `json!` object) for inputs the generated mirror types too loosely.
pub variables: V,
}

impl<V> MexRequest<V> {
/// Pair a `(name, id)` from a generated op module with its variables.
/// Prefer the [`mex_request!`] macro, which names the op once.
pub fn new(name: &'static str, id: &'static str, variables: V) -> Self {
Self {
doc: MexDoc { name, id },
variables,
}
}
}

/// Build a [`MexRequest`] from a generated mex operation module, pulling its
/// `NAME`/`DOC_ID` so the op is named once. Two forms:
///
/// ```ignore
/// // typed Variables, struct-literal sugar:
/// mex_request!(join_newsletter { newsletter_id: Some(jid.to_string()) })
/// // explicit value (typed Variables value, or a json! for loosely-typed inputs):
/// mex_request!(update_group_property, serde_json::json!({ "group_id": id }))
/// ```
macro_rules! mex_request {
($op:path { $($body:tt)* }) => {{
use $op as __mex_op;
$crate::features::mex::MexRequest::new(
__mex_op::NAME,
__mex_op::DOC_ID,
__mex_op::Variables { $($body)* },
)
}};
($op:path, $vars:expr $(,)?) => {{
use $op as __mex_op;
$crate::features::mex::MexRequest::new(__mex_op::NAME, __mex_op::DOC_ID, $vars)
}};
}
pub(crate) use mex_request;

/// Feature handle for MEX GraphQL operations.
pub struct Mex<'a> {
client: &'a Client,
Expand All @@ -55,18 +96,29 @@ impl<'a> Mex<'a> {

/// Execute a GraphQL query.
#[inline]
pub async fn query(&self, request: MexRequest) -> Result<MexResponse, MexError> {
pub async fn query<V: Serialize>(
&self,
request: MexRequest<V>,
) -> Result<MexResponse, MexError> {
self.execute_request(request).await
}

/// Execute a GraphQL mutation.
#[inline]
pub async fn mutate(&self, request: MexRequest) -> Result<MexResponse, MexError> {
pub async fn mutate<V: Serialize>(
&self,
request: MexRequest<V>,
) -> Result<MexResponse, MexError> {
self.execute_request(request).await
}

async fn execute_request(&self, request: MexRequest) -> Result<MexResponse, MexError> {
let spec = MexQuerySpec::new(request.doc, request.variables);
async fn execute_request<V: Serialize>(
&self,
request: MexRequest<V>,
) -> Result<MexResponse, MexError> {
// Serialize the variables here so a caller-side serialization error
// surfaces as MexError::Json instead of a malformed empty request.
let spec = MexQuerySpec::new(request.doc, &request.variables)?;

let response = self.client.execute(spec).await?;

Expand Down
Loading
Loading