Skip to content

Commit 8dcb166

Browse files
anchenyihermanwenhe
authored andcommitted
feat: switch to builder API for declarative agent apps (OfficeDev#13056)
* feat: add builer api
1 parent 8539e80 commit 8dcb166

File tree

4 files changed

+422
-6
lines changed

4 files changed

+422
-6
lines changed

packages/fx-core/src/component/m365/packageService.ts

+142-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { hooks } from "@feathersjs/hooks";
5-
import { LogProvider, SystemError, UserError } from "@microsoft/teamsfx-api";
5+
import { LogProvider, SystemError, TeamsAppManifest, UserError } from "@microsoft/teamsfx-api";
66
import AdmZip from "adm-zip";
77
import FormData from "form-data";
88
import fs from "fs-extra";
@@ -20,10 +20,17 @@ import { waitSeconds } from "../../common/utils";
2020
import { WrappedAxiosClient } from "../../common/wrappedAxiosClient";
2121
import { NotExtendedToM365Error } from "./errors";
2222
import { MosServiceEndpoint } from "./serviceConstant";
23+
import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker";
24+
import stripBom from "strip-bom";
2325

2426
const M365ErrorSource = "M365";
2527
const M365ErrorComponent = "PackageService";
2628

29+
export enum AppScope {
30+
Personal = "Personal",
31+
Shared = "Shared",
32+
}
33+
2734
// Call m365 service for package CRUD
2835
export class PackageService {
2936
private static sharedInstance: PackageService;
@@ -139,14 +146,96 @@ export class PackageService {
139146
}
140147

141148
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
142-
public async sideLoading(token: string, manifestPath: string): Promise<[string, string]> {
149+
public async sideLoading(
150+
token: string,
151+
packagePath: string,
152+
appScope = AppScope.Personal
153+
): Promise<[string, string, string]> {
154+
const manifest = this.getManifestFromZip(packagePath);
155+
if (!manifest) {
156+
throw new Error("Invalid app package zip. manifest.json is missing");
157+
}
158+
const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest);
159+
if (isDelcarativeAgentApp) {
160+
const res = await this.sideLoadingV2(token, packagePath, appScope);
161+
let shareLink = "";
162+
if (appScope == AppScope.Shared) {
163+
shareLink = await this.getShareLink(token, res[0]);
164+
}
165+
return [res[0], res[1], shareLink];
166+
} else {
167+
const res = await this.sideLoadingV1(token, packagePath);
168+
return [res[0], res[1], ""];
169+
}
170+
}
171+
// Side loading using Builder API
172+
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
173+
public async sideLoadingV2(
174+
token: string,
175+
manifestPath: string,
176+
appScope: AppScope
177+
): Promise<[string, string]> {
143178
try {
144179
this.checkZip(manifestPath);
145180
const data = await fs.readFile(manifestPath);
146181
const content = new FormData();
147182
content.append("package", data);
148183
const serviceUrl = await this.getTitleServiceUrl(token);
149-
this.logger?.verbose("Uploading package ...");
184+
this.logger?.debug("Uploading package with sideLoading V2 ...");
185+
const uploadHeaders = content.getHeaders();
186+
uploadHeaders["Authorization"] = `Bearer ${token}`;
187+
const uploadResponse = await this.axiosInstance.post(
188+
"/builder/v1/users/packages",
189+
content.getBuffer(),
190+
{
191+
baseURL: serviceUrl,
192+
headers: uploadHeaders,
193+
params: {
194+
scope: appScope,
195+
},
196+
}
197+
);
198+
199+
const statusId = uploadResponse.data.statusId;
200+
this.logger?.debug(`Acquiring package with statusId: ${statusId as string} ...`);
201+
202+
do {
203+
const statusResponse = await this.axiosInstance.get(
204+
`/builder/v1/users/packages/status/${statusId as string}`,
205+
{
206+
baseURL: serviceUrl,
207+
headers: { Authorization: `Bearer ${token}` },
208+
}
209+
);
210+
const resCode = statusResponse.status;
211+
this.logger?.debug(`Package status: ${resCode} ...`);
212+
if (resCode === 200) {
213+
const titleId: string = statusResponse.data.titleId;
214+
const appId: string = statusResponse.data.appId;
215+
this.logger?.info(`TitleId: ${titleId}`);
216+
this.logger?.info(`AppId: ${appId}`);
217+
this.logger?.verbose("Sideloading done.");
218+
return [titleId, appId];
219+
} else {
220+
await waitSeconds(2);
221+
}
222+
} while (true);
223+
} catch (error: any) {
224+
if (error.response) {
225+
error = this.traceError(error);
226+
}
227+
throw assembleError(error, M365ErrorSource);
228+
}
229+
}
230+
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
231+
public async sideLoadingV1(token: string, manifestPath: string): Promise<[string, string]> {
232+
try {
233+
this.checkZip(manifestPath);
234+
const data = await fs.readFile(manifestPath);
235+
const content = new FormData();
236+
content.append("package", data);
237+
const serviceUrl = await this.getTitleServiceUrl(token);
238+
this.logger?.debug("Uploading package with sideLoading V1 ...");
150239
const uploadHeaders = content.getHeaders();
151240
uploadHeaders["Authorization"] = `Bearer ${token}`;
152241
const uploadResponse = await this.axiosInstance.post(
@@ -211,6 +300,27 @@ export class PackageService {
211300
}
212301
}
213302
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
303+
public async getShareLink(token: string, titleId: string): Promise<string> {
304+
const serviceUrl = await this.getTitleServiceUrl(token);
305+
try {
306+
const resp = await this.axiosInstance.get(
307+
`/marketplace/v1/users/titles/${titleId}/sharingInfo`,
308+
{
309+
baseURL: serviceUrl,
310+
headers: {
311+
Authorization: `Bearer ${token}`,
312+
},
313+
}
314+
);
315+
return resp.data.unifiedStoreLink;
316+
} catch (error: any) {
317+
if (error.response) {
318+
error = this.traceError(error);
319+
}
320+
throw assembleError(error, M365ErrorSource);
321+
}
322+
}
323+
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
214324
public async getLaunchInfoByManifestId(token: string, manifestId: string): Promise<any> {
215325
try {
216326
const serviceUrl = await this.getTitleServiceUrl(token);
@@ -293,6 +403,24 @@ export class PackageService {
293403
});
294404
this.logger?.verbose("Unacquiring done.");
295405
} catch (error: any) {
406+
// try to delete in the builder API
407+
try {
408+
const serviceUrl = await this.getTitleServiceUrl(token);
409+
this.logger?.verbose(`Unacquiring package with TitleId ${titleId} in builder API...`);
410+
await this.axiosInstance.delete(`/builder/v1/users/titles/${titleId}`, {
411+
baseURL: serviceUrl,
412+
headers: {
413+
Authorization: `Bearer ${token}`,
414+
},
415+
});
416+
this.logger?.verbose("Unacquiring using builder api done.");
417+
return;
418+
} catch (subError: any) {
419+
if (subError.response) {
420+
subError = this.traceError(subError);
421+
}
422+
this.logger?.error(subError);
423+
}
296424
if (error.response) {
297425
error = this.traceError(error);
298426
}
@@ -440,4 +568,15 @@ export class PackageService {
440568
this.logger?.warning(`Please make sure input path is a valid app package zip. ${path}`);
441569
}
442570
}
571+
572+
private getManifestFromZip(path: string): TeamsAppManifest | undefined {
573+
const zip = new AdmZip(path);
574+
const manifestEntry = zip.getEntry("manifest.json");
575+
if (!manifestEntry) {
576+
return undefined;
577+
}
578+
let manifestContent = manifestEntry.getData().toString("utf8");
579+
manifestContent = stripBom(manifestContent);
580+
return JSON.parse(manifestContent) as TeamsAppManifest;
581+
}
443582
}

packages/fx-core/tests/component/driver/m365/acquire.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ describe("teamsApp/extendToM365", async () => {
143143
["appId", "MY_APP_ID"],
144144
]);
145145

146-
sinon.stub(PackageService.prototype, "sideLoading").resolves(["test-title-id", "test-app-id"]);
146+
sinon
147+
.stub(PackageService.prototype, "sideLoading")
148+
.resolves(["test-title-id", "test-app-id", ""]);
147149
sinon.stub(fs, "pathExists").resolves(true);
148150

149151
const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames);

0 commit comments

Comments
 (0)