diff --git a/.github/scripts/repair-moderation-policy.cjs b/.github/scripts/repair-moderation-policy.cjs new file mode 100644 index 00000000..790fdddf --- /dev/null +++ b/.github/scripts/repair-moderation-policy.cjs @@ -0,0 +1,180 @@ +const { StreamClient } = require('@stream-io/node-sdk'); + +const templateNames = [ + 'moderation_template_activity', + 'moderation_template_reaction', +]; +const blockListName = 'stream_java_moderation_tests'; +const triggerWord = 'pissoar'; +const rule = { name: blockListName, action: 'remove' }; +const unavailableBlockListNames = new Set(['profanity_en']); +const propagationRetryDelaysMs = [1000, 2000, 4000, 8000]; + +function sleep(delayMs) { + return new Promise((resolve) => setTimeout(resolve, delayMs)); +} + +async function retryAfterBlockListPropagation(operation) { + for (const delayMs of propagationRetryDelaysMs) { + try { + return await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes(`Blocklist not found: ${blockListName}`)) { + throw error; + } + + console.log(`Waiting ${delayMs}ms for ${blockListName} to propagate`); + await sleep(delayMs); + } + } + + return operation(); +} + +async function ensureTestBlockList(client) { + const response = await client.listBlockLists(); + const blockList = response.blocklists.find( + (candidate) => candidate.name === blockListName, + ); + + if (!blockList) { + await client.createBlockList({ + name: blockListName, + type: 'word', + words: [triggerWord], + }); + console.log(`Created test blocklist ${blockListName}`); + return; + } + + if (!blockList.words.includes(triggerWord)) { + await client.updateBlockList({ + name: blockListName, + words: [...blockList.words, triggerWord], + }); + console.log(`Updated test blocklist ${blockListName}`); + return; + } + + console.log(`Test blocklist ${blockListName} already exists`); +} + +function withRequiredRule(blockListConfig = {}) { + const rules = (blockListConfig.rules || []).filter( + (candidate) => !unavailableBlockListNames.has(candidate.name), + ); + const index = rules.findIndex((candidate) => candidate.name === rule.name); + + if (index === -1) { + rules.push(rule); + } else { + rules[index] = { ...rules[index], action: rule.action }; + } + + return { ...blockListConfig, rules }; +} + +function hasRequiredRule(blockListConfig) { + return blockListConfig?.rules?.some( + (candidate) => + candidate.name === rule.name && candidate.action === rule.action, + ); +} + +async function repairPolicy(client, key) { + const response = await client.moderation.getConfig({ key }); + const config = response.config; + if (!config) { + throw new Error(`Moderation policy ${key} was not found`); + } + if (hasRequiredRule(config.block_list_config)) { + console.log(`Moderation policy ${key} already contains the required rule`); + return; + } + + const writableFields = [ + 'async', + 'team', + 'ai_audio_config', + 'ai_image_config', + 'ai_text_config', + 'ai_video_config', + 'automod_platform_circumvention_config', + 'automod_toxicity_config', + 'aws_rekognition_config', + 'block_list_config', + 'bodyguard_config', + 'flood_config', + 'google_vision_config', + 'llm_config', + 'rule_builder_config', + 'velocity_filter_config', + 'video_call_rule_config', + ]; + const payload = { key }; + for (const field of writableFields) { + if (config[field] !== undefined) { + payload[field] = config[field]; + } + } + payload.block_list_config = withRequiredRule(config.block_list_config); + + await retryAfterBlockListPropagation(() => + client.moderation.upsertConfig(payload), + ); + console.log(`Repaired moderation policy ${key}`); +} + +async function main() { + if (!process.env.STREAM_KEY || !process.env.STREAM_SECRET) { + throw new Error('STREAM_KEY and STREAM_SECRET are required'); + } + + const client = new StreamClient( + process.env.STREAM_KEY, + process.env.STREAM_SECRET, + ); + await ensureTestBlockList(client); + const response = await client.moderation.v2QueryTemplates(); + const templates = new Map( + response.templates.map((template) => [template.name, template]), + ); + const repairedPolicies = new Set(); + + for (const name of templateNames) { + const template = templates.get(name); + if (!template?.config) { + throw new Error(`Moderation template ${name} was not found`); + } + + if (template.config.config_key) { + if (!repairedPolicies.has(template.config.config_key)) { + await repairPolicy(client, template.config.config_key); + repairedPolicies.add(template.config.config_key); + } + continue; + } + + if (hasRequiredRule(template.config.block_list_config)) { + console.log(`Moderation template ${name} already contains the required rule`); + continue; + } + + await retryAfterBlockListPropagation(() => + client.moderation.v2UpsertTemplate({ + name, + config: { + ...template.config, + block_list_config: withRequiredRule(template.config.block_list_config), + }, + }), + ); + console.log(`Repaired moderation template ${name}`); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bcc6037..a3891742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,31 +6,47 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true +permissions: + contents: read + pull-requests: read + jobs: build: name: ๐Ÿงช Test & lint runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Commit message lint - uses: wagoid/commitlint-github-action@v4 + uses: wagoid/commitlint-github-action@v6 - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*') }} restore-keys: | ${{ runner.os }}-gradle- + - uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Repair moderation test policy + env: + STREAM_KEY: ${{ secrets.STREAM_KEY }} + STREAM_SECRET: ${{ secrets.STREAM_SECRET }} + run: | + npm install --prefix "$RUNNER_TEMP/moderation-repair" --no-save --package-lock=false --ignore-scripts @stream-io/node-sdk@0.8.3 + NODE_PATH="$RUNNER_TEMP/moderation-repair/node_modules" node .github/scripts/repair-moderation-policy.cjs + - name: Test env: STREAM_KEY: ${{ secrets.STREAM_KEY }} diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index 59c1c98c..f5710385 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -7,15 +7,19 @@ on: description: "The new version number with a 'v' prefix. Example: v1.40.1" required: true +permissions: + contents: write + pull-requests: write + jobs: init_release: name: ๐Ÿš€ Create release PR runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 # gives the changelog generator access to all previous commits - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -32,7 +36,7 @@ jobs: git push -q -u origin "release-$VERSION" - name: Get changelog diff - uses: actions/github-script@v5 + uses: actions/github-script@v9 with: script: | const get_change_log_diff = require('./scripts/get_changelog_diff.js') diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index 30938502..aa3fa739 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -3,27 +3,31 @@ on: push: branches: - main + +permissions: + contents: write + jobs: javadoc: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' - name: Set up Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: node-version: 16 - name: Generate doc run: ./gradlew --no-daemon javadoc - name: Deploy - uses: JamesIves/github-pages-deploy-action@releases/v3 + uses: JamesIves/github-pages-deploy-action@v4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: build/docs/javadoc/ + token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + folder: build/docs/javadoc/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da5eba72..c7d70147 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,20 +6,23 @@ on: branches: - main +permissions: + contents: write + jobs: Release: name: ๐Ÿš€ Release if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - - uses: actions/github-script@v5 + - uses: actions/github-script@v9 with: script: | const get_change_log_diff = require('./scripts/get_changelog_diff.js')