major refactor
This commit is contained in:
+1
-4
@@ -1,4 +1,3 @@
|
|||||||
import {adaptiveFetchAndUpdate, eventCache} from './cache/cache.js'
|
|
||||||
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
|
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
|
||||||
async function main() {
|
async function main() {
|
||||||
const env = readEnv()
|
const env = readEnv()
|
||||||
@@ -15,13 +14,11 @@ async function main() {
|
|||||||
const link = await LinkService.create(cfg)
|
const link = await LinkService.create(cfg)
|
||||||
|
|
||||||
if (cfg.service.safelink === 1) {
|
if (cfg.service.safelink === 1) {
|
||||||
eventCache.init(cfg.service)
|
cfg.eventCache.adaptiveFetchAndUpdate()
|
||||||
adaptiveFetchAndUpdate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await link.start()
|
await link.start()
|
||||||
httpLogger.info('link service is running')
|
httpLogger.info('link service is running')
|
||||||
console.log('running service')
|
|
||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
httpLogger.info('link service is stopping')
|
httpLogger.info('link service is stopping')
|
||||||
await link.destroy()
|
await link.destroy()
|
||||||
|
|||||||
Vendored
+96
-112
@@ -8,11 +8,9 @@ let cacheCursor: string | undefined
|
|||||||
export class EventCache {
|
export class EventCache {
|
||||||
private rules = new Map<string, ToolsOzoneSafelinkDefs.Event>()
|
private rules = new Map<string, ToolsOzoneSafelinkDefs.Event>()
|
||||||
private cfg: ServiceConfig | undefined = undefined
|
private cfg: ServiceConfig | undefined = undefined
|
||||||
|
private pollInterval = 1 * 1000 // start at 1 second
|
||||||
|
|
||||||
constructor() {
|
constructor(cfg: ServiceConfig) {
|
||||||
this.cfg = undefined
|
|
||||||
}
|
|
||||||
async init(cfg: ServiceConfig) {
|
|
||||||
this.cfg = cfg
|
this.cfg = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,14 +35,12 @@ export class EventCache {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
||||||
// If the action is to remove the rule, delete it from the cache
|
|
||||||
this.insert(domain, event)
|
this.insert(domain, event)
|
||||||
redirectLogger.info(
|
redirectLogger.info(
|
||||||
`[EventCache] Removed rule for domain, adjusted audit log: ${domain}`,
|
`[EventCache] Removed rule for domain, adjusted audit log: ${domain}`,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if update should happen
|
|
||||||
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
|
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
|
||||||
this.insert(domain, event)
|
this.insert(domain, event)
|
||||||
redirectLogger.info(`[EventCache] Whitelisted domain: ${domain}`)
|
redirectLogger.info(`[EventCache] Whitelisted domain: ${domain}`)
|
||||||
@@ -75,12 +71,10 @@ export class EventCache {
|
|||||||
`[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`,
|
`[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`,
|
||||||
)
|
)
|
||||||
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
||||||
// If the action is to remove the rule, update it from the cache
|
|
||||||
this.insert(event.url, event)
|
this.insert(event.url, event)
|
||||||
redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`)
|
redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if update should happen
|
|
||||||
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
|
if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) {
|
||||||
this.insert(event.url, event)
|
this.insert(event.url, event)
|
||||||
redirectLogger.info(`[EventCache] Whitelisted url: ${event.url}`)
|
redirectLogger.info(`[EventCache] Whitelisted url: ${event.url}`)
|
||||||
@@ -98,7 +92,6 @@ export class EventCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert or update an event
|
|
||||||
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
|
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
|
||||||
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
|
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
|
||||||
redirectLogger.info(
|
redirectLogger.info(
|
||||||
@@ -124,19 +117,16 @@ export class EventCache {
|
|||||||
const domain = parsedUrl.hostname
|
const domain = parsedUrl.hostname
|
||||||
const domainAndPath = domain + parsedUrl.pathname
|
const domainAndPath = domain + parsedUrl.pathname
|
||||||
|
|
||||||
// 1. Check for a rule by domain only
|
|
||||||
const byDomain = this.rules.get(domain)
|
const byDomain = this.rules.get(domain)
|
||||||
if (byDomain) {
|
if (byDomain) {
|
||||||
return byDomain
|
return byDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for a rule by domain + path
|
|
||||||
const byDomainAndPath = this.rules.get(domainAndPath)
|
const byDomainAndPath = this.rules.get(domainAndPath)
|
||||||
if (byDomainAndPath) {
|
if (byDomainAndPath) {
|
||||||
return byDomainAndPath
|
return byDomainAndPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check for a rule by full URL
|
|
||||||
return this.rules.get(url)
|
return this.rules.get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,112 +134,106 @@ export class EventCache {
|
|||||||
this.rules.delete(event.url)
|
this.rules.delete(event.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get an event by full URL
|
|
||||||
get(url: string): ToolsOzoneSafelinkDefs.Event | undefined {
|
get(url: string): ToolsOzoneSafelinkDefs.Event | undefined {
|
||||||
const event = this.rules.get(url)
|
const event = this.rules.get(url)
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all events
|
|
||||||
list(): ToolsOzoneSafelinkDefs.Event[] {
|
list(): ToolsOzoneSafelinkDefs.Event[] {
|
||||||
return Array.from(this.rules.values())
|
return Array.from(this.rules.values())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Adaptive polling: slow down if no new events, speed up if updates found
|
||||||
// Adaptive polling: slow down if no new events, speed up if updates found
|
async adaptiveFetchAndUpdate() {
|
||||||
let pollInterval = 1 * 1000 // start at 1 seconds
|
const prevCursor = cacheCursor
|
||||||
|
const eventConfig = await this.getConfig()
|
||||||
export async function adaptiveFetchAndUpdate() {
|
if (eventConfig === undefined) {
|
||||||
const prevCursor = cacheCursor
|
redirectLogger.info(
|
||||||
const eventConfig = await eventCache.getConfig()
|
`[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`,
|
||||||
if (eventConfig === undefined) {
|
)
|
||||||
redirectLogger.info(
|
} else {
|
||||||
`[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`,
|
await this.fetchAndUpdateEvents(eventConfig)
|
||||||
)
|
}
|
||||||
} else {
|
if (cacheCursor === prevCursor) {
|
||||||
await fetchAndUpdateEvents(eventConfig)
|
this.pollInterval = Math.min(this.pollInterval * 2, 10 * 60 * 1000)
|
||||||
}
|
redirectLogger.info(
|
||||||
// If no new events, increase interval (up to 10 minutes), else reset to 5s
|
`[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${
|
||||||
if (cacheCursor === prevCursor) {
|
this.pollInterval / 1000
|
||||||
pollInterval = Math.min(pollInterval * 2, 10 * 60 * 1000)
|
}s`,
|
||||||
redirectLogger.info(
|
)
|
||||||
`[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${
|
} else {
|
||||||
pollInterval / 1000
|
this.pollInterval = 5 * 1000
|
||||||
}s`,
|
redirectLogger.info(
|
||||||
)
|
`[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${
|
||||||
} else {
|
this.pollInterval / 1000
|
||||||
pollInterval = 5 * 1000
|
}s`,
|
||||||
redirectLogger.info(
|
)
|
||||||
`[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${
|
}
|
||||||
pollInterval / 1000
|
setTimeout(() => this.adaptiveFetchAndUpdate(), this.pollInterval)
|
||||||
}s`,
|
}
|
||||||
)
|
|
||||||
}
|
// Fetch and update events from the server
|
||||||
setTimeout(adaptiveFetchAndUpdate, pollInterval)
|
async fetchAndUpdateEvents(cfg: ServiceConfig) {
|
||||||
}
|
if (!cfg || !cfg.ozoneUrl || !cfg.ozoneAgentHandle || !cfg.ozoneAgentPass) {
|
||||||
|
console.error(
|
||||||
// Export a singleton instance
|
'[eventCache:fetchAndUpdateEvents] No active config, skipping actions',
|
||||||
export const eventCache = new EventCache()
|
)
|
||||||
// Start adaptive polling
|
return
|
||||||
// adaptiveFetchAndUpdate()
|
}
|
||||||
|
redirectLogger.info(
|
||||||
// Function to fetch and update events from the server
|
`[eventCache] Fetching events with cursor: ${cacheCursor}`,
|
||||||
export async function fetchAndUpdateEvents(cfg: ServiceConfig) {
|
)
|
||||||
if (!cfg || !cfg.ozoneUrl || !cfg.ozoneAgentHandle || !cfg.ozoneAgentPass) {
|
const ozoneAgent = new OzoneAgent(cfg)
|
||||||
console.error(
|
const ozoneSession = await ozoneAgent.getSession?.()
|
||||||
'[eventCache:fetchAndUpdateEvents] No active config, skipping actions',
|
if (!ozoneSession) {
|
||||||
)
|
console.error(
|
||||||
return
|
'[eventCache:fetchAndUpdateEvents] No active session found.',
|
||||||
}
|
)
|
||||||
redirectLogger.info(
|
return
|
||||||
`[eventCache] Fetching events with cursor: ${cacheCursor}`,
|
}
|
||||||
)
|
const ozoneDid = ozoneSession.did
|
||||||
// Use current session DID instead of env variable
|
if (!ozoneDid || ozoneDid === 'did:plc:invalid') {
|
||||||
const ozoneAgent = await new OzoneAgent(cfg)
|
console.error(
|
||||||
const ozoneSession = await ozoneAgent.getSession?.()
|
'[eventCache:fetchAndUpdateEvents] Invalid or missing session DID.',
|
||||||
if (!ozoneSession) {
|
)
|
||||||
console.error('[eventCache:fetchAndUpdateEvents] No active session found.')
|
return
|
||||||
return
|
}
|
||||||
}
|
ozoneAgent.agent.setHeader?.('atproto-proxy', `${ozoneDid}#atproto_labeler`)
|
||||||
const ozoneDid = ozoneSession.did
|
|
||||||
if (!ozoneDid || ozoneDid === 'did:plc:invalid') {
|
const res = await ozoneAgent.agent.tools?.ozone?.safelink?.queryEvents?.({
|
||||||
console.error(
|
cursor: cacheCursor,
|
||||||
'[eventCache:fetchAndUpdateEvents] Invalid or missing session DID.',
|
limit: 100,
|
||||||
)
|
})
|
||||||
return
|
|
||||||
}
|
if (res?.data.cursor === cacheCursor) {
|
||||||
ozoneAgent.agent.setHeader?.('atproto-proxy', `${ozoneDid}#atproto_labeler`)
|
redirectLogger.info(
|
||||||
|
'[eventCache:fetchAndUpdateEvents] No new events to update.',
|
||||||
const res = await ozoneAgent.agent.tools?.ozone?.safelink?.queryEvents?.({
|
)
|
||||||
cursor: cacheCursor,
|
return
|
||||||
limit: 100, // Adjust as needed
|
}
|
||||||
})
|
|
||||||
|
redirectLogger.info(
|
||||||
if (res?.data.cursor === cacheCursor) {
|
`[eventCache:fetchAndUpdateEvents] Received response:`,
|
||||||
redirectLogger.info(
|
{
|
||||||
'[eventCache:fetchAndUpdateEvents] No new events to update.',
|
...res,
|
||||||
)
|
data: {
|
||||||
return
|
...res?.data,
|
||||||
}
|
rules: Array.isArray(res?.data?.events)
|
||||||
|
? res.data.events.map(event => JSON.stringify(event))
|
||||||
redirectLogger.info(`[eventCache:fetchAndUpdateEvents] Received response:`, {
|
: res?.data?.events,
|
||||||
...res,
|
},
|
||||||
data: {
|
},
|
||||||
...res?.data,
|
)
|
||||||
rules: Array.isArray(res?.data?.events)
|
|
||||||
? res.data.events.map(event => JSON.stringify(event))
|
for (const event of res.data.events) {
|
||||||
: res?.data?.events,
|
this.smartUpdate(event)
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
cacheCursor = res.data?.cursor
|
||||||
for (const event of res.data.events) {
|
|
||||||
eventCache.smartUpdate(event)
|
redirectLogger.info(
|
||||||
}
|
'[eventCache:fetchAndUpdateEvents] Current cache contents:',
|
||||||
|
)
|
||||||
cacheCursor = res.data?.cursor
|
redirectLogger.info(this.list())
|
||||||
|
}
|
||||||
redirectLogger.info(
|
|
||||||
'[eventCache:fetchAndUpdateEvents] Current cache contents:',
|
|
||||||
)
|
|
||||||
redirectLogger.info(eventCache.list())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {envInt, envList, envStr} from '@atproto/common'
|
import {envInt, envList, envStr} from '@atproto/common'
|
||||||
|
|
||||||
// import { type EventCache, eventCache } from '../cache/cache.js'
|
// import { type EventCache, eventCache } from '../cache/cache.js'
|
||||||
import {type EventCache, eventCache} from './cache/cache.js'
|
import {EventCache} from './cache/cache.js'
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
service: ServiceConfig
|
service: ServiceConfig
|
||||||
@@ -96,6 +96,8 @@ export const envToCfg = (env: Environment): Config => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventCache = new EventCache(serviceCfg)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service: serviceCfg,
|
service: serviceCfg,
|
||||||
eventCache: eventCache,
|
eventCache: eventCache,
|
||||||
|
|||||||
@@ -81,9 +81,8 @@ describe('link service', async () => {
|
|||||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||||
createdBy: 'did:example:admin',
|
createdBy: 'did:example:admin',
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
comment: 'BONES has been erroneously blocked by due to an error',
|
comment: 'BONES has been erroneously blocked for the sake of this test',
|
||||||
})
|
})
|
||||||
// Ensure 'later' is after 'now'
|
|
||||||
const later = new Date(Date.now() + 1000).toISOString()
|
const later = new Date(Date.now() + 1000).toISOString()
|
||||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||||
$type: 'tools.ozone.safelink.defs#event',
|
$type: 'tools.ozone.safelink.defs#event',
|
||||||
@@ -98,12 +97,6 @@ describe('link service', async () => {
|
|||||||
comment:
|
comment:
|
||||||
'BONES has been resurrected to bring good music to the world once again',
|
'BONES has been resurrected to bring good music to the world once again',
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
|
||||||
linkService.ctx.cfg.eventCache.smartGet(
|
|
||||||
'https://www.instagram.com/teamseshbones/?hl=en',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await linkService?.destroy()
|
await linkService?.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user