msteams 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # teams-api
2
+
3
+ `teams.cloud.microsoft.har` から Teams Web API の認証情報と実行コンテキストを抽出し、読み取り専用 API を呼び出すためのクライアントです。
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun install
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { TeamsApiClient } from './index.ts'
15
+
16
+ const client = await TeamsApiClient.fromHarFile('./teams.cloud.microsoft.har')
17
+
18
+ const bootstrap = await client.listBootstrap()
19
+ console.log('teams:', bootstrap.teams?.length ?? 0)
20
+ console.log('chats:', bootstrap.chats?.length ?? 0)
21
+
22
+ const notifications = await client.listNotificationMessages({ pageSize: 20 })
23
+ console.log('notification messages:', notifications.messages?.length ?? 0)
24
+ ```
25
+
26
+ Cookie だけで開始する場合:
27
+
28
+ ```ts
29
+ import { TeamsApiClient } from './index.ts'
30
+
31
+ const client = TeamsApiClient.fromCookie(process.env.TEAMS_COOKIE ?? '')
32
+ const auth = await client.refreshSkypeTokenFromCookie()
33
+ console.log(auth.source, auth.expiresAt)
34
+ ```
35
+
36
+ HAR から認証関連 JS を抽出して調べる場合:
37
+
38
+ ```bash
39
+ bun run example/extract-auth-js.ts ./teams.cloud.microsoft.har ./example/har-js 8
40
+ # 出力されたファイルは bunx oxfmt で整形
41
+ ```
42
+
43
+ ## Main APIs
44
+
45
+ - `TeamsApiClient.fromHarFile(harPath)`
46
+ - `HAR` から `skypeToken` とヘッダー情報を抽出してクライアントを作成
47
+ - `TeamsApiClient.fromCookie(cookie)`
48
+ - `cookie` のみでクライアントを作成。初回 API 呼び出し時、または `refreshSkypeTokenFromCookie()` で `authsvc` から `skypeToken` を取得
49
+ - `listBootstrap()`
50
+ - `api/csa/{geo}/api/v3/teams/users/me` を呼び、チーム/チャット一覧を取得
51
+ - `listTeams()`, `listChats()`
52
+ - `listBootstrap()` の便利ラッパー
53
+ - `listChannelPosts(teamId, channelId)`
54
+ - `api/csa/{geo}/api/v1/teams/{teamId}/channels/{channelId}` を呼び、投稿スレッド(`replyChains`)を取得
55
+ - `listChannelMessages(teamId, channelId)`
56
+ - `replyChains` からメッセージを平坦化
57
+ - `getConversation(conversationId)`
58
+ - `api/chatsvc/{geo}/api/v1/users/ME/conversations/{conversationId}` を取得
59
+ - `listConversationMessages(conversationId, options)`
60
+ - `api/chatsvc/{geo}/api/v1/users/ME/conversations/{conversationId}/messages` を取得
61
+ - `listNotificationMessages()`, `listMentionMessages()`, `listAnnotationMessages()`
62
+ - それぞれ `48:notifications`, `48:mentions`, `48:annotations` の便利ラッパー
63
+
64
+ ## CLI Usage
65
+
66
+ ```bash
67
+ bun run src/cli/index.ts [options] <command> [arguments]
68
+ ```
69
+
70
+ またはインストール後:
71
+
72
+ ```bash
73
+ teams [options] <command> [arguments]
74
+ ```
75
+
76
+ ### Commands
77
+
78
+ - `teams notifications [--limit N]`
79
+ - 通知一覧を取得
80
+ - `teams messages <conversationId> [--limit N]`
81
+ - 会話のメッセージを取得
82
+ - `teams channel messages <channelId> [--limit N]`
83
+ - チームチャンネルのメッセージを取得(`me` 結果から `teamId` を自動解決)
84
+ - `teams teams list`
85
+ - 参加チーム一覧を取得
86
+ - `teams teams channels <teamId>`
87
+ - 指定チームのチャンネル一覧を取得
88
+ - `teams me`
89
+ - 現在ユーザー情報(チーム/チャット含む)を取得
90
+ - `teams set-refresh-token --refresh-token=<token> [--profile=<name>]`
91
+ - プロファイルへリフレッシュトークンを保存
92
+
93
+ ### Options
94
+
95
+ - `--profile=<name>`: `~/.teams-cli/<name>.json` を使用(既定: `default`)
96
+ - `--profile-json=<path>`: 任意の profile json を使用
97
+ - `--refresh-token=<token>`: リフレッシュトークンを指定
98
+ - `--json`: 機械可読 JSON で出力
99
+ - `--no-color`: ANSI カラーを無効化
100
+ - `--help`: ヘルプ表示
101
+
102
+ ## Notes
103
+
104
+ - 本実装は **write 系 API を含みません**。
105
+ - `HAR` は通常 `Authorization/Cookie` を完全に含まないため、抽出できる `skypeToken` の有効期限切れ時は再取得が必要です。
106
+ - `cookie` 単体で動かす場合でも、内部では `https://teams.microsoft.com/api/authsvc/v1.0/authz` に対して `cookie` を使い `skypeToken` を取得してから各 API を呼びます。
107
+ - `fromCookie()` で `endpoint.baseUrl` を上書きした場合、`endpoint.authzUrl` を未指定ならクラウド環境に応じた `authsvc` URL を自動推定します。
@@ -0,0 +1,225 @@
1
+ //#region src/auth/TokenManager.ts
2
+ var TokenManager = class {
3
+ #refresh;
4
+ #tokenCache = /* @__PURE__ */ new Map();
5
+ constructor(refreshToken) {
6
+ this.#refresh = refreshToken;
7
+ }
8
+ async getToken(scope) {
9
+ return this.getTokenFromScope(scope);
10
+ }
11
+ getRefreshToken() {
12
+ return this.#refresh;
13
+ }
14
+ async getTokenFromScope(scope) {
15
+ const cached = this.#tokenCache.get(scope);
16
+ if (cached && cached.expiresAt > Date.now()) return cached.token;
17
+ const formData = new URLSearchParams();
18
+ formData.append("client_id", "5e3ce6c0-2b1f-4285-8d4b-75ee78787346");
19
+ formData.append("redirect_uri", "https://teams.cloud.microsoft/v2/auth");
20
+ formData.append("scope", scope);
21
+ formData.append("grant_type", "refresh_token");
22
+ formData.append("client_info", "1");
23
+ formData.append("x-client-SKU", "msal.js.browser");
24
+ formData.append("x-client-VER", "3.30.0");
25
+ formData.append("x-ms-lib-capability", "retry-after, h429");
26
+ formData.append("x-client-current-telemetry", "5|61,0,,,|");
27
+ formData.append("x-client-last-telemetry", "5|0|||0,0");
28
+ formData.append("refresh_token", this.#refresh);
29
+ const newToken = await fetch("https://login.microsoftonline.com/83d9219c-a57d-4d58-b3e5-4abef53925a2/oauth2/v2.0/token?client-request-id=Core-5e01fb3c-a48b-44c9-83fa-2aa7be8fd4e3", {
30
+ headers: {
31
+ "content-type": "application/x-www-form-urlencoded;charset=utf-8",
32
+ origin: "https://teams.cloud.microsoft"
33
+ },
34
+ referrer: "https://teams.cloud.microsoft/",
35
+ body: formData,
36
+ method: "POST",
37
+ mode: "cors",
38
+ credentials: "omit"
39
+ });
40
+ if (!newToken.ok) throw new Error(`Failed to refresh token: ${newToken.status} ${newToken.statusText}`);
41
+ const newTokenJson = await newToken.json();
42
+ this.#refresh = newTokenJson.refresh_token;
43
+ this.#tokenCache.set(scope, {
44
+ token: newTokenJson.access_token,
45
+ expiresAt: Date.now() + newTokenJson.expires_in * 1e3 - 300 * 1e3
46
+ });
47
+ return newTokenJson.access_token;
48
+ }
49
+ };
50
+
51
+ //#endregion
52
+ //#region src/rest/constants.ts
53
+ const TEAMS_WORKER_REFERRER = "https://teams.cloud.microsoft/v2/worker/precompiled-web-worker-8e61d59c0abedf86.js";
54
+ const SCOPES = {
55
+ chats: "https://ic3.teams.office.com/.default openid profile offline_access",
56
+ channels: "https://chatsvcagg.teams.microsoft.com/.default openid profile offline_access",
57
+ users: "https://api.spaces.skype.com/.default openid profile offline_access"
58
+ };
59
+
60
+ //#endregion
61
+ //#region src/rest/RestClient.ts
62
+ const appendQuery = (url, query) => {
63
+ if (!query) return url;
64
+ const queryString = (query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => {
65
+ if (typeof value === "undefined") return [];
66
+ return [[key, String(value)]];
67
+ }))).toString();
68
+ if (!queryString) return url;
69
+ return `${url}${url.includes("?") ? "&" : "?"}${queryString}`;
70
+ };
71
+ var RestClient = class {
72
+ #tokenProvider;
73
+ #referrer;
74
+ constructor(tokenProvider, referrer = TEAMS_WORKER_REFERRER) {
75
+ this.#tokenProvider = tokenProvider;
76
+ this.#referrer = referrer;
77
+ }
78
+ async request(url, options) {
79
+ const token = await this.#tokenProvider.getTokenFromScope(options.scope);
80
+ const headers = new Headers(options.headers);
81
+ headers.set("authorization", `Bearer ${token}`);
82
+ const method = options.method ?? "GET";
83
+ const res = await fetch(appendQuery(url, options.query), {
84
+ method,
85
+ headers,
86
+ referrer: options.referrer ?? this.#referrer,
87
+ body: options.body
88
+ });
89
+ if (!res.ok) throw new Error(`Failed request ${method} ${url}: ${res.status} ${res.statusText}`);
90
+ return await res.json();
91
+ }
92
+ };
93
+
94
+ //#endregion
95
+ //#region src/rest/routes/channels.ts
96
+ const fetchChannel = async (rest, teamId, channelId, options) => {
97
+ const pageSize = options?.pageSize ?? 20;
98
+ return rest.request(`https://teams.cloud.microsoft/api/csa/apac/api/v1/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}`, {
99
+ scope: SCOPES.channels,
100
+ query: { pageSize }
101
+ });
102
+ };
103
+ const flattenChannelMessages = (posts) => {
104
+ const messages = [];
105
+ for (const chain of posts.replyChains) {
106
+ if (Array.isArray(chain.messages)) {
107
+ messages.push(...chain.messages);
108
+ continue;
109
+ }
110
+ if (Array.isArray(chain.replies)) {
111
+ messages.push(...chain.replies);
112
+ continue;
113
+ }
114
+ if (chain.message) messages.push(chain.message);
115
+ }
116
+ return messages;
117
+ };
118
+ const fetchChannelMessages = async (rest, teamId, channelId, options) => {
119
+ return flattenChannelMessages(await fetchChannel(rest, teamId, channelId, options));
120
+ };
121
+
122
+ //#endregion
123
+ //#region src/rest/routes/conversations.ts
124
+ const fetchConversationMessages = async (rest, conversationId, options) => {
125
+ const { view = "msnp24Equivalent|supportsMessageProperties", pageSize = 200, startTime = 1, syncState, endTime, draftVersion } = options ?? {};
126
+ const query = new URLSearchParams({
127
+ view,
128
+ pageSize: `${pageSize}`,
129
+ startTime: `${startTime}`
130
+ });
131
+ if (syncState) query.set("syncState", syncState);
132
+ if (typeof endTime !== "undefined") query.set("endTime", `${endTime}`);
133
+ if (draftVersion) query.set("draftVersion", draftVersion);
134
+ const normalizedConversationId = encodeURIComponent(conversationId);
135
+ return rest.request(`https://teams.cloud.microsoft/api/chatsvc/jp/v1/users/ME/conversations/${normalizedConversationId}/messages`, {
136
+ scope: SCOPES.chats,
137
+ query
138
+ });
139
+ };
140
+
141
+ //#endregion
142
+ //#region src/rest/routes/users.ts
143
+ const fetchShortProfile = async (rest, mriOrEmailArray, options) => {
144
+ const { isMailAddress = false, skypeTeamsInfo = true, canBeSmtpAddress = false, includeIBBarredUsers = true, includeDisabledAccounts = true } = options ?? {};
145
+ const query = new URLSearchParams({
146
+ isMailAddress: `${isMailAddress}`,
147
+ enableGuest: "true",
148
+ skypeTeamsInfo: `${skypeTeamsInfo}`,
149
+ canBeSmtpAddress: `${canBeSmtpAddress}`,
150
+ includeIBBarredUsers: `${includeIBBarredUsers}`,
151
+ includeDisabledAccounts: `${includeDisabledAccounts}`
152
+ });
153
+ return (await rest.request("https://teams.cloud.microsoft/api/mt/apac/beta/users/fetchShortProfile", {
154
+ scope: SCOPES.users,
155
+ method: "POST",
156
+ query,
157
+ headers: { "content-type": "application/json;charset=UTF-8" },
158
+ body: JSON.stringify(mriOrEmailArray)
159
+ })).value;
160
+ };
161
+ const fetchCurrentUser = async (rest) => {
162
+ return rest.request("https://teams.cloud.microsoft/api/csa/apac/api/v3/teams/users/me", {
163
+ scope: SCOPES.channels,
164
+ query: {
165
+ isPrefetch: false,
166
+ enableMembershipSummary: true,
167
+ supportsAdditionalSystemGeneratedFolders: true,
168
+ supportsSliceItems: true,
169
+ enableEngageCommunities: false
170
+ }
171
+ });
172
+ };
173
+ const fetchPinnedChannels = async (rest) => {
174
+ return rest.request("https://teams.cloud.microsoft/api/csa/apac/api/v1/teams/users/me/pinnedChannels", { scope: SCOPES.channels });
175
+ };
176
+
177
+ //#endregion
178
+ //#region src/client/TeamsClient.ts
179
+ var TeamsClient = class {
180
+ rest;
181
+ teams;
182
+ constructor(tokenProvider) {
183
+ this.rest = new RestClient(tokenProvider);
184
+ this.teams = {
185
+ conversations: { fetchMessages: (conversationId, options) => this.fetchConversationMessages(conversationId, options) },
186
+ notifications: {
187
+ fetchMessages: (options) => this.fetchConversationMessages("48:notifications", options),
188
+ fetchMentions: (options) => this.fetchConversationMessages("48:mentions", options),
189
+ fetchAnnotations: (options) => this.fetchConversationMessages("48:annotations", options)
190
+ },
191
+ channels: {
192
+ fetch: (teamId, channelId, options) => this.fetchChannel(teamId, channelId, options),
193
+ fetchMessages: (teamId, channelId, options) => this.fetchChannelMessages(teamId, channelId, options)
194
+ },
195
+ users: {
196
+ fetchShortProfile: (mriOrEmailArray, options) => this.fetchShortProfile(mriOrEmailArray, options),
197
+ me: {
198
+ fetch: () => this.fetchCurrentUser(),
199
+ fetchPinnedChannels: () => this.fetchPinnedChannels()
200
+ }
201
+ }
202
+ };
203
+ }
204
+ async fetchConversationMessages(conversationId, options) {
205
+ return fetchConversationMessages(this.rest, conversationId, options);
206
+ }
207
+ async fetchChannel(teamId, channelId, options) {
208
+ return fetchChannel(this.rest, teamId, channelId, options);
209
+ }
210
+ async fetchChannelMessages(teamId, channelId, options) {
211
+ return fetchChannelMessages(this.rest, teamId, channelId, options);
212
+ }
213
+ async fetchShortProfile(mriOrEmailArray, options) {
214
+ return fetchShortProfile(this.rest, mriOrEmailArray, options);
215
+ }
216
+ async fetchCurrentUser() {
217
+ return fetchCurrentUser(this.rest);
218
+ }
219
+ async fetchPinnedChannels() {
220
+ return fetchPinnedChannels(this.rest);
221
+ }
222
+ };
223
+
224
+ //#endregion
225
+ export { TokenManager as n, TeamsClient as t };
@@ -0,0 +1 @@
1
+ export { };