Merge pull request #1447 from omnivore-app/rule-archive-page-action

Add mark page as read and archived action in rule engine
This commit is contained in:
Hongbo Wu 2022-11-23 14:31:12 +08:00 committed by GitHub
commit 6a2d143155
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 176 additions and 45 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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