Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/217 twitter api endpoints #220

Merged
merged 11 commits into from
Sep 22, 2023
380 changes: 380 additions & 0 deletions __tests__/integration/twitter.test.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@notionhq/client": "^2.2.3",
"@sentry/node": "^7.50.0",
"@togethercrew.dev/db": "^2.4.99",
"@togethercrew.dev/tc-messagebroker": "^0.0.40",
"@togethercrew.dev/tc-messagebroker": "^0.0.42",
"@types/express-session": "^1.17.7",
"@types/morgan": "^1.9.5",
"babel-jest": "^29.3.1",
Expand Down
142 changes: 139 additions & 3 deletions src/controllers/twitter.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Response } from 'express';
import { userService, } from '../services';
import { guildService, userService, } from '../services';
import { IAuthRequest } from '../interfaces/request.interface';
import { catchAsync } from "../utils";
import { ApiError, catchAsync } from "../utils";
import httpStatus from 'http-status';
import { tokenTypes } from '../config/tokens';
import { Token } from '@togethercrew.dev/db';
import twitterService from '../services/twitter.service';

const disconnectTwitter = catchAsync(async function (req: IAuthRequest, res: Response) {
const user = req.user;
Expand All @@ -18,7 +19,142 @@ const disconnectTwitter = catchAsync(async function (req: IAuthRequest, res: Res
res.status(httpStatus.NO_CONTENT).send();
});

const refreshTwitter = catchAsync(async function (req: IAuthRequest, res: Response) {
const { twitter_username } = req.body;
const discordId = req.user.discordId

const guild = await guildService.getGuild({ user: discordId });
if (!guild) {
throw new ApiError(440, 'Oops, something went wrong! Could you please try logging in');
}

twitterService.twitterRefresh(twitter_username, { discordId, guildId: guild.guildId })
res.status(httpStatus.NO_CONTENT).send();
})

type TwitterActivityResponse = {
posts: number | null
replies: number | null
retweets: number | null
likes: number | null
mentions: number | null
}
const activityMetrics = catchAsync(async function (req: IAuthRequest, res: Response<TwitterActivityResponse>) {
const userId = req.params.twitterId

// TODO: also we can make it in a way that all below functions run in parallel
const postNumber = await twitterService.getUserPostNumber(userId)
const replyNumber = await twitterService.getUserReplyNumber(userId)
const retweetNumber = await twitterService.getUserRetweetNumber(userId)
const likeNumber = await twitterService.getUserLikeNumber(userId)
const mentionNumber = await twitterService.getUserMentionNumber(userId)
const activityMetrics = {
posts: postNumber,
replies: replyNumber,
retweets: retweetNumber,
likes: likeNumber,
mentions: mentionNumber
}
res.send(activityMetrics)
});

type TwitterAudienceResponse = {
replies: number | null
retweets: number | null
likes: number | null
mentions: number | null
}
const audienceMetrics = catchAsync(async function (req: IAuthRequest, res: Response<TwitterAudienceResponse>) {
const userId = req.params.twitterId

// TODO: also we can make it in a way that all below functions run in parallel
const replyNumber = await twitterService.getAudienceReplyNumber(userId)
const retweetNumber = await twitterService.getAudienceRetweetNumber(userId)
const likeNumber = await twitterService.getAudienceLikeNumber(userId)
const mentionNumber = await twitterService.getAudienceMentionNumber(userId)
const audienceMetrics = {
replies: replyNumber,
retweets: retweetNumber,
likes: likeNumber,
mentions: mentionNumber,
}
res.send(audienceMetrics)
});

type TwitterEngagementResponse = {
hqla: number
hqhe: number
lqla: number
lqhe: number
}
const engagementMetrics = catchAsync(async function (req: IAuthRequest, res: Response<TwitterEngagementResponse>) {
const userId = req.params.twitterId
let hqla = 0
let hqhe = 0
let lqla = 0
let lqhe = 0

const repliesInteraction = await twitterService.getRepliesInteraction(userId)
const quotesInteraction = await twitterService.getQuotesInteraction(userId)
const mentionsInteraction = await twitterService.getMentionsInteraction(userId)

const retweetsInteraction = await twitterService.getRetweetsInteraction(userId)
const likesInteraction = await twitterService.getLikesInteraction(userId)

const repliesInteractionUsers = repliesInteraction.map(ri => ri.userId)
const quotesInteractionUsers = quotesInteraction.map(qi => qi.userId)
const mentionsInteractionUsers = mentionsInteraction.map(mi => mi.userId)

const retweetsInteractionUsers = retweetsInteraction.map(ri => ri.userId)
const likesInteractionUsers = likesInteraction.map(li => li.userId)


// calculate `hqla` and `hqhe`
const replyQuoteMentionUsers = new Set([...repliesInteractionUsers, ...quotesInteractionUsers, ...mentionsInteractionUsers])
for(const userId of Array.from(replyQuoteMentionUsers)){
const replyInteraction = repliesInteraction.find(ri => ri.userId == userId)
const quoteInteraction = quotesInteraction.find(qi => qi.userId == userId)
const mentionInteraction = mentionsInteraction.find(mi => mi.userId == userId)

const replyInteractionNumber = replyInteraction?.replyCount || 0
const quoteInteractionNumber = quoteInteraction?.quoteCount || 0
const mentionInteractionNumber = mentionInteraction?.mentionCount || 0

if(replyInteractionNumber + quoteInteractionNumber + mentionInteractionNumber < 3)
hqla++
else
hqhe++
}

// calculate `lqla` and `lqhe`
const retweetLikeUsers = new Set([...retweetsInteractionUsers, ...likesInteractionUsers])
for(const userId of Array.from(retweetLikeUsers)){
const retweetInteraction = retweetsInteraction.find(ri => ri.userId == userId)
const likeInteraction = likesInteraction.find(li => li.userId == userId)

const retweetInteractionNumber = retweetInteraction?.retweetCount || 0
const likeInteractionNumber = likeInteraction?.likeCount || 0

if(retweetInteractionNumber + likeInteractionNumber < 3)
lqla++
else
lqhe++
}

const engagementMetrics = {
hqla: hqla,
hqhe: hqhe,
lqla: lqla,
lqhe: lqhe,
}
res.send(engagementMetrics)
});

export default {
disconnectTwitter
disconnectTwitter,
refreshTwitter,
activityMetrics,
audienceMetrics,
engagementMetrics
}

131 changes: 129 additions & 2 deletions src/docs/twitter.doc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
paths:
paths:
/api/v1/twitter/disconnect:
post:
tags:
Expand All @@ -14,8 +14,135 @@ paths:
"401":
description: Unauthorized
$ref: "#/components/responses/Unauthorized"

/api/v1/twitter/metrics/refresh:
post:
tags:
- [Twitter]
summary: Call Twitter Refresh saga
description: after calling this end-point a new TwitterRefresh saga will be called and new information will be received
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
twitter_username:
type: string
required: true
example: katerinabohlec
response:
"401":
description: Unauthorized
$ref: "#/components/responses/Unauthorized"

/api/v1/twitter/{twitterId}/metrics/activity:
get:
tags:
- [Twitter]
summary: get twitter Activity Metrics
description: get twitter Activity Metrics
security:
- bearerAuth: []
parameters:
- in: path
name: twitterId
required: true
schema:
type: string
description: Twitter ID
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
posts:
type: number
replies:
type: number
retweets:
type: number
likes:
type: number
mentions:
type: number
"401":
description: Unauthorized
$ref: "#/components/responses/Unauthorized"

/api/v1/twitter/{twitterId}/metrics/audience:
get:
tags:
- [Twitter]
summary: get twitter Audience Metrics
description: get twitter Audience Metrics
security:
- bearerAuth: []
parameters:
- in: path
name: twitterId
required: true
schema:
type: string
description: Twitter ID
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
replies:
type: number
retweets:
type: number
likes:
type: number
mentions:
type: number
"401":
description: Unauthorized
$ref: "#/components/responses/Unauthorized"


/api/v1/twitter/{twitterId}/metrics/engagement:
get:
tags:
- [Twitter]
summary: get twitter Engagement Metrics
description: get twitter Engagement Metrics
security:
- bearerAuth: []
parameters:
- in: path
name: twitterId
required: true
schema:
type: string
description: Twitter ID
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
hqla:
type: number
hqhe:
type: number
lqla:
type: number
lqhe:
type: number
"401":
description: Unauthorized
$ref: "#/components/responses/Unauthorized"

7 changes: 6 additions & 1 deletion src/routes/v1/twitter.route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import express from "express";
import { twitterController } from "../../controllers";
import twitterValidation from '../../validations/twitter.validation';

import { auth } from '../../middlewares';
import { validate, auth } from '../../middlewares';
const router = express.Router();

// Router
router.post('/disconnect', auth(), twitterController.disconnectTwitter);
router.post('/metrics/refresh', auth(), validate(twitterValidation.refreshTweet), twitterController.refreshTwitter);
router.get('/:twitterId/metrics/activity', auth(), twitterController.activityMetrics);
router.get('/:twitterId/metrics/audience', auth(), twitterController.audienceMetrics);
router.get('/:twitterId/metrics/engagement', auth(), twitterController.engagementMetrics);


export default router;
Expand Down
15 changes: 14 additions & 1 deletion src/services/saga.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,20 @@ async function createAndStartFetchMemberSaga(guildId: Snowflake) {
await saga.start(() => { })
}

async function createAndStartRefreshTwitterSaga(twitter_username:string, other: { discordId: Snowflake, guildId: string, message: string }) {
const saga = await MBConnection.models.Saga.create({
status: Status.NOT_STARTED,
data: { twitter_username, ...other },
choreography: ChoreographyDict.TWITTER_REFRESH
})

// eslint-disable-next-line @typescript-eslint/no-empty-function
await saga.start(() => { })
return saga
}

export default {
createAndStartGuildSaga,
createAndStartFetchMemberSaga
createAndStartFetchMemberSaga,
createAndStartRefreshTwitterSaga
}
Loading