import { HttpException, HttpStatus, Injectable, Param, Query, } from '@nestjs/common'; import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider'; import { FacebookProvider } from '@gitroom/nestjs-libraries/integrations/social/facebook.provider'; import { AnalyticsData, SocialProvider } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { Integration, Organization } from '@prisma/client'; import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { LinkedinPageProvider } from '@gitroom/nestjs-libraries/integrations/social/linkedin.page.provider'; import { simpleUpload } from '@gitroom/nestjs-libraries/upload/r2.uploader'; import axios from 'axios'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import dayjs from 'dayjs'; import { timer } from '@gitroom/helpers/utils/timer'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto'; @Injectable() export class IntegrationService { constructor( private _integrationRepository: IntegrationRepository, private _integrationManager: IntegrationManager, private _notificationService: NotificationService ) {} async setTimes(orgId: string, integrationId: string, times: IntegrationTimeDto) { return this._integrationRepository.setTimes(orgId, integrationId, times); } async createOrUpdateIntegration( org: string, name: string, picture: string, type: 'article' | 'social', internalId: string, provider: string, token: string, refreshToken = '', expiresIn?: number, username?: string, isBetweenSteps = false, refresh?: string, timezone?: number ) { const loadImage = await axios.get(picture, { responseType: 'arraybuffer' }); const uploadedPicture = await simpleUpload( loadImage.data, `${makeId(10)}.png`, 'image/png' ); return this._integrationRepository.createOrUpdateIntegration( org, name, uploadedPicture, type, internalId, provider, token, refreshToken, expiresIn, username, isBetweenSteps, refresh, timezone ); } getIntegrationsList(org: string) { return this._integrationRepository.getIntegrationsList(org); } getIntegrationForOrder(id: string, order: string, user: string, org: string) { return this._integrationRepository.getIntegrationForOrder( id, order, user, org ); } updateNameAndUrl(id: string, name: string, url: string) { return this._integrationRepository.updateNameAndUrl(id, name, url); } getIntegrationById(org: string, id: string) { return this._integrationRepository.getIntegrationById(org, id); } async refreshToken(provider: SocialProvider, refresh: string) { try { const { refreshToken, accessToken, expiresIn } = await provider.refreshToken(refresh); if (!refreshToken || !accessToken || !expiresIn) { return false; } return { refreshToken, accessToken, expiresIn }; } catch (e) { return false; } } async disconnectChannel(orgId: string, integration: Integration) { await this._integrationRepository.disconnectChannel(orgId, integration.id); await this.informAboutRefreshError(orgId, integration); } async informAboutRefreshError(orgId: string, integration: Integration) { await this._notificationService.inAppNotification( orgId, `Could not refresh your ${integration.providerIdentifier} channel`, `Could not refresh your ${integration.providerIdentifier} channel. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`, true ); } async refreshNeeded(org: string, id: string) { return this._integrationRepository.refreshNeeded(org, id); } async refreshTokens() { const integrations = await this._integrationRepository.needsToBeRefreshed(); for (const integration of integrations) { const provider = this._integrationManager.getSocialIntegration( integration.providerIdentifier ); const data = await this.refreshToken(provider, integration.refreshToken!); if (!data) { await this.informAboutRefreshError( integration.organizationId, integration ); await this._integrationRepository.refreshNeeded( integration.organizationId, integration.id ); return; } const { refreshToken, accessToken, expiresIn } = data; await this.createOrUpdateIntegration( integration.organizationId, integration.name, integration.picture!, 'social', integration.internalId, integration.providerIdentifier, accessToken, refreshToken, expiresIn ); } } async disableChannel(org: string, id: string) { return this._integrationRepository.disableChannel(org, id); } async enableChannel(org: string, totalChannels: number, id: string) { const integrations = ( await this._integrationRepository.getIntegrationsList(org) ).filter((f) => !f.disabled); if (integrations.length >= totalChannels) { throw new Error('You have reached the maximum number of channels'); } return this._integrationRepository.enableChannel(org, id); } async getPostsForChannel(org: string, id: string) { return this._integrationRepository.getPostsForChannel(org, id); } async deleteChannel(org: string, id: string) { return this._integrationRepository.deleteChannel(org, id); } async disableIntegrations(org: string, totalChannels: number) { return this._integrationRepository.disableIntegrations(org, totalChannels); } async checkForDeletedOnceAndUpdate(org: string, page: string) { return this._integrationRepository.checkForDeletedOnceAndUpdate(org, page); } async saveInstagram( org: string, id: string, data: { pageId: string; id: string } ) { const getIntegration = await this._integrationRepository.getIntegrationById( org, id ); if (getIntegration && !getIntegration.inBetweenSteps) { throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST); } const instagram = this._integrationManager.getSocialIntegration( 'instagram' ) as InstagramProvider; const getIntegrationInformation = await instagram.fetchPageInformation( getIntegration?.token!, data ); await this.checkForDeletedOnceAndUpdate(org, getIntegrationInformation.id); await this._integrationRepository.updateIntegration(id, { picture: getIntegrationInformation.picture, internalId: getIntegrationInformation.id, name: getIntegrationInformation.name, inBetweenSteps: false, token: getIntegrationInformation.access_token, profile: getIntegrationInformation.username, }); return { success: true }; } async saveLinkedin(org: string, id: string, page: string) { const getIntegration = await this._integrationRepository.getIntegrationById( org, id ); if (getIntegration && !getIntegration.inBetweenSteps) { throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST); } const linkedin = this._integrationManager.getSocialIntegration( 'linkedin-page' ) as LinkedinPageProvider; const getIntegrationInformation = await linkedin.fetchPageInformation( getIntegration?.token!, page ); await this.checkForDeletedOnceAndUpdate( org, String(getIntegrationInformation.id) ); await this._integrationRepository.updateIntegration(String(id), { picture: getIntegrationInformation.picture, internalId: String(getIntegrationInformation.id), name: getIntegrationInformation.name, inBetweenSteps: false, token: getIntegrationInformation.access_token, profile: getIntegrationInformation.username, }); return { success: true }; } async saveFacebook(org: string, id: string, page: string) { const getIntegration = await this._integrationRepository.getIntegrationById( org, id ); if (getIntegration && !getIntegration.inBetweenSteps) { throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST); } const facebook = this._integrationManager.getSocialIntegration( 'facebook' ) as FacebookProvider; const getIntegrationInformation = await facebook.fetchPageInformation( getIntegration?.token!, page ); await this.checkForDeletedOnceAndUpdate(org, getIntegrationInformation.id); await this._integrationRepository.updateIntegration(id, { picture: getIntegrationInformation.picture, internalId: getIntegrationInformation.id, name: getIntegrationInformation.name, inBetweenSteps: false, token: getIntegrationInformation.access_token, profile: getIntegrationInformation.username, }); return { success: true }; } async checkAnalytics(org: Organization, integration: string, date: string, forceRefresh = false): Promise { const getIntegration = await this.getIntegrationById(org.id, integration); if (!getIntegration) { throw new Error('Invalid integration'); } if (getIntegration.type !== 'social') { return []; } const integrationProvider = this._integrationManager.getSocialIntegration( getIntegration.providerIdentifier ); if (dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) { const { accessToken, expiresIn, refreshToken } = await integrationProvider.refreshToken(getIntegration.refreshToken!); if (accessToken) { await this.createOrUpdateIntegration( getIntegration.organizationId, getIntegration.name, getIntegration.picture!, 'social', getIntegration.internalId, getIntegration.providerIdentifier, accessToken, refreshToken, expiresIn ); getIntegration.token = accessToken; if (integrationProvider.refreshWait) { await timer(10000); } } else { await this.disconnectChannel(org.id, getIntegration); return []; } } const getIntegrationData = await ioRedis.get( `integration:${org.id}:${integration}:${date}` ); if (getIntegrationData) { return JSON.parse(getIntegrationData); } if (integrationProvider.analytics) { try { const loadAnalytics = await integrationProvider.analytics( getIntegration.internalId, getIntegration.token, +date ); await ioRedis.set( `integration:${org.id}:${integration}:${date}`, JSON.stringify(loadAnalytics), 'EX', !process.env.NODE_ENV || process.env.NODE_ENV === 'development' ? 1 : 3600 ); return loadAnalytics; } catch (e) { if (e instanceof RefreshToken) { return this.checkAnalytics(org, integration, date); } } } return []; } }