diff --git a/.detoxrc.js b/.detoxrc.js index 1e41165dac..9066204308 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -41,7 +41,7 @@ module.exports = { simulator: { type: 'ios.simulator', device: { - type: 'iPhone 15', + type: 'iPhone 15 Pro', }, }, attached: { diff --git a/.easignore b/.easignore index cdf1b7abfc..29fc4124b5 100644 --- a/.easignore +++ b/.easignore @@ -91,8 +91,8 @@ web-build/ # Android & iOS folders -android/ -ios/ +/android/ +/ios/ # environment variables .env diff --git a/.env.example b/.env.example index b4213aea24..5cf8e07b1c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,6 @@ +# Copy this to `.env` and `.env.test` files + SENTRY_AUTH_TOKEN= +EXPO_PUBLIC_ENV=development +EXPO_PUBLIC_LOG_LEVEL=debug +EXPO_PUBLIC_LOG_DEBUG= diff --git a/.eslintrc.js b/.eslintrc.js index bc4a2a3942..6165517f77 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,10 +7,52 @@ module.exports = { 'prettier', ], parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'detox', 'react'], + plugins: [ + '@typescript-eslint', + 'detox', + 'react', + 'lingui', + 'simple-import-sort', + ], rules: { 'react/no-unescaped-entities': 0, 'react-native/no-inline-styles': 0, + 'simple-import-sort/imports': [ + 'warn', + { + groups: [ + // Side effect imports. + ['^\\u0000'], + // Node.js builtins prefixed with `node:`. + ['^node:'], + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + // React/React Native priortized, followed by expo + // Followed by all packages excluding unprefixed relative ones + [ + '^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)', + '^(expo(.*)$)|^(expo$)', + '^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w', + ], + // Relative imports. + // Ideally, anything that starts with a dot or # + // due to unprefixed relative imports being used, we whitelist the relative paths we use + // (?:$|\\/) matches end of string or / + [ + '^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)', + '^(?:#\\/)?view(?:$|\\/)', + '^(?:#\\/)?screens(?:$|\\/)', + '^(?:#\\/)?alf(?:$|\\/)', + '^(?:#\\/)?components(?:$|\\/)', + '^#\\/', + '^\\.', + ], + // anything else - hopefully we don't have any of these + ['^'], + ], + }, + ], + 'simple-import-sort/exports': 'warn', }, ignorePatterns: [ '**/__mocks__/*.ts', @@ -25,8 +67,14 @@ module.exports = { 'bskyweb', '*.html', 'bskyweb', + 'src/locale/locales/_build/', + 'src/locale/locales/**/*.js', ], settings: { componentWrapperFunctions: ['observer'], }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, } diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index a7c8f4588e..3f60705792 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -3,7 +3,8 @@ on: push: branches: - main - - jake/bskyweb-additions + - 3p-moderators + env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml new file mode 100644 index 0000000000..8cbd90984c --- /dev/null +++ b/.github/workflows/build-submit-android.yml @@ -0,0 +1,65 @@ +--- +name: Build and Submit Android + +on: + workflow_dispatch: + inputs: + profile: + type: choice + description: Build profile to use + options: + - production + +jobs: + build: + name: Build and Submit Android + runs-on: ubuntu-latest + steps: + - name: Check for EXPO_TOKEN + run: > + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" + exit 1 + fi + + - name: ⬇️ Checkout + uses: actions/checkout@v4 + + - name: 🔧 Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: yarn + + - name: 🔨 Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: ⛏️ Setup EAS local builds + run: yarn global add eas-cli-local-build-plugin + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: ⚙️ Install dependencies + run: yarn install + + - name: 🔤 Compile translations + run: yarn intl:build + + - name: ✏️ Write environment variables + run: | + export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' + echo "${{ secrets.ENV_TOKEN }}" > .env + echo "$json" > google-services.json + + - name: 🏗️ EAS Build + run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive + + - name: 🚀 Deploy + run: eas submit -p android --non-interactive --path build.aab diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml new file mode 100644 index 0000000000..f5188b4b4b --- /dev/null +++ b/.github/workflows/build-submit-ios.yml @@ -0,0 +1,75 @@ +--- +name: Build and Submit iOS + +on: + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + inputs: + profile: + type: choice + description: Build profile to use + options: + - production + +jobs: + build: + name: Build and Submit iOS + runs-on: macos-14 + steps: + - name: Check for EXPO_TOKEN + run: > + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" + exit 1 + fi + + - name: ⬇️ Checkout + uses: actions/checkout@v4 + + - name: 🔧 Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: yarn + + - name: 🔨 Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: ⛏️ Setup EAS local builds + run: yarn global add eas-cli-local-build-plugin + + - name: ⚙️ Install dependencies + run: yarn install + + - name: ☕️ Setup Cocoapods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.14.3 + + - name: 💾 Cache Pods + uses: actions/cache@v3 + id: pods-cache + with: + path: ./ios/Pods + # We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not + # change unless the yarn version changes as well. + key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }} + + - name: 🔤 Compile translations + run: yarn intl:build + + - name: ✏️ Write environment variables + run: | + echo "${{ secrets.ENV_TOKEN }}" > .env + echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json + + - name: 🏗️ EAS Build + run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive + + - name: 🚀 Deploy + run: eas submit -p ios --non-interactive --path build.ipa diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml new file mode 100644 index 0000000000..72a38eaa65 --- /dev/null +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -0,0 +1,55 @@ +--- +name: Bundle and Deploy EAS Update + +on: + workflow_dispatch: + inputs: + runtimeVersion: + type: string + description: Runtime version (in x.x.x format) that this update is for + required: true + +jobs: + bundleDeploy: + name: Bundle and Deploy EAS Update + runs-on: ubuntu-latest + steps: + - name: 🧐 Validate version + run: | + [[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1 + + - name: ⬇️ Checkout + uses: actions/checkout@v4 + + - name: 🔧 Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: yarn + + - name: ⚙️ Install Dependencies + run: yarn install + + - name: 🪛 Install jq + uses: dcarbone/install-jq-action@v2 + + - name: ⛏️ Setup Expo + run: yarn global add eas-cli-local-build-plugin + + - name: 🔤 Compile Translations + run: yarn intl:build + + - name: ✏️ Write environment variables + run: | + export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' + echo "${{ secrets.ENV_TOKEN }}" > .env + echo "$json" > google-services.json + + - name: 🏗️ Create Bundle + run: yarn export + + - name: 📦 Package Bundle and 🚀 Deploy + run: yarn make-deploy-bundle + env: + DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }} + RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1d2a032c1e..9aa55ca07a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,23 +17,35 @@ jobs: - name: Check out Git repository uses: actions/checkout@v3 - name: Yarn install - run: yarn --frozen-lockfile + uses: Wandalen/wretry.action@master + with: + command: yarn --frozen-lockfile + attempt_limit: 3 + attempt_delay: 2000 - name: Lint check run: yarn lint + - name: Check & compile i18n + run: yarn intl:build - name: Type check run: yarn typecheck testing: name: Run tests runs-on: ubuntu-latest steps: - - name: Install node 18 - uses: actions/setup-node@v3 - with: - node-version: 18 - name: Check out Git repository uses: actions/checkout@v3 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc - name: Yarn install - run: yarn --frozen-lockfile + uses: Wandalen/wretry.action@master + with: + command: yarn --frozen-lockfile + attempt_limit: 3 + attempt_delay: 2000 + - name: Check & compile i18n + run: yarn intl:build - name: Run tests run: | - yarn test --forceExit + NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml new file mode 100644 index 0000000000..85ebae4dba --- /dev/null +++ b/.github/workflows/pull-request-commit.yml @@ -0,0 +1,185 @@ +# Credit https://github.com/expo/expo +# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml +--- +name: PR labeler + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-suite-fingerprint: + runs-on: ubuntu-22.04 + if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }} + # REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database + concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }} + permissions: + # REQUIRED: Allow comments of PRs + pull-requests: write + # REQUIRED: Allow updating fingerprint in acton caches + actions: write + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 100 + + - name: ⬇️ Fetch commits from base branch + run: git fetch origin main:main --depth 100 + if: github.event_name == 'pull_request' + + - name: 🔧 Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: yarn + + - name: ⚙️ Install Dependencies + run: yarn install + + - name: Get the base commit + id: base-commit + run: | + # Since we limit this pr-labeler workflow only triggered from limited paths, we should use custom base commit + echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT" + + - name: 📷 Check fingerprint + id: fingerprint + uses: expo/expo-github-action/fingerprint@main + with: + previous-git-commit: ${{ steps.base-commit.outputs.base-commit }} + + - name: 👀 Debug fingerprint + run: | + echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}" + echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}" + + - name: 🏷️ Labeling PR + uses: actions/github-script@v6 + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }} + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['bot: fingerprint changed'] + }) + } catch (e) { + if (e.status != 404) { + throw e; + } + } + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['bot: fingerprint compatible'] + }) + + - name: 🏷️ Labeling PR + uses: actions/github-script@v6 + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }} + with: + script: | + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['bot: fingerprint compatible'] + }) + } catch (e) { + if (e.status != 404) { + throw e; + } + } + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['bot: fingerprint changed'] + }) + + - name: 🔍 Find old comment if it exists + uses: peter-evans/find-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + id: old_comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: + + - name: 💬 Add comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id == '' }} + uses: actions/github-script@v6 + with: + script: | + const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2); + const body = ` + The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }} +
Fingerprint diff + + \`\`\`json + ${diff} + \`\`\` + +
+ + --- + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + + - name: 💬 Update comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id != '' }} + uses: actions/github-script@v6 + with: + script: | + const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2); + const body = ` + The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }} +
Fingerprint diff + + \`\`\`json + ${diff} + \`\`\` + +
+ + --- + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* + `; + + github.rest.issues.updateComment({ + issue_number: context.issue.number, + comment_id: '${{ steps.old_comment.outputs.comment-id }}', + owner: context.repo.owner, + repo: context.repo.repo, + body: body, + }); + + - name: 💬 Delete comment with fingerprint + if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' && steps.old_comment.outputs.comment-id != '' }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + comment_id: '${{ steps.old_comment.outputs.comment-id }}', + owner: context.repo.owner, + repo: context.repo.repo, + }); diff --git a/.gitignore b/.gitignore index 66658f8e4d..f96d0d5ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -91,12 +91,19 @@ web-build/ # Android & iOS folders -android/ -ios/ +/android/ +/ios/ # environment variables .env .env.* # Firebase (Android) Google services -google-services.json \ No newline at end of file +google-services.json + +# Performance results (Flashlight) +.perf/ + +# i18n +src/locale/locales/_build/ +src/locale/locales/**/*.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..3c032078a4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/Dockerfile b/Dockerfile index 388b742cc0..3ad05b6ec6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ COPY . . RUN mkdir --parents $NVM_DIR && \ wget \ --output-document=/tmp/nvm-install.sh \ - https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh && \ + https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \ bash /tmp/nvm-install.sh RUN \. "$NVM_DIR/nvm.sh" && \ @@ -31,6 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \ nvm use $NODE_VERSION && \ npm install --global yarn && \ yarn && \ + yarn intl:build && \ yarn build-web # DEBUG diff --git a/LICENSE b/LICENSE index d6da98bd58..601f94b4bf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 Bluesky PBC +Copyright 2023–2024 Bluesky PBC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Makefile b/Makefile index e93b6357a5..c90abb783e 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,12 @@ help: ## Print info about all commands .PHONY: build-web build-web: ## Compile web bundle, copy to bskyweb directory + yarn intl:build yarn build-web .PHONY: test test: ## Run all tests - yarn test + NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test .PHONY: lint lint: ## Run style checks and verify syntax diff --git a/README.md b/README.md index c32d726e41..49c4b016ff 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,33 @@ # Bluesky Social App -Welcome friends! This is the codebase for the Bluesky Social app. It serves as a resource to engineers building on the [AT Protocol](https://atproto.com). +Welcome friends! This is the codebase for the Bluesky Social app. + +Get the app itself: - **Web: [bsky.app](https://bsky.app)** - **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)** -- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&hl=en_US&gl=US)** +- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app)** -Links: +## Development Resources -- [Build instructions](./docs/build.md) -- [ATProto repo](https://github.com/bluesky-social/atproto) -- [ATProto docs](https://atproto.com) +This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto). -## Rules & guidelines +There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. ---- +The [Build Instructions](./docs/build.md) are a good place to get started with the app itself. -ℹ️ While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. +The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at: ---- +- [Overview and Guides](https://atproto.com/guides/overview) +- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions +- [Protocol Specifications](https://atproto.com/specs/atp) +- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol) + +The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`. + +## Contributions + +> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. **Rules:** @@ -56,8 +65,6 @@ If you discover any security issues, please send an email to security@bsky.app. Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more. -If you're a developer interested in building on atproto, we'd love to email you a Bluesky invite code. Simply share your GitHub (or similar) profile with us via [this form](https://forms.gle/BF21oxVNZiDjDhXF9). - ## License (MIT) See [./LICENSE](./LICENSE) for the full license. diff --git a/__e2e__/maestro/scroll.yaml b/__e2e__/maestro/scroll.yaml new file mode 100644 index 0000000000..2d32793eb0 --- /dev/null +++ b/__e2e__/maestro/scroll.yaml @@ -0,0 +1,77 @@ +# flow.yaml + +appId: xyz.blueskyweb.app +--- +- launchApp +# Login +# - runFlow: +# when: +# - tapOn: "Sign In" +# - tapOn: "Username or email address" +# - inputText: "ansh.bsky.team" +# - tapOn: "Password" +# - inputText: "PASSWORd" +# - tapOn: "Next" +# Allow notifications if popup is visible +# - runFlow: +# when: +# visible: "Notifications" +# commands: +# - tapOn: "Allow" +# Scroll in main feed +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +# Swipe between feeds +- swipe: + direction: "LEFT" +- swipe: + direction: "LEFT" +- swipe: + direction: "LEFT" +- swipe: + direction: "RIGHT" +- swipe: + direction: "RIGHT" +- swipe: + direction: "RIGHT" +# Go to Notifications +- tapOn: + id: "viewHeaderDrawerBtn" +- tapOn: "Notifications" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- swipe: + direction: "DOWN" # Make header visible +# Go to Feeds tab +- tapOn: + id: "viewHeaderDrawerBtn" +- tapOn: "Feeds" +- scrollUntilVisible: + element: "Discover" + direction: UP +- tapOn: "Discover" +- waitForAnimationToEnd +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" +# Click on post +- tapOn: + id: "postText" + index: 0 +- "scroll" +- "scroll" +- "scroll" +- "scroll" +- "scroll" + diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 6613f54d07..24f62a6470 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -1,5 +1,6 @@ import {createServer as createHTTPServer} from 'node:http' import {parse} from 'node:url' + import {createServer, TestPDS} from '../jest/test-pds' async function main() { @@ -14,7 +15,8 @@ async function main() { await server?.close() console.log('Starting new server') const inviteRequired = url?.query && 'invite' in url.query - server = await createServer({inviteRequired}) + const phoneRequired = url?.query && 'phone' in url.query + server = await createServer({inviteRequired, phoneRequired}) console.log('Listening at', server.pdsUrl) if (url?.query) { if ('users' in url.query) { @@ -502,6 +504,9 @@ async function main() { createdAt: new Date().toISOString(), }, ) + + // flush caches + await server.mocker.testNet.processAll() } } console.log('Ready') diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts index 6251ad0c8e..4aa25c95e6 100644 --- a/__e2e__/tests/composer.test.ts +++ b/__e2e__/tests/composer.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer, sleep} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp, sleep} from '../util' describe('Composer', () => { beforeAll(async () => { @@ -45,6 +48,8 @@ describe('Composer', () => { }) it('Reply text only', async () => { + await element(by.id('e2eRefreshHome')).tap() + const post = by.id('feedItem-by-alice.test') await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('composerTextInput')).typeText('Reply text only') diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts index 283eda3410..34b9895bfb 100644 --- a/__e2e__/tests/create-account.test.ts +++ b/__e2e__/tests/create-account.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, openApp} from '../util' describe('Create account', () => { let service: string @@ -10,27 +13,28 @@ describe('Create account', () => { }) it('I can create a new account', async () => { + await element(by.id('e2eOpenLoggedOutView')).tap() + await element(by.id('createAccountButton')).tap() await device.takeScreenshot('1- opened create account screen') - await element(by.id('otherServerBtn')).tap() + await element(by.id('selectServiceButton')).tap() await device.takeScreenshot('2- selected other server') - await element(by.id('customServerInput')).clearText() - await element(by.id('customServerInput')).typeText(service) + await element(by.id('customSelectBtn')).tap() + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() + await element(by.id('doneBtn')).tap() await device.takeScreenshot('3- input test server URL') - await element(by.id('nextBtn')).tap() await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('passwordInput')).typeText('hunter2') await device.takeScreenshot('4- entered account details') + await element(by.id('nextBtn')).tap() + await element(by.id('handleInput')).typeText('e2e-test') - await device.takeScreenshot('4- entered handle') + await device.takeScreenshot('5- entered handle') + await element(by.id('nextBtn')).tap() - await expect(element(by.id('welcomeOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('homeScreen'))).toBeVisible() + + await expect(element(by.id('onboardingInterests'))).toBeVisible() }) }) diff --git a/__e2e__/tests/curate-lists.test.ts b/__e2e__/tests/curate-lists.test.ts new file mode 100644 index 0000000000..a4deab6c19 --- /dev/null +++ b/__e2e__/tests/curate-lists.test.ts @@ -0,0 +1,212 @@ +/* eslint-env detox/detox */ + +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, loginAsBob, openApp, sleep} from '../util' + +describe('Curate lists', () => { + beforeAll(async () => { + await createServer('?users&follows&posts') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login and create a curatelists', async () => { + await loginAsAlice() + await element(by.id('e2eGotoLists')).tap() + await element(by.id('newUserListBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editNameInput')).typeText('Good Ppl') + await element(by.id('editDescriptionInput')).typeText('They good') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await element(by.text('About')).tap() + await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They good') + }) + + it('Edit display name and description via the edit curatelist modal', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editNameInput')).clearText() + await element(by.id('editNameInput')).typeText('Bad Ppl') + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('editDescriptionInput')).typeText('They bad') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They bad') + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Remove description via the edit curatelist modal', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('listDescription'))).not.toBeVisible() + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Set avi via the edit curatelist modal', async () => { + await expect(element(by.id('userAvatarFallback'))).toExist() + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('changeAvatarBtn')).tap() + await element(by.text('Upload from Library')).tap() + await sleep(3e3) + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('userAvatarImage'))).toExist() + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Remove avi via the edit curatelist modal', async () => { + await expect(element(by.id('userAvatarImage'))).toExist() + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('changeAvatarBtn')).tap() + await element(by.text('Remove Avatar')).tap() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('userAvatarFallback'))).toExist() + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Delete the curatelist', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Delete List')).tap() + await element(by.id('confirmBtn')).tap() + await expect(element(by.id('listsEmpty'))).toBeVisible() + }) + + it('Create a new curatelist', async () => { + await element(by.id('e2eGotoLists')).tap() + await element(by.id('newUserListBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editNameInput')).typeText('Good Ppl') + await element(by.id('editDescriptionInput')).typeText('They good') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await element(by.text('About')).tap() + await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They good') + }) + + it('Adds users on curatelists from the list', async () => { + await element(by.text('About')).tap() + await element(by.id('addUserBtn')).tap() + await expect(element(by.id('listAddUserModal'))).toBeVisible() + await waitFor(element(by.id('user-bob.test-addBtn'))) + .toBeVisible() + .withTimeout(5000) + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('listAddUserModal'))).not.toBeVisible() + await expect(element(by.id('user-bob.test'))).toBeVisible() + }) + + it('Shows posts by the users in the list', async () => { + await element(by.text('Posts')).tap() + await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() + }) + + it('Pins the list', async () => { + await expect(element(by.id('pinBtn'))).toBeVisible() + await element(by.id('pinBtn')).tap() + await element(by.id('e2eGotoHome')).tap() + await element(by.id('homeScreenFeedTabs-Good Ppl')).tap() + await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() + + await element(by.id('bottomBarFeedsBtn')).tap() + await element(by.id('saved-feed-Good Ppl')).tap() + await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() + + await element(by.id('unpinBtn')).tap() + await element(by.id('bottomBarHomeBtn')).tap() + await expect( + element(by.id('homeScreenFeedTabs-Good Ppl')), + ).not.toBeVisible() + + await element(by.id('e2eGotoLists')).tap() + await element(by.id('list-Good Ppl')).tap() + }) + + it('Removes users on curatelists from the list', async () => { + await element(by.text('About')).tap() + await expect(element(by.id('user-bob.test'))).toBeVisible() + await element(by.id('user-bob.test-editBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + }) + + it('Shows the curatelist on my profile', async () => { + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-5')).tap() + await element(by.id('list-Good Ppl')).tap() + }) + + it('Adds and removes users on curatelists from the profile', async () => { + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('bob') + await element(by.id('searchAutoCompleteResult-bob.test')).tap() + await expect(element(by.id('profileView'))).toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.text('Add to Lists')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.text('Add to Lists')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + }) + + it('Can report a user list', async () => { + await element(by.id('e2eGotoSettings')).tap() + await element(by.id('signOutBtn')).tap() + await loginAsBob() + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('alice') + await element(by.id('searchAutoCompleteResult-alice.test')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-3')).tap() + await element(by.id('list-Good Ppl')).tap() + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Report List')).tap() + await expect(element(by.id('reportModal'))).toBeVisible() + await expect(element(by.text('Report List'))).toBeVisible() + await element( + by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), + ).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts index 7647b55cbf..7bb72ec915 100644 --- a/__e2e__/tests/home-screen.test.ts +++ b/__e2e__/tests/home-screen.test.ts @@ -1,10 +1,13 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp} from '../util' describe('Home screen', () => { beforeAll(async () => { - await createServer('?users&follows&posts') + await createServer('?users&follows&posts&feeds') await openApp({permissions: {notifications: 'YES'}}) }) @@ -13,11 +16,28 @@ describe('Home screen', () => { await element(by.id('homeScreenFeedTabs-Following')).tap() }) + it('Can go to feeds page using feeds button in tab bar', async () => { + await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap() + await expect(element(by.text('Discover New Feeds'))).toBeVisible() + }) + + it('Feeds button disappears after pinning a feed', async () => { + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-4')).tap() + await element(by.id('feed-alice-favs')).tap() + await element(by.id('pinBtn')).tap() + await element(by.id('bottomBarHomeBtn')).tap() + await expect( + element(by.id('homeScreenFeedTabs-Feeds ✨')), + ).not.toBeVisible() + }) + it('Can like posts', async () => { const carlaPosts = by.id('feedItem-by-carla.test') await expect( element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), @@ -25,14 +45,14 @@ describe('Home screen', () => { await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can repost posts', async () => { const carlaPosts = by.id('feedItem-by-carla.test') await expect( element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() await expect(element(by.id('repostModal'))).toBeVisible() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() @@ -46,7 +66,7 @@ describe('Home screen', () => { await expect(element(by.id('repostModal'))).not.toBeVisible() await expect( element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can report posts', async () => { @@ -65,14 +85,14 @@ describe('Home screen', () => { it('Can swipe between feeds', async () => { await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) - await expect(element(by.id('whatshotFeedPage'))).toBeVisible() + await expect(element(by.id('customFeedPage'))).toBeVisible() await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) await expect(element(by.id('followingFeedPage'))).toBeVisible() }) it('Can tap between feeds', async () => { - await element(by.id("homeScreenFeedTabs-What's hot")).tap() - await expect(element(by.id('whatshotFeedPage'))).toBeVisible() + await element(by.id('homeScreenFeedTabs-alice-favs')).tap() + await expect(element(by.id('customFeedPage'))).toBeVisible() await element(by.id('homeScreenFeedTabs-Following')).tap() await expect(element(by.id('followingFeedPage'))).toBeVisible() }) diff --git a/__e2e__/tests/invite-codes.test.ts b/__e2e__/tests/invite-codes.test.ts index 124c1af903..9f00f05255 100644 --- a/__e2e__/tests/invite-codes.test.ts +++ b/__e2e__/tests/invite-codes.test.ts @@ -1,11 +1,9 @@ /* eslint-env detox/detox */ -/** - * This test is being skipped until we can resolve the detox crash issue - * with the side drawer. - */ +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' -import {openApp, loginAsAlice, createServer} from '../util' +import {createServer, loginAsAlice, openApp} from '../util' describe('invite-codes', () => { let service: string @@ -16,7 +14,6 @@ describe('invite-codes', () => { }) it('I can fetch invite codes', async () => { - await expect(element(by.id('signInButton'))).toBeVisible() await loginAsAlice() await element(by.id('e2eOpenInviteCodesModal')).tap() await expect(element(by.id('inviteCodesModal'))).toBeVisible() @@ -27,14 +24,16 @@ describe('invite-codes', () => { }) it('I can create a new account with the invite code', async () => { + await element(by.id('e2eOpenLoggedOutView')).tap() await element(by.id('createAccountButton')).tap() await device.takeScreenshot('1- opened create account screen') - await element(by.id('otherServerBtn')).tap() + await element(by.id('selectServiceButton')).tap() await device.takeScreenshot('2- selected other server') - await element(by.id('customServerInput')).clearText() - await element(by.id('customServerInput')).typeText(service) + await element(by.id('customSelectBtn')).tap() + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() + await element(by.id('doneBtn')).tap() await device.takeScreenshot('3- input test server URL') - await element(by.id('nextBtn')).tap() await element(by.id('inviteCodeInput')).typeText(inviteCode) await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('passwordInput')).typeText('hunter2') @@ -43,27 +42,6 @@ describe('invite-codes', () => { await element(by.id('handleInput')).typeText('e2e-test') await device.takeScreenshot('4- entered handle') await element(by.id('nextBtn')).tap() - await expect(element(by.id('welcomeOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible() - await element(by.id('continueBtn')).tap() - await expect(element(by.id('homeScreen'))).toBeVisible() - }) - - it('I get a notification for the new user', async () => { - await element(by.id('e2eSignOut')).tap() - await loginAsAlice() - await waitFor(element(by.id('homeScreen'))) - .toBeVisible() - .withTimeout(5000) - await element(by.id('bottomBarNotificationsBtn')).tap() - await expect(element(by.id('invitedUser'))).toBeVisible() - }) - - it('I can dismiss the new user notification', async () => { - await element(by.id('dismissBtn')).tap() - await expect(element(by.id('invitedUser'))).not.toBeVisible() + await expect(element(by.id('onboardingInterests'))).toBeVisible() }) }) diff --git a/__e2e__/tests/invites-and-text-verification.test.skip.ts b/__e2e__/tests/invites-and-text-verification.test.skip.ts new file mode 100644 index 0000000000..17c92b54cb --- /dev/null +++ b/__e2e__/tests/invites-and-text-verification.test.skip.ts @@ -0,0 +1,53 @@ +/* eslint-env detox/detox */ + +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp} from '../util' + +describe('invite-codes', () => { + let service: string + let inviteCode = '' + beforeAll(async () => { + service = await createServer('?users&invite&phone') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('I can fetch invite codes', async () => { + await loginAsAlice() + await element(by.id('e2eOpenInviteCodesModal')).tap() + await expect(element(by.id('inviteCodesModal'))).toBeVisible() + const attrs = await element(by.id('inviteCode-0-code')).getAttributes() + inviteCode = attrs.text + await element(by.id('closeBtn')).tap() + await element(by.id('e2eSignOut')).tap() + }) + + it('I can create a new account with the invite code', async () => { + await element(by.id('e2eOpenLoggedOutView')).tap() + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('selectServiceButton')).tap() + await device.takeScreenshot('2- selected other server') + await element(by.id('customSelectBtn')).tap() + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() + await element(by.id('doneBtn')).tap() + await device.takeScreenshot('3- input test server URL') + await element(by.id('inviteCodeInput')).typeText(inviteCode) + await element(by.id('emailInput')).typeText('example@test.com') + await element(by.id('passwordInput')).typeText('hunter2') + await device.takeScreenshot('4- entered account details') + await element(by.id('nextBtn')).tap() + await element(by.id('phoneInput')).typeText('2345551234') + await element(by.id('requestCodeBtn')).tap() + await device.takeScreenshot('5- requested code') + await element(by.id('codeInput')).typeText('000000') + await device.takeScreenshot('6- entered code') + await element(by.id('nextBtn')).tap() + await element(by.id('handleInput')).typeText('e2e-test') + await device.takeScreenshot('7- entered handle') + await element(by.id('nextBtn')).tap() + await expect(element(by.id('onboardingInterests'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/login.test.ts b/__e2e__/tests/login.test.ts index 788016db67..2bb02e39f1 100644 --- a/__e2e__/tests/login.test.ts +++ b/__e2e__/tests/login.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, login, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, login, openApp} from '../util' describe('Login', () => { let service: string @@ -10,6 +13,8 @@ describe('Login', () => { }) it('As Alice, I can login', async () => { + await element(by.id('e2eOpenLoggedOutView')).tap() + await expect(element(by.id('signInButton'))).toBeVisible() await login(service, 'alice', 'hunter2', { takeScreenshots: true, diff --git a/__e2e__/tests/merge-feed.test.ts b/__e2e__/tests/merge-feed.test.skip.ts similarity index 92% rename from __e2e__/tests/merge-feed.test.ts rename to __e2e__/tests/merge-feed.test.skip.ts index 903e34328d..1e110b4a60 100644 --- a/__e2e__/tests/merge-feed.test.ts +++ b/__e2e__/tests/merge-feed.test.skip.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp} from '../util' describe('Mergefeed', () => { beforeAll(async () => { @@ -9,8 +12,12 @@ describe('Mergefeed', () => { }) it('Login', async () => { + await element(by.id('e2eOpenLoggedOutView')).tap() await loginAsAlice() await element(by.id('e2eToggleMergefeed')).tap() + await element(by.id('bottomBarFeedsBtn')).tap() + await element(by.id('feed-alice-favs-toggleSave')).tap() + await element(by.id('e2eGotoHome')).tap() }) it('Sees the expected mix of posts with default filters', async () => { diff --git a/__e2e__/tests/mod-lists.test.ts b/__e2e__/tests/mod-lists.test.ts new file mode 100644 index 0000000000..e0928d3a46 --- /dev/null +++ b/__e2e__/tests/mod-lists.test.ts @@ -0,0 +1,190 @@ +/* eslint-env detox/detox */ + +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, loginAsBob, openApp} from '../util' + +describe('Mod lists', () => { + beforeAll(async () => { + await createServer('?users&follows&labels') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login and view my modlists', async () => { + await loginAsAlice() + await element(by.id('e2eGotoModeration')).tap() + await element(by.id('moderationlistsBtn')).tap() + await expect(element(by.id('list-Muted Users'))).toBeVisible() + await element(by.id('list-Muted Users')).tap() + await expect( + element(by.id('user-muted-by-list-account.test')), + ).toBeVisible() + }) + + it('Toggle mute subscription', async () => { + await element(by.id('unmuteBtn')).tap() + await element(by.id('subscribeBtn')).tap() + await element(by.text('Mute accounts')).tap() + await element(by.id('confirmBtn')).tap() + }) + + it('Edit display name and description via the edit modlist modal', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editNameInput')).clearText() + await element(by.id('editNameInput')).typeText('Bad Ppl') + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('editDescriptionInput')).typeText('They bad') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They bad') + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + it('Remove description via the edit modlist modal', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Edit list details')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editDescriptionInput')).clearText() + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('listDescription'))).not.toBeVisible() + // have to wait for the toast to clear + await waitFor(element(by.id('headerDropdownBtn'))) + .toBeVisible() + .withTimeout(5000) + }) + + // DISABLED e2e environment is real finicky about avatar uploads -prf + // it('Set avi via the edit modlist modal', async () => { + // await expect(element(by.id('userAvatarFallback'))).toExist() + // await element(by.id('headerDropdownBtn')).tap() + // await element(by.text('Edit list details')).tap() + // await expect(element(by.id('createOrEditListModal'))).toBeVisible() + // await element(by.id('changeAvatarBtn')).tap() + // await element(by.text('Library')).tap() + // await sleep(3e3) + // await element(by.id('saveBtn')).tap() + // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + // await expect(element(by.id('userAvatarImage'))).toExist() + // // have to wait for the toast to clear + // await waitFor(element(by.id('headerDropdownBtn'))) + // .toBeVisible() + // .withTimeout(5000) + // }) + + // it('Remove avi via the edit modlist modal', async () => { + // await expect(element(by.id('userAvatarImage'))).toExist() + // await element(by.id('headerDropdownBtn')).tap() + // await element(by.text('Edit list details')).tap() + // await expect(element(by.id('createOrEditListModal'))).toBeVisible() + // await element(by.id('changeAvatarBtn')).tap() + // await element(by.text('Remove')).tap() + // await element(by.id('saveBtn')).tap() + // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + // await expect(element(by.id('userAvatarFallback'))).toExist() + // // have to wait for the toast to clear + // await waitFor(element(by.id('headerDropdownBtn'))) + // .toBeVisible() + // .withTimeout(5000) + // }) + + it('Delete the modlist', async () => { + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Delete List')).tap() + await element(by.id('confirmBtn')).tap() + await expect(element(by.id('listsEmpty'))).toBeVisible() + }) + + it('Create a new modlist', async () => { + await element(by.id('newModListBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).toBeVisible() + await element(by.id('editNameInput')).typeText('Bad Ppl') + await element(by.id('editDescriptionInput')).typeText('They bad') + await element(by.id('saveBtn')).tap() + await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() + await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') + await expect(element(by.id('listDescription'))).toHaveText('They bad') + }) + + it('Adds and removes users on modlists from the list', async () => { + await element(by.id('addUserBtn')).tap() + await expect(element(by.id('listAddUserModal'))).toBeVisible() + await waitFor(element(by.id('user-warn-posts.test-addBtn'))) + .toBeVisible() + .withTimeout(5000) + await element(by.id('user-warn-posts.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('listAddUserModal'))).not.toBeVisible() + await element(by.id('listItems-flatlist')).swipe( + 'down', + 'slow', + 1, + 0.5, + 0.5, + ) + await expect(element(by.id('user-warn-posts.test'))).toBeVisible() + await element(by.id('user-warn-posts.test-editBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-warn-posts.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + }) + + it('Shows the modlist on my profile', async () => { + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-5')).tap() + await element(by.id('list-Bad Ppl')).tap() + }) + + it('Adds and removes users on modlists from the profile', async () => { + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('bob') + await element(by.id('searchAutoCompleteResult-bob.test')).tap() + await expect(element(by.id('profileView'))).toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.text('Add to Lists')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.text('Add to Lists')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() + await element(by.id('user-bob.test-addBtn')).tap() + await element(by.id('doneBtn')).tap() + await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() + }) + + it('Can report a mute list', async () => { + await element(by.id('e2eGotoSettings')).tap() + await element(by.id('signOutBtn')).tap() + await loginAsBob() + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('alice') + await element(by.id('searchAutoCompleteResult-alice.test')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-3')).tap() + await element(by.id('list-Bad Ppl')).tap() + await element(by.id('headerDropdownBtn')).tap() + await element(by.text('Report List')).tap() + await expect(element(by.id('reportModal'))).toBeVisible() + await expect(element(by.text('Report List'))).toBeVisible() + await element( + by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), + ).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/tests/mute-lists.test.ts b/__e2e__/tests/mute-lists.test.ts deleted file mode 100644 index 6c46de0ec0..0000000000 --- a/__e2e__/tests/mute-lists.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* eslint-env detox/detox */ - -import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util' - -describe('Mute lists', () => { - beforeAll(async () => { - await createServer('?users&follows&labels') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login and view my mutelists', async () => { - await expect(element(by.id('signInButton'))).toBeVisible() - await loginAsAlice() - await element(by.id('e2eGotoModeration')).tap() - await element(by.id('mutelistsBtn')).tap() - await expect(element(by.id('list-Muted Users'))).toBeVisible() - await element(by.id('list-Muted Users')).tap() - await expect( - element(by.id('user-muted-by-list-account.test')), - ).toBeVisible() - }) - - it('Toggle subscription', async () => { - await element(by.id('unsubscribeListBtn')).tap() - await element(by.id('subscribeListBtn')).tap() - }) - - it('Edit display name and description via the edit mutelist modal', async () => { - await element(by.id('editListBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() - await element(by.id('editNameInput')).clearText() - await element(by.id('editNameInput')).typeText('Bad Ppl') - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('editDescriptionInput')).typeText('They bad') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() - await expect(element(by.id('listName'))).toHaveText('Bad Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They bad') - // have to wait for the toast to clear - await waitFor(element(by.id('editListBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Remove description via the edit mutelist modal', async () => { - await element(by.id('editListBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() - await expect(element(by.id('listDescription'))).not.toBeVisible() - // have to wait for the toast to clear - await waitFor(element(by.id('editListBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Set avi via the edit mutelist modal', async () => { - await expect(element(by.id('userAvatarFallback'))).toExist() - await element(by.id('editListBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Library')).tap() - await sleep(3e3) - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() - await expect(element(by.id('userAvatarImage'))).toExist() - // have to wait for the toast to clear - await waitFor(element(by.id('editListBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Remove avi via the edit mutelist modal', async () => { - await expect(element(by.id('userAvatarImage'))).toExist() - await element(by.id('editListBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Remove')).tap() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() - await expect(element(by.id('userAvatarFallback'))).toExist() - // have to wait for the toast to clear - await waitFor(element(by.id('editListBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Delete the mutelist', async () => { - await element(by.id('deleteListBtn')).tap() - await element(by.id('confirmBtn')).tap() - await expect(element(by.id('emptyMuteLists'))).toBeVisible() - }) - - it('Create a new mutelist', async () => { - await element(by.id('emptyMuteLists-button')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible() - await element(by.id('editNameInput')).typeText('Bad Ppl') - await element(by.id('editDescriptionInput')).typeText('They bad') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible() - await expect(element(by.id('listName'))).toHaveText('Bad Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They bad') - // have to wait for the toast to clear - await waitFor(element(by.id('editListBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Shows the mutelist on my profile', async () => { - await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('selector')).swipe('left') - await element(by.id('selector-4')).tap() - await element(by.id('list-Bad Ppl')).tap() - }) - - it('Adds and removes users on mutelists', async () => { - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible() - await element(by.id('toggleBtn-Bad Ppl')).tap() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible() - await element(by.id('toggleBtn-Bad Ppl')).tap() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible() - }) - - it('Can report a mute list', async () => { - await element(by.id('e2eGotoSettings')).tap() - await element(by.id('signOutBtn')).tap() - await loginAsBob() - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('alice') - await element(by.id('searchAutoCompleteResult-alice.test')).tap() - await element(by.id('selector')).swipe('left') - await element(by.id('selector-3')).tap() - await element(by.id('list-Bad Ppl')).tap() - await element(by.id('reportListBtn')).tap() - await expect(element(by.id('reportModal'))).toBeVisible() - await expect(element(by.text('Report List'))).toBeVisible() - await element( - by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), - ).tap() - await element(by.id('sendReportBtn')).tap() - await expect(element(by.id('reportModal'))).not.toBeVisible() - }) -}) diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts index 82722f381b..8b79f26711 100644 --- a/__e2e__/tests/profile-screen.test.ts +++ b/__e2e__/tests/profile-screen.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer, sleep} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp, sleep} from '../util' describe('Profile screen', () => { beforeAll(async () => { @@ -11,17 +14,16 @@ describe('Profile screen', () => { }) it('Login and navigate to my profile', async () => { - await expect(element(by.id('signInButton'))).toBeVisible() await loginAsAlice() await element(by.id('bottomBarProfileBtn')).tap() }) it('Can see feeds', async () => { - await element(by.id('selector')).swipe('left') - await element(by.id('selector-4')).tap() + await element(by.id('profilePager-selector')).swipe('left') + await element(by.id('profilePager-selector-4')).tap() await expect(element(by.id('feed-alice-favs'))).toBeVisible() - await element(by.id('selector')).swipe('right') - await element(by.id('selector-0')).tap() + await element(by.id('profilePager-selector')).swipe('right') + await element(by.id('profilePager-selector-0')).tap() }) it('Open and close edit profile modal', async () => { @@ -69,10 +71,10 @@ describe('Profile screen', () => { await element(by.id('profileHeaderEditProfileButton')).tap() await expect(element(by.id('editProfileModal'))).toBeVisible() await element(by.id('changeBannerBtn')).tap() - await element(by.text('Library')).tap() + await element(by.text('Upload from Library')).tap() await sleep(3e3) await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Library')).tap() + await element(by.text('Upload from Library')).tap() await sleep(3e3) await element(by.id('editProfileSaveBtn')).tap() await expect(element(by.id('editProfileModal'))).not.toBeVisible() @@ -86,9 +88,9 @@ describe('Profile screen', () => { await element(by.id('profileHeaderEditProfileButton')).tap() await expect(element(by.id('editProfileModal'))).toBeVisible() await element(by.id('changeBannerBtn')).tap() - await element(by.text('Remove')).tap() + await element(by.text('Remove Banner')).tap() await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Remove')).tap() + await element(by.text('Remove Avatar')).tap() await element(by.id('editProfileSaveBtn')).tap() await expect(element(by.id('editProfileModal'))).not.toBeVisible() await expect(element(by.id('userBannerFallback'))).toExist() @@ -135,10 +137,18 @@ describe('Profile screen', () => { }) it('Can like posts', async () => { + await element(by.id('postsFeed-flatlist')).swipe( + 'down', + 'slow', + 1, + 0.5, + 0.5, + ) + const posts = by.id('feedItem-by-bob.test') await expect( element(by.id('likeCount').withAncestor(posts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(posts)).atIndex(0), @@ -146,14 +156,14 @@ describe('Profile screen', () => { await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(posts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can repost posts', async () => { const posts = by.id('feedItem-by-bob.test') await expect( element(by.id('repostCount').withAncestor(posts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() await expect(element(by.id('repostModal'))).toBeVisible() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() @@ -167,7 +177,7 @@ describe('Profile screen', () => { await expect(element(by.id('repostModal'))).not.toBeVisible() await expect( element(by.id('repostCount').withAncestor(posts)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can report posts', async () => { diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts index 8b3f55b3d0..8f8f436171 100644 --- a/__e2e__/tests/search-screen.test.ts +++ b/__e2e__/tests/search-screen.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp} from '../util' describe('Search screen', () => { beforeAll(async () => { diff --git a/__e2e__/tests/self-labeling.test.ts b/__e2e__/tests/self-labeling.test.ts index 68678688d6..da69796ff6 100644 --- a/__e2e__/tests/self-labeling.test.ts +++ b/__e2e__/tests/self-labeling.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer, sleep} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp, sleep} from '../util' describe('Self-labeling', () => { beforeAll(async () => { @@ -26,6 +29,7 @@ describe('Self-labeling', () => { await element(by.id('composerPublishBtn')).tap() await expect(element(by.id('composeFAB'))).toBeVisible() const posts = by.id('feedItem-by-alice.test') + await element(by.id('e2eRefreshHome')).tap() await expect( element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0), ).toExist() diff --git a/__e2e__/tests/shell.test.ts b/__e2e__/tests/shell.test.skip.ts similarity index 94% rename from __e2e__/tests/shell.test.ts rename to __e2e__/tests/shell.test.skip.ts index 69619dd81b..2bb93aa803 100644 --- a/__e2e__/tests/shell.test.ts +++ b/__e2e__/tests/shell.test.skip.ts @@ -1,6 +1,6 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer} from '../util' +import {createServer, loginAsAlice, openApp} from '../util' describe('Shell', () => { beforeAll(async () => { diff --git a/__e2e__/tests/text-verification.test.skip.ts b/__e2e__/tests/text-verification.test.skip.ts new file mode 100644 index 0000000000..8be09c5e9b --- /dev/null +++ b/__e2e__/tests/text-verification.test.skip.ts @@ -0,0 +1,84 @@ +/* eslint-env detox/detox */ + +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, openApp} from '../util' + +describe('Create account', () => { + let service: string + beforeAll(async () => { + service = await createServer('?phone') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('I can create a new account with text verification', async () => { + console.log('SERVICE IS', service) + await element(by.id('e2eOpenLoggedOutView')).tap() + + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('selectServiceButton')).tap() + await device.takeScreenshot('2- selected other server') + await element(by.id('customSelectBtn')).tap() + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() + await element(by.id('doneBtn')).tap() + await device.takeScreenshot('3- input test server URL') + await element(by.id('emailInput')).typeText('text-verification@test.com') + await element(by.id('passwordInput')).typeText('hunter2') + await device.takeScreenshot('4- entered account details') + await element(by.id('nextBtn')).tap() + + await element(by.id('handleInput')).typeText('text-verification-test') + await device.takeScreenshot('5- entered handle') + await element(by.id('nextBtn')).tap() + + await element(by.id('phoneInput')).typeText('8042221111') + await element(by.id('requestCodeBtn')).tap() + await device.takeScreenshot('6- requested code') + + await element(by.id('codeInput')).typeText('000000') + await device.takeScreenshot('7- entered code') + await element(by.id('nextBtn')).tap() + + await element(by.id('nextBtn')).tap() + + await expect(element(by.id('onboardingInterests'))).toBeVisible() + }) + + it('failed text verification correctly goes back to the code input screen', async () => { + await element(by.id('e2eSignOut')).tap() + await element(by.id('e2eOpenLoggedOutView')).tap() + + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('selectServiceButton')).tap() + await device.takeScreenshot('2- selected other server') + await element(by.id('customSelectBtn')).tap() + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() + await element(by.id('doneBtn')).tap() + await device.takeScreenshot('3- input test server URL') + await element(by.id('emailInput')).typeText('text-verification2@test.com') + await element(by.id('passwordInput')).typeText('hunter2') + await device.takeScreenshot('4- entered account details') + await element(by.id('nextBtn')).tap() + + await element(by.id('phoneInput')).typeText('8042221111') + await element(by.id('requestCodeBtn')).tap() + await device.takeScreenshot('5- requested code') + + await element(by.id('codeInput')).typeText('111111') + await device.takeScreenshot('6- entered code') + await element(by.id('nextBtn')).tap() + + await element(by.id('handleInput')).typeText('text-verification-test2') + await device.takeScreenshot('7- entered handle') + + await element(by.id('nextBtn')).tap() + + await expect(element(by.id('codeInput'))).toBeVisible() + await device.takeScreenshot('8- got error') + }) +}) diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts index 3b2dc1221f..410859f16b 100644 --- a/__e2e__/tests/thread-muting.test.ts +++ b/__e2e__/tests/thread-muting.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, loginAsBob, openApp} from '../util' describe('Thread muting', () => { beforeAll(async () => { @@ -48,7 +51,7 @@ describe('Thread muting', () => { await loginAsBob() await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('selector-1')).tap() + await element(by.id('profilePager-selector-1')).tap() const bobPosts = by.id('feedItem-by-bob.test') await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap() await element(by.id('composerTextInput')).typeText('Reply 2') diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts index 02831d055e..5b9fc3c357 100644 --- a/__e2e__/tests/thread-screen.test.ts +++ b/__e2e__/tests/thread-screen.test.ts @@ -1,6 +1,9 @@ /* eslint-env detox/detox */ -import {openApp, loginAsAlice, createServer} from '../util' +import {beforeAll, describe, it} from '@jest/globals' +import {expect} from 'detox' + +import {createServer, loginAsAlice, openApp} from '../util' describe('Thread screen', () => { beforeAll(async () => { @@ -31,15 +34,15 @@ describe('Thread screen', () => { it('Can like the root post', async () => { const post = by.id('postThreadItem-by-bob.test') await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), + element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), ).not.toExist() await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), + element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), ).toHaveText('1 like') await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), + element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), ).not.toExist() }) @@ -47,7 +50,7 @@ describe('Thread screen', () => { const post = by.id('postThreadItem-by-carla.test') await expect( element(by.id('likeCount').withAncestor(post)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(post)).atIndex(0), @@ -55,27 +58,27 @@ describe('Thread screen', () => { await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await expect( element(by.id('likeCount').withAncestor(post)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can repost the root post', async () => { const post = by.id('postThreadItem-by-bob.test') await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), + element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), ).not.toExist() await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() await expect(element(by.id('repostModal'))).toBeVisible() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() await expect(element(by.id('repostModal'))).not.toBeVisible() await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), + element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), ).toHaveText('1 repost') await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() await expect(element(by.id('repostModal'))).toBeVisible() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() await expect(element(by.id('repostModal'))).not.toBeVisible() await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), + element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), ).not.toExist() }) @@ -83,7 +86,7 @@ describe('Thread screen', () => { const post = by.id('postThreadItem-by-carla.test') await expect( element(by.id('repostCount').withAncestor(post)).atIndex(0), - ).toHaveText('0') + ).not.toExist() await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() await expect(element(by.id('repostModal'))).toBeVisible() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() @@ -97,7 +100,7 @@ describe('Thread screen', () => { await expect(element(by.id('repostModal'))).not.toBeVisible() await expect( element(by.id('repostCount').withAncestor(post)).atIndex(0), - ).toHaveText('0') + ).not.toExist() }) it('Can report the root post', async () => { diff --git a/__e2e__/util.ts b/__e2e__/util.ts index f6f3b1b806..6f02257623 100644 --- a/__e2e__/util.ts +++ b/__e2e__/util.ts @@ -1,5 +1,6 @@ -import {resolveConfig} from 'detox/internals' import {execSync} from 'child_process' +import {resolveConfig} from 'detox/internals' +import http from 'http' const platform = device.getPlatform() @@ -55,9 +56,10 @@ export async function login( if (takeScreenshots) { await device.takeScreenshot('2- opened service selector') } + await element(by.id('customSelectBtn')).tap() await element(by.id('customServerTextInput')).typeText(service) await element(by.id('customServerTextInput')).tapReturnKey() - await element(by.id('customServerSelectBtn')).tap() + await element(by.id('doneBtn')).tap() if (takeScreenshots) { await device.takeScreenshot('3- input custom service') } @@ -104,10 +106,30 @@ async function openAppForDebugBuild(platform: string, opts: any) { await sleep(3000) } -export async function createServer(path = '') { - const res = await fetch(`http://localhost:1986/${path}`, {method: 'POST'}) - const resBody = await res.text() - return resBody +export async function createServer(path = ''): Promise { + return new Promise(function (resolve, reject) { + var req = http.request( + { + method: 'POST', + host: 'localhost', + port: 1986, + path: `/${path}`, + }, + function (res) { + const body: Buffer[] = [] + res.on('data', chunk => body.push(chunk)) + res.on('end', function () { + try { + resolve(Buffer.concat(body).toString()) + } catch (e) { + reject(e) + } + }) + }, + ) + req.on('error', reject) + req.end() + }) } const getDeepLinkUrl = (url: string) => diff --git a/__mocks__/sentry-expo.js b/__mocks__/sentry-expo.js new file mode 100644 index 0000000000..5a0e644e8a --- /dev/null +++ b/__mocks__/sentry-expo.js @@ -0,0 +1,3 @@ +jest.mock('sentry-expo', () => ({ + init: () => jest.fn(), +})) diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 63bd785ea7..0b6c2c9444 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -1,17 +1,18 @@ import {RichText} from '@atproto/api' -import { - getYoutubeVideoId, - makeRecordUri, - toNiceDomain, - toShortUrl, - toShareUrl, -} from '../../src/lib/strings/url-helpers' -import {pluralize, enforceLen} from '../../src/lib/strings/helpers' -import {ago} from '../../src/lib/strings/time' + +import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import {cleanError} from '../../src/lib/strings/errors' +import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' +import {enforceLen, pluralize} from '../../src/lib/strings/helpers' import {detectLinkables} from '../../src/lib/strings/rich-text-detection' import {shortenLinks} from '../../src/lib/strings/rich-text-manip' -import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' -import {cleanError} from '../../src/lib/strings/errors' +import {ago} from '../../src/lib/strings/time' +import { + makeRecordUri, + toNiceDomain, + toShareUrl, + toShortUrl, +} from '../../src/lib/strings/url-helpers' describe('detectLinkables', () => { const inputs = [ @@ -335,32 +336,6 @@ describe('toShareUrl', () => { }) }) -describe('getYoutubeVideoId', () => { - it(' should return undefined for invalid youtube links', () => { - expect(getYoutubeVideoId('')).toBeUndefined() - expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined() - expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined() - expect( - getYoutubeVideoId('https://www.youtube.com/channelName'), - ).toBeUndefined() - expect( - getYoutubeVideoId('https://www.youtube.com/channel/channelName'), - ).toBeUndefined() - }) - - it('getYoutubeVideoId should return video id for valid youtube links', () => { - expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe( - 'videoId', - ) - expect( - getYoutubeVideoId( - 'https://www.youtube.com/watch?v=videoId&feature=share', - ), - ).toBe('videoId') - expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId') - }) -}) - describe('shortenLinks', () => { const inputs = [ 'start https://middle.com/foo/bar?baz=bux#hash end', @@ -396,6 +371,7 @@ describe('shortenLinks', () => { ], ], ] + it('correctly shortens rich text while preserving facet URIs', () => { for (let i = 0; i < inputs.length; i++) { const input = inputs[i] @@ -410,3 +386,374 @@ describe('shortenLinks', () => { } }) }) + +describe('parseEmbedPlayerFromUrl', () => { + const inputs = [ + 'https://youtu.be/videoId', + 'https://www.youtube.com/watch?v=videoId', + 'https://www.youtube.com/watch?v=videoId&feature=share', + 'https://youtube.com/watch?v=videoId', + 'https://youtube.com/watch?v=videoId&feature=share', + 'https://youtube.com/shorts/videoId', + 'https://m.youtube.com/watch?v=videoId', + + 'https://youtube.com/shorts/', + 'https://youtube.com/', + 'https://youtube.com/random', + + 'https://twitch.tv/channelName', + 'https://www.twitch.tv/channelName', + 'https://m.twitch.tv/channelName', + + 'https://twitch.tv/channelName/clip/clipId', + 'https://twitch.tv/videos/videoId', + + 'https://open.spotify.com/playlist/playlistId', + 'https://open.spotify.com/playlist/playlistId?param=value', + 'https://open.spotify.com/locale/playlist/playlistId', + + 'https://open.spotify.com/track/songId', + 'https://open.spotify.com/track/songId?param=value', + 'https://open.spotify.com/locale/track/songId', + + 'https://open.spotify.com/album/albumId', + 'https://open.spotify.com/album/albumId?param=value', + 'https://open.spotify.com/locale/album/albumId', + + 'https://soundcloud.com/user/track', + 'https://soundcloud.com/user/sets/set', + 'https://soundcloud.com/user/', + + 'https://music.apple.com/us/playlist/playlistName/playlistId', + 'https://music.apple.com/us/album/albumName/albumId', + 'https://music.apple.com/us/album/albumName/albumId?i=songId', + + 'https://vimeo.com/videoId', + 'https://vimeo.com/videoId?autoplay=0', + + 'https://giphy.com/gifs/some-random-gif-name-gifId', + 'https://giphy.com/gif/some-random-gif-name-gifId', + 'https://giphy.com/gifs/', + + 'https://media.giphy.com/media/gifId/giphy.webp', + 'https://media0.giphy.com/media/gifId/giphy.webp', + 'https://media1.giphy.com/media/gifId/giphy.gif', + 'https://media2.giphy.com/media/gifId/giphy.webp', + 'https://media3.giphy.com/media/gifId/giphy.mp4', + 'https://media4.giphy.com/media/gifId/giphy.webp', + 'https://media5.giphy.com/media/gifId/giphy.mp4', + 'https://media0.giphy.com/media/gifId/giphy.mp3', + 'https://media1.google.com/media/gifId/giphy.webp', + + 'https://media.giphy.com/media/trackingId/gifId/giphy.webp', + + 'https://i.giphy.com/media/gifId/giphy.webp', + 'https://i.giphy.com/media/gifId/giphy.webp', + 'https://i.giphy.com/gifId.gif', + 'https://i.giphy.com/gifId.gif', + + 'https://tenor.com/view/gifId', + 'https://tenor.com/notView/gifId', + 'https://tenor.com/view', + 'https://tenor.com/view/gifId.gif', + 'https://tenor.com/intl/view/gifId.gif', + ] + + const outputs = [ + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_short', + source: 'youtubeShorts', + hideDetails: true, + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, + + undefined, + undefined, + undefined, + + { + type: 'twitch_video', + source: 'twitch', + playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`, + }, + { + type: 'twitch_video', + source: 'twitch', + playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`, + }, + { + type: 'twitch_video', + source: 'twitch', + playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`, + }, + { + type: 'twitch_video', + source: 'twitch', + playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=clipId&parent=localhost`, + }, + { + type: 'twitch_video', + source: 'twitch', + playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=videoId&parent=localhost`, + }, + + { + type: 'spotify_playlist', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/playlist/playlistId`, + }, + { + type: 'spotify_playlist', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/playlist/playlistId`, + }, + { + type: 'spotify_playlist', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/playlist/playlistId`, + }, + + { + type: 'spotify_song', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/track/songId`, + }, + { + type: 'spotify_song', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/track/songId`, + }, + { + type: 'spotify_song', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/track/songId`, + }, + + { + type: 'spotify_album', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/album/albumId`, + }, + { + type: 'spotify_album', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/album/albumId`, + }, + { + type: 'spotify_album', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/album/albumId`, + }, + + { + type: 'soundcloud_track', + source: 'soundcloud', + playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`, + }, + { + type: 'soundcloud_set', + source: 'soundcloud', + playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`, + }, + undefined, + + { + type: 'apple_music_playlist', + source: 'appleMusic', + playerUri: + 'https://embed.music.apple.com/us/playlist/playlistName/playlistId', + }, + { + type: 'apple_music_album', + source: 'appleMusic', + playerUri: 'https://embed.music.apple.com/us/album/albumName/albumId', + }, + { + type: 'apple_music_song', + source: 'appleMusic', + playerUri: + 'https://embed.music.apple.com/us/album/albumName/albumId?i=songId', + }, + + { + type: 'vimeo_video', + source: 'vimeo', + playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1', + }, + { + type: 'vimeo_video', + source: 'vimeo', + playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1', + }, + + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + undefined, + undefined, + + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + undefined, + undefined, + undefined, + + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/gifId', + playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + }, + + { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri: 'https://tenor.com/view/gifId.gif', + }, + undefined, + undefined, + { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri: 'https://tenor.com/view/gifId.gif', + }, + { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri: 'https://tenor.com/intl/view/gifId.gif', + }, + ] + + it('correctly grabs the correct id from uri', () => { + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i] + const output = outputs[i] + + const res = parseEmbedPlayerFromUrl(input) + + expect(res).toEqual(output) + } + }) +}) diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts new file mode 100644 index 0000000000..247f5850e2 --- /dev/null +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -0,0 +1,179 @@ +import {describe, expect, it} from '@jest/globals' + +import { + isPossiblyAUrl, + isTrustedUrl, + linkRequiresWarning, + splitApexDomain, +} from '../../../src/lib/strings/url-helpers' + +describe('linkRequiresWarning', () => { + type Case = [string, string, boolean] + + const cases: Case[] = [ + ['http://example.com', 'http://example.com', false], + ['http://example.com', 'example.com', false], + ['http://example.com', 'example.com/page', false], + ['http://example.com', '', true], + ['http://example.com', 'other.com', true], + ['http://example.com', 'http://other.com', true], + ['http://example.com', 'some label', true], + ['http://example.com', 'example.com more', true], + ['http://example.com', 'http://example.co', true], + ['http://example.co', 'http://example.com', true], + ['http://example.com', 'example.co', true], + ['http://example.co', 'example.com', true], + ['http://site.pages.dev', 'http://site.page', true], + ['http://site.page', 'http://site.pages.dev', true], + ['http://site.pages.dev', 'site.page', true], + ['http://site.page', 'site.pages.dev', true], + ['http://site.pages.dev', 'http://site.pages', true], + ['http://site.pages', 'http://site.pages.dev', true], + ['http://site.pages.dev', 'site.pages', true], + ['http://site.pages', 'site.pages.dev', true], + ['http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false], + ['https://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false], + ['http://bsky.app/', 'bluesky', false], + ['https://bsky.app/', 'bluesky', false], + [ + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + false, + ], + [ + 'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + false, + ], + [ + 'http://bsky.app/', + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + false, + ], + [ + 'https://bsky.app/', + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + false, + ], + [ + 'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + 'https://google.com', + true, + ], + [ + 'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v', + 'https://google.com', + true, + ], + ['http://bsky.app/', 'https://google.com', true], + ['https://bsky.app/', 'https://google.com', true], + + // case insensitive + ['https://Example.com', 'example.com', false], + ['https://example.com', 'Example.com', false], + + // bad uri inputs, default to true + ['', '', true], + ['example.com', 'example.com', true], + ['/profile', 'Username', false], + ['#', 'Show More', false], + ['https://docs.bsky.app', 'https://docs.bsky.app', false], + ['https://bsky.app/compose/intent?text=test', 'Compose a post', false], + ] + + it.each(cases)( + 'given input uri %p and text %p, returns %p', + (uri, text, expected) => { + const output = linkRequiresWarning(uri, text) + expect(output).toEqual(expected) + }, + ) +}) + +describe('isPossiblyAUrl', () => { + type Case = [string, boolean] + const cases: Case[] = [ + ['', false], + ['text', false], + ['some text', false], + ['some text', false], + ['some domain.com', false], + ['domain.com', true], + [' domain.com', true], + ['domain.com ', true], + [' domain.com ', true], + ['http://domain.com', true], + [' http://domain.com', true], + ['http://domain.com ', true], + [' http://domain.com ', true], + ['https://domain.com', true], + [' https://domain.com', true], + ['https://domain.com ', true], + [' https://domain.com ', true], + ['http://domain.com/foo', true], + ['http://domain.com stuff', true], + ] + + it.each(cases)('given input uri %p, returns %p', (str, expected) => { + const output = isPossiblyAUrl(str) + expect(output).toEqual(expected) + }) +}) + +describe('splitApexDomain', () => { + type Case = [string, string, string] + const cases: Case[] = [ + ['', '', ''], + ['example.com', '', 'example.com'], + ['foo.example.com', 'foo.', 'example.com'], + ['foo.bar.example.com', 'foo.bar.', 'example.com'], + ['example.co.uk', '', 'example.co.uk'], + ['foo.example.co.uk', 'foo.', 'example.co.uk'], + ['example.nonsense', '', 'example.nonsense'], + ['foo.example.nonsense', '', 'foo.example.nonsense'], + ['foo.bar.example.nonsense', '', 'foo.bar.example.nonsense'], + ['example.com.example.com', 'example.com.', 'example.com'], + ] + + it.each(cases)( + 'given input uri %p, returns %p,%p', + (str, expected1, expected2) => { + const output = splitApexDomain(str) + expect(output[0]).toEqual(expected1) + expect(output[1]).toEqual(expected2) + }, + ) +}) + +describe('isTrustedUrl', () => { + const cases = [ + ['#', true], + ['#profile', true], + ['/', true], + ['/profile', true], + ['/profile/', true], + ['/profile/bob.test', true], + ['https://bsky.app', true], + ['https://bsky.app/', true], + ['https://bsky.app/profile/bob.test', true], + ['https://www.bsky.app', true], + ['https://www.bsky.app/', true], + ['https://docs.bsky.app', true], + ['https://bsky.social', true], + ['https://bsky.social/blog', true], + ['https://blueskyweb.xyz', true], + ['https://blueskyweb.zendesk.com', true], + ['http://bsky.app', true], + ['http://bsky.social', true], + ['http://blueskyweb.xyz', true], + ['http://blueskyweb.zendesk.com', true], + ['https://google.com', false], + ['https://docs.google.com', false], + ['https://google.com/#', false], + ] + + it.each(cases)('given input uri %p, returns %p', (str, expected) => { + const output = isTrustedUrl(str) + expect(output).toEqual(expected) + }) +}) diff --git a/app.config.js b/app.config.js index 2073b42135..2951e9433e 100644 --- a/app.config.js +++ b/app.config.js @@ -1,25 +1,62 @@ -module.exports = function () { - const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN +const pkg = require('./package.json') + +const SPLASH_CONFIG = { + backgroundColor: '#ffffff', + image: './assets/splash.png', + resizeMode: 'cover', +} +const DARK_SPLASH_CONFIG = { + backgroundColor: '#001429', + image: './assets/splash-dark.png', + resizeMode: 'cover', +} + +const SPLASH_CONFIG_ANDROID = { + backgroundColor: '#0c7cff', + image: './assets/splash.png', + resizeMode: 'cover', +} +const DARK_SPLASH_CONFIG_ANDROID = { + backgroundColor: '#0f141b', + image: './assets/splash-dark.png', + resizeMode: 'cover', +} + +module.exports = function (config) { + /** + * App version number. Should be incremented as part of a release cycle. + */ + const VERSION = pkg.version + + /** + * Uses built-in Expo env vars + * + * @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables + */ + const PLATFORM = process.env.EAS_BUILD_PLATFORM + + const DIST_BUILD_NUMBER = + PLATFORM === 'android' + ? process.env.BSKY_ANDROID_VERSION_CODE + : process.env.BSKY_IOS_BUILD_NUMBER + + const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development' + return { expo: { + version: VERSION, name: 'Bluesky', slug: 'bluesky', scheme: 'bluesky', owner: 'blueskysocial', - version: '1.51.0', runtimeVersion: { policy: 'appVersion', }, orientation: 'portrait', icon: './assets/icon.png', userInterfaceStyle: 'automatic', - splash: { - image: './assets/cloud-splash.png', - resizeMode: 'cover', - backgroundColor: '#ffffff', - }, + splash: SPLASH_CONFIG, ios: { - buildNumber: '5', supportsTablet: true, bundleIdentifier: 'xyz.blueskyweb.app', config: { @@ -37,16 +74,25 @@ module.exports = function () { 'Used for profile pictures, posts, and other kinds of content', }, associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'], + splash: { + ...SPLASH_CONFIG, + dark: DARK_SPLASH_CONFIG, + }, + entitlements: { + 'com.apple.security.application-groups': 'group.app.bsky', + }, }, androidStatusBar: { - barStyle: 'dark-content', - backgroundColor: '#ffffff', + barStyle: 'light-content', + backgroundColor: '#00000000', }, android: { - versionCode: 39, + icon: './assets/icon.png', adaptiveIcon: { - foregroundImage: './assets/adaptive-icon.png', - backgroundColor: '#ffffff', + foregroundImage: './assets/icon-android-foreground.png', + monochromeImage: './assets/icon-android-foreground.png', + backgroundImage: './assets/icon-android-background.png', + backgroundColor: '#1185FE', }, googleServicesFile: './google-services.json', package: 'xyz.blueskyweb.app', @@ -59,10 +105,18 @@ module.exports = function () { scheme: 'https', host: 'bsky.app', }, + IS_DEV && { + scheme: 'http', + host: 'localhost:19006', + }, ], category: ['BROWSABLE', 'DEFAULT'], }, ], + splash: { + ...SPLASH_CONFIG_ANDROID, + dark: DARK_SPLASH_CONFIG_ANDROID, + }, }, web: { favicon: './assets/favicon.png', @@ -74,15 +128,20 @@ module.exports = function () { }, plugins: [ 'expo-localization', - hasSentryToken && 'sentry-expo', + Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo', [ 'expo-build-properties', { + ios: { + deploymentTarget: '13.4', + newArchEnabled: false, + }, android: { compileSdkVersion: 34, targetSdkVersion: 34, buildToolsVersion: '34.0.0', kotlinVersion: '1.8.0', + newArchEnabled: false, }, }, ], @@ -92,19 +151,53 @@ module.exports = function () { username: 'blueskysocial', }, ], + [ + 'expo-notifications', + { + icon: './assets/icon-android-notification.png', + color: '#1185fe', + }, + ], + './plugins/withAndroidManifestPlugin.js', + './plugins/withAndroidManifestFCMIconPlugin.js', + './plugins/withAndroidStylesWindowBackgroundPlugin.js', + './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js', + './plugins/shareExtension/withShareExtensions.js', ].filter(Boolean), extra: { eas: { + build: { + experimental: { + ios: { + appExtensions: [ + { + targetName: 'Share-with-Bluesky', + bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky', + entitlements: { + 'com.apple.security.application-groups': [ + 'group.app.bsky', + ], + }, + }, + ], + }, + }, + }, projectId: '55bd077a-d905-4184-9c7f-94789ba0f302', }, }, hooks: { postPublish: [ + /* + * @see https://docs.expo.dev/guides/using-sentry/#app-configuration + */ { file: 'sentry-expo/upload-sourcemaps', config: { organization: 'blueskyweb', project: 'react-native', + release: VERSION, + dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`, }, }, ], diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png deleted file mode 100644 index 1dda9a342c..0000000000 Binary files a/assets/adaptive-icon.png and /dev/null differ diff --git a/assets/cloud-splash.png b/assets/cloud-splash.png deleted file mode 100644 index 188625331d..0000000000 Binary files a/assets/cloud-splash.png and /dev/null differ diff --git a/assets/default-avatar.jpg b/assets/default-avatar.jpg deleted file mode 100644 index ab141651e7..0000000000 Binary files a/assets/default-avatar.jpg and /dev/null differ diff --git a/assets/default-avatar.png b/assets/default-avatar.png new file mode 100644 index 0000000000..d2bd03842f Binary files /dev/null and b/assets/default-avatar.png differ diff --git a/assets/favicon.png b/assets/favicon.png index 24ec61e0a5..ddf55f4c81 100644 Binary files a/assets/favicon.png and b/assets/favicon.png differ diff --git a/assets/icon-android-background.png b/assets/icon-android-background.png new file mode 100644 index 0000000000..2c9d153823 Binary files /dev/null and b/assets/icon-android-background.png differ diff --git a/assets/icon-android-foreground.png b/assets/icon-android-foreground.png new file mode 100644 index 0000000000..61e788747f Binary files /dev/null and b/assets/icon-android-foreground.png differ diff --git a/assets/icon-android-notification.png b/assets/icon-android-notification.png new file mode 100644 index 0000000000..9d8730838e Binary files /dev/null and b/assets/icon-android-notification.png differ diff --git a/assets/icon.png b/assets/icon.png index 1dda9a342c..75866fc827 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icons/arrowOutOfBox_stroke2_corner0_rounded.svg b/assets/icons/arrowOutOfBox_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2312ccd55e --- /dev/null +++ b/assets/icons/arrowOutOfBox_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrowRotateCounterClockwise_stroke2_corner0_rounded.svg b/assets/icons/arrowRotateCounterClockwise_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..955b3dbc3b --- /dev/null +++ b/assets/icons/arrowRotateCounterClockwise_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg b/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg new file mode 100644 index 0000000000..554a7374ec --- /dev/null +++ b/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrowTriangleBottom_stroke2_corner1_rounded.svg b/assets/icons/arrowTriangleBottom_stroke2_corner1_rounded.svg new file mode 100644 index 0000000000..f40546f7cd --- /dev/null +++ b/assets/icons/arrowTriangleBottom_stroke2_corner1_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/at_stroke2_corner0_rounded.svg b/assets/icons/at_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..8d30d7c8c5 --- /dev/null +++ b/assets/icons/at_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bars3_stroke2_corner0_rounded.svg b/assets/icons/bars3_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..cbcb531a6e --- /dev/null +++ b/assets/icons/bars3_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..0bfcc48a0e --- /dev/null +++ b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/calendarDays_stroke2_corner0_rounded.svg b/assets/icons/calendarDays_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..09d9c0f490 --- /dev/null +++ b/assets/icons/calendarDays_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/calendar_stroke2_corner0_rounded.svg b/assets/icons/calendar_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..703f389dba --- /dev/null +++ b/assets/icons/calendar_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/camera_filled_stroke2_corner0_rounded.svg b/assets/icons/camera_filled_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..fa0101cf0d --- /dev/null +++ b/assets/icons/camera_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/camera_stroke2_corner0_rounded.svg b/assets/icons/camera_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..ce0c29ae50 --- /dev/null +++ b/assets/icons/camera_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/checkThick_stroke2_corner0_rounded.svg b/assets/icons/checkThick_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..54af3e8598 --- /dev/null +++ b/assets/icons/checkThick_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/check_stroke2_corner0_rounded.svg b/assets/icons/check_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b336a518f5 --- /dev/null +++ b/assets/icons/check_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/chevronBottom_stroke2_corner0_rounded.svg b/assets/icons/chevronBottom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..705c1c5139 --- /dev/null +++ b/assets/icons/chevronBottom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/chevronLeft_stroke2_corner0_rounded.svg b/assets/icons/chevronLeft_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d9a8660f7f --- /dev/null +++ b/assets/icons/chevronLeft_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/chevronRight_stroke2_corner0_rounded.svg b/assets/icons/chevronRight_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b57fd03986 --- /dev/null +++ b/assets/icons/chevronRight_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/chevronTop_stroke2_corner0_rounded.svg b/assets/icons/chevronTop_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..da94ba911f --- /dev/null +++ b/assets/icons/chevronTop_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/circleBanSign_stroke2_corner0_rounded.svg b/assets/icons/circleBanSign_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..73251477fa --- /dev/null +++ b/assets/icons/circleBanSign_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circleInfo_stroke2_corner0_rounded.svg b/assets/icons/circleInfo_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..926d4b3910 --- /dev/null +++ b/assets/icons/circleInfo_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/clipboard_stroke2_corner2_rounded.svg b/assets/icons/clipboard_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..f403cfb929 --- /dev/null +++ b/assets/icons/clipboard_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/colorPalette_stroke2_corner0_rounded.svg b/assets/icons/colorPalette_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b1056e1a96 --- /dev/null +++ b/assets/icons/colorPalette_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/dotGrid1x3Horizontal_stroke2_corner2_rounded.svg b/assets/icons/dotGrid1x3Horizontal_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..c3b456b100 --- /dev/null +++ b/assets/icons/dotGrid1x3Horizontal_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/emojiSad_stroke2_corner0_rounded.svg b/assets/icons/emojiSad_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..0a5a43cd0b --- /dev/null +++ b/assets/icons/emojiSad_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/envelope_stroke2_corner0_rounded.svg b/assets/icons/envelope_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..c3ab45980b --- /dev/null +++ b/assets/icons/envelope_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/eyeSlash_stroke2_corner0_rounded.svg b/assets/icons/eyeSlash_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..f11bdd937f --- /dev/null +++ b/assets/icons/eyeSlash_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/filterTimeline_stroke2_corner0_rounded.svg b/assets/icons/filterTimeline_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..459b9212aa --- /dev/null +++ b/assets/icons/filterTimeline_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/filter_stroke2_corner0_rounded.svg b/assets/icons/filter_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1fbcfc5711 --- /dev/null +++ b/assets/icons/filter_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/flag_stroke2_corner0_rounded.svg b/assets/icons/flag_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..9f9cc5cdd1 --- /dev/null +++ b/assets/icons/flag_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/globe_stroke2_corner0_rounded.svg b/assets/icons/globe_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..83cb88d136 --- /dev/null +++ b/assets/icons/globe_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/group3_stroke2_corner0_rounded.svg b/assets/icons/group3_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2a8f43a8a4 --- /dev/null +++ b/assets/icons/group3_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/growth_stroke2_corner0_rounded.svg b/assets/icons/growth_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..ec9083fb1e --- /dev/null +++ b/assets/icons/growth_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/hashtag_stroke2_corner0_rounded.svg b/assets/icons/hashtag_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..05b2353db8 --- /dev/null +++ b/assets/icons/hashtag_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/heart2_filled_stroke2_corner0_rounded.svg b/assets/icons/heart2_filled_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1dfefb4c99 --- /dev/null +++ b/assets/icons/heart2_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/heart2_stroke2_corner0_rounded.svg b/assets/icons/heart2_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..5b3da8e00e --- /dev/null +++ b/assets/icons/heart2_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/listMagnifyingGlass_stroke2_corner0_rounded.svg b/assets/icons/listMagnifyingGlass_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1b1857efab --- /dev/null +++ b/assets/icons/listMagnifyingGlass_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/listSparkle_stroke2_corner0_rounded.svg b/assets/icons/listSparkle_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..702e1895f5 --- /dev/null +++ b/assets/icons/listSparkle_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/loader_stroke2_corner0_rounded.svg b/assets/icons/loader_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..9dbc013798 --- /dev/null +++ b/assets/icons/loader_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/lock_stroke2_corner0_rounded.svg b/assets/icons/lock_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..8b094ba5eb --- /dev/null +++ b/assets/icons/lock_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/magnifyingGlass2_stroke2_corner0_rounded.svg b/assets/icons/magnifyingGlass2_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2759aaf2cc --- /dev/null +++ b/assets/icons/magnifyingGlass2_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/mute_stroke2_corner0_rounded.svg b/assets/icons/mute_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..8ebecb3920 --- /dev/null +++ b/assets/icons/mute_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/news2_stroke2_corner0_rounded.svg b/assets/icons/news2_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..66e4c373a0 --- /dev/null +++ b/assets/icons/news2_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pageText_stroke2_corner0_rounded.svg b/assets/icons/pageText_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..826a36cd7e --- /dev/null +++ b/assets/icons/pageText_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pencilLine_stroke2_corner0_rounded.svg b/assets/icons/pencilLine_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..c58bef9fa2 --- /dev/null +++ b/assets/icons/pencilLine_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2e798cbe29 --- /dev/null +++ b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/personCheck_stroke2_corner0_rounded.svg b/assets/icons/personCheck_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b3231c2780 --- /dev/null +++ b/assets/icons/personCheck_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/personX_stroke2_corner0_rounded.svg b/assets/icons/personX_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..073015bc54 --- /dev/null +++ b/assets/icons/personX_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/person_stroke2_corner0_rounded.svg b/assets/icons/person_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..a23ad76071 --- /dev/null +++ b/assets/icons/person_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/plusLarge_stroke2_corner0_rounded.svg b/assets/icons/plusLarge_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..8d568437b5 --- /dev/null +++ b/assets/icons/plusLarge_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/raisingHand4Finger_stroke2_corner0_rounded.svg b/assets/icons/raisingHand4Finger_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..aed3d9e7ec --- /dev/null +++ b/assets/icons/raisingHand4Finger_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/settingsGear2_stroke2_corner0_rounded.svg b/assets/icons/settingsGear2_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..de8a579194 --- /dev/null +++ b/assets/icons/settingsGear2_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/shield_stroke2_corner0_rounded.svg b/assets/icons/shield_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..c4ef98e5ad --- /dev/null +++ b/assets/icons/shield_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..81357a12e3 --- /dev/null +++ b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg b/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1407a1d6fe --- /dev/null +++ b/assets/icons/squareArrowTopRight_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/squareBehindSquare4_stroke2_corner0_rounded.svg b/assets/icons/squareBehindSquare4_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..3fa7e5d390 --- /dev/null +++ b/assets/icons/squareBehindSquare4_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/streamingLive_stroke2_corner0_rounded.svg b/assets/icons/streamingLive_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b6cdd34d77 --- /dev/null +++ b/assets/icons/streamingLive_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..a45a90ae5f --- /dev/null +++ b/assets/icons/ticket_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/trash_stroke2_corner0_rounded.svg b/assets/icons/trash_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d4b32f81fe --- /dev/null +++ b/assets/icons/trash_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/trending2_stroke2_corner2_rounded.svg b/assets/icons/trending2_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..cc806b0eb6 --- /dev/null +++ b/assets/icons/trending2_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/triangleExclamation_stroke2_corner2_rounded.svg b/assets/icons/triangleExclamation_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..aa56404457 --- /dev/null +++ b/assets/icons/triangleExclamation_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/warning_stroke2_corner0_rounded.svg b/assets/icons/warning_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d5b6f13d5f --- /dev/null +++ b/assets/icons/warning_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/splash-dark.png b/assets/splash-dark.png new file mode 100644 index 0000000000..95176b3ff4 Binary files /dev/null and b/assets/splash-dark.png differ diff --git a/assets/splash.png b/assets/splash.png new file mode 100644 index 0000000000..05a7392b37 Binary files /dev/null and b/assets/splash.png differ diff --git a/assets/tabs-explainer.jpg b/assets/tabs-explainer.jpg deleted file mode 100644 index 64f0a8fe5b..0000000000 Binary files a/assets/tabs-explainer.jpg and /dev/null differ diff --git a/babel.config.js b/babel.config.js index 598e2a5671..43b2c7bce3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,23 @@ module.exports = function (api) { api.cache(true) + const isTestEnv = process.env.NODE_ENV === 'test' return { - presets: ['babel-preset-expo'], + presets: [ + [ + 'babel-preset-expo', + { + lazyImports: true, + native: { + // We should be able to remove this after upgrading Expo + // to a version that includes https://github.com/expo/expo/pull/24672. + unstable_transformProfile: 'hermes-stable', + // Disable ESM -> CJS compilation because Metro takes care of it. + // However, we need it in Jest tests since those run without Metro. + disableImportExportTransform: !isTestEnv, + }, + }, + ], + ], plugins: [ [ 'module:react-native-dotenv', @@ -21,14 +37,22 @@ module.exports = function (api) { { alias: { // This needs to be mirrored in tsconfig.json + '#': './src', lib: './src/lib', platform: './src/platform', state: './src/state', view: './src/view', + crypto: './src/platform/crypto.ts', }, }, ], + 'macros', 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last ], + env: { + production: { + plugins: ['transform-remove-console'], + }, + }, } } diff --git a/bskyweb/Makefile b/bskyweb/Makefile index e0ba8aec06..6f979fa849 100644 --- a/bskyweb/Makefile +++ b/bskyweb/Makefile @@ -42,4 +42,4 @@ check: ## Compile everything, checking syntax (does not output binaries) .PHONY: run-dev-bskyweb run-dev-bskyweb: .env ## Runs 'bskyweb' for local dev - GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve --debug + GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve diff --git a/bskyweb/README.md b/bskyweb/README.md index c8efe04482..640c30f4ad 100644 --- a/bskyweb/README.md +++ b/bskyweb/README.md @@ -6,9 +6,9 @@ To build the SPA bundle (`bundle.web.js`), first get a JavaScript development environment set up. Either follow the top-level README, or something quick like: - # install nodejs 18 (specifically) - nvm install 18 - nvm use 18 + # install nodejs + nvm install + nvm use npm install --global yarn # setup tools and deps (in top level of this repo) diff --git a/bskyweb/cmd/bskyweb/formating.go b/bskyweb/cmd/bskyweb/formating.go new file mode 100644 index 0000000000..edd085ce8f --- /dev/null +++ b/bskyweb/cmd/bskyweb/formating.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "slices" + "strings" + + appbsky "github.com/bluesky-social/indigo/api/bsky" +) + +// Function to expand shortened links in rich text back to full urls, replacing shortened urls in social card meta tags and the noscript output. +// +// This essentially reverses the effect of the typescript function `shortenLinks()` in `src/lib/strings/rich-text-manip.ts` +func ExpandPostText(post *appbsky.FeedPost) string { + postText := post.Text + var charsAdded int = 0 + // iterate over facets, check if they're link facets, and if found, grab the uri + for _, facet := range post.Facets { + linkUri := "" + if slices.ContainsFunc(facet.Features, func(feat *appbsky.RichtextFacet_Features_Elem) bool { + if feat.RichtextFacet_Link == nil || feat.RichtextFacet_Link.LexiconTypeID != "app.bsky.richtext.facet#link" { + return false + } + + // bail out if bounds checks fail + if int(facet.Index.ByteStart)+charsAdded > len(postText) || int(facet.Index.ByteEnd)+charsAdded > len(postText) { + return false + } + linkText := postText[int(facet.Index.ByteStart)+charsAdded : int(facet.Index.ByteEnd)+charsAdded] + linkUri = feat.RichtextFacet_Link.Uri + + // only expand uris that have been shortened (as opposed to those with non-uri anchor text) + if strings.HasSuffix(linkText, "...") && strings.Contains(linkUri, linkText[0:len(linkText)-3]) { + return true + } + return false + }) { + // replace the shortened uri with the full length one from the facet using utf8 byte offsets + // NOTE: we already did bounds check above + postText = postText[0:int(facet.Index.ByteStart)+charsAdded] + linkUri + postText[int(facet.Index.ByteEnd)+charsAdded:] + charsAdded += len(linkUri) - int(facet.Index.ByteEnd-facet.Index.ByteStart) + } + } + // if the post has an embeded link and its url doesn't already appear in postText, append it to + // the end to avoid social cards with missing links + if post.Embed != nil && post.Embed.EmbedExternal != nil && post.Embed.EmbedExternal.External != nil { + externalURI := post.Embed.EmbedExternal.External.Uri + if !strings.Contains(postText, externalURI) { + postText = fmt.Sprintf("%s\n%s", postText, externalURI) + } + } + // TODO: could embed the actual post text? + if post.Embed != nil && (post.Embed.EmbedRecord != nil || post.Embed.EmbedRecordWithMedia != nil) { + postText = fmt.Sprintf("%s\n\n[contains quote post or other embedded content]", postText) + } + return postText +} diff --git a/bskyweb/cmd/bskyweb/formatting_test.go b/bskyweb/cmd/bskyweb/formatting_test.go new file mode 100644 index 0000000000..1fbf8d5ee4 --- /dev/null +++ b/bskyweb/cmd/bskyweb/formatting_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "io" + "os" + "strings" + "testing" + + appbsky "github.com/bluesky-social/indigo/api/bsky" +) + +func loadPost(t *testing.T, p string) appbsky.FeedPost { + + f, err := os.Open(p) + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + postBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + var post appbsky.FeedPost + if err := json.Unmarshal(postBytes, &post); err != nil { + t.Fatal(err) + } + return post +} + +func TestExpandPostText(t *testing.T) { + post := loadPost(t, "testdata/atproto_embed_post.json") + + text := ExpandPostText(&post) + if !strings.Contains(text, "https://github.com/snarfed/bridgy-fed") { + t.Fail() + } +} diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go deleted file mode 100644 index e892971f9c..0000000000 --- a/bskyweb/cmd/bskyweb/mailmodo.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "net/http" - "time" -) - -type Mailmodo struct { - httpClient *http.Client - APIKey string - BaseURL string - ListName string -} - -func NewMailmodo(apiKey, listName string) *Mailmodo { - return &Mailmodo{ - APIKey: apiKey, - BaseURL: "https://api.mailmodo.com/api/v1", - httpClient: &http.Client{}, - ListName: listName, - } -} - -func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error { - endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod) - js, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("Mailmodo JSON encoding failed: %w", err) - } - req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js)) - if err != nil { - return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err) - } - req.Header.Set("mmApiKey", m.APIKey) - req.Header.Set("Content-Type", "application/json") - - res, err := m.httpClient.Do(req) - if err != nil { - return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err) - } - defer res.Body.Close() - - status := struct { - Success bool `json:"success"` - Message string `json:"message"` - }{} - if err := json.NewDecoder(res.Body).Decode(&status); err != nil { - return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err) - } - if !status.Success { - return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message) - } - return nil -} - -func (m *Mailmodo) AddToList(ctx context.Context, email string) error { - return m.request(ctx, "POST", "addToList", map[string]any{ - "listName": m.ListName, - "email": email, - "data": map[string]any{ - "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))), - }, - "created_at": time.Now().UTC().Format(time.RFC3339), - }) -} diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index a2952cae2b..5185ff573a 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -40,18 +40,6 @@ func run(args []string) { // retain old PDS env var for easy transition EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, }, - &cli.StringFlag{ - Name: "mailmodo-api-key", - Usage: "Mailmodo API key", - Required: false, - EnvVars: []string{"MAILMODO_API_KEY"}, - }, - &cli.StringFlag{ - Name: "mailmodo-list-name", - Usage: "Mailmodo contact list to add email addresses to", - Required: false, - EnvVars: []string{"MAILMODO_LIST_NAME"}, - }, &cli.StringFlag{ Name: "http-address", Usage: "Specify the local IP/port to bind to", diff --git a/bskyweb/cmd/bskyweb/rss.go b/bskyweb/cmd/bskyweb/rss.go new file mode 100644 index 0000000000..76689abb51 --- /dev/null +++ b/bskyweb/cmd/bskyweb/rss.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/xml" + "fmt" + "net/http" + "strings" + "time" + + appbsky "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/labstack/echo/v4" +) + +type ItemGUID struct { + XMLName xml.Name `xml:"guid"` + Value string `xml:",chardata"` + IsPerma bool `xml:"isPermaLink,attr"` +} + +// We don't actually populate the title for "posts". +// Some background: https://book.micro.blog/rss-for-microblogs/ +type Item struct { + Title string `xml:"title,omitempty"` + Link string `xml:"link,omitempty"` + Description string `xml:"description,omitempty"` + PubDate string `xml:"pubDate,omitempty"` + GUID ItemGUID +} + +type rss struct { + Version string `xml:"version,attr"` + Description string `xml:"channel>description,omitempty"` + Link string `xml:"channel>link"` + Title string `xml:"channel>title"` + + Item []Item `xml:"channel>item"` +} + +func (srv *Server) WebProfileRSS(c echo.Context) error { + ctx := c.Request().Context() + req := c.Request() + + identParam := c.Param("ident") + + // if not a DID, try parsing as a handle and doing a redirect + if !strings.HasPrefix(identParam, "did:") { + handle, err := syntax.ParseHandle(identParam) + if err != nil { + return echo.NewHTTPError(400, fmt.Sprintf("not a valid handle: %s", identParam)) + } + + // check that public view is Ok, and resolve DID + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle.String()) + if err != nil { + return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle)) + } + for _, label := range pv.Labels { + if label.Src == pv.Did && label.Val == "!no-unauthenticated" { + return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle)) + } + } + return c.Redirect(http.StatusFound, fmt.Sprintf("/profile/%s/rss", pv.Did)) + } + + did, err := syntax.ParseDID(identParam) + if err != nil { + return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", identParam)) + } + + // check that public view is Ok + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did.String()) + if err != nil { + return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did)) + } + for _, label := range pv.Labels { + if label.Src == pv.Did && label.Val == "!no-unauthenticated" { + return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did)) + } + } + + af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "posts_no_replies", 30) + if err != nil { + log.Warn("failed to fetch author feed", "did", did, "err", err) + return err + } + + posts := []Item{} + for _, p := range af.Feed { + // only include author's own posts in RSS + if p.Post.Author.Did != pv.Did { + continue + } + aturi, err := syntax.ParseATURI(p.Post.Uri) + if err != nil { + return err + } + rec, ok := p.Post.Record.Val.(*appbsky.FeedPost) + if !ok { + continue + } + // only top-level posts in RSS (no replies) + if rec.Reply != nil { + continue + } + pubDate := "" + createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt) + if nil == err { + pubDate = createdAt.Time().Format(time.RFC822Z) + } + posts = append(posts, Item{ + Link: fmt.Sprintf("https://%s/profile/%s/post/%s", req.Host, pv.Handle, aturi.RecordKey().String()), + Description: ExpandPostText(rec), + PubDate: pubDate, + GUID: ItemGUID{ + Value: aturi.String(), + IsPerma: false, + }, + }) + } + + title := "@" + pv.Handle + if pv.DisplayName != nil { + title = title + " - " + *pv.DisplayName + } + desc := "" + if pv.Description != nil { + desc = *pv.Description + } + feed := &rss{ + Version: "2.0", + Description: desc, + Link: fmt.Sprintf("https://%s/profile/%s", req.Host, pv.Handle), + Title: title, + Item: posts, + } + return c.XML(http.StatusOK, feed) +} diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index d5d864069b..54a3925c6d 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,11 +2,9 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "io/fs" - "io/ioutil" "net/http" "os" "os/signal" @@ -15,7 +13,8 @@ import ( "time" appbsky "github.com/bluesky-social/indigo/api/bsky" - cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/util/cliutil" "github.com/bluesky-social/indigo/xrpc" "github.com/bluesky-social/social-app/bskyweb" @@ -28,25 +27,19 @@ import ( ) type Server struct { - echo *echo.Echo - httpd *http.Server - mailmodo *Mailmodo - xrpcc *xrpc.Client + echo *echo.Echo + httpd *http.Server + xrpcc *xrpc.Client } func serve(cctx *cli.Context) error { debug := cctx.Bool("debug") httpAddress := cctx.String("http-address") appviewHost := cctx.String("appview-host") - mailmodoAPIKey := cctx.String("mailmodo-api-key") - mailmodoListName := cctx.String("mailmodo-list-name") // Echo e := echo.New() - // Mailmodo client. - mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName) - // create a new session (no auth) xrpcc := &xrpc.Client{ Client: cliutil.NewHttpClient(), @@ -76,9 +69,8 @@ func serve(cctx *cli.Context) error { // server // server := &Server{ - echo: e, - mailmodo: mailmodo, - xrpcc: xrpcc, + echo: e, + xrpcc: xrpcc, } // Create the HTTP server. @@ -91,6 +83,11 @@ func serve(cctx *cli.Context) error { } e.HideBanner = true + e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) + e.HTTPErrorHandler = server.errorHandler + + e.IPExtractor = echo.ExtractIPFromXFFHeader() + // SECURITY: Do not modify without due consideration. e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ContentTypeNosniff: "nosniff", @@ -106,8 +103,23 @@ func serve(cctx *cli.Context) error { return strings.HasPrefix(c.Request().URL.Path, "/static") }, })) - e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) - e.HTTPErrorHandler = server.errorHandler + e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{ + Rate: 10, // requests per second + Burst: 30, // allow bursts + ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes + }, + ), + IdentifierExtractor: func(ctx echo.Context) (string, error) { + id := ctx.RealIP() + return id, nil + }, + DenyHandler: func(c echo.Context, identifier string, err error) error { + return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact security@bsky.app if you believe this was a mistake.\n") + }, + })) // redirect trailing slash to non-trailing slash. // all of our current endpoints have no trailing slash. @@ -138,6 +150,7 @@ func serve(cctx *cli.Context) error { e.GET("/security.txt", func(c echo.Context) error { return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") }) + e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { path := c.Request().URL.Path @@ -158,42 +171,48 @@ func serve(cctx *cli.Context) error { e.GET("/", server.WebHome) // generic routes + e.GET("/hashtag/:tag", server.WebGeneric) e.GET("/search", server.WebGeneric) e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) + e.GET("/lists", server.WebGeneric) e.GET("/moderation", server.WebGeneric) - e.GET("/moderation/mute-lists", server.WebGeneric) + e.GET("/moderation/modlists", server.WebGeneric) e.GET("/moderation/muted-accounts", server.WebGeneric) e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) e.GET("/settings/language", server.WebGeneric) e.GET("/settings/app-passwords", server.WebGeneric) - e.GET("/settings/home-feed", server.WebGeneric) + e.GET("/settings/following-feed", server.WebGeneric) e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/settings/threads", server.WebGeneric) + e.GET("/settings/external-embeds", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) + e.GET("/sys/debug-mod", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) e.GET("/support/privacy", server.WebGeneric) e.GET("/support/tos", server.WebGeneric) e.GET("/support/community-guidelines", server.WebGeneric) e.GET("/support/copyright", server.WebGeneric) + e.GET("/intent/compose", server.WebGeneric) // profile endpoints; only first populates info - e.GET("/profile/:handle", server.WebProfile) - e.GET("/profile/:handle/follows", server.WebGeneric) - e.GET("/profile/:handle/followers", server.WebGeneric) - e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) - e.GET("/profile/:handle/feed/:rkey", server.WebGeneric) - e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) + e.GET("/profile/:handleOrDID", server.WebProfile) + e.GET("/profile/:handleOrDID/follows", server.WebGeneric) + e.GET("/profile/:handleOrDID/followers", server.WebGeneric) + e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric) + e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) + e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric) + e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric) + + // profile RSS feed (DID not handle) + e.GET("/profile/:ident/rss", server.WebProfileRSS) // post endpoints; only first populates info - e.GET("/profile/:handle/post/:rkey", server.WebPost) - e.GET("/profile/:handle/post/:rkey/liked-by", server.WebGeneric) - e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) - - // Mailmodo - e.POST("/api/waitlist", server.apiWaitlist) + e.GET("/profile/:handleOrDID/post/:rkey", server.WebPost) + e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) + e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) // Start the server. log.Infof("starting server address=%s", httpAddress) @@ -264,88 +283,108 @@ func (srv *Server) WebHome(c echo.Context) error { } func (srv *Server) WebPost(c echo.Context) error { + ctx := c.Request().Context() data := pongo2.Context{} - handle := c.Param("handle") - rkey := c.Param("rkey") - // sanity check argument - if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { - ctx := c.Request().Context() - // requires two fetches: first fetch profile (!) - pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) - if err != nil { - log.Warnf("failed to fetch handle: %s\t%v", handle, err) - } else { - did := pv.Did - data["did"] = did - - // then fetch the post thread (with extra context) - uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) - tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, uri) - if err != nil { - log.Warnf("failed to fetch post: %s\t%v", uri, err) - } else { - req := c.Request() - postView := tpv.Thread.FeedDefs_ThreadViewPost.Post - data["postView"] = postView - data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) - if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { - data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb - } - } - } + // sanity check arguments. don't 4xx, just let app handle if not expected format + rkeyParam := c.Param("rkey") + rkey, err := syntax.ParseRecordKey(rkeyParam) + if err != nil { + return c.Render(http.StatusOK, "post.html", data) } + handleOrDIDParam := c.Param("handleOrDID") + handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam) + if err != nil { + return c.Render(http.StatusOK, "post.html", data) + } + + identifier := handleOrDID.Normalize().String() + + // requires two fetches: first fetch profile (!) + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier) + if err != nil { + log.Warnf("failed to fetch profile for: %s\t%v", identifier, err) + return c.Render(http.StatusOK, "post.html", data) + } + unauthedViewingOkay := true + for _, label := range pv.Labels { + if label.Src == pv.Did && label.Val == "!no-unauthenticated" { + unauthedViewingOkay = false + } + } + + if !unauthedViewingOkay { + return c.Render(http.StatusOK, "post.html", data) + } + did := pv.Did + data["did"] = did + + // then fetch the post thread (with extra context) + uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) + tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri) + if err != nil { + log.Warnf("failed to fetch post: %s\t%v", uri, err) + return c.Render(http.StatusOK, "post.html", data) + } + req := c.Request() + postView := tpv.Thread.FeedDefs_ThreadViewPost.Post + data["postView"] = postView + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) + if postView.Embed != nil { + if postView.Embed.EmbedImages_View != nil { + var thumbUrls []string + for i := range postView.Embed.EmbedImages_View.Images { + thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb) + } + data["imgThumbUrls"] = thumbUrls + } else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil { + var thumbUrls []string + for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images { + thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb) + } + data["imgThumbUrls"] = thumbUrls + } + } + + if postView.Record != nil { + postRecord, ok := postView.Record.Val.(*appbsky.FeedPost) + if ok { + data["postText"] = ExpandPostText(postRecord) + } + } + return c.Render(http.StatusOK, "post.html", data) } func (srv *Server) WebProfile(c echo.Context) error { + ctx := c.Request().Context() data := pongo2.Context{} - handle := c.Param("handle") - // sanity check argument - if len(handle) > 4 && len(handle) < 128 { - ctx := c.Request().Context() - pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) - if err != nil { - log.Warnf("failed to fetch handle: %s\t%v", handle, err) - } else { - req := c.Request() - data["profileView"] = pv - data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) + + // sanity check arguments. don't 4xx, just let app handle if not expected format + handleOrDIDParam := c.Param("handleOrDID") + handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam) + if err != nil { + return c.Render(http.StatusOK, "profile.html", data) + } + identifier := handleOrDID.Normalize().String() + + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier) + if err != nil { + log.Warnf("failed to fetch profile for: %s\t%v", identifier, err) + return c.Render(http.StatusOK, "profile.html", data) + } + unauthedViewingOkay := true + for _, label := range pv.Labels { + if label.Src == pv.Did && label.Val == "!no-unauthenticated" { + unauthedViewingOkay = false } } - + if !unauthedViewingOkay { + return c.Render(http.StatusOK, "profile.html", data) + } + req := c.Request() + data["profileView"] = pv + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) + data["requestHost"] = req.Host return c.Render(http.StatusOK, "profile.html", data) } - -func (srv *Server) apiWaitlist(c echo.Context) error { - type jsonError struct { - Error string `json:"error"` - } - - // Read the API request. - type apiRequest struct { - Email string `json:"email"` - } - - bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) - payload, err := ioutil.ReadAll(bodyReader) - if err != nil { - return err - } - var req apiRequest - if err := json.Unmarshal(payload, &req); err != nil { - return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) - } - - if req.Email == "" { - return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) - } - - if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil { - log.Errorf("adding email to waitlist failed: %s", err) - return c.JSON(http.StatusBadRequest, jsonError{ - Error: "Storing email in waitlist failed. Please enter a valid email address.", - }) - } - return c.JSON(http.StatusOK, map[string]bool{"success": true}) -} diff --git a/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json b/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json new file mode 100644 index 0000000000..2e54854eee --- /dev/null +++ b/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json @@ -0,0 +1,60 @@ +{ + "$type": "app.bsky.feed.post", + "createdAt": "2023-12-04T19:30:03.024Z", + "embed": { + "$type": "app.bsky.embed.external", + "external": { + "description": "🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub. - GitHub - snarfed/bridgy-fed: 🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub.", + "thumb": { + "$type": "blob", + "ref": { + "$link": "bafkreidplhjcnrl2c74r3xs7nh7k7q3ny6ul7cgxr2fophblvdeky6t64e" + }, + "mimeType": "image/jpeg", + "size": 347998 + }, + "title": "GitHub - snarfed/bridgy-fed: 🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub...", + "uri": "https://github.com/snarfed/bridgy-fed" + } + }, + "facets": [ + { + "features": [ + { + "$type": "app.bsky.richtext.facet#link", + "uri": "https://github.com/snarfed/bridgy-fed" + } + ], + "index": { + "byteEnd": 92, + "byteStart": 66 + } + }, + { + "features": [ + { + "$type": "app.bsky.richtext.facet#mention", + "did": "did:plc:fdme4gb7mu7zrie7peay7tst" + } + ], + "index": { + "byteEnd": 149, + "byteStart": 137 + } + } + ], + "langs": [ + "en" + ], + "reply": { + "parent": { + "cid": "bafyreifaidyl62p4snkdwsygviemsxyidi3cd7dxvjomh5644sovxhsppa", + "uri": "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3kfqklhpalh2c" + }, + "root": { + "cid": "bafyreibiimdwmsp5mqpm7utqcdmvo6fdqmofblp5obs3h7ub6652zyooci", + "uri": "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3kfqkkjdkic2e" + } + }, + "text": "Bridgy Fed is an open-source project — check out the code here: github.com/snarfed/brid...\n\nStay updated with the project by following @snarfed.org!" +} diff --git a/bskyweb/example.env b/bskyweb/example.env index f8a45d7fc0..80adc15550 100644 --- a/bskyweb/example.env +++ b/bskyweb/example.env @@ -1,2 +1,2 @@ GOLOG_LOG_LEVEL=info -ATP_APPVIEW_HOST=https://api.bsky.app +ATP_APPVIEW_HOST=https://public.api.bsky.app diff --git a/bskyweb/go.mod b/bskyweb/go.mod index bc513727c7..0989217cac 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -3,88 +3,104 @@ module github.com/bluesky-social/social-app/bskyweb go 1.21 require ( - github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 + github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5 github.com/flosch/pongo2/v6 v6.0.0 github.com/ipfs/go-log v1.0.5 github.com/joho/godotenv v1.5.1 - github.com/klauspost/compress v1.16.5 - github.com/labstack/echo/v4 v4.10.2 - github.com/urfave/cli/v2 v2.25.3 + github.com/klauspost/compress v1.17.3 + github.com/labstack/echo/v4 v4.11.3 + github.com/urfave/cli/v2 v2.25.7 ) require ( - github.com/benbjohnson/clock v1.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/carlmjohnson/versioninfo v0.22.5 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.2 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/arc/v2 v2.0.6 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/go-block-format v0.1.2 // indirect + github.com/ipfs/go-block-format v0.2.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect - github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect - github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect - github.com/ipfs/go-ipfs-util v0.0.2 // indirect - github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc // indirect - github.com/ipfs/go-ipld-format v0.4.0 // indirect + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect + github.com/ipfs/go-ipfs-util v0.0.3 // indirect + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect + github.com/ipfs/go-ipld-format v0.6.0 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/ipfs/go-metrics-interface v0.0.1 // indirect - github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/jackc/pgx/v5 v5.5.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jbenet/goprocess v0.1.4 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/labstack/gommon v0.4.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.0.9 // indirect + github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.18 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 // indirect - github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 // indirect + github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f // indirect + github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.opentelemetry.io/otel v1.15.1 // indirect - go.opentelemetry.io/otel/trace v1.15.1 // indirect + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.8.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/protobuf v1.31.0 // indirect gorm.io/driver/postgres v1.5.0 // indirect - gorm.io/driver/sqlite v1.5.0 // indirect - gorm.io/gorm v1.25.0 // indirect - lukechampine.com/blake3 v1.1.7 // indirect + gorm.io/driver/sqlite v1.5.4 // indirect + gorm.io/gorm v1.25.5 // indirect + lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/bskyweb/go.sum b/bskyweb/go.sum index a07e446f4a..59797e35db 100644 --- a/bskyweb/go.sum +++ b/bskyweb/go.sum @@ -1,25 +1,30 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA= -github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5 h1:Zk1c+mxCYH6G/vLL0+9lO2Eci4OT3AFy73qPWa9auDM= +github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5/go.mod h1:a8cPbqDkRX+aPwJnXF7kAi3PF26hYiR4w5H8624MB7k= +github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= +github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -29,53 +34,49 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= -github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg= +github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= -github.com/ipfs/go-block-format v0.1.2 h1:GAjkfhVx1f4YTODS6Esrj1wt2HhrtwTnhEr+DyPUaJo= -github.com/ipfs/go-block-format v0.1.2/go.mod h1:mACVcrxarQKstUU3Yf/RdwbC4DzPV6++rO2a3d+a/KE= -github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= -github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= -github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= +github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= +github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= -github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk= github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= -github.com/ipfs/go-ipfs-blockstore v1.3.0 h1:m2EXaWgwTzAfsmt5UdJ7Is6l4gJcaM/A12XwJyvYvMM= -github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= -github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= -github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q= -github.com/ipfs/go-ipfs-ds-help v1.1.0/go.mod h1:YR5+6EaebOhfcqVCyqemItCLthrpVNot+rsOU/5IatU= -github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= -github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= -github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= -github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc h1:eUEo764smNy0EVRuMTSmirmuh552Mf2aBjfpDcLnDa8= -github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc/go.mod h1:X7SgEIwC4COC5OWfcepZBWafO5kA1Rmt9ZsLLbhihQk= -github.com/ipfs/go-ipld-format v0.4.0 h1:yqJSaJftjmjc9jEOFYlpkwOLVKv68OD27jFLlSghBlQ= -github.com/ipfs/go-ipld-format v0.4.0/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM= +github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= +github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= +github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= +github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= +github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= +github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= +github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= +github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= +github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= +github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= @@ -83,16 +84,16 @@ github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= -github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 h1:QG4CGBqCeuBo6aZlGAamSkxWdgWfZGeE49eUOWJPA4c= -github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52/go.mod h1:fdg+/X9Gg4AsAIzWpEHwnqd+QY3b7lajxyjE1m4hkq4= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= -github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= -github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= +github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= @@ -106,25 +107,23 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= +github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= -github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -133,26 +132,25 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8= -github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM= +github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= +github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= @@ -165,32 +163,39 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= -github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= @@ -209,39 +214,46 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= -github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= -github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 h1:XYEgH2nJgsrcrj32p+SAbx6T3s/6QknOXezXtz7kzbg= -github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= -github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 h1:EO/9z3yDvx1van1/0esdcqhalZZQGRj3I1BPTWr5k3A= -github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220/go.mod h1:qPtRyexGM5XMHFIfjH+EiA/A/1n2JakWEdMPC53pJAE= +github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f h1:SBuSxXJL0/ZJMtTxbXZgHZkThl9dNrzyaNhlyaqscRo= +github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= +github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic= +github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= -go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= -go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= -go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -249,9 +261,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -259,9 +270,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -278,17 +289,18 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -296,27 +308,29 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -335,11 +349,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -351,11 +367,11 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= -gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= -gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= -gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/bskyweb/static/apple-touch-icon.png b/bskyweb/static/apple-touch-icon.png index 5ebb6787c9..09393a7413 100644 Binary files a/bskyweb/static/apple-touch-icon.png and b/bskyweb/static/apple-touch-icon.png differ diff --git a/bskyweb/static/favicon-16x16.png b/bskyweb/static/favicon-16x16.png index 4267562b80..ea256e0569 100644 Binary files a/bskyweb/static/favicon-16x16.png and b/bskyweb/static/favicon-16x16.png differ diff --git a/bskyweb/static/favicon-32x32.png b/bskyweb/static/favicon-32x32.png index 869f6df5fc..a5ca7eed1e 100644 Binary files a/bskyweb/static/favicon-32x32.png and b/bskyweb/static/favicon-32x32.png differ diff --git a/bskyweb/static/favicon.ico b/bskyweb/static/favicon.ico deleted file mode 100644 index b4290a797a..0000000000 Binary files a/bskyweb/static/favicon.ico and /dev/null differ diff --git a/bskyweb/static/favicon.png b/bskyweb/static/favicon.png index 61cf7c943b..ddf55f4c81 100644 Binary files a/bskyweb/static/favicon.png and b/bskyweb/static/favicon.png differ diff --git a/bskyweb/static/iframe/youtube.html b/bskyweb/static/iframe/youtube.html new file mode 100644 index 0000000000..4b74d6fcd9 --- /dev/null +++ b/bskyweb/static/iframe/youtube.html @@ -0,0 +1,47 @@ + + +
+ diff --git a/bskyweb/static/safari-pinned-tab.svg b/bskyweb/static/safari-pinned-tab.svg new file mode 100644 index 0000000000..279d7b4b7a --- /dev/null +++ b/bskyweb/static/safari-pinned-tab.svg @@ -0,0 +1,33 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/bskyweb/static/social-card-default-gradient.png b/bskyweb/static/social-card-default-gradient.png new file mode 100644 index 0000000000..ba46e21f87 Binary files /dev/null and b/bskyweb/static/social-card-default-gradient.png differ diff --git a/bskyweb/static/social-card-default.png b/bskyweb/static/social-card-default.png index 3bb93c799c..848deab3f7 100644 Binary files a/bskyweb/static/social-card-default.png and b/bskyweb/static/social-card-default.png differ diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 7eeb7e4cc1..678729ffb3 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -1,13 +1,18 @@ - - - + + + + + + {%- block head_title -%}Bluesky{%- endblock -%} - + {% include "scripts.html" %} + + + + + + + {% block html_head_extra -%}{%- endblock %} - - {%- block body_all %}