Run search API to filter rules

This commit is contained in:
Hongbo Wu
2022-11-24 15:04:59 +08:00
parent 3f24d9a99e
commit 1e6e2b82ee
7 changed files with 110 additions and 153 deletions

View 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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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()
)
}
}

View File

@ -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
}
}