Skip to content

Commit

Permalink
Merge pull request #937 from Hylozoic/826-zapier-integration
Browse files Browse the repository at this point in the history
826 zapier integration
  • Loading branch information
tibetsprague authored Feb 27, 2023
2 parents 814d0ee + de31895 commit fb3974a
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 40 deletions.
2 changes: 1 addition & 1 deletion api/graphql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function makeMutations (expressContext, userId, isAdmin, fetchOne) {

createSavedSearch: (root, { data }) => createSavedSearch(data),

createZapierTrigger: (root, { groupId, targetUrl, type }) => createZapierTrigger(userId, groupId, targetUrl, type),
createZapierTrigger: (root, { groupIds, targetUrl, type, params }) => createZapierTrigger(userId, groupIds, targetUrl, type, params),

joinGroup: (root, { groupId }) => joinGroup(groupId, userId),

Expand Down
33 changes: 20 additions & 13 deletions api/graphql/mutations/zapier.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
export async function createZapierTrigger (userId, groupId, targetUrl, type) {
if (groupId) {
const membership = await GroupMembership.forPair(userId, groupId)
if (!membership) {
throw new GraphQLYogaError('You don\'t have access to a group with this ID')
export async function createZapierTrigger (userId, groupIds, targetUrl, type, params) {
return bookshelf.transaction(async (transacting) => {
const trigger = await ZapierTrigger.forge({ user_id: userId, target_url: targetUrl, type, params }).save({}, { transacting })

if (groupIds && groupIds.length > 0) {
const memberships = await GroupMembership.query(q => q.where({ user_id: userId }).whereIn('group_id', groupIds)).fetchAll({ transacting })
if (!memberships || memberships.length === 0) {
throw new GraphQLYogaError('You don\'t have access to any of these groups')
}
await trigger.groups().attach(memberships.map(m => m.get('group_id')), { transacting })
}
}

const trigger = await ZapierTrigger.forge({ user_id: userId, group_id: groupId, target_url: targetUrl, type }).save()
return trigger
return trigger
})
}

export async function deleteZapierTrigger (userId, id) {
const trigger = await ZapierTrigger.query(q => q.where({ id, userId })).fetch()
return bookshelf.transaction(async (transacting) => {
const trigger = await ZapierTrigger.query(q => q.where({ id, userId })).fetch()

if (trigger) {
await trigger.destroy()
}
return true
if (trigger) {
await ZapierTriggerGroup.where({ zapier_trigger_id: id }).destroy({ transacting })
await trigger.destroy({ transacting })
}
return true
})
}
10 changes: 8 additions & 2 deletions api/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,13 @@ type ZapierTrigger {
groupId: ID
# The Zapier URL to call when this type is triggered
targetUrl: String
# When this gets triggered. Options: 'new_member' = when 1 or more members are added to a group
"""
When this gets triggered. Options:
'new_member' = when 1 or more members are added to a group
'member_leaves' = when a person leaves or is kicked out of a group
'member_updated' = when a member of a group updates their profile
'new_post' = when a new post is created
"""
type: String
}

Expand Down Expand Up @@ -1954,7 +1960,7 @@ type Mutation {
# Create a new topic within a Group
createTopic(topicName: String, groupId: ID, isDefault: Boolean, isSubscribing: Boolean): Topic
# Add a new Zapier trigger (called by Zapier)
createZapierTrigger(groupId: ID, targetUrl: String, type: String): ZapierTrigger
createZapierTrigger(groupIds: [ID], targetUrl: String, type: String, params: JSON): ZapierTrigger
# Used by current logged in user to deactivate their account
deactivateMe(id: ID): GenericResult
# Used by current logged in user to delete their account permanently
Expand Down
2 changes: 1 addition & 1 deletion api/models/FlaggedItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = bookshelf.Model.extend({
const comment = await this.getObject()
return Frontend.Route.comment({ comment, groupSlug: group ? group.get('slug') : null })
case FlaggedItem.Type.MEMBER:
return Frontend.Route.profile(this.get('object_id'))
return Frontend.Route.profile(this.get('object_id'), group)
default:
throw new GraphQLYogaError('Unsupported type for Flagged Item', this.get('object_type'))
}
Expand Down
20 changes: 18 additions & 2 deletions api/models/Group.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,16 +544,32 @@ module.exports = bookshelf.Model.extend(merge({

// Background task to do additional work/tasks when new members are added to a group
async afterAddMembers({ groupId, newUserIds, reactivatedUserIds }) {
const zapierTriggers = await ZapierTrigger.query(q => q.where({ group_id: groupId, type: 'new_member' })).fetchAll()
const zapierTriggers = await ZapierTrigger.forTypeAndGroups('new_member', groupId).fetchAll()

if (zapierTriggers && zapierTriggers.length > 0) {
const group = await Group.find(groupId)
const members = await User.query(q => q.whereIn('id', newUserIds.concat(reactivatedUserIds))).fetchAll()
for (const trigger of zapierTriggers) {
const response = await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify(members.map(m => ({
id: m.id,
avatarUrl: m.get('avatar_url'),
bio: m.get('bio'),
contactEmail: m.get('contact_email'),
contactPhone: m.get('contact_phone'),
facebookUrl: m.get('facebook_url'),
linkedinUrl: m.get('linkedin_url'),
location: m.get('location'),
name: m.get('name'),
reactivated: reactivatedUserIds.includes(m.id)
profileUrl: Frontend.Route.profile(m, group),
tagline: m.get('tagline'),
twitterName: m.get('twitter_name'),
url: m.get('url'),
// Whether this user was previously in the group and is being reactivated
reactivated: reactivatedUserIds.includes(m.id),
// Which group were they added to, since the trigger can be for multiple groups
group: { id: group.id, name: group.get('name'), url: Frontend.Route.group(group) }
}))),
headers: { 'Content-Type': 'application/json' }
})
Expand Down
48 changes: 45 additions & 3 deletions api/models/Post.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { init, getEmojiDataFromNative } from 'emoji-mart'
import { difference, filter, isNull, omitBy, uniqBy, isEmpty, intersection, isUndefined, pick } from 'lodash/fp'
import { flatten, sortBy } from 'lodash'
import { TextHelpers } from 'hylo-shared'
import fetch from 'node-fetch'
import { postRoom, pushToSockets } from '../services/Websockets'
import { fulfill, unfulfill } from './post/fulfillPost'
import EnsureLoad from './mixins/EnsureLoad'
Expand Down Expand Up @@ -662,7 +663,48 @@ module.exports = bookshelf.Model.extend(Object.assign({
Post.find(postId, {withRelated: ['groups', 'user', 'relatedUsers']})
.then(post => {
if (!post) return
const slackCommunities = post.relations.groups.filter(c => c.get('slack_hook_url'))
return Promise.map(slackCommunities, c => Group.notifySlack(c.id, post))
})
const slackCommunities = post.relations.groups.filter(g => g.get('slack_hook_url'))
return Promise.map(slackCommunities, g => Group.notifySlack(g.id, post))
}),

// Background task to fire zapier triggers on new_post
zapierTriggers: async ({ postId }) => {
const post = await Post.find(postId, { withRelated: ['groups', 'tags', 'user'] })
if (!post) return

const groupIds = post.relations.groups.map(g => g.id)
const zapierTriggers = await ZapierTrigger.forTypeAndGroups('new_post', groupIds).fetchAll()
if (zapierTriggers && zapierTriggers.length > 0) {
for (const trigger of zapierTriggers) {
// Check if this trigger is only for certain post types and if so whether it matches this post type
if (trigger.get('params')?.types?.length > 0 && !trigger.get('params').types.includes(post.get('type'))) {
continue
}

const creator = post.relations.user
const response = await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify({
id: post.id,
announcement: post.get('announcement'),
createdAt: post.get('created_at'),
creator: { name: creator.get('name'), url: Frontend.Route.profile(creator) },
details: post.details(),
endTime: post.get('end_time'),
isPublic: post.get('is_public'),
location: post.get('location'),
startTime: post.get('start_time'),
title: post.summary(),
type: post.get('type'),
url: Frontend.Route.post(post),
groups: post.relations.groups.map(g => ({ id: g.id, name: g.get('name'), url: Frontend.Route.group(g), postUrl: Frontend.Route.post(post, g) })),
topics: post.relations.tags.map(t => ({ name: t.get('name')})),
}),
headers: { 'Content-Type': 'application/json' }
})
// TODO: what to do with the response? check if succeeded or not?
}
}
}

})
31 changes: 20 additions & 11 deletions api/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,30 +776,39 @@ module.exports = bookshelf.Model.extend(merge({
// Background jobs

async afterLeaveGroup({ removedByModerator, groupId, userId }) {
const zapierTriggers = await ZapierTrigger.query(q => q.where({ group_id: groupId, type: 'member_leaves' })).fetchAll()
const zapierTriggers = await ZapierTrigger.forTypeAndGroups('member_leaves', groupId).fetchAll()
if (zapierTriggers && zapierTriggers.length > 0) {
const user = await User.find(userId)
for (const trigger of zapierTriggers) {
const response = await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify({ id: user.id, name: user.get('name'), removedByModerator }),
headers: { 'Content-Type': 'application/json' }
})
// TODO: what to do with the response? check if succeeded or not?
const group = await Group.find(groupId)
if (user && group) {
for (const trigger of zapierTriggers) {
const response = await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify({
id: user.id,
name: user.get('name'),
// Which group were they removed from, since the trigger can be for multiple groups
group: { id: group.id, name: group.get('name'), url: Frontend.Route.group(group) },
removedByModerator
}),
headers: { 'Content-Type': 'application/json' }
})
// TODO: what to do with the response? check if succeeded or not?
}
}
}
},

async afterUpdate({ userId, changes }) {
const user = await User.find(userId)
if (user) {
const memberships = await user.memberships().fetch()
const memberships = await user.memberships().fetch({ withRelated: 'group' })
memberships.models.forEach(async (membership) => {
const zapierTriggers = await ZapierTrigger.query(q => q.where({ group_id: membership.get('group_id'), type: 'member_updated' })).fetchAll()
const zapierTriggers = await ZapierTrigger.forTypeAndGroups('member_updated', membership.get('group_id')).fetchAll()
for (const trigger of zapierTriggers) {
const response = await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify(Object.assign({ id: user.id }, changes)),
body: JSON.stringify(Object.assign({ id: user.id, profileUrl: Frontend.Route.profile(user, membership.relations.group) }, changes)),
headers: { 'Content-Type': 'application/json' }
})
// TODO: what to do with the response? check if succeeded or not?
Expand Down
14 changes: 11 additions & 3 deletions api/models/ZapierTrigger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEmpty, isEqual, difference } from 'lodash'
import { castArray, isEmpty, isEqual, difference } from 'lodash'

module.exports = bookshelf.Model.extend(Object.assign({
tableName: 'zapier_triggers',
Expand All @@ -9,8 +9,8 @@ module.exports = bookshelf.Model.extend(Object.assign({
return this.belongsTo(User)
},

group () {
return this.belongsTo(Group)
groups () {
return this.belongsToMany(Group).through(ZapierTriggerGroup)
}

}), {
Expand All @@ -19,6 +19,14 @@ module.exports = bookshelf.Model.extend(Object.assign({
if (!id) return Promise.resolve(null)
const where = { id }
return this.where(where).fetch(opts)
},

forTypeAndGroups (type, groupIdOrArray) {
const groupIds = castArray(groupIdOrArray)
return ZapierTrigger.query(q => {
q.join('zapier_triggers_groups', 'zapier_trigger_id', 'zapier_triggers.id')
q.where({ type }).whereIn('group_id', groupIds)
})
}

})
13 changes: 13 additions & 0 deletions api/models/ZapierTriggerGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = bookshelf.Model.extend({
tableName: 'zapier_triggers_groups',
requireFetch: false,
hasTimestamps: false,

trigger: function () {
return this.belongsTo(ZapierTrigger)
},

group: function () {
return this.belongsTo(Group)
}
})
5 changes: 3 additions & 2 deletions api/models/post/createPost.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ export function afterCreatingPost (post, opts) {
.then(() => post.isEvent() && post.updateEventInvitees(opts.eventInviteeIds || [], userId, trxOpts))
.then(() => Tag.updateForPost(post, opts.topicNames, userId, trx))
.then(() => updateTagsAndGroups(post, trx))
.then(() => Queue.classMethod('Post', 'createActivities', {postId: post.id}))
.then(() => Queue.classMethod('Post', 'notifySlack', {postId: post.id}))
.then(() => Queue.classMethod('Post', 'createActivities', { postId: post.id }))
.then(() => Queue.classMethod('Post', 'notifySlack', { postId: post.id }))
.then(() => Queue.classMethod('Post', 'zapierTriggers', { postId: post.id }))
}

async function updateTagsAndGroups (post, trx) {
Expand Down
5 changes: 4 additions & 1 deletion api/services/Frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ module.exports = {
return url(`${contextUrl}/map/post/${getModelId(post)}`)
},

profile: function (user) {
profile: function (user, group) {
if (group) {
return url(`/groups/${getSlug(group)}/members/${getModelId(user)}`)
}
return url(`/members/${getModelId(user)}`)
},

Expand Down
28 changes: 28 additions & 0 deletions migrations/20230221155959_zapier-triggers-multiple-groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

exports.up = async function (knex) {
await knex.schema.createTable('zapier_triggers_groups', table => {
table.increments().primary()
table.bigInteger('zapier_trigger_id').references('id').inTable('zapier_triggers')
table.bigInteger('group_id').references('id').inTable('groups')
table.index(['zapier_trigger_id'])
})

await knex.raw('alter table zapier_triggers_groups alter constraint zapier_triggers_groups_zapier_trigger_id_foreign deferrable initially deferred')
await knex.raw('alter table zapier_triggers_groups alter constraint zapier_triggers_groups_group_id_foreign deferrable initially deferred')

await knex.raw('insert into zapier_triggers_groups (zapier_trigger_id, group_id) (select id, group_id from zapier_triggers)')

await knex.schema.table('zapier_triggers', table => {
table.dropColumn('group_id')
table.jsonb('params')
})
}

exports.down = async function (knex) {
await knex.schema.table('zapier_triggers', table => {
table.dropColumn('params')
table.bigInteger('group_id').references('id').inTable('groups')
})
await knex.raw('UPDATE zapier_triggers SET group_id = \'externalLink\' WHERE type= \'externalLink\'')
await knex.schema.dropTable('zapier_triggers_groups')
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@
"UserExternalData",
"UserSession",
"UserVerificationCode",
"Validation"
"Validation",
"ZapierTrigger",
"ZapierTriggerGroup"
]
},
"nodemonConfig": {
Expand Down

0 comments on commit fb3974a

Please sign in to comment.