diff --git a/wowup-electron/app/ipc-events.ts b/wowup-electron/app/ipc-events.ts index 9b6bbca2..294ad866 100644 --- a/wowup-electron/app/ipc-events.ts +++ b/wowup-electron/app/ipc-events.ts @@ -157,6 +157,12 @@ export function setPendingOpenUrl(...openUrls: string[]): void { export function initializeIpcHandlers(window: BrowserWindow): void { log.info("process.versions", process.versions); + // Just forward the token event out to the window + // this is not a handler, just a passive listener + ipcMain.on("wago-token-received", (evt, token) => { + window?.webContents?.send("wago-token-received", token); + }); + // Remove the pending URLs once read so they are only able to be gotten once handle(IPC_GET_PENDING_OPEN_URLS, (): string[] => { const urls = PENDING_OPEN_URLS; @@ -219,7 +225,7 @@ export function initializeIpcHandlers(window: BrowserWindow): void { if (!Array.isArray(addons)) { return; } - + for (const addon of addons) { addonStore.set(addon.id, addon); } diff --git a/wowup-electron/src/app/addon-providers/addon-provider.ts b/wowup-electron/src/app/addon-providers/addon-provider.ts index 7e6a6b66..6e4ecc23 100644 --- a/wowup-electron/src/app/addon-providers/addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/addon-provider.ts @@ -3,7 +3,7 @@ import { WowInstallation } from "../../common/warcraft/wow-installation"; import { Observable, of } from "rxjs"; import { Addon } from "../../common/entities/addon"; -import { AddonCategory, AddonChannelType } from "../../common/wowup/models"; +import { AddonCategory, AddonChannelType, AdPageOptions } from "../../common/wowup/models"; import { AddonFolder } from "../models/wowup/addon-folder"; import { AddonSearchResult } from "../models/wowup/addon-search-result"; import { ProtocolSearchResult } from "../models/wowup/protocol-search-result"; @@ -44,6 +44,8 @@ export abstract class AddonProvider { public allowViewAtSource = true; public canShowChangelog = true; public canBatchFetch = false; + public authRequired = false; + public adRequired = false; public getAllBatch(installations: WowInstallation[], addonIds: string[]): Promise { return Promise.resolve({ @@ -119,4 +121,8 @@ export abstract class AddonProvider { public async getDescription(installation: WowInstallation, externalId: string, addon?: Addon): Promise { return Promise.resolve(""); } + + public getAdPageParams(): AdPageOptions | undefined { + return undefined; + } } diff --git a/wowup-electron/src/app/addon-providers/wago-addon-provider.ts b/wowup-electron/src/app/addon-providers/wago-addon-provider.ts index d530502d..fc298b0d 100644 --- a/wowup-electron/src/app/addon-providers/wago-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/wago-addon-provider.ts @@ -6,13 +6,18 @@ import { CachingService } from "../services/caching/caching-service"; import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service"; import { AddonProvider } from "./addon-provider"; import { WowInstallation } from "../../common/warcraft/wow-installation"; -import { AddonChannelType } from "../../common/wowup/models"; +import { AddonChannelType, AdPageOptions } from "../../common/wowup/models"; import { AddonFolder } from "../models/wowup/addon-folder"; import { WowClientGroup, WowClientType } from "../../common/warcraft/wow-client-type"; import { AppWowUpScanResult } from "../models/wowup/app-wowup-scan-result"; import { TocService } from "../services/toc/toc.service"; +import { AddonSearchResult } from "../models/wowup/addon-search-result"; +import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file"; +import { Addon } from "../../common/entities/addon"; +import { convertBbcode } from "../utils/bbcode.utils"; declare type WagoGameVersion = "retail" | "classic" | "bcc"; +declare type WagoStability = "stable" | "beta" | "alpha"; interface WagoFingerprintAddon { hash: string; // hash fingerprint of the folder @@ -26,12 +31,76 @@ interface WagoFingerprintRequest { addons: { [folder: string]: WagoFingerprintAddon }; } +interface WagoSearchResponse { + data: WagoSearchResponseItem[]; +} + +interface WagoSearchResponseItem { + display_name: string; + id: string; + releases: { + alpha?: WagoSearchResponseRelease; + beta?: WagoSearchResponseRelease; + stable?: WagoSearchResponseRelease; + }; + summary: string; + thumbnail_image: string; +} + +interface WagoSearchResponseRelease { + download_link: string; + label: string; + created_at: string; + logical_timestamp: number; // download link expiration time +} + +interface WagoAddon { + id: string; + slug: string; + display_name: string; + thumbnail_image: string; + summary: string; + description: string; + website: string; + gallery: string[]; + recent_releases: { + stable?: WagoRelease; + beta?: WagoRelease; + alpha?: WagoRelease; + }; +} + +interface WagoRelease { + label: string; + supported_retail_patch: string; + supported_classic_patch: string; + supported_bc_patch: string; + changelog: string; + stability: WagoStability; + download_link: string; +} + +const WAGO_BASE_URL = "https://addons.wago.io/api/external"; +const WAGO_AD_URL = "https://addons.wago.io/wowup_ad"; +const WAGO_AD_REFERRER = "https://wago.io"; +const WAGO_AD_USER_AGENT = + "`Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36`"; // the ad requires a normal looking user agent +const WAGO_AD_PRELOAD = "preload/wago.js"; +const WAGO_SEARCH_CACHE_TIME_SEC = 20; +const WAGO_DETAILS_CACHE_TIME_SEC = 20; + export class WagoAddonProvider extends AddonProvider { private readonly _circuitBreaker: CircuitBreakerWrapper; + private _apiToken = ""; + private _enabled = true; + public readonly name = ADDON_PROVIDER_WAGO; public readonly forceIgnore = false; public enabled = true; + public authRequired = true; + public adRequired = true; + public allowEdit = true; public constructor( private _electronService: ElectronService, @@ -47,6 +116,8 @@ export class WagoAddonProvider extends AddonProvider { AppConfig.defaultHttpResetTimeoutMs, AppConfig.wagoHttpTimeoutMs ); + + this._electronService.on("wago-token-received", this.onWagoTokenReceived); } public async scan( @@ -84,6 +155,121 @@ export class WagoAddonProvider extends AddonProvider { console.debug(JSON.stringify(request, null, 2)); } + public async searchByQuery( + query: string, + installation: WowInstallation, + channelType?: AddonChannelType + ): Promise { + if (!this._apiToken) { + console.warn("[wago] searchByQuery no api token found"); + return []; + } + + const url = new URL(`${WAGO_BASE_URL}/addons/_search`); + url.searchParams.set("query", query); + url.searchParams.set("game_version", this.getGameVersion(installation.clientType)); + url.searchParams.set("stability", this.getStability(channelType)); + + const response = await this._cachingService.transaction( + url.toString(), + () => this._circuitBreaker.getJson(url, this.getRequestHeaders()), + WAGO_SEARCH_CACHE_TIME_SEC + ); + + const searchResults = response.data?.map((item) => this.toSearchResult(item)) ?? []; + + console.debug(`[wago] searchByQuery`, response, searchResults); + + return searchResults; + } + + public async getDescription(installation: WowInstallation, externalId: string, addon?: Addon): Promise { + try { + const response = await this.getAddonById(externalId); + return convertBbcode(response?.description ?? ""); + } catch (e) { + console.error(`[wago] failed to get description`, e); + return ""; + } + } + + public getAdPageParams(): AdPageOptions { + return { + pageUrl: WAGO_AD_URL, + referrer: WAGO_AD_REFERRER, + userAgent: WAGO_AD_USER_AGENT, + preloadFilePath: WAGO_AD_PRELOAD, + }; + } + + private async getAddonById(addonId: string) { + //https://addons.wago.io/api/external/addons/qv63o6bQ?game_version=retail&__debug__=true + const url = new URL(`${WAGO_BASE_URL}/addons/${addonId}`); + return await this._cachingService.transaction( + url.toString(), + () => this._circuitBreaker.getJson(url, this.getRequestHeaders()), + WAGO_DETAILS_CACHE_TIME_SEC + ); + } + + private toSearchResult(item: WagoSearchResponseItem): AddonSearchResult { + const releaseTypes = Object.keys(item.releases); + const searchResultFiles: AddonSearchResultFile[] = []; + for (const type of releaseTypes) { + searchResultFiles.push(this.toSearchResultFile(item.releases[type], type as WagoStability)); + } + + return { + author: "", + externalId: item.id, + externalUrl: "", + name: item.display_name, + providerName: this.name, + thumbnailUrl: item.thumbnail_image, + downloadCount: 0, + files: searchResultFiles, + releasedAt: new Date(), + summary: item.summary, + }; + } + + private toSearchResultFile(release: WagoSearchResponseRelease, stability: WagoStability): AddonSearchResultFile { + return { + channelType: this.getAddonChannelType(stability), + downloadUrl: release.download_link, + folders: [], + gameVersion: "", + releaseDate: new Date(release.created_at), + version: release.label, + dependencies: [], + }; + } + + private getAddonChannelType(stability: WagoStability): AddonChannelType { + switch (stability) { + case "alpha": + return AddonChannelType.Alpha; + case "beta": + return AddonChannelType.Beta; + case "stable": + default: + return AddonChannelType.Stable; + } + } + + // Get the wago friendly name for our addon channel + private getStability(channelType: AddonChannelType): WagoStability { + switch (channelType) { + case AddonChannelType.Alpha: + return "alpha"; + case AddonChannelType.Beta: + return "beta"; + case AddonChannelType.Stable: + default: + return "stable"; + } + } + // The wago name for the client type private getGameVersion(clientType: WowClientType): WagoGameVersion { const clientGroup = this._warcraftService.getClientGroup(clientType); @@ -105,4 +291,17 @@ export class WagoAddonProvider extends AddonProvider { const scanResults: AppWowUpScanResult[] = await this._electronService.invoke("wowup-get-scan-results", filePaths); return scanResults; }; + + private onWagoTokenReceived = (evt, token) => { + console.debug(`[wago] onWagoTokenReceived`, token); + this._apiToken = token; + }; + + private getRequestHeaders(): { + [header: string]: string | string[]; + } { + return { + Authorization: `Bearer ${this._apiToken}`, + }; + } } diff --git a/wowup-electron/src/app/app.component.html b/wowup-electron/src/app/app.component.html index 8034cb3b..da8c3088 100644 --- a/wowup-electron/src/app/app.component.html +++ b/wowup-electron/src/app/app.component.html @@ -8,7 +8,7 @@
-
+
@@ -22,7 +22,7 @@
- +
diff --git a/wowup-electron/src/app/app.component.ts b/wowup-electron/src/app/app.component.ts index fbb46eaa..805c7cfd 100644 --- a/wowup-electron/src/app/app.component.ts +++ b/wowup-electron/src/app/app.component.ts @@ -35,7 +35,7 @@ import { ZOOM_FACTOR_KEY, } from "../common/constants"; import { Addon } from "../common/entities/addon"; -import { AppUpdateState, MenuConfig, SystemTrayConfig } from "../common/wowup/models"; +import { AdPageOptions, AppUpdateState, MenuConfig, SystemTrayConfig } from "../common/wowup/models"; import { AppConfig } from "../environments/environment"; import { InstallFromUrlDialogComponent } from "./components/addons/install-from-url-dialog/install-from-url-dialog.component"; import { AlertDialogComponent } from "./components/common/alert-dialog/alert-dialog.component"; @@ -83,6 +83,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { public quitEnabled?: boolean; public showPreLoad = true; + public adPageParams: AdPageOptions[] = []; public constructor( private _addonService: AddonService, @@ -135,6 +136,17 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { }); } }); + + this.sessionService.adSpace$.subscribe((enabled) => { + if (enabled) { + const providers = this._addonService.getAdRequiredProviders(); + this.adPageParams = providers + .map((provider) => provider.getAdPageParams()) + .filter((param) => param !== undefined); + } else { + this.adPageParams = []; + } + }); } public ngOnInit(): void { @@ -215,9 +227,6 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } public ngAfterViewInit(): void { - // TODO this is driven by provider - this.sessionService.enableAdSpace(true); - from(this.createAppMenu()) .pipe( first(), diff --git a/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html b/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html index d5d7077f..037c4332 100644 --- a/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html +++ b/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html @@ -1,9 +1,12 @@
-
-
+ [cdkCopyToClipboard]="fullExternalId" + (click)="onClickExternalId()" + > +
{{ thumbnailLetter }}
@@ -12,30 +15,34 @@

{{ title }}

-

{{ 'DIALOGS.ADDON_DETAILS.BY_AUTHOR' | translate:{ authorName: subtitle } }}

+

{{ "DIALOGS.ADDON_DETAILS.BY_AUTHOR" | translate: { authorName: subtitle } }}

{{ version }}

- +
-
-
+
-

{{'DIALOGS.ADDON_DETAILS.MISSING_DEPENDENCIES' | translate}}

+

{{ "DIALOGS.ADDON_DETAILS.MISSING_DEPENDENCIES" | translate }}

    -
  • {{dependency}}
  • +
  • {{ dependency }}
@@ -46,28 +53,36 @@
- + -
-
+
- {{'DIALOGS.ADDON_DETAILS.NO_CHANGELOG_TEXT' | - translate}}
+ {{ "DIALOGS.ADDON_DETAILS.NO_CHANGELOG_TEXT" | translate }} +
-
- +
+
@@ -76,17 +91,27 @@
- +
- {{ - "DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX" | translate }} + {{ "DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX" | translate }} {{ provider }} - +
-
\ No newline at end of file + diff --git a/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts b/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts index eedc11d4..926da680 100644 --- a/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts +++ b/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts @@ -20,7 +20,6 @@ export class OptionsAddonSectionComponent implements OnInit { public ngOnInit(): void { this.addonProviderStates = filter(this._addonService.getAddonProviderStates(), (provider) => provider.canEdit); this.enabledAddonProviders.setValue(this.getEnabledProviderNames()); - console.debug("addonProviderStates", this.addonProviderStates); } public onProviderStateSelectionChange(event: MatSelectionListChange): void { diff --git a/wowup-electron/src/app/directives/webview.component.ts b/wowup-electron/src/app/directives/webview.component.ts index aac555c5..1e56f36c 100644 --- a/wowup-electron/src/app/directives/webview.component.ts +++ b/wowup-electron/src/app/directives/webview.component.ts @@ -1,13 +1,13 @@ -import { Directive, ElementRef, OnInit } from "@angular/core"; +import { Directive, ElementRef, Input, OnDestroy, OnInit } from "@angular/core"; import { nanoid } from "nanoid"; +import { AdPageOptions } from "../../common/wowup/models"; import { FileService } from "../services/files/file.service"; @Directive({ selector: "app-webview", }) -export class WebviewComponent implements OnInit { - // TODO add input URL - // TODO add input config object +export class WebviewComponent implements OnInit, OnDestroy { + @Input("options") options: AdPageOptions; private _tag: Electron.WebviewTag; private _id: string = nanoid(); @@ -21,24 +21,37 @@ export class WebviewComponent implements OnInit { this.initWebview(this._element).catch((e) => console.error(e)); } + ngOnDestroy(): void { + // Clean up the webview element + if (this._tag) { + if (this._tag.isDevToolsOpened()) { + this._tag.closeDevTools(); + } + this._tag = undefined; + } + + this._element.nativeElement.innerHTML = 0; + } + private async initWebview(element: ElementRef) { - // ad container requires a 'normal' UA - const userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36`; - const preloadPath = await this._fileService.getAssetFilePath("preload/wago.js"); - console.log("preloadPath", preloadPath); + const pageReferrer = this.options.referrer ? `httpreferrer="${this.options.referrer}"` : ""; + const userAgent = this.options.userAgent ? `useragent="${this.options.userAgent}"` : ""; + const preload = this.options.preloadFilePath + ? `preload="${await this._fileService.getAssetFilePath(this.options.preloadFilePath)}"` + : ""; const placeholder = document.createElement("div"); placeholder.innerHTML = ` + ${preload} + ${userAgent}> `; this._tag = placeholder.firstElementChild as Electron.WebviewTag; @@ -50,6 +63,6 @@ export class WebviewComponent implements OnInit { private onWebviewReady = () => { console.debug("onWebviewReady", this._tag); this._tag.removeEventListener("dom-ready", this.onWebviewReady); - this._tag.openDevTools(); + // this._tag.openDevTools(); }; } diff --git a/wowup-electron/src/app/services/addons/addon.service.ts b/wowup-electron/src/app/services/addons/addon.service.ts index bc0ba246..d5547f18 100644 --- a/wowup-electron/src/app/services/addons/addon.service.ts +++ b/wowup-electron/src/app/services/addons/addon.service.ts @@ -100,6 +100,7 @@ export class AddonService { private readonly _searchErrorSrc = new Subject(); private readonly _installQueue = new Subject(); private readonly _anyUpdatesAvailableSrc = new BehaviorSubject(false); + private readonly _addonProviderChangeSrc = new Subject(); private _activeInstalls: AddonUpdateEvent[] = []; private _subscriptions: Subscription[] = []; @@ -112,6 +113,7 @@ export class AddonService { public readonly scanError$ = this._scanErrorSrc.asObservable(); public readonly searchError$ = this._searchErrorSrc.asObservable(); public readonly anyUpdatesAvailable$ = this._anyUpdatesAvailableSrc.asObservable(); + public readonly addonProviderChange$ = this._addonProviderChangeSrc.asObservable(); public constructor( private _addonStorage: AddonStorageService, @@ -126,6 +128,7 @@ export class AddonService { ) { // Create our base set of addon providers this._addonProviders = addonProviderFactory.getProviders(); + console.debug("_addonProviders", this._addonProviders); // This should keep the current update queue state snapshot up to date const addonInstalledSub = this.addonInstalled$ @@ -1831,11 +1834,14 @@ export class AddonService { return !!this.getByExternalId(externalId, providerName, installation.id); } + // TODO move this to a different service public setProviderEnabled(providerName: string, enabled: boolean): void { const provider = this.getProvider(providerName); if (provider) { provider.enabled = enabled; } + + this._addonProviderChangeSrc.next(provider); } private getProvider(providerName: string): AddonProvider | undefined { @@ -2018,6 +2024,10 @@ export class AddonService { return _.filter(this._addonProviders, (provider) => provider.enabled); } + public getAdRequiredProviders(): AddonProvider[] { + return this.getEnabledAddonProviders().filter((provider) => provider.adRequired); + } + private trackInstallAction(installType: InstallType, addon: Addon) { this._analyticsService.trackAction(USER_ACTION_ADDON_INSTALL, { clientType: getEnumName(WowClientType, addon.clientType), diff --git a/wowup-electron/src/app/services/session/session.service.ts b/wowup-electron/src/app/services/session/session.service.ts index 00bc606a..a95103f0 100644 --- a/wowup-electron/src/app/services/session/session.service.ts +++ b/wowup-electron/src/app/services/session/session.service.ts @@ -10,6 +10,7 @@ import { WarcraftInstallationService } from "../warcraft/warcraft-installation.s import { ColumnState } from "../../models/wowup/column-state"; import { map } from "rxjs/operators"; import { WowUpAccountService } from "../wowup/wowup-account.service"; +import { AddonService } from "../addons/addon.service"; @Injectable({ providedIn: "root", @@ -55,7 +56,8 @@ export class SessionService { public constructor( private _warcraftInstallationService: WarcraftInstallationService, private _preferenceStorageService: PreferenceStorageService, - private _wowUpAccountService: WowUpAccountService + private _wowUpAccountService: WowUpAccountService, + private _addonService: AddonService ) { this._selectedDetailTabType = this._preferenceStorageService.getObject(SELECTED_DETAILS_TAB_KEY) || "description"; @@ -63,6 +65,17 @@ export class SessionService { this._warcraftInstallationService.wowInstallations$.subscribe((installations) => this.onWowInstallationsChange(installations) ); + + this._addonService.addonProviderChange$.subscribe((provider) => { + this.updateAdSpace(); + }); + + this.updateAdSpace(); + } + + private updateAdSpace() { + const allProviders = this._addonService.getEnabledAddonProviders(); + this._adSpaceSrc.next(allProviders.findIndex((p) => p.adRequired) !== -1); } public get wowUpAuthToken(): string { @@ -93,10 +106,6 @@ export class SessionService { this._addonsChangedSrc.next(true); } - public enableAdSpace(enabled: boolean): void { - this._adSpaceSrc.next(enabled); - } - public getSelectedDetailsTab(): DetailsTabType { return this._selectedDetailTabType; } diff --git a/wowup-electron/src/common/wowup.d.ts b/wowup-electron/src/common/wowup.d.ts index 95e8fc5a..ababe012 100644 --- a/wowup-electron/src/common/wowup.d.ts +++ b/wowup-electron/src/common/wowup.d.ts @@ -88,7 +88,8 @@ declare type RendererChannels = | "base64-encode" | "base64-decode" | "set-release-channel" - | "zip-list-files"; + | "zip-list-files" + | "wago-token-received"; declare global { interface Window { diff --git a/wowup-electron/src/common/wowup/models.ts b/wowup-electron/src/common/wowup/models.ts index 091bb8ee..6ca45248 100644 --- a/wowup-electron/src/common/wowup/models.ts +++ b/wowup-electron/src/common/wowup/models.ts @@ -131,3 +131,11 @@ export interface AddonUpdatePushNotification extends PushNotificationData { addonName: string; addonId: string; } + +export interface AdPageOptions { + pageUrl: string; + referrer?: string; + userAgent?: string; + preloadFilePath?: string; + explanationKey?: string; // locale key of the translated explanation of this ad +}