Run search API to filter rules
This commit is contained in:
81
packages/rule-handler/src/filter.ts
Normal file
81
packages/rule-handler/src/filter.ts
Normal file
@ -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<Node[]> => {
|
||||
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<SearchResponse>(
|
||||
`${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<boolean> => {
|
||||
filter += ` includes:${pageId}`
|
||||
const nodes = await search(userId, apiEndpoint, auth, filter)
|
||||
|
||||
return nodes.length > 0
|
||||
}
|
||||
@ -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<AxiosResponse<any, any> | 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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user