This commit is contained in:
19
react/features/google-api/actionTypes.ts
Normal file
19
react/features/google-api/actionTypes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* The type of Redux action which changes Google API state.
|
||||
*
|
||||
* {
|
||||
* type: SET_GOOGLE_API_STATE
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_GOOGLE_API_STATE = 'SET_GOOGLE_API_STATE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which changes Google API profile state.
|
||||
*
|
||||
* {
|
||||
* type: SET_GOOGLE_API_PROFILE
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_GOOGLE_API_PROFILE = 'SET_GOOGLE_API_PROFILE';
|
||||
226
react/features/google-api/actions.ts
Normal file
226
react/features/google-api/actions.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { getShareInfoText } from '../invite/functions';
|
||||
import { getLiveStreaming } from '../recording/components/LiveStream/functions';
|
||||
|
||||
import {
|
||||
SET_GOOGLE_API_PROFILE,
|
||||
SET_GOOGLE_API_STATE
|
||||
} from './actionTypes';
|
||||
import { GOOGLE_API_STATES } from './constants';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import googleApi from './googleApi';
|
||||
|
||||
/**
|
||||
* Retrieves the current calendar events.
|
||||
*
|
||||
* @param {number} fetchStartDays - The number of days to go back when fetching.
|
||||
* @param {number} fetchEndDays - The number of days to fetch.
|
||||
* @returns {function(Dispatch<any>): Promise<CalendarEntries>}
|
||||
*/
|
||||
export function getCalendarEntries(
|
||||
fetchStartDays?: number, fetchEndDays?: number) {
|
||||
return () =>
|
||||
googleApi.get()
|
||||
.then(() =>
|
||||
googleApi._getCalendarEntries(fetchStartDays, fetchEndDays));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Google API.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function loadGoogleAPI() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) =>
|
||||
googleApi.get()
|
||||
.then(() => {
|
||||
const {
|
||||
enableCalendarIntegration,
|
||||
googleApiApplicationClientID
|
||||
} = getState()['features/base/config'];
|
||||
|
||||
const liveStreaming = getLiveStreaming(getState());
|
||||
|
||||
if (getState()['features/google-api'].googleAPIState
|
||||
=== GOOGLE_API_STATES.NEEDS_LOADING) {
|
||||
return googleApi.initializeClient(
|
||||
googleApiApplicationClientID, liveStreaming.enabled, enableCalendarIntegration);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED)))
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => googleApi.isSignedIn())
|
||||
.then((isSignedIn: boolean) => {
|
||||
if (isSignedIn) {
|
||||
dispatch(setGoogleAPIState(GOOGLE_API_STATES.SIGNED_IN));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a request for a list of all YouTube broadcasts associated with
|
||||
* user currently signed in to the Google API Client Library.
|
||||
*
|
||||
* @returns {function(): (Promise<*>|Promise<any[] | never>)}
|
||||
*/
|
||||
export function requestAvailableYouTubeBroadcasts() {
|
||||
return () =>
|
||||
googleApi.requestAvailableYouTubeBroadcasts()
|
||||
.then((response: any) => {
|
||||
// Takes in a list of broadcasts from the YouTube API,
|
||||
// removes dupes, removes broadcasts that cannot get a stream key,
|
||||
// and parses the broadcasts into flat objects.
|
||||
const broadcasts = response.result.items;
|
||||
|
||||
const parsedBroadcasts: any = {};
|
||||
|
||||
for (let i = 0; i < broadcasts.length; i++) {
|
||||
const broadcast = broadcasts[i];
|
||||
const boundStreamID = broadcast.contentDetails.boundStreamId;
|
||||
|
||||
if (boundStreamID && !parsedBroadcasts[boundStreamID]) {
|
||||
parsedBroadcasts[boundStreamID] = {
|
||||
boundStreamID,
|
||||
id: broadcast.id,
|
||||
status: broadcast.status.lifeCycleStatus,
|
||||
title: broadcast.snippet.title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(parsedBroadcasts);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the stream key for a YouTube broadcast and updates the internal
|
||||
* state to display the associated stream key as being entered.
|
||||
*
|
||||
* @param {string} boundStreamID - The bound stream ID associated with the
|
||||
* broadcast from which to get the stream key.
|
||||
* @returns {function(): (Promise<*>|Promise<{
|
||||
* streamKey: (*|string),
|
||||
* selectedBoundStreamID: *} | never>)}
|
||||
*/
|
||||
export function requestLiveStreamsForYouTubeBroadcast(boundStreamID: string) {
|
||||
return () =>
|
||||
googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
|
||||
.then((response: any) => {
|
||||
const broadcasts = response.result.items;
|
||||
const streamName = broadcasts?.[0]?.cdn.ingestionInfo.streamName;
|
||||
const streamKey = streamName || '';
|
||||
|
||||
return {
|
||||
streamKey,
|
||||
selectedBoundStreamID: boundStreamID
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current Google API state.
|
||||
*
|
||||
* @param {number} googleAPIState - The state to be set.
|
||||
* @param {Object} googleResponse - The last response from Google.
|
||||
* @returns {{
|
||||
* type: SET_GOOGLE_API_STATE,
|
||||
* googleAPIState: number
|
||||
* }}
|
||||
*/
|
||||
export function setGoogleAPIState(
|
||||
googleAPIState: number, googleResponse?: Object) {
|
||||
return {
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState,
|
||||
googleResponse
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the Google web client application to prompt for a sign in, such as
|
||||
* when changing account, and will then fetch available YouTube broadcasts.
|
||||
*
|
||||
* @returns {function(): (Promise<*>|Promise<{
|
||||
* streamKey: (*|string),
|
||||
* selectedBoundStreamID: *} | never>)}
|
||||
*/
|
||||
export function showAccountSelection() {
|
||||
return () => googleApi.showAccountSelection(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library.
|
||||
*
|
||||
* @returns {function(Dispatch<any>): Promise<string | never>}
|
||||
*/
|
||||
export function signIn() {
|
||||
return (dispatch: IStore['dispatch']) => googleApi.get()
|
||||
.then(() => googleApi.signInIfNotSignedIn(true))
|
||||
.then(() => dispatch({
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the user.
|
||||
*
|
||||
* @returns {function(Dispatch<any>): Promise<string | never>}
|
||||
*/
|
||||
export function signOut() {
|
||||
return (dispatch: IStore['dispatch']) =>
|
||||
googleApi.get()
|
||||
.then(() => googleApi.signOut())
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState: GOOGLE_API_STATES.LOADED
|
||||
});
|
||||
dispatch({
|
||||
type: SET_GOOGLE_API_PROFILE,
|
||||
profileEmail: ''
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the profile data that is currently used.
|
||||
*
|
||||
* @returns {function(Dispatch<any>): Promise<string | never>}
|
||||
*/
|
||||
export function updateProfile() {
|
||||
return (dispatch: IStore['dispatch']) => googleApi.get()
|
||||
.then(() => googleApi.signInIfNotSignedIn())
|
||||
.then(() => dispatch({
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
|
||||
}))
|
||||
.then(() => googleApi.getCurrentUserProfile())
|
||||
.then((profile: any) => {
|
||||
dispatch({
|
||||
type: SET_GOOGLE_API_PROFILE,
|
||||
profileEmail: profile.email
|
||||
});
|
||||
|
||||
return profile.email;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the calendar event and adds a location and text.
|
||||
*
|
||||
* @param {string} id - The event id to update.
|
||||
* @param {string} calendarId - The calendar id to use.
|
||||
* @param {string} location - The location to add to the event.
|
||||
* @returns {function(Dispatch<any>): Promise<string | never>}
|
||||
*/
|
||||
export function updateCalendarEvent(
|
||||
id: string, calendarId: string, location: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) =>
|
||||
getShareInfoText(getState(), location)
|
||||
.then(text =>
|
||||
googleApi._updateCalendarEntry(id, calendarId, location, text));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { GestureResponderEvent, Image, ImageStyle, TouchableOpacity, ViewStyle } from 'react-native';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Button from '../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
// eslint-disable-next-line
|
||||
const GOOGLE_BRAND_IMAGE = require('../../../../images/btn_google_signin_dark_normal.png');
|
||||
|
||||
/**
|
||||
* The Google Brand image for Sign In.
|
||||
*
|
||||
* NOTE: iOS doesn't handle the react-native-google-signin button component
|
||||
* well due to our CocoaPods build process (the lib is not intended to be used
|
||||
* this way), hence the custom button implementation.
|
||||
*/
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The callback to invoke when the button is clicked.
|
||||
*/
|
||||
onClick: (e?: React.MouseEvent<HTMLButtonElement> | GestureResponderEvent) => void;
|
||||
|
||||
/**
|
||||
* True if the user is signed in, so it needs to render a different label
|
||||
* and maybe different style (for the future).
|
||||
*/
|
||||
signedIn?: boolean;
|
||||
|
||||
/**
|
||||
* The text to display within {@code GoogleSignInButton}.
|
||||
*/
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component showing a button to sign in with Google.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class GoogleSignInButton extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { onClick, signedIn } = this.props;
|
||||
|
||||
if (signedIn) {
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = 'liveStreaming.signOut'
|
||||
labelKey = 'liveStreaming.signOut'
|
||||
onClick = { onClick }
|
||||
style = { styles.signOutButton }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress = { onClick }
|
||||
style = { styles.signInButton as ViewStyle } >
|
||||
<Image
|
||||
resizeMode = { 'contain' }
|
||||
source = { GOOGLE_BRAND_IMAGE }
|
||||
style = { styles.signInImage as ImageStyle } />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(GoogleSignInButton);
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The callback to invoke when the button is clicked.
|
||||
*/
|
||||
onClick: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* True if the user is signed in, so it needs to render a different label
|
||||
* and maybe different style (for the future).
|
||||
*/
|
||||
signedIn?: boolean;
|
||||
|
||||
/**
|
||||
* The text to display within {@code GoogleSignInButton}.
|
||||
*/
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React Component showing a button to sign in with Google.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class GoogleSignInButton extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'google-sign-in'
|
||||
onClick = { this.props.onClick }>
|
||||
<img
|
||||
alt = { t('welcomepage.logo.googleLogo') }
|
||||
className = 'google-logo'
|
||||
src = 'images/googleLogo.svg' />
|
||||
<div className = 'google-cta'>
|
||||
{
|
||||
t(this.props.signedIn
|
||||
? 'liveStreaming.signOut'
|
||||
: 'liveStreaming.signIn')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(GoogleSignInButton);
|
||||
39
react/features/google-api/components/styles.ts
Normal file
39
react/features/google-api/components/styles.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createStyleSheet } from '../../base/styles/functions.any';
|
||||
|
||||
/**
|
||||
* For styling explanations, see:
|
||||
* https://developers.google.com/identity/branding-guidelines.
|
||||
*/
|
||||
const BUTTON_HEIGHT = 40;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of google-api.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Image of the sign in button (Google branded).
|
||||
*/
|
||||
signInImage: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* An image-based button for sign in.
|
||||
*/
|
||||
signInButton: {
|
||||
alignItems: 'center',
|
||||
height: BUTTON_HEIGHT,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* A text-based button for sign out (no sign out button guidance for
|
||||
* Google).
|
||||
*/
|
||||
signOutButton: {
|
||||
alignSelf: 'center',
|
||||
maxWidth: 120,
|
||||
width: 'auto'
|
||||
}
|
||||
});
|
||||
76
react/features/google-api/constants.ts
Normal file
76
react/features/google-api/constants.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Google API URL to retrieve streams for a live broadcast of a user.
|
||||
*
|
||||
* NOTE: The URL must be appended by a broadcast ID returned by a call towards
|
||||
* {@code API_URL_LIVE_BROADCASTS}.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
export const API_URL_BROADCAST_STREAMS = 'https://content.googleapis.com/youtube/v3/liveStreams?part=id%2Csnippet%2Ccdn%2Cstatus&id=';
|
||||
|
||||
/**
|
||||
* Google API URL to retrieve live broadcasts of a user.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v3/liveBroadcasts?broadcastType=all&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus';
|
||||
|
||||
/**
|
||||
* Array of API discovery doc URLs for APIs used by the googleApi.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const DISCOVERY_DOCS = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest';
|
||||
|
||||
/**
|
||||
* An enumeration of the different states the Google API can be in.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
export const GOOGLE_API_STATES = {
|
||||
/**
|
||||
* The state in which the Google API still needs to be loaded.
|
||||
*/
|
||||
NEEDS_LOADING: 0,
|
||||
|
||||
/**
|
||||
* The state in which the Google API is loaded and ready for use.
|
||||
*/
|
||||
LOADED: 1,
|
||||
|
||||
/**
|
||||
* The state in which a user has been logged in through the Google API.
|
||||
*/
|
||||
SIGNED_IN: 2,
|
||||
|
||||
/**
|
||||
* The state in which the Google authentication is not available (e.g. Play
|
||||
* services are not installed on Android).
|
||||
*/
|
||||
NOT_AVAILABLE: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Google API auth scope to access Google calendar.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar';
|
||||
|
||||
/**
|
||||
* Google API auth scope to access user email.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const GOOGLE_SCOPE_USERINFO = 'https://www.googleapis.com/auth/userinfo.email';
|
||||
|
||||
/**
|
||||
* Google API auth scope to access YouTube streams.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const GOOGLE_SCOPE_YOUTUBE
|
||||
= 'https://www.googleapis.com/auth/youtube.readonly';
|
||||
191
react/features/google-api/googleApi.native.js
Normal file
191
react/features/google-api/googleApi.native.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
let GoogleSignin;
|
||||
|
||||
if (NativeModules.RNGoogleSignin) {
|
||||
GoogleSignin = require('@react-native-google-signin/google-signin').GoogleSignin;
|
||||
}
|
||||
|
||||
import {
|
||||
API_URL_BROADCAST_STREAMS,
|
||||
API_URL_LIVE_BROADCASTS
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Class to encapsulate Google API functionalities and provide a similar
|
||||
* interface to what WEB has. The methods are different, but the point is that
|
||||
* the export object is similar so no need for different export logic.
|
||||
*
|
||||
* For more detailed documentation of the {@code GoogleSignin} API, please visit
|
||||
* https://github.com/@react-native-google-signin/google-signin.
|
||||
*/
|
||||
class GoogleApi {
|
||||
/**
|
||||
* Wraps the {@code GoogleSignin.configure} method.
|
||||
*
|
||||
* @param {Object} config - The config object to be passed to
|
||||
* {@code GoogleSignin.configure}.
|
||||
* @returns {void}
|
||||
*/
|
||||
configure(config) {
|
||||
if (GoogleSignin) {
|
||||
GoogleSignin.configure(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current tokens.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getTokens() {
|
||||
return GoogleSignin.getTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the available YouTube streams the user can use for live
|
||||
* streaming.
|
||||
*
|
||||
* @param {string} accessToken - The Google auth token.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getYouTubeLiveStreams(accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Fetching the list of available broadcasts first.
|
||||
this._fetchGoogleEndpoint(accessToken,
|
||||
API_URL_LIVE_BROADCASTS)
|
||||
.then(broadcasts => {
|
||||
// Then fetching all the available live streams that the
|
||||
// user has access to with the broadcasts we retrieved
|
||||
// earlier.
|
||||
this._getLiveStreamsForBroadcasts(
|
||||
accessToken, broadcasts).then(resolve, reject);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the {@code GoogleSignin.hasPlayServices} method.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
hasPlayServices() {
|
||||
if (!GoogleSignin) {
|
||||
return Promise.reject(new Error('Google SignIn not supported'));
|
||||
}
|
||||
|
||||
return GoogleSignin.hasPlayServices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the {@code GoogleSignin.signIn} method.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
signIn() {
|
||||
return GoogleSignin.signIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the {@code GoogleSignin.signInSilently} method.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
signInSilently() {
|
||||
return GoogleSignin.signInSilently();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the {@code GoogleSignin.signOut} method.
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
signOut() {
|
||||
return GoogleSignin.signOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to fetch a Google API endpoint in a generic way.
|
||||
*
|
||||
* @private
|
||||
* @param {string} accessToken - The access token used for the API call.
|
||||
* @param {string} endpoint - The endpoint to fetch, including the URL
|
||||
* params if needed.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchGoogleEndpoint(accessToken, endpoint) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
};
|
||||
|
||||
fetch(endpoint, {
|
||||
headers
|
||||
}).then(response => response.json())
|
||||
.then(responseJSON => {
|
||||
if (responseJSON.error) {
|
||||
reject(responseJSON.error.message);
|
||||
} else {
|
||||
resolve(responseJSON.items || []);
|
||||
}
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the available YouTube streams that are available for the
|
||||
* provided broadcast IDs.
|
||||
*
|
||||
* @private
|
||||
* @param {string} accessToken - The Google access token.
|
||||
* @param {Array<Object>} broadcasts - The list of broadcasts that we want
|
||||
* to retrieve streams for.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getLiveStreamsForBroadcasts(accessToken, broadcasts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ids = [];
|
||||
|
||||
for (const broadcast of broadcasts) {
|
||||
broadcast.contentDetails
|
||||
&& broadcast.contentDetails.boundStreamId
|
||||
&& ids.push(broadcast.contentDetails.boundStreamId);
|
||||
}
|
||||
|
||||
this._fetchGoogleEndpoint(
|
||||
accessToken,
|
||||
`${API_URL_BROADCAST_STREAMS}${ids.join(',')}`)
|
||||
.then(streams => {
|
||||
const keys = [];
|
||||
|
||||
// We construct an array of keys bind with the broadcast
|
||||
// name for a nice display.
|
||||
for (const stream of streams) {
|
||||
const key = stream.cdn.ingestionInfo.streamName;
|
||||
let title;
|
||||
|
||||
// Finding title from the broadcast with the same
|
||||
// boundStreamId. If not found (unknown scenario), we
|
||||
// use the key as title again.
|
||||
for (const broadcast of broadcasts) {
|
||||
if (broadcast.contentDetails
|
||||
&& broadcast.contentDetails.boundStreamId
|
||||
=== stream.id) {
|
||||
title = broadcast.snippet.title;
|
||||
}
|
||||
}
|
||||
|
||||
keys.push({
|
||||
key,
|
||||
title: title || key
|
||||
});
|
||||
}
|
||||
|
||||
resolve(keys);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new GoogleApi();
|
||||
461
react/features/google-api/googleApi.web.js
Normal file
461
react/features/google-api/googleApi.web.js
Normal file
@@ -0,0 +1,461 @@
|
||||
import {
|
||||
API_URL_BROADCAST_STREAMS,
|
||||
API_URL_LIVE_BROADCASTS,
|
||||
DISCOVERY_DOCS,
|
||||
GOOGLE_SCOPE_CALENDAR,
|
||||
GOOGLE_SCOPE_USERINFO,
|
||||
GOOGLE_SCOPE_YOUTUBE
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
|
||||
const GOOGLE_GIS_LIBRARY_URL = 'https://accounts.google.com/gsi/client';
|
||||
|
||||
/**
|
||||
* A promise for dynamically loading the Google API Client Library.
|
||||
*
|
||||
* @private
|
||||
* @type {Promise}
|
||||
*/
|
||||
let googleClientLoadPromise;
|
||||
|
||||
/**
|
||||
* A singleton for loading and interacting with the Google API.
|
||||
*/
|
||||
const googleApi = {
|
||||
/**
|
||||
* Obtains Google API Client Library, loading the library dynamically if
|
||||
* needed.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get() {
|
||||
const globalGoogleApi = this._getGoogleApiClient();
|
||||
|
||||
if (!globalGoogleApi) {
|
||||
return this.load();
|
||||
}
|
||||
|
||||
return Promise.resolve(globalGoogleApi);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the profile for the user signed in to the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCurrentUserProfile() {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._getGoogleApiClient()
|
||||
.client.oauth2
|
||||
.userinfo.get().getPromise()
|
||||
.then(r => r.result);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the Google Web Client ID used for authenticating with Google and
|
||||
* making Google API requests.
|
||||
*
|
||||
* @param {string} clientId - The client ID to be used with the API library.
|
||||
* @param {boolean} enableYoutube - Whether youtube scope is enabled.
|
||||
* @param {boolean} enableCalendar - Whether calendar scope is enabled.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
initializeClient(clientId, enableYoutube, enableCalendar) {
|
||||
return this.get()
|
||||
.then(api => new Promise((resolve, reject) => {
|
||||
// setTimeout is used as a workaround for api.client.init not
|
||||
// resolving consistently when the Google API Client Library is
|
||||
// loaded asynchronously. See:
|
||||
// github.com/google/google-api-javascript-client/issues/399
|
||||
setTimeout(() => {
|
||||
api.client.init({})
|
||||
.then(() => {
|
||||
if (enableCalendar) {
|
||||
api.client.load(DISCOVERY_DOCS);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
api.client.load('https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest');
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, 500);
|
||||
}))
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scope
|
||||
= `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}`
|
||||
.trim();
|
||||
|
||||
this.tokenClient = this._getGoogleGISApiClient().accounts.oauth2.initTokenClient({
|
||||
// eslint-disable-next-line camelcase
|
||||
client_id: clientId,
|
||||
scope: `${scope} ${GOOGLE_SCOPE_USERINFO}`,
|
||||
callback: '' // defined at request time in await/promise scope.
|
||||
});
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a user is currently authenticated with Google through an
|
||||
* initialized Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
isSignedIn() {
|
||||
return new Promise((resolve, _) => {
|
||||
const te = parseInt(this.tokenExpires, 10);
|
||||
const isExpired = isNaN(this.tokenExpires) ? true : new Date().getTime() > te;
|
||||
|
||||
resolve(Boolean(!isExpired));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a script tag.
|
||||
*
|
||||
* @param {string} src - The source for the script tag.
|
||||
* @returns {Promise<unknown>}
|
||||
* @private
|
||||
*/
|
||||
_loadScriptTag(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
||||
scriptTag.async = true;
|
||||
scriptTag.addEventListener('error', () => {
|
||||
scriptTag.remove();
|
||||
|
||||
reject();
|
||||
});
|
||||
scriptTag.addEventListener('load', resolve);
|
||||
scriptTag.type = 'text/javascript';
|
||||
|
||||
scriptTag.src = src;
|
||||
|
||||
document.head.appendChild(scriptTag);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a script tag and downloads the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load() {
|
||||
if (googleClientLoadPromise) {
|
||||
return googleClientLoadPromise;
|
||||
}
|
||||
|
||||
googleClientLoadPromise = this._loadScriptTag(GOOGLE_API_CLIENT_LIBRARY_URL)
|
||||
.catch(() => {
|
||||
googleClientLoadPromise = null;
|
||||
})
|
||||
.then(() => new Promise((resolve, reject) =>
|
||||
this._getGoogleApiClient().load('client', {
|
||||
callback: resolve,
|
||||
onerror: reject
|
||||
})))
|
||||
.then(this._loadScriptTag(GOOGLE_GIS_LIBRARY_URL))
|
||||
.catch(() => {
|
||||
googleClientLoadPromise = null;
|
||||
})
|
||||
.then(() => this._getGoogleApiClient());
|
||||
|
||||
return googleClientLoadPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a request for a list of all YouTube broadcasts associated with
|
||||
* user currently signed in to the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestAvailableYouTubeBroadcasts() {
|
||||
return this.get()
|
||||
.then(api => api.client.request(API_URL_LIVE_BROADCASTS));
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a request to get all live streams associated with a broadcast
|
||||
* in YouTube.
|
||||
*
|
||||
* @param {string} boundStreamID - The bound stream ID associated with a
|
||||
* broadcast in YouTube.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestLiveStreamsForYouTubeBroadcast(boundStreamID) {
|
||||
return this.get()
|
||||
.then(api => api.client.request(
|
||||
`${API_URL_BROADCAST_STREAMS}${boundStreamID}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library, even
|
||||
* if already signed in.
|
||||
*
|
||||
* @param {boolean} consent - Whether to show account selection dialog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
showAccountSelection(consent) {
|
||||
return this.get()
|
||||
.then(api => new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Settle this promise in the response callback for requestAccessToken()
|
||||
this.tokenClient.callback = resp => {
|
||||
if (resp.error !== undefined) {
|
||||
reject(resp);
|
||||
}
|
||||
|
||||
// Get the number of seconds the token is valid for, subtract 5 minutes
|
||||
// to account for differences in clock settings and convert to ms.
|
||||
const expiresIn = (parseInt(api.client.getToken().expires_in, 10) - 300) * 1000;
|
||||
const now = new Date();
|
||||
const expireDate = new Date(now.getTime() + expiresIn);
|
||||
|
||||
this.tokenExpires = expireDate.getTime().toString();
|
||||
|
||||
resolve(resp);
|
||||
};
|
||||
|
||||
this.tokenClient.requestAccessToken({ prompt: consent ? 'consent' : '' });
|
||||
} catch (err) {
|
||||
logger.error('Error requesting token', err);
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the participant to sign in to the Google API Client Library, if
|
||||
* not already signed in.
|
||||
*
|
||||
* @param {boolean} consent - Whether to show account selection dialog.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
signInIfNotSignedIn(consent) {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return this.showAccountSelection(consent);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign out from the Google API Client Library.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
signOut() {
|
||||
return this.get()
|
||||
.then(() => {
|
||||
this.tokenClient = undefined;
|
||||
this.tokenExpires = undefined;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the google calendar entries to a known format.
|
||||
*
|
||||
* @param {Object} entry - The google calendar entry.
|
||||
* @returns {{
|
||||
* calendarId: string,
|
||||
* description: string,
|
||||
* endDate: string,
|
||||
* id: string,
|
||||
* location: string,
|
||||
* startDate: string,
|
||||
* title: string}}
|
||||
* @private
|
||||
*/
|
||||
_convertCalendarEntry(entry) {
|
||||
return {
|
||||
calendarId: entry.calendarId,
|
||||
description: entry.description,
|
||||
endDate: entry.end.dateTime,
|
||||
id: entry.id,
|
||||
location: entry.location,
|
||||
startDate: entry.start.dateTime,
|
||||
title: entry.summary,
|
||||
url: this._getConferenceDataVideoUri(entry.conferenceData)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks conference data for jitsi conference solution and returns
|
||||
* its video url.
|
||||
*
|
||||
* @param {Object} conferenceData - The conference data of the event.
|
||||
* @returns {string|undefined} Returns the found video uri or undefined.
|
||||
*/
|
||||
_getConferenceDataVideoUri(conferenceData = {}) {
|
||||
try {
|
||||
// check conference data coming from calendar addons
|
||||
if (conferenceData.parameters.addOnParameters.parameters
|
||||
.conferenceSolutionType === 'jitsi') {
|
||||
const videoEntry = conferenceData.entryPoints.find(
|
||||
e => e.entryPointType === 'video');
|
||||
|
||||
if (videoEntry) {
|
||||
return videoEntry.uri;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// we don't care about undefined fields
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves calendar entries from all available calendars.
|
||||
*
|
||||
* @param {number} fetchStartDays - The number of days to go back
|
||||
* when fetching.
|
||||
* @param {number} fetchEndDays - The number of days to fetch.
|
||||
* @returns {Promise<CalendarEntry>}
|
||||
* @private
|
||||
*/
|
||||
_getCalendarEntries(fetchStartDays, fetchEndDays) {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// user can edit the events, so we want only those that
|
||||
// can be edited
|
||||
return this._getGoogleApiClient()
|
||||
.client.calendar.calendarList.list();
|
||||
})
|
||||
.then(calendarList => {
|
||||
|
||||
// no result, maybe not signed in
|
||||
if (!calendarList) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const calendarIds
|
||||
= calendarList.result.items.map(en => {
|
||||
return {
|
||||
id: en.id,
|
||||
accessRole: en.accessRole
|
||||
};
|
||||
});
|
||||
const promises = calendarIds.map(({ id, accessRole }) => {
|
||||
const startDate = new Date();
|
||||
const endDate = new Date();
|
||||
|
||||
startDate.setDate(startDate.getDate() + fetchStartDays);
|
||||
endDate.setDate(endDate.getDate() + fetchEndDays);
|
||||
|
||||
// retrieve the events and adds to the result the calendarId
|
||||
return this._getGoogleApiClient()
|
||||
.client.calendar.events.list({
|
||||
'calendarId': id,
|
||||
'timeMin': startDate.toISOString(),
|
||||
'timeMax': endDate.toISOString(),
|
||||
'showDeleted': false,
|
||||
'singleEvents': true,
|
||||
'orderBy': 'startTime'
|
||||
})
|
||||
.then(result => result.result.items
|
||||
.map(item => {
|
||||
const resultItem = { ...item };
|
||||
|
||||
// add the calendarId only for the events
|
||||
// we can edit
|
||||
if (accessRole === 'writer'
|
||||
|| accessRole === 'owner') {
|
||||
resultItem.calendarId = id;
|
||||
}
|
||||
|
||||
return resultItem;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(results => [].concat(...results))
|
||||
.then(entries =>
|
||||
entries.map(e => this._convertCalendarEntry(e)));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the calendar event and adds a location and text.
|
||||
*
|
||||
* @param {string} id - The event id to update.
|
||||
* @param {string} calendarId - The calendar id to use.
|
||||
* @param {string} location - The location to add to the event.
|
||||
* @param {string} text - The description text to set/append.
|
||||
* @returns {Promise<T | never>}
|
||||
* @private
|
||||
*/
|
||||
_updateCalendarEntry(id, calendarId, location, text) {
|
||||
return this.get()
|
||||
.then(() => this.isSignedIn())
|
||||
.then(isSignedIn => {
|
||||
if (!isSignedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._getGoogleApiClient()
|
||||
.client.calendar.events.get({
|
||||
'calendarId': calendarId,
|
||||
'eventId': id
|
||||
}).then(event => {
|
||||
let newDescription = text;
|
||||
|
||||
if (event.result.description) {
|
||||
newDescription = `${event.result.description}\n\n${
|
||||
text}`;
|
||||
}
|
||||
|
||||
return this._getGoogleApiClient()
|
||||
.client.calendar.events.patch({
|
||||
'calendarId': calendarId,
|
||||
'eventId': id,
|
||||
'description': newDescription,
|
||||
'location': location
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the global Google API Client Library object. Direct use of this
|
||||
* method is discouraged; instead use the {@link get} method.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
_getGoogleApiClient() {
|
||||
return window.gapi;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns the global Google Identity Services Library object.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
_getGoogleGISApiClient() {
|
||||
return window.google;
|
||||
}
|
||||
};
|
||||
|
||||
export default googleApi;
|
||||
3
react/features/google-api/logger.ts
Normal file
3
react/features/google-api/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/base/redux');
|
||||
45
react/features/google-api/reducer.ts
Normal file
45
react/features/google-api/reducer.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_GOOGLE_API_PROFILE,
|
||||
SET_GOOGLE_API_STATE
|
||||
} from './actionTypes';
|
||||
import { GOOGLE_API_STATES } from './constants';
|
||||
|
||||
/**
|
||||
* The default state is the Google API needs loading.
|
||||
*
|
||||
* @type {{googleAPIState: number}}
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
|
||||
profileEmail: ''
|
||||
};
|
||||
|
||||
export interface IGoogleApiState {
|
||||
googleAPIState: number;
|
||||
googleResponse?: Object;
|
||||
profileEmail: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/google-api.
|
||||
*/
|
||||
ReducerRegistry.register<IGoogleApiState>('features/google-api',
|
||||
(state = DEFAULT_STATE, action): IGoogleApiState => {
|
||||
switch (action.type) {
|
||||
case SET_GOOGLE_API_STATE:
|
||||
return {
|
||||
...state,
|
||||
googleAPIState: action.googleAPIState,
|
||||
googleResponse: action.googleResponse
|
||||
};
|
||||
case SET_GOOGLE_API_PROFILE:
|
||||
return {
|
||||
...state,
|
||||
profileEmail: action.profileEmail
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user