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 +107 -0
- package/dist/TeamsClient-D4-exLIC.mjs +225 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +595 -0
- package/dist/index.d.mts +640 -0
- package/dist/index.mjs +3 -0
- package/package.json +32 -0
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 { };
|