diff --git a/codegen.json b/codegen.json index b584d84cc68..8bbcb4dfe13 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,9 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", + "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", + "AddSlackAuthPayload": "./types/AddSlackAuthPayload#AddSlackAuthPayloadSource", "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource", "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource", "UpdateAgendaItemPayload": "./types/UpdateAgendaItemPayload#UpdateAgendaItemPayloadSource", diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index d5855cf4e3c..50f21bd06d4 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -4,6 +4,7 @@ import { selectAgendaItems, selectOrganizations, selectRetroReflections, + selectSlackAuths, selectSuggestedAction, selectTeams, selectTemplateDimension, @@ -191,3 +192,7 @@ export const agendaItemsByMeetingId = foreignKeyLoaderMaker( return selectAgendaItems().where('meetingId', 'in', meetingIds).orderBy('sortOrder').execute() } ) + +export const slackAuthByUserId = foreignKeyLoaderMaker('slackAuths', 'userId', async (userIds) => { + return selectSlackAuths().where('userId', 'in', userIds).execute() +}) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 7ac2ed6389d..3c4d1696d7e 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -10,6 +10,7 @@ import { selectMeetingSettings, selectOrganizations, selectRetroReflections, + selectSlackAuths, selectSuggestedAction, selectTeamPromptResponses, selectTeams, @@ -95,3 +96,7 @@ export const meetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => export const agendaItems = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectAgendaItems().where('id', 'in', ids).execute() }) + +export const slackAuths = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectSlackAuths().where('id', 'in', ids).execute() +}) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 24afe08e897..4a4e1d3a8f1 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -89,15 +89,6 @@ export const meetingMembersByUserId = new RethinkForeignKeyLoaderMaker( } ) -export const slackAuthByUserId = new RethinkForeignKeyLoaderMaker( - 'slackAuths', - 'userId', - async (userIds) => { - const r = await getRethink() - return r.table('SlackAuth').getAll(r.args(userIds), {index: 'userId'}).run() - } -) - export const slackNotificationsByTeamId = new RethinkForeignKeyLoaderMaker( 'slackNotifications', 'teamId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index e2840c3ed0a..fbb1e5c1efe 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -10,7 +10,6 @@ export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') export const newMeetings = new RethinkPrimaryKeyLoaderMaker('NewMeeting') export const newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') -export const slackAuths = new RethinkPrimaryKeyLoaderMaker('SlackAuth') export const slackNotifications = new RethinkPrimaryKeyLoaderMaker('SlackNotification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') export const teamInvitations = new RethinkPrimaryKeyLoaderMaker('TeamInvitation') diff --git a/packages/server/graphql/mutations/addSlackAuth.ts b/packages/server/graphql/mutations/addSlackAuth.ts deleted file mode 100644 index 5c6110e0840..00000000000 --- a/packages/server/graphql/mutations/addSlackAuth.ts +++ /dev/null @@ -1,135 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import SlackAuth from '../../database/types/SlackAuth' -import SlackNotification, {SlackNotificationEvent} from '../../database/types/SlackNotification' -import SlackServerManager from '../../utils/SlackServerManager' -import {analytics} from '../../utils/analytics/analytics' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import AddSlackAuthPayload from '../types/AddSlackAuthPayload' - -export const upsertNotifications = async ( - viewerId: string, - teamId: string, - teamChannelId: string, - channelId: string -) => { - const r = await getRethink() - const existingNotifications = await r - .table('SlackNotification') - .getAll(viewerId, {index: 'userId'}) - .filter({teamId}) - .run() - const teamEvents = [ - 'meetingStart', - 'meetingEnd', - 'MEETING_STAGE_TIME_LIMIT_START', - 'STANDUP_RESPONSE_SUBMITTED' - ] as SlackNotificationEvent[] - const userEvents = ['MEETING_STAGE_TIME_LIMIT_END'] as SlackNotificationEvent[] - const events = [...teamEvents, ...userEvents] - const upsertableNotifications = events.map((event) => { - const existingNotification = existingNotifications.find( - (notification) => notification.event === event - ) - return new SlackNotification({ - event, - // the existing notification channel could be a bad one (legacy reasons, bad means not public or not @Parabol) - channelId: teamEvents.includes(event) ? teamChannelId : channelId, - teamId, - userId: viewerId, - id: (existingNotification && existingNotification.id) || undefined - }) - }) - await r.table('SlackNotification').insert(upsertableNotifications, {conflict: 'replace'}).run() -} - -const upsertAuth = async ( - viewerId: string, - teamId: string, - teamChannelId: string, - slackUserName: string, - slackRes: NonNullable -) => { - const r = await getRethink() - const existingAuth = (await r - .table('SlackAuth') - .getAll(viewerId, {index: 'userId'}) - .filter({teamId}) - .nth(0) - .default(null) - .run()) as SlackAuth | null - - const slackAuth = new SlackAuth({ - id: (existingAuth && existingAuth.id) || undefined, - createdAt: (existingAuth && existingAuth.createdAt) || undefined, - defaultTeamChannelId: teamChannelId, - teamId, - userId: viewerId, - slackTeamId: slackRes.team.id, - slackTeamName: slackRes.team.name, - slackUserId: slackRes.authed_user.id, - slackUserName, - botUserId: slackRes.bot_user_id, - botAccessToken: slackRes.access_token - }) - await r.table('SlackAuth').insert(slackAuth, {conflict: 'replace'}).run() - return slackAuth.id -} - -export default { - name: 'AddSlackAuth', - type: new GraphQLNonNull(AddSlackAuthPayload), - args: { - code: { - type: new GraphQLNonNull(GraphQLID) - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ( - _source: unknown, - {code, teamId}: {code: string; teamId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) => { - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - - // AUTH - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) - } - - // RESOLUTION - const manager = await SlackServerManager.init(code) - const {response} = manager - const slackUserId = response.authed_user.id - const defaultChannelId = response.incoming_webhook.channel_id - const [joinConvoRes, userInfoRes, viewer] = await Promise.all([ - manager.joinConversation(defaultChannelId), - manager.getUserInfo(slackUserId), - dataLoader.get('users').loadNonNull(viewerId) - ]) - if (!userInfoRes.ok) { - return standardError(new Error(userInfoRes.error), {userId: viewerId}) - } - - // The default channel could be anything: public, private, im, mpim. Only allow public channels or the @Parabol channel - // Using the slackUserId sends a DM to the user from @Parabol - const teamChannelId = joinConvoRes.ok ? joinConvoRes.channel.id : slackUserId - - const [, slackAuthId] = await Promise.all([ - upsertNotifications(viewerId, teamId, teamChannelId, defaultChannelId), - upsertAuth(viewerId, teamId, teamChannelId, userInfoRes.user.profile.display_name, response) - ]) - analytics.integrationAdded(viewer, teamId, 'slack') - const data = {slackAuthId, userId: viewerId} - publish(SubscriptionChannel.TEAM, teamId, 'AddSlackAuthPayload', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts b/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts index dc2a59eaea4..1a41522487c 100644 --- a/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts +++ b/packages/server/graphql/mutations/helpers/activatePrevSlackAuth.ts @@ -1,20 +1,17 @@ import ms from 'ms' -import getRethink from '../../../database/rethinkDriver' +import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' +import getKysely from '../../../postgres/getKysely' import {Logger} from '../../../utils/Logger' import SlackServerManager from '../../../utils/SlackServerManager' -import {upsertNotifications} from '../addSlackAuth' - -const activatePrevSlackAuth = async (userId: string, teamId: string) => { - const r = await getRethink() - const now = new Date() - const previousAuth = await r - .table('SlackAuth') - .getAll(userId, {index: 'userId'}) - .filter({teamId}) - .nth(0) - .default(null) - .run() +import {upsertNotifications} from '../../public/mutations/addSlackAuth' +const activatePrevSlackAuth = async ( + userId: string, + teamId: string, + dataLoader: DataLoaderInstance +) => { + const previousAuths = await dataLoader.get('slackAuthByUserId').load(userId) + const previousAuth = previousAuths.find((auth) => auth.teamId === teamId) if (!previousAuth) return const LAST_YEAR = new Date(Date.now() - ms('1y')) const { @@ -34,10 +31,13 @@ const activatePrevSlackAuth = async (userId: string, teamId: string) => { return } - await r({ - auth: r.table('SlackAuth').get(authId).update({isActive: true, updatedAt: now}), - notifications: upsertNotifications(userId, teamId, defaultTeamChannelId, slackUserId) - }).run() + await getKysely() + .updateTable('SlackAuth') + .set({isActive: true}) + .where('id', '=', authId) + .execute() + dataLoader.clearAll('slackAuths') + await upsertNotifications(userId, teamId, defaultTeamChannelId, slackUserId) } } diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index e2e37ad240c..ca2524fb233 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -592,22 +592,15 @@ export const SlackNotifier = { stageIndex: number, channelId: string ) { - const r = await getRethink() - const [team, user, meeting, reflectionGroup, reflections, slackAuth] = await Promise.all([ + const [team, user, meeting, reflectionGroup, reflections, userSlackAuths] = await Promise.all([ dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('users').loadNonNull(userId), dataLoader.get('newMeetings').load(meetingId), dataLoader.get('retroReflectionGroups').loadNonNull(reflectionGroupId), dataLoader.get('retroReflectionsByGroupId').load(reflectionGroupId), - r - .table('SlackAuth') - .getAll(userId, {index: 'userId'}) - .filter({teamId}) - .nth(0) - .default(null) - .run() + dataLoader.get('slackAuthByUserId').load(userId) ]) - + const slackAuth = userSlackAuths.find((auth) => auth.teamId === teamId) if (!slackAuth) { throw new Error('Slack auth not found') } diff --git a/packages/server/graphql/mutations/helpers/removeSlackAuths.ts b/packages/server/graphql/mutations/helpers/removeSlackAuths.ts index 07db0d45fc1..32481e4f741 100644 --- a/packages/server/graphql/mutations/helpers/removeSlackAuths.ts +++ b/packages/server/graphql/mutations/helpers/removeSlackAuths.ts @@ -1,4 +1,5 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' const removeSlackAuths = async ( userId: string, @@ -6,36 +7,26 @@ const removeSlackAuths = async ( removeToken?: boolean ) => { const r = await getRethink() - const now = new Date() const teamIdsArr = Array.isArray(teamIds) ? teamIds : [teamIds] - const existingAuths = await r - .table('SlackAuth') - .getAll(r.args(teamIdsArr), {index: 'teamId'}) - .filter({userId}) - .run() - - if (!existingAuths.length) { - const error = new Error('Auth not found') - return {authIds: null, error} + if (teamIdsArr.length === 0) { + return {authIds: null, error: 'No teams provided'} } + const inactiveAuths = await getKysely() + .updateTable('SlackAuth') + .set({botAccessToken: removeToken ? null : undefined, isActive: false}) + .where('teamId', 'in', teamIdsArr) + .where('userId', '=', userId) + .returning('id') + .execute() - const authIds = existingAuths.map(({id}) => id) await r({ - auth: r - .table('SlackAuth') - .getAll(r.args(authIds)) - .update({ - botAccessToken: removeToken ? null : undefined, - isActive: false, - updatedAt: now - }), notifications: r .table('SlackNotification') .getAll(r.args(teamIdsArr), {index: 'teamId'}) .filter({userId}) .delete() }).run() - + const authIds = inactiveAuths.map(({id}) => id) return {authIds, error: null} } diff --git a/packages/server/graphql/mutations/removeSlackAuth.ts b/packages/server/graphql/mutations/removeSlackAuth.ts index 3e11d1d45a6..3a77b17a141 100644 --- a/packages/server/graphql/mutations/removeSlackAuth.ts +++ b/packages/server/graphql/mutations/removeSlackAuth.ts @@ -37,8 +37,8 @@ export default { removeSlackAuths(viewerId, teamId, true), dataLoader.get('users').loadNonNull(viewerId) ]) - if (res.error) { - return standardError(res.error, {userId: viewerId}) + if (!res.authIds) { + return {error: {message: res.error}} } const authId = res.authIds[0] diff --git a/packages/server/graphql/mutations/setDefaultSlackChannel.ts b/packages/server/graphql/mutations/setDefaultSlackChannel.ts deleted file mode 100644 index d33709c3bdc..00000000000 --- a/packages/server/graphql/mutations/setDefaultSlackChannel.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import getRethink from '../../database/rethinkDriver' -import SlackServerManager from '../../utils/SlackServerManager' -import {getUserId, isTeamMember} from '../../utils/authorization' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import SetDefaultSlackChannelPayload from '../types/SetDefaultSlackChannelPayload' - -const setDefaultSlackChannel = { - type: new GraphQLNonNull(SetDefaultSlackChannelPayload), - description: 'Update the default Slack channel where notifications are sent', - args: { - slackChannelId: { - type: new GraphQLNonNull(GraphQLID) - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ( - _source: unknown, - {slackChannelId, teamId}: {slackChannelId: string; teamId: string}, - {authToken, dataLoader}: GQLContext - ) => { - const r = await getRethink() - const viewerId = getUserId(authToken) - - // AUTH - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) - } - - // VALIDATION - const slackAuths = await dataLoader.get('slackAuthByUserId').load(viewerId) - const slackAuth = slackAuths.find((auth) => auth.teamId === teamId) - if (!slackAuth) { - return standardError(new Error('Slack authentication not found'), {userId: viewerId}) - } - const {id: slackAuthId, botAccessToken, defaultTeamChannelId, slackUserId} = slackAuth - const manager = new SlackServerManager(botAccessToken!) - const channelInfo = await manager.getConversationInfo(slackChannelId) - - // should either be a public / private channel or the slackUserId if messaging from @Parabol - if (slackChannelId !== slackUserId) { - if (!channelInfo.ok) { - return standardError(new Error(channelInfo.error), {userId: viewerId}) - } - const {channel} = channelInfo - const {id: channelId, is_member: isMember, is_archived: isArchived} = channel - if (isArchived) { - return standardError(new Error('Slack channel archived'), {userId: viewerId}) - } - if (!isMember) { - const joinConvoRes = await manager.joinConversation(channelId) - if (!joinConvoRes.ok) { - return standardError(new Error('Unable to join slack channel'), {userId: viewerId}) - } - } - } - - // RESOLUTION - if (slackChannelId !== defaultTeamChannelId) { - await r - .table('SlackAuth') - .get(slackAuthId) - .update({defaultTeamChannelId: slackChannelId}) - .run() - } - const data = {slackChannelId, teamId, userId: viewerId} - return data - } -} - -export default setDefaultSlackChannel diff --git a/packages/server/graphql/mutations/setSlackNotification.ts b/packages/server/graphql/mutations/setSlackNotification.ts deleted file mode 100644 index 34557980b88..00000000000 --- a/packages/server/graphql/mutations/setSlackNotification.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {GraphQLID, GraphQLList, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import {RDatum} from '../../database/stricterR' -import SlackNotification, { - SlackNotificationEventEnum as TSlackNotificationEventEnum -} from '../../database/types/SlackNotification' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import SetSlackNotificationPayload from '../types/SetSlackNotificationPayload' -import SlackNotificationEventEnum from '../types/SlackNotificationEventEnum' - -type SetSlackNotificationMutationVariables = { - slackNotificationEvents: Array - slackChannelId?: string | null - teamId: string -} -export default { - name: 'SetSlackNotification', - type: new GraphQLNonNull(SetSlackNotificationPayload), - args: { - slackChannelId: { - type: GraphQLID - }, - slackNotificationEvents: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SlackNotificationEventEnum))) - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve: async ( - _source: unknown, - {slackChannelId, slackNotificationEvents, teamId}: SetSlackNotificationMutationVariables, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) => { - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const r = await getRethink() - - // AUTH - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) - } - - // VALIDATION - const slackAuths = await dataLoader.get('slackAuthByUserId').load(viewerId) - const slackAuth = slackAuths.find((auth) => auth.teamId === teamId) - - if (!slackAuth) { - return standardError(new Error('Slack authentication not found'), {userId: viewerId}) - } - - // RESOLUTION - const existingNotifications = await r - .table('SlackNotification') - .getAll(viewerId, {index: 'userId'}) - .filter({teamId}) - .filter((row: RDatum) => r(slackNotificationEvents).contains(row('event'))) - .run() - - const notifications = slackNotificationEvents.map((event) => { - const existingNotification = existingNotifications.find( - (notification) => notification.event === event - ) - return new SlackNotification({ - event, - channelId: slackChannelId, - teamId, - userId: viewerId, - id: (existingNotification && existingNotification.id) || undefined - }) - }) - await r.table('SlackNotification').insert(notifications, {conflict: 'replace'}).run() - const slackNotificationIds = notifications.map(({id}) => id) - const data = {userId: viewerId, slackNotificationIds} - publish(SubscriptionChannel.TEAM, teamId, 'SetSlackNotificationPayload', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/private/mutations/messageAllSlackUsers.ts b/packages/server/graphql/private/mutations/messageAllSlackUsers.ts index 4066a661188..802353a1c69 100644 --- a/packages/server/graphql/private/mutations/messageAllSlackUsers.ts +++ b/packages/server/graphql/private/mutations/messageAllSlackUsers.ts @@ -1,4 +1,4 @@ -import getRethink from '../../../database/rethinkDriver' +import {selectSlackAuths} from '../../../postgres/select' import SlackServerManager from '../../../utils/SlackServerManager' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' @@ -12,11 +12,9 @@ const messageAllSlackUsers: MutationResolvers['messageAllSlackUsers'] = async ( _source, {message} ) => { - const r = await getRethink() - // RESOLUTION - const allSlackAuths = await r.table('SlackAuth').filter({isActive: true}).run() - if (!allSlackAuths || !allSlackAuths.length) { + const allSlackAuths = await selectSlackAuths().where('isActive', '=', true).execute() + if (!allSlackAuths.length) { return standardError(new Error('No authorised Slack users')) } diff --git a/packages/server/graphql/private/mutations/removeAllSlackAuths.ts b/packages/server/graphql/private/mutations/removeAllSlackAuths.ts index c63dd3b9bf8..6350b5a903a 100644 --- a/packages/server/graphql/private/mutations/removeAllSlackAuths.ts +++ b/packages/server/graphql/private/mutations/removeAllSlackAuths.ts @@ -1,19 +1,17 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {MutationResolvers} from '../resolverTypes' const removeAllSlackAuths: MutationResolvers['removeAllSlackAuths'] = async () => { const r = await getRethink() - const now = new Date() // RESOLUTION - const allSlackAuths = await r.table('SlackAuth').filter({isActive: true}).run() - const allSlackAuthIds = allSlackAuths.map(({id}) => id) const [slackAuthRes, slackNotificationRes] = await Promise.all([ - r - .table('SlackAuth') - .getAll(r.args(allSlackAuthIds)) - .update({botAccessToken: null, isActive: false, updatedAt: now}) - .run(), + getKysely() + .updateTable('SlackAuth') + .set({botAccessToken: null, isActive: false}) + .returning('id') + .execute(), r.table('SlackNotification').delete().run() ]) const data = { diff --git a/packages/server/graphql/private/queries/dailyPulse.ts b/packages/server/graphql/private/queries/dailyPulse.ts index 6cb63ed20a5..3a6546acaf1 100644 --- a/packages/server/graphql/private/queries/dailyPulse.ts +++ b/packages/server/graphql/private/queries/dailyPulse.ts @@ -1,5 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import getPg from '../../../postgres/getPg' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' import SlackServerManager from '../../../utils/SlackServerManager' @@ -88,17 +86,11 @@ const dailyPulse: QueryResolvers['dailyPulse'] = async ( {after, email, channelId}, {dataLoader} ) => { - const r = await getRethink() const user = await getUserByEmail(email) if (!user) throw new Error('Bad user') const {id: userId} = user - const slackAuth = await r - .table('SlackAuth') - .getAll(userId, {index: 'userId'}) - .filter((row: RValue) => row('botAccessToken').default(null).ne(null)) - .nth(0) - .default(null) - .run() + const slackAuths = await dataLoader.get('slackAuthByUserId').load(userId) + const slackAuth = slackAuths.find((auth) => auth.isActive && auth.botAccessToken) if (!slackAuth) throw new Error('No Slack Auth Found!') const {botAccessToken} = slackAuth const [rawSignups, rawLogins] = await Promise.all([ diff --git a/packages/server/graphql/public/mutations/acceptTeamInvitation.ts b/packages/server/graphql/public/mutations/acceptTeamInvitation.ts index 34f06d3943d..54a83956347 100644 --- a/packages/server/graphql/public/mutations/acceptTeamInvitation.ts +++ b/packages/server/graphql/public/mutations/acceptTeamInvitation.ts @@ -94,7 +94,7 @@ const acceptTeamInvitation: MutationResolvers['acceptTeamInvitation'] = async ( viewerId, dataLoader ) - activatePrevSlackAuth(viewerId, teamId) + activatePrevSlackAuth(viewerId, teamId, dataLoader) await redisLock.unlock() const tms = authToken.tms ? authToken.tms.concat(teamId) : [teamId] // IMPORTANT! mutate the current authToken so any queries or subscriptions can get the latest diff --git a/packages/server/graphql/public/mutations/addSlackAuth.ts b/packages/server/graphql/public/mutations/addSlackAuth.ts new file mode 100644 index 00000000000..63a650989de --- /dev/null +++ b/packages/server/graphql/public/mutations/addSlackAuth.ts @@ -0,0 +1,131 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import SlackNotification, {SlackNotificationEvent} from '../../../database/types/SlackNotification' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' +import SlackServerManager from '../../../utils/SlackServerManager' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +export const upsertNotifications = async ( + viewerId: string, + teamId: string, + teamChannelId: string, + channelId: string +) => { + const r = await getRethink() + const existingNotifications = await r + .table('SlackNotification') + .getAll(viewerId, {index: 'userId'}) + .filter({teamId}) + .run() + const teamEvents = [ + 'meetingStart', + 'meetingEnd', + 'MEETING_STAGE_TIME_LIMIT_START', + 'STANDUP_RESPONSE_SUBMITTED' + ] as SlackNotificationEvent[] + const userEvents = ['MEETING_STAGE_TIME_LIMIT_END'] as SlackNotificationEvent[] + const events = [...teamEvents, ...userEvents] + const upsertableNotifications = events.map((event) => { + const existingNotification = existingNotifications.find( + (notification) => notification.event === event + ) + return new SlackNotification({ + event, + // the existing notification channel could be a bad one (legacy reasons, bad means not public or not @Parabol) + channelId: teamEvents.includes(event) ? teamChannelId : channelId, + teamId, + userId: viewerId, + id: (existingNotification && existingNotification.id) || undefined + }) + }) + await r.table('SlackNotification').insert(upsertableNotifications, {conflict: 'replace'}).run() +} + +const upsertAuth = async ( + viewerId: string, + teamId: string, + teamChannelId: string, + slackUserName: string, + slackRes: NonNullable +) => { + const pg = getKysely() + const res = await pg + .insertInto('SlackAuth') + .values({ + id: generateUID(), + defaultTeamChannelId: teamChannelId, + teamId, + userId: viewerId, + slackTeamId: slackRes.team.id, + slackTeamName: slackRes.team.name, + slackUserId: slackRes.authed_user.id, + slackUserName, + botUserId: slackRes.bot_user_id, + botAccessToken: slackRes.access_token + }) + .onConflict((oc) => + oc.columns(['teamId', 'userId']).doUpdateSet((eb) => ({ + isActive: true, + defaultTeamChannelId: eb.ref('excluded.defaultTeamChannelId'), + slackTeamId: eb.ref('excluded.slackTeamId'), + slackTeamName: eb.ref('excluded.slackTeamName'), + slackUserId: eb.ref('excluded.slackUserId'), + slackUserName: eb.ref('excluded.slackUserName'), + botUserId: eb.ref('excluded.botUserId'), + botAccessToken: eb.ref('excluded.botAccessToken') + })) + ) + .returning('id') + .executeTakeFirstOrThrow() + + return res.id +} + +const addSlackAuth: MutationResolvers['addSlackAuth'] = async ( + _source, + {code, teamId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + + // AUTH + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) + } + + // RESOLUTION + const manager = await SlackServerManager.init(code) + const {response} = manager + const slackUserId = response.authed_user.id + const defaultChannelId = response.incoming_webhook.channel_id + const [joinConvoRes, userInfoRes, viewer] = await Promise.all([ + manager.joinConversation(defaultChannelId), + manager.getUserInfo(slackUserId), + dataLoader.get('users').loadNonNull(viewerId) + ]) + if (!userInfoRes.ok) { + return standardError(new Error(userInfoRes.error), {userId: viewerId}) + } + + // The default channel could be anything: public, private, im, mpim. Only allow public channels or the @Parabol channel + // Using the slackUserId sends a DM to the user from @Parabol + const teamChannelId = joinConvoRes.ok ? joinConvoRes.channel.id : slackUserId + + const [, slackAuthId] = await Promise.all([ + upsertNotifications(viewerId, teamId, teamChannelId, defaultChannelId), + upsertAuth(viewerId, teamId, teamChannelId, userInfoRes.user.profile.display_name, response) + ]) + analytics.integrationAdded(viewer, teamId, 'slack') + const data = {slackAuthId, userId: viewerId} + publish(SubscriptionChannel.TEAM, teamId, 'AddSlackAuthPayload', data, subOptions) + return data +} + +export default addSlackAuth diff --git a/packages/server/graphql/public/mutations/setDefaultSlackChannel.ts b/packages/server/graphql/public/mutations/setDefaultSlackChannel.ts new file mode 100644 index 00000000000..b33559b9fd5 --- /dev/null +++ b/packages/server/graphql/public/mutations/setDefaultSlackChannel.ts @@ -0,0 +1,61 @@ +import getKysely from '../../../postgres/getKysely' +import SlackServerManager from '../../../utils/SlackServerManager' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const setDefaultSlackChannel: MutationResolvers['setDefaultSlackChannel'] = async ( + _source, + {slackChannelId, teamId}, + {authToken, dataLoader} +) => { + const viewerId = getUserId(authToken) + + // AUTH + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) + } + + // VALIDATION + const slackAuths = await dataLoader.get('slackAuthByUserId').load(viewerId) + const slackAuth = slackAuths.find((auth) => auth.teamId === teamId) + if (!slackAuth) { + return standardError(new Error('Slack authentication not found'), {userId: viewerId}) + } + const {id: slackAuthId, botAccessToken, defaultTeamChannelId, slackUserId} = slackAuth + const manager = new SlackServerManager(botAccessToken!) + const channelInfo = await manager.getConversationInfo(slackChannelId) + + // should either be a public / private channel or the slackUserId if messaging from @Parabol + if (slackChannelId !== slackUserId) { + if (!channelInfo.ok) { + return standardError(new Error(channelInfo.error), {userId: viewerId}) + } + const {channel} = channelInfo + const {id: channelId, is_member: isMember, is_archived: isArchived} = channel + if (isArchived) { + return standardError(new Error('Slack channel archived'), {userId: viewerId}) + } + if (!isMember) { + const joinConvoRes = await manager.joinConversation(channelId) + if (!joinConvoRes.ok) { + return standardError(new Error('Unable to join slack channel'), {userId: viewerId}) + } + } + } + + // RESOLUTION + if (slackChannelId !== defaultTeamChannelId) { + await getKysely() + .updateTable('SlackAuth') + .set({ + defaultTeamChannelId: slackChannelId + }) + .where('id', '=', slackAuthId) + .execute() + } + const data = {slackChannelId, teamId, userId: viewerId} + return data +} + +export default setDefaultSlackChannel diff --git a/packages/server/graphql/public/mutations/setSlackNotification.ts b/packages/server/graphql/public/mutations/setSlackNotification.ts new file mode 100644 index 00000000000..6cddacc36b1 --- /dev/null +++ b/packages/server/graphql/public/mutations/setSlackNotification.ts @@ -0,0 +1,60 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import {RDatum} from '../../../database/stricterR' +import SlackNotification from '../../../database/types/SlackNotification' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const setSlackNotification: MutationResolvers['setSlackNotification'] = async ( + _source, + {slackChannelId, slackNotificationEvents, teamId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const r = await getRethink() + + // AUTH + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) + } + + // VALIDATION + const slackAuths = await dataLoader.get('slackAuthByUserId').load(viewerId) + const slackAuth = slackAuths.find((auth) => auth.teamId === teamId) + + if (!slackAuth) { + return standardError(new Error('Slack authentication not found'), {userId: viewerId}) + } + + // RESOLUTION + const existingNotifications = await r + .table('SlackNotification') + .getAll(viewerId, {index: 'userId'}) + .filter({teamId}) + .filter((row: RDatum) => r(slackNotificationEvents).contains(row('event'))) + .run() + + const notifications = slackNotificationEvents.map((event) => { + const existingNotification = existingNotifications.find( + (notification) => notification.event === event + ) + return new SlackNotification({ + event, + channelId: slackChannelId, + teamId, + userId: viewerId, + id: (existingNotification && existingNotification.id) || undefined + }) + }) + await r.table('SlackNotification').insert(notifications, {conflict: 'replace'}).run() + const slackNotificationIds = notifications.map(({id}) => id) + const data = {userId: viewerId, slackNotificationIds} + publish(SubscriptionChannel.TEAM, teamId, 'SetSlackNotificationPayload', data, subOptions) + return data +} + +export default setSlackNotification diff --git a/packages/server/graphql/public/types/AddSlackAuthPayload.ts b/packages/server/graphql/public/types/AddSlackAuthPayload.ts new file mode 100644 index 00000000000..25b2b042a01 --- /dev/null +++ b/packages/server/graphql/public/types/AddSlackAuthPayload.ts @@ -0,0 +1,22 @@ +import {AddSlackAuthPayloadResolvers} from '../resolverTypes' + +export type AddSlackAuthPayloadSource = + | { + slackAuthId: string + userId: string + } + | {error: {message: string}} + +const AddSlackAuthPayload: AddSlackAuthPayloadResolvers = { + slackIntegration: async (source, _args, {dataLoader}) => { + return 'slackAuthId' in source + ? dataLoader.get('slackAuths').loadNonNull(source.slackAuthId) + : null + }, + + user: (source, _args, {dataLoader}) => { + return 'userId' in source ? dataLoader.get('users').loadNonNull(source.userId) : null + } +} + +export default AddSlackAuthPayload diff --git a/packages/server/graphql/public/types/SetDefaultSlackChannelSuccess.ts b/packages/server/graphql/public/types/SetDefaultSlackChannelSuccess.ts new file mode 100644 index 00000000000..a3a889b1f49 --- /dev/null +++ b/packages/server/graphql/public/types/SetDefaultSlackChannelSuccess.ts @@ -0,0 +1,17 @@ +import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' +import {SetDefaultSlackChannelSuccessResolvers} from '../resolverTypes' + +export type SetDefaultSlackChannelSuccessSource = { + slackChannelId: string + userId: string + teamId: string +} + +const SetDefaultSlackChannelSuccess: SetDefaultSlackChannelSuccessResolvers = { + teamMember: ({userId, teamId}, _args, {dataLoader}) => { + const teamMemberId = toTeamMemberId(teamId, userId) + return dataLoader.get('teamMembers').loadNonNull(teamMemberId) + } +} + +export default SetDefaultSlackChannelSuccess diff --git a/packages/server/graphql/public/types/SetSlackNotificationPayload.ts b/packages/server/graphql/public/types/SetSlackNotificationPayload.ts new file mode 100644 index 00000000000..74b9517f484 --- /dev/null +++ b/packages/server/graphql/public/types/SetSlackNotificationPayload.ts @@ -0,0 +1,25 @@ +import isValid from '../../isValid' +import {SetSlackNotificationPayloadResolvers} from '../resolverTypes' + +export type SetSlackNotificationPayloadSource = + | { + slackNotificationIds: string[] + userId: string + } + | {error: {message: string}} + +const SetSlackNotificationPayload: SetSlackNotificationPayloadResolvers = { + slackNotifications: async (source, _args, {dataLoader}) => { + return 'slackNotificationIds' in source + ? (await dataLoader.get('slackNotifications').loadMany(source.slackNotificationIds)).filter( + isValid + ) + : null + }, + + user: (source, _args, {dataLoader}) => { + return 'userId' in source ? dataLoader.get('users').loadNonNull(source.userId) : null + } +} + +export default SetSlackNotificationPayload diff --git a/packages/server/graphql/public/types/SlackNotification.ts b/packages/server/graphql/public/types/SlackNotification.ts new file mode 100644 index 00000000000..38a92e72dac --- /dev/null +++ b/packages/server/graphql/public/types/SlackNotification.ts @@ -0,0 +1,10 @@ +import {slackNotificationEventTypeLookup} from '../../../database/types/SlackNotification' +import {SlackNotificationResolvers} from '../resolverTypes' + +const SlackNotification: SlackNotificationResolvers = { + eventType: ({event}) => { + return slackNotificationEventTypeLookup[event] + } +} + +export default SlackNotification diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 9493f6e092c..71fde7a3e06 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -9,7 +9,6 @@ import addPokerTemplateDimension from './mutations/addPokerTemplateDimension' import addPokerTemplateScale from './mutations/addPokerTemplateScale' import addPokerTemplateScaleValue from './mutations/addPokerTemplateScaleValue' import addReflectTemplatePrompt from './mutations/addReflectTemplatePrompt' -import addSlackAuth from './mutations/addSlackAuth' import addTeam from './mutations/addTeam' import archiveOrganization from './mutations/archiveOrganization' import archiveTeam from './mutations/archiveTeam' @@ -83,11 +82,9 @@ import resetPassword from './mutations/resetPassword' import resetRetroMeetingToGroupStage from './mutations/resetRetroMeetingToGroupStage' import selectTemplate from './mutations/selectTemplate' import setAppLocation from './mutations/setAppLocation' -import setDefaultSlackChannel from './mutations/setDefaultSlackChannel' import setNotificationStatus from './mutations/setNotificationStatus' import setPhaseFocus from './mutations/setPhaseFocus' import setPokerSpectate from './mutations/setPokerSpectate' -import setSlackNotification from './mutations/setSlackNotification' import setStageTimer from './mutations/setStageTimer' import setTaskEstimate from './mutations/setTaskEstimate' import setTaskHighlight from './mutations/setTaskHighlight' @@ -122,7 +119,6 @@ export default new GraphQLObjectType({ addPokerTemplateScale, addPokerTemplateScaleValue, addReflectTemplatePrompt, - addSlackAuth, addGitHubAuth, addOrg, addTeam, @@ -189,10 +185,8 @@ export default new GraphQLObjectType({ resetRetroMeetingToGroupStage, selectTemplate, setAppLocation, - setDefaultSlackChannel, setPhaseFocus, setStageTimer, - setSlackNotification, startDraggingReflection, startSprintPoker, setTaskHighlight, diff --git a/packages/server/graphql/types/AddSlackAuthPayload.ts b/packages/server/graphql/types/AddSlackAuthPayload.ts deleted file mode 100644 index 530cdef88b3..00000000000 --- a/packages/server/graphql/types/AddSlackAuthPayload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import SlackIntegration from './SlackIntegration' -import StandardMutationError from './StandardMutationError' -import User from './User' - -const AddSlackAuthPayload = new GraphQLObjectType({ - name: 'AddSlackAuthPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - slackIntegration: { - type: SlackIntegration, - description: 'The newly created auth', - resolve: async ({slackAuthId}, _args: unknown, {dataLoader}: GQLContext) => { - return dataLoader.get('slackAuths').load(slackAuthId) - } - }, - user: { - type: User, - description: 'The user with updated slackAuth', - resolve: ({userId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('users').load(userId) - } - } - }) -}) - -export default AddSlackAuthPayload diff --git a/packages/server/graphql/types/SetDefaultSlackChannelPayload.ts b/packages/server/graphql/types/SetDefaultSlackChannelPayload.ts deleted file mode 100644 index 81381afe1bf..00000000000 --- a/packages/server/graphql/types/SetDefaultSlackChannelPayload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import {GQLContext} from '../graphql' -import TeamMember from './TeamMember' -import makeMutationPayload from './makeMutationPayload' - -export const SetDefaultSlackChannelSuccess = new GraphQLObjectType({ - name: 'SetDefaultSlackChannelSuccess', - fields: () => ({ - slackChannelId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the slack channel that is now the default slack channel' - }, - teamMember: { - type: new GraphQLNonNull(TeamMember), - description: 'The team member with the updated slack channel', - resolve: ({teamId, userId}, _args: unknown, {dataLoader}) => { - const teamMemberId = toTeamMemberId(teamId, userId) - return dataLoader.get('teamMembers').load(teamMemberId) - } - } - }) -}) - -const SetDefaultSlackChannelPayload = makeMutationPayload( - 'SetDefaultSlackChannelPayload', - SetDefaultSlackChannelSuccess -) - -export default SetDefaultSlackChannelPayload diff --git a/packages/server/graphql/types/SetSlackNotificationPayload.ts b/packages/server/graphql/types/SetSlackNotificationPayload.ts deleted file mode 100644 index b9725031f75..00000000000 --- a/packages/server/graphql/types/SetSlackNotificationPayload.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import SlackNotification from './SlackNotification' -import StandardMutationError from './StandardMutationError' -import User from './User' - -const SetSlackNotificationPayload = new GraphQLObjectType({ - name: 'SetSlackNotificationPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - slackNotifications: { - type: new GraphQLList(new GraphQLNonNull(SlackNotification)), - resolve: async ({slackNotificationIds}, _args: unknown, {dataLoader}) => { - return dataLoader.get('slackNotifications').loadMany(slackNotificationIds) - } - }, - user: { - type: User, - description: 'The user with updated slack notifications', - resolve: ({userId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('users').load(userId) - } - } - }) -}) - -export default SetSlackNotificationPayload diff --git a/packages/server/graphql/types/SlackIntegration.ts b/packages/server/graphql/types/SlackIntegration.ts deleted file mode 100644 index ae9c958569d..00000000000 --- a/packages/server/graphql/types/SlackIntegration.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {GraphQLObjectType} from 'graphql' - -const SlackIntegration = new GraphQLObjectType({ - name: 'SlackIntegration', - fields: {} -}) - -export default SlackIntegration diff --git a/packages/server/graphql/types/SlackNotification.ts b/packages/server/graphql/types/SlackNotification.ts deleted file mode 100644 index 000d8bdfcce..00000000000 --- a/packages/server/graphql/types/SlackNotification.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import { - SlackNotificationEvent, - slackNotificationEventTypeLookup -} from '../../database/types/SlackNotification' -import {GQLContext} from '../graphql' -import SlackNotificationEventEnum from './SlackNotificationEventEnum' -import SlackNotificationEventTypeEnum from './SlackNotificationEventTypeEnum' - -const SlackNotification = new GraphQLObjectType({ - name: 'SlackNotification', - description: 'an event trigger and slack channel to receive it', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID) - }, - event: { - type: new GraphQLNonNull(SlackNotificationEventEnum) - }, - eventType: { - type: new GraphQLNonNull(SlackNotificationEventTypeEnum), - resolve: ({event}: {event: SlackNotificationEvent}) => { - return slackNotificationEventTypeLookup[event] - } - }, - channelId: { - type: GraphQLID, - description: 'null if no notification is to be sent' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - }, - userId: { - type: new GraphQLNonNull(GraphQLID) - } - }) -}) - -export default SlackNotification diff --git a/packages/server/graphql/types/SlackNotificationEventEnum.ts b/packages/server/graphql/types/SlackNotificationEventEnum.ts deleted file mode 100644 index 9100c62384b..00000000000 --- a/packages/server/graphql/types/SlackNotificationEventEnum.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {GraphQLEnumType} from 'graphql' - -const SlackNotificationEventEnum = new GraphQLEnumType({ - name: 'SlackNotificationEventEnum', - description: 'The event that triggers a slack notification', - values: { - meetingStart: {}, - meetingEnd: {}, - MEETING_STAGE_TIME_LIMIT_END: {}, // user event - MEETING_STAGE_TIME_LIMIT_START: {}, - TOPIC_SHARED: {}, - STANDUP_RESPONSE_SUBMITTED: {} - } -}) - -export default SlackNotificationEventEnum diff --git a/packages/server/graphql/types/SlackNotificationEventTypeEnum.ts b/packages/server/graphql/types/SlackNotificationEventTypeEnum.ts deleted file mode 100644 index f84499cffe8..00000000000 --- a/packages/server/graphql/types/SlackNotificationEventTypeEnum.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {GraphQLEnumType} from 'graphql' - -const SlackNotificationEventTypeEnum = new GraphQLEnumType({ - name: 'SlackNotificationEventTypeEnum', - description: 'The type of event for a slack notification', - values: { - team: { - value: 'team', - description: 'notification that concerns the whole team' - }, - member: { - value: 'member', - description: 'notification that concerns a single member on the team' - } - } -}) - -export default SlackNotificationEventTypeEnum diff --git a/packages/server/postgres/migrations/1724261917988_SlackAuth.ts b/packages/server/postgres/migrations/1724261917988_SlackAuth.ts new file mode 100644 index 00000000000..0a4549fe0e2 --- /dev/null +++ b/packages/server/postgres/migrations/1724261917988_SlackAuth.ts @@ -0,0 +1,108 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "SlackAuth" ( + "isActive" BOOLEAN NOT NULL DEFAULT TRUE, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "id" VARCHAR(100) PRIMARY KEY, + "botUserId" VARCHAR(100) NOT NULL, + "botAccessToken" VARCHAR(100), + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "defaultTeamChannelId" VARCHAR(100) NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "userId" VARCHAR(100) NOT NULL, + "slackTeamId" VARCHAR(100) NOT NULL, + "slackTeamName" VARCHAR(100) NOT NULL, + "slackUserId" VARCHAR(100) NOT NULL, + "slackUserName" VARCHAR(100) NOT NULL, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE CASCADE, + UNIQUE("teamId", "userId") + ); + CREATE INDEX IF NOT EXISTS "idx_SlackAuth_teamId" ON "SlackAuth"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_SlackAuth_userId" ON "SlackAuth"("userId"); + DROP TRIGGER IF EXISTS "update_SlackAuth_updatedAt" ON "SlackAuth"; + CREATE TRIGGER "update_SlackAuth_updatedAt" BEFORE UPDATE ON "SlackAuth" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); + END $$; +`.execute(pg) + + const rAuths = await r.table('SlackAuth').coerceTo('array').run() + + await Promise.all( + rAuths.map(async (auth) => { + const { + isActive, + updatedAt, + id, + botUserId, + botAccessToken, + createdAt, + defaultTeamChannelId, + teamId, + userId, + slackTeamId, + slackTeamName, + slackUserId, + slackUserName + } = auth + if (!botUserId || !botAccessToken) return + try { + return await pg + .insertInto('SlackAuth') + .values({ + isActive, + updatedAt, + id, + botUserId, + botAccessToken, + createdAt, + defaultTeamChannelId, + teamId, + userId, + slackTeamId, + slackTeamName, + slackUserId, + slackUserName + }) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId' || e.constraint === 'fk_userId') { + console.log(`Skipping ${auth.id} because it has no user/team`) + return + } + console.log(e, auth) + } + }) + ) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "SlackAuth"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 9a6df8e0d30..308709c8454 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -205,3 +205,5 @@ export const selectMeetingSettings = () => ]) export const selectAgendaItems = () => getKysely().selectFrom('AgendaItem').selectAll() + +export const selectSlackAuths = () => getKysely().selectFrom('SlackAuth').selectAll()