diff --git a/packages/rule-handler/src/filter.ts b/packages/rule-handler/src/filter.ts new file mode 100644 index 000000000..6f361db51 --- /dev/null +++ b/packages/rule-handler/src/filter.ts @@ -0,0 +1,81 @@ +import axios from 'axios' + +interface SearchResponse { + data: { + search: { + edges: Edge[] + } + } +} + +interface Edge { + node: Node +} + +interface Node { + id: string +} + +export const search = async ( + userId: string, + apiEndpoint: string, + auth: string, + query: string +): Promise => { + const requestData = JSON.stringify({ + query: `query Search($query: String) { + search(query: $query) { + ... on SearchSuccess { + edges { + node { + id + } + } + } + ... on SearchError { + errorCodes + } + } + }`, + variables: { + query, + }, + }) + + try { + const response = await axios.post( + `${apiEndpoint}/graphql`, + requestData, + { + headers: { + Cookie: `auth=${auth};`, + 'Content-Type': 'application/json', + }, + } + ) + + const edges = response.data.data.search.edges + if (edges.length == 0) { + return [] + } + + return edges.map((edge: Edge) => edge.node) + } catch (e) { + console.error(e) + + return [] + } +} + +export const isMatched = async ( + userId: string, + apiEndpoint: string, + auth: string, + filter: string, + pageId: string +): Promise => { + filter += ` includes:${pageId}` + const nodes = await search(userId, apiEndpoint, auth, filter) + + return nodes.length > 0 +} diff --git a/packages/rule-handler/src/rule.ts b/packages/rule-handler/src/rule.ts index 027f2b198..d8dd35f48 100644 --- a/packages/rule-handler/src/rule.ts +++ b/packages/rule-handler/src/rule.ts @@ -1,14 +1,9 @@ import { sendNotification } from './notification' import { getAuthToken, PubSubData } from './index' -import axios from 'axios' -import { parse, SearchParserKeyWordOffset } from 'search-query-parser' +import axios, { AxiosResponse } from 'axios' 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' -import { ReadFilter } from './search_filter/read_filter' -import { TypeFilter } from './search_filter/type_filter' +import { isMatched } from './filter' export enum RuleActionType { AddLabel = 'ADD_LABEL', @@ -34,58 +29,6 @@ export interface Rule { updatedAt: Date } -const parseSearchFilter = (filter: string): SearchFilter[] => { - const searchFilter = filter ? filter.replace(/\W\s":/g, '') : undefined - const result: SearchFilter[] = [] - - if (!searchFilter || searchFilter === '*') { - return result - } - - const parsed = parse(searchFilter, { - keywords: ['subscription', 'content', 'is', 'type'], - tokenize: true, - }) - if (parsed.offsets) { - const keywords = parsed.offsets - .filter((offset) => 'keyword' in offset) - .map((offset) => offset as SearchParserKeyWordOffset) - - for (const keyword of keywords) { - if (!keyword.value) { - continue - } - switch (keyword.keyword) { - case 'subscription': - result.push(new SubscriptionFilter(keyword.value)) - break - case 'content': - result.push(new ContentFilter(keyword.value)) - break - case 'is': - result.push(new ReadFilter(keyword.value)) - break - case 'type': - result.push(new TypeFilter(keyword.value)) - break - } - } - } - - return result -} - -const isValidData = (filter: string, data: PubSubData): boolean => { - const searchFilters = parseSearchFilter(filter) - - if (searchFilters.length === 0) { - console.debug('no search filters found') - return true - } - - return searchFilters.every((searchFilter) => searchFilter.isValid(data)) -} - export const getEnabledRules = async ( userId: string, apiEndpoint: string, @@ -133,44 +76,43 @@ export const triggerActions = async ( jwtSecret: string ) => { const authToken = await getAuthToken(userId, jwtSecret) + const actionPromises: Promise | undefined>[] = [] - const actionPromises = rules.map((rule) => { - if (!isValidData(rule.filter, data)) { - return + for (const rule of rules) { + if ( + !(await isMatched(userId, apiEndpoint, authToken, rule.filter, data.id)) + ) { + continue } - return rule.actions.map((action) => { + rule.actions.forEach((action) => { switch (action.type) { case RuleActionType.AddLabel: - if (!data.id || action.params.length === 0) { - console.log('invalid data for add label action') - return - } - - return addLabels(apiEndpoint, authToken, data.id, action.params) + data.id && + actionPromises.push( + addLabels(apiEndpoint, authToken, data.id, action.params) + ) + break case RuleActionType.Archive: - if (!data.id) { - console.log('invalid data for archive action') - return - } - - return archivePage(apiEndpoint, authToken, data.id) + data.id && + actionPromises.push(archivePage(apiEndpoint, authToken, data.id)) + break case RuleActionType.MarkAsRead: - if (!data.id) { - console.log('invalid data for mark as read action') - return - } - - return markPageAsRead(apiEndpoint, authToken, data.id) + data.id && + actionPromises.push(markPageAsRead(apiEndpoint, authToken, data.id)) + break case RuleActionType.SendNotification: - return sendNotification( - apiEndpoint, - authToken, - 'New page added to your feed' + actionPromises.push( + sendNotification( + apiEndpoint, + authToken, + 'New page added to your feed' + ) ) + break } }) - }) + } - return Promise.all(actionPromises.flat().filter((p) => p !== undefined)) + return Promise.all(actionPromises) } diff --git a/packages/rule-handler/src/search_filter/content_filter.ts b/packages/rule-handler/src/search_filter/content_filter.ts deleted file mode 100644 index 0207c79a2..000000000 --- a/packages/rule-handler/src/search_filter/content_filter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SearchFilter } from './index' -import { PubSubData } from '../index' - -export class ContentFilter extends SearchFilter { - public isValid(data: PubSubData): boolean { - if (!data.content) { - return false - } - - // TODO: implement content filter with semantic search - return this.query === '*' || data.content.includes(this.query) - } -} diff --git a/packages/rule-handler/src/search_filter/index.ts b/packages/rule-handler/src/search_filter/index.ts deleted file mode 100644 index adb2230c9..000000000 --- a/packages/rule-handler/src/search_filter/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PubSubData } from '../index' - -export abstract class SearchFilter { - constructor(protected query: string) { - this.query = query - } - - public abstract isValid(data: PubSubData): boolean -} diff --git a/packages/rule-handler/src/search_filter/read_filter.ts b/packages/rule-handler/src/search_filter/read_filter.ts deleted file mode 100644 index 7d4904b2a..000000000 --- a/packages/rule-handler/src/search_filter/read_filter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PubSubData } from '../index' -import { SearchFilter } from './index' - -export class ReadFilter extends SearchFilter { - public isValid(data: PubSubData): boolean { - if (!data.readingProgressPercent) { - return false - } - - if (this.query === 'read') { - return data.readingProgressPercent >= 98 - } - - return data.readingProgressPercent < 98 - } -} diff --git a/packages/rule-handler/src/search_filter/subscription_filter.ts b/packages/rule-handler/src/search_filter/subscription_filter.ts deleted file mode 100644 index ac2f4ee73..000000000 --- a/packages/rule-handler/src/search_filter/subscription_filter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SearchFilter } from './index' -import { PubSubData } from '../index' - -export class SubscriptionFilter extends SearchFilter { - public isValid(data: PubSubData): boolean { - if (!data.subscription) { - return false - } - - // compare subscription name case insensitive - return ( - this.query === '*' || - data.subscription.toLowerCase() === this.query.toLowerCase() - ) - } -} diff --git a/packages/rule-handler/src/search_filter/type_filter.ts b/packages/rule-handler/src/search_filter/type_filter.ts deleted file mode 100644 index 5fac9b48d..000000000 --- a/packages/rule-handler/src/search_filter/type_filter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SearchFilter } from './index' -import { PubSubData } from '../index' - -export class TypeFilter extends SearchFilter { - public isValid(data: PubSubData): boolean { - if (!data.pageType) { - return false - } - - return this.query === '*' || data.pageType === this.query - } -}