diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 00000000..10a96b49 --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,82 @@ +name: Release Candidate +on: + push: + tags: + - 'v*-b*' + +jobs: + Linux: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: "18" + cache: "yarn" + # See https://github.com/nodejs/node-gyp/blob/main/docs/Force-npm-to-use-global-node-gyp.md + # https://github.com/nodejs/node-gyp/blob/main/docs/Updating-npm-bundled-node-gyp.md + # - name: Update node-gyp + # run: | + # npm install --global node-gyp@8.x + # npm config set node_gyp $(npm prefix -g)/lib/node_modules/node-gyp/bin/node-gyp.js + - name: Install dependencies + run: yarn install --immutable + - name: Build and Release + run: yarn release + + MacOs: + runs-on: macos-13 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + CSC_LINK: ${{ secrets.CSC_LINK }} + AC_USERNAME: ${{ secrets.AC_USERNAME }} + AC_PASSWORD: ${{ secrets.AC_PASSWORD }} + AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: "18" + cache: "yarn" + - name: Configure Node + run: | + # npm v9 doesn't allow custom config parameters (i.e. node_gyp) anymode, so we have to downgrade it to v8 + npm install -g npm@8 + npm install -g node-gyp@latest + npm config set node_gyp $(npm prefix -g)/lib/node_modules/node-gyp/bin/node-gyp.js + - name: Install dependencies + run: yarn install --immutable + - name: Build and Release + run: yarn release + + Windows: + runs-on: windows-2019 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + CSC_LINK: ${{ secrets.CSC_LINK }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: "18" + - name: Configure Node + shell: powershell + run: | + # npm v9 doesn't allow custom config parameters (i.e. node_gyp) anymode, so we have to downgrade it to v8 + npm install -g npm@8 + npm install -g node-gyp@latest + npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"} + - name: Install dependencies + run: yarn install --immutable + - name: Build and Release + run: yarn release \ No newline at end of file diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz new file mode 100644 index 00000000..79c161ae Binary files /dev/null and b/.yarn/install-state.gz differ diff --git a/package.json b/package.json index 4dd3ef08..5e643c20 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "station-project", "private": true, "productName": "Station", - "version": "3.1.0-b2", + "version": "3.1.0-b3", "description": "Station", "homepage": "https://getstation.com", "author": { diff --git a/packages/app/manifests/definitions/140.json b/packages/app/manifests/definitions/140.json index bf27da6a..7a0cd786 100644 --- a/packages/app/manifests/definitions/140.json +++ b/packages/app/manifests/definitions/140.json @@ -11,5 +11,10 @@ "theme_color": "#0070C9", "scope": "https://outlook.live.com", "bx_legacy_service_id": "outlook", + "extended_scopes": [ + "https://*.live.com", + "https://*.office.com", + "https://*.microsoft.com" + ], "recommendedPosition": "12" } diff --git a/packages/app/package.json b/packages/app/package.json index cb6949bc..6afc5454 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "station-desktop-app", "productName": "Station", - "version": "3.1.0-b2", + "version": "3.1.0-b3", "description": "Station", "homepage": "https://getstation.com", "author": { diff --git a/packages/app/src/app-worker.ts b/packages/app/src/app-worker.ts index 1b7e7b1e..73382f89 100644 --- a/packages/app/src/app-worker.ts +++ b/packages/app/src/app-worker.ts @@ -332,9 +332,9 @@ export class BrowserXAppWorker { this.mainWindowManager.on('leave-full-screen', () => this.dispatch(setFullScreenState(false))); this.mainWindowManager.on('swipe-left', () => this.dispatch(executeWebviewMethodForCurrentTab('go-back'))); this.mainWindowManager.on('swipe-right', () => this.dispatch(executeWebviewMethodForCurrentTab('go-forward'))); - this.mainWindowManager.on('new-notification', (notificationId: string, props: NotificationProps, options: NotificationOptions) => + this.mainWindowManager.on('new-notification', (notificationId: string, props: NotificationProps, options: NotificationOptions) => { this.dispatch(notificationCenter.newNotification(undefined, undefined, notificationId, props, options)) - ); + }); } private initSDK() { diff --git a/packages/app/src/applications/Application.tsx b/packages/app/src/applications/Application.tsx index 2e76674a..a798ff85 100644 --- a/packages/app/src/applications/Application.tsx +++ b/packages/app/src/applications/Application.tsx @@ -346,8 +346,11 @@ class ApplicationImpl extends React.PureComponent { } async handleDomReady() { - const js = await injectJS(this.props.legacyServiceId); + const webviewInjectJS = require(`!!raw-loader!../static/preload/webview-inject.js`).default + this.webView.view.executeJavaScript(webviewInjectJS); //`(function(){\n${bxNotifJS}\n})()`); + + const js = await injectJS(this.props.legacyServiceId); if (js && this.webView && this.webView.view) { this.webView.view.executeJavaScript(js); // const webContents = remote.webContents.fromId(this.webView.view.getWebContentsId()); diff --git a/packages/app/src/applications/manifest-provider/const/index.ts b/packages/app/src/applications/manifest-provider/const/index.ts index 75e2484c..ee39957e 100644 --- a/packages/app/src/applications/manifest-provider/const/index.ts +++ b/packages/app/src/applications/manifest-provider/const/index.ts @@ -32,12 +32,14 @@ export const JAVASCRIPT_INJECTIONS = { gmail: ['gmailInjectedScript'], slack: ['slackInjectedScript'], 'station-support': ['slackInjectedScript'], - 'gdrive-mu': ['removeGoogleAccountInjectedScript'], + 'gdrive-mu': ['removeGoogleAccountInjectedScript'], 'gcalendar-mu': ['removeGoogleAccountInjectedScript'], + 'google-cloud': ['removeGoogleAccountInjectedScript'], //vk: FIXME: doesn't work + 'google-keep': ['removeGoogleAccountInjectedScript'], + 'meet': ['removeGoogleAccountInjectedScript'], outlook: ['office365InjectedScript'], 'office-365': ['office365InjectedScript'], 'outlook-pro': ['office365InjectedScript'], - 'google-keep': ['removeGoogleAccountInjectedScript'], 'facebook-messenger': ['messengerInjectedScript'], 'whatsapp': ['whatsappInjectedScript'], }; diff --git a/packages/app/src/notification-center/sagas.ts b/packages/app/src/notification-center/sagas.ts index 8cdf7009..dccb3a02 100644 --- a/packages/app/src/notification-center/sagas.ts +++ b/packages/app/src/notification-center/sagas.ts @@ -206,6 +206,7 @@ function* interceptNotificationEventsFromWebContents({ webcontentsId, tabId }: { step: RequestForApplicationNotificationsStep.ASK, })); } + yield put(newNotification(applicationId, tabId, props.id, props)); }); diff --git a/packages/app/src/notification-center/webview-preload.ts b/packages/app/src/notification-center/webview-preload.ts index d0434b0e..625a9c43 100644 --- a/packages/app/src/notification-center/webview-preload.ts +++ b/packages/app/src/notification-center/webview-preload.ts @@ -1,4 +1,5 @@ /* tslint:disable:function-name */ +//import { ipcRenderer } from 'electron'; import * as shortid from 'shortid'; import { EventTarget } from 'event-target-shim'; diff --git a/packages/app/src/services/services/electron-google-oauth/interface.ts b/packages/app/src/services/services/electron-google-oauth/interface.ts index 079ffe16..c66e24ba 100644 --- a/packages/app/src/services/services/electron-google-oauth/interface.ts +++ b/packages/app/src/services/services/electron-google-oauth/interface.ts @@ -1,15 +1,17 @@ -import { Schema$Person } from 'googleapis/build/src/apis/plus/v1'; +import { people_v1 } from 'googleapis/build/src/apis/people/v1'; import { ServiceBase } from '../../lib/class'; import { service, timeout } from '../../lib/decorator'; import { RPC } from '../../lib/types'; -import { Credentials } from 'google-auth-library/build/src/auth/credentials'; +import { Credentials } from 'google-auth-library'; + +export type ElectronGoogleSignInResponse = { + tokens: Credentials, + profile: people_v1.Schema$Person, +} @service('electron-google-oauth') export class ElectronGoogleOAuthService extends ServiceBase implements RPC.Interface { @timeout(0) // @ts-ignore - signIn(scopes: string[], forceAddSession?: boolean): Promise<{ - tokens: Credentials, - profile: Schema$Person, - }> {} + signIn(scopes: string[], forceAddSession?: boolean): Promise {} } diff --git a/packages/app/src/services/services/electron-google-oauth/main.ts b/packages/app/src/services/services/electron-google-oauth/main.ts index bfd55b99..d7ebd655 100644 --- a/packages/app/src/services/services/electron-google-oauth/main.ts +++ b/packages/app/src/services/services/electron-google-oauth/main.ts @@ -1,31 +1,95 @@ import { google } from 'googleapis'; import { people_v1 } from 'googleapis/build/src/apis/people/v1'; +import { Credentials } from 'google-auth-library'; import ElectronGoogleOAuth2 from '@getstation/electron-google-oauth2'; +import log from 'electron-log'; + import { RPC } from '../../lib/types'; -import { ElectronGoogleOAuthService } from './interface'; +import { ElectronGoogleOAuthService, ElectronGoogleSignInResponse } from './interface'; const CLIENT_ID = process.env.GOOGLE_CLIENT_ID!; const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!; export class ElectronGoogleOAuthServiceImpl extends ElectronGoogleOAuthService implements RPC.Interface { - async signIn(scopes: string[], forceAddSession?: boolean) { + async signIn(scopes: string[], forceAddSession?: boolean): Promise { const client = new ElectronGoogleOAuth2(CLIENT_ID, CLIENT_SECRET, scopes, { successRedirectURL: 'https://getstation.com/' }); return client.openAuthWindowAndGetTokens(forceAddSession) .then(async (tokens) => { - - const service = google.people({ + try { + const service = google.people({ version: 'v1', auth: client.oauth2Client, - }); + }); - const response = await service.people.get({ - resourceName: 'people/me', - personFields: 'names,emailAddresses,photos', - sources: ['READ_SOURCE_TYPE_PROFILE'], - }); + const response = await service.people.get({ + resourceName: 'people/me', + personFields: 'names,emailAddresses,photos', + sources: ['READ_SOURCE_TYPE_PROFILE'], + }); - return { tokens, profile: response.data as people_v1.Schema$Person }; + return { tokens, profile: response.data as people_v1.Schema$Person }; + } + catch (err) { + log.error(`Google profile request error ${err}`); + return this.parseToken(tokens); + } }); } + + private parseToken(tokens: Credentials): ElectronGoogleSignInResponse { + try { + //vk: id_token format https://developers.google.com/identity/gsi/web/reference/js-reference#credential + const decodedStr = Buffer.from(tokens.id_token!.split('.')[1], 'base64').toString() + const tokenPayload = JSON.parse(decodedStr); + + return { + tokens, + profile: { + names: [ + { + metadata: { + source: { + id: tokenPayload.sub, + } + }, + displayName: tokenPayload.name, + givenName: tokenPayload.given_name, + familyName: tokenPayload.family_name, + } + ], + emailAddresses: [ + { + type: '', + value: tokenPayload.email, + } + ], + photos: [ + { + url: tokenPayload.picture, + } + ] + } + }; + } + catch (err) { + log.error(`Parse token error ${err}`); + return { + tokens, + profile: { + names: [ + { + displayName: 'unknown', + } + ], + emailAddresses: [ + { + type: '', + value: 'unknown', + } + ] + } + }; + }; + } } diff --git a/packages/app/src/services/services/os-notification/main.ts b/packages/app/src/services/services/os-notification/main.ts index bece3fa8..cf1ec2f4 100644 --- a/packages/app/src/services/services/os-notification/main.ts +++ b/packages/app/src/services/services/os-notification/main.ts @@ -1,4 +1,5 @@ import { Notification, webContents } from 'electron'; +import log from 'electron-log'; import { ServiceSubscription } from '../../lib/class'; import { RPC } from '../../lib/types'; @@ -8,6 +9,9 @@ import { getDoNotDisturb, asNativeImage } from './utils'; export class OSNotificationServiceImpl extends OSNotificationService implements RPC.Interface { async show(param: IOSNotificationServiceShowParam) { + + log.info(`>>> OSNotificationServiceImpl.show ${JSON.stringify(param)}`); + const notificationOptions: Electron.NotificationConstructorOptions = { title: param.title, actions: [], @@ -16,7 +20,6 @@ export class OSNotificationServiceImpl extends OSNotificationService implements }; if (param.imageURL) { - notificationOptions.icon = await asNativeImage(param.imageURL); } if (param.body) { @@ -25,6 +28,7 @@ export class OSNotificationServiceImpl extends OSNotificationService implements const notification = new Notification(notificationOptions); notification.show(); + return new OSNotificationImpl(notification); } diff --git a/packages/app/src/services/services/os-notification/utils.ts b/packages/app/src/services/services/os-notification/utils.ts index 594be4b2..f6396d64 100644 --- a/packages/app/src/services/services/os-notification/utils.ts +++ b/packages/app/src/services/services/os-notification/utils.ts @@ -4,12 +4,12 @@ import * as memoize from 'memoizee'; export const asNativeImage = memoize((url: string): Promise => { return new Promise((resolve, reject) => { - if (url.indexOf('data:') === 0) { + if (url.startsWith('data:')) { resolve(nativeImage.createFromDataURL(url)); return; } - if (url.indexOf('http:') === 0 || url.indexOf('https:') === 0) { + if (url.startsWith('http:') || url.startsWith('https:')) { fetch(url) .then((res: any) => res.buffer()) .then((buffer: Buffer) => { @@ -19,9 +19,16 @@ export const asNativeImage = memoize((url: string): Promise { @@ -78,7 +78,7 @@ const getUserAgentForApp = (url: string, currentUserAgent: string): string => { return defaultUserAgent; }; -const getHeaderName = (headerName: string, headers?: Record): string | undefined => { +const getHeaderName = (headerName: string, headers?: Record): string | undefined => { if (headers) { const lowCaseHeader = headerName.toLowerCase(); for (const key in headers) { @@ -90,12 +90,12 @@ const getHeaderName = (headerName: string, headers?: Record): st return undefined; } -export const getHeader = (headerName: string, headers?: Record): any => { +export const getHeader = (headerName: string, headers?: Record): any => { const realHeaderName = getHeaderName(headerName, headers); return headers && realHeaderName ? headers[realHeaderName] : undefined; } -export const setHeader = (headerName: string, headerValue: any, headers?: Record) => { +export const setHeader = (headerName: string, headerValue: any, headers?: Record): Record | undefined => { if (headers) { const realHeaderName = getHeaderName(headerName, headers); return { @@ -132,4 +132,19 @@ export const enhanceSession = (session: Session) => { }); } ); + + session.webRequest.onHeadersReceived( + (details: OnHeadersReceivedListenerDetails, callback: (headersReceivedResponse: HeadersReceivedResponse) => void) => { + const responseHeaders = details.responseHeaders; + + if (responseHeaders) { + delete responseHeaders['content-security-policy']; //vk: causes "This document requires 'TrustedHTML' assignment." error. Does not allow us to modify page CSS. + delete responseHeaders['content-security-policy-report-only']; + } + + callback({ + responseHeaders, + }) + } + ) } \ No newline at end of file diff --git a/packages/app/src/static/preload/preload.js b/packages/app/src/static/preload/preload.js index 948694a7..0963c072 100644 --- a/packages/app/src/static/preload/preload.js +++ b/packages/app/src/static/preload/preload.js @@ -133,7 +133,7 @@ require('./window-open'); })(); require('../../plugins/webview-preload'); -require('../../notification-center/webview-preload'); +//require('../../notification-center/webview-preload'); require('../../dialogs/webview-preload'); require('../../ui/webview-preload'); require('./autologin'); diff --git a/packages/app/src/static/preload/webview-inject.js b/packages/app/src/static/preload/webview-inject.js new file mode 100644 index 00000000..680c8187 --- /dev/null +++ b/packages/app/src/static/preload/webview-inject.js @@ -0,0 +1,216 @@ + +console.log('>>>>>> WebView inject start'); + +// nanoid (copy from https://github.com/ai/nanoid/blob/main/non-secure/index.js) +const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + +const nanoid = (size = 21) => { + let id = '' + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0] + } + return id +} +// nanoid + +//////////////////////////// + +const onReady = (documentObject) => { + const isReady = (x) => x === 'complete'; + return new Promise((resolve) => { + if (isReady(documentObject.readyState)) { + resolve(); + } else { + documentObject.addEventListener('readystatechange', (e) => { + if (isReady(e.target.readyState)) { + resolve(); + } + }, false); + } + }); +}; + +const recursiveOverride = (document, window, action) => { + const recursiveOverrideInt = (windowObject, documentObject) => { + action(windowObject); + onReady(document).then(() => { + const iframes = documentObject.getElementsByTagName('iframe'); + for (const iframe of iframes) { + try { + recursiveOverrideInt(iframe.contentWindow, iframe.contentDocument); + } + catch (e) { + // contentDocument can be inaccessible depending on CORS + // we just ignores it because we can't do anything about it + } + } + }) + }; + + recursiveOverrideInt(window, document); +}; + +// Notifications + +const GRANTED = 'granted'; + +const getDefaultProperties = (title) => ({ + actions: [], + badge: '', + body: '', + data: null, + dir: 'auto', + lang: '', + tag: '', + icon: '', + image: '', + requireInteraction: false, + silent: false, + timestamp: (new Date()).getTime(), + title, + vibrate: [], +}); + +class BxNotification { + constructor(title, options = {}) { + // Chrome, Safari, etc. does not throw when title is empty string + if (!title && title !== '') { + throw new Error('Title is required'); + } + this.id = `notif/${nanoid()}`; + + // default properties + const properties = Object.assign({ }, getDefaultProperties(title), options || {}); + + Object.keys(properties).forEach(key => { + Object.defineProperty(this, key, { + value: properties[key], + writable: false, + }); + }); + + this._registerIPC(); + + console.log( + '>>>>>> New notification 1', (new Date()).toLocaleTimeString(), JSON.stringify({ + id: this.id, + timestamp: this.timestamp, + title: this.title, + body: this.body, + icon: this.icon, + }) + ); + + let fixedIconUrl = this.icon; + if (typeof fixedIconUrl === 'string') { + //vk: I have no idea why but... + if (fixedIconUrl.startsWith('//')) { // Gmail + fixedIconUrl = 'https:' + this.icon; + } + } + + window.bxApi.notificationCenter.sendNotification(this.id, { + timestamp: this.timestamp, + title: this.title, + body: this.body, + icon: fixedIconUrl, + }); + } + + _registerIPC() { + window.bxApi.notificationCenter.addNotificationClickListener(this._handleNotificationClickIPC); + } + + _unregisterIPC() { + try { + console.log('unregisterIPC'); + window.bxApi.notificationCenter.removeNotificationClickListener(this._handleNotificationClickIPC); + } catch (error) { + console.log('ERROR unregisterIPC', error); + } + } + + _handleNotificationClickIPC(_e /*: Event */, notificationId /*: string */) { + if (this.id !== notificationId) { + return; + } + this.dispatchEvent(new MouseEvent('click')); + this._unregisterIPC(); + } + + close() { + console.log('close'); + try { + window.bxApi.notificationCenter.closeNotification(this.id); + } catch (error) { + console.log('ERROR close', error); + } + } + + get onclick() { + console.log('get onclick'); + return null; + } + set onclick(value) { + console.log('set onclick'); + } + + get onclose() { + console.log('get onclose'); + return null; + } + set onclose(value) { + console.log('set onclose'); + } + + get onerror() { + console.log('get onerror'); + return null; + } + set onerror(value) { + console.log('set onerror'); + } + + get onshow() { + console.log('get onshow'); + return null; + } + set onshow(value) { + console.log('set onshow'); + } + + dispatchEvent(event) { + console.log('dispatchEvent', event); + } + + addEventListener(type, listener, options) { + console.log('addEventListener', type); + } + + removeEventListener(type, listener, options) { + console.log('removeEventListener', type); + } + + requestPermission(deprecatedCallback) { + const request = Promise.resolve(GRANTED); + if (deprecatedCallback) { + request.then(deprecatedCallback); + } + return request; + } +} + +const overrideNotifications = () => { + BxNotification.permission = GRANTED; + + // window.Notification = BxNotification; + recursiveOverride(document, window, (windowObject) => { windowObject.Notification = BxNotification }); + + console.log('>>>>>> Notification override. Done'); +} + +overrideNotifications(); + diff --git a/packages/app/src/theme/api.ts b/packages/app/src/theme/api.ts index 362ce916..750a6323 100644 --- a/packages/app/src/theme/api.ts +++ b/packages/app/src/theme/api.ts @@ -133,12 +133,13 @@ export const fixSuncalc = (suncalc: SunCalc) => { // Drop invalid values if (typeof v !== 'number') return {}; switch (k) { - case 'nightEnd': - // Fix key name - return { dawn: v }; + case 'dusk': + return { night: v }; + case 'solarNoon': + return { midday: v }; case 'sunrise': case 'sunset': - case 'night': + case 'dawn': // Keep valid values return { [k]: v }; default: