Loading...
Loading...
EXPERIMENTAL, UNTESTED recipe for posting messages to a Slack workspace from a Caffeine canister via the `slack-client` mops package (Slack Web API). Use it when the user wants their app to send a message to a Slack channel — "post to Slack", "notify a channel", "send a Slack message", or equivalent. The client is a pre-release 0.0.3 drop (bot `xoxb-` or user `xoxp-` token): its request path is verified against the live Slack API (a real message posts), but the success-response decode is not yet runtime-confirmed, so treat it as a starting point and do NOT present Slack as a fully supported platform feature yet. Hand-rolling `ic.http_request` calls to `slack.com/api` is still the wrong move — prefer the generated client so bearer auth, percent-encoding, and JSON parsing come for free.
npx skill4agent add caffeinelabs/skills connector-slack⚠️ Experimental (). The request path is fixed and verified — a realslack-client@0.0.3posts successfully against the live Slack API (the earlier query-vs-form bug is resolved; POST params now go in anchat.postMessagebody). NOT yet runtime-verified: the success-response decode (application/x-www-form-urlencoded→ the success schema) and the{"ok":true,…}error envelope (see Known limitations). Treat as pre-release; don't advertise Slack as fully supported until a >= 0.1.0 release.{"ok":false}
slack-clientic.http_requesthttps://slack.com/api/*| User intent | Capability |
|---|---|
| Post a message to a Slack channel as the app | |
| Post to Slack as a named person | |
chatconversationsusersfilesreactionspinsxoxb-xoxp-| The user wants messages to appear as… | Token | Consequences to report back |
|---|---|---|
the app itself (posts show the app's name with an | | One workspace-wide credential, independent of any employee. The bot must be invited to every channel it posts in ( |
| a specific person (posts show that human's name and avatar) | | Every action is attributed to, and audited as, that person. Reaches whatever they can reach, no channel invite needed. Dies when they leave the workspace or revoke the app. Required for a few user-only APIs (e.g. |
xoxb-xoxp-xoxb-chat:writechannels:readreactions:writexoxb-/invite @YourAppnot_in_channelxoxp-chat:writesearch:readxoxp-xoxp-config.authAccessControl.hasPermission(state, caller, #admin)⚠️ Never gate the setter on a first-caller-claims-ownership scheme. On the IC every unauthenticated caller is the same anonymous principal, so if an anonymous call claims ownership first, every anonymous caller passes thecheck and can overwrite the workspace token. Use the authorization component'scaller == ownerpermission, as the example below does.#admin
config.auth = ?#bearer(token)Authorization: Bearer …is_replicated = ?falseis_replicated = ?falseConfigAuthorization: Bearer xoxb-…xoxp-…tsmops add slack-client@0.0.3
mops add caffeineai-authorization@1.0.1ChatApi.chatPostMessage(config, channel, asUser, attachments, blocks, iconEmoji, iconUrl, linkNames, mrkdwn, parse, replyBroadcast, text, threadTs, unfurlLinks, unfurlMedia, username)falseconfig.authimport AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinSlackConfig "mixins/slack-config";
import MixinSlackMessaging "mixins/slack-messaging";
actor {
let accessControlState = AccessControl.initState();
include MixinAuthorization(accessControlState, null);
// Admin-held Slack token, `xoxb-…` or `xoxp-…` — never returned to the frontend.
let slackConfig = { var token : Text = "" };
include MixinSlackConfig(accessControlState, slackConfig);
include MixinSlackMessaging(slackConfig);
};import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";
mixin (
accessControlState : AccessControl.AccessControlState,
slackConfig : { var token : Text },
) {
public query func isSlackConfigured() : async Bool {
slackConfig.token.size() > 0;
};
// Admin-only; accepts either token flavour. NOTE: `#admin` — never a
// first-caller-claims-ownership check,
// which the shared anonymous principal would defeat.
public shared ({ caller }) func setSlackToken(token : Text) : async () {
if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
Runtime.trap("Unauthorized: Only admins can set the Slack token");
};
slackConfig.token := token;
};
};import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import { chatPostMessage } "mo:slack-client/Apis/ChatApi";
import { defaultConfig; type Config } "mo:slack-client/Config";
mixin (slackConfig : { var token : Text }) {
// Non-replicated outcall carrying the Slack token as a bearer credential.
func slackClientConfig(token : Text) : Config {
{
defaultConfig with
auth = ?#bearer(token);
is_replicated = ?false;
max_response_bytes = ?(1_000_000 : Nat64);
};
};
// Post `text` to `channel` (channel ID like "C012AB3CD" or "#general").
// Returns the posted message timestamp (`ts`).
public shared ({ caller }) func postSlackMessage(channel : Text, text : Text) : async Text {
if (caller.isAnonymous()) Runtime.trap("Sign in to post to Slack");
if (slackConfig.token.size() == 0) {
Runtime.trap("Slack is not configured (an admin must set the token)");
};
let res = await* chatPostMessage(
slackClientConfig(slackConfig.token), // token rides config.auth — never a URL param
channel,
"", "", "", "", "", // asUser, attachments, blocks, iconEmoji, iconUrl
false, // linkNames
true, // mrkdwn
"", // parse
false, // replyBroadcast
text, // text
"", // threadTs
false, // unfurlLinks
false, // unfurlMedia
"", // username
);
res.ts;
};
};{"ok": false}{"ok": false, "error": "…"}Error.rejectawaittry { … } catch (e) { Error.message(e) }diagnosticserrorif (res.ok) …oktrueresponseEnvelopeokEnvelopeslack.comconfig.baseUrlhttps://slack.com/apiAuthorization: Bearerdiagnostics