diff --git a/.env.example b/.env.example index fb9fde2..6523c76 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ SIGN_KEY= DEBUG=0 MAIN_REDIRECT=https://github.com/shiftinv/github-webhook-filter +REDIS_URL= diff --git a/deps.ts b/deps.ts index 103ebee..752a04b 100644 --- a/deps.ts +++ b/deps.ts @@ -1,4 +1,4 @@ export * as http from "https://deno.land/std@0.154.0/http/mod.ts"; export * as log from "https://deno.land/std@0.154.0/log/mod.ts"; export * as hex from "https://deno.land/std@0.154.0/encoding/hex.ts"; -export { default as TTL } from "https://deno.land/x/ttl@1.0.1/mod.ts"; +export * as redis from "https://deno.land/x/redis@v0.27.0/mod.ts"; diff --git a/lib/config.ts b/lib/config.ts index 7749b2a..445f508 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -17,6 +17,7 @@ export default { maxWebhookRetries: parseInt(get("MAX_RETRIES", "3")), maxWebhookRetryMs: parseInt(get("MAX_RETRY_MS", "30000")), mainRedirect: get("MAIN_REDIRECT", null), + redisUrl: get("REDIS_URL"), // set by deno deploy deployId: get("DENO_DEPLOYMENT_ID", ""), diff --git a/lib/filter.ts b/lib/filter.ts index 1779cbc..753b25c 100644 --- a/lib/filter.ts +++ b/lib/filter.ts @@ -1,10 +1,12 @@ -import { TTL } from "../deps.ts"; +import { reviewCommentManager } from "./manager.ts"; import { UrlConfig } from "./types.d.ts"; import { requestLog } from "./util.ts"; -const reviewComments = new TTL(2 * 1000); - -export default function filter(headers: Headers, json: any, config: UrlConfig): string | null { +export default async function filter( + headers: Headers, + json: any, + config: UrlConfig, +): Promise { const reqLog = requestLog(headers); const event = headers.get("x-github-event") || "unknown"; const login: string | undefined = json.sender?.login?.toLowerCase(); @@ -47,14 +49,11 @@ export default function filter(headers: Headers, json: any, config: UrlConfig): if (config.commentBurstLimit && reviewId) { const cacheKey = `${reviewId}-${login}`; reqLog.debug(`filter: checking cache key ${cacheKey}`); - reqLog.debug( - `filter: full comment cache ${JSON.stringify(Array.from(reviewComments))}`, - ); - const curr = reviewComments.get(cacheKey); + const curr = await reviewCommentManager.getAndIncrement(cacheKey); + reqLog.debug(`filter: current value: ${curr}`); if (curr && curr >= config.commentBurstLimit) { return `exceeded comment burst limit (${config.commentBurstLimit}) for review ${reviewId}`; } - reviewComments.set(cacheKey, (curr ?? 0) + 1); } } diff --git a/lib/handler.ts b/lib/handler.ts index f8a773f..cbf27d2 100644 --- a/lib/handler.ts +++ b/lib/handler.ts @@ -40,7 +40,7 @@ export default async function handle( const json = JSON.parse(data); // do the thing - const filterReason = filterWebhook(req.headers, json, urlConfig); + const filterReason = await filterWebhook(req.headers, json, urlConfig); if (filterReason !== null) { const reqLog = requestLog(req.headers); reqLog.debug(`handler: ignored due to '${filterReason}'`); diff --git a/lib/manager.ts b/lib/manager.ts new file mode 100644 index 0000000..78fce32 --- /dev/null +++ b/lib/manager.ts @@ -0,0 +1,28 @@ +import { redis } from "../deps.ts"; +import config from "./config.ts"; + +class ReviewCommentManager { + private redis: redis.Redis; + constructor() { + this.redis = redis.createLazyClient({ + ...redis.parseURL(config.redisUrl), + maxRetryCount: 3, + }); + } + + async getAndIncrement(key: string): Promise { + if (!this.redis.isConnected) { + // seems like pipelines don't quite work with the lazy client + await this.redis.ping(); + } + const pl = this.redis.pipeline(); + pl.incr(`reviewcomment:${key}`); + pl.expire(`reviewcomment:${key}`, 3); + const results = await pl.flush(); + + const newValue = results[0] as number; + return newValue - 1; // return old value + } +} + +export const reviewCommentManager = new ReviewCommentManager();