920 lines
25 KiB
TypeScript
920 lines
25 KiB
TypeScript
import {
|
|
AnalyticsData,
|
|
AuthTokenDetails,
|
|
PostDetails,
|
|
PostResponse,
|
|
SocialProvider,
|
|
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
|
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
|
import { LinkedinProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.provider';
|
|
import dayjs from 'dayjs';
|
|
import { Integration } from '@prisma/client';
|
|
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
|
import { timer } from '@gitroom/helpers/utils/timer';
|
|
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
|
|
|
@Rules(
|
|
'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment'
|
|
)
|
|
export class LinkedinPageProvider
|
|
extends LinkedinProvider
|
|
implements SocialProvider
|
|
{
|
|
override identifier = 'linkedin-page';
|
|
override name = 'LinkedIn Page';
|
|
override isBetweenSteps = true;
|
|
override refreshWait = true;
|
|
override maxConcurrentJob = 2; // LinkedIn Page has professional posting limits
|
|
override scopes = [
|
|
'openid',
|
|
'profile',
|
|
'w_member_social',
|
|
'r_basicprofile',
|
|
'rw_organization_admin',
|
|
'w_organization_social',
|
|
'r_organization_social',
|
|
];
|
|
|
|
override editor = 'normal' as const;
|
|
|
|
override async refreshToken(
|
|
refresh_token: string
|
|
): Promise<AuthTokenDetails> {
|
|
const {
|
|
access_token: accessToken,
|
|
expires_in,
|
|
refresh_token: refreshToken,
|
|
} = await (
|
|
await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'refresh_token',
|
|
refresh_token,
|
|
client_id: process.env.LINKEDIN_CLIENT_ID!,
|
|
client_secret: process.env.LINKEDIN_CLIENT_SECRET!,
|
|
}),
|
|
})
|
|
).json();
|
|
|
|
const { vanityName } = await (
|
|
await fetch('https://api.linkedin.com/v2/me', {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
})
|
|
).json();
|
|
|
|
const {
|
|
name,
|
|
sub: id,
|
|
picture,
|
|
} = await (
|
|
await fetch('https://api.linkedin.com/v2/userinfo', {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
})
|
|
).json();
|
|
|
|
return {
|
|
id,
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn: expires_in,
|
|
name,
|
|
picture,
|
|
username: vanityName,
|
|
};
|
|
}
|
|
|
|
override async addComment(
|
|
integration: Integration,
|
|
originalIntegration: Integration,
|
|
postId: string,
|
|
information: any,
|
|
) {
|
|
return super.addComment(
|
|
integration,
|
|
originalIntegration,
|
|
postId,
|
|
information,
|
|
false
|
|
);
|
|
}
|
|
|
|
override async repostPostUsers(
|
|
integration: Integration,
|
|
originalIntegration: Integration,
|
|
postId: string,
|
|
information: any
|
|
) {
|
|
return super.repostPostUsers(
|
|
integration,
|
|
originalIntegration,
|
|
postId,
|
|
information,
|
|
false
|
|
);
|
|
}
|
|
|
|
override async generateAuthUrl() {
|
|
const state = makeId(6);
|
|
const codeVerifier = makeId(30);
|
|
const url = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&prompt=none&client_id=${
|
|
process.env.LINKEDIN_CLIENT_ID
|
|
}&redirect_uri=${encodeURIComponent(
|
|
`${process.env.FRONTEND_URL}/integrations/social/linkedin-page`
|
|
)}&state=${state}&scope=${encodeURIComponent(this.scopes.join(' '))}`;
|
|
return {
|
|
url,
|
|
codeVerifier,
|
|
state,
|
|
};
|
|
}
|
|
|
|
async companies(accessToken: string) {
|
|
const { elements, ...all } = await (
|
|
await fetch(
|
|
'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organizationalTarget~(localizedName,vanityName,logoV2(original~:playableStreams))))',
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
'LinkedIn-Version': '202501',
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
return (elements || []).map((e: any) => ({
|
|
id: e.organizationalTarget.split(':').pop(),
|
|
page: e.organizationalTarget.split(':').pop(),
|
|
username: e['organizationalTarget~'].vanityName,
|
|
name: e['organizationalTarget~'].localizedName,
|
|
picture:
|
|
e['organizationalTarget~'].logoV2?.['original~']?.elements?.[0]
|
|
?.identifiers?.[0]?.identifier,
|
|
}));
|
|
}
|
|
|
|
async reConnect(
|
|
id: string,
|
|
requiredId: string,
|
|
accessToken: string
|
|
): Promise<Omit<AuthTokenDetails, 'refreshToken' | 'expiresIn'>> {
|
|
const information = await this.fetchPageInformation(accessToken, {
|
|
page: requiredId,
|
|
});
|
|
|
|
return {
|
|
id: information.id,
|
|
name: information.name,
|
|
accessToken: information.access_token,
|
|
picture: information.picture,
|
|
username: information.username,
|
|
};
|
|
}
|
|
|
|
async fetchPageInformation(accessToken: string, params: { page: string }) {
|
|
const pageId = params.page;
|
|
const data = await (
|
|
await fetch(
|
|
`https://api.linkedin.com/v2/organizations/${pageId}?projection=(id,localizedName,vanityName,logoV2(original~:playableStreams))`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
return {
|
|
id: data.id,
|
|
name: data.localizedName,
|
|
access_token: accessToken,
|
|
picture:
|
|
data?.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0].identifier,
|
|
username: data.vanityName,
|
|
};
|
|
}
|
|
|
|
override async authenticate(params: {
|
|
code: string;
|
|
codeVerifier: string;
|
|
refresh?: string;
|
|
}) {
|
|
const body = new URLSearchParams();
|
|
body.append('grant_type', 'authorization_code');
|
|
body.append('code', params.code);
|
|
body.append(
|
|
'redirect_uri',
|
|
`${process.env.FRONTEND_URL}/integrations/social/linkedin-page`
|
|
);
|
|
body.append('client_id', process.env.LINKEDIN_CLIENT_ID!);
|
|
body.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET!);
|
|
|
|
const {
|
|
access_token: accessToken,
|
|
expires_in: expiresIn,
|
|
refresh_token: refreshToken,
|
|
scope,
|
|
} = await (
|
|
await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body,
|
|
})
|
|
).json();
|
|
|
|
this.checkScopes(this.scopes, scope);
|
|
|
|
const {
|
|
name,
|
|
sub: id,
|
|
picture,
|
|
} = await (
|
|
await fetch('https://api.linkedin.com/v2/userinfo', {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
})
|
|
).json();
|
|
|
|
const { vanityName } = await (
|
|
await fetch('https://api.linkedin.com/v2/me', {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
})
|
|
).json();
|
|
|
|
return {
|
|
id: `p_${id}`,
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn,
|
|
name,
|
|
picture,
|
|
username: vanityName,
|
|
};
|
|
}
|
|
|
|
override async post(
|
|
id: string,
|
|
accessToken: string,
|
|
postDetails: PostDetails[],
|
|
integration: Integration
|
|
): Promise<PostResponse[]> {
|
|
return super.post(id, accessToken, postDetails, integration, 'company');
|
|
}
|
|
|
|
override async comment(
|
|
id: string,
|
|
postId: string,
|
|
lastCommentId: string | undefined,
|
|
accessToken: string,
|
|
postDetails: PostDetails[],
|
|
integration: Integration
|
|
): Promise<PostResponse[]> {
|
|
return super.comment(
|
|
id,
|
|
postId,
|
|
lastCommentId,
|
|
accessToken,
|
|
postDetails,
|
|
integration,
|
|
'company'
|
|
);
|
|
}
|
|
|
|
async analytics(
|
|
id: string,
|
|
accessToken: string,
|
|
date: number
|
|
): Promise<AnalyticsData[]> {
|
|
const endDate = dayjs().unix() * 1000;
|
|
const startDate = dayjs().subtract(date, 'days').unix() * 1000;
|
|
|
|
const { elements }: { elements: Root[]; paging: any } = await (
|
|
await fetch(
|
|
`https://api.linkedin.com/v2/organizationPageStatistics?q=organization&organization=${encodeURIComponent(
|
|
`urn:li:organization:${id}`
|
|
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Linkedin-Version': '202511',
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
const { elements: elements2 }: { elements: Root[]; paging: any } = await (
|
|
await fetch(
|
|
`https://api.linkedin.com/v2/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
|
|
`urn:li:organization:${id}`
|
|
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Linkedin-Version': '202511',
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
const { elements: elements3 }: { elements: Root[]; paging: any } = await (
|
|
await fetch(
|
|
`https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
|
|
`urn:li:organization:${id}`
|
|
)}&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Linkedin-Version': '202511',
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
const analytics = [...elements2, ...elements, ...elements3].reduce(
|
|
(all, current) => {
|
|
if (
|
|
typeof current?.totalPageStatistics?.views?.allPageViews
|
|
?.pageViews !== 'undefined'
|
|
) {
|
|
all['Page Views'].push({
|
|
total: current.totalPageStatistics.views.allPageViews.pageViews,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
}
|
|
|
|
if (
|
|
typeof current?.followerGains?.organicFollowerGain !== 'undefined'
|
|
) {
|
|
all['Organic Followers'].push({
|
|
total: current?.followerGains?.organicFollowerGain,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
}
|
|
|
|
if (typeof current?.followerGains?.paidFollowerGain !== 'undefined') {
|
|
all['Paid Followers'].push({
|
|
total: current?.followerGains?.paidFollowerGain,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
}
|
|
|
|
if (typeof current?.totalShareStatistics !== 'undefined') {
|
|
all['Clicks'].push({
|
|
total: current?.totalShareStatistics.clickCount,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
|
|
all['Shares'].push({
|
|
total: current?.totalShareStatistics.shareCount,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
|
|
all['Engagement'].push({
|
|
total: current?.totalShareStatistics.engagement,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
|
|
all['Comments'].push({
|
|
total: current?.totalShareStatistics.commentCount,
|
|
date: dayjs(current.timeRange.start).format('YYYY-MM-DD'),
|
|
});
|
|
}
|
|
|
|
return all;
|
|
},
|
|
{
|
|
'Page Views': [] as any[],
|
|
Clicks: [] as any[],
|
|
Shares: [] as any[],
|
|
Engagement: [] as any[],
|
|
Comments: [] as any[],
|
|
'Organic Followers': [] as any[],
|
|
'Paid Followers': [] as any[],
|
|
}
|
|
);
|
|
|
|
return Object.keys(analytics).map((key) => ({
|
|
label: key,
|
|
data: analytics[
|
|
key as 'Page Views' | 'Organic Followers' | 'Paid Followers'
|
|
],
|
|
percentageChange: 5,
|
|
}));
|
|
}
|
|
|
|
async postAnalytics(
|
|
integrationId: string,
|
|
accessToken: string,
|
|
postId: string,
|
|
date: number
|
|
): Promise<AnalyticsData[]> {
|
|
const endDate = dayjs().unix() * 1000;
|
|
const startDate = dayjs().subtract(date, 'days').unix() * 1000;
|
|
|
|
// Fetch share statistics for the specific post
|
|
const shareStatsUrl = `https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
|
|
`urn:li:organization:${integrationId}`
|
|
)}&shares=List(${encodeURIComponent(postId)})&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`;
|
|
|
|
const { elements: shareElements }: { elements: PostShareStatElement[] } =
|
|
await (
|
|
await this.fetch(shareStatsUrl, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'LinkedIn-Version': '202511',
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
},
|
|
})
|
|
).json();
|
|
|
|
// Also fetch social actions (likes, comments, shares) for the specific post
|
|
let socialActions: SocialActionsResponse | null = null;
|
|
try {
|
|
const socialActionsUrl = `https://api.linkedin.com/v2/socialActions/${encodeURIComponent(
|
|
postId
|
|
)}`;
|
|
socialActions = await (
|
|
await this.fetch(socialActionsUrl, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'LinkedIn-Version': '202511',
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
},
|
|
})
|
|
).json();
|
|
} catch (e) {
|
|
// Social actions may not be available for all posts
|
|
}
|
|
|
|
// Process share statistics into time series data
|
|
const analytics = (shareElements || []).reduce(
|
|
(all, current) => {
|
|
if (typeof current?.totalShareStatistics !== 'undefined') {
|
|
const dateStr = dayjs(current.timeRange.start).format('YYYY-MM-DD');
|
|
|
|
all['Impressions'].push({
|
|
total: current.totalShareStatistics.impressionCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Unique Impressions'].push({
|
|
total: current.totalShareStatistics.uniqueImpressionsCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Clicks'].push({
|
|
total: current.totalShareStatistics.clickCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Likes'].push({
|
|
total: current.totalShareStatistics.likeCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Comments'].push({
|
|
total: current.totalShareStatistics.commentCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Shares'].push({
|
|
total: current.totalShareStatistics.shareCount || 0,
|
|
date: dateStr,
|
|
});
|
|
|
|
all['Engagement'].push({
|
|
total: current.totalShareStatistics.engagement || 0,
|
|
date: dateStr,
|
|
});
|
|
}
|
|
return all;
|
|
},
|
|
{
|
|
Impressions: [] as { total: number; date: string }[],
|
|
'Unique Impressions': [] as { total: number; date: string }[],
|
|
Clicks: [] as { total: number; date: string }[],
|
|
Likes: [] as { total: number; date: string }[],
|
|
Comments: [] as { total: number; date: string }[],
|
|
Shares: [] as { total: number; date: string }[],
|
|
Engagement: [] as { total: number; date: string }[],
|
|
}
|
|
);
|
|
|
|
// If no time series data but we have social actions, create a single data point
|
|
if (
|
|
Object.values(analytics).every((arr) => arr.length === 0) &&
|
|
socialActions
|
|
) {
|
|
const today = dayjs().format('YYYY-MM-DD');
|
|
analytics['Likes'].push({
|
|
total: socialActions.likesSummary?.totalLikes || 0,
|
|
date: today,
|
|
});
|
|
analytics['Comments'].push({
|
|
total: socialActions.commentsSummary?.totalFirstLevelComments || 0,
|
|
date: today,
|
|
});
|
|
}
|
|
|
|
// Filter out empty analytics
|
|
const result = Object.entries(analytics)
|
|
.filter(([_, data]) => data.length > 0)
|
|
.map(([label, data]) => ({
|
|
label,
|
|
data,
|
|
percentageChange: 0,
|
|
}));
|
|
|
|
return result as any;
|
|
}
|
|
|
|
@Plug({
|
|
identifier: 'linkedin-page-autoRepostPost',
|
|
title: 'Auto Repost Posts',
|
|
description:
|
|
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
|
|
runEveryMilliseconds: 21600000,
|
|
totalRuns: 3,
|
|
fields: [
|
|
{
|
|
name: 'likesAmount',
|
|
type: 'number',
|
|
placeholder: 'Amount of likes',
|
|
description: 'The amount of likes to trigger the repost',
|
|
validation: /^\d+$/,
|
|
},
|
|
],
|
|
})
|
|
async autoRepostPost(
|
|
integration: Integration,
|
|
id: string,
|
|
fields: { likesAmount: string }
|
|
) {
|
|
const {
|
|
likesSummary: { totalLikes },
|
|
} = await (
|
|
await this.fetch(
|
|
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
'Content-Type': 'application/json',
|
|
'LinkedIn-Version': '202501',
|
|
Authorization: `Bearer ${integration.token}`,
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
if (totalLikes >= +fields.likesAmount) {
|
|
await timer(2000);
|
|
await this.fetch(`https://api.linkedin.com/rest/posts`, {
|
|
body: JSON.stringify({
|
|
author: `urn:li:organization:${integration.internalId}`,
|
|
commentary: '',
|
|
visibility: 'PUBLIC',
|
|
distribution: {
|
|
feedDistribution: 'MAIN_FEED',
|
|
targetEntities: [],
|
|
thirdPartyDistributionChannels: [],
|
|
},
|
|
lifecycleState: 'PUBLISHED',
|
|
isReshareDisabledByAuthor: false,
|
|
reshareContext: {
|
|
parent: id,
|
|
},
|
|
}),
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
'Content-Type': 'application/json',
|
|
'LinkedIn-Version': '202504',
|
|
Authorization: `Bearer ${integration.token}`,
|
|
},
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Plug({
|
|
identifier: 'linkedin-page-autoPlugPost',
|
|
title: 'Auto plug post',
|
|
description:
|
|
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
|
|
runEveryMilliseconds: 21600000,
|
|
totalRuns: 3,
|
|
fields: [
|
|
{
|
|
name: 'likesAmount',
|
|
type: 'number',
|
|
placeholder: 'Amount of likes',
|
|
description: 'The amount of likes to trigger the repost',
|
|
validation: /^\d+$/,
|
|
},
|
|
{
|
|
name: 'post',
|
|
type: 'richtext',
|
|
placeholder: 'Post to plug',
|
|
description: 'Message content to plug',
|
|
validation: /^[\s\S]{3,}$/g,
|
|
},
|
|
],
|
|
})
|
|
async autoPlugPost(
|
|
integration: Integration,
|
|
id: string,
|
|
fields: { likesAmount: string; post: string }
|
|
) {
|
|
const {
|
|
likesSummary: { totalLikes },
|
|
} = await (
|
|
await this.fetch(
|
|
`https://api.linkedin.com/v2/socialActions/${encodeURIComponent(id)}`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
'Content-Type': 'application/json',
|
|
'LinkedIn-Version': '202501',
|
|
Authorization: `Bearer ${integration.token}`,
|
|
},
|
|
}
|
|
)
|
|
).json();
|
|
|
|
if (totalLikes >= fields.likesAmount) {
|
|
await timer(2000);
|
|
await this.fetch(
|
|
`https://api.linkedin.com/v2/socialActions/${decodeURIComponent(
|
|
id
|
|
)}/comments`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${integration.token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
actor: `urn:li:organization:${integration.internalId}`,
|
|
object: id,
|
|
message: {
|
|
text: this.fixText(fields.post),
|
|
},
|
|
}),
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export interface Root {
|
|
pageStatisticsByIndustryV2: any[];
|
|
pageStatisticsBySeniority: any[];
|
|
organization: string;
|
|
pageStatisticsByGeoCountry: any[];
|
|
pageStatisticsByTargetedContent: any[];
|
|
totalPageStatistics: TotalPageStatistics;
|
|
pageStatisticsByStaffCountRange: any[];
|
|
pageStatisticsByFunction: any[];
|
|
pageStatisticsByGeo: any[];
|
|
followerGains: { organicFollowerGain: number; paidFollowerGain: number };
|
|
timeRange: TimeRange;
|
|
totalShareStatistics: {
|
|
uniqueImpressionsCount: number;
|
|
shareCount: number;
|
|
engagement: number;
|
|
clickCount: number;
|
|
likeCount: number;
|
|
impressionCount: number;
|
|
commentCount: number;
|
|
};
|
|
}
|
|
|
|
export interface TotalPageStatistics {
|
|
clicks: Clicks;
|
|
views: Views;
|
|
}
|
|
|
|
export interface Clicks {
|
|
mobileCustomButtonClickCounts: any[];
|
|
desktopCustomButtonClickCounts: any[];
|
|
}
|
|
|
|
export interface Views {
|
|
mobileProductsPageViews: MobileProductsPageViews;
|
|
allDesktopPageViews: AllDesktopPageViews;
|
|
insightsPageViews: InsightsPageViews;
|
|
mobileAboutPageViews: MobileAboutPageViews;
|
|
allMobilePageViews: AllMobilePageViews;
|
|
productsPageViews: ProductsPageViews;
|
|
desktopProductsPageViews: DesktopProductsPageViews;
|
|
jobsPageViews: JobsPageViews;
|
|
peoplePageViews: PeoplePageViews;
|
|
overviewPageViews: OverviewPageViews;
|
|
mobileOverviewPageViews: MobileOverviewPageViews;
|
|
lifeAtPageViews: LifeAtPageViews;
|
|
desktopOverviewPageViews: DesktopOverviewPageViews;
|
|
mobileCareersPageViews: MobileCareersPageViews;
|
|
allPageViews: AllPageViews;
|
|
careersPageViews: CareersPageViews;
|
|
mobileJobsPageViews: MobileJobsPageViews;
|
|
mobileLifeAtPageViews: MobileLifeAtPageViews;
|
|
desktopJobsPageViews: DesktopJobsPageViews;
|
|
desktopPeoplePageViews: DesktopPeoplePageViews;
|
|
aboutPageViews: AboutPageViews;
|
|
desktopAboutPageViews: DesktopAboutPageViews;
|
|
mobilePeoplePageViews: MobilePeoplePageViews;
|
|
desktopCareersPageViews: DesktopCareersPageViews;
|
|
desktopInsightsPageViews: DesktopInsightsPageViews;
|
|
desktopLifeAtPageViews: DesktopLifeAtPageViews;
|
|
mobileInsightsPageViews: MobileInsightsPageViews;
|
|
}
|
|
|
|
export interface MobileProductsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface AllDesktopPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface InsightsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileAboutPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface AllMobilePageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface ProductsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopProductsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface JobsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface PeoplePageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface OverviewPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileOverviewPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface LifeAtPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopOverviewPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileCareersPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface AllPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface CareersPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileJobsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileLifeAtPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopJobsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopPeoplePageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface AboutPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopAboutPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobilePeoplePageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopCareersPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopInsightsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface DesktopLifeAtPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface MobileInsightsPageViews {
|
|
pageViews: number;
|
|
uniquePageViews: number;
|
|
}
|
|
|
|
export interface TimeRange {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
// Post analytics interfaces
|
|
export interface PostShareStatElement {
|
|
organizationalEntity: string;
|
|
share: string;
|
|
totalShareStatistics: {
|
|
uniqueImpressionsCount: number;
|
|
shareCount: number;
|
|
engagement: number;
|
|
clickCount: number;
|
|
likeCount: number;
|
|
impressionCount: number;
|
|
commentCount: number;
|
|
};
|
|
timeRange: TimeRange;
|
|
}
|
|
|
|
export interface SocialActionsResponse {
|
|
likesSummary?: {
|
|
totalLikes: number;
|
|
likedByCurrentUser: boolean;
|
|
};
|
|
commentsSummary?: {
|
|
totalFirstLevelComments: number;
|
|
commentsState: string;
|
|
};
|
|
}
|