diff --git a/packages/rule-handler/src/index.ts b/packages/rule-handler/src/index.ts index ab2a3d92b..f2274cccf 100644 --- a/packages/rule-handler/src/index.ts +++ b/packages/rule-handler/src/index.ts @@ -22,8 +22,9 @@ export interface PubSubData { id: string userId: string type: EntityType - subscription?: string - image?: string + subscription: string + image: string + content: string } enum EntityType { @@ -114,7 +115,17 @@ export const ruleHandler = Sentry.GCPFunction.wrapHttpFunction( return } - await triggerActions(userId, rules, data, apiEndpoint, jwtSecret) + const triggeredActions = await triggerActions( + userId, + rules, + data, + apiEndpoint, + jwtSecret + ) + if (triggeredActions.length === 0) { + res.status(200).send('No Actions') + return + } res.status(200).send('OK') } catch (error) { diff --git a/packages/rule-handler/src/label.ts b/packages/rule-handler/src/label.ts index 948c193be..18f1549ec 100644 --- a/packages/rule-handler/src/label.ts +++ b/packages/rule-handler/src/label.ts @@ -1,15 +1,11 @@ import axios from 'axios' -import { getAuthToken } from './index' export const addLabels = async ( - userId: string, apiEndpoint: string, - jwtSecret: string, + auth: string, pageId: string, labelIds: string[] ) => { - const auth = await getAuthToken(userId, jwtSecret) - const data = JSON.stringify({ query: `mutation SetLabels($input: SetLabelsInput!) { setLabels(input: $input) { diff --git a/packages/rule-handler/src/notification.ts b/packages/rule-handler/src/notification.ts index 6a12a94a2..ed715209d 100644 --- a/packages/rule-handler/src/notification.ts +++ b/packages/rule-handler/src/notification.ts @@ -1,5 +1,4 @@ import axios from 'axios' -import { getAuthToken } from './index' interface NotificationData { body: string @@ -10,15 +9,12 @@ interface NotificationData { } export const sendNotification = async ( - userId: string, apiEndpoint: string, - jwtSecret: string, + auth: string, message: string, title?: string, image?: string ) => { - const auth = await getAuthToken(userId, jwtSecret) - const data: NotificationData = { body: message, title: title || message, diff --git a/packages/rule-handler/src/page.ts b/packages/rule-handler/src/page.ts new file mode 100644 index 000000000..27b99b230 --- /dev/null +++ b/packages/rule-handler/src/page.ts @@ -0,0 +1,78 @@ +import axios from 'axios' + +export const archivePage = async ( + apiEndpoint: string, + auth: string, + pageId: string +) => { + const data = JSON.stringify({ + query: `mutation SetLinkArchived($input: ArchiveLinkInput!) { + setLinkArchived(input: $input) { + ... on ArchiveLinkSuccess { + linkId + message + } + ... on ArchiveLinkError { + message + errorCodes + } + } + }`, + variables: { + input: { + linkId: pageId, + archived: true, + }, + }, + }) + + try { + await axios.post(`${apiEndpoint}/graphql`, data, { + headers: { + Cookie: `auth=${auth};`, + 'Content-Type': 'application/json', + }, + }) + } catch (e) { + console.error(e) + } +} + +export const markPageAsRead = async ( + apiEndpoint: string, + auth: string, + pageId: string +) => { + const data = JSON.stringify({ + query: `mutation SaveArticleReadingProgress($input: SaveArticleReadingProgressInput!) { + saveArticleReadingProgress(input: $input) { + ... on SaveArticleReadingProgressSuccess { + updatedArticle { + id + } + } + ... on SaveArticleReadingProgressError { + errorCodes + } + } + }`, + variables: { + input: { + id: pageId, + readingProgressPercent: 100, + readingProgressAnchorIndex: 0, + }, + }, + }) + + try { + await axios.post(`${apiEndpoint}/graphql`, data, { + headers: { + Cookie: `auth=${auth};`, + 'Content-Type': 'application/json', + }, + }) + } catch (e) { + console.error(e) + } +} diff --git a/packages/rule-handler/src/rule.ts b/packages/rule-handler/src/rule.ts index 7dc01c70b..df89c9039 100644 --- a/packages/rule-handler/src/rule.ts +++ b/packages/rule-handler/src/rule.ts @@ -3,6 +3,10 @@ import { getAuthToken, PubSubData } from './index' import axios from 'axios' import { parse, SearchParserKeyWordOffset } from 'search-query-parser' import { addLabels } from './label' +import { archivePage, markPageAsRead } from './page' +import { SearchFilter } from './search_filter' +import { SubscriptionFilter } from './search_filter/subscription_filter' +import { ContentFilter } from './search_filter/content_filter' export enum RuleActionType { AddLabel = 'ADD_LABEL', @@ -28,20 +32,16 @@ export interface Rule { updatedAt: Date } -interface SearchFilter { - subscriptionFilter?: string -} - -const parseSearchFilter = (filter: string): SearchFilter => { +const parseSearchFilter = (filter: string): SearchFilter[] => { const searchFilter = filter ? filter.replace(/\W\s":/g, '') : undefined - const result: SearchFilter = {} + const result: SearchFilter[] = [] if (!searchFilter || searchFilter === '*') { return result } const parsed = parse(searchFilter, { - keywords: ['subscription'], + keywords: ['subscription', 'content'], tokenize: true, }) if (parsed.offsets) { @@ -52,7 +52,11 @@ const parseSearchFilter = (filter: string): SearchFilter => { for (const keyword of keywords) { switch (keyword.keyword) { case 'subscription': - result.subscriptionFilter = keyword.value + keyword.value && result.push(new SubscriptionFilter(keyword.value)) + break + case 'content': + keyword.value && result.push(new ContentFilter(keyword.value)) + break } } } @@ -61,24 +65,14 @@ const parseSearchFilter = (filter: string): SearchFilter => { } const isValidData = (filter: string, data: PubSubData): boolean => { - const searchFilter = parseSearchFilter(filter) + const searchFilters = parseSearchFilter(filter) - if (searchFilter.subscriptionFilter) { - return isValidSubscription(searchFilter.subscriptionFilter, data) + if (searchFilters.length === 0) { + console.debug('no search filters found') + return true } - return true -} - -const isValidSubscription = ( - subscriptionFilter: string, - data: PubSubData -): boolean => { - if (!data.subscription) { - return false - } - - return subscriptionFilter === '*' || data.subscription === subscriptionFilter + return searchFilters.every((searchFilter) => searchFilter.isValid(data)) } export const getEnabledRules = async ( @@ -127,6 +121,9 @@ export const triggerActions = async ( apiEndpoint: string, jwtSecret: string ) => { + const triggeredActions: RuleAction[] = [] + const authToken = await getAuthToken(userId, jwtSecret) + for (const rule of rules) { if (!isValidData(rule.filter, data)) { continue @@ -139,23 +136,34 @@ export const triggerActions = async ( console.log('invalid data for add label action') continue } - await addLabels( - userId, - apiEndpoint, - jwtSecret, - data.id, - action.params - ) + await addLabels(apiEndpoint, authToken, data.id, action.params) + triggeredActions.push(action) break case RuleActionType.Archive: + if (!data.id) { + console.log('invalid data for archive action') + continue + } + await archivePage(apiEndpoint, authToken, data.id) + triggeredActions.push(action) + break case RuleActionType.MarkAsRead: - continue + if (!data.id) { + console.log('invalid data for mark as read action') + continue + } + await markPageAsRead(apiEndpoint, authToken, data.id) + triggeredActions.push(action) + break case RuleActionType.SendNotification: for (const message of action.params) { - await sendNotification(userId, apiEndpoint, jwtSecret, message) + await sendNotification(apiEndpoint, authToken, message) } + triggeredActions.push(action) break } } } + + return triggeredActions } diff --git a/packages/rule-handler/src/search_filter/content_filter.ts b/packages/rule-handler/src/search_filter/content_filter.ts new file mode 100644 index 000000000..822c27b0d --- /dev/null +++ b/packages/rule-handler/src/search_filter/content_filter.ts @@ -0,0 +1,15 @@ +import { SearchFilter } from './index' +import { PubSubData } from '../index' + +export class ContentFilter extends SearchFilter { + public isValid(data: PubSubData): boolean { + console.debug('ContentFilter.isValid', this.filter, data.content) + + if (!data.content) { + return false + } + + // TODO: implement content filter with semantic search + return this.filter === '*' || data.content.includes(this.filter) + } +} diff --git a/packages/rule-handler/src/search_filter/index.ts b/packages/rule-handler/src/search_filter/index.ts new file mode 100644 index 000000000..37461efbc --- /dev/null +++ b/packages/rule-handler/src/search_filter/index.ts @@ -0,0 +1,9 @@ +import { PubSubData } from '../index' + +export abstract class SearchFilter { + constructor(protected filter: string) { + this.filter = filter + } + + public abstract isValid(data: PubSubData): boolean +} diff --git a/packages/rule-handler/src/search_filter/subscription_filter.ts b/packages/rule-handler/src/search_filter/subscription_filter.ts new file mode 100644 index 000000000..43893c318 --- /dev/null +++ b/packages/rule-handler/src/search_filter/subscription_filter.ts @@ -0,0 +1,18 @@ +import { SearchFilter } from './index' +import { PubSubData } from '../index' + +export class SubscriptionFilter extends SearchFilter { + public isValid(data: PubSubData): boolean { + console.debug('SubscriptionFilter.isValid', this.filter, data.subscription) + + if (!data.subscription) { + return false + } + + // compare subscription name case insensitive + return ( + this.filter === '*' || + data.subscription.toLowerCase() === this.filter.toLowerCase() + ) + } +}