aboutsummaryrefslogtreecommitdiff
path: root/src/server/apis/google
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/apis/google')
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts98
-rw-r--r--src/server/apis/google/GooglePhotosServerUtils.ts68
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts28
-rw-r--r--src/server/apis/google/typings/albums.ts150
4 files changed, 320 insertions, 24 deletions
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 8785cd974..2c9085ebb 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,10 +1,12 @@
-import { google, docs_v1, slides_v1 } from "googleapis";
+import { google } from "googleapis";
import { createInterface } from "readline";
import { readFile, writeFile } from "fs";
-import { OAuth2Client } from "google-auth-library";
+import { OAuth2Client, Credentials } from "google-auth-library";
import { Opt } from "../../../new_fields/Doc";
import { GlobalOptions } from "googleapis-common";
import { GaxiosResponse } from "gaxios";
+import request = require('request-promise');
+import * as qs from 'query-string';
/**
* Server side authentication for Google Api queries.
@@ -20,6 +22,8 @@ export namespace GoogleApiServerUtils {
'presentations.readonly',
'drive',
'drive.file',
+ 'photoslibrary',
+ 'photoslibrary.sharing'
];
export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
@@ -29,7 +33,6 @@ export namespace GoogleApiServerUtils {
Slides = "Slides"
}
-
export interface CredentialPaths {
credentials: string;
token: string;
@@ -44,51 +47,98 @@ export namespace GoogleApiServerUtils {
export type EndpointParameters = GlobalOptions & { version: "v1" };
export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
- return new Promise<Opt<Endpoint>>((resolve, reject) => {
- readFile(paths.credentials, (err, credentials) => {
+ return new Promise<Opt<Endpoint>>(resolve => {
+ RetrieveCredentials(paths).then(authentication => {
+ let routed: Opt<Endpoint>;
+ let parameters: EndpointParameters = { auth: authentication.client, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
+ });
+ });
+ };
+
+ export const RetrieveCredentials = async (paths: CredentialPaths) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ readFile(paths.credentials, async (err, credentials) => {
if (err) {
reject(err);
return console.log('Error loading client secret file:', err);
}
- return authorize(parseBuffer(credentials), paths.token).then(auth => {
- let routed: Opt<Endpoint>;
- let parameters: EndpointParameters = { auth, version: "v1" };
- switch (sector) {
- case Service.Documents:
- routed = google.docs(parameters).documents;
- break;
- case Service.Slides:
- routed = google.slides(parameters).presentations;
- break;
- }
- resolve(routed);
- });
+ authorize(parseBuffer(credentials), paths.token).then(resolve, reject);
});
});
};
+ export const RetrieveAccessToken = async (paths: CredentialPaths) => {
+ return new Promise<string>((resolve, reject) => {
+ RetrieveCredentials(paths).then(
+ credentials => resolve(credentials.token.access_token!),
+ error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
+ );
+ });
+ };
+ type TokenResult = { token: Credentials, client: OAuth2Client };
/**
* Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
* @param {Object} credentials The authorization client credentials.
*/
- export function authorize(credentials: any, token_path: string): Promise<OAuth2Client> {
+ export function authorize(credentials: any, token_path: string): Promise<TokenResult> {
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
- return new Promise<OAuth2Client>((resolve, reject) => {
+ return new Promise<TokenResult>((resolve, reject) => {
readFile(token_path, (err, token) => {
// Check if we have previously stored a token.
if (err) {
return getNewToken(oAuth2Client, token_path).then(resolve, reject);
}
- oAuth2Client.setCredentials(parseBuffer(token));
- resolve(oAuth2Client);
+ let parsed: Credentials = parseBuffer(token);
+ if (parsed.expiry_date! < new Date().getTime()) {
+ return refreshToken(parsed, client_id, client_secret, oAuth2Client, token_path).then(resolve, reject);
+ }
+ oAuth2Client.setCredentials(parsed);
+ resolve({ token: parsed, client: oAuth2Client });
});
});
}
+ const refreshEndpoint = "https://oauth2.googleapis.com/token";
+ const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, token_path: string) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+ let queryParameters = {
+ refreshToken: credentials.refresh_token,
+ client_id,
+ client_secret,
+ grant_type: "refresh_token"
+ };
+ let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`;
+ request.post(url, headerParameters).then(response => {
+ let parsed = JSON.parse(response);
+ credentials.access_token = parsed.access_token;
+ credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000;
+ writeFile(token_path, JSON.stringify(credentials), (err) => {
+ if (err) {
+ console.error(err);
+ reject(err);
+ }
+ console.log('Refreshed token stored to', token_path);
+ oAuth2Client.setCredentials(credentials);
+ resolve({ token: credentials, client: oAuth2Client });
+ });
+ });
+ });
+ };
+
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
@@ -96,7 +146,7 @@ export namespace GoogleApiServerUtils {
* @param {getEventsCallback} callback The callback for the authorized client.
*/
function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
- return new Promise<OAuth2Client>((resolve, reject) => {
+ return new Promise<TokenResult>((resolve, reject) => {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES.map(relative => prefix + relative),
@@ -122,7 +172,7 @@ export namespace GoogleApiServerUtils {
}
console.log('Token stored to', token_path);
});
- resolve(oAuth2Client);
+ resolve({ token, client: oAuth2Client });
});
});
});
diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts
new file mode 100644
index 000000000..cb5464abc
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosServerUtils.ts
@@ -0,0 +1,68 @@
+import request = require('request-promise');
+import { Album } from './typings/albums';
+import * as qs from 'query-string';
+
+const apiEndpoint = "https://photoslibrary.googleapis.com/v1/";
+
+export interface Authorization {
+ token: string;
+}
+
+export namespace GooglePhotos {
+
+ export type Query = Album.Query;
+ export type QueryParameters = { query: GooglePhotos.Query };
+ interface DispatchParameters {
+ required: boolean;
+ method: "GET" | "POST";
+ ignore?: boolean;
+ }
+
+ export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise<any> => {
+ let action = parameters.query.action;
+ let dispatch = SuffixMap.get(action)!;
+ let suffix = Suffix(parameters, dispatch, action);
+ if (suffix) {
+ let query: any = parameters.query;
+ let options: any = {
+ headers: { 'Content-Type': 'application/json' },
+ auth: { 'bearer': parameters.token },
+ };
+ if (query.body) {
+ options.body = query.body;
+ options.json = true;
+ }
+ let queryParameters = query.parameters;
+ if (queryParameters) {
+ suffix += `?${qs.stringify(queryParameters)}`;
+ }
+ let dispatcher = dispatch.method === "POST" ? request.post : request.get;
+ return dispatcher(apiEndpoint + suffix, options);
+ }
+ };
+
+ const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => {
+ let query: any = parameters.query;
+ let id = query.albumId;
+ let suffix = 'albums';
+ if (dispatch.required) {
+ if (!id) {
+ return undefined;
+ }
+ suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`;
+ }
+ return suffix;
+ };
+
+ const SuffixMap = new Map<Album.Action, DispatchParameters>([
+ [Album.Action.AddEnrichment, { required: true, method: "POST" }],
+ [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }],
+ [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }],
+ [Album.Action.Create, { required: false, method: "POST" }],
+ [Album.Action.Get, { required: true, ignore: true, method: "GET" }],
+ [Album.Action.List, { required: false, method: "GET" }],
+ [Album.Action.Share, { required: true, method: "POST" }],
+ [Album.Action.Unshare, { required: true, method: "POST" }]
+ ]);
+
+}
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
new file mode 100644
index 000000000..b358f9698
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -0,0 +1,28 @@
+import request = require('request-promise');
+import { Authorization } from './GooglePhotosServerUtils';
+
+export namespace GooglePhotosUploadUtils {
+
+ interface UploadInformation {
+ title: string;
+ MEDIA_BINARY_DATA: string;
+ }
+
+ const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads";
+
+ export const SubmitUpload = async (parameters: Authorization & UploadInformation) => {
+ let options = {
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ auth: { 'bearer': parameters.token },
+ 'X-Goog-Upload-File-Name': parameters.title,
+ 'X-Goog-Upload-Protocol': 'raw'
+ },
+ body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA },
+ json: true
+ };
+ const result = await request.post(apiEndpoint, options);
+ return result;
+ };
+
+} \ No newline at end of file
diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts
new file mode 100644
index 000000000..f3025567d
--- /dev/null
+++ b/src/server/apis/google/typings/albums.ts
@@ -0,0 +1,150 @@
+export namespace Album {
+
+ export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare);
+
+ export enum Action {
+ AddEnrichment = "addEnrichment",
+ BatchAddMediaItems = "batchAddMediaItems",
+ BatchRemoveMediaItems = "batchRemoveMediaItems",
+ Create = "create",
+ Get = "get",
+ List = "list",
+ Share = "share",
+ Unshare = "unshare"
+ }
+
+ export interface AddEnrichment {
+ action: Action.AddEnrichment;
+ albumId: string;
+ body: {
+ newEnrichmentItem: NewEnrichmentItem;
+ albumPosition: MediaRelativeAlbumPosition;
+ };
+ }
+
+ export interface BatchAddMediaItems {
+ action: Action.BatchAddMediaItems;
+ albumId: string;
+ body: {
+ mediaItemIds: string[];
+ };
+ }
+
+ export interface BatchRemoveMediaItems {
+ action: Action.BatchRemoveMediaItems;
+ albumId: string;
+ body: {
+ mediaItemIds: string[];
+ };
+ }
+
+ export interface Create {
+ action: Action.Create;
+ body: {
+ album: Template;
+ };
+ }
+
+ export interface Get {
+ action: Action.Get;
+ albumId: string;
+ }
+
+ export interface List {
+ action: Action.List;
+ parameters: ListOptions;
+ }
+
+ export interface ListOptions {
+ pageSize: number;
+ pageToken: string;
+ excludeNonAppCreatedData: boolean;
+ }
+
+ export interface Share {
+ action: Action.Share;
+ albumId: string;
+ body: {
+ sharedAlbumOptions: SharedOptions;
+ };
+ }
+
+ export interface Unshare {
+ action: Action.Unshare;
+ albumId: string;
+ }
+
+ export interface Template {
+ title: string;
+ }
+
+ export interface Model {
+ id: string;
+ title: string;
+ productUrl: string;
+ isWriteable: boolean;
+ shareInfo: ShareInfo;
+ mediaItemsCount: string;
+ coverPhotoBaseUrl: string;
+ coverPhotoMediaItemId: string;
+ }
+
+ export interface ShareInfo {
+ sharedAlbumOptions: SharedOptions;
+ shareableUrl: string;
+ shareToken: string;
+ isJoined: boolean;
+ isOwned: boolean;
+ }
+
+ export interface SharedOptions {
+ isCollaborative: boolean;
+ isCommentable: boolean;
+ }
+
+ export enum PositionType {
+ POSITION_TYPE_UNSPECIFIED,
+ FIRST_IN_ALBUM,
+ LAST_IN_ALBUM,
+ AFTER_MEDIA_ITEM,
+ AFTER_ENRICHMENT_ITEM
+ }
+
+ export type Position = GeneralAlbumPosition | MediaRelativeAlbumPosition | EnrichmentRelativeAlbumPosition;
+
+ interface GeneralAlbumPosition {
+ position: PositionType.FIRST_IN_ALBUM | PositionType.LAST_IN_ALBUM | PositionType.POSITION_TYPE_UNSPECIFIED;
+ }
+
+ interface MediaRelativeAlbumPosition {
+ position: PositionType.AFTER_MEDIA_ITEM;
+ relativeMediaItemId: string;
+ }
+
+ interface EnrichmentRelativeAlbumPosition {
+ position: PositionType.AFTER_ENRICHMENT_ITEM;
+ relativeEnrichmentItemId: string;
+ }
+
+ export interface Location {
+ locationName: string;
+ latlng: {
+ latitude: number,
+ longitude: number
+ };
+ }
+
+ export interface NewEnrichmentItem {
+ textEnrichment: {
+ text: string;
+ };
+ locationEnrichment: {
+ location: Location
+ };
+ mapEnrichment: {
+ origin: { location: Location },
+ destination: { location: Location }
+ };
+ }
+
+} \ No newline at end of file