diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..b4213aea24 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SENTRY_AUTH_TOKEN= diff --git a/.eslintrc.js b/.eslintrc.js index 93348b0d03..19fcf23083 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { root: true, - extends: '@react-native-community', + extends: [ + '@react-native-community', + 'plugin:react-native-a11y/ios', + 'prettier', + ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'detox'], ignorePatterns: [ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..c608f66000 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** + + +**To Reproduce** + +Steps to reproduce the behavior: + +1. + +**Expected behavior** + + + +**Screenshots** + + + +**Details** + + - Platform: + - Platform version: + - App version: + +**Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..0ac6b3c416 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature-request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + + +**Describe the solution you'd like** + + + +**Describe alternatives you've considered** + + + +**Additional context** + + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 77e58f5478..c5b9a324d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,10 @@ jobs: uses: actions/checkout@v3 - name: Yarn install run: yarn - - name: Typescript & Lint check + - name: Lint check run: yarn lint + - name: Type check + run: yarn typecheck testing: name: Run tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index bab37d2ad4..2fa850bf76 100644 --- a/.gitignore +++ b/.gitignore @@ -92,4 +92,8 @@ web-build/ # Android & iOS folders android/ -ios/ \ No newline at end of file +ios/ + +# environment variables +.env +.env.* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 95f0ec02e8..241926db4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ENV CGO_ENABLED=1 COPY . . # -# Generate the Javascript webpack. +# Generate the JavaScript webpack. # RUN mkdir --parents $NVM_DIR && \ wget \ @@ -35,11 +35,8 @@ RUN \. "$NVM_DIR/nvm.sh" && \ # DEBUG RUN find ./bskyweb/static && find ./web-build/static -# Copy the bundle js files. -RUN cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/ - # -# Generate the bksyweb Go binary. +# Generate the bskyweb Go binary. # RUN cd bskyweb/ && \ go mod download && \ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..8016366443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2023 Bluesky PBLLC + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..e93b6357a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ + +SHELL = /bin/bash +.SHELLFLAGS = -o pipefail -c + +.PHONY: help +help: ## Print info about all commands + @echo "Commands:" + @echo + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: build-web +build-web: ## Compile web bundle, copy to bskyweb directory + yarn build-web + +.PHONY: test +test: ## Run all tests + yarn test + +.PHONY: lint +lint: ## Run style checks and verify syntax + yarn run lint + +#.PHONY: fmt +#fmt: ## Run syntax re-formatting +# yarn prettier + +.PHONY: deps +deps: ## Installs dependent libs using 'yarn install' + yarn install --frozen-lockfile + +.PHONY: nvm-setup +nvm-setup: ## Use NVM to install and activate node+yarn + nvm install 18 + nvm use 18 + npm install --global yarn diff --git a/README.md b/README.md index 3595641e2f..ead1e677d7 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,61 @@ -# Bluesky +# Bluesky Social App -## Build instructions +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). -- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup). -- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started): - - yarn global add detox-cli - - brew tap wix/brew - - brew install applesimutils -- After initial setup: - - `npx expo prebuild` -> you will also need to run this anytime `app.json` or `package.json` changes -- Start the dev servers - - `git clone git@github.com:bluesky-social/atproto.git` - - `cd atproto` - - `yarn` - - `cd packages/dev-env && yarn start` -- Run the dev app - - iOS: `yarn ios` - - Android: `yarn android` - - Web: `yarn web` -- Run e2e tests - - Start in various console tabs: - - `yarn e2e:server` - - `yarn e2e:metro` - - Run once: `yarn e2e:build` - - Each test run: `yarn e2e:run` -- Tips - - `npx react-native info` Checks what has been installed. - - The android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}` - - For instance, the localhosted dev-wallet will need `adb reverse tcp:3001 tcp:3001` - - For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should bee the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).) +- **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)** -## Various notes +Links: -### Debugging +- [Build instructions](./docs/build.md) +- [ATProto repo](https://github.com/bluesky-social/atproto) +- [ATProto docs](https://atproto.com) -- Note that since 0.70, debugging using the old debugger (which shows up using CMD+D) doesn't work anymore. Follow the instructions below to debug the code: https://reactnative.dev/docs/next/hermes#debugging-js-on-hermes-using-google-chromes-devtools +## Rules & guidelines -### Running E2E Tests +--- -- Make sure you've setup your environment following above -- Make sure Metro and the dev server are running -- Run `yarn e2e` -- Find the artifacts in the `artifact` folder +ℹ️ While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. -### Polyfills +--- -`./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes: +**Rules:** -- TextEncoder / TextDecoder +- We may not respond to your issue or PR. +- We may close an issue or PR without much feedback. +- We may lock discussions or contributions if our attention is getting DDOSed. +- We're not going to provide support for build issues. + +**Guidelines:** + +- Check for existing issues before filing a new one please. +- Open an issue and give some time for discussion before submitting a PR. +- Stay away from PRs like... + - Changing "Post" to "Skeet." + - Refactoring the codebase, eg to replace mobx with redux or something. + - Adding entirely new features without prior discussion. + +Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for! + +## Forking guidelines + +You have our blessing 🪄✨ to fork this application! However, it's very important to be clear to users when you're giving them a fork. + +Please be sure to: + +- Change all branding in the repository and UI to clearly differentiate from Bluesky. +- Change any support links (feedback, email, terms of service, etc) to your own systems. +- Replace any analytics or error-collection systems with your own so we don't get super confused. + +## Security disclosures + +If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly. + +## License (MIT) + +See [./LICENSE](./LICENSE) for the full license. + +## P.S. + +We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place! diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts index 7bcad47f32..6ddfe3ca02 100644 --- a/__e2e__/mock-server.ts +++ b/__e2e__/mock-server.ts @@ -63,6 +63,232 @@ async function main() { }, }) } + if ('labels' in url.query) { + console.log('Generating naughty users with labels') + + const anchorPost = await server.mocker.createPost( + 'alice', + 'Anchor post', + ) + + for (const user of [ + 'csam-account', + 'csam-profile', + 'csam-posts', + 'porn-account', + 'porn-profile', + 'porn-posts', + 'nudity-account', + 'nudity-profile', + 'nudity-posts', + 'unknown-account', + 'unknown-profile', + 'unknown-posts', + 'always-filter-account', + 'always-filter-profile', + 'always-filter-posts', + 'always-warn-account', + 'always-warn-profile', + 'always-warn-posts', + 'muted-account', + 'muted-by-list-account', + ]) { + await server.mocker.createUser(user) + await server.mocker.follow('alice', user) + await server.mocker.follow(user, 'alice') + await server.mocker.createPost(user, `Unlabeled post from ${user}`) + await server.mocker.createReply( + user, + `Unlabeled reply from ${user}`, + anchorPost, + ) + await server.mocker.like(user, anchorPost) + } + + await server.mocker.labelAccount('csam', 'csam-account') + await server.mocker.labelProfile('csam', 'csam-profile') + await server.mocker.labelPost( + 'csam', + await server.mocker.createPost('csam-posts', 'csam post'), + ) + await server.mocker.labelPost( + 'csam', + await server.mocker.createQuotePost( + 'csam-posts', + 'csam quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'csam', + await server.mocker.createReply( + 'csam-posts', + 'csam reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('porn', 'porn-account') + await server.mocker.labelProfile('porn', 'porn-profile') + await server.mocker.labelPost( + 'porn', + await server.mocker.createPost('porn-posts', 'porn post'), + ) + await server.mocker.labelPost( + 'porn', + await server.mocker.createQuotePost( + 'porn-posts', + 'porn quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'porn', + await server.mocker.createReply( + 'porn-posts', + 'porn reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('nudity', 'nudity-account') + await server.mocker.labelProfile('nudity', 'nudity-profile') + await server.mocker.labelPost( + 'nudity', + await server.mocker.createPost('nudity-posts', 'nudity post'), + ) + await server.mocker.labelPost( + 'nudity', + await server.mocker.createQuotePost( + 'nudity-posts', + 'nudity quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'nudity', + await server.mocker.createReply( + 'nudity-posts', + 'nudity reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount( + 'not-a-real-label', + 'unknown-account', + ) + await server.mocker.labelProfile( + 'not-a-real-label', + 'unknown-profile', + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createPost('unknown-posts', 'unknown post'), + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createQuotePost( + 'unknown-posts', + 'unknown quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + 'not-a-real-label', + await server.mocker.createReply( + 'unknown-posts', + 'unknown reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('!filter', 'always-filter-account') + await server.mocker.labelProfile('!filter', 'always-filter-profile') + await server.mocker.labelPost( + '!filter', + await server.mocker.createPost( + 'always-filter-posts', + 'always-filter post', + ), + ) + await server.mocker.labelPost( + '!filter', + await server.mocker.createQuotePost( + 'always-filter-posts', + 'always-filter quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + '!filter', + await server.mocker.createReply( + 'always-filter-posts', + 'always-filter reply', + anchorPost, + ), + ) + + await server.mocker.labelAccount('!warn', 'always-warn-account') + await server.mocker.labelProfile('!warn', 'always-warn-profile') + await server.mocker.labelPost( + '!warn', + await server.mocker.createPost( + 'always-warn-posts', + 'always-warn post', + ), + ) + await server.mocker.labelPost( + '!warn', + await server.mocker.createQuotePost( + 'always-warn-posts', + 'always-warn quote post', + anchorPost, + ), + ) + await server.mocker.labelPost( + '!warn', + await server.mocker.createReply( + 'always-warn-posts', + 'always-warn reply', + anchorPost, + ), + ) + + await server.mocker.users.alice.agent.mute('muted-account.test') + await server.mocker.createPost('muted-account', 'muted post') + await server.mocker.createQuotePost( + 'muted-account', + 'muted quote post', + anchorPost, + ) + await server.mocker.createReply( + 'muted-account', + 'muted reply', + anchorPost, + ) + + const list = await server.mocker.createMuteList( + 'alice', + 'Muted Users', + ) + await server.mocker.addToMuteList( + 'alice', + list, + server.mocker.users['muted-by-list-account'].did, + ) + await server.mocker.createPost('muted-by-list-account', 'muted post') + await server.mocker.createQuotePost( + 'muted-by-list-account', + 'account quote post', + anchorPost, + ) + await server.mocker.createReply( + 'muted-by-list-account', + 'account reply', + anchorPost, + ) + } } console.log('Ready') return res.writeHead(200).end(server.pdsUrl) diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts index 38466ed8e8..7db4e912a6 100644 --- a/__e2e__/tests/create-account.test.ts +++ b/__e2e__/tests/create-account.test.ts @@ -20,7 +20,6 @@ describe('Create account', () => { await element(by.id('nextBtn')).tap() await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('passwordInput')).typeText('hunter2') - await element(by.id('is13Input')).tap() await device.takeScreenshot('4- entered account details') await element(by.id('nextBtn')).tap() await element(by.id('handleInput')).typeText('e2e-test') diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts index 1ec1774f3e..7fa9ff28c5 100644 --- a/__e2e__/tests/home-screen.test.ts +++ b/__e2e__/tests/home-screen.test.ts @@ -57,7 +57,9 @@ describe('Home screen', () => { .tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) diff --git a/__e2e__/tests/invite-codes.test.ts b/__e2e__/tests/invite-codes.test.ts index e3bb5d7f22..846d3b7688 100644 --- a/__e2e__/tests/invite-codes.test.ts +++ b/__e2e__/tests/invite-codes.test.ts @@ -37,7 +37,6 @@ describe('invite-codes', () => { await element(by.id('inviteCodeInput')).typeText(inviteCode) await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('passwordInput')).typeText('hunter2') - await element(by.id('is13Input')).tap() await device.takeScreenshot('4- entered account details') await element(by.id('nextBtn')).tap() await element(by.id('handleInput')).typeText('e2e-test') diff --git a/__e2e__/tests/mute-lists.test.ts b/__e2e__/tests/mute-lists.test.ts new file mode 100644 index 0000000000..e931625139 --- /dev/null +++ b/__e2e__/tests/mute-lists.test.ts @@ -0,0 +1,141 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Profile screen', () => { + let service: string + beforeAll(async () => { + service = 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 login(service, 'alice', 'hunter2') + await element(by.id('viewHeaderDrawerBtn')).tap() + await expect(element(by.id('drawer'))).toBeVisible() + await element(by.id('menuItemButton-Moderation')).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.id('changeAvatarLibraryBtn')).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.id('changeAvatarRemoveBtn')).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-2')).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.id('profileHeaderDropdownListAddRemoveBtn')).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.id('profileHeaderDropdownListAddRemoveBtn')).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() + }) +}) diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts index cf9debb59a..a7bb93656b 100644 --- a/__e2e__/tests/profile-screen.test.ts +++ b/__e2e__/tests/profile-screen.test.ts @@ -120,7 +120,9 @@ describe('Profile screen', () => { await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownReportBtn')).tap() await expect(element(by.id('reportAccountModal'))).toBeVisible() - await element(by.id('reportAccountRadios-spam')).tap() + await element( + by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportAccountModal'))).not.toBeVisible() }) @@ -166,7 +168,9 @@ describe('Profile screen', () => { await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts new file mode 100644 index 0000000000..a5cefdb26b --- /dev/null +++ b/__e2e__/tests/thread-muting.test.ts @@ -0,0 +1,116 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Thread muting', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login, create a thread, and log out', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + await element(by.id('composeFAB')).tap() + await element(by.id('composerTextInput')).typeText('Test thread') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, reply to the thread, and log out', async () => { + await login(service, 'bob', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply 1') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, confirm notification exists, mute thread, and log out', async () => { + await login(service, 'alice', 'hunter2') + + await element(by.id('bottomBarNotificationsBtn')).tap() + const bobNotifs = by.id('feedItem-by-bob.test') + await expect( + element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + ).toHaveText('Reply 1') + await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) + .atIndex(0) + .tap() + await element(by.id('postDropdownMuteThreadBtn')).tap() + // have to wait for the toast to clear + await waitFor(element(by.id('viewHeaderDrawerBtn'))) + .toBeVisible() + .withTimeout(5000) + + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, reply to the thread twice, and log out', async () => { + await login(service, 'bob', 'hunter2') + + await element(by.id('bottomBarProfileBtn')).tap() + await element(by.id('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') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply 3') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + + await element(by.id('bottomBarHomeBtn')).tap() + await element(by.id('viewHeaderDrawerBtn')).tap() + await element(by.id('menuItemButton-Settings')).tap() + await element(by.id('signOutBtn')).tap() + }) + + it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => { + await login(service, 'alice', 'hunter2') + + await element(by.id('bottomBarNotificationsBtn')).tap() + const bobNotifs = by.id('feedItem-by-bob.test') + await expect( + element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + ).not.toExist() + + await element(by.id('bottomBarHomeBtn')).tap() + const alicePosts = by.id('feedItem-by-alice.test') + await element(by.id('postDropdownBtn').withAncestor(alicePosts)) + .atIndex(0) + .tap() + await element(by.id('postDropdownMuteThreadBtn')).tap() + + // TODO + // the swipe down to trigger PTR isnt working and I dont want to block on this + // -prf + // await element(by.id('bottomBarNotificationsBtn')).tap() + // await element(by.id('notifsFeed')).swipe('down', 'fast') + // await waitFor(element(by.id('postText').withAncestor(bobNotifs))) + // .toBeVisible() + // .withTimeout(5000) + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), + // ).toHaveText('Reply 2') + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(1), + // ).toHaveText('Reply 3') + // await expect( + // element(by.id('postText').withAncestor(bobNotifs)).atIndex(2), + // ).toHaveText('Reply 1') + }) +}) diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts index f84c339cef..8d3eacc884 100644 --- a/__e2e__/tests/thread-screen.test.ts +++ b/__e2e__/tests/thread-screen.test.ts @@ -106,7 +106,9 @@ describe('Thread screen', () => { await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) @@ -116,7 +118,9 @@ describe('Thread screen', () => { await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('postDropdownReportBtn')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() - await element(by.id('reportPostRadios-spam')).tap() + await element( + by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), + ).tap() await element(by.id('sendReportBtn')).tap() await expect(element(by.id('reportPostModal'))).not.toBeVisible() }) diff --git a/__e2e__/util.ts b/__e2e__/util.ts index 78d9f9f5d1..f5bb728151 100644 --- a/__e2e__/util.ts +++ b/__e2e__/util.ts @@ -1,10 +1,24 @@ import {resolveConfig} from 'detox/internals' +import {execSync} from 'child_process' const platform = device.getPlatform() export async function openApp(opts: any) { opts = opts || {} const config = await resolveConfig() + + if (device.getPlatform() === 'ios') { + // disable password autofill + execSync( + `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`, + ) + execSync( + `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`, + ) + execSync( + `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`, + ) + } if (config.configurationName.split('.').includes('debug')) { return await openAppForDebugBuild(platform, opts) } else { @@ -42,6 +56,7 @@ export async function login( await device.takeScreenshot('2- opened service selector') } await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerTextInput')).tapReturnKey() await element(by.id('customServerSelectBtn')).tap() if (takeScreenshots) { await device.takeScreenshot('3- input custom service') diff --git a/__mocks__/expo-localization.js b/__mocks__/expo-localization.js new file mode 100644 index 0000000000..8bd537cf6c --- /dev/null +++ b/__mocks__/expo-localization.js @@ -0,0 +1 @@ +export const getLocales = jest.fn().mockResolvedValue([]) diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index f0ca7a9d4b..504b11c22e 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -1,106 +1,4 @@ -import { - LikelyType, - getLinkMeta, - getLikelyType, -} from '../../src/lib/link-meta/link-meta' -import {exampleComHtml} from './__mocks__/exampleComHtml' -import {BskyAgent} from '@atproto/api' -import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state' - -describe('getLinkMeta', () => { - let rootStore: RootStoreModel - - beforeEach(() => { - rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE})) - }) - - const inputs = [ - '', - 'httpbadurl', - 'https://example.com', - 'https://example.com/index.html', - 'https://example.com/image.png', - 'https://example.com/video.avi', - 'https://example.com/audio.ogg', - 'https://example.com/text.txt', - 'https://example.com/javascript.js', - 'https://bsky.app/', - 'https://bsky.app/index.html', - ] - const outputs = [ - { - error: 'Invalid URL', - likelyType: LikelyType.Other, - url: '', - }, - { - error: 'Invalid URL', - likelyType: LikelyType.Other, - url: 'httpbadurl', - }, - { - likelyType: LikelyType.HTML, - url: 'https://example.com', - title: 'Example Domain', - description: 'An example website', - }, - { - likelyType: LikelyType.HTML, - url: 'https://example.com/index.html', - title: 'Example Domain', - description: 'An example website', - }, - { - likelyType: LikelyType.Image, - url: 'https://example.com/image.png', - }, - { - likelyType: LikelyType.Video, - url: 'https://example.com/video.avi', - }, - { - likelyType: LikelyType.Audio, - url: 'https://example.com/audio.ogg', - }, - { - likelyType: LikelyType.Text, - url: 'https://example.com/text.txt', - }, - { - likelyType: LikelyType.Other, - url: 'https://example.com/javascript.js', - }, - { - likelyType: LikelyType.AtpData, - url: '/', - }, - { - likelyType: LikelyType.AtpData, - url: '/index.html', - }, - { - likelyType: LikelyType.Other, - url: '', - title: '', - }, - ] - it('correctly handles a set of text inputs', async () => { - for (let i = 0; i < inputs.length; i++) { - global.fetch = jest.fn().mockImplementationOnce(() => { - return new Promise((resolve, _reject) => { - resolve({ - ok: true, - status: 200, - text: () => exampleComHtml, - }) - }) - }) - const input = inputs[i] - const output = await getLinkMeta(rootStore, input) - expect(output).toEqual(outputs[i]) - } - }) -}) +import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta' describe('getLikelyType', () => { it('correctly handles non-parsed url', async () => { diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index f25bd02a78..936708cf26 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -48,7 +48,7 @@ describe('detectLinkables', () => { 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ', 'https://foo.com https://bar.com/whatever https://baz.com', 'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.', - 'parenthentical (https://foo.com)', + 'parenthetical (https://foo.com)', 'except for https://foo.com/thing_(cool)', ] const outputs = [ @@ -112,7 +112,7 @@ describe('detectLinkables', () => { {link: 'https://baz.com'}, '.', ], - ['parenthentical (', {link: 'https://foo.com'}, ')'], + ['parenthetical (', {link: 'https://foo.com'}, ')'], ['except for ', {link: 'https://foo.com/thing_(cool)'}], ] it('correctly handles a set of text inputs', () => { diff --git a/app.json b/app.json index ba2c6958fe..d427e04d0e 100644 --- a/app.json +++ b/app.json @@ -2,11 +2,15 @@ "expo": { "name": "Bluesky", "slug": "bluesky", + "scheme": "bluesky", "owner": "blueskysocial", - "version": "1.18.0", + "version": "1.32.0", + "runtimeVersion": { + "policy": "appVersion" + }, "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "automatic", "splash": { "image": "./assets/cloud-splash.png", "resizeMode": "cover", @@ -26,27 +30,51 @@ ], "BGTaskSchedulerPermittedIdentifiers": [ "com.transistorsoft.fetch" - ] - } + ], + "NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.", + "NSMicrophoneUsageDescription": "Used for posts and other kinds of content.", + "NSPhotoLibraryAddUsageDescription": "Used to save images to your library.", + "NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content" + }, + "associatedDomains": ["applinks:bsky.app", "applinks:staging.bsky.app"] }, "androidStatusBar": { "barStyle": "dark-content", "backgroundColor": "#ffffff" }, "android": { - "versionCode": 3, + "versionCode": 18, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" }, - "package": "xyz.blueskyweb.app" + "package": "xyz.blueskyweb.app", + "intentFilters": [ + { + "action": "VIEW", + "autoVerify": true, + "data": [ + { + "scheme": "https", + "host": "bsky.app" + } + ], + "category": ["BROWSABLE", "DEFAULT"] + } + ] }, "web": { "favicon": "./assets/favicon.png" }, + "updates": { + "enabled": true, + "fallbackToCacheTimeout": 1000, + "url": "https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302" + }, "plugins": [ "expo-localization", "react-native-background-fetch", + "sentry-expo", [ "expo-build-properties", { @@ -66,6 +94,17 @@ "eas": { "projectId": "55bd077a-d905-4184-9c7f-94789ba0f302" } + }, + "hooks": { + "postPublish": [ + { + "file": "sentry-expo/upload-sourcemaps", + "config": { + "organization": "blueskyweb", + "project": "react-native" + } + } + ] } } } diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore index b2a31beb38..1d945e1dab 100644 --- a/bskyweb/.gitignore +++ b/bskyweb/.gitignore @@ -4,5 +4,11 @@ test-coverage.out # Don't check in the binary. /bskyweb +# Don't accidentally commit JS-generated code +static/js/*.js +static/js/*.map +static/js/*.js.LICENSE.txt +templates/scripts.html + # Don't ignore this file !.gitignore diff --git a/bskyweb/Makefile b/bskyweb/Makefile index 957f043f52..7561a14544 100644 --- a/bskyweb/Makefile +++ b/bskyweb/Makefile @@ -39,4 +39,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 + GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve --debug diff --git a/bskyweb/README.md b/bskyweb/README.md index a74cda0d5d..d60647379b 100644 --- a/bskyweb/README.md +++ b/bskyweb/README.md @@ -2,7 +2,7 @@ ### SPA Bundle (monolithic static javascript file) -To build the SPA bundle (`bundle.web.js`), first get a Javascript development +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: diff --git a/bskyweb/cmd/bskyweb/.gitignore b/bskyweb/cmd/bskyweb/.gitignore new file mode 100644 index 0000000000..c810652a10 --- /dev/null +++ b/bskyweb/cmd/bskyweb/.gitignore @@ -0,0 +1 @@ +/bskyweb diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go index 67ee6a2d68..e892971f9c 100644 --- a/bskyweb/cmd/bskyweb/mailmodo.go +++ b/bskyweb/cmd/bskyweb/mailmodo.go @@ -14,13 +14,15 @@ type Mailmodo struct { httpClient *http.Client APIKey string BaseURL string + ListName string } -func NewMailmodo(apiKey string) *Mailmodo { +func NewMailmodo(apiKey, listName string) *Mailmodo { return &Mailmodo{ APIKey: apiKey, BaseURL: "https://api.mailmodo.com/api/v1", httpClient: &http.Client{}, + ListName: listName, } } @@ -56,9 +58,9 @@ func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod str return nil } -func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error { +func (m *Mailmodo) AddToList(ctx context.Context, email string) error { return m.request(ctx, "POST", "addToList", map[string]any{ - "listName": listName, + "listName": m.ListName, "email": email, "data": map[string]any{ "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))), diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 0c8e9f8d2a..e8a71dfe47 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,11 +2,17 @@ package main import ( "context" + "encoding/json" + "errors" "fmt" "io/fs" + "io/ioutil" "net/http" "os" + "os/signal" "strings" + "syscall" + "time" comatproto "github.com/bluesky-social/indigo/api/atproto" appbsky "github.com/bluesky-social/indigo/api/bsky" @@ -15,13 +21,18 @@ import ( "github.com/bluesky-social/social-app/bskyweb" "github.com/flosch/pongo2/v6" + "github.com/klauspost/compress/gzhttp" + "github.com/klauspost/compress/gzip" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/urfave/cli/v2" ) type Server struct { - xrpcc *xrpc.Client + echo *echo.Echo + httpd *http.Server + mailmodo *Mailmodo + xrpcc *xrpc.Client } func serve(cctx *cli.Context) error { @@ -33,8 +44,11 @@ func serve(cctx *cli.Context) error { mailmodoAPIKey := cctx.String("mailmodo-api-key") mailmodoListName := cctx.String("mailmodo-list-name") + // Echo + e := echo.New() + // Mailmodo client. - mailmodo := NewMailmodo(mailmodoAPIKey) + mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName) // create a new session // TODO: does this work with no auth at all? @@ -47,7 +61,7 @@ func serve(cctx *cli.Context) error { } auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ - Identifier: &xrpcc.Auth.Handle, + Identifier: xrpcc.Auth.Handle, Password: atpPassword, }) if err != nil { @@ -58,10 +72,76 @@ func serve(cctx *cli.Context) error { xrpcc.Auth.Did = auth.Did xrpcc.Auth.Handle = auth.Handle - server := Server{xrpcc} + // httpd + var ( + httpTimeout = 2 * time.Minute + httpMaxHeaderBytes = 2 * (1024 * 1024) + gzipMinSizeBytes = 1024 * 2 + gzipCompressionLevel = gzip.BestSpeed + gzipExceptMIMETypes = []string{"image/png"} + ) + // Wrap the server handler in a gzip handler to compress larger responses. + gzipHandler, err := gzhttp.NewWrapper( + gzhttp.MinSize(gzipMinSizeBytes), + gzhttp.CompressionLevel(gzipCompressionLevel), + gzhttp.ExceptContentTypes(gzipExceptMIMETypes), + ) + if err != nil { + return err + } + + // + // server + // + server := &Server{ + echo: e, + mailmodo: mailmodo, + xrpcc: xrpcc, + } + + // Create the HTTP server. + server.httpd = &http.Server{ + Handler: gzipHandler(server), + Addr: httpAddress, + WriteTimeout: httpTimeout, + ReadTimeout: httpTimeout, + MaxHeaderBytes: httpMaxHeaderBytes, + } + + e.HideBanner = true + // SECURITY: Do not modify without due consideration. + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSMaxAge: 31536000, // 365 days + // TODO: + // ContentSecurityPolicy + // XSSProtection + })) + e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + // Don't log requests for static content. + Skipper: func(c echo.Context) bool { + return strings.HasPrefix(c.Request().URL.Path, "/static") + }, + })) + e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) + e.HTTPErrorHandler = server.errorHandler + + // redirect trailing slash to non-trailing slash. + // all of our current endpoints have no trailing slash. + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusFound, + })) + + // + // configure routes + // + + // static files staticHandler := http.FileServer(func() http.FileSystem { if debug { + log.Debugf("serving static file from the local file system") return http.FS(os.DirFS("static")) } fsys, err := fs.Sub(bskyweb.StaticFS, "static") @@ -70,28 +150,28 @@ func serve(cctx *cli.Context) error { } return http.FS(fsys) }()) - - e := echo.New() - e.HideBanner = true - e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - // Don't log requests for static content. - Skipper: func(c echo.Context) bool { - return strings.HasPrefix(c.Request().URL.Path, "/static") - }, - Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", - })) - e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) - e.HTTPErrorHandler = customHTTPErrorHandler - - // configure routes e.GET("/robots.txt", echo.WrapHandler(staticHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) + e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) + e.GET("/security.txt", func(c echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") + }) + + // home e.GET("/", server.WebHome) // generic routes e.GET("/search", server.WebGeneric) + e.GET("/search/feeds", server.WebGeneric) + e.GET("/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) + e.GET("/moderation", server.WebGeneric) + e.GET("/moderation/mute-lists", server.WebGeneric) + e.GET("/moderation/muted-accounts", server.WebGeneric) + e.GET("/moderation/blocked-accounts", server.WebGeneric) e.GET("/settings", server.WebGeneric) + e.GET("/settings/app-passwords", server.WebGeneric) + e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) e.GET("/support", server.WebGeneric) @@ -104,6 +184,9 @@ func serve(cctx *cli.Context) error { 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) // post endpoints; only first populates info e.GET("/profile/:handle/post/:rkey", server.WebPost) @@ -111,19 +194,54 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) // Mailmodo - e.POST("/waitlist", func(c echo.Context) error { - email := strings.TrimSpace(c.FormValue("email")) - if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { - return err - } - return c.JSON(http.StatusOK, map[string]bool{"success": true}) - }) + e.POST("/api/waitlist", server.apiWaitlist) + // Start the server. log.Infof("starting server address=%s", httpAddress) - return e.Start(httpAddress) + go func() { + if err := server.httpd.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + log.Errorf("HTTP server shutting down unexpectedly: %s", err) + } + } + }() + + // Wait for a signal to exit. + log.Info("registering OS exit signal handler") + quit := make(chan struct{}) + exitSignals := make(chan os.Signal, 1) + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-exitSignals + log.Infof("received OS exit signal: %s", sig) + + // Shut down the HTTP server. + if err := server.Shutdown(); err != nil { + log.Errorf("HTTP server shutdown error: %s", err) + } + + // Trigger the return that causes an exit. + close(quit) + }() + <-quit + log.Infof("graceful shutdown complete") + return nil } -func customHTTPErrorHandler(err error, c echo.Context) { +func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + srv.echo.ServeHTTP(rw, req) +} + +func (srv *Server) Shutdown() error { + log.Info("shutting down") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + return srv.httpd.Shutdown(ctx) +} + +func (srv *Server) errorHandler(err error, c echo.Context) { code := http.StatusInternalServerError if he, ok := err.(*echo.HTTPError); ok { code = he.Code @@ -167,7 +285,13 @@ func (srv *Server) WebPost(c echo.Context) error { if err != nil { log.Warnf("failed to fetch post: %s\t%v", uri, err) } else { - data["postView"] = tpv.Thread.FeedDefs_ThreadViewPost.Post + 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 + } } } @@ -185,9 +309,44 @@ func (srv *Server) WebProfile(c echo.Context) error { 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) } } 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/go.mod b/bskyweb/go.mod index 9014fa1b55..5f06bfc459 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -3,18 +3,19 @@ module github.com/bluesky-social/social-app/bskyweb go 1.20 require ( - github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8 + github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 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.1 + github.com/urfave/cli/v2 v2.25.3 ) require ( github.com/benbjohnson/clock v1.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -26,7 +27,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.1.2 // indirect - github.com/ipfs/go-cid v0.4.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 @@ -68,22 +69,22 @@ require ( 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-20230331140348-1f892b517e70 // 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/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.opentelemetry.io/otel v1.15.1 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // 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.7.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.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 golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gorm.io/driver/postgres v1.5.0 // indirect - gorm.io/driver/sqlite v1.4.4 // indirect - gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 // indirect + gorm.io/driver/sqlite v1.5.0 // indirect + gorm.io/gorm v1.25.0 // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/bskyweb/go.sum b/bskyweb/go.sum index 9155dc34d4..ae5d7defb5 100644 --- a/bskyweb/go.sum +++ b/bskyweb/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 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-20230403211508-3cb4320bd5c8 h1:6A8D48CksjnlmJb8r5WaFAB4J2osllkmQoQhhiREMHw= -github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8/go.mod h1:9dcnKLtEnDPBdWm5/BLJHvbaPndCdKzAaBK7sbGt3S4= +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/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= @@ -12,8 +12,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +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/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= @@ -55,8 +56,8 @@ github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUP 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-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= -github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA= -github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +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= @@ -96,7 +97,6 @@ github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -105,6 +105,8 @@ 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= @@ -209,8 +211,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +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/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= @@ -218,8 +220,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ 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-20230331140348-1f892b517e70 h1:iNBzUKTsJc9RqStEVX2VYgVHATTU39IuB7g0e8OPWXU= -github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= +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/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -228,14 +230,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +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= 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.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -255,8 +257,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 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 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/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= @@ -273,8 +276,9 @@ 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 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 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/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= @@ -297,8 +301,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc 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 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 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/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= @@ -307,8 +312,9 @@ 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 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 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/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= @@ -343,11 +349,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.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= -gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= -gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= +gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= +gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= 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= 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= diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association new file mode 100644 index 0000000000..232acdf255 --- /dev/null +++ b/bskyweb/static/.well-known/apple-app-site-association @@ -0,0 +1,13 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "B3LX46C5HS.xyz.blueskyweb.app", + "paths": [ + "*" + ] + } + ] + } +} \ No newline at end of file diff --git a/bskyweb/static/.well-known/assetlinks.json b/bskyweb/static/.well-known/assetlinks.json new file mode 100644 index 0000000000..5ca12d5b31 --- /dev/null +++ b/bskyweb/static/.well-known/assetlinks.json @@ -0,0 +1,11 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "xyz.blueskyweb.app", + "sha256_cert_fingerprints": + ["C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"] + } + } +] diff --git a/bskyweb/static/.well-known/security.txt b/bskyweb/static/.well-known/security.txt new file mode 100644 index 0000000000..8173cb72d6 --- /dev/null +++ b/bskyweb/static/.well-known/security.txt @@ -0,0 +1,4 @@ +Contact: mailto:security@bsky.app +Preferred-Languages: en +Canonical: https://bsky.app/.well-known/security.txt +Acknowledgements: https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md diff --git a/bskyweb/static/robots.txt b/bskyweb/static/robots.txt index d3475984e8..4f8510d18d 100644 --- a/bskyweb/static/robots.txt +++ b/bskyweb/static/robots.txt @@ -1 +1,9 @@ -# hello friends! +# Hello Friends! +# If you are considering bulk or automated crawling, you may want to look in +# to our protocol (API), including a firehose of updates. See: https://atproto.com/ + +# By default, may crawl anything on this domain. HTTP 429 ("backoff") status +# codes are used for rate-limiting. Up to a handful concurrent requests should +# be ok. +User-Agent: * +Allow: / diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 28b92958ef..b5ed329cd0 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -1,9 +1,8 @@ - - - + + {%- block head_title -%}Bluesky{%- endblock -%} @@ -57,10 +56,6 @@ } }*/ - /* Remove focus state on inputs */ - *:focus { - outline: 0; - } /* Remove default link styling */ a { color: inherit; @@ -68,6 +63,9 @@ a[role="link"]:hover { text-decoration: underline; } + a[role="link"][data-no-underline="1"]:hover { + text-decoration: none; + } /* Styling hacks */ *[data-word-wrap] { @@ -99,58 +97,35 @@ color: #0085ff; cursor: pointer; } + /* OLLIE: TODO -- this is not accessible */ + /* Remove focus state on inputs */ + .ProseMirror-focused { + outline: 0; + } + textarea:focus, + input:focus { + outline: 0; + } .tippy-content .items { - border-radius: 6px; - background: #F3F3F8; - border: 1px solid #e0d9d9; - padding: 3px 3px; - } - .tippy-content .items .item { - display: block; - background: transparent; - color: #8a8c9a; - border: 0; - font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - padding: 7px 10px 8px; - width: 100%; - text-align: left; - box-sizing: border-box; - letter-spacing: 0.2px; - } - .tippy-content .items .item.is-selected { - background: #fff; - border-radius: 4px; - color: #333; + width: fit-content; } {% include "scripts.html" %} - - - - {% block head_page_meta -%} - - - - - - - - - {%- endblock %} - - - + + + + {% block html_head_extra -%}{%- endblock %} + - {% block head_metadata %}{% endblock %} {%- block body_all %}