-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rate limit jitter, more tests, refactors
- Loading branch information
Showing
4 changed files
with
134 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,27 @@ | ||
# This file specifies files that are *not* uploaded to Google Cloud | ||
# This file specifies files that are _not_ uploaded to Google Cloud | ||
|
||
# using gcloud. It follows the same syntax as .gitignore, with the addition of | ||
|
||
# "#!include" directives (which insert the entries of the given .gitignore-style | ||
|
||
# file at that point). | ||
|
||
# | ||
|
||
# For more information, run: | ||
# $ gcloud topic gcloudignore | ||
|
||
# $ gcloud topic gcloudignore | ||
|
||
# | ||
|
||
.DS\*Store | ||
.gcloudignore | ||
# If you would like to upload your .git directory, .gitignore file or files | ||
# from your .gitignore file, remove the corresponding line | ||
# below: | ||
.git | ||
.git/ | ||
.gitignore | ||
|
||
node_modules | ||
LICENSE | ||
README.md | ||
coverage/ | ||
example_openai.ts | ||
node_modules | ||
pnpm-lock.yaml | ||
test/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,81 @@ | ||
'use strict'; | ||
|
||
import functions from '@google-cloud/functions-framework'; | ||
import OpenAI from 'openai'; | ||
import { validateEvent, verifySignature } from 'nostr-tools'; | ||
// We use lib instead of explicit functions so that we can stub them in our tests | ||
import lib from './src/lib.js'; | ||
|
||
import functions from '@google-cloud/functions-framework'; | ||
let startTime; | ||
const FUNCTION_TIMEOUT_MS = 60000; | ||
const RATE_LIMIT_ERROR_CODE = 429; | ||
|
||
// Assumes OPENAI_API_KEY has been set in the environment | ||
// Keep this initialization in the global module scope so it can be reused | ||
// across function invocations | ||
const openai = new OpenAI(); | ||
|
||
functions.cloudEvent('nostrEventsPubSub', async (cloudEvent) => { | ||
try { | ||
const data = cloudEvent.data.message.data; | ||
startTime = Date.now(); | ||
|
||
const eventJSON = data ? Buffer.from(data, 'base64').toString() : '{}'; | ||
const event = JSON.parse(eventJSON); | ||
|
||
if (!validateEvent(event)) { | ||
console.error('Invalid Nostr Event'); | ||
return; | ||
} | ||
try { | ||
const event = getVerifiedEvent(cloudEvent.data.message.data); | ||
|
||
if (!verifySignature(event)) { | ||
console.error('Invalid Nostr Event Signature'); | ||
if (!event) { | ||
return; | ||
} | ||
|
||
const response = await openai.moderations.create({ input: event.content }); | ||
const moderation = response.results[0]; | ||
const moderation = await getModeration(event); | ||
|
||
if (!moderation.flagged) { | ||
if (!moderation) { | ||
console.log(`Nostr Event ${event.id} Passed Moderation. Skipping`); | ||
return; | ||
} | ||
|
||
const moderationEvent = await lib.publishModerationResult( | ||
event, | ||
moderation | ||
); | ||
} catch (err) { | ||
// For the moment log every error to the console and let the function finish | ||
// Well handle retries | ||
console.error(err); | ||
await lib.publishModeration(event, moderation); | ||
} catch (error) { | ||
if (error?.response?.status === RATE_LIMIT_ERROR_CODE) { | ||
console.error('Rate limit error. Adding random pause'); | ||
await randomPause(); | ||
throw error; | ||
} | ||
} | ||
}); | ||
|
||
function getVerifiedEvent(data) { | ||
const eventJSON = data ? Buffer.from(data, 'base64').toString() : '{}'; | ||
const event = JSON.parse(eventJSON); | ||
|
||
if (!validateEvent(event)) { | ||
console.error('Invalid Nostr Event'); | ||
return; | ||
} | ||
|
||
if (!verifySignature(event)) { | ||
console.error('Invalid Nostr Event Signature'); | ||
return; | ||
} | ||
|
||
return event; | ||
} | ||
|
||
async function getModeration(event) { | ||
const response = await openai.moderations.create({ input: event.content }); | ||
const moderation = response.results[0]; | ||
|
||
if (moderation.flagged) { | ||
return moderation; | ||
} | ||
} | ||
|
||
// Random pause within the window of half of the remaining available time before | ||
// hitting timeout. | ||
// https://platform.openai.com/docs/guides/rate-limits/error-mitigation | ||
async function randomPause() { | ||
const elapsedMs = Date.now() - startTime; | ||
const remainingMs = FUNCTION_TIMEOUT_MS - elapsedMs; | ||
const halfOfRemainingTime = remainingMs / 2; | ||
const jitterTimeoutMs = Math.random() * halfOfRemainingTime; | ||
|
||
await lib.waitMillis(jitterTimeoutMs); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters