diff --git a/deps.ts b/deps.ts index 752a04b..b4f0029 100644 --- a/deps.ts +++ b/deps.ts @@ -2,3 +2,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 * as redis from "https://deno.land/x/redis@v0.27.0/mod.ts"; +export { default as TTLCache } from "https://deno.land/x/ttl@1.0.1/mod.ts"; diff --git a/lib/config.ts b/lib/config.ts index 445f508..cf96e8a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -17,7 +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"), + redisUrl: get("REDIS_URL", null), // set by deno deploy deployId: get("DENO_DEPLOYMENT_ID", ""), diff --git a/lib/filter.ts b/lib/filter.ts index 753b25c..9d3da99 100644 --- a/lib/filter.ts +++ b/lib/filter.ts @@ -1,4 +1,4 @@ -import { reviewCommentManager } from "./manager.ts"; +import { commentManager } from "./manager.ts"; import { UrlConfig } from "./types.d.ts"; import { requestLog } from "./util.ts"; @@ -36,21 +36,25 @@ export default async function filter( if (event === "pull_request_review") { // ignore edit/dismiss actions if (json.action !== "submitted") return `no-op PR review action '${json.action}'`; + // if comment (not approval or changes requested), ignore empty review body - else if (json.review?.state === "commented" && !json.review?.body) return "empty PR review"; + if (json.review?.state === "commented" && !json.review?.body) return "empty PR review"; } // ignore some PR comment events if (event === "pull_request_review_comment") { // ignore edit/delete actions if (json.action !== "created") return `no-op PR comment action '${json.action}'`; + // check if more than x comments on a PR review in a short timespan const reviewId: number = json.comment?.pull_request_review_id; if (config.commentBurstLimit && reviewId) { const cacheKey = `${reviewId}-${login}`; + reqLog.debug(`filter: checking cache key ${cacheKey}`); - const curr = await reviewCommentManager.getAndIncrement(cacheKey); + const curr = await commentManager.getAndIncrement(cacheKey); reqLog.debug(`filter: current value: ${curr}`); + if (curr && curr >= config.commentBurstLimit) { return `exceeded comment burst limit (${config.commentBurstLimit}) for review ${reviewId}`; } diff --git a/lib/manager.ts b/lib/manager.ts index 78fce32..1177284 100644 --- a/lib/manager.ts +++ b/lib/manager.ts @@ -1,23 +1,45 @@ -import { redis } from "../deps.ts"; +import { redis, TTLCache } from "../deps.ts"; import config from "./config.ts"; -class ReviewCommentManager { - private redis: redis.Redis; +const KEY_EXPIRY = 3; // seconds + +interface CommentManager { + getAndIncrement(key: string): Promise; +} + +class LocalCommentManager implements CommentManager { + private cache: TTLCache; constructor() { + this.cache = new TTLCache(KEY_EXPIRY * 1000); + } + + getAndIncrement(key: string): Promise { + const value = this.cache.get(key) ?? 0; + this.cache.set(key, value + 1); + return Promise.resolve(value); + } +} + +class RedisCommentManager implements CommentManager { + private redis: redis.Redis; + constructor(redisUrl: string) { this.redis = redis.createLazyClient({ - ...redis.parseURL(config.redisUrl), + ...redis.parseURL(redisUrl), maxRetryCount: 3, }); } async getAndIncrement(key: string): Promise { + key = `reviewcomment:${key}`; + + // seems like pipelines don't quite work with the lazy client, so force a connection 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); + pl.incr(key); + pl.expire(key, KEY_EXPIRY); const results = await pl.flush(); const newValue = results[0] as number; @@ -25,4 +47,6 @@ class ReviewCommentManager { } } -export const reviewCommentManager = new ReviewCommentManager(); +export const commentManager = config.redisUrl + ? new RedisCommentManager(config.redisUrl) + : new LocalCommentManager();