Overview
After initialization, your bot must join groups or communities before it can send messages. OpenChat supports three join operations:
Groups - Standalone chat groups
Communities - Collections of channels with shared membership
Channels - Individual chat rooms within a community
For private groups/communities, an admin must first invite your bot using the OpenChat UI before it can join.
Joining a Group
Groups are standalone chat spaces identified by their canister ID.
Obtain the group canister ID
Get the group’s canister ID from OpenChat. Example: "evg6t-laaaa-aaaar-a4j5q-cai"
Look up the local user index
Before joining, the bot must find the group’s local user index canister: // From BotService.mo:499-516
func lookupLocalUserIndex(group: Text) : async* Result.Result<Principal, Text> {
let res = await* ocService.publicGroupSummary(group, { invite_code = null});
switch(res){
case(#ok(data)){
switch(data){
case (#Success(response)){
#ok(response.summary.local_user_index_canister_id)
};
case (#NotAuthorized(_)){
#err("NotAuthorized")
};
}
};
case(#err(msg)){ #err(msg) };
}
};
This step is handled automatically by joinGroup.
Call joinGroup
Join the group with an optional invite code: # With an invite code
dfx canister call OCBot tryJoinGroup '("abc123", opt 123456789)' --ic
# Without an invite code (for public groups)
dfx canister call OCBot tryJoinGroup '("abc123", null)' --ic
Group Join Implementation
// From BotService.mo:159-185
public func joinGroup(groupCanisterId : Text, inviteCode : ?Nat64) : async* Result.Result<Text, Text>{
let indexCanister = await* lookupLocalUserIndex(groupCanisterId);
switch(indexCanister){
case(#ok(id)){
let #ok(res) = await* ocService.joinGroup(Principal.toText(id), {
chat_id = Principal.fromText(groupCanisterId);
invite_code = inviteCode;
correlation_id = 0
}) else{
return #err("Trapped");
};
switch(res){
case(#Success(_)){
Map.set(botModel.groups, thash, groupCanisterId, ());
#ok("OK")
};
case(#AlreadyInGroup or #AlreadyInGroupV2(_)){
#err("Already in group")
};
case(_){ #err("Error") }
};
};
case(#err(msg)){ #err(msg) }
};
};
Group Join Responses
Response Description #SuccessSuccessfully joined group #AlreadyInGroupBot is already a member #NotAuthorizedGroup is private, invitation required #UserBlockedBot has been blocked from group
Communities contain multiple channels and require two-step joining process.
Get community canister ID
Obtain the community’s canister ID from OpenChat.
Look up the local community index
The bot finds the community’s local user index: // From BotService.mo:187-204
func localCommunityIndex(communityCanisterId : Text) : async* Result.Result<Principal, Text>{
let res = await* ocService.publicCommunitySummary(communityCanisterId, { invite_code = null});
switch(res){
case(#ok(data)){
switch(data){
case (#Success(response)){
#ok(response.local_user_index_canister_id)
};
case (#PrivateCommunity(_)){
#err("PrivateCommunity")
};
}
};
case(#err(msg)){ #err(msg) };
}
};
Join the community
# With an invite code
dfx canister call OCBot tryJoinCommunity '("community-id", opt 123456789)' --ic
# Without an invite code (for public communities)
dfx canister call OCBot tryJoinCommunity '("community-id", null)' --ic
// From BotService.mo:208-263
public func joinCommunity(
communityCanisterId : Text,
inviteCode : ?Nat64,
botPrincipal : Principal
) : async* Result.Result<Text, Text>{
let indexCanister = await* localCommunityIndex(communityCanisterId);
switch(indexCanister){
case(#ok(id)){
let res = await* ocService.joinCommunity(Principal.toText(id), {
community_id = Principal.fromText(communityCanisterId);
user_id = botPrincipal;
principal = botPrincipal;
invite_code = inviteCode;
is_platform_moderator = false;
is_bot = true;
diamond_membership_expires_at = null;
verified_credential_args = null;
});
switch(res){
case(#ok(data)){
switch(data){
case(#Success(_)){ #ok("OK") };
case(#AlreadyInCommunity(_)){ #err("Already in community") };
case(#GateCheckFailed(_)){ #err("GateCheckFailed") };
case(#NotInvited){ #err("NotInvited") };
case(#UserBlocked){ #err("UserBlocked") };
case(#MemberLimitReached(limit)){ #err("MemberLimitReached") };
case(#CommunityFrozen){ #err("CommunityFrozen") };
case(#InternalError(e)){ #err("InternalError: " # e) };
};
};
case(#err(e)){ #err(e) }
};
};
case(#err(msg)){ #err(msg) }
};
};
Response Description #SuccessSuccessfully joined community #AlreadyInCommunityBot is already a member #GateCheckFailedBot doesn’t meet gate requirements (tokens, credentials) #NotInvitedCommunity is private, invitation required #UserBlockedBot has been blocked #MemberLimitReachedCommunity is at maximum capacity #CommunityFrozenCommunity has been frozen
Joining a Channel
You must join the parent community before joining any of its channels.
Ensure community membership
First join the community using joinCommunity as shown above.
Join the channel
# With an invite code
dfx canister call OCBot tryJoinChannel '("${community_id}", ${channel_id}, opt 123456789)' --ic
# Without an invite code
dfx canister call OCBot tryJoinChannel '("${community_id}", ${channel_id}, null)' --ic
Example: dfx canister call OCBot tryJoinChannel '("uxyan-oyaaa-aaaaf-aaa5q-cai", 123, null)' --ic
Channel Join Implementation
// From BotService.mo:265-334
public func joinChannel(
communityCanisterId : Text,
channelId: Nat,
inviteCode : ?Nat64
) : async* Result.Result<Text, Text>{
let indexCanister = await* localCommunityIndex(communityCanisterId);
switch(indexCanister){
case(#ok(id)){
let res = await* ocService.joinChannel(Principal.toText(id), {
community_id = Principal.fromText(communityCanisterId);
channel_id = channelId;
user_id = Principal.fromText("7g2oq-raaaa-aaaap-qb7sq-cai");
principal = Principal.fromText("7g2oq-raaaa-aaaap-qb7sq-cai");
invite_code = inviteCode;
is_platform_moderator = false;
is_bot = true;
diamond_membership_expires_at = null;
verified_credential_args = null;
});
switch(res){
case(#ok(data)){
switch(data){
case(#Success(_)){ #ok("OK") };
case(#SuccessJoinedCommunity(_)){ #ok("OK") };
case(#AlreadyInChannel(_)){ #err("Already in community") };
case(#UserNotInCommunity){ #err("UserNotInCommunity") };
case(#ChannelNotFound){ #err("UserNotInCommunity") };
// ... other error cases
};
};
case(#err(e)){ #err(e) }
};
};
case(#err(msg)){ #err(msg) }
};
};
Channel Join Responses
Response Description #SuccessSuccessfully joined channel #SuccessJoinedCommunityJoined both community and channel #AlreadyInChannelBot is already a member #UserNotInCommunityMust join community first #ChannelNotFoundChannel does not exist #UserSuspendedBot has been suspended
Complete Workflow Example
Here’s a complete example joining a community and channel:
// 1. Get bot principal
let botPrincipal = Principal.fromActor(this);
// 2. Join the community
let communityResult = await* botService.joinCommunity(
"uxyan-oyaaa-aaaaf-aaa5q-cai",
null, // No invite code for public communities
botPrincipal
);
switch(communityResult){
case(#ok(msg)){
logService.logInfo("Joined community: " # msg, null);
// 3. Join a channel in that community
let channelResult = await* botService.joinChannel(
"uxyan-oyaaa-aaaaf-aaa5q-cai",
42, // channel ID
null
);
switch(channelResult){
case(#ok(msg)){
logService.logInfo("Joined channel: " # msg, null);
};
case(#err(e)){
logService.logError("Failed to join channel: " # e, null);
};
};
};
case(#err(e)){
logService.logError("Failed to join community: " # e, null);
};
};
Error Handling Best Practices
// Handle already-member gracefully
let result = await* botService.joinGroup(groupId, null);
switch(result){
case(#ok(_)){ /* Success */ };
case(#err("Already in group")){
// Not actually an error - bot is already member
logService.logInfo("Bot already in group", null);
};
case(#err(msg)){
// Actual error
logService.logError("Failed to join: " # msg, null);
};
};
Next Steps
Send Messages Start sending messages to your joined groups
Manage Subscriptions Set up notification subscriptions