From 8d4d450feef081563e35f9b592731f09e237389b Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 3 Oct 2024 13:33:29 +0700 Subject: [PATCH] feat: slack and discord --- .../src/api/routes/integrations.controller.ts | 83 +++++-- .../public/icons/platforms/discord.png | Bin 0 -> 14896 bytes .../frontend/public/icons/platforms/slack.png | Bin 0 -> 16549 bytes .../src/components/launches/bot.picture.tsx | 101 ++++++++ .../components/launches/calendar.context.tsx | 2 + .../launches/launches.component.tsx | 6 +- .../src/components/launches/menu/menu.tsx | 69 +++++- .../discord/discord.channel.select.tsx | 44 ++++ .../providers/discord/discord.provider.tsx | 25 ++ .../launches/providers/show.all.providers.tsx | 4 + .../providers/slack/slack.channel.select.tsx | 44 ++++ .../providers/slack/slack.provider.tsx | 25 ++ .../onboarding/connect.channels.tsx | 2 + .../integrations/integration.repository.ts | 13 +- .../integrations/integration.service.ts | 4 + .../database/prisma/posts/posts.service.ts | 5 +- .../src/dtos/posts/create.post.dto.ts | 4 + .../posts/providers-settings/discord.dto.ts | 8 + .../posts/providers-settings/slack.dto.ts | 8 + .../src/integrations/integration.manager.ts | 4 + .../integrations/social/discord.provider.ts | 227 ++++++++++++++++++ .../social/linkedin.page.provider.ts | 6 +- .../integrations/social/linkedin.provider.ts | 2 + .../src/integrations/social/slack.provider.ts | 207 ++++++++++++++++ .../social/social.integrations.interface.ts | 21 +- .../src/helpers/image.with.fallback.tsx | 7 +- 26 files changed, 894 insertions(+), 27 deletions(-) create mode 100644 apps/frontend/public/icons/platforms/discord.png create mode 100644 apps/frontend/public/icons/platforms/slack.png create mode 100644 apps/frontend/src/components/launches/bot.picture.tsx create mode 100644 apps/frontend/src/components/launches/providers/discord/discord.channel.select.tsx create mode 100644 apps/frontend/src/components/launches/providers/discord/discord.provider.tsx create mode 100644 apps/frontend/src/components/launches/providers/slack/slack.channel.select.tsx create mode 100644 apps/frontend/src/components/launches/providers/slack/slack.provider.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/discord.dto.ts create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/slack.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/discord.provider.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/slack.provider.ts diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index a9a92b2d..b0e01043 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -46,21 +46,68 @@ export class IntegrationsController { return { integrations: ( await this._integrationService.getIntegrationsList(org.id) - ).map((p) => ({ - name: p.name, - id: p.id, - internalId: p.internalId, - disabled: p.disabled, - picture: p.picture, - identifier: p.providerIdentifier, - inBetweenSteps: p.inBetweenSteps, - refreshNeeded: p.refreshNeeded, - type: p.type, - time: JSON.parse(p.postingTimes) - })), + ).map((p) => { + const findIntegration = this._integrationManager.getSocialIntegration( + p.providerIdentifier + ); + return { + name: p.name, + id: p.id, + internalId: p.internalId, + disabled: p.disabled, + picture: p.picture, + identifier: p.providerIdentifier, + inBetweenSteps: p.inBetweenSteps, + refreshNeeded: p.refreshNeeded, + type: p.type, + time: JSON.parse(p.postingTimes), + changeProfilePicture: !!findIntegration.changeProfilePicture, + changeNickName: !!findIntegration.changeNickname, + }; + }), }; } + @Post('/:id/nickname') + async setNickname( + @GetOrgFromRequest() org: Organization, + @Param('id') id: string, + @Body() body: { name: string; picture: string } + ) { + const integration = await this._integrationService.getIntegrationById( + org.id, + id + ); + if (!integration) { + throw new Error('Invalid integration'); + } + + const manager = this._integrationManager.getSocialIntegration( + integration.providerIdentifier + ); + if (!manager.changeProfilePicture && !manager.changeNickname) { + throw new Error('Invalid integration'); + } + + const { url } = manager.changeProfilePicture + ? await manager.changeProfilePicture( + integration.internalId, + integration.token, + body.picture + ) + : { url: '' }; + + const { name } = manager.changeNickname + ? await manager.changeNickname( + integration.internalId, + integration.token, + body.name + ) + : { name: '' }; + + return this._integrationService.updateNameAndUrl(id, name, url); + } + @Get('/:id') getSingleIntegration( @Param('id') id: string, @@ -129,7 +176,11 @@ export class IntegrationsController { } if (integrationProvider[body.name]) { - return integrationProvider[body.name](getIntegration.token, body.data); + return integrationProvider[body.name]( + getIntegration.token, + body.data, + getIntegration.internalId + ); } throw new Error('Function not found'); } @@ -144,7 +195,11 @@ export class IntegrationsController { } if (integrationProvider[body.name]) { - return integrationProvider[body.name](getIntegration.token, body.data); + return integrationProvider[body.name]( + getIntegration.token, + body.data, + getIntegration.internalId + ); } throw new Error('Function not found'); } diff --git a/apps/frontend/public/icons/platforms/discord.png b/apps/frontend/public/icons/platforms/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..90af762aabcb6d8a18ead0d012441e831611b3db GIT binary patch literal 14896 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/components/launches/bot.picture.tsx b/apps/frontend/src/components/launches/bot.picture.tsx new file mode 100644 index 00000000..fadac522 --- /dev/null +++ b/apps/frontend/src/components/launches/bot.picture.tsx @@ -0,0 +1,101 @@ +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import React, { FC, FormEventHandler, useCallback, useState } from 'react'; +import { Integrations } from '@gitroom/frontend/components/launches/calendar.context'; +import { useModals } from '@mantine/modals'; +import { Input } from '@gitroom/react/form/input'; +import { Button } from '@gitroom/react/form/button'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { + MediaComponent, + showMediaBox, +} from '@gitroom/frontend/components/media/media.component'; + +export const BotPicture: FC<{ + integration: Integrations; + canChangeProfilePicture: boolean; + canChangeNickName: boolean; + mutate: () => void; +}> = (props) => { + const modal = useModals(); + const toast = useToaster(); + const [nick, setNickname] = useState(props.integration.name); + const [picture, setPicture] = useState(props.integration.picture); + + const fetch = useFetch(); + const submitForm: FormEventHandler = useCallback( + async (e) => { + e.preventDefault(); + await fetch(`/integrations/${props.integration.id}/nickname`, { + method: 'POST', + body: JSON.stringify({ name: nick, picture }), + }); + + props.mutate(); + toast.show('Updated', 'success'); + modal.closeAll(); + }, + [nick, picture, props.mutate] + ); + + const openMedia = useCallback(() => { + showMediaBox((values) => { + setPicture(values.path); + }); + }, []); + + return ( +
+ + + +
+
+ {props.canChangeProfilePicture && ( +
+ Bot Picture + +
+ )} + {props.canChangeNickName && ( + setNickname(e.target.value)} + name="Nickname" + label="Nickname" + placeholder="" + disableForm={true} + /> + )} + +
+ +
+
+
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index 901b02f5..2c508e66 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -57,6 +57,8 @@ export interface Integrations { identifier: string; type: string; picture: string; + changeProfilePicture: boolean; + changeNickName: boolean; time: { time: number }[]; } diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index d0a5ef52..27cf01dc 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -94,7 +94,7 @@ export const LaunchesComponent = () => { useEffect(() => { if (typeof window === 'undefined') { - return ; + return; } if (search.get('scope') === 'missing') { toast.show('You have to approve all the channel permissions', 'warning'); @@ -117,7 +117,7 @@ export const LaunchesComponent = () => {
-
+

Channels

{sortedIntegrations.length === 0 && ( @@ -196,6 +196,8 @@ export const LaunchesComponent = () => { {integration.name}
void, + mutate: () => void; onChange: (shouldReload: boolean) => void; }> = (props) => { - const { canEnable, canDisable, id, onChange, mutate } = props; + const { + canEnable, + canDisable, + id, + onChange, + mutate, + canChangeProfilePicture, + canChangeNickName, + } = props; const fetch = useFetch(); const { integrations } = useCalendar(); const toast = useToaster(); @@ -98,8 +109,30 @@ export const Menu: FC<{ withCloseButton: false, closeOnEscape: false, closeOnClickOutside: false, + children: , + }); + setShow(false); + }, [integrations]); + + const changeBotPicture = 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: ( - + ), }); setShow(false); @@ -128,6 +161,36 @@ export const Menu: FC<{ onClick={(e) => e.stopPropagation()} className={`absolute top-[100%] left-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder ${interClass} text-nowrap`} > + {(canChangeProfilePicture || canChangeNickName) && ( +
+
+ + + +
+
+ Change Bot{' '} + {[ + canChangeProfilePicture && 'Picture', + canChangeNickName && 'Nickname', + ] + .filter((f) => f) + .join(' / ')} +
+
+ )}
void; +}> = (props) => { + const { onChange, name } = props; + const customFunc = useCustomProviderFunction(); + const [publications, setOrgs] = useState([]); + const { getValues } = useSettings(); + const [currentMedia, setCurrentMedia] = useState(); + + const onChangeInner = (event: { target: { value: string, name: string } }) => { + setCurrentMedia(event.target.value); + onChange(event); + }; + + useEffect(() => { + customFunc.get('channels').then((data) => setOrgs(data)); + const settings = getValues()[props.name]; + if (settings) { + setCurrentMedia(settings); + } + }, []); + + + if (!publications.length) { + return null; + } + + return ( + + ); +}; diff --git a/apps/frontend/src/components/launches/providers/discord/discord.provider.tsx b/apps/frontend/src/components/launches/providers/discord/discord.provider.tsx new file mode 100644 index 00000000..86f00438 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/discord/discord.provider.tsx @@ -0,0 +1,25 @@ +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { FC } from 'react'; +import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto'; +import { DiscordChannelSelect } from '@gitroom/frontend/components/launches/providers/discord/discord.channel.select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +const Empty: FC = () => { + return null; +}; + +const DiscordComponent: FC = () => { + const form = useSettings(); + return ( +
+ +
+ ); +}; +export default withProvider( + DiscordComponent, + Empty, + DiscordDto, + undefined, + 280 +); diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index e215d71e..50d9392a 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -13,6 +13,8 @@ import TiktokProvider from '@gitroom/frontend/components/launches/providers/tikt import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider'; import DribbbleProvider from '@gitroom/frontend/components/launches/providers/dribbble/dribbble.provider'; import ThreadsProvider from '@gitroom/frontend/components/launches/providers/threads/threads.provider'; +import DiscordProvider from '@gitroom/frontend/components/launches/providers/discord/discord.provider'; +import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider'; export const Providers = [ {identifier: 'devto', component: DevtoProvider}, @@ -29,6 +31,8 @@ export const Providers = [ {identifier: 'pinterest', component: PinterestProvider}, {identifier: 'dribbble', component: DribbbleProvider}, {identifier: 'threads', component: ThreadsProvider}, + {identifier: 'discord', component: DiscordProvider}, + {identifier: 'slack', component: SlackProvider}, ]; diff --git a/apps/frontend/src/components/launches/providers/slack/slack.channel.select.tsx b/apps/frontend/src/components/launches/providers/slack/slack.channel.select.tsx new file mode 100644 index 00000000..d07f4260 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/slack/slack.channel.select.tsx @@ -0,0 +1,44 @@ +import { FC, useEffect, useState } from 'react'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { Select } from '@gitroom/react/form/select'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +export const SlackChannelSelect: FC<{ + name: string; + onChange: (event: { target: { value: string; name: string } }) => void; +}> = (props) => { + const { onChange, name } = props; + const customFunc = useCustomProviderFunction(); + const [publications, setOrgs] = useState([]); + const { getValues } = useSettings(); + const [currentMedia, setCurrentMedia] = useState(); + + const onChangeInner = (event: { target: { value: string, name: string } }) => { + setCurrentMedia(event.target.value); + onChange(event); + }; + + useEffect(() => { + customFunc.get('channels').then((data) => setOrgs(data)); + const settings = getValues()[props.name]; + if (settings) { + setCurrentMedia(settings); + } + }, []); + + + if (!publications.length) { + return null; + } + + return ( + + ); +}; diff --git a/apps/frontend/src/components/launches/providers/slack/slack.provider.tsx b/apps/frontend/src/components/launches/providers/slack/slack.provider.tsx new file mode 100644 index 00000000..90857a87 --- /dev/null +++ b/apps/frontend/src/components/launches/providers/slack/slack.provider.tsx @@ -0,0 +1,25 @@ +import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { FC } from 'react'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; +import { SlackChannelSelect } from '@gitroom/frontend/components/launches/providers/slack/slack.channel.select'; +import { SlackDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/slack.dto'; + +const Empty: FC = () => { + return null; +}; + +const SlackComponent: FC = () => { + const form = useSettings(); + return ( +
+ +
+ ); +}; +export default withProvider( + SlackComponent, + Empty, + SlackDto, + undefined, + 280 +); diff --git a/apps/frontend/src/components/onboarding/connect.channels.tsx b/apps/frontend/src/components/onboarding/connect.channels.tsx index 02987db8..65e5c7f7 100644 --- a/apps/frontend/src/components/onboarding/connect.channels.tsx +++ b/apps/frontend/src/components/onboarding/connect.channels.tsx @@ -242,6 +242,8 @@ export const ConnectChannels: FC = () => { {integration.name}
{ + const { access_token, expires_in, refresh_token } = await ( + await this.fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + body: new URLSearchParams({ + refresh_token: refreshToken, + grant_type: 'refresh_token', + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + process.env.DISCORD_CLIENT_ID + + ':' + + process.env.DISCORD_CLIENT_SECRET + ).toString('base64')}`, + }, + }) + ).json(); + + const { application } = await ( + await fetch('https://discord.com/api/oauth2/@me', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + refreshToken: refresh_token, + expiresIn: expires_in, + accessToken: access_token, + id: '', + name: application.name, + picture: '', + username: '', + }; + } + async generateAuthUrl(refresh?: string) { + const state = makeId(6); + return { + url: `https://discord.com/oauth2/authorize?client_id=${ + process.env.DISCORD_CLIENT_ID + }&permissions=377957124096&response_type=code&redirect_uri=${encodeURIComponent( + `${process.env.FRONTEND_URL}/integrations/social/discord${ + refresh ? `?refresh=${refresh}` : '' + }` + )}&integration_type=0&scope=bot+identify+guilds&state=${state}`, + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh?: string; + }) { + const [newCode, guild] = params.code.split(':'); + const { access_token, expires_in, refresh_token, scope } = await ( + await this.fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + body: new URLSearchParams({ + code: newCode, + grant_type: 'authorization_code', + redirect_uri: `${process.env.FRONTEND_URL}/integrations/social/discord`, + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + process.env.DISCORD_CLIENT_ID + + ':' + + process.env.DISCORD_CLIENT_SECRET + ).toString('base64')}`, + }, + }) + ).json(); + + this.checkScopes(this.scopes, scope.split(' ')); + + const { application } = await ( + await fetch('https://discord.com/api/oauth2/@me', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + id: guild, + name: application.name, + accessToken: access_token, + refreshToken: refresh_token, + expiresIn: expires_in, + picture: `https://cdn.discordapp.com/avatars/${application.bot.id}/${application.bot.avatar}.png`, + username: application.bot.username, + }; + } + + async channels(accessToken: string, params: any, id: string) { + const list = await ( + await fetch(`https://discord.com/api/guilds/${id}/channels`, { + headers: { + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`, + }, + }) + ).json(); + + console.log(list); + + return list + .filter((p: any) => p.type === 0 || p.type === 15) + .map((p: any) => ({ + id: String(p.id), + name: p.name, + })); + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[] + ): Promise { + let channel = postDetails[0].settings.channel; + if (postDetails.length > 1) { + const { id: threadId } = await ( + await fetch( + `https://discord.com/api/channels/${postDetails[0].settings.channel}/threads`, + { + method: 'POST', + headers: { + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: postDetails[0].message, + auto_archive_duration: 1440, + type: 11, // Public thread type + }), + } + ) + ).json(); + channel = threadId; + } + + const finalData = []; + for (const post of postDetails) { + const form = new FormData(); + form.append( + 'payload_json', + JSON.stringify({ + content: post.message, + attachments: post.media?.map((p, index) => ({ + id: index, + description: `Picture ${index}`, + filename: p.url.split('/').pop(), + })), + }) + ); + + let index = 0; + for (const media of post.media || []) { + const loadMedia = await fetch(media.url); + + form.append( + `files[${index}]`, + await loadMedia.blob(), + media.url.split('/').pop() + ); + index++; + } + + const data = await ( + await fetch(`https://discord.com/api/channels/${channel}/messages`, { + method: 'POST', + headers: { + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`, + }, + body: form, + }) + ).json(); + + finalData.push({ + id: post.id, + releaseURL: `https://discord.com/channels/${id}/${channel}/${data.id}`, + postId: data.id, + status: 'success', + }); + } + + return finalData; + } + + async changeNickname( + id: string, + accessToken: string, + name: string, + ) { + await (await fetch(`https://discord.com/api/guilds/${id}/members/@me`, { + method: 'PATCH', + headers: { + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nick: name, + }) + })).json(); + + return { + name, + } + } +} diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts index 452b9123..831dfc38 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts @@ -8,6 +8,7 @@ import { 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'; export class LinkedinPageProvider extends LinkedinProvider @@ -206,9 +207,10 @@ export class LinkedinPageProvider override async post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails[], + integration: Integration ): Promise { - return super.post(id, accessToken, postDetails, 'company'); + return super.post(id, accessToken, postDetails, integration, 'company'); } async analytics( diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index b76db213..ef80d46a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -13,6 +13,7 @@ import { BadBody, SocialAbstract, } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { Integration } from '@prisma/client'; export class LinkedinProvider extends SocialAbstract implements SocialProvider { identifier = 'linkedin'; @@ -287,6 +288,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { id: string, accessToken: string, postDetails: PostDetails[], + integration: Integration, type = 'personal' as 'company' | 'personal' ): Promise { const [firstPost, ...restPosts] = postDetails; diff --git a/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts new file mode 100644 index 00000000..da34b898 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/slack.provider.ts @@ -0,0 +1,207 @@ +import { + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import dayjs from 'dayjs'; +import { Integration } from '@prisma/client'; + +export class SlackProvider extends SocialAbstract implements SocialProvider { + identifier = 'slack'; + name = 'Slack'; + isBetweenSteps = false; + scopes = ['identify', 'guilds']; + async refreshToken(refreshToken: string): Promise { + const { access_token, expires_in, refresh_token } = await ( + await this.fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + body: new URLSearchParams({ + refresh_token: refreshToken, + grant_type: 'refresh_token', + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${Buffer.from( + process.env.DISCORD_CLIENT_ID + + ':' + + process.env.DISCORD_CLIENT_SECRET + ).toString('base64')}`, + }, + }) + ).json(); + + const { application } = await ( + await fetch('https://discord.com/api/oauth2/@me', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + refreshToken: refresh_token, + expiresIn: expires_in, + accessToken: access_token, + id: '', + name: application.name, + picture: '', + username: '', + }; + } + async generateAuthUrl(refresh?: string) { + const state = makeId(6); + + return { + url: `https://slack.com/oauth/v2/authorize?client_id=${ + process.env.SLACK_ID + }&redirect_uri=${encodeURIComponent( + `${ + process?.env?.FRONTEND_URL?.indexOf('https') === -1 + ? 'https://redirectmeto.com/' + : '' + }${process?.env?.FRONTEND_URL}/integrations/social/slack${ + refresh ? `?refresh=${refresh}` : '' + }` + )}&scope=channels:read,chat:write,users:read,groups:read,channels:join,chat:write.customize&state=${state}`, + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh?: string; + }) { + const { access_token, team, bot_user_id, authed_user, ...all } = await ( + await this.fetch(`https://slack.com/api/oauth.v2.access`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: process.env.SLACK_ID!, + client_secret: process.env.SLACK_SECRET!, + code: params.code, + redirect_uri: `${ + process?.env?.FRONTEND_URL?.indexOf('https') === -1 + ? 'https://redirectmeto.com/' + : '' + }${process?.env?.FRONTEND_URL}/integrations/social/slack${ + params.refresh ? `?refresh=${params.refresh}` : '' + }`, + }), + }) + ).json(); + + const { user } = await ( + await fetch(`https://slack.com/api/users.info?user=${bot_user_id}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + ).json(); + + return { + id: team.id, + name: user.real_name, + accessToken: access_token, + refreshToken: 'null', + expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(), + picture: user.profile.image_48, + username: user.name, + }; + } + + async channels(accessToken: string, params: any, id: string) { + const list = await ( + await fetch( + `https://slack.com/api/conversations.list?types=public_channel,private_channel`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + ).json(); + + return list.channels.map((p: any) => ({ + id: p.id, + name: p.name, + })); + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[], + integration: Integration + ): Promise { + await fetch(`https://slack.com/api/conversations.join`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: postDetails[0].settings.channel, + }), + }); + + let lastId = ''; + for (const post of postDetails) { + const { ts } = await ( + await fetch(`https://slack.com/api/chat.postMessage`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: postDetails[0].settings.channel, + username: integration.name, + icon_url: integration.picture, + ...(lastId ? { thread_ts: lastId } : {}), + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: post.message, + }, + }, + ...(post.media?.length + ? post.media.map((m) => ({ + type: 'image', + image_url: m.url, + alt_text: '', + })) + : []), + ], + }), + }) + ).json(); + + lastId = ts; + } + + return []; + } + + async changeProfilePicture(id: string, accessToken: string, url: string) { + return { + url, + }; + } + + async changeNickname(id: string, accessToken: string, name: string) { + return { + name, + }; + } +} 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 35a9c299..e3b232bb 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -1,3 +1,5 @@ +import { Integration } from '@prisma/client'; + export interface IAuthenticator { authenticate(params: { code: string; @@ -6,7 +8,21 @@ export interface IAuthenticator { }): Promise; refreshToken(refreshToken: string): Promise; generateAuthUrl(refresh?: string): Promise; - analytics?(id: string, accessToken: string, date: number): Promise; + analytics?( + id: string, + accessToken: string, + date: number + ): Promise; + changeNickname?( + id: string, + accessToken: string, + name: string + ): Promise<{ name: string }>; + changeProfilePicture?( + id: string, + accessToken: string, + url: string + ): Promise<{ url: string }>; } export interface AnalyticsData { @@ -35,7 +51,8 @@ export interface ISocialMediaIntegration { post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails[], + integration: Integration ): Promise; // Schedules a new post } diff --git a/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx index 430d9b9a..c4ddc1fb 100644 --- a/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx +++ b/libraries/react-shared-libraries/src/helpers/image.with.fallback.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import Image from 'next/image'; interface ImageSrc { @@ -12,6 +12,11 @@ interface ImageSrc { const ImageWithFallback: FC = (props) => { const { src, fallbackSrc, ...rest } = props; const [imgSrc, setImgSrc] = useState(src); + useEffect(() => { + if (src !== imgSrc) { + setImgSrc(src); + } + }, [src]); return (