#include "framework/framework.h" #include #include #include #define NUM_PEERS 5 #define GROUP_NAME "Moderation Test Group" #define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) typedef struct { uint32_t group_number; uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; bool chat_id_ready; uint32_t peer_count; bool connected; Tox_Group_Role self_role; Tox_Group_Voice_State voice_state; bool kicked; uint32_t peer_ids[NUM_PEERS]; // Map node index to group peer_id Tox_Group_Mod_Event last_mod_event; uint32_t last_mod_target; } ModState; static void on_group_self_join(const Tox_Event_Group_Self_Join *event, void *user_data) { ToxNode *self = (ToxNode *)user_data; ModState *state = (ModState *)tox_node_get_script_ctx(self); state->connected = true; uint32_t group_number = tox_event_group_self_join_get_group_number(event); uint32_t self_id = tox_group_self_get_peer_id(tox_node_get_tox(self), group_number, nullptr); state->self_role = tox_group_self_get_role(tox_node_get_tox(self), group_number, nullptr); tox_node_log(self, "Joined group %u (Peer ID: %u) with role %s", group_number, self_id, tox_group_role_to_string(state->self_role)); } static void on_group_peer_join(const Tox_Event_Group_Peer_Join *event, void *user_data) { ToxNode *self = (ToxNode *)user_data; ModState *state = (ModState *)tox_node_get_script_ctx(self); uint32_t group_number = tox_event_group_peer_join_get_group_number(event); uint32_t peer_id = tox_event_group_peer_join_get_peer_id(event); tox_node_log(self, "Peer %u joined the group", peer_id); state->peer_count++; Tox_Err_Group_Peer_Query q_err; size_t length = tox_group_peer_get_name_size(tox_node_get_tox(self), group_number, peer_id, &q_err); if (q_err == TOX_ERR_GROUP_PEER_QUERY_OK && length > 0) { uint8_t name[TOX_MAX_NAME_LENGTH]; tox_group_peer_get_name(tox_node_get_tox(self), group_number, peer_id, name, &q_err); if (q_err == TOX_ERR_GROUP_PEER_QUERY_OK) { tox_node_log(self, "Peer %u name identified: %.*s", peer_id, (int)length, name); if (length == 7 && memcmp(name, "Founder", 7) == 0) { state->peer_ids[0] = peer_id; } else if (length >= 5 && memcmp(name, "Peer", 4) == 0) { int idx = atoi((const char *)name + 4); if (idx > 0 && idx < NUM_PEERS) { state->peer_ids[idx] = peer_id; } } } } } static void on_group_moderation(const Tox_Event_Group_Moderation *event, void *user_data) { ToxNode *self = (ToxNode *)user_data; ModState *state = (ModState *)tox_node_get_script_ctx(self); state->last_mod_event = tox_event_group_moderation_get_mod_type(event); state->last_mod_target = tox_event_group_moderation_get_target_peer_id(event); Tox_Err_Group_Self_Query err; state->self_role = tox_group_self_get_role(tox_node_get_tox(self), state->group_number, &err); if (state->last_mod_event == TOX_GROUP_MOD_EVENT_KICK && state->last_mod_target == tox_group_self_get_peer_id(tox_node_get_tox(self), state->group_number, nullptr)) { state->kicked = true; } tox_node_log(self, "Moderation event: %s on peer %u. My role is now %s", tox_group_mod_event_to_string(state->last_mod_event), state->last_mod_target, tox_group_role_to_string(state->self_role)); } static void on_group_voice_state(const Tox_Event_Group_Voice_State *event, void *user_data) { ToxNode *self = (ToxNode *)user_data; ModState *state = (ModState *)tox_node_get_script_ctx(self); state->voice_state = tox_event_group_voice_state_get_voice_state(event); tox_node_log(self, "Voice state updated: %u", state->voice_state); } static void common_init(ToxNode *self, ModState *state) { Tox_Dispatch *dispatch = tox_node_get_dispatch(self); tox_events_callback_group_self_join(dispatch, on_group_self_join); tox_events_callback_group_peer_join(dispatch, on_group_peer_join); tox_events_callback_group_moderation(dispatch, on_group_moderation); tox_events_callback_group_voice_state(dispatch, on_group_voice_state); for (int i = 0; i < NUM_PEERS; ++i) { state->peer_ids[i] = UINT32_MAX; } tox_node_log(self, "Waiting for self connection..."); tox_node_wait_for_self_connected(self); tox_node_log(self, "Connected!"); } static void wait_for_peer_role(ToxNode *self, uint32_t peer_idx, Tox_Group_Role expected_role) { const ToxNode *peer = tox_scenario_get_node(tox_node_get_scenario(self), peer_idx); const ModState *peer_view = (const ModState *)tox_node_get_peer_ctx(peer); tox_node_log(self, "Waiting for Peer %u to have role %s", peer_idx, tox_group_role_to_string(expected_role)); WAIT_UNTIL(peer_view->self_role == expected_role); tox_node_log(self, "Peer %u now has role %s", peer_idx, tox_group_role_to_string(expected_role)); } static void founder_script(ToxNode *self, void *ctx) { ModState *state = (ModState *)ctx; Tox *tox = tox_node_get_tox(self); common_init(self, state); Tox_Err_Group_New err_new; state->group_number = tox_group_new(tox, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, GROUP_NAME_LEN, (const uint8_t *)"Founder", 7, &err_new); ck_assert(err_new == TOX_ERR_GROUP_NEW_OK); state->self_role = TOX_GROUP_ROLE_FOUNDER; state->peer_ids[0] = tox_group_self_get_peer_id(tox, state->group_number, nullptr); tox_group_get_chat_id(tox, state->group_number, state->chat_id, nullptr); state->chat_id_ready = true; // Barrier 1: Wait for all peers to join and be seen by everyone tox_scenario_barrier_wait(self); WAIT_UNTIL(state->peer_count == NUM_PEERS - 1); tox_node_log(self, "All peers joined"); // Wait until we know all peer IDs for (int i = 1; i < NUM_PEERS; ++i) { WAIT_UNTIL(state->peer_ids[i] != UINT32_MAX); } tox_node_log(self, "All peer IDs identified"); tox_scenario_barrier_wait(self); // Sync point after everyone sees everyone // Barrier 2: Peer 1 becomes Moderator tox_group_set_role(tox, state->group_number, state->peer_ids[1], TOX_GROUP_ROLE_MODERATOR, nullptr); for (int i = 0; i < NUM_PEERS; ++i) { wait_for_peer_role(self, 1, TOX_GROUP_ROLE_MODERATOR); } tox_scenario_barrier_wait(self); // Barrier 3: Peer 2 and 3 become Observer tox_group_set_role(tox, state->group_number, state->peer_ids[2], TOX_GROUP_ROLE_OBSERVER, nullptr); tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_OBSERVER, nullptr); for (int i = 0; i < NUM_PEERS; ++i) { wait_for_peer_role(self, 2, TOX_GROUP_ROLE_OBSERVER); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_OBSERVER); } tox_scenario_barrier_wait(self); // Barrier 4: Voice State tests tox_node_log(self, "Setting voice state to MODERATOR"); tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_MODERATOR, nullptr); tox_scenario_barrier_wait(self); // Phase 1 set tox_scenario_barrier_wait(self); // Phase 1 done tox_node_log(self, "Setting voice state to FOUNDER"); tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_FOUNDER, nullptr); tox_scenario_barrier_wait(self); // Phase 2 set tox_scenario_barrier_wait(self); // Phase 2 done tox_node_log(self, "Setting voice state to ALL"); tox_group_set_voice_state(tox, state->group_number, TOX_GROUP_VOICE_STATE_ALL, nullptr); tox_scenario_barrier_wait(self); // Phase 3 set tox_scenario_barrier_wait(self); // Phase 3 done // Barrier 5: Peer 1 (Mod) promotes Peer 2 back to User tox_scenario_barrier_wait(self); wait_for_peer_role(self, 2, TOX_GROUP_ROLE_USER); // Barrier 6: Founder promotes Peer 3 to Moderator tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_MODERATOR, nullptr); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_MODERATOR); tox_scenario_barrier_wait(self); // Barrier 7: Moderator (Peer 1) attempts to kick/demote Founder (should fail) tox_scenario_barrier_wait(self); // Barrier 8: Founder kicks Moderator (Peer 1) tox_group_kick_peer(tox, state->group_number, state->peer_ids[1], nullptr); tox_scenario_barrier_wait(self); // Barrier 9: Founder demotes Moderator (Peer 3) to User tox_group_set_role(tox, state->group_number, state->peer_ids[3], TOX_GROUP_ROLE_USER, nullptr); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_USER); tox_scenario_barrier_wait(self); tox_scenario_barrier_wait(self); // Done } static void peer_script(ToxNode *self, void *ctx) { ModState *state = (ModState *)ctx; Tox *tox = tox_node_get_tox(self); common_init(self, state); const ToxNode *founder = tox_scenario_get_node(tox_node_get_scenario(self), 0); const ModState *founder_view = (const ModState *)tox_node_get_peer_ctx(founder); while (!founder_view->chat_id_ready) { tox_scenario_yield(self); } tox_node_log(self, "Got chat ID from founder"); char name[16]; snprintf(name, sizeof(name), "Peer%u", tox_node_get_index(self)); Tox_Err_Group_Join err_join; state->group_number = tox_group_join(tox, founder_view->chat_id, (const uint8_t *)name, strlen(name), nullptr, 0, &err_join); if (state->group_number == UINT32_MAX) { tox_node_log(self, "tox_group_join failed with error %u", err_join); } ck_assert(state->group_number != UINT32_MAX); WAIT_UNTIL(state->connected); uint32_t self_id = tox_group_self_get_peer_id(tox, state->group_number, nullptr); state->peer_ids[tox_node_get_index(self)] = self_id; tox_scenario_barrier_wait(self); // Barrier 1: Joined // Wait until we know all peer IDs for (uint32_t i = 0; i < NUM_PEERS; ++i) { if (tox_node_get_index(self) != i) { WAIT_UNTIL(state->peer_ids[i] != UINT32_MAX); } } tox_scenario_barrier_wait(self); // Sync point after everyone sees everyone tox_scenario_barrier_wait(self); // Barrier 2: Peer 1 Moderator wait_for_peer_role(self, 1, TOX_GROUP_ROLE_MODERATOR); tox_scenario_barrier_wait(self); // Barrier 3: Peer 2/3 Observer wait_for_peer_role(self, 2, TOX_GROUP_ROLE_OBSERVER); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_OBSERVER); // Barrier 4: Voice State tests // Sub-phase 1: MODERATOR tox_scenario_barrier_wait(self); // Phase 1 set WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_MODERATOR); Tox_Err_Group_Send_Message err_msg; tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg); if (state->self_role == TOX_GROUP_ROLE_MODERATOR || state->self_role == TOX_GROUP_ROLE_FOUNDER) { if (err_msg != TOX_ERR_GROUP_SEND_MESSAGE_OK) { tox_node_log(self, "Expected OK, got %u. Role: %s", err_msg, tox_group_role_to_string(state->self_role)); } ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK); } else { if (err_msg != TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS) { tox_node_log(self, "Expected PERMISSIONS, got %u. Role: %s", err_msg, tox_group_role_to_string(state->self_role)); } ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); } tox_scenario_barrier_wait(self); // Phase 1 done // Sub-phase 2: FOUNDER tox_scenario_barrier_wait(self); // Phase 2 set WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_FOUNDER); tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg); if (state->self_role == TOX_GROUP_ROLE_FOUNDER) { ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK); } else { ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); } tox_scenario_barrier_wait(self); // Phase 2 done // Sub-phase 3: ALL tox_scenario_barrier_wait(self); // Phase 3 set WAIT_UNTIL(state->voice_state == TOX_GROUP_VOICE_STATE_ALL); tox_group_send_message(tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)"hello", 5, &err_msg); if (state->self_role == TOX_GROUP_ROLE_OBSERVER) { ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); } else { ck_assert(err_msg == TOX_ERR_GROUP_SEND_MESSAGE_OK); } tox_scenario_barrier_wait(self); // Phase 3 done // Barrier 5: Peer 1 (Mod) promotes Peer 2 back to User if (tox_node_get_index(self) == 1) { // Peer 1 uint32_t peer2_id = state->peer_ids[2]; tox_group_set_role(tox, state->group_number, peer2_id, TOX_GROUP_ROLE_USER, nullptr); } tox_scenario_barrier_wait(self); wait_for_peer_role(self, 2, TOX_GROUP_ROLE_USER); // Barrier 6: Founder promotes Peer 3 to Moderator tox_scenario_barrier_wait(self); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_MODERATOR); // Barrier 7: Moderator (Peer 1) attempts to kick/demote Founder (should fail) if (tox_node_get_index(self) == 1) { Tox_Err_Group_Kick_Peer err_kick; uint32_t founder_peer_id = state->peer_ids[0]; tox_group_kick_peer(tox, state->group_number, founder_peer_id, &err_kick); ck_assert(err_kick != TOX_ERR_GROUP_KICK_PEER_OK); Tox_Err_Group_Set_Role err_role; tox_group_set_role(tox, state->group_number, founder_peer_id, TOX_GROUP_ROLE_USER, &err_role); ck_assert(err_role != TOX_ERR_GROUP_SET_ROLE_OK); } tox_scenario_barrier_wait(self); // Barrier 8: Founder kicks Moderator (Peer 1) tox_scenario_barrier_wait(self); if (tox_node_get_index(self) == 1) { WAIT_UNTIL(state->kicked); return; // Exit script } // Barrier 9: Founder demotes Moderator (Peer 3) to User tox_scenario_barrier_wait(self); wait_for_peer_role(self, 3, TOX_GROUP_ROLE_USER); tox_scenario_barrier_wait(self); // Done } int main(int argc, char *argv[]) { setvbuf(stdout, nullptr, _IONBF, 0); setvbuf(stderr, nullptr, _IONBF, 0); ToxScenario *s = tox_scenario_new(argc, argv, 300000); // 5 virtual minutes ModState states[NUM_PEERS] = {0}; ToxNode *nodes[NUM_PEERS]; nodes[0] = tox_scenario_add_node(s, "Founder", founder_script, &states[0], sizeof(ModState)); static char aliases[NUM_PEERS][16]; for (int i = 1; i < NUM_PEERS; ++i) { snprintf(aliases[i], sizeof(aliases[i]), "Peer%d", i); nodes[i] = tox_scenario_add_node(s, aliases[i], peer_script, &states[i], sizeof(ModState)); } for (int i = 0; i < NUM_PEERS; ++i) { for (int j = 0; j < NUM_PEERS; ++j) { if (i != j) { tox_node_bootstrap(nodes[i], nodes[j]); tox_node_friend_add(nodes[i], nodes[j]); } } } ToxScenarioStatus res = tox_scenario_run(s); if (res != TOX_SCENARIO_DONE) { tox_scenario_log(s, "Test failed with status %u", res); return 1; } tox_scenario_free(s); return 0; } #undef GROUP_NAME #undef GROUP_NAME_LEN #undef NUM_PEERS