Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c8c6f69
create and validation api routes
garrrikkotua Apr 13, 2023
17a26ae
discourse webhook endpoint
garrrikkotua Apr 14, 2023
07e5eef
endpoint to test that webhooks are received
garrrikkotua Apr 14, 2023
60112f2
wip
garrrikkotua Apr 17, 2023
18c804b
polling
garrrikkotua Apr 24, 2023
eb7869a
lint
garrrikkotua Apr 24, 2023
bf171f4
webhooks
garrrikkotua Apr 27, 2023
00d2ac1
small bug fixed for backend
garrrikkotua Apr 27, 2023
db2bc5b
Merge branch 'main' into feature/discourse-integration
garrrikkotua May 15, 2023
c0a357b
update member stricture
garrrikkotua May 15, 2023
31e5001
discourse logo & integration card
garrrikkotua May 15, 2023
8f14417
wip frontend
garrrikkotua May 16, 2023
3f9bbcc
frontend drawer
garrrikkotua May 19, 2023
6868584
discourse frontend & some backend fixes
garrrikkotua May 22, 2023
6a33b12
Merge branch 'main' into feature/discourse-integration
garrrikkotua May 22, 2023
3fb84e4
frontend loading fix
garrrikkotua May 22, 2023
c34ef80
add discourse to i18n
garrrikkotua May 22, 2023
6892515
small corrections
garrrikkotua May 22, 2023
39f224a
fix source id when webhook
garrrikkotua May 23, 2023
91c5651
fix activity urls
garrrikkotua May 24, 2023
d89cb7e
lint & format
garrrikkotua May 24, 2023
cd8c4f6
filter out bots
garrrikkotua May 24, 2023
3cb29bc
small frontend fixes
garrrikkotua May 24, 2023
05db567
Merge branch 'main' into feature/discourse-integration
garrrikkotua May 24, 2023
970a066
fix handle cancel
garrrikkotua May 25, 2023
3cea5e6
change hover color for verify webhook button
garrrikkotua May 25, 2023
521acf9
fix eearly error state for api key field
garrrikkotua May 25, 2023
fa113ff
add links to activities
garrrikkotua May 25, 2023
be0a1ce
lint and format
garrrikkotua May 26, 2023
03834e2
ts ignore
garrrikkotua May 26, 2023
7a7415c
format
garrrikkotua May 26, 2023
686a3fb
Merge branch 'main' into feature/discourse-integration
garrrikkotua May 26, 2023
2aaafe7
minor frontend cleanup
garrrikkotua May 29, 2023
7ceca3b
backend cleanup
garrrikkotua May 29, 2023
58a04e4
button style & svg icon
garrrikkotua May 29, 2023
a257472
add typing for count
garrrikkotua May 29, 2023
a0ee994
Merge branch 'main' into feature/discourse-integration
garrrikkotua May 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Permissions from '../../../security/permissions'
import IntegrationService from '../../../services/integrationService'
import PermissionChecker from '../../../services/user/permissionChecker'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
const payload = await new IntegrationService(req).discourseConnectOrUpdate(req.body)
await req.responseHandler.success(req, res, payload)
}
15 changes: 15 additions & 0 deletions backend/src/api/integration/helpers/discourseTestWebhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Permissions from '../../../security/permissions'
import PermissionChecker from '../../../services/user/permissionChecker'
import IncomingWebhookRepository from '../../../database/repositories/incomingWebhookRepository'
import SequelizeRepository from '../../../database/repositories/sequelizeRepository'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)

const options = await SequelizeRepository.getDefaultIRepositoryOptions()
const repo = new IncomingWebhookRepository(options)

const isWebhooksReceived = await repo.checkWebhooksExistForIntegration(req.body.integrationId)

await req.responseHandler.success(req, res, { isWebhooksReceived })
}
30 changes: 30 additions & 0 deletions backend/src/api/integration/helpers/discourseValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from 'axios'
import Error400 from '../../../errors/Error400'
import Permissions from '../../../security/permissions'
import PermissionChecker from '../../../services/user/permissionChecker'

export default async (req, res) => {
new PermissionChecker(req).validateHasAny([
Permissions.values.integrationCreate,
Permissions.values.integrationEdit,
])

const { apiKey, apiUsername, forumHostname } = req.body

if (apiKey && apiUsername && forumHostname) {
try {
const result = await axios.get(`${forumHostname}/admin/users/list/active.json`, {
headers: {
'Api-Key': apiKey,
'Api-Username': apiUsername,
},
})
if (result.status === 200 && result.data && result.data.length > 0) {
return req.responseHandler.success(req, res, result.data)
}
} catch (e) {
return req.responseHandler.error(req, res, new Error400(req.language))
}
}
return req.responseHandler.error(req, res, new Error400(req.language))
}
15 changes: 15 additions & 0 deletions backend/src/api/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export default (app) => {
safeWrap(require('./helpers/stackOverflowVolume').default),
)

app.post(
'/tenant/:tenantId/discourse-connect',
safeWrap(require('./helpers/discourseCreateOrUpdate').default),
)

app.post(
'/tenant/:tenantId/discourse-validate',
safeWrap(require('./helpers/discourseValidator').default),
)

app.post(
'/tenant/:tenantId/discourse-test-webhook',
safeWrap(require('./helpers/discourseTestWebhook').default),
)

if (TWITTER_CONFIG.clientId) {
/**
* Using the passport.authenticate this endpoint forces a
Expand Down
89 changes: 89 additions & 0 deletions backend/src/api/webhooks/discourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import IntegrationRepository from '../../database/repositories/integrationRepository'
import TenantRepository from '../../database/repositories/tenantRepository'
import SequelizeRepository from '../../database/repositories/sequelizeRepository'
import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository'
import { WebhookType } from '../../types/webhooks'
import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS'
import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage'
import { verifyWebhookSignature } from '../../utils/crypto'
import { PlatformType } from '../../types/integrationEnums'

export default async (req, res) => {
const signature = req.headers['x-discourse-event-signature']
const eventId = req.headers['x-discourse-event-id']
const eventType = req.headers['x-discourse-event-type']
const event = req.headers['x-discourse-event']
const data = req.body

const options = await SequelizeRepository.getDefaultIRepositoryOptions()
const tenant = await TenantRepository.findById(req.params.tenantId, options)
const optionsWithTenant = await SequelizeRepository.getDefaultIRepositoryOptions(null, tenant)
const integration = (await IntegrationRepository.findByPlatform(
PlatformType.DISCOURSE,
optionsWithTenant,
)) as any

if (integration) {
try {
if (!signature) {
req.log.error({ signature }, 'Discourse Webhook signature header missing!')
await req.responseHandler.success(
req,
res,
'Discourse Webhook signature header missing!',
200,
)
return
}

if (
!verifyWebhookSignature(JSON.stringify(data), integration.settings.webhookSecret, signature)
) {
req.log.error({ signature }, 'Discourse Webhook signature verification failed!')
await req.responseHandler.success(
req,
res,
'Discourse Webhook signature verification failed!',
200,
)
return
}
} catch (error) {
req.log.error({ signature, error }, 'Internal error when verifying discourse webhook')
await req.responseHandler.success(
req,
res,
'Internal error when verifying discourse webhook',
200,
)
return
}

req.log.info({ integrationId: integration.id }, 'Incoming Discourse Webhook!')
const options = await SequelizeRepository.getDefaultIRepositoryOptions()
const repo = new IncomingWebhookRepository(options)

const result = await repo.create({
tenantId: integration.tenantId,
integrationId: integration.id,
type: WebhookType.DISCOURSE,
payload: {
signature,
eventId,
eventType,
event,
data,
},
})

await sendNodeWorkerMessage(
integration.tenantId,
new NodeWorkerProcessWebhookMessage(integration.tenantId, result.id),
)

await req.responseHandler.success(req, res, {}, 204)
} else {
req.log.error({ tenant }, 'No integration found for incoming Discourse Webhook!')
await req.responseHandler.success(req, res, {}, 200)
}
}
1 change: 1 addition & 0 deletions backend/src/api/webhooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export default (app) => {
app.post(`/github`, safeWrap(require('./github').default))
app.post(`/stripe`, safeWrap(require('./stripe').default))
app.post(`/sendgrid`, safeWrap(require('./sendgrid').default))
app.post(`/discourse/:tenantId`, safeWrap(require('./discourse').default))
}
27 changes: 27 additions & 0 deletions backend/src/database/attributes/member/discourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Attribute } from '../attribute'
import { AttributeType } from '../types'
import { MemberAttributes, MemberAttributeName } from './enums'

export const DiscourseMemberAttributes: Attribute[] = [
{
name: MemberAttributes[MemberAttributeName.URL].name,
label: MemberAttributes[MemberAttributeName.URL].label,
type: AttributeType.URL,
canDelete: false,
show: true,
},
{
name: MemberAttributes[MemberAttributeName.BIO].name,
label: MemberAttributes[MemberAttributeName.BIO].label,
type: AttributeType.STRING,
canDelete: false,
show: true,
},
{
name: MemberAttributes[MemberAttributeName.AVATAR_URL].name,
label: MemberAttributes[MemberAttributeName.AVATAR_URL].label,
type: AttributeType.URL,
canDelete: false,
show: false,
},
]
26 changes: 26 additions & 0 deletions backend/src/database/repositories/incomingWebhookRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,30 @@ export default class IncomingWebhookRepository extends RepositoryBase<
type: QueryTypes.DELETE,
})
}

async checkWebhooksExistForIntegration(integrationId: string): Promise<boolean> {
interface QueryResult {
count: number
}

const transaction = this.transaction

const results: QueryResult[] = await this.seq.query(
`
select count(*)::int as count
from "incomingWebhooks"
where "integrationId" = :integrationId
limit 1
`,
{
replacements: {
integrationId,
},
type: QueryTypes.SELECT,
transaction,
},
)

return results.length > 0 && results[0].count > 0
}
}
1 change: 1 addition & 0 deletions backend/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ const en = {
discord: 'Discord',
slack: 'Slack',
hackernews: 'Hacker News',
discourse: 'Discourse',
},
},
automation: {
Expand Down
5 changes: 5 additions & 0 deletions backend/src/serverless/integrations/grid/discordGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export class DiscordGrid {
score: 6,
isContribution: true,
}

static topic: gridEntry = {
score: 8,
isContribution: true,
}
}
23 changes: 23 additions & 0 deletions backend/src/serverless/integrations/grid/discourseGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { gridEntry } from './grid'

export class DiscourseGrid {
static create_topic: gridEntry = {
score: 8,
isContribution: true,
}

static message_in_topic: gridEntry = {
score: 6,
isContribution: true,
}

static join: gridEntry = {
score: 3,
isContribution: false,
}

static like: gridEntry = {
score: 1,
isContribution: false,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SlackIntegrationService } from './integrations/slackIntegrationService'
import { StackOverlflowIntegrationService } from './integrations/stackOverflowIntegrationService'
import { TwitterIntegrationService } from './integrations/twitterIntegrationService'
import { TwitterReachIntegrationService } from './integrations/twitterReachIntegrationService'
import { DiscourseIntegrationService } from './integrations/discourseIntegrationService'
import { IntegrationServiceBase } from './integrationServiceBase'
import { IntegrationTickProcessor } from './integrationTickProcessor'
import { WebhookProcessor } from './webhookProcessor'
Expand All @@ -45,6 +46,7 @@ export class IntegrationProcessor extends LoggingBase {
new SlackIntegrationService(),
new GithubIntegrationService(),
new StackOverlflowIntegrationService(),
new DiscourseIntegrationService(),
]

// add premium integrations
Expand Down
Loading