Skip to content

Commit c3c69b1

Browse files
themaroltJoan Reyeroepipav
authored
DEV.to integration (#11)
Co-authored-by: Joan Reyero <[email protected]> Co-authored-by: anilb <[email protected]>
1 parent 3882dd1 commit c3c69b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2152
-269
lines changed

backend/package-lock.json

Lines changed: 478 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"express-rate-limit": "6.5.1",
4949
"formidable-serverless": "1.1.1",
5050
"helmet": "4.1.1",
51+
"html-to-text": "^8.2.1",
5152
"jsonwebtoken": "8.5.1",
5253
"lodash": "4.17.21",
5354
"meilisearch": "^0.26.0",
@@ -64,6 +65,7 @@
6465
"passport-slack": "0.0.7",
6566
"pg": "^8.7.3",
6667
"pm2": "^5.2.0",
68+
"sanitize-html": "^2.7.1",
6769
"sequelize": "6.21.2",
6870
"sequelize-cli-typescript": "^3.2.0-c",
6971
"stripe": "10.0.0",
@@ -75,8 +77,10 @@
7577
"devDependencies": {
7678
"@babel/plugin-transform-runtime": "^7.18.10",
7779
"@babel/preset-env": "^7.16.10",
80+
"@types/html-to-text": "^8.1.1",
7881
"@types/jest": "^27.4.0",
7982
"@types/node": "^17.0.21",
83+
"@types/sanitize-html": "^2.6.2",
8084
"@typescript-eslint/eslint-plugin": "^5.17.0",
8185
"@typescript-eslint/parser": "^5.17.0",
8286
"copyfiles": "2.4.1",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import PermissionChecker from '../../../services/user/permissionChecker'
2+
import ApiResponseHandler from '../../apiResponseHandler'
3+
import Permissions from '../../../security/permissions'
4+
import IntegrationService from '../../../services/integrationService'
5+
6+
export default async (req, res) => {
7+
try {
8+
new PermissionChecker(req).validateHas(Permissions.values.tenantEdit)
9+
const payload = await new IntegrationService(req).devtoConnectOrUpdate(req.body)
10+
await ApiResponseHandler.success(req, res, payload)
11+
} catch (error) {
12+
await ApiResponseHandler.error(req, res, error)
13+
}
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import PermissionChecker from '../../../services/user/permissionChecker'
2+
import Permissions from '../../../security/permissions'
3+
import ApiResponseHandler from '../../apiResponseHandler'
4+
import Error400 from '../../../errors/Error400'
5+
import { getUserByUsername } from '../../../serverless/integrations/usecases/devto/getUser'
6+
import { getOrganization } from '../../../serverless/integrations/usecases/devto/getOrganization'
7+
8+
export default async (req, res) => {
9+
try {
10+
new PermissionChecker(req).validateHasAny([
11+
Permissions.values.integrationCreate,
12+
Permissions.values.integrationEdit,
13+
])
14+
15+
if (req.query.username) {
16+
const result = await getUserByUsername(req.query.username)
17+
await ApiResponseHandler.success(req, res, result)
18+
} else if (req.query.organization) {
19+
const result = await getOrganization(req.query.organization)
20+
await ApiResponseHandler.success(req, res, result)
21+
} else {
22+
// throw bad request since we don't have either of the query params
23+
await ApiResponseHandler.error(req, res, new Error400(req.language))
24+
}
25+
} catch (error) {
26+
console.error(error)
27+
await ApiResponseHandler.error(req, res, error)
28+
}
29+
}

backend/src/api/integration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default (app) => {
2222
`/discord-authenticate/:tenantId/:guild_id`,
2323
require('./helpers/discordAuthenticate').default,
2424
)
25+
app.get('/tenant/:tenantId/devto-validate', require('./helpers/devtoValidators').default)
26+
app.post('/tenant/:tenantId/devto-connect', require('./helpers/devtoCreateOrUpdate').default)
2527

2628
if (getConfig().AUTH_SOCIAL_TWITTER_CLIENT_ID) {
2729
passport.use(getTwitterStrategy())

backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,59 @@ describe('eagleEyeContentRepository tests', () => {
477477

478478
await addAll(mockIRepositoryOptions)
479479

480+
const k1 = {
481+
sourceId: 'sourceIdk1',
482+
vectorId: 'sourceIdk1',
483+
status: null,
484+
platform: 'hacker_news',
485+
title: 'title',
486+
userAttributes: { github: 'hey', twitter: 'ho' },
487+
text: 'text',
488+
postAttributes: {
489+
score: 10,
490+
},
491+
url: 'url',
492+
timestamp: new Date(),
493+
username: 'username',
494+
keywords: ['keyword1'],
495+
similarityScore: 0.9,
496+
}
497+
498+
await new EagleEyeContentService(mockIRepositoryOptions).upsert(k1)
499+
500+
const k2 = {
501+
sourceId: 'sourceIdk2',
502+
vectorId: 'sourceIdk2',
503+
status: null,
504+
platform: 'hacker_news',
505+
title: 'title',
506+
userAttributes: { github: 'hey', twitter: 'ho' },
507+
text: 'text',
508+
postAttributes: {
509+
score: 10,
510+
},
511+
url: 'url',
512+
timestamp: new Date(),
513+
username: 'username',
514+
keywords: ['keyword2'],
515+
similarityScore: 0.9,
516+
}
517+
518+
try {
519+
await EagleEyeContentRepository.findAndCountAll(
520+
{
521+
filter: {
522+
keywords: 'keyword1,keyword2',
523+
},
524+
},
525+
mockIRepositoryOptions,
526+
)
527+
} catch (e) {
528+
console.log(e)
529+
}
530+
531+
await new EagleEyeContentService(mockIRepositoryOptions).upsert(k2)
532+
480533
expect(
481534
(
482535
await EagleEyeContentRepository.findAndCountAll(
@@ -488,7 +541,7 @@ describe('eagleEyeContentRepository tests', () => {
488541
mockIRepositoryOptions,
489542
)
490543
).count,
491-
).toBe(2)
544+
).toBe(5)
492545
})
493546
})
494547

backend/src/database/repositories/activityRepository.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sanitizeHtml from 'sanitize-html'
12
import lodash from 'lodash'
23
import Sequelize from 'sequelize'
34
import SequelizeRepository from './sequelizeRepository'
@@ -18,6 +19,14 @@ class ActivityRepository {
1819

1920
const transaction = SequelizeRepository.getTransaction(options)
2021

22+
if (data.crowdInfo && data.crowdInfo.body) {
23+
data.crowdInfo.body = sanitizeHtml(data.crowdInfo.body).trim()
24+
}
25+
26+
if (data.crowdInfo && data.crowdInfo.title) {
27+
data.crowdInfo.title = sanitizeHtml(data.crowdInfo.title).trim()
28+
}
29+
2130
const record = await options.database.activity.create(
2231
{
2332
...lodash.pick(data, [
@@ -68,6 +77,14 @@ class ActivityRepository {
6877
throw new Error404()
6978
}
7079

80+
if (data.crowdInfo && data.crowdInfo.body) {
81+
data.crowdInfo.body = sanitizeHtml(data.crowdInfo.body).trim()
82+
}
83+
84+
if (data.crowdInfo && data.crowdInfo.title) {
85+
data.crowdInfo.title = sanitizeHtml(data.crowdInfo.title).trim()
86+
}
87+
7188
record = await record.update(
7289
{
7390
...lodash.pick(data, [

backend/src/database/repositories/eagleEyeContentRepository.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,10 @@ export default class EagleEyeContentRepository {
211211
}
212212

213213
if (filter.keywords) {
214+
// Overlap will take a post where any keyword matches any of the filter keywords
214215
whereAnd.push({
215216
keywords: {
216-
[Op.contains]: filter.keywords.split(','),
217+
[Op.overlap]: filter.keywords.split(','),
217218
},
218219
})
219220
}

backend/src/serverless/integrations/iterators/__tests__/devtoIterator.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,14 @@ describe('Dev.to iterator tests', () => {
145145
expect(activity.isKeyAction).toEqual(DevtoGrid.comment.isKeyAction)
146146

147147
const activityCrowdInfo = activity.crowdInfo as any
148-
expect(activityCrowdInfo.bodyHtml).toEqual('Hello world!')
148+
expect(activityCrowdInfo.body).toEqual('Hello world!')
149149
expect(activityCrowdInfo.userUrl).toEqual('https://dev.to/johndoe')
150-
expect(activityCrowdInfo.commentUrl).toEqual('https://dev.to/johndoe/comment/123')
150+
expect(activityCrowdInfo.url).toEqual('https://dev.to/johndoe/comment/123')
151151
expect(activityCrowdInfo.articleUrl).toEqual(article.url)
152+
expect(activityCrowdInfo.articleTitle).toEqual(article.title)
152153

153154
const communityMember = activity.communityMember
154155
expect(communityMember.username[PlatformType.DEVTO]).toEqual('johndoe')
155-
expect(communityMember.crowdInfo[PlatformType.DEVTO].id).toEqual(123)
156156
expect(communityMember.bio).toEqual('Nice profile you got there')
157157
expect(communityMember.location).toEqual('Venice, Italy')
158158
expect(communityMember.crowdInfo[PlatformType.TWITTER].url).toEqual(

backend/src/serverless/integrations/iterators/devtoIterator.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import lodash from 'lodash'
2+
import sanitizeHtml from 'sanitize-html'
23
import { DevtoArticle, DevtoComment, DevtoUser } from '../usecases/devto/types'
34
import { single } from '../../../utils/arrays'
4-
import { getUser } from '../usecases/devto/getUser'
5+
import { getUserById } from '../usecases/devto/getUser'
56
import IntegrationRepository from '../../../database/repositories/integrationRepository'
67
import { IRepositoryOptions } from '../../../database/repositories/IRepositoryOptions'
78
import Operations from '../../dbOperations/operations'
@@ -96,8 +97,10 @@ export default class DevtoIterator extends BaseIterator {
9697
// does not contain all the user information we can get full profile and set it to comments
9798
const userIds: number[] = DevtoIterator.getUserIdsFromComments(comments)
9899
for (const userId of userIds) {
99-
const fullUser = await getUser(userId)
100-
DevtoIterator.setFullUser(comments, fullUser)
100+
const fullUser = await getUserById(userId)
101+
if (fullUser !== null) {
102+
DevtoIterator.setFullUser(comments, fullUser)
103+
}
101104
}
102105

103106
return {
@@ -195,30 +198,33 @@ export default class DevtoIterator extends BaseIterator {
195198

196199
const communityMember: CommunityMember = {
197200
username: {
198-
[PlatformType.DEVTO]: comment.fullUser.username,
201+
[PlatformType.DEVTO]: comment.user.username,
199202
},
200203
crowdInfo: {
201204
[PlatformType.DEVTO]: {
202-
id: comment.fullUser.id,
205+
url: `https://dev.to/${encodeURIComponent(comment.fullUser.username)}`,
203206
},
204207
},
205-
bio: comment.fullUser?.summary || '',
206-
location: comment.fullUser?.location || '',
207208
}
208209

209-
if (comment.fullUser.twitter_username) {
210+
if (comment.user.twitter_username) {
210211
communityMember.crowdInfo.twitter = {
211-
url: `https://twitter.com/${comment.fullUser.twitter_username}`,
212+
url: `https://twitter.com/${comment.user.twitter_username}`,
212213
}
213-
communityMember.username.twitter = comment.fullUser.twitter_username
214+
communityMember.username.twitter = comment.user.twitter_username
214215
}
215216

216-
if (comment.fullUser.github_username) {
217+
if (comment.user.github_username) {
217218
communityMember.crowdInfo.github = {
218-
name: comment.fullUser.name,
219-
url: `https://github.com/${comment.fullUser.github_username}`,
219+
name: comment.user.name,
220+
url: `https://github.com/${comment.user.github_username}`,
220221
}
221-
communityMember.username.github = comment.fullUser.github_username
222+
communityMember.username.github = comment.user.github_username
223+
}
224+
225+
if (comment.fullUser) {
226+
communityMember.bio = comment.fullUser?.summary || ''
227+
communityMember.location = comment.fullUser?.location || ''
222228
}
223229

224230
activities.push({
@@ -229,12 +235,15 @@ export default class DevtoIterator extends BaseIterator {
229235
sourceId: comment.id_code,
230236
sourceParentId: parentCommentId,
231237
crowdInfo: {
232-
bodyHtml: comment.body_html,
233-
userUrl: `https://dev.to/${encodeURIComponent(comment.fullUser.username)}`,
234-
commentUrl: `https://dev.to/${encodeURIComponent(comment.fullUser.username)}/comment/${
238+
body: sanitizeHtml(comment.body_html),
239+
url: `https://dev.to/${encodeURIComponent(comment.fullUser.username)}/comment/${
235240
comment.id_code
236241
}`,
242+
thread: parentCommentId !== undefined,
243+
244+
userUrl: `https://dev.to/${encodeURIComponent(comment.fullUser.username)}`,
237245
articleUrl: article.url,
246+
articleTitle: article.title,
238247
},
239248
communityMember,
240249

0 commit comments

Comments
 (0)