diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index 6588ead8..6cdf7ee6 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -106,11 +106,24 @@ export class IntegrationsController { changeProfilePicture: !!findIntegration?.changeProfilePicture, changeNickName: !!findIntegration?.changeNickname, customer: p.customer, + additionalSettings: p.additionalSettings || '[]', }; }), }; } + @Post('/:id/settings') + async updateProviderSettings( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body('additionalSettings') body: string + ) { + if (typeof body !== 'string') { + throw new Error('Invalid body'); + } + + await this._integrationService.updateProviderSettings(org.id, id, body); + } @Post('/:id/nickname') async setNickname( @GetOrgFromRequest() org: Organization, @@ -257,13 +270,14 @@ export class IntegrationsController { return load; } catch (err) { if (err instanceof RefreshToken) { - const { accessToken, refreshToken, expiresIn } = + const { accessToken, refreshToken, expiresIn, additionalSettings } = await integrationProvider.refreshToken( getIntegration.refreshToken ); if (accessToken) { await this._integrationService.createOrUpdateIntegration( + additionalSettings, !!integrationProvider.oneTimeToken, getIntegration.organizationId, getIntegration.name, @@ -346,6 +360,7 @@ export class IntegrationsController { } return this._integrationService.createOrUpdateIntegration( + undefined, true, org.id, name, @@ -413,6 +428,7 @@ export class IntegrationsController { name, picture, username, + additionalSettings, // eslint-disable-next-line no-async-promise-executor } = await new Promise(async (res) => { const auth = await integrationProvider.authenticate( @@ -432,6 +448,7 @@ export class IntegrationsController { name: '', picture: '', username: '', + additionalSettings: [], }); } @@ -470,6 +487,7 @@ export class IntegrationsController { } } return this._integrationService.createOrUpdateIntegration( + additionalSettings, !!integrationProvider.oneTimeToken, org.id, validName.trim(), diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index c4c6a702..fef350b3 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -60,6 +60,7 @@ export interface Integrations { type: string; picture: string; changeProfilePicture: boolean; + additionalSettings: string; changeNickName: boolean; time: { time: number }[]; customer?: { diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx index db982e90..0b8d8036 100644 --- a/apps/frontend/src/components/launches/menu/menu.tsx +++ b/apps/frontend/src/components/launches/menu/menu.tsx @@ -10,6 +10,7 @@ import { useCalendar } from '@gitroom/frontend/components/launches/calendar.cont import { BotPicture } from '@gitroom/frontend/components/launches/bot.picture'; import { CustomerModal } from '@gitroom/frontend/components/launches/customer.modal'; import { Integration } from '@prisma/client'; +import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal'; export const Menu: FC<{ canEnable: boolean; @@ -31,7 +32,7 @@ export const Menu: FC<{ mutate, canChangeProfilePicture, canChangeNickName, - refreshChannel + refreshChannel, } = props; const fetch = useFetch(); const { integrations } = useCalendar(); @@ -152,6 +153,34 @@ export const Menu: FC<{ setShow(false); }, [integrations]); + const additionalSettings = useCallback(() => { + const findIntegration = integrations.find( + (integration) => integration.id === id + ); + + modal.openModal({ + classNames: { + modal: 'w-[100%] max-w-[600px] bg-transparent text-textColor', + }, + size: '100%', + withCloseButton: false, + closeOnEscape: true, + closeOnClickOutside: true, + children: ( + { + mutate(); + toast.show('Settings Updated', 'success'); + }} + /> + ), + }); + + setShow(false); + }, [integrations]); + const addToCustomer = useCallback(() => { const findIntegration = integrations.find( (integration) => integration.id === id @@ -222,7 +251,29 @@ export const Menu: FC<{ /> -
Refresh channel
+
Reconnect channel
+ + )} + {findIntegration?.additionalSettings !== '[]' && ( +
+
+ + + +
+
Additional Settings
)} {(canChangeProfilePicture || canChangeNickName) && ( @@ -240,7 +291,7 @@ export const Menu: FC<{ > @@ -266,7 +317,7 @@ export const Menu: FC<{ > @@ -283,7 +334,7 @@ export const Menu: FC<{ > @@ -304,7 +355,7 @@ export const Menu: FC<{ > @@ -327,7 +378,7 @@ export const Menu: FC<{ > @@ -342,7 +393,7 @@ export const Menu: FC<{ width="16" height="16" viewBox="0 0 16 16" - fill="none" + fill="currentColor" > ( value: Array>, settings: T ) => Promise, - maximumCharacters?: number + maximumCharacters?: number | ((settings: any) => number) ) { return (props: { identifier: string; @@ -155,7 +155,11 @@ export const withProvider = function ( editInPlace ? InPlaceValue : props.value, dto, checkValidity, - maximumCharacters + !maximumCharacters + ? undefined + : typeof maximumCharacters === 'number' + ? maximumCharacters + : maximumCharacters(JSON.parse(integration?.additionalSettings || '[]')) ); // change editor value @@ -348,10 +352,12 @@ export const withProvider = function ( ); const getInternalPlugs = useCallback(async () => { - return (await fetch(`/integrations/${props.identifier}/internal-plugs`)).json(); + return ( + await fetch(`/integrations/${props.identifier}/internal-plugs`) + ).json(); }, [props.identifier]); - const {data} = useSWR(`internal-${props.identifier}`, getInternalPlugs); + const { data } = useSWR(`internal-${props.identifier}`, getInternalPlugs); // this is a trick to prevent the data from being deleted, yet we don't render the elements if (!props.show) { @@ -423,7 +429,8 @@ export const withProvider = function (
- {(integration?.identifier === 'linkedin' || integration?.identifier === 'linkedin-page') && ( + {(integration?.identifier === 'linkedin' || + integration?.identifier === 'linkedin-page') && ( + +
+ {values.map((setting: any, index: number) => ( + + ))} +
+ +
+ +
+
+ ); +}; diff --git a/libraries/helpers/src/utils/count.length.ts b/libraries/helpers/src/utils/count.length.ts index 02976874..65691ff7 100644 --- a/libraries/helpers/src/utils/count.length.ts +++ b/libraries/helpers/src/utils/count.length.ts @@ -4,7 +4,7 @@ import twitter from 'twitter-text'; export const textSlicer = ( integrationType: string, end: number, - text: string + text: string, ): {start: number, end: number} => { if (integrationType !== 'x') { return { @@ -13,7 +13,21 @@ export const textSlicer = ( } } - const {validRangeEnd, valid} = twitter.parseTweet(text); + const {validRangeEnd, valid} = twitter.parseTweet(text, { + version: 3, + maxWeightedTweetLength: end, + scale: 100, + defaultWeight: 200, + emojiParsingEnabled: true, + transformedURLLength: 23, + ranges: [ + { start: 0, end: 4351, weight: 100 }, + { start: 8192, end: 8205, weight: 100 }, + { start: 8208, end: 8223, weight: 100 }, + { start: 8242, end: 8247, weight: 100 } + ] + }); + return { start: 0, end: valid ? end : validRangeEnd diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index 179c00e9..d8e92687 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -18,6 +18,18 @@ export class IntegrationRepository { private _customers: PrismaRepository<'customer'> ) {} + updateProviderSettings(org: string, id: string, settings: string) { + return this._integration.model.integration.update({ + where: { + id, + organizationId: org, + }, + data: { + additionalSettings: settings, + }, + }); + } + async setTimes(org: string, id: string, times: IntegrationTimeDto) { return this._integration.model.integration.update({ select: { @@ -94,6 +106,15 @@ export class IntegrationRepository { } async createOrUpdateIntegration( + additionalSettings: + | { + title: string; + description: string; + type: 'checkbox' | 'text' | 'textarea'; + value: any; + regex?: string; + }[] + | undefined, oneTimeToken: boolean, org: string, name: string, @@ -144,6 +165,9 @@ export class IntegrationRepository { refreshNeeded: false, rootInternalId: internalId.split('_').pop(), ...(customInstanceDetails ? { customInstanceDetails } : {}), + additionalSettings: additionalSettings + ? JSON.stringify(additionalSettings) + : '[]', }, update: { type: type as any, @@ -168,17 +192,28 @@ export class IntegrationRepository { }); if (oneTimeToken) { + const rootId = + ( + await this._integration.model.integration.findFirst({ + where: { + organizationId: org, + internalId: internalId, + }, + }) + )?.rootInternalId || internalId.split('_').pop()!; + await this._integration.model.integration.updateMany({ where: { id: { not: upsert.id, }, organizationId: org, - rootInternalId: internalId.split('_').pop(), + rootInternalId: rootId, }, data: { token, refreshToken, + refreshNeeded: false, ...(expiresIn ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) } : {}), diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 5bace62c..443d87a5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -41,7 +41,26 @@ export class IntegrationService { return this._integrationRepository.setTimes(orgId, integrationId, times); } + updateProviderSettings( + org: string, + id: string, + additionalSettings: string + ) { + return this._integrationRepository.updateProviderSettings( + org, + id, + additionalSettings + ); + } + async createOrUpdateIntegration( + additionalSettings: { + title: string; + description: string; + type: 'checkbox' | 'text' | 'textarea'; + value: any; + regex?: string; + }[] | undefined, oneTimeToken: boolean, org: string, name: string, @@ -62,6 +81,7 @@ export class IntegrationService { ? await this.storage.uploadSimple(picture) : undefined; return this._integrationRepository.createOrUpdateIntegration( + additionalSettings, oneTimeToken, org, name, @@ -166,6 +186,7 @@ export class IntegrationService { const { refreshToken, accessToken, expiresIn } = data; await this.createOrUpdateIntegration( + undefined, !!provider.oneTimeToken, integration.organizationId, integration.name, @@ -334,7 +355,7 @@ export class IntegrationService { dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) || forceRefresh ) { - const { accessToken, expiresIn, refreshToken } = + const { accessToken, expiresIn, refreshToken, additionalSettings } = await new Promise((res) => { return integrationProvider .refreshToken(getIntegration.refreshToken!) @@ -347,12 +368,14 @@ export class IntegrationService { name: '', picture: '', username: '', + additionalSettings: undefined, }); }); }); if (accessToken) { await this.createOrUpdateIntegration( + additionalSettings, !!integrationProvider.oneTimeToken, getIntegration.organizationId, getIntegration.name, @@ -429,10 +452,11 @@ export class IntegrationService { delay: number; information: any; }) { - const originalIntegration = await this._integrationRepository.getIntegrationById( - data.orgId, - data.originalIntegration - ); + const originalIntegration = + await this._integrationRepository.getIntegrationById( + data.orgId, + data.originalIntegration + ); const getIntegration = await this._integrationRepository.getIntegrationById( data.orgId, diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 13285e24..6b066d08 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -217,7 +217,7 @@ export class PostsService { } if (dayjs(integration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) { - const { accessToken, expiresIn, refreshToken } = + const { accessToken, expiresIn, refreshToken, additionalSettings } = await new Promise((res) => { getIntegration .refreshToken(integration.refreshToken!) @@ -231,6 +231,7 @@ export class PostsService { name: '', username: '', picture: '', + additionalSettings: undefined, }) ); }); @@ -249,6 +250,7 @@ export class PostsService { } await this._integrationService.createOrUpdateIntegration( + additionalSettings, !!getIntegration.oneTimeToken, integration.organizationId, integration.name, diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index f7c264f1..5d9deae3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -288,6 +288,7 @@ model Integration { plugs Plugs[] exisingPlugData ExisingPlugData[] rootInternalId String? + additionalSettings String? @default("[]") @@index([rootInternalId]) @@index([updatedAt]) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 3d81fd47..90c0b0b8 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -49,8 +49,6 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }) ).json(); - console.log('refreshToken', refreshToken); - const { vanityName } = await ( await this.fetch('https://api.linkedin.com/v2/me', { headers: { diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index fd99ee44..bd959942 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -13,9 +13,13 @@ export interface IAuthenticator { refresh?: string; }, clientInformation?: ClientInformation - ): Promise; + ): Promise; refreshToken(refreshToken: string): Promise; - reConnect?(id: string, requiredId: string, accessToken: string): Promise; + reConnect?( + id: string, + requiredId: string, + accessToken: string + ): Promise; generateAuthUrl( clientInformation?: ClientInformation ): Promise; @@ -57,6 +61,13 @@ export type AuthTokenDetails = { expiresIn?: number; // The duration in seconds for which the access token is valid picture?: string; username: string; + additionalSettings?: { + title: string; + description: string; + type: 'checkbox' | 'text' | 'textarea'; + value: any, + regex?: string; + }[]; }; export interface ISocialMediaIntegration { diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index eac594e9..0b2a9569 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -86,7 +86,9 @@ export class XProvider extends SocialAbstract implements SocialProvider { accessSecret: accessSecretSplit, }); - const {data: {id}} = await client.v2.me(); + const { + data: { id }, + } = await client.v2.me(); try { await client.v2.retweet(id, postId); @@ -153,35 +155,15 @@ export class XProvider extends SocialAbstract implements SocialProvider { return false; } - async refreshToken(refreshToken: string): Promise { - const startingClient = new TwitterApi({ - appKey: process.env.X_API_KEY!, - appSecret: process.env.X_API_SECRET!, - }); - const { - accessToken, - refreshToken: newRefreshToken, - expiresIn, - client, - } = await startingClient.refreshOAuth2Token(refreshToken); - const { - data: { id, name, profile_image_url }, - } = await client.v2.me(); - - const { - data: { username }, - } = await client.v2.me({ - 'user.fields': 'username', - }); - + async refreshToken(): Promise { return { - id, - name, - accessToken, - refreshToken: newRefreshToken, - expiresIn, - picture: profile_image_url, - username, + id: '', + name: '', + accessToken: '', + refreshToken: '', + expiresIn: 0, + picture: '', + username: '', }; } @@ -217,18 +199,13 @@ export class XProvider extends SocialAbstract implements SocialProvider { accessSecret: oauth_token_secret, }); - const { accessToken, client, accessSecret, userId } = await startingClient.login( - code - ); - - const { id, name, profile_image_url_https } = await client.currentUser( - true - ); + const { accessToken, client, accessSecret } = + await startingClient.login(code); const { - data: { username }, + data: { username, verified, profile_image_url, name, id }, } = await client.v2.me({ - 'user.fields': 'username', + 'user.fields': ['username', 'verified', 'verified_type', 'profile_image_url', 'name'], }); return { @@ -237,8 +214,16 @@ export class XProvider extends SocialAbstract implements SocialProvider { name, refreshToken: '', expiresIn: 999999999, - picture: profile_image_url_https, + picture: profile_image_url, username, + additionalSettings: [ + { + title: 'Verified', + description: 'Is this a verified user? (Premium)', + type: 'checkbox' as const, + value: verified, + }, + ], }; }