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

[2] Convert webhook handlers to Typescript #365

Merged
merged 12 commits into from
Oct 18, 2023
Merged
397 changes: 73 additions & 324 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"@storybook/addon-links": "^6.2.9",
"@storybook/addon-svelte-csf": "^2.0.11",
"@storybook/svelte": "^6.2.9",
"@total-typescript/ts-reset": "^0.5.1",
"@types/jest": "^24.9.1",
"@types/koa-router": "^7.4.4",
"@types/string-similarity": "^4.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/database/models/userArticleLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class UserArticleLink extends Base {
*
* @param {string} userId
* @param {string} articleId
* @param {object} data
* @param {object} [data]
* @returns {Promise<UserArticleLink>}
*/
static async createOrUpdateByUserIdAndArticleId(userId, articleId, data) {
Expand Down
62 changes: 38 additions & 24 deletions src/graphql/cofacts-api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Query {
"""
GetUser(id: String, slug: String): User
GetCategory(id: String): Category
GetYdoc(id: String!): Ydoc
ListArticles(
filter: ListArticleFilter
orderBy: [ListArticleOrderBy]
Expand Down Expand Up @@ -176,13 +177,13 @@ type Query {
type Article implements Node {
id: ID!
text: String
createdAt: String
createdAt: String!
updatedAt: String
status: ReplyRequestStatusEnum!
references: [ArticleReference]

"""Number of normal article replies"""
replyCount: Int
replyCount: Int!

"""
Connections between this article and replies. Sorted by the logic described in https://github.com/cofacts/rumors-line-bot/issues/78.
Expand All @@ -207,7 +208,7 @@ type Article implements Node {

"""Only list the articleReplies created by the currently logged in user"""
selfOnly: Boolean
): [ArticleReply]
): [ArticleReply!]!

"""
Automated reply from AI before human fact checkers compose an fact check
Expand All @@ -224,10 +225,10 @@ type Article implements Node {

"""Returns only article categories with the specified statuses"""
statuses: [ArticleCategoryStatusEnum!] = [NORMAL]
): [ArticleCategory]
): [ArticleCategory!]!

"""Number of normal article categories"""
categoryCount: Int
categoryCount: Int!
replyRequests(
"""Returns only article replies with the specified statuses"""
statuses: [ReplyRequestStatusEnum!] = [NORMAL]
Expand Down Expand Up @@ -258,7 +259,7 @@ type Article implements Node {
Specify a cursor, returns results before this cursor. cannot be used with "after".
"""
before: String
): ArticleConnection
): ArticleConnection!

"""Hyperlinks in article text"""
hyperlinks: [Hyperlink]
Expand Down Expand Up @@ -293,8 +294,8 @@ enum ReplyRequestStatusEnum {
}

type ArticleReference {
createdAt: String
type: ArticleReferenceTypeEnum
createdAt: String!
type: ArticleReferenceTypeEnum!
permalink: String
}

Expand All @@ -311,33 +312,33 @@ enum ArticleReferenceTypeEnum {

"""The linkage between an Article and a Reply"""
type ArticleReply {
replyId: String
replyId: String!
reply: Reply

"""Cached reply type value stored in ArticleReply"""
replyType: ReplyTypeEnum
articleId: String
articleId: String!
article: Article

"""The user who conencted this reply and this article."""
user: User
userId: String!
appId: String!
canUpdateStatus: Boolean
feedbackCount: Int
positiveFeedbackCount: Int
negativeFeedbackCount: Int
canUpdateStatus: Boolean!
feedbackCount: Int!
positiveFeedbackCount: Int!
negativeFeedbackCount: Int!
feedbacks(
"""Returns only aricle reply feedbacks with the specified statuses"""
statuses: [ArticleReplyFeedbackStatusEnum!] = [NORMAL]
): [ArticleReplyFeedback]
): [ArticleReplyFeedback!]!

"""
The feedback of current user. null when not logged in or not voted yet.
"""
ownVote: FeedbackVote
status: ArticleReplyStatusEnum
createdAt: String
status: ArticleReplyStatusEnum!
createdAt: String!
updatedAt: String
}

Expand All @@ -346,17 +347,17 @@ type Reply implements Node {

"""The user submitted this reply version"""
user: User
createdAt: String
createdAt: String!
text: String
type: ReplyTypeEnum
type: ReplyTypeEnum!
reference: String
articleReplies(
"""Deprecated. Please use statuses instead."""
status: ArticleReplyStatusEnum

"""Returns only article replies with the specified statuses"""
statuses: [ArticleReplyStatusEnum!] = [NORMAL]
): [ArticleReply]
): [ArticleReply!]!

"""
Hyperlinks in reply text or reference. May be empty array if no URLs are included. `null` when hyperlinks are still fetching.
Expand All @@ -379,7 +380,7 @@ type Reply implements Node {
Specify a cursor, returns results before this cursor. cannot be used with "after".
"""
before: String
): ReplyConnection
): ReplyConnection!
}

type User implements Node {
Expand Down Expand Up @@ -979,6 +980,21 @@ type Cooccurrence implements Node {
updatedAt: String!
}

type Ydoc {
"""Binary that stores as base64 encoded string"""
data: String

"""Ydoc snapshots which are used to restore to specific version"""
versions: [YdocVersion]
}

type YdocVersion {
createdAt: String

"""Binary that stores as base64 encoded string"""
snapshot: String
}

input ListArticleFilter {
"""Show only articles created by a specific app."""
appId: String
Expand Down Expand Up @@ -1557,9 +1573,7 @@ type Mutation {
articleType: ArticleTypeEnum!
reference: ArticleReferenceInput!

"""
The reason why the user want to submit this article. Mandatory for 1st sender
"""
"""The reason why the user want to submit this article"""
reason: String
): MutationResult

Expand Down
3 changes: 3 additions & 0 deletions src/types/chatbotState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export type Context = {
/** User selected article in DB */
selectedArticleId?: string;
selectedArticleText?: string;

/** FIXME: Probably not required now */
selectedReplyId?: string;
};

export type ChatbotStateHandlerParams = {
Expand Down
1 change: 1 addition & 0 deletions src/types/reset.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@total-typescript/ts-reset';
1 change: 0 additions & 1 deletion src/webhook/handlers/__tests__/askingArticleSource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ it('returns instructions if user did not forward the whole message', async () =>
"type": "span",
},
],
"text": "Instructions",
"type": "text",
"wrap": true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
} from './utils';

import { TUTORIAL_STEPS } from './tutorial';
import { ChatbotStateHandler } from 'src/types/chatbotState';

export default async function askingArticleSource(params) {
let { data, state, event, userId, replies } = params;
const askingArticleSource: ChatbotStateHandler = async (params) => {
const { data, state, event, userId } = params;
let { replies } = params;

const visitor = ga(userId, state, data.searchedText);

Expand All @@ -25,7 +27,7 @@ export default async function askingArticleSource(params) {
case POSTBACK_NO:
replies = [
createTextMessage({
text: t`Instructions`,
altText: t`Instructions`,
contents: [
{
type: 'span',
Expand Down Expand Up @@ -171,4 +173,6 @@ export default async function askingArticleSource(params) {
visitor.send();

return { data, event, userId, replies };
}
};

export default askingArticleSource;
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { t } from 'ttag';
import { Message } from '@line/bot-sdk';

import { ChatbotStateHandler } from 'src/types/chatbotState';
import ga from 'src/lib/ga';
import gql from 'src/lib/gql';
import { getArticleURL } from 'src/lib/sharedUtils';
Expand All @@ -15,9 +18,21 @@ import {
} from './utils';
import UserSettings from 'src/database/models/userSettings';
import UserArticleLink from 'src/database/models/userArticleLink';
import {
ArticleTypeEnum,
SubmitMediaArticleUnderConsentMutation,
SubmitMediaArticleUnderConsentMutationVariables,
SubmitTextArticleUnderConsentMutation,
SubmitTextArticleUnderConsentMutationVariables,
} from 'typegen/graphql';

function uppercase<T extends string>(s: T) {
return s.toUpperCase() as Uppercase<T>;
}

export default async function askingArticleSubmissionConsent(params) {
let { data, state, event, userId, replies } = params;
const askingArticleSubmissionConsent: ChatbotStateHandler = async (params) => {
const { data, event, userId } = params;
let { state, replies } = params;

const visitor = ga(userId, state, data.searchedText);

Expand All @@ -41,22 +56,43 @@ export default async function askingArticleSubmissionConsent(params) {
let article;
if (isTextArticle) {
const result = await gql`
mutation ($text: String!) {
mutation SubmitTextArticleUnderConsent($text: String!) {
CreateArticle(text: $text, reference: { type: LINE }) {
id
}
}
`({ text: data.searchedText }, { userId });
`<
SubmitTextArticleUnderConsentMutation,
SubmitTextArticleUnderConsentMutationVariables
>({ text: data.searchedText ?? '' }, { userId });
article = result.data.CreateArticle;
} else {
/* istanbul ignore if */
if (!data.messageId) {
// Should not be here
throw new Error('No message ID found, cannot submit message.');
}

const articleType: ArticleTypeEnum = (() => {
switch (data.messageType) {
case 'image':
case 'audio':
case 'video':
return uppercase(data.messageType);
default:
throw new Error(
`[askingArticleSubmissionConsent] unsupported message type ${data.messageType}`
);
}
})();

const proxyUrl = getLineContentProxyURL(data.messageId);

const result = await gql`
mutation ($mediaUrl: String!, $articleType: ArticleTypeEnum!) {
mutation SubmitMediaArticleUnderConsent(
$mediaUrl: String!
$articleType: ArticleTypeEnum!
) {
CreateMediaArticle(
mediaUrl: $mediaUrl
articleType: $articleType
Expand All @@ -65,13 +101,20 @@ export default async function askingArticleSubmissionConsent(params) {
id
}
}
`(
{ mediaUrl: proxyUrl, articleType: data.messageType.toUpperCase() },
{ userId }
);
`<
SubmitMediaArticleUnderConsentMutation,
SubmitMediaArticleUnderConsentMutationVariables
>({ mediaUrl: proxyUrl, articleType }, { userId });
article = result.data.CreateMediaArticle;
}

/* istanbul ignore if */
if (!article?.id) {
throw new Error(
'[askingArticleSubmissionConsent] article is not created successfully'
);
}

await UserArticleLink.createOrUpdateByUserIdAndArticleId(
userId,
article.id
Expand All @@ -86,7 +129,7 @@ export default async function askingArticleSubmissionConsent(params) {
userId
);

let maybeAIReplies = [
let maybeAIReplies: Message[] = [
createTextMessage({
text: t`In the meantime, you can:`,
}),
Expand Down Expand Up @@ -150,7 +193,7 @@ export default async function askingArticleSubmissionConsent(params) {
!allowNewReplyUpdate &&
createNotificationSettingsBubble(),
createArticleShareBubble(articleUrl),
].filter((m) => m),
].filter(Boolean),
},
},
];
Expand All @@ -160,4 +203,6 @@ export default async function askingArticleSubmissionConsent(params) {

visitor.send();
return { data, event, userId, replies };
}
};

export default askingArticleSubmissionConsent;
Loading
Loading