Skip to content

Commit 6b107e9

Browse files
committed
refactor(msteams): consolidate stores and send context
1 parent 6d22330 commit 6b107e9

9 files changed

Lines changed: 247 additions & 309 deletions

File tree

src/msteams/conversation-store-fs.ts

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
import crypto from "node:crypto";
2-
import fs from "node:fs";
3-
import path from "node:path";
4-
5-
import lockfile from "proper-lockfile";
6-
71
import type {
82
MSTeamsConversationStore,
93
MSTeamsConversationStoreEntry,
104
StoredConversationReference,
115
} from "./conversation-store.js";
126
import { resolveMSTeamsStorePath } from "./storage.js";
7+
import { readJsonFile, withFileLock, writeJsonFile } from "./store-fs.js";
138

149
type ConversationStoreData = {
1510
version: 1;
@@ -22,83 +17,6 @@ type ConversationStoreData = {
2217
const STORE_FILENAME = "msteams-conversations.json";
2318
const MAX_CONVERSATIONS = 1000;
2419
const CONVERSATION_TTL_MS = 365 * 24 * 60 * 60 * 1000;
25-
const STORE_LOCK_OPTIONS = {
26-
retries: {
27-
retries: 10,
28-
factor: 2,
29-
minTimeout: 100,
30-
maxTimeout: 10_000,
31-
randomize: true,
32-
},
33-
stale: 30_000,
34-
} as const;
35-
36-
function safeParseJson<T>(raw: string): T | null {
37-
try {
38-
return JSON.parse(raw) as T;
39-
} catch {
40-
return null;
41-
}
42-
}
43-
44-
async function readJsonFile<T>(
45-
filePath: string,
46-
fallback: T,
47-
): Promise<{ value: T; exists: boolean }> {
48-
try {
49-
const raw = await fs.promises.readFile(filePath, "utf-8");
50-
const parsed = safeParseJson<T>(raw);
51-
if (parsed == null) return { value: fallback, exists: true };
52-
return { value: parsed, exists: true };
53-
} catch (err) {
54-
const code = (err as { code?: string }).code;
55-
if (code === "ENOENT") return { value: fallback, exists: false };
56-
return { value: fallback, exists: false };
57-
}
58-
}
59-
60-
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
61-
const dir = path.dirname(filePath);
62-
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
63-
const tmp = path.join(
64-
dir,
65-
`${path.basename(filePath)}.${crypto.randomUUID()}.tmp`,
66-
);
67-
await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
68-
encoding: "utf-8",
69-
});
70-
await fs.promises.chmod(tmp, 0o600);
71-
await fs.promises.rename(tmp, filePath);
72-
}
73-
74-
async function ensureJsonFile(filePath: string, fallback: unknown) {
75-
try {
76-
await fs.promises.access(filePath);
77-
} catch {
78-
await writeJsonFile(filePath, fallback);
79-
}
80-
}
81-
82-
async function withFileLock<T>(
83-
filePath: string,
84-
fallback: unknown,
85-
fn: () => Promise<T>,
86-
): Promise<T> {
87-
await ensureJsonFile(filePath, fallback);
88-
let release: (() => Promise<void>) | undefined;
89-
try {
90-
release = await lockfile.lock(filePath, STORE_LOCK_OPTIONS);
91-
return await fn();
92-
} finally {
93-
if (release) {
94-
try {
95-
await release();
96-
} catch {
97-
// ignore unlock errors
98-
}
99-
}
100-
}
101-
}
10220

10321
function parseTimestamp(value: string | undefined): number | null {
10422
if (!value) return null;

src/msteams/messenger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export type MSTeamsAdapter = {
2525
reference: MSTeamsConversationReference,
2626
logic: (context: SendContext) => Promise<void>,
2727
) => Promise<void>;
28+
process: (
29+
req: unknown,
30+
res: unknown,
31+
logic: (context: unknown) => Promise<void>,
32+
) => Promise<void>;
2833
};
2934

3035
export type MSTeamsReplyRenderOptions = {

src/msteams/monitor-handler.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,29 +71,30 @@ export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>(
7171
deps: MSTeamsMessageHandlerDeps,
7272
): T {
7373
const handleTeamsMessage = createMSTeamsMessageHandler(deps);
74-
75-
return handler
76-
.onMessage(async (context, next) => {
77-
try {
78-
await handleTeamsMessage(context as MSTeamsTurnContext);
79-
} catch (err) {
80-
deps.runtime.error?.(danger(`msteams handler failed: ${String(err)}`));
81-
}
82-
await next();
83-
})
84-
.onMembersAdded(async (context, next) => {
85-
const membersAdded =
86-
(context as MSTeamsTurnContext).activity?.membersAdded ?? [];
87-
for (const member of membersAdded) {
88-
if (
89-
member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id
90-
) {
91-
deps.log.debug("member added", { member: member.id });
92-
// Don't send welcome message - let the user initiate conversation.
93-
}
74+
handler.onMessage(async (context, next) => {
75+
try {
76+
await handleTeamsMessage(context as MSTeamsTurnContext);
77+
} catch (err) {
78+
deps.runtime.error?.(danger(`msteams handler failed: ${String(err)}`));
79+
}
80+
await next();
81+
});
82+
83+
handler.onMembersAdded(async (context, next) => {
84+
const membersAdded =
85+
(context as MSTeamsTurnContext).activity?.membersAdded ?? [];
86+
for (const member of membersAdded) {
87+
if (
88+
member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id
89+
) {
90+
deps.log.debug("member added", { member: member.id });
91+
// Don't send welcome message - let the user initiate conversation.
9492
}
95-
await next();
96-
});
93+
}
94+
await next();
95+
});
96+
97+
return handler;
9798
}
9899

99100
function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
@@ -192,8 +193,8 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
192193
if (dmPolicy === "pairing") {
193194
const request = await upsertProviderPairingRequest({
194195
provider: "msteams",
195-
sender: senderId,
196-
label: senderName,
196+
id: senderId,
197+
meta: { name: senderName },
197198
});
198199
if (request) {
199200
log.info("msteams pairing request created", {

src/msteams/monitor.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ export async function monitorMSTeamsProvider(
9696
// Set up the messages endpoint - use configured path and /api/messages as fallback
9797
const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
9898
const messageHandler = (req: Request, res: Response) => {
99+
type HandlerContext = Parameters<(typeof handler)["run"]>[0];
99100
void adapter
100-
.process(req, res, (context) => handler.run(context))
101-
.catch((err) => {
101+
.process(req, res, (context: unknown) =>
102+
handler.run(context as HandlerContext),
103+
)
104+
.catch((err: unknown) => {
102105
log.error("msteams webhook failed", { error: formatUnknownError(err) });
103106
});
104107
};

src/msteams/polls.ts

Lines changed: 1 addition & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import crypto from "node:crypto";
2-
import fs from "node:fs";
3-
import path from "node:path";
4-
5-
import lockfile from "proper-lockfile";
62

73
import { resolveMSTeamsStorePath } from "./storage.js";
4+
import { readJsonFile, withFileLock, writeJsonFile } from "./store-fs.js";
85

96
export type MSTeamsPollVote = {
107
pollId: string;
@@ -50,17 +47,6 @@ type PollStoreData = {
5047
const STORE_FILENAME = "msteams-polls.json";
5148
const MAX_POLLS = 1000;
5249
const POLL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
53-
const STORE_LOCK_OPTIONS = {
54-
retries: {
55-
retries: 10,
56-
factor: 2,
57-
minTimeout: 100,
58-
maxTimeout: 10_000,
59-
randomize: true,
60-
},
61-
stale: 30_000,
62-
} as const;
63-
6450
function isRecord(value: unknown): value is Record<string, unknown> {
6551
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6652
}
@@ -239,73 +225,6 @@ export type MSTeamsPollStoreFsOptions = {
239225
storePath?: string;
240226
};
241227

242-
function safeParseJson<T>(raw: string): T | null {
243-
try {
244-
return JSON.parse(raw) as T;
245-
} catch {
246-
return null;
247-
}
248-
}
249-
250-
async function readJsonFile<T>(
251-
filePath: string,
252-
fallback: T,
253-
): Promise<{ value: T; exists: boolean }> {
254-
try {
255-
const raw = await fs.promises.readFile(filePath, "utf-8");
256-
const parsed = safeParseJson<T>(raw);
257-
if (parsed == null) return { value: fallback, exists: true };
258-
return { value: parsed, exists: true };
259-
} catch (err) {
260-
const code = (err as { code?: string }).code;
261-
if (code === "ENOENT") return { value: fallback, exists: false };
262-
return { value: fallback, exists: false };
263-
}
264-
}
265-
266-
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
267-
const dir = path.dirname(filePath);
268-
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
269-
const tmp = path.join(
270-
dir,
271-
`${path.basename(filePath)}.${crypto.randomUUID()}.tmp`,
272-
);
273-
await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
274-
encoding: "utf-8",
275-
});
276-
await fs.promises.chmod(tmp, 0o600);
277-
await fs.promises.rename(tmp, filePath);
278-
}
279-
280-
async function ensureJsonFile(filePath: string, fallback: unknown) {
281-
try {
282-
await fs.promises.access(filePath);
283-
} catch {
284-
await writeJsonFile(filePath, fallback);
285-
}
286-
}
287-
288-
async function withFileLock<T>(
289-
filePath: string,
290-
fallback: unknown,
291-
fn: () => Promise<T>,
292-
): Promise<T> {
293-
await ensureJsonFile(filePath, fallback);
294-
let release: (() => Promise<void>) | undefined;
295-
try {
296-
release = await lockfile.lock(filePath, STORE_LOCK_OPTIONS);
297-
return await fn();
298-
} finally {
299-
if (release) {
300-
try {
301-
await release();
302-
} catch {
303-
// ignore unlock errors
304-
}
305-
}
306-
}
307-
}
308-
309228
function parseTimestamp(value?: string): number | null {
310229
if (!value) return null;
311230
const parsed = Date.parse(value);

src/msteams/sdk.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { MSTeamsAdapter } from "./messenger.js";
22
import type { MSTeamsCredentials } from "./token.js";
33

4-
export type MSTeamsSdk = Awaited<
5-
ReturnType<typeof import("@microsoft/agents-hosting")>
4+
export type MSTeamsSdk = typeof import("@microsoft/agents-hosting");
5+
export type MSTeamsAuthConfig = ReturnType<
6+
MSTeamsSdk["getAuthConfigWithDefaults"]
67
>;
78

89
export async function loadMSTeamsSdk(): Promise<MSTeamsSdk> {
@@ -12,7 +13,7 @@ export async function loadMSTeamsSdk(): Promise<MSTeamsSdk> {
1213
export function buildMSTeamsAuthConfig(
1314
creds: MSTeamsCredentials,
1415
sdk: MSTeamsSdk,
15-
) {
16+
): MSTeamsAuthConfig {
1617
return sdk.getAuthConfigWithDefaults({
1718
clientId: creds.appId,
1819
clientSecret: creds.appPassword,
@@ -21,7 +22,7 @@ export function buildMSTeamsAuthConfig(
2122
}
2223

2324
export function createMSTeamsAdapter(
24-
authConfig: unknown,
25+
authConfig: MSTeamsAuthConfig,
2526
sdk: MSTeamsSdk,
2627
): MSTeamsAdapter {
2728
return new sdk.CloudAdapter(authConfig) as unknown as MSTeamsAdapter;

0 commit comments

Comments
 (0)