2024-10-16 08:18:53 -07:00
|
|
|
import {
|
|
|
|
NodeApiError,
|
|
|
|
NodeOperationError,
|
|
|
|
type DeclarativeRestApiSettings,
|
|
|
|
type IDataObject,
|
|
|
|
type IExecutePaginationFunctions,
|
|
|
|
type IExecuteSingleFunctions,
|
|
|
|
type IHttpRequestMethods,
|
|
|
|
type IHttpRequestOptions,
|
|
|
|
type ILoadOptionsFunctions,
|
|
|
|
type IN8nHttpFullResponse,
|
|
|
|
type INodeExecutionData,
|
|
|
|
type INodeListSearchItems,
|
|
|
|
type INodeListSearchResult,
|
|
|
|
type IPollFunctions,
|
|
|
|
type JsonObject,
|
|
|
|
} from 'n8n-workflow';
|
|
|
|
|
|
|
|
import type { ITimeInterval } from './Interfaces';
|
|
|
|
|
|
|
|
const addOptName = 'additionalOptions';
|
|
|
|
const possibleRootProperties = ['localPosts', 'reviews'];
|
|
|
|
|
|
|
|
const getAllParams = (execFns: IExecuteSingleFunctions): Record<string, unknown> => {
|
|
|
|
const params = execFns.getNode().parameters;
|
|
|
|
const additionalOptions = execFns.getNodeParameter(addOptName, {}) as Record<string, unknown>;
|
|
|
|
|
|
|
|
// Merge standard parameters with additional options from the node parameters
|
|
|
|
return { ...params, ...additionalOptions };
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Helper function to adjust date-time parameters for API requests */
|
|
|
|
export async function handleDatesPresend(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
opts: IHttpRequestOptions,
|
|
|
|
): Promise<IHttpRequestOptions> {
|
|
|
|
const params = getAllParams(this);
|
|
|
|
const body = Object.assign({}, opts.body) as IDataObject;
|
|
|
|
const event = (body.event as IDataObject) ?? ({} as IDataObject);
|
|
|
|
|
|
|
|
if (!params.startDateTime && !params.startDate && !params.endDateTime && !params.endDate) {
|
|
|
|
return opts;
|
|
|
|
}
|
|
|
|
|
|
|
|
const createDateTimeObject = (dateString: string) => {
|
|
|
|
const date = new Date(dateString);
|
|
|
|
return {
|
|
|
|
date: {
|
|
|
|
year: date.getUTCFullYear(),
|
|
|
|
month: date.getUTCMonth() + 1,
|
|
|
|
day: date.getUTCDate(),
|
|
|
|
},
|
|
|
|
time: dateString.includes('T')
|
|
|
|
? {
|
|
|
|
hours: date.getUTCHours(),
|
|
|
|
minutes: date.getUTCMinutes(),
|
|
|
|
seconds: date.getUTCSeconds(),
|
|
|
|
nanos: 0,
|
|
|
|
}
|
|
|
|
: undefined,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
// Convert start and end date-time parameters if provided
|
|
|
|
const startDateTime =
|
|
|
|
params.startDateTime || params.startDate
|
|
|
|
? createDateTimeObject((params.startDateTime || params.startDate) as string)
|
|
|
|
: null;
|
|
|
|
const endDateTime =
|
|
|
|
params.endDateTime || params.endDate
|
|
|
|
? createDateTimeObject((params.endDateTime || params.endDate) as string)
|
|
|
|
: null;
|
|
|
|
|
|
|
|
const schedule: Partial<ITimeInterval> = {
|
|
|
|
startDate: startDateTime?.date,
|
|
|
|
endDate: endDateTime?.date,
|
|
|
|
startTime: startDateTime?.time,
|
|
|
|
endTime: endDateTime?.time,
|
|
|
|
};
|
|
|
|
|
|
|
|
event.schedule = schedule;
|
|
|
|
Object.assign(body, { event });
|
|
|
|
opts.body = body;
|
|
|
|
return opts;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Helper function adding update mask to the request */
|
|
|
|
export async function addUpdateMaskPresend(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
opts: IHttpRequestOptions,
|
|
|
|
): Promise<IHttpRequestOptions> {
|
|
|
|
const additionalOptions = this.getNodeParameter('additionalOptions') as IDataObject;
|
|
|
|
const propertyMapping: { [key: string]: string } = {
|
|
|
|
postType: 'topicType',
|
|
|
|
actionType: 'actionType',
|
|
|
|
callToActionType: 'callToAction.actionType',
|
|
|
|
url: 'callToAction.url',
|
|
|
|
startDateTime: 'event.schedule.startDate,event.schedule.startTime',
|
|
|
|
endDateTime: 'event.schedule.endDate,event.schedule.endTime',
|
|
|
|
title: 'event.title',
|
|
|
|
startDate: 'event.schedule.startDate',
|
|
|
|
endDate: 'event.schedule.endDate',
|
|
|
|
couponCode: 'offer.couponCode',
|
|
|
|
redeemOnlineUrl: 'offer.redeemOnlineUrl',
|
|
|
|
termsAndConditions: 'offer.termsAndConditions',
|
|
|
|
};
|
|
|
|
|
|
|
|
if (Object.keys(additionalOptions).length) {
|
|
|
|
const updateMask = Object.keys(additionalOptions)
|
|
|
|
.map((key) => propertyMapping[key] || key)
|
|
|
|
.join(',');
|
|
|
|
opts.qs = {
|
|
|
|
...opts.qs,
|
|
|
|
updateMask,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return opts;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Helper function to handle pagination */
|
|
|
|
export async function handlePagination(
|
|
|
|
this: IExecutePaginationFunctions,
|
|
|
|
resultOptions: DeclarativeRestApiSettings.ResultOptions,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
const aggregatedResult: IDataObject[] = [];
|
|
|
|
let nextPageToken: string | undefined;
|
|
|
|
const returnAll = this.getNodeParameter('returnAll') as boolean;
|
|
|
|
let limit = 100;
|
|
|
|
if (!returnAll) {
|
|
|
|
limit = this.getNodeParameter('limit') as number;
|
|
|
|
resultOptions.maxResults = limit;
|
|
|
|
}
|
|
|
|
resultOptions.paginate = true;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if (nextPageToken) {
|
|
|
|
resultOptions.options.qs = { ...resultOptions.options.qs, pageToken: nextPageToken };
|
|
|
|
}
|
|
|
|
|
|
|
|
const responseData = await this.makeRoutingRequest(resultOptions);
|
|
|
|
|
|
|
|
for (const page of responseData) {
|
|
|
|
for (const prop of possibleRootProperties) {
|
|
|
|
if (page.json[prop]) {
|
|
|
|
const currentData = page.json[prop] as IDataObject[];
|
|
|
|
aggregatedResult.push(...currentData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!returnAll && aggregatedResult.length >= limit) {
|
|
|
|
return aggregatedResult.slice(0, limit).map((item) => ({ json: item }));
|
|
|
|
}
|
|
|
|
|
|
|
|
nextPageToken = page.json.nextPageToken as string | undefined;
|
|
|
|
}
|
|
|
|
} while (nextPageToken);
|
|
|
|
|
|
|
|
return aggregatedResult.map((item) => ({ json: item }));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Helper functions to handle errors */
|
|
|
|
|
|
|
|
export async function handleErrorsDeletePost(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const post = this.getNodeParameter('post', undefined) as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (post && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The post you are deleting could not be found. Adjust the "post" parameter setting to delete the post correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function handleErrorsGetPost(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const post = this.getNodeParameter('post', undefined) as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (post && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The post you are requesting could not be found. Adjust the "post" parameter setting to retrieve the post correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function handleErrorsUpdatePost(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const post = this.getNodeParameter('post') as IDataObject;
|
|
|
|
const additionalOptions = this.getNodeParameter('additionalOptions') as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (post && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The post you are updating could not be found. Adjust the "post" parameter setting to update the post correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do not throw an error if the user didn't set additional options (a hint will be shown)
|
|
|
|
if (response.statusCode === 400 && Object.keys(additionalOptions).length === 0) {
|
|
|
|
return [{ json: { success: true } }];
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function handleErrorsDeleteReply(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const review = this.getNodeParameter('review', undefined) as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (review && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The review you are deleting could not be found. Adjust the "review" parameter setting to update the review correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function handleErrorsGetReview(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const review = this.getNodeParameter('review', undefined) as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (review && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The review you are requesting could not be found. Adjust the "review" parameter setting to update the review correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function handleErrorsReplyToReview(
|
|
|
|
this: IExecuteSingleFunctions,
|
|
|
|
data: INodeExecutionData[],
|
|
|
|
response: IN8nHttpFullResponse,
|
|
|
|
): Promise<INodeExecutionData[]> {
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
const review = this.getNodeParameter('review', undefined) as IDataObject;
|
|
|
|
|
|
|
|
// Provide a better error message
|
|
|
|
if (review && response.statusCode === 404) {
|
|
|
|
throw new NodeOperationError(
|
|
|
|
this.getNode(),
|
|
|
|
'The review you are replying to could not be found. Adjust the "review" parameter setting to reply to the review correctly.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new NodeApiError(this.getNode(), response.body as JsonObject, {
|
|
|
|
message: response.statusMessage,
|
|
|
|
httpCode: response.statusCode.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Helper function used in listSearch methods */
|
|
|
|
export async function googleApiRequest(
|
|
|
|
this: ILoadOptionsFunctions | IPollFunctions,
|
|
|
|
method: IHttpRequestMethods,
|
|
|
|
resource: string,
|
|
|
|
body: IDataObject = {},
|
|
|
|
qs: IDataObject = {},
|
|
|
|
url?: string,
|
|
|
|
): Promise<IDataObject> {
|
|
|
|
const options: IHttpRequestOptions = {
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
|
|
|
method,
|
|
|
|
body,
|
|
|
|
qs,
|
|
|
|
url: url ?? `https://mybusiness.googleapis.com/v4${resource}`,
|
|
|
|
json: true,
|
|
|
|
};
|
|
|
|
try {
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
|
|
delete options.body;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (await this.helpers.httpRequestWithAuthentication.call(
|
|
|
|
this,
|
2024-10-23 04:02:16 -07:00
|
|
|
'googleBusinessProfileOAuth2Api',
|
2024-10-16 08:18:53 -07:00
|
|
|
options,
|
|
|
|
)) as IDataObject;
|
|
|
|
} catch (error) {
|
|
|
|
throw new NodeApiError(this.getNode(), error as JsonObject);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* listSearch methods */
|
|
|
|
|
|
|
|
export async function searchAccounts(
|
|
|
|
this: ILoadOptionsFunctions,
|
|
|
|
filter?: string,
|
|
|
|
paginationToken?: string,
|
|
|
|
): Promise<INodeListSearchResult> {
|
|
|
|
// Docs for this API call can be found here:
|
|
|
|
// https://developers.google.com/my-business/reference/accountmanagement/rest/v1/accounts/list
|
|
|
|
const query: IDataObject = {};
|
|
|
|
if (paginationToken) {
|
|
|
|
query.pageToken = paginationToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
const responseData: IDataObject = await googleApiRequest.call(
|
|
|
|
this,
|
|
|
|
'GET',
|
|
|
|
'',
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
pageSize: 20,
|
|
|
|
...query,
|
|
|
|
},
|
|
|
|
'https://mybusinessaccountmanagement.googleapis.com/v1/accounts',
|
|
|
|
);
|
|
|
|
|
|
|
|
const accounts = responseData.accounts as Array<{ name: string; accountName: string }>;
|
|
|
|
|
|
|
|
const results: INodeListSearchItems[] = accounts
|
|
|
|
.map((a) => ({
|
|
|
|
name: a.accountName,
|
|
|
|
value: a.name,
|
|
|
|
}))
|
|
|
|
.filter(
|
|
|
|
(a) =>
|
|
|
|
!filter ||
|
|
|
|
a.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
|
|
a.value.toLowerCase().includes(filter.toLowerCase()),
|
|
|
|
)
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
return { results, paginationToken: responseData.nextPageToken };
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function searchLocations(
|
|
|
|
this: ILoadOptionsFunctions,
|
|
|
|
filter?: string,
|
|
|
|
paginationToken?: string,
|
|
|
|
): Promise<INodeListSearchResult> {
|
|
|
|
// Docs for this API call can be found here:
|
|
|
|
// https://developers.google.com/my-business/reference/businessinformation/rest/v1/accounts.locations/list
|
|
|
|
const query: IDataObject = {};
|
|
|
|
if (paginationToken) {
|
|
|
|
query.pageToken = paginationToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
const account = (this.getNodeParameter('account') as IDataObject).value as string;
|
|
|
|
|
|
|
|
const responseData: IDataObject = await googleApiRequest.call(
|
|
|
|
this,
|
|
|
|
'GET',
|
|
|
|
'',
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
readMask: 'name',
|
|
|
|
pageSize: 100,
|
|
|
|
...query,
|
|
|
|
},
|
|
|
|
`https://mybusinessbusinessinformation.googleapis.com/v1/${account}/locations`,
|
|
|
|
);
|
|
|
|
|
|
|
|
const locations = responseData.locations as Array<{ name: string }>;
|
|
|
|
|
|
|
|
const results: INodeListSearchItems[] = locations
|
|
|
|
.map((a) => ({
|
|
|
|
name: a.name,
|
|
|
|
value: a.name,
|
|
|
|
}))
|
|
|
|
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
return { results, paginationToken: responseData.nextPageToken };
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function searchReviews(
|
|
|
|
this: ILoadOptionsFunctions,
|
|
|
|
filter?: string,
|
|
|
|
paginationToken?: string,
|
|
|
|
): Promise<INodeListSearchResult> {
|
|
|
|
const query: IDataObject = {};
|
|
|
|
if (paginationToken) {
|
|
|
|
query.pageToken = paginationToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
const account = (this.getNodeParameter('account') as IDataObject).value as string;
|
|
|
|
const location = (this.getNodeParameter('location') as IDataObject).value as string;
|
|
|
|
|
|
|
|
const responseData: IDataObject = await googleApiRequest.call(
|
|
|
|
this,
|
|
|
|
'GET',
|
|
|
|
`/${account}/${location}/reviews`,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
pageSize: 50,
|
|
|
|
...query,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const reviews = responseData.reviews as Array<{ name: string; comment: string }>;
|
|
|
|
|
|
|
|
const results: INodeListSearchItems[] = reviews
|
|
|
|
.map((a) => ({
|
|
|
|
name: a.comment,
|
|
|
|
value: a.name,
|
|
|
|
}))
|
|
|
|
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
return { results, paginationToken: responseData.nextPageToken };
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function searchPosts(
|
|
|
|
this: ILoadOptionsFunctions,
|
|
|
|
filter?: string,
|
|
|
|
paginationToken?: string,
|
|
|
|
): Promise<INodeListSearchResult> {
|
|
|
|
const query: IDataObject = {};
|
|
|
|
if (paginationToken) {
|
|
|
|
query.pageToken = paginationToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
const account = (this.getNodeParameter('account') as IDataObject).value as string;
|
|
|
|
const location = (this.getNodeParameter('location') as IDataObject).value as string;
|
|
|
|
|
|
|
|
const responseData: IDataObject = await googleApiRequest.call(
|
|
|
|
this,
|
|
|
|
'GET',
|
|
|
|
`/${account}/${location}/localPosts`,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
pageSize: 100,
|
|
|
|
...query,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const localPosts = responseData.localPosts as Array<{ name: string; summary: string }>;
|
|
|
|
|
|
|
|
const results: INodeListSearchItems[] = localPosts
|
|
|
|
.map((a) => ({
|
|
|
|
name: a.summary,
|
|
|
|
value: a.name,
|
|
|
|
}))
|
|
|
|
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
return { results, paginationToken: responseData.nextPageToken };
|
|
|
|
}
|