docs: add MS Teams provider research document
Initial research and implementation guide for adding msteams as a new messaging provider. Includes: - Provider structure patterns from existing implementations - Gateway integration requirements - Config types and validation schemas - Onboarding flow patterns - MS Teams Bot Framework SDK considerations - Files to create/modify checklist This is exploratory work - implementation plan to follow.
This commit is contained in:
585
tmp/msteams-provider-research.md
Normal file
585
tmp/msteams-provider-research.md
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
# MS Teams Provider Research
|
||||||
|
|
||||||
|
> Exploratory notes for adding `msteams` as a new provider to Clawdbot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Existing Provider Structure Analysis
|
||||||
|
|
||||||
|
### Directory Structure Pattern
|
||||||
|
|
||||||
|
Each provider follows this structure (using Slack as reference):
|
||||||
|
|
||||||
|
```
|
||||||
|
src/slack/
|
||||||
|
├── index.ts # Public exports (barrel file)
|
||||||
|
├── monitor.ts # Main event loop & message handling
|
||||||
|
├── monitor.test.ts # Unit tests
|
||||||
|
├── monitor.tool-result.test.ts # Integration tests
|
||||||
|
├── send.ts # Outbound message delivery
|
||||||
|
├── actions.ts # Platform API actions (reactions, edits, pins)
|
||||||
|
├── token.ts # Token resolution & validation
|
||||||
|
└── probe.ts # Health check / connectivity validation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Files by Provider
|
||||||
|
|
||||||
|
| Provider | Files |
|
||||||
|
|----------|-------|
|
||||||
|
| Telegram | bot.ts, monitor.ts, send.ts, probe.ts, token.ts, webhook.ts, download.ts, draft-stream.ts, pairing-store.ts |
|
||||||
|
| Discord | monitor.ts, send.ts, probe.ts, token.ts |
|
||||||
|
| Slack | monitor.ts, send.ts, actions.ts, probe.ts, token.ts |
|
||||||
|
| Signal | monitor.ts, send.ts, probe.ts (uses signal-cli) |
|
||||||
|
| iMessage | monitor.ts, send.ts, probe.ts (uses imsg CLI) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Monitor Pattern (Event Loop)
|
||||||
|
|
||||||
|
The `monitorXxxProvider()` function is the heart of each provider. Pattern from Slack:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||||
|
// 1. Load configuration
|
||||||
|
const cfg = loadConfig();
|
||||||
|
|
||||||
|
// 2. Resolve tokens (options > env > config)
|
||||||
|
const botToken = resolveSlackBotToken(
|
||||||
|
opts.botToken ?? process.env.SLACK_BOT_TOKEN ?? cfg.slack?.botToken
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Create SDK client
|
||||||
|
const app = new App({
|
||||||
|
token: botToken,
|
||||||
|
appToken,
|
||||||
|
socketMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Authenticate and cache identity
|
||||||
|
const auth = await app.client.auth.test({ token: botToken });
|
||||||
|
|
||||||
|
// 5. Set up caches (channel info, user info, message dedup)
|
||||||
|
const channelCache = new Map<string, ChannelInfo>();
|
||||||
|
const userCache = new Map<string, UserInfo>();
|
||||||
|
const seenMessages = new Map<string, number>();
|
||||||
|
|
||||||
|
// 6. Register event handlers
|
||||||
|
app.event("message", async ({ event }) => {
|
||||||
|
await handleMessage(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Start and wait for abort signal
|
||||||
|
await app.start();
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
opts.abortSignal?.addEventListener("abort", () => resolve());
|
||||||
|
});
|
||||||
|
await app.stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Processing Pipeline
|
||||||
|
|
||||||
|
1. **Validation**: Check message type, ignore bots, dedup check
|
||||||
|
2. **Channel Resolution**: Get channel metadata (name, type, topic)
|
||||||
|
3. **Authorization Checks**: DM policy, channel allowlist, user allowlist, mention requirements
|
||||||
|
4. **Media Download**: Fetch attachments with size limits
|
||||||
|
5. **Acknowledgment**: Send reaction to confirm receipt
|
||||||
|
6. **Envelope Construction**: Build `ctxPayload` with all message metadata
|
||||||
|
7. **System Event Logging**: `enqueueSystemEvent()`
|
||||||
|
8. **Reply Dispatcher Setup**: Configure typing indicators and threading
|
||||||
|
9. **Dispatch to Agent**: `dispatchReplyFromConfig()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Gateway Integration
|
||||||
|
|
||||||
|
### Provider Manager (src/gateway/server-providers.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Status types per provider
|
||||||
|
export type SlackRuntimeStatus = {
|
||||||
|
running: boolean;
|
||||||
|
lastStartAt?: number | null;
|
||||||
|
lastStopAt?: number | null;
|
||||||
|
lastError?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combined snapshot
|
||||||
|
export type ProviderRuntimeSnapshot = {
|
||||||
|
whatsapp: WebProviderStatus;
|
||||||
|
telegram: TelegramRuntimeStatus;
|
||||||
|
discord: DiscordRuntimeStatus;
|
||||||
|
slack: SlackRuntimeStatus;
|
||||||
|
signal: SignalRuntimeStatus;
|
||||||
|
imessage: IMessageRuntimeStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manager interface
|
||||||
|
export type ProviderManager = {
|
||||||
|
getRuntimeSnapshot: () => ProviderRuntimeSnapshot;
|
||||||
|
startProviders: () => Promise<void>;
|
||||||
|
startSlackProvider: () => Promise<void>;
|
||||||
|
stopSlackProvider: () => Promise<void>;
|
||||||
|
// ... per provider
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// State tracking
|
||||||
|
let slackAbort: AbortController | null = null;
|
||||||
|
let slackTask: Promise<unknown> | null = null;
|
||||||
|
let slackRuntime: SlackRuntimeStatus = { running: false };
|
||||||
|
|
||||||
|
const startSlackProvider = async () => {
|
||||||
|
if (slackTask) return; // Already running
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
if (cfg.slack?.enabled === false) return;
|
||||||
|
|
||||||
|
const botToken = resolveSlackBotToken(...);
|
||||||
|
if (!botToken) return; // Not configured
|
||||||
|
|
||||||
|
slackAbort = new AbortController();
|
||||||
|
slackRuntime = { running: true, lastStartAt: Date.now() };
|
||||||
|
|
||||||
|
slackTask = monitorSlackProvider({
|
||||||
|
botToken,
|
||||||
|
runtime: slackRuntimeEnv,
|
||||||
|
abortSignal: slackAbort.signal,
|
||||||
|
})
|
||||||
|
.catch(err => { slackRuntime.lastError = formatError(err); })
|
||||||
|
.finally(() => {
|
||||||
|
slackAbort = null;
|
||||||
|
slackTask = null;
|
||||||
|
slackRuntime.running = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### RuntimeEnv Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Minimal interface for provider DI
|
||||||
|
export type RuntimeEnv = {
|
||||||
|
log: typeof console.log;
|
||||||
|
error: typeof console.error;
|
||||||
|
exit: (code: number) => never;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Created from subsystem logger
|
||||||
|
const logSlack = logProviders.child("slack");
|
||||||
|
const slackRuntimeEnv = runtimeForLogger(logSlack);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Hot-Reload (src/gateway/config-reload.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const RELOAD_RULES: ReloadRule[] = [
|
||||||
|
{ prefix: "slack", kind: "hot", actions: ["restart-provider:slack"] },
|
||||||
|
{ prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] },
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Configuration Types
|
||||||
|
|
||||||
|
### Pattern from SlackConfig (src/config/types.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type SlackConfig = {
|
||||||
|
enabled?: boolean; // Master toggle
|
||||||
|
botToken?: string; // Primary credential
|
||||||
|
appToken?: string; // Socket mode credential
|
||||||
|
groupPolicy?: GroupPolicy; // "open" | "disabled" | "allowlist"
|
||||||
|
textChunkLimit?: number; // Platform message limit
|
||||||
|
mediaMaxMb?: number; // File size limit
|
||||||
|
dm?: SlackDmConfig; // DM-specific settings
|
||||||
|
channels?: Record<string, SlackChannelConfig>; // Per-channel config
|
||||||
|
actions?: SlackActionConfig; // Feature gating
|
||||||
|
slashCommand?: SlackSlashCommandConfig; // Command config
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SlackDmConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
policy?: DmPolicy; // "pairing" | "allowlist" | "open" | "disabled"
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
groupEnabled?: boolean;
|
||||||
|
groupChannels?: Array<string | number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SlackChannelConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
users?: Array<string | number>; // Per-channel allowlist
|
||||||
|
skills?: string[]; // Skill filter
|
||||||
|
systemPrompt?: string; // Channel-specific prompt
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SlackActionConfig = {
|
||||||
|
reactions?: boolean;
|
||||||
|
messages?: boolean;
|
||||||
|
pins?: boolean;
|
||||||
|
search?: boolean;
|
||||||
|
// ... feature toggles
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Where Provider Appears in Config
|
||||||
|
|
||||||
|
- `ClawdbotConfig.slack` - main config block
|
||||||
|
- `QueueModeByProvider.slack` - queue mode override
|
||||||
|
- `AgentElevatedAllowFromConfig.slack` - elevated permissions
|
||||||
|
- `HookMappingConfig.provider` - webhook routing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Zod Validation Schema
|
||||||
|
|
||||||
|
### Pattern (src/config/zod-schema.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const SlackConfigSchema = z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
botToken: z.string().optional(),
|
||||||
|
appToken: z.string().optional(),
|
||||||
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
||||||
|
textChunkLimit: z.number().optional(),
|
||||||
|
mediaMaxMb: z.number().optional(),
|
||||||
|
dm: SlackDmConfigSchema.optional(),
|
||||||
|
channels: z.record(z.string(), SlackChannelConfigSchema).optional(),
|
||||||
|
actions: SlackActionConfigSchema.optional(),
|
||||||
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
// Cross-field validation
|
||||||
|
if (value.dm?.policy === "open" && !value.dm?.allowFrom?.includes("*")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["dm", "allowFrom"],
|
||||||
|
message: 'slack.dm.policy="open" requires allowFrom to include "*"',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Onboarding Flow
|
||||||
|
|
||||||
|
### Pattern (src/commands/onboard-providers.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Status detection
|
||||||
|
const slackConfigured = Boolean(
|
||||||
|
process.env.SLACK_BOT_TOKEN || cfg.slack?.botToken
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Provider selection
|
||||||
|
const selection = await prompter.multiselect({
|
||||||
|
message: "Select providers",
|
||||||
|
options: [
|
||||||
|
{ value: "slack", label: "Slack", hint: slackConfigured ? "configured" : "needs token" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Credential collection
|
||||||
|
if (selection.includes("slack")) {
|
||||||
|
if (process.env.SLACK_BOT_TOKEN && !cfg.slack?.botToken) {
|
||||||
|
const useEnv = await prompter.confirm({
|
||||||
|
message: "SLACK_BOT_TOKEN detected. Use env var?",
|
||||||
|
});
|
||||||
|
if (!useEnv) {
|
||||||
|
token = await prompter.text({ message: "Enter Slack bot token" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ... also collect app token for socket mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. DM policy configuration
|
||||||
|
const policy = await selectPolicy({ label: "Slack", provider: "slack" });
|
||||||
|
cfg = setSlackDmPolicy(cfg, policy);
|
||||||
|
```
|
||||||
|
|
||||||
|
### DM Policy Setter Helper
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function setSlackDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
|
||||||
|
const dm = cfg.slack?.dm ?? {};
|
||||||
|
const allowFrom = dmPolicy === "open"
|
||||||
|
? addWildcardAllowFrom(dm.allowFrom)
|
||||||
|
: dm.allowFrom;
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
slack: {
|
||||||
|
...cfg.slack,
|
||||||
|
dm: { ...dm, policy: dmPolicy, ...(allowFrom ? { allowFrom } : {}) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Probe (Health Check)
|
||||||
|
|
||||||
|
### Pattern (src/slack/probe.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type SlackProbe = {
|
||||||
|
ok: boolean;
|
||||||
|
status?: number | null;
|
||||||
|
error?: string | null;
|
||||||
|
elapsedMs?: number | null;
|
||||||
|
bot?: { id?: string; name?: string };
|
||||||
|
team?: { id?: string; name?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function probeSlack(
|
||||||
|
token: string,
|
||||||
|
timeoutMs = 2500,
|
||||||
|
): Promise<SlackProbe> {
|
||||||
|
const client = new WebClient(token);
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await withTimeout(client.auth.test(), timeoutMs);
|
||||||
|
if (!result.ok) {
|
||||||
|
return { ok: false, status: 200, error: result.error };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
elapsedMs: Date.now() - start,
|
||||||
|
bot: { id: result.user_id, name: result.user },
|
||||||
|
team: { id: result.team_id, name: result.team },
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, status: err.status, error: err.message, elapsedMs: Date.now() - start };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Send Function
|
||||||
|
|
||||||
|
### Pattern (src/slack/send.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function sendMessageSlack(
|
||||||
|
to: string,
|
||||||
|
message: string,
|
||||||
|
opts: SlackSendOpts = {},
|
||||||
|
): Promise<SlackSendResult> {
|
||||||
|
// 1. Parse recipient (user:X, channel:Y, #channel, @user, etc.)
|
||||||
|
const recipient = parseRecipient(to);
|
||||||
|
|
||||||
|
// 2. Resolve channel ID (open DM if needed)
|
||||||
|
const { channelId } = await resolveChannelId(client, recipient);
|
||||||
|
|
||||||
|
// 3. Chunk text to platform limit
|
||||||
|
const chunks = chunkMarkdownText(message, chunkLimit);
|
||||||
|
|
||||||
|
// 4. Upload media if present
|
||||||
|
if (opts.mediaUrl) {
|
||||||
|
await uploadSlackFile({ client, channelId, mediaUrl, threadTs });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Send each chunk
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
await client.chat.postMessage({
|
||||||
|
channel: channelId,
|
||||||
|
text: chunk,
|
||||||
|
thread_ts: opts.threadTs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messageId, channelId };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. CLI Integration
|
||||||
|
|
||||||
|
### Dependencies (src/cli/deps.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type CliDeps = {
|
||||||
|
sendMessageWhatsApp: typeof sendMessageWhatsApp;
|
||||||
|
sendMessageTelegram: typeof sendMessageTelegram;
|
||||||
|
sendMessageDiscord: typeof sendMessageDiscord;
|
||||||
|
sendMessageSlack: typeof sendMessageSlack;
|
||||||
|
sendMessageSignal: typeof sendMessageSignal;
|
||||||
|
sendMessageIMessage: typeof sendMessageIMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDefaultDeps(): CliDeps {
|
||||||
|
return {
|
||||||
|
sendMessageWhatsApp,
|
||||||
|
sendMessageTelegram,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send Command (src/commands/send.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const provider = (opts.provider ?? "whatsapp").toLowerCase();
|
||||||
|
|
||||||
|
// Provider-specific delivery
|
||||||
|
const results = await deliverOutboundPayloads({
|
||||||
|
cfg: loadConfig(),
|
||||||
|
provider,
|
||||||
|
to: resolvedTarget.to,
|
||||||
|
payloads: [{ text: opts.message, mediaUrl: opts.media }],
|
||||||
|
deps: {
|
||||||
|
sendSlack: deps.sendMessageSlack,
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Files to Create/Modify for MS Teams
|
||||||
|
|
||||||
|
### New Files (src/msteams/)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/msteams/
|
||||||
|
├── index.ts # Exports
|
||||||
|
├── monitor.ts # Bot Framework event loop
|
||||||
|
├── send.ts # Send via Graph API
|
||||||
|
├── probe.ts # Health check (Graph API /me)
|
||||||
|
├── token.ts # Token resolution
|
||||||
|
├── actions.ts # Optional: reactions, edits, etc.
|
||||||
|
└── *.test.ts # Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `src/config/types.ts` | Add `MSTeamsConfig`, update `QueueModeByProvider`, `AgentElevatedAllowFromConfig`, `HookMappingConfig` |
|
||||||
|
| `src/config/zod-schema.ts` | Add `MSTeamsConfigSchema` |
|
||||||
|
| `src/gateway/server-providers.ts` | Add `MSTeamsRuntimeStatus`, lifecycle methods, update `ProviderRuntimeSnapshot`, `ProviderManager` |
|
||||||
|
| `src/gateway/server.ts` | Add logger, runtimeEnv, pass to provider manager |
|
||||||
|
| `src/gateway/config-reload.ts` | Add reload rule |
|
||||||
|
| `src/gateway/server-methods/providers.ts` | Add status endpoint |
|
||||||
|
| `src/cli/deps.ts` | Add `sendMessageMSTeams` |
|
||||||
|
| `src/cli/program.ts` | Add to `--provider` options |
|
||||||
|
| `src/commands/send.ts` | Add msteams case |
|
||||||
|
| `src/commands/onboard-providers.ts` | Add wizard flow |
|
||||||
|
| `src/commands/onboard-types.ts` | Add to `ProviderChoice` |
|
||||||
|
| `docs/providers/msteams.md` | Documentation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. MS Teams SDK Options
|
||||||
|
|
||||||
|
### Option A: Bot Framework SDK (@microsoft/botframework)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CloudAdapter, ConfigurationBotFrameworkAuthentication } from "botbuilder";
|
||||||
|
|
||||||
|
// Pros: Full-featured, handles auth, typing indicators, cards
|
||||||
|
// Cons: More complex, requires Azure Bot registration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Microsoft Graph API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
|
||||||
|
// Pros: Simpler for basic messaging, direct API access
|
||||||
|
// Cons: Less rich features, manual auth handling
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended: Bot Framework for receiving, Graph for some sends
|
||||||
|
|
||||||
|
MS Teams bots use the Bot Framework for receiving messages (webhook-based), and can use either Bot Framework or Graph API for sending.
|
||||||
|
|
||||||
|
### Required Azure Resources
|
||||||
|
|
||||||
|
1. **Azure Bot Registration** - Bot identity and channel configuration
|
||||||
|
2. **App Registration** - OAuth for Graph API access
|
||||||
|
3. **Teams App Manifest** - Defines bot capabilities in Teams
|
||||||
|
|
||||||
|
### Credentials Needed
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type MSTeamsConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
appId?: string; // Azure AD App ID
|
||||||
|
appPassword?: string; // Azure AD App Secret
|
||||||
|
tenantId?: string; // Optional: restrict to tenant
|
||||||
|
// ... rest follows pattern
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Key Differences from Slack
|
||||||
|
|
||||||
|
| Aspect | Slack | MS Teams |
|
||||||
|
|--------|-------|----------|
|
||||||
|
| Connection | Socket Mode (WebSocket) | Webhook (HTTP POST) |
|
||||||
|
| Auth | Bot Token + App Token | Azure AD App ID + Secret |
|
||||||
|
| Message ID | `ts` (timestamp) | Activity ID |
|
||||||
|
| Threading | `thread_ts` | `replyToId` in conversation |
|
||||||
|
| Channels | Channel ID | Channel ID + Team ID |
|
||||||
|
| DMs | `conversations.open` | Proactive messaging with conversation reference |
|
||||||
|
| Typing | `assistant.threads.setStatus` | `sendTypingActivity()` |
|
||||||
|
| Reactions | `reactions.add` | Separate message with reaction |
|
||||||
|
| Media | `files.uploadV2` | Attachments in activity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Implementation Considerations
|
||||||
|
|
||||||
|
### Webhook vs Polling
|
||||||
|
|
||||||
|
MS Teams uses webhooks exclusively (no polling option like Telegram). Need to:
|
||||||
|
- Expose HTTP endpoint for Bot Framework
|
||||||
|
- Handle activity validation (HMAC signature)
|
||||||
|
- Consider tunneling for local dev (ngrok, Tailscale funnel)
|
||||||
|
|
||||||
|
### Proactive Messaging
|
||||||
|
|
||||||
|
Unlike Slack where you can message any user, Teams requires:
|
||||||
|
- User must have interacted with bot first, OR
|
||||||
|
- Bot must be installed in team/chat, OR
|
||||||
|
- Use Graph API with appropriate permissions
|
||||||
|
|
||||||
|
### Tenant Restrictions
|
||||||
|
|
||||||
|
Enterprise Teams often restrict:
|
||||||
|
- External app installations
|
||||||
|
- Cross-tenant communication
|
||||||
|
- Certain API permissions
|
||||||
|
|
||||||
|
Config should support `tenantId` restriction.
|
||||||
|
|
||||||
|
### Cards and Adaptive Cards
|
||||||
|
|
||||||
|
Teams heavily uses Adaptive Cards for rich UI. Consider supporting:
|
||||||
|
- Basic text (markdown subset)
|
||||||
|
- Adaptive Card JSON
|
||||||
|
- Hero Cards for media
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Research**: MS Teams Bot Framework SDK specifics
|
||||||
|
2. **Azure Setup**: Document bot registration process
|
||||||
|
3. **Implement**: Start with monitor.ts and basic send
|
||||||
|
4. **Test**: Local dev with ngrok/tunnel
|
||||||
|
5. **Docs**: Provider setup guide
|
||||||
Reference in New Issue
Block a user