├── .babelrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── base.tsconfig.json ├── browser.tsconfig.json ├── database.json ├── docker └── Dockerfile ├── jest.config.js ├── migrations ├── 20180401000000-persons.js ├── 20180402000000-cookies.js ├── 20180403000000-notes.js ├── 20180404000000-follows.js ├── 20180405000000-local_accounts.js ├── 20180406000000-remote_accounts.js ├── 20180407000000-insert_local_account.js ├── 20180408000000-insert_remote_account.js ├── 20180409000000-uris.js ├── 20180503000000-attributed_to_id.js ├── 20180504000000-actor_id.js ├── 20180505000000-object_id.js ├── 20180506000000-remote_account_uris.js ├── 20180507000000-local_accounts_id.js ├── 20180508000000-notes_id.js ├── 20180509000000-mentions.js ├── 20180510000000-insert_note.js ├── 20180601000000-statuses.js ├── 20180602000000-tombstones.js ├── 20180603000000-delete_status.js ├── 20180604000000-cookies_change_person_id.js ├── 20180605000000-cookies_add_account_id.js ├── 20180606000000-cookies_remove_person_id.js ├── 20180607000000-cookies_rename_person_id_to_account_id.js ├── 20180608000000-likes.js ├── 20180609000000-notes_summary.js ├── 20180610000000-notes_in_reply_to_id.js ├── 20180620000000-statuses_published.js ├── 20180621000000-hashtags.js ├── 20180622000000-persons_summary.js ├── 20180623000000-persons_name.js ├── 20180630000000-actors.js ├── 20180630000000-uris_allocated.js ├── 20180640000000-announces_object_id.js ├── 20180720000000-documents.js ├── 20190408000000-der.js ├── 20190606000000-dirty_documents.js └── 20190606000000-insert_document_without_url.js ├── mini-migrate.js ├── mini-serve.js ├── node-session.tsconfig.json ├── node.tsconfig.json ├── package-lock.json ├── package.d.ts ├── package.json ├── src ├── bull-arena.d.ts ├── client.ts ├── http-signature.d.ts ├── lib │ ├── generate_uuid.test.ts │ ├── generate_uuid.ts │ ├── generated_activitystreams.ts │ ├── generated_webfinger.ts │ ├── isomorphism │ │ ├── browser │ │ │ └── session │ │ │ │ └── event_source.ts │ │ └── node │ │ │ └── session │ │ │ └── event_source.ts │ ├── parsed_activitystreams │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── resolver.test.ts │ │ └── resolver.ts │ ├── repository │ │ ├── actors.test.ts │ │ ├── actors.ts │ │ ├── announces.test.ts │ │ ├── announces.ts │ │ ├── challenges.test.ts │ │ ├── challenges.ts │ │ ├── cookies.ts │ │ ├── dirty_documents.ts │ │ ├── documents.test.ts │ │ ├── documents.ts │ │ ├── follows.test.ts │ │ ├── follows.ts │ │ ├── hashtags.test.ts │ │ ├── hashtags.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── likes.test.ts │ │ ├── likes.ts │ │ ├── local_accounts.test.ts │ │ ├── local_accounts.ts │ │ ├── mentions.test.ts │ │ ├── mentions.ts │ │ ├── notes.test.ts │ │ ├── notes.ts │ │ ├── pg.test.ts │ │ ├── pg.ts │ │ ├── remote_accounts.test.ts │ │ ├── remote_accounts.ts │ │ ├── statuses.test.ts │ │ ├── statuses.ts │ │ ├── subscribers.ts │ │ ├── syslog.ts │ │ ├── unlinked_documents.ts │ │ ├── uris.test.ts │ │ └── uris.ts │ ├── session │ │ ├── actor.ts │ │ ├── announce.ts │ │ ├── fetch.ts │ │ ├── identity.ts │ │ ├── like.ts │ │ ├── note.ts │ │ └── types.ts │ ├── test │ │ ├── fabricator.ts │ │ ├── repository.ts │ │ └── types.ts │ ├── transfer.test.ts │ ├── transfer.ts │ └── tuples │ │ ├── accept.test.ts │ │ ├── accept.ts │ │ ├── actor │ │ ├── base.ts │ │ ├── from_parsed_activitystreams.test.ts │ │ ├── from_parsed_activitystreams.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── resolver.test.ts │ │ └── resolver.ts │ │ ├── announce.test.ts │ │ ├── announce.ts │ │ ├── challenge.test.ts │ │ ├── challenge.ts │ │ ├── cookie.test.ts │ │ ├── cookie.ts │ │ ├── create.test.ts │ │ ├── create.ts │ │ ├── delete.d.ts │ │ ├── delete.test.ts │ │ ├── delete.ts │ │ ├── dirty_document.ts │ │ ├── document.test.ts │ │ ├── document.ts │ │ ├── follow.test.ts │ │ ├── follow.ts │ │ ├── hashtag.test.ts │ │ ├── hashtag.ts │ │ ├── key.test.ts │ │ ├── key.ts │ │ ├── like.test.ts │ │ ├── like.ts │ │ ├── local_account.test.ts │ │ ├── local_account.ts │ │ ├── mention.test.ts │ │ ├── mention.ts │ │ ├── note.test.ts │ │ ├── note.ts │ │ ├── ordered_collection.test.ts │ │ ├── ordered_collection.ts │ │ ├── ordered_collection_page.test.ts │ │ ├── ordered_collection_page.ts │ │ ├── relation.test.ts │ │ ├── relation.ts │ │ ├── remote_account.test.ts │ │ ├── remote_account.ts │ │ ├── status.test.ts │ │ ├── status.ts │ │ ├── undo.test.ts │ │ ├── undo.ts │ │ ├── uri.test.ts │ │ └── uri.ts ├── options.ts ├── processor.ts ├── routes │ ├── .well-known │ │ └── webfinger.ts │ ├── @[acct] │ │ ├── [id].ts │ │ ├── inbox.ts │ │ ├── index.svelte │ │ ├── index.ts │ │ └── outbox.ts │ ├── [uuid].ts │ ├── _dashboard.svelte │ ├── _error.svelte │ ├── _landing.svelte │ ├── _layout.svelte │ ├── _secure.ts │ ├── _send_activitystreams.ts │ ├── api │ │ ├── _cookie.ts │ │ ├── events.ts │ │ ├── proxy.ts │ │ ├── signin.ts │ │ ├── signup.ts │ │ └── uploadMedia.ts │ └── index.svelte ├── sapper.d.ts ├── server.ts ├── service-worker.js ├── subsystems │ ├── client.ts │ ├── processor │ │ ├── handlers │ │ │ ├── accept.test.ts │ │ │ ├── accept.ts │ │ │ ├── index.ts │ │ │ ├── post_follow.test.ts │ │ │ ├── post_follow.ts │ │ │ ├── post_like.test.ts │ │ │ ├── post_like.ts │ │ │ ├── post_status.test.ts │ │ │ ├── post_status.ts │ │ │ ├── process_inbox.test.ts │ │ │ └── process_inbox.ts │ │ └── index.ts │ └── server.ts ├── svelte.d.ts └── template.html ├── static └── agpl-3.0-standalone.html ├── test.tsconfig.json ├── travis └── create-bucket.js ├── ts.eslintrc.json ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["transform-es2015-modules-commonjs"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | checks: 3 | argument-count: 4 | enabled: false 5 | complex-logic: 6 | enabled: false 7 | file-lines: 8 | enabled: false 9 | method-complexity: 10 | enabled: false 11 | method-count: 12 | enabled: false 13 | method-lines: 14 | enabled: false 15 | nested-control-flow: 16 | enabled: false 17 | return-statements: 18 | enabled: false 19 | similar-code: 20 | enabled: false 21 | identical-code: 22 | enabled: false 23 | plugins: 24 | eslint: 25 | enabled: true 26 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Miniverse authors 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, version 3 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU Affero General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Affero General Public License 13 | # along with this program. If not, see . 14 | 15 | /__sapper__ 16 | /src/node_modules 17 | /src/service-worker.js 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "browser": true, "es6": true, "jest": true, "node": true }, 3 | "extends": "eslint:recommended", 4 | "globals": { "ga": false, "__non_webpack_require__": false }, 5 | "parserOptions": { "ecmaVersion": 8, "sourceType": "module" } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Miniverse authors 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, version 3 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU Affero General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Affero General Public License 13 | # along with this program. If not, see . 14 | 15 | node_modules 16 | /__sapper__ 17 | /docker/miniverse-*.tgz 18 | /static/license.html 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "DefinitelyTyped"] 2 | path = DefinitelyTyped 3 | url = https://github.com/miniorg/DefinitelyTyped.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Miniverse authors 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, version 3 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU Affero General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Affero General Public License 13 | # along with this program. If not, see . 14 | 15 | * 16 | !/__sapper__/** 17 | !/AUTHORS 18 | !/LICENSE 19 | !/README.md 20 | !/database.json 21 | !/migrations/** 22 | !/mini-migrate.js 23 | !/mini-serve.js 24 | !/static/** 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 11 3 | stages: 4 | - test 5 | - name: deploy 6 | if: branch = master 7 | jobs: 8 | include: 9 | - &job_test_linux_100 10 | stage: test 11 | os: linux 12 | dist: xenial 13 | sudo: false 14 | addons: 15 | postgresql: 10 16 | env: 17 | - AWS_ENDPOINT=http://127.0.0.1:9000 18 | - AWS_ACCESS_KEY_ID=minio 19 | - AWS_SECRET_ACCESS_KEY=miniostorage 20 | - AWS_SIGNATURE_VERSION=v4 21 | - AWS_S3_BUCKET=miniverse 22 | - AWS_S3_FORCE_PATH_STYLE=1 23 | - MINIO_ACCESS_KEY=minio 24 | - MINIO_SECRET_KEY=miniostorage 25 | - <<: *job_test_linux_100 26 | addons: 27 | postgresql: 9.6 28 | - <<: *job_test_linux_100 29 | addons: 30 | postgresql: 9.5 31 | - <<: *job_test_linux_100 32 | addons: 33 | postgresql: 9.6 34 | dist: trusty 35 | - <<: *job_test_linux_100 36 | addons: 37 | postgresql: 9.5 38 | dist: trusty 39 | - <<: *job_test_linux_100 40 | os: osx 41 | before_install: 42 | - initdb -D "$HOME/postgres" 43 | - pg_ctl -D "$HOME/postgres" start 44 | - brew install minio redis 45 | - minio server travis/minio.RELEASE.2018-07-23T18-34-49Z/srv/data & 46 | - redis-server --daemonize yes 47 | - pg_isready 48 | cache: 49 | directories: 50 | - node_modules 51 | - "$HOME/Library/Caches/Homebrew" 52 | - stage: deploy 53 | os: linux 54 | dist: xenial 55 | sudo: false 56 | addons: 57 | apt: 58 | packages: 59 | - awscli 60 | script: 61 | - npm pack 62 | - mv miniverse-0.0.1.tgz docker 63 | - docker build -t miniorg/miniverse docker 64 | - docker login -p "$DOCKER_PASSWORD" -u "$DOCKER_USER" 65 | - docker push miniorg/miniverse 66 | - pip install --user awscli 67 | - aws ecs update-service --force-new-deployment --service server 68 | services: 69 | - redis-server 70 | cache: 71 | directories: 72 | - node_modules 73 | - travis/minio.RELEASE.2019-04-04T18-31-46Z/bin 74 | before_install: 75 | - | 76 | if [ ! -f travis/minio.RELEASE.2019-04-04T18-31-46Z/bin/minio ] 77 | then 78 | mkdir -p travis/minio.RELEASE.2019-04-04T18-31-46Z/bin 79 | wget https://dl.minio.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2019-04-04T18-31-46Z -O travis/minio.RELEASE.2019-04-04T18-31-46Z/bin/minio 80 | sha1sum -c <<< '06ad2f55f370d2ad1d1310cbdcae8889637a8eb1 travis/minio.RELEASE.2019-04-04T18-31-46Z/bin/minio' 81 | chmod a+x travis/minio.RELEASE.2019-04-04T18-31-46Z/bin/minio 82 | fi 83 | - ./travis/minio.RELEASE.2019-04-04T18-31-46Z/bin/minio server travis/minio.RELEASE.2019-04-04T18-31-46Z/srv/data & 84 | script: 85 | - npx eslint . 86 | - npx eslint -c ts.eslintrc.json --ext .ts . 87 | - createdb travis_test_1 88 | - PGDATABASE=travis_test_1 npx db-migrate up 89 | - node travis/create-bucket.js 90 | - npm test -- -w 1 91 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Akihiko Odaki (https://pub.miniverse.social/@aki) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miniverse 2 | 3 | Miniverse is an experimental microblogging software capbable to perform 4 | server-to-server interaction with recent Mastodon versions. 5 | 6 | # Some database caveats 7 | ## Do not delete any record of `local_accounts` 8 | References from `local_accounts` has `ON DELETE RESTRICT` because every records 9 | of `local_accounts` includes a precious private key. You should follow the 10 | principle when making a new reference. 11 | 12 | ## Do not update `id` column of any record 13 | The application tracks references by `id` column and therefore 14 | `ON UPDATE CASCADE`, which is effective only on the database, is not sufficient. 15 | References to `id` column should have `ON UPDATE RESTRICT`. 16 | 17 | # License 18 | 19 | `app/service-worker.js` is copied from: 20 | https://github.com/sveltejs/sapper-template/blob/master/app/service-worker.js 21 | 22 | It is written by Sapper authors, and licensed under LIL. 23 | 24 | The other portions are licensed under AGPL-3.0 by Akihiko Odaki. 25 | -------------------------------------------------------------------------------- /base.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "downlevelIteration": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "strict": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /browser.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.tsconfig", 3 | "compilerOptions": { 4 | "lib": ["es2017", "dom"], 5 | "types": ["webpack-env"] 6 | }, 7 | "include": [ 8 | "src/client.ts", 9 | "src/lib/session", 10 | "src/sapper.d.ts", 11 | "src/svelte.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /database.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "universal", 3 | "universal": { 4 | "driver": "pg", 5 | "user": { "ENV": "PGUSER" }, 6 | "database": { "ENV": "PGDATABASE" }, 7 | "port": { "ENV": "PGPORT" }, 8 | "host": { "ENV": "PGHOST" }, 9 | "password": { "ENV": "PGPASSWORD" }, 10 | "binary": { "ENV": "PGBINARY" }, 11 | "client_encoding": { "ENV": "PGCLIENT_ENCODING" }, 12 | "replication": { "ENV": "PGREPLICATION" }, 13 | "application_name": { "ENV": "PGAPPNAME" } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Miniverse authors 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, version 3 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU Affero General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Affero General Public License 13 | # along with this program. If not, see . 14 | 15 | FROM node:11-alpine as base 16 | RUN apk add expat fftw giflib glib lcms2 libexif libjpeg-turbo libpng libwebp orc tiff 17 | 18 | FROM base 19 | RUN apk add --no-cache --virtual build \ 20 | build-base expat-dev fftw-dev giflib-dev glib-dev lcms2-dev libexif-dev \ 21 | libjpeg-turbo-dev libpng-dev libwebp-dev pkgconfig python2 orc-dev tiff-dev 22 | RUN wget https://github.com/libvips/libvips/releases/download/v8.7.4/vips-8.7.4.tar.gz && \ 23 | echo 'c2ba4455e477ab707fb0abf946fbae010e266364 vips-8.7.4.tar.gz' | sha1sum -c 24 | COPY miniverse-0.0.1.tgz / 25 | RUN tar xf vips-8.7.4.tar.gz && \ 26 | cd vips-8.7.4 && \ 27 | ./configure && \ 28 | make -j`getconf _NPROCESSORS_ONLN` install && \ 29 | cd .. && \ 30 | npm -g config set user root && npm -g install miniverse-0.0.1.tgz && \ 31 | rm -r miniverse-0.0.1.tgz vips-8.7.4 vips-8.7.4.tar.gz && \ 32 | apk del build 33 | 34 | FROM base 35 | CMD mini-migrate up && exec mini-serve 36 | ENV NO_UPDATE_NOTIFIER=1 37 | USER node 38 | COPY --from=1 / / 39 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 'ts-jest': { tsConfig: 'base.tsconfig.json' } }, 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/DefinitelyTyped/', '/node_modules/'], 5 | transform: { '\\.ts$': 'ts-jest' } 6 | } 7 | -------------------------------------------------------------------------------- /migrations/20180401000000-persons.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('persons', { 18 | id: { type: 'int', autoIncrement: true, notNull: true, primaryKey: true }, 19 | username: { type: 'string', notNull: true }, 20 | host: { type: 'string', notNull: true }, 21 | }, error => { 22 | if (error) { 23 | callback(error); 24 | } else { 25 | db.runSql('CREATE UNIQUE INDEX persons_acct ON persons (lower(host), username)', callback); 26 | } 27 | }); 28 | 29 | exports._meta = { version: 1 }; 30 | -------------------------------------------------------------------------------- /migrations/20180402000000-cookies.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('cookies', { 18 | digest: { type: 'bytea', notNull: true, primaryKey: true }, 19 | person_id: { 20 | type: 'int', 21 | notNull: true, 22 | foreignKey: { 23 | name: 'person_id', 24 | table: 'persons', 25 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 26 | mapping: 'id', 27 | } 28 | } 29 | }, callback); 30 | 31 | exports._meta = { version: 1 }; 32 | -------------------------------------------------------------------------------- /migrations/20180403000000-notes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('notes', { 18 | id: { type: 'bigint', autoIncrement: true, notNull: true, primaryKey: true }, 19 | attributed_to_id: { 20 | type: 'int', 21 | notNull: true, 22 | foreignKey: { 23 | name: 'attributed_to_id', 24 | table: 'persons', 25 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 26 | mapping: 'id', 27 | } 28 | }, 29 | content: { type: 'string', notNull: true } 30 | }, callback); 31 | 32 | exports._meta = { version: 1 }; 33 | -------------------------------------------------------------------------------- /migrations/20180404000000-follows.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('follows', { 18 | id: { type: 'int', autoIncrement: true, notNull: true, primaryKey: true }, 19 | actor_id: { 20 | type: 'int', 21 | notNull: true, 22 | foreignKey: { 23 | name: 'actor_id', 24 | table: 'persons', 25 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 26 | mapping: 'id', 27 | } 28 | }, 29 | object_id: { 30 | type: 'int', 31 | notNull: true, 32 | foreignKey: { 33 | name: 'object_id', 34 | table: 'persons', 35 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 36 | mapping: 'id', 37 | } 38 | } 39 | }, error => { 40 | if (error) { 41 | callback(error); 42 | } else { 43 | db.addIndex( 44 | 'follows', 45 | 'follows_actor_id_object_id', 46 | ['actor_id', 'object_id'], 47 | true, 48 | callback); 49 | } 50 | }); 51 | 52 | exports._meta = { version: 1 }; 53 | -------------------------------------------------------------------------------- /migrations/20180405000000-local_accounts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('local_accounts', { 18 | person_id: { 19 | type: 'int', 20 | notNull: true, 21 | primaryKey: true, 22 | foreignKey: { 23 | name: 'person_id', 24 | table: 'persons', 25 | 26 | // local_accounts should not be deleted as it contains precious private 27 | // keys. 28 | rules: { onDelete: 'RESTRICT', onUpdate: 'CASCADE' }, 29 | 30 | mapping: 'id', 31 | } 32 | }, 33 | admin: { type: 'boolean', notNull: true }, 34 | private_key_pem: { type: 'string', notNull: true }, 35 | salt: { type: 'bytea', notNull: true }, 36 | server_key: { type: 'bytea', notNull: true }, 37 | stored_key: { type: 'bytea', notNull: true } 38 | }, callback); 39 | 40 | exports._meta = { version: 1 }; 41 | -------------------------------------------------------------------------------- /migrations/20180406000000-remote_accounts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('remote_accounts', { 18 | uri: { type: 'string', notNull: true, unique: true }, 19 | inbox_uri: { type: 'string', notNull: true }, 20 | key_uri: { type: 'string', notNull: true, unique: true }, 21 | public_key_pem: { type: 'string', notNull: true }, 22 | person_id: { 23 | type: 'int', 24 | notNull: true, 25 | primaryKey: true, 26 | foreignKey: { 27 | name: 'person_id', 28 | table: 'persons', 29 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 30 | mapping: 'id', 31 | } 32 | }, 33 | }, callback); 34 | 35 | exports._meta = { version: 1 }; 36 | -------------------------------------------------------------------------------- /migrations/20180407000000-insert_local_account.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE FUNCTION insert_local_account(username TEXT, admin BOOLEAN, private_key_pem TEXT, salt BYTEA, server_key BYTEA, stored_key BYTEA) 18 | RETURNS INTEGER AS $$ 19 | DECLARE person_id INTEGER; 20 | BEGIN 21 | INSERT INTO persons (username, host) VALUES ($1, '') RETURNING id INTO person_id; 22 | INSERT INTO local_accounts (person_id, admin, private_key_pem, salt, server_key, stored_key) VALUES (person_id, $2, $3, $4, $5, $6); 23 | RETURN person_id; 24 | END 25 | $$ LANGUAGE plpgsql`, callback); 26 | -------------------------------------------------------------------------------- /migrations/20180408000000-insert_remote_account.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE FUNCTION insert_remote_account(username TEXT, host TEXT, uri TEXT, inbox_uri TEXT, key_uri TEXT, public_key_pem TEXT) 18 | RETURNS INTEGER AS $$ 19 | DECLARE person_id INTEGER; 20 | BEGIN 21 | INSERT INTO persons (username, host) VALUES ($1, $2) RETURNING id INTO person_id; 22 | INSERT INTO remote_accounts(person_id, uri, inbox_uri, key_uri, public_key_pem) VALUES (person_id, $3, $4, $5, $6); 23 | RETURN person_id; 24 | END 25 | $$ LANGUAGE plpgsql`, callback); 26 | -------------------------------------------------------------------------------- /migrations/20180503000000-attributed_to_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.changeColumn( 18 | 'notes', 'attributed_to_id', { type: 'bigint', notNull: true }, callback); 19 | -------------------------------------------------------------------------------- /migrations/20180504000000-actor_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.changeColumn( 18 | 'follows', 'actor_id', { type: 'bigint', notNull: true }, callback); 19 | -------------------------------------------------------------------------------- /migrations/20180505000000-object_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.changeColumn( 18 | 'follows', 'object_id', { type: 'bigint', notNull: true }, callback); 19 | -------------------------------------------------------------------------------- /migrations/20180506000000-remote_account_uris.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`ALTER TABLE remote_accounts RENAME person_id TO id; 18 | INSERT INTO uris (id, uri) SELECT id, uri FROM remote_accounts; 19 | ALTER TABLE remote_accounts DROP uri; 20 | 21 | CREATE OR REPLACE FUNCTION insert_remote_account( 22 | username TEXT, 23 | host TEXT, 24 | uri TEXT, 25 | inbox_uri TEXT, 26 | key_uri TEXT, 27 | public_key_pem TEXT 28 | ) RETURNS RECORD AS $$ 29 | DECLARE id BIGINT; 30 | DECLARE inbox_uri_id BIGINT; 31 | DECLARE key_uri_id BIGINT; 32 | DECLARE result RECORD; 33 | BEGIN 34 | INSERT INTO persons (username, host) VALUES ($1, $2) 35 | RETURNING persons.id INTO id; 36 | 37 | INSERT INTO uris (id, uri) VALUES (id, $3); 38 | 39 | SELECT INTO inbox_uri_id uris.id FROM uris WHERE uris.uri = $4; 40 | IF inbox_uri_id IS NULL 41 | THEN 42 | INSERT INTO uris (uri) VALUES ($4) RETURNING uris.id INTO inbox_uri_id; 43 | END IF; 44 | 45 | SELECT INTO key_uri_id uris.id FROM uris WHERE uris.uri = $5; 46 | IF key_uri_id IS NULL 47 | THEN 48 | INSERT INTO uris (uri) VALUES ($5) RETURNING uris.id INTO key_uri_id; 49 | END IF; 50 | 51 | INSERT INTO remote_accounts ( 52 | id, 53 | inbox_uri_id, 54 | key_uri_id, 55 | public_key_pem 56 | ) VALUES (id, inbox_uri_id, key_uri_id, $6); 57 | 58 | result := (id, inbox_uri_id, key_uri_id); 59 | RETURN result; 60 | END 61 | $$ LANGUAGE plpgsql; 62 | `, callback); 63 | -------------------------------------------------------------------------------- /migrations/20180507000000-local_accounts_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`ALTER TABLE local_accounts RENAME person_id TO id; 18 | 19 | CREATE OR REPLACE FUNCTION insert_local_account( 20 | username TEXT, 21 | admin BOOLEAN, 22 | private_key_pem TEXT, 23 | salt BYTEA, 24 | server_key BYTEA, 25 | stored_key BYTEA 26 | ) RETURNS BIGINT AS $$ 27 | DECLARE id BIGINT; 28 | BEGIN 29 | INSERT INTO persons (username, host) VALUES ($1, '') 30 | RETURNING persons.id INTO id; 31 | 32 | INSERT INTO local_accounts 33 | (id, admin, private_key_pem, salt, server_key, stored_key) 34 | VALUES (id, $2, $3, $4, $5, $6); 35 | 36 | RETURN id; 37 | END 38 | $$ LANGUAGE plpgsql; 39 | `, callback); 40 | -------------------------------------------------------------------------------- /migrations/20180508000000-notes_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => 18 | db.runSql('ALTER TABLE notes ALTER id TYPE BIGINT', callback); 19 | -------------------------------------------------------------------------------- /migrations/20180509000000-mentions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('mentions', { 18 | note_id: { 19 | type: 'bigint', 20 | notNull: true, 21 | foreignKey: { 22 | name: 'note_id', 23 | table: 'notes', 24 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 25 | mapping: 'id', 26 | } 27 | }, 28 | href_id: { 29 | type: 'bigint', 30 | notNull: true, 31 | foreignKey: { 32 | name: 'href_id', 33 | table: 'persons', 34 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 35 | mapping: 'id', 36 | } 37 | } 38 | }, callback); 39 | -------------------------------------------------------------------------------- /migrations/20180510000000-insert_note.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE FUNCTION insert_note( 18 | uri VARCHAR, 19 | attributed_to_id BIGINT, 20 | content TEXT, 21 | mentions BIGINT[] 22 | ) RETURNS BIGINT AS $$ 23 | DECLARE id BIGINT; 24 | BEGIN 25 | INSERT INTO notes (attributed_to_id, content) VALUES ($2, $3) 26 | RETURNING notes.id INTO id; 27 | 28 | IF uri IS NOT NULL THEN 29 | INSERT INTO uris (id, uri) VALUES (id, $1); 30 | END IF; 31 | 32 | INSERT INTO mentions (note_id, href_id) 33 | SELECT id, unnest FROM unnest(mentions); 34 | 35 | RETURN id; 36 | END 37 | $$ LANGUAGE plpgsql; 38 | 39 | DROP FUNCTION insert_note_with_uri(VARCHAR, BIGINT, TEXT); 40 | `, callback); 41 | -------------------------------------------------------------------------------- /migrations/20180601000000-statuses.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE TABLE statuses ( 18 | id BIGINT PRIMARY KEY DEFAULT nextval('id_seq'), 19 | person_id BIGINT REFERENCES persons (id) ON DELETE CASCADE ON UPDATE CASCADE 20 | ); 21 | 22 | CREATE TABLE announces ( 23 | id BIGINT PRIMARY KEY REFERENCES statuses (id) 24 | ON DELETE CASCADE ON UPDATE CASCADE, 25 | object_id BIGINT REFERENCES notes (id) 26 | ON DELETE RESTRICT ON UPDATE CASCADE 27 | ); 28 | 29 | CREATE FUNCTION delete_status( 30 | id BIGINT, 31 | attributed_to_id BIGINT 32 | ) RETURNS VOID AS $$ 33 | BEGIN 34 | IF EXISTS (SELECT TRUE FROM statuses WHERE statuses.id = $1 AND attributed_to_id = $2) THEN 35 | DELETE FROM statuses USING announces 36 | WHERE statuses.id = announces.id AND announces.object_id = $1; 37 | DELETE FROM statuses WHERE statuses.id = $1; 38 | END IF; 39 | END 40 | $$ LANGUAGE plpgsql; 41 | 42 | CREATE FUNCTION insert_announce( 43 | uri VARCHAR, 44 | actor_id BIGINT, 45 | object_id BIGINT 46 | ) RETURNS BIGINT AS $$ 47 | DECLARE id BIGINT; 48 | BEGIN 49 | INSERT INTO statuses (person_id) VALUES ($2) RETURNING statuses.id INTO id; 50 | INSERT INTO announces (id, object_id) VALUES (id, $3); 51 | 52 | IF uri IS NOT NULL THEN 53 | INSERT INTO uris (id, uri) VALUES (id, $1); 54 | END IF; 55 | 56 | RETURN id; 57 | END 58 | $$ LANGUAGE plpgsql; 59 | 60 | CREATE OR REPLACE FUNCTION insert_note( 61 | uri VARCHAR, 62 | attributed_to_id BIGINT, 63 | content TEXT, 64 | mentions BIGINT[] 65 | ) RETURNS BIGINT AS $$ 66 | DECLARE id BIGINT; 67 | BEGIN 68 | INSERT INTO statuses (person_id) VALUES ($2) RETURNING statuses.id INTO id; 69 | INSERT INTO notes (id, content) VALUES (id, $3); 70 | 71 | IF uri IS NOT NULL THEN 72 | INSERT INTO uris (id, uri) VALUES (id, $1); 73 | END IF; 74 | 75 | INSERT INTO mentions (note_id, href_id) 76 | SELECT id, unnest FROM unnest(mentions); 77 | 78 | RETURN id; 79 | END 80 | $$ LANGUAGE plpgsql; 81 | 82 | INSERT INTO statuses (id, person_id) SELECT id, attributed_to_id FROM notes; 83 | ALTER TABLE notes ADD CONSTRAINT id FOREIGN KEY (id) REFERENCES statuses (id) 84 | ON DELETE CASCADE ON UPDATE CASCADE; 85 | ALTER TABLE notes DROP COLUMN attributed_to_id; 86 | `, callback); 87 | -------------------------------------------------------------------------------- /migrations/20180602000000-tombstones.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.createTable('tombstones', { 18 | id: { 19 | type: 'bigint', 20 | primaryKey: true, 21 | foreignKey: { 22 | name: 'id', 23 | table: 'uris', 24 | rules: { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 25 | mapping: 'id', 26 | } 27 | } 28 | }, callback); 29 | 30 | exports._meta = { version: 1 }; 31 | -------------------------------------------------------------------------------- /migrations/20180603000000-delete_status.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE OR REPLACE FUNCTION delete_status( 18 | id BIGINT, 19 | attributed_to_id BIGINT 20 | ) RETURNS VOID AS $$ 21 | DECLARE announce_id BIGINT; 22 | BEGIN 23 | IF EXISTS (SELECT TRUE FROM statuses WHERE statuses.id = $1 AND attributed_to_id = $2) THEN 24 | INSERT INTO tombstones (id) VALUES ($1); 25 | 26 | FOR announce_id IN SELECT announces.id FROM announces WHERE object_id = $1 LOOP 27 | INSERT INTO tombstones (id) VALUES (announce_id); 28 | DELETE FROM statuses WHERE statuses.id = announce_id; 29 | END LOOP; 30 | 31 | DELETE FROM statuses WHERE statuses.id = $1; 32 | END IF; 33 | END 34 | $$ LANGUAGE plpgsql; 35 | `, callback); 36 | 37 | exports._meta = { version: 1 }; 38 | -------------------------------------------------------------------------------- /migrations/20180604000000-cookies_change_person_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.changeColumn( 18 | 'cookies', 'person_id', { type: 'bigint', notNull: true }, callback); 19 | 20 | exports._meta = { version: 1 }; 21 | -------------------------------------------------------------------------------- /migrations/20180605000000-cookies_add_account_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.addForeignKey( 18 | 'cookies', 19 | 'local_accounts', 20 | 'account_id', 21 | { person_id: 'id' }, 22 | { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, 23 | callback); 24 | 25 | exports._meta = { version: 1 }; 26 | -------------------------------------------------------------------------------- /migrations/20180606000000-cookies_remove_person_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => 18 | db.removeForeignKey('cookies', 'person_id', callback); 19 | 20 | exports._meta = { version: 1 }; 21 | -------------------------------------------------------------------------------- /migrations/20180607000000-cookies_rename_person_id_to_account_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => 18 | db.renameColumn('cookies', 'person_id', 'account_id', callback); 19 | 20 | exports._meta = { version: 1 }; 21 | -------------------------------------------------------------------------------- /migrations/20180608000000-likes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`CREATE TABLE likes ( 18 | id BIGSERIAL PRIMARY KEY, 19 | actor_id BIGINT REFERENCES persons (id) ON DELETE CASCADE ON UPDATE CASCADE, 20 | object_id BIGINT REFERENCES notes (id) ON DELETE CASCADE ON UPDATE CASCADE, 21 | UNIQUE (actor_id, object_id) 22 | )`, callback); 23 | 24 | exports._meta = { version: 1 }; 25 | -------------------------------------------------------------------------------- /migrations/20180609000000-notes_summary.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(`ALTER TABLE notes ADD summary TEXT; 18 | UPDATE notes SET summary = ''; 19 | ALTER TABLE notes ALTER summary SET NOT NULL; 20 | 21 | DROP FUNCTION insert_note(VARCHAR, BIGINT, TEXT, BIGINT[]); 22 | CREATE OR REPLACE FUNCTION insert_note( 23 | uri VARCHAR, 24 | attributed_to_id BIGINT, 25 | summary TEXT, 26 | content TEXT, 27 | mentions BIGINT[] 28 | ) RETURNS BIGINT AS $$ 29 | DECLARE id BIGINT; 30 | BEGIN 31 | INSERT INTO statuses (person_id) VALUES ($2) RETURNING statuses.id INTO id; 32 | INSERT INTO notes (id, summary, content) VALUES (id, $3, $4); 33 | 34 | IF $1 IS NOT NULL THEN 35 | INSERT INTO uris (id, uri) VALUES (id, $1); 36 | END IF; 37 | 38 | INSERT INTO mentions (note_id, href_id) SELECT id, unnest FROM unnest($5); 39 | 40 | RETURN id; 41 | END 42 | $$ LANGUAGE plpgsql; 43 | `, callback); 44 | 45 | exports._meta = { version: 1 }; 46 | -------------------------------------------------------------------------------- /migrations/20180640000000-announces_object_id.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.changeColumn( 18 | 'announces', 'object_id', { type: 'bigint', notNull: true }, callback); 19 | 20 | exports._meta = { version: 1 }; 21 | -------------------------------------------------------------------------------- /migrations/20190606000000-dirty_documents.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(` 18 | CREATE TABLE dirty_documents ( 19 | id serial PRIMARY KEY, 20 | uuid uuid NOT NULL, 21 | format varchar NOT NULL); 22 | 23 | DROP FUNCTION insert_document_with_url(uuid, varchar, varchar); 24 | 25 | CREATE FUNCTION insert_document_with_url(dirty_id integer, url varchar) 26 | RETURNS bigint LANGUAGE plpgsql AS $_$ 27 | DECLARE uri_id bigint; 28 | BEGIN 29 | INSERT INTO uris (uri, allocated) VALUES ($2, TRUE) 30 | ON CONFLICT (uri) DO UPDATE SET allocated = TRUE WHERE NOT uris.allocated 31 | RETURNING uris.id INTO uri_id; 32 | 33 | INSERT INTO documents (id, uuid, format) SELECT uri_id, uuid, format 34 | FROM dirty_documents WHERE dirty_documents.id = $1; 35 | 36 | DELETE FROM dirty_documents WHERE id = $1; 37 | 38 | RETURN uri_id; 39 | END 40 | $_$;`, callback); 41 | 42 | exports._meta = { version: 1 }; 43 | -------------------------------------------------------------------------------- /migrations/20190606000000-insert_document_without_url.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | exports.up = (db, callback) => db.runSql(` 18 | CREATE FUNCTION insert_document_without_url(dirty_id integer) 19 | RETURNS bigint LANGUAGE plpgsql AS $_$ 20 | DECLARE id bigint; 21 | BEGIN 22 | INSERT INTO documents (uuid, format) SELECT uuid, format 23 | FROM dirty_documents WHERE dirty_documents.id = $1 24 | RETURNING documents.id INTO id; 25 | 26 | DELETE FROM dirty_documents WHERE dirty_documents.id = $1; 27 | 28 | RETURN id; 29 | END 30 | $_$;`, callback); 31 | 32 | exports._meta = { version: 1 }; 33 | -------------------------------------------------------------------------------- /mini-migrate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Copyright (C) 2018 Miniverse authors 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as published by 8 | the Free Software Foundation, version 3 of the License. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | const { getInstance } = require('db-migrate'); 20 | 21 | if (process.argv.indexOf('--verbose') >= 0 || process.argv.indexOf('-v') >= 0) { 22 | global.verbose = true; 23 | } 24 | 25 | process.title = 'db-migrate'; 26 | const dbmigrate = getInstance(false, { cwd: __dirname, noPlugins: true }); 27 | dbmigrate.registerAPIHook().then(() => dbmigrate.run()); 28 | -------------------------------------------------------------------------------- /mini-serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Copyright (C) 2018 Miniverse authors 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as published by 8 | the Free Software Foundation, version 3 of the License. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | process.chdir(__dirname); 20 | require('./__sapper__/build'); 21 | -------------------------------------------------------------------------------- /node-session.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.tsconfig", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ES2017", 6 | "types": [] 7 | }, 8 | "include": [ 9 | "src/lib/session", 10 | "src/svelte.d.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /node.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.tsconfig", 3 | "compilerOptions": { 4 | "lib": ["es2017"], 5 | "module": "CommonJS", 6 | "target": "ES2017", 7 | "types": [] 8 | }, 9 | "include": [ 10 | "migrations", 11 | "src/bull-arena.d.ts", 12 | "src/http-signature.d.ts", 13 | "src/processor.ts", 14 | "src/routes", 15 | "src/sapper.d.ts", 16 | "src/server.ts", 17 | "src/svelte.d.ts", 18 | "webpack.config.js" 19 | ], 20 | "exclude": ["**/*.test.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /package.d.ts: -------------------------------------------------------------------------------- 1 | export const dependencies: { [package: string]: string }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniverse", 3 | "version": "0.0.1", 4 | "description": "Miniverse is a microblogging software.", 5 | "bin": { 6 | "mini-migrate": "mini-migrate.js", 7 | "mini-process": "__sapper__/build/server/processor.js", 8 | "mini-serve": "mini-serve.js" 9 | }, 10 | "scripts": { 11 | "dev": "sapper dev", 12 | "build": "sapper build", 13 | "prepare": "sapper build", 14 | "start": "node __sapper__/build", 15 | "test": "jest" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/miniorg/miniverse.git" 20 | }, 21 | "license": "AGPL-3.0", 22 | "bugs": { 23 | "url": "https://github.com/miniorg/miniverse/issues" 24 | }, 25 | "homepage": "https://github.com/miniorg/miniverse#readme", 26 | "dependencies": { 27 | "@hapi/accept": "^3.2.2", 28 | "abort-controller": "^3.0.0", 29 | "aws-sdk": "^2.485.0", 30 | "body-parser": "^1.19.0", 31 | "bull": "^3.10.0", 32 | "bull-arena": "^2.6.3", 33 | "busboy": "^0.3.1", 34 | "cookie": "^0.4.0", 35 | "db-migrate": "^0.11.6", 36 | "db-migrate-pg": "^1.0.0", 37 | "http-signature": "^1.2.0", 38 | "ioredis": "^4.10.0", 39 | "node-fetch": "https://github.com/miniorg/node-fetch/releases/download/v2.3.0-miniverse-r0/node-fetch-2.3.0.tgz", 40 | "pg": "^7.11.0", 41 | "sanitize-html": "^1.20.1", 42 | "sapper": "^0.27.4", 43 | "sharp": "^0.22.1", 44 | "svelte": "^3.6.1", 45 | "type-is": "^1.6.18" 46 | }, 47 | "devDependencies": { 48 | "@jetbrains/ring-ui-license-checker": "^1.5.2", 49 | "@types/bull": "^3.5.15", 50 | "@types/busboy": "^0.2.3", 51 | "@types/cookie": "^0.3.3", 52 | "@types/express": "^4.17.0", 53 | "@types/hapi__accept": "^3.2.0", 54 | "@types/ioredis": "^4.0.12", 55 | "@types/jest": "^24.0.15", 56 | "@types/nock": "^10.0.3", 57 | "@types/node": "https://github.com/miniorg/DefinitelyTyped/releases/download/miniverse-r0/node.tgz", 58 | "@types/node-fetch": "https://github.com/miniorg/DefinitelyTyped/releases/download/miniverse-r0/node-fetch.tgz", 59 | "@types/pg": "^7.4.14", 60 | "@types/sanitize-html": "^1.20.0", 61 | "@types/sharp": "^0.22.2", 62 | "@types/type-is": "^1.6.2", 63 | "@types/webpack-env": "^1.13.9", 64 | "@typescript-eslint/eslint-plugin": "^1.11.0", 65 | "@typescript-eslint/parser": "^1.11.0", 66 | "babel-core": "^6.26.3", 67 | "babel-jest": "^24.8.0", 68 | "eslint": "^5.16.0", 69 | "express": "^4.17.1", 70 | "jest": "^24.8.0", 71 | "nock": "^10.0.6", 72 | "svelte-loader": "^2.13.4", 73 | "ts-jest": "^24.0.2", 74 | "ts-loader": "^6.0.4", 75 | "typescript": "^3.5.2", 76 | "webpack": "^4.35.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/bull-arena.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bull-arena' { 2 | import { RequestHandler } from 'express'; 3 | import { RedisOptions } from 'ioredis'; 4 | 5 | function Arena( 6 | config: { 7 | queues: ({ 8 | host: string; 9 | port?: string; 10 | password?: string; 11 | db?: number; 12 | } | { 13 | url?: string; 14 | } | { 15 | redis?: RedisOptions; 16 | } & { 17 | name: string; 18 | hostId: string; 19 | type?: null | string; 20 | prefix?: string; 21 | })[]; 22 | }, 23 | listenOpts: { 24 | port?: number; 25 | host?: string; 26 | basePath?: string; 27 | disableListen?: boolean; 28 | useCdn?: boolean; 29 | }): RequestHandler; 30 | 31 | export = Arena; 32 | } 33 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import start from './subsystems/client'; 18 | 19 | const root = document.getElementById('root'); 20 | 21 | if (root) { 22 | start(root); 23 | } else { 24 | // eslint-disable-next-line no-console 25 | console.error('failed to get the root element'); 26 | } 27 | 28 | if (module.hot) { 29 | module.hot.accept(); 30 | } 31 | -------------------------------------------------------------------------------- /src/http-signature.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'http-signature' { 2 | import { ClientRequest, IncomingMessage } from 'http'; 3 | 4 | export function parseRequest(request: IncomingMessage): {}; 5 | 6 | export function sign(request: ClientRequest, options: { 7 | readonly authorizationHeaderName?: 'Signature'; 8 | readonly key: string; 9 | readonly keyId: string; 10 | }): boolean; 11 | 12 | export function verifySignature(parsedSignature: {}, pubkey: string): boolean; 13 | } 14 | 15 | declare module 'http-signature/lib/utils' { 16 | export class HttpSignatureError extends Error { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/generate_uuid.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { randomBytes } from 'crypto'; 18 | import { promisify } from 'util'; 19 | 20 | const promisifiedRandomBytes = promisify(randomBytes); 21 | 22 | export default async function() { 23 | const timestamp = (Date.now() + 12219292800000) * 10000; 24 | const timestampQuotient = timestamp / 0x100000000; 25 | const timestampRemainder = timestamp % 0x100000000; 26 | const random = await promisifiedRandomBytes(8); 27 | 28 | random[0] = (random[0] & 0x3f) | 0x80; 29 | random[2] |= 1; 30 | 31 | const timeLow = Math.floor(timestampRemainder); 32 | const timeMid = timestampQuotient & 0xffff; 33 | const timeHighAndVersion = (timestampQuotient >> 16) | 0x1000; 34 | const clockSeq = random.slice(0, 2); 35 | const node = random.slice(2); 36 | 37 | return [ 38 | ('0000000' + timeLow.toString(16)).slice(-8), 39 | ('000' + timeMid.toString(16)).slice(-4), 40 | timeHighAndVersion.toString(16), 41 | clockSeq.toString('hex'), 42 | node.toString('hex') 43 | ].join('-'); 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/generated_activitystreams.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | export interface Accept { 18 | type: string; 19 | object: Follow; 20 | } 21 | 22 | export interface Announce { 23 | type: string; 24 | id?: string; 25 | published: Date; 26 | object: string; 27 | } 28 | 29 | export type Any = Accept | Actor | Announce | Create | 30 | Follow | Key | LocalActor | Note | 31 | OrderedCollection | OrderedCollectionPage; 32 | 33 | export interface Actor { 34 | id: string; 35 | preferredUsername: string; 36 | name: string; 37 | summary: string; 38 | inbox: string; 39 | outbox: string; 40 | } 41 | 42 | export interface Create { 43 | type: string; 44 | id?: string; 45 | object: Document | Note; 46 | } 47 | 48 | export interface Document { 49 | type: string; 50 | mediaType: string; 51 | url: string; 52 | } 53 | 54 | export interface Endpoints { 55 | proxyUrl: string; 56 | uploadMedia: string; 57 | } 58 | 59 | export interface Follow { 60 | type: string; 61 | actor: string; 62 | object: string; 63 | } 64 | 65 | export interface Hashtag { 66 | type: string; 67 | name: string; 68 | } 69 | 70 | export interface Key { 71 | id: string; 72 | type: string; 73 | owner: string; 74 | publicKeyPem: string; 75 | } 76 | 77 | export interface Like { 78 | type: string; 79 | object: string; 80 | } 81 | 82 | export interface LocalActor extends Actor { 83 | type: string; 84 | endpoints: Endpoints; 85 | publicKey: Key; 86 | 'miniverse:salt': string; 87 | } 88 | 89 | export interface Mention { 90 | type: string; 91 | href: string; 92 | } 93 | 94 | export interface Note { 95 | type: string; 96 | id: string; 97 | published: Date; 98 | attributedTo: string; 99 | inReplyTo: string | null; 100 | to: string; 101 | summary: string | null; 102 | content: string; 103 | attachment: Document[]; 104 | tag: (Hashtag | Mention)[]; 105 | } 106 | 107 | export interface OrderedCollection { 108 | type: string; 109 | orderedItems: Any[]; 110 | } 111 | 112 | export interface OrderedCollectionPage { 113 | type: string; 114 | orderedItems: Any[]; 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/generated_webfinger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | export interface Account { 18 | subject: string; 19 | links: [ 20 | { 21 | rel: 'self'; 22 | type: 'application/activity+json'; 23 | href: string; 24 | } 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/isomorphism/browser/session/event_source.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { readable } from 'svelte/store'; 18 | 19 | export function listenEventSource() { 20 | const eventSource = new EventSource(`https://${location.host}/api/events`); 21 | let inbox: unknown[] = []; 22 | 23 | return readable(inbox, set => { 24 | eventSource.onmessage = ({ data }) => { 25 | const { type, orderedItems } = JSON.parse(data); 26 | 27 | if (type == 'OrderedCollectionPage') { 28 | inbox = orderedItems.reverse().concat(inbox); 29 | set(inbox); 30 | } 31 | }; 32 | 33 | return () => eventSource.close(); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/isomorphism/node/session/event_source.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { readable } from 'svelte/store'; 18 | 19 | export function listenEventSource() { 20 | return readable([], () => () => {}); 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/repository/announces.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Announce, { Seed } from '../tuples/announce'; 19 | import Status from '../tuples/status'; 20 | import URI from '../tuples/uri'; 21 | import Repository, { conflict } from '.'; 22 | 23 | export default class { 24 | async insertAnnounce( 25 | this: Repository, 26 | { status, object }: Seed, 27 | signal: AbortSignal, 28 | recover: (error: Error & { name?: string; [conflict]?: boolean }) => unknown 29 | ) { 30 | const { rows: [{ insert_announce }]} = await this.pg.query({ 31 | name: 'insertAnnounce', 32 | text: 'SELECT insert_announce($1, $2, $3, $4)', 33 | values: [ 34 | status.published, 35 | status.uri, 36 | status.actor.id, 37 | object.id 38 | ] 39 | }, signal, error => { 40 | if (error.name == 'AbortError') { 41 | return recover(error); 42 | } 43 | 44 | if (error.code == '23502') { 45 | return recover(Object.assign( 46 | new Error('uri conflicts.'), 47 | { [conflict]: true })); 48 | } 49 | 50 | return error; 51 | }); 52 | 53 | return new Announce({ 54 | repository: this, 55 | status: new Status({ 56 | repository: this, 57 | id: insert_announce, 58 | published: status.published, 59 | actor: status.actor, 60 | uri: status.uri == null ? null : new URI({ 61 | repository: this, 62 | id: insert_announce, 63 | uri: status.uri, 64 | allocated: true 65 | }) 66 | }), 67 | object 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/repository/challenges.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import repository from '../test/repository'; 18 | 19 | test('inserts challenge and allows to query with digest', async () => { 20 | await repository.insertChallenge(Buffer.from('digest')); 21 | 22 | const digest = Buffer.from('digest'); 23 | await expect(repository.selectChallengeByDigest(digest)) 24 | .resolves 25 | .toHaveProperty('digest', digest); 26 | }); 27 | 28 | test('does not return a challenge if not present', () => 29 | expect(repository.selectChallengeByDigest(Buffer.from('digest'))) 30 | .resolves 31 | .toBe(null)); 32 | -------------------------------------------------------------------------------- /src/lib/repository/challenges.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Challenge from '../tuples/challenge'; 18 | import Repository from '.'; 19 | 20 | function createKey(this: Repository, digest: Buffer) { 21 | const repositoryPrefixLength = Buffer.byteLength(this.redis.prefix); 22 | const challengesPrefixLength = Buffer.byteLength('challenges:'); 23 | const buffer = Buffer.allocUnsafe( 24 | repositoryPrefixLength + challengesPrefixLength + digest.byteLength); 25 | 26 | buffer.write(this.redis.prefix); 27 | buffer.write('challenges:', repositoryPrefixLength); 28 | digest.copy(buffer, repositoryPrefixLength + challengesPrefixLength); 29 | 30 | return buffer; 31 | } 32 | 33 | export default class { 34 | async insertChallenge(this: Repository, digest: Buffer) { 35 | const key = createKey.call(this, digest); 36 | await this.redis.client.setex(key, 1048576, ''); 37 | return new Challenge({ digest }); 38 | } 39 | 40 | async selectChallengeByDigest(this: Repository, digest: Buffer) { 41 | if (await this.redis.client.get(createKey.call(this, digest)) == null) { 42 | return null; 43 | } 44 | 45 | return new Challenge({ digest }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/repository/cookies.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Cookie from '../tuples/cookie'; 19 | import LocalAccount from '../tuples/local_account'; 20 | import Repository from '.'; 21 | 22 | export default class { 23 | async insertCookie(this: Repository, { account, digest }: { 24 | readonly account: LocalAccount; 25 | readonly digest: Buffer; 26 | }, signal: AbortSignal, recover: (error: Error & { name: string }) => unknown) { 27 | await this.pg.query({ 28 | name: 'insertCookie', 29 | text: 'INSERT INTO cookies (digest, account_id) VALUES ($1, $2)', 30 | values: [digest, account.id] 31 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 32 | 33 | return new Cookie({ repository: this, account, digest }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/repository/dirty_documents.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import DirtyDocument from '../tuples/dirty_document'; 19 | import Repository from '.'; 20 | 21 | export default class { 22 | async deleteDirtyDocument( 23 | this: Repository, 24 | { id }: DirtyDocument, 25 | signal: AbortSignal, 26 | recover: (error: Error & { name: string }) => unknown 27 | ) { 28 | await this.pg.query({ 29 | name: 'deleteDirtyDocument', 30 | text: 'DELETE FROM dirty_documents WHERE id = $1', 31 | values: [id] 32 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 33 | } 34 | 35 | async insertDirtyDocument( 36 | this: Repository, 37 | uuid: string, 38 | format: string, 39 | signal: AbortSignal, 40 | recover: (error: Error & { name: string }) => unknown 41 | ) { 42 | const { rows } = await this.pg.query({ 43 | name: 'insertDirtyDocument', 44 | text: 'INSERT INTO dirty_documents (uuid, format) VALUES ($1, $2) RETURNING id', 45 | values: [uuid, format] 46 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 47 | 48 | return new DirtyDocument({ repository: this, id: rows[0].id, uuid, format }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/repository/hashtags.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateNote } from '../test/fabricator'; 19 | import repository from '../test/repository'; 20 | 21 | test('inserts note and allows to query its hashtags', async () => { 22 | const { id } = await fabricateNote({ hashtags: ['名前'] }); 23 | const recover = jest.fn(); 24 | const { signal } = new AbortController; 25 | const hashtags = await repository.selectHashtagsByNoteId(id, signal, recover); 26 | expect(recover).not.toHaveBeenCalled(); 27 | expect(hashtags[0]).toHaveProperty('name', '名前'); 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/repository/hashtags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Hashtag from '../tuples/hashtag'; 19 | import Repository from '.'; 20 | 21 | export default class { 22 | async selectHashtagsByNoteId( 23 | this: Repository, 24 | noteId: string, 25 | signal: AbortSignal, 26 | recover: (error: Error & { name: string }) => unknown 27 | ) { 28 | const { rows } = await this.pg.query({ 29 | name: 'selectHashtagsByNoteId', 30 | text: 'SELECT * FROM hashtags JOIN hashtags_notes ON hashtags.id = hashtags_notes.hashtag_id WHERE hashtags_notes.note_id = $1', 31 | values: [noteId] 32 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 33 | 34 | return (rows as any[]).map(properties => new Hashtag(properties)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/repository/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Repository from '.'; 18 | import S3 = require('aws-sdk/clients/s3'); 19 | 20 | test('defaults finger host to host', async () => { 21 | const repository = new Repository({ 22 | analytics: {}, 23 | console, 24 | content: { frame: {}, image: {}, script: { sources: [] } }, 25 | host: 'إختبار', 26 | pg: { host: 'localhost', port: 5432 }, 27 | redis: {}, 28 | s3: { service: new S3, bucket: '', keyPrefix: '', urlPrefix: '' } 29 | }); 30 | 31 | try { 32 | expect(repository).toHaveProperty('fingerHost', 'إختبار'); 33 | } finally { 34 | await repository.end(); 35 | } 36 | }); 37 | 38 | test('allows to override finger host', async () => { 39 | const repository = new Repository({ 40 | analytics: {}, 41 | console, 42 | content: { frame: {}, image: {}, script: { sources: [] } }, 43 | fingerHost: 'إختبار', 44 | host: '', 45 | pg: { host: 'localhost', port: 5432 }, 46 | redis: {}, 47 | s3: { service: new S3, bucket: '', keyPrefix: '', urlPrefix: '' } 48 | }); 49 | 50 | try { 51 | expect(repository).toHaveProperty('fingerHost', 'إختبار'); 52 | } finally { 53 | await repository.end(); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/lib/repository/likes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Actor from '../tuples/actor'; 19 | import Like, { Seed } from '../tuples/like'; 20 | import Note from '../tuples/note'; 21 | import Repository from '.'; 22 | 23 | export default class { 24 | async deleteLikeByActorAndObject( 25 | this: Repository, 26 | actor: Actor, 27 | object: Note, 28 | signal: AbortSignal, 29 | recover: (error: Error & { name: string }) => unknown 30 | ) { 31 | await this.pg.query({ 32 | name: 'deleteLikeByActorAndObject', 33 | text: 'DELETE FROM likes WHERE actor_id = $1 AND object_id = $2', 34 | values: [actor.id, object.id] 35 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 36 | } 37 | 38 | async insertLike( 39 | this: Repository, 40 | { actor, object }: Seed, 41 | signal: AbortSignal, 42 | recover: (error: Error & { name?: string }) => unknown 43 | ) { 44 | const { rows: [{ id }] } = await this.pg.query({ 45 | name: 'insertLike', 46 | text: 'INSERT INTO likes (actor_id, object_id) VALUES ($1, $2) RETURNING id', 47 | values: [actor.id, object.id] 48 | }, signal, error => { 49 | if (error.name == 'AbortError') { 50 | return recover(error); 51 | } 52 | 53 | if (error.code == '23505') { 54 | return recover(new Error('Already liked.')); 55 | } 56 | 57 | return error; 58 | }); 59 | 60 | return new Like({ repository: this, id, actor, object }); 61 | } 62 | 63 | async selectLikeById( 64 | this: Repository, 65 | id: string, 66 | signal: AbortSignal, 67 | recover: (error: Error & { name: string }) => unknown 68 | ): Promise { 69 | const { rows } = await this.pg.query({ 70 | name: 'selectLikeById', 71 | text: 'SELECT * FROM likes WHERE id = $1', 72 | values: [id] 73 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 74 | 75 | return rows[0] ? new Like({ 76 | repository: this, 77 | id, 78 | actorId: rows[0].actor_id, 79 | objectId: rows[0].object_id 80 | }) : null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/repository/mentions.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateLocalAccount, fabricateNote } from '../test/fabricator'; 19 | import repository from '../test/repository'; 20 | import { unwrap } from '../test/types'; 21 | 22 | test('inserts note and allows to query its mentions', async () => { 23 | const recover = jest.fn(); 24 | const { signal } = new AbortController; 25 | const account = await fabricateLocalAccount(); 26 | const actor = unwrap(await account.select('actor', signal, recover)); 27 | const note = await fabricateNote({ mentions: [actor] }); 28 | 29 | const mentions = await repository.selectMentionsIncludingActorsByNoteId( 30 | note.id, signal, recover); 31 | 32 | expect(recover).not.toHaveBeenCalled(); 33 | expect(mentions[0]).toHaveProperty('hrefId', actor.id); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/repository/mentions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Mention from '../tuples/mention'; 19 | import Repository from '.'; 20 | 21 | export default class { 22 | async selectMentionsIncludingActorsByNoteId( 23 | this: Repository, 24 | id: string, 25 | signal: AbortSignal, 26 | recover: (error: Error & { name: string }) => unknown 27 | ): Promise { 28 | const hrefs = await this.selectActorsMentionedByNoteId(id, signal, recover); 29 | return hrefs.map(href => new Mention({ repository: this, href })); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/repository/pg.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import repository from '../test/repository'; 19 | 20 | describe('query', () => { 21 | test('aborts if AbortSignal says already aborted', () => { 22 | const controller = new AbortController; 23 | const { pg } = repository; 24 | const recovery = {}; 25 | const { signal } = controller; 26 | 27 | controller.abort(); 28 | 29 | return expect(pg.query('SELECT pg_wait(9)', signal, ({ name }) => { 30 | expect(name).toBe('AbortError'); 31 | return recovery; 32 | })).rejects.toBe(recovery); 33 | }); 34 | 35 | test('aborts query', () => { 36 | const controller = new AbortController; 37 | const { pg } = repository; 38 | const recovery = {}; 39 | const { signal } = controller; 40 | 41 | const promise = pg.query('SELECT pg_wait(9)', signal, ({ name }) => { 42 | expect(name).toBe('AbortError'); 43 | return recovery; 44 | }); 45 | 46 | controller.abort(); 47 | return expect(promise).rejects.toBe(recovery); 48 | }); 49 | 50 | test('queries', async () => { 51 | const { pg } = repository; 52 | const recover = jest.fn(); 53 | const { signal } = new AbortController; 54 | 55 | await expect(pg.query('SELECT', signal, recover)) 56 | .resolves.toHaveProperty('command', 'SELECT'); 57 | 58 | expect(recover).not.toHaveBeenCalled(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/lib/repository/subscribers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Repository from '.'; 18 | 19 | export type Listen = (channel: string, message: string) => unknown; 20 | 21 | export default class { 22 | async subscribe(this: Repository, channel: string, listen: Listen) { 23 | if (this.listeners[channel]) { 24 | this.listeners[channel].add(listen); 25 | } else { 26 | this.listeners[channel] = new Set([listen]); 27 | 28 | const subscription = this.redis.subscriber.subscribe(channel); 29 | 30 | subscription.catch(() => { 31 | if (this.listeners[channel].size <= 1) { 32 | delete this.listeners[channel]; 33 | } else { 34 | this.listeners[channel].delete(listen); 35 | } 36 | }); 37 | 38 | return subscription; 39 | } 40 | } 41 | 42 | async unsubscribe(this: Repository, channel: string, listen: Listen) { 43 | if (this.listeners[channel].size <= 1) { 44 | delete this.listeners[channel]; 45 | 46 | return this.redis.subscriber.unsubscribe(channel); 47 | } 48 | 49 | this.listeners[channel].delete(listen); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/repository/syslog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { format } from 'util'; 18 | 19 | /* 20 | RFC 5424 - The Syslog Protocol 21 | 6.2.1. PRI 22 | https://tools.ietf.org/html/rfc5424#section-6.2.1 23 | */ 24 | 25 | export default class Syslog { 26 | private readonly console: Console; 27 | 28 | constructor(console: Console) { 29 | this.console = console; 30 | } 31 | 32 | error(string: unknown, ...args: unknown[]) { 33 | this.console.error(format(string, ...args).replace(/^/gm, '<3>')); 34 | } 35 | 36 | warn(string: unknown, ...args: unknown[]) { 37 | this.console.warn(format(string, ...args).replace(/^/gm, '<4>')); 38 | } 39 | 40 | info(string: unknown, ...args: unknown[]) { 41 | this.console.info(format(string, ...args).replace(/^/gm, '<6>')); 42 | } 43 | 44 | debug(string: unknown, ...args: unknown[]) { 45 | this.console.debug(format(string, ...args).replace(/^/gm, '<7>')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/repository/unlinked_documents.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import Repository from '.'; 19 | 20 | export default class { 21 | async deleteUnlinkedDocumentsByIds( 22 | this: Repository, 23 | ids: string[], 24 | signal: AbortSignal, 25 | recover: (error: Error & { name: string }) => unknown 26 | ) { 27 | await this.pg.query({ 28 | name: 'deleteUnlinkedDocumentsByIds', 29 | text: 'DELETE FROM unlinked_documents WHERE id = ANY($1)', 30 | values: [ids] 31 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 32 | } 33 | 34 | async selectUnlinkedDocuments( 35 | this: Repository, 36 | signal: AbortSignal, 37 | recover: (error: Error & { name: string }) => unknown 38 | ) { 39 | const { rows } = await this.pg.query({ 40 | name: 'selectUnlinkedDocuments', 41 | text: 'SELECT * FROM unlinked_documents' 42 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 43 | 44 | return rows as { id: string; uuid: string; format: string }[]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/repository/uris.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateRemoteAccount } from '../test/fabricator'; 19 | import repository from '../test/repository'; 20 | import { unwrap } from '../test/types'; 21 | 22 | const { signal } = new AbortController; 23 | 24 | describe('selectURIById', () => { 25 | test('resolves with null if not found', async () => { 26 | const recover = jest.fn(); 27 | await expect(repository.selectURIById('0', signal, recover)) 28 | .resolves.toBe(null); 29 | expect(recover).not.toHaveBeenCalled(); 30 | }); 31 | }); 32 | 33 | describe('selectAllocatedURI', () => { 34 | test('resolves with null if not found', async () => { 35 | const recover = jest.fn(); 36 | await expect(repository.selectAllocatedURI('', signal, recover)) 37 | .resolves.toBe(null); 38 | expect(recover).not.toHaveBeenCalled(); 39 | }); 40 | }); 41 | 42 | test('inserts remote account and allows to query the URI of its inbox by ID', async () => { 43 | const recover = jest.fn(); 44 | 45 | const account = await fabricateRemoteAccount( 46 | { inbox: { uri: 'https://ReMoTe.إختبار/inbox' } }); 47 | 48 | const { id } = unwrap(await account.select('inboxURI', signal, recover)); 49 | 50 | await expect(repository.selectURIById(id, signal, recover)) 51 | .resolves 52 | .toHaveProperty('uri', 'https://ReMoTe.إختبار/inbox'); 53 | 54 | expect(recover).not.toHaveBeenCalled(); 55 | }); 56 | 57 | test('inserts remote account and allows to query the URI of its inbox', async () => { 58 | const recover = jest.fn(); 59 | 60 | const account = await fabricateRemoteAccount( 61 | { inbox: { uri: 'https://ReMoTe.إختبار/inbox' } }); 62 | 63 | const { id } = unwrap(await account.select('inboxURI', signal, recover)); 64 | 65 | const uri = await repository.selectAllocatedURI( 66 | 'https://ReMoTe.إختبار/inbox', signal, recover); 67 | 68 | expect(recover).not.toHaveBeenCalled(); 69 | expect(uri).toHaveProperty('id', id); 70 | expect(uri).toHaveProperty('uri', 'https://ReMoTe.إختبار/inbox'); 71 | }); 72 | -------------------------------------------------------------------------------- /src/lib/repository/uris.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import URI from '../tuples/uri'; 19 | import Repository from '.'; 20 | 21 | export default class { 22 | async selectURIById( 23 | this: Repository, 24 | id: string, 25 | signal: AbortSignal, 26 | recover: (error: Error & { name: string }) => unknown 27 | ): Promise { 28 | const { rows } = await this.pg.query({ 29 | name: 'selectURIById', 30 | text: 'SELECT * FROM uris WHERE id = $1', 31 | values: [id] 32 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 33 | 34 | return rows[0] ? new URI({ 35 | repository: this, 36 | id, 37 | uri: rows[0].uri, 38 | allocated: rows[0].allocated 39 | }) : null; 40 | } 41 | 42 | async selectAllocatedURI( 43 | this: Repository, 44 | uri: string, 45 | signal: AbortSignal, 46 | recover: (error: Error & { name: string }) => unknown 47 | ): Promise { 48 | const { rows } = await this.pg.query({ 49 | name: 'selectAllocatedURI', 50 | text: 'SELECT * FROM uris WHERE uri = $1 AND allocated', 51 | values: [uri] 52 | }, signal, error => error.name == 'AbortError' ? recover(error) : error); 53 | 54 | return rows[0] ? new URI({ 55 | repository: this, 56 | id: rows[0].id, 57 | uri, 58 | allocated: rows[0].allocated 59 | }) : null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/session/actor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { OrderedCollection } from '../generated_activitystreams'; 18 | import { Account } from '../generated_webfinger'; 19 | import { postOutbox } from './fetch'; 20 | import Session from './types'; 21 | 22 | const headers = { 23 | Accept: 'application/activity+json;q=0.9,application/ld+json;q=0.8' 24 | }; 25 | 26 | interface Actor { 27 | id: string; 28 | preferredUsername: string; 29 | name: string; 30 | summary: string; 31 | inbox: string; 32 | outbox: OrderedCollection & { readonly id: string } | string; 33 | } 34 | 35 | async function fetchActor({ endpoints, fingerHost }: Session, givenFetch: typeof fetch, acct: string) { 36 | const remote = acct.includes('@'); 37 | const encodedAcct = encodeURIComponent(remote ? 38 | acct : `${acct}@${fingerHost}`); 39 | 40 | const finger = await givenFetch( 41 | '/.well-known/webfinger?resource=acct:' + encodedAcct); 42 | 43 | if ([404, 410].includes(finger.status)) { 44 | return null; 45 | } 46 | 47 | const { links } = await finger.json() as Account; 48 | const link = links.find(({ rel }) => rel == 'self'); 49 | if (!link) { 50 | return null; 51 | } 52 | 53 | const activityStreams = await (remote ? 54 | givenFetch(endpoints.proxyUrl, 55 | { method: 'POST', headers, body: new URLSearchParams({ id: link.href }) }) : 56 | givenFetch(link.href, { headers })); 57 | 58 | return [404, 410].includes(activityStreams.status) ? null : activityStreams.json(); 59 | } 60 | 61 | export { fetchActor as fetch } 62 | 63 | export async function fetchOutbox(givenFetch: typeof fetch, actor: Actor) { 64 | const id = typeof actor.outbox == 'string' ? actor.outbox : actor.outbox.id; 65 | const fetched = await givenFetch(id, { 66 | headers: { Accept: 'application/activity+json;q=0.9,application/ld+json;q=0.8' } 67 | }); 68 | 69 | const outbox = await fetched.json(); 70 | 71 | outbox.id = id; 72 | actor.outbox = outbox; 73 | } 74 | 75 | export async function follow(session: Session, givenFetch: typeof fetch, { id }: Actor) { 76 | await postOutbox(session, givenFetch, { 77 | '@context': 'https://www.w3.org/ns/activitystreams', 78 | type: 'Follow', 79 | object: id, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/session/announce.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { postOutbox } from './fetch'; 18 | import Session from './types'; 19 | 20 | export async function announce(session: Session, givenFetch: typeof fetch, object: string) { 21 | await postOutbox(session, givenFetch, { 22 | '@context': 'https://www.w3.org/ns/activitystreams', 23 | type: 'Announce', 24 | published: new Date, 25 | to: 'https://www.w3.org/ns/activitystreams#Public', 26 | object 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/session/fetch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Session from './types'; 18 | 19 | export function postOutbox({ user }: Session, givenFetch: typeof fetch, body: unknown) { 20 | if (!user) { 21 | throw new Error('Not signed in.'); 22 | } 23 | 24 | return givenFetch(user.outbox, { 25 | mode: 'cors', 26 | method: 'POST', 27 | credentials: 'same-origin', 28 | headers: { 'Content-Type': 'application/activity+json' }, 29 | body: JSON.stringify(body) 30 | }); 31 | } 32 | 33 | export function uploadMedia({ user }: Session, givenFetch: typeof fetch, body: FormData) { 34 | if (!user) { 35 | throw new Error('Not signed in.'); 36 | } 37 | 38 | return givenFetch(user.endpoints.uploadMedia, { 39 | mode: 'cors', 40 | method: 'POST', 41 | credentials: 'same-origin', 42 | body 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/session/like.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { postOutbox } from './fetch'; 18 | import Session from './types'; 19 | 20 | export async function like(session: Session, givenFetch: typeof fetch, object: string) { 21 | await postOutbox(session, givenFetch, { 22 | '@context': 'https://www.w3.org/ns/activitystreams', 23 | type: 'Like', 24 | object 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/session/note.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { postOutbox, uploadMedia } from './fetch'; 18 | import Session from './types'; 19 | 20 | export async function create(session: Session, givenFetch: typeof fetch, content: string, document?: Blob) { 21 | const attachment = []; 22 | 23 | if (document) { 24 | const data = new FormData; 25 | data.set('file', document); 26 | const { headers } = await uploadMedia(session, givenFetch, data); 27 | const location = headers.get('Location'); 28 | 29 | if (location) { 30 | attachment.push((await (await fetch(location)).json()).object); 31 | } 32 | } 33 | 34 | await postOutbox(session, givenFetch, { 35 | '@context': 'https://www.w3.org/ns/activitystreams', 36 | type: 'Note', 37 | published: new Date, 38 | to: 'https://www.w3.org/ns/activitystreams#Public', 39 | content, 40 | attachment, 41 | tag: [] 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/session/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { Announce, Endpoints, Key, Note } from '../generated_activitystreams'; 18 | 19 | export interface Analytics { 20 | readonly trackingId?: string; 21 | } 22 | 23 | export interface User { 24 | id: string; 25 | preferredUsername: string; 26 | name: string; 27 | summary: string; 28 | inbox: string | (Announce | Note)[]; 29 | outbox: string; 30 | type: string; 31 | endpoints: Endpoints; 32 | publicKey: Key; 33 | 'miniverse:salt': string; 34 | } 35 | 36 | export default interface Session { 37 | readonly analytics: Analytics; 38 | readonly endpoints: { readonly proxyUrl: string }; 39 | readonly fingerHost: string; 40 | readonly nonce: string; 41 | readonly user: User | null; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/test/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | export function unwrap(value: T | null | undefined): T { 18 | expect(value).not.toBe(null); 19 | expect(value).not.toBeUndefined(); 20 | 21 | if (value == null) { 22 | throw new Error('Unexpected null.'); 23 | } 24 | 25 | return value; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/tuples/accept.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Accept as ActivityStreams } from '../generated_activitystreams'; 19 | import Follow from './follow'; 20 | import Relation, { Reference } from './relation'; 21 | import Repository from '../repository'; 22 | 23 | interface References { 24 | object: Follow | null; 25 | } 26 | 27 | type Properties = { objectId: string } | { objectId?: string; object: Follow }; 28 | 29 | export default class Accept extends Relation { 30 | readonly object?: Reference; 31 | readonly objectId!: string; 32 | 33 | async toActivityStreams( 34 | signal: AbortSignal, 35 | recover: (error: Error & { name?: string }) => unknown 36 | ): Promise { 37 | const object = await this.select('object', signal, recover); 38 | 39 | if (!object) { 40 | throw recover(new Error('object not found.')); 41 | } 42 | 43 | return { 44 | type: 'Accept', 45 | object: await object.toActivityStreams(signal, recover) 46 | }; 47 | } 48 | 49 | static async create( 50 | repository: Repository, 51 | object: Follow, 52 | signal: AbortSignal, 53 | recover: (error: Error & { name?: string }) => unknown 54 | ) { 55 | const accept = new this({ object, repository }); 56 | const [objectActor, objectObject] = await Promise.all([ 57 | object.select('actor', signal, recover), 58 | object.select('object', signal, recover) 59 | ]); 60 | 61 | if (!objectActor) { 62 | throw recover(new Error('object\'s actor not found.')); 63 | } 64 | 65 | if (!objectObject) { 66 | throw recover(new Error('object\'s object not found.')); 67 | } 68 | 69 | if (!objectObject.host && objectActor.host) { 70 | await repository.queue.add({ type: 'accept', objectId: object.id }, { timeout: 16384 }); 71 | } 72 | 73 | return accept; 74 | } 75 | } 76 | 77 | Accept.references = { 78 | object: { 79 | query: Accept.withRepository('selectFollowIncludingActorAndObjectById'), 80 | id: 'objectId' 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/lib/tuples/actor/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Base from './base'; 18 | import FromParsedActivityStreams from './from_parsed_activitystreams'; 19 | import Resolver from './resolver'; 20 | 21 | export default class Actor extends Base {} 22 | 23 | for (const Constructor of [FromParsedActivityStreams, Resolver]) { 24 | for (const key of Object.getOwnPropertyNames(Constructor)) { 25 | if (!['length', 'name', 'prototype'].includes(key)) { 26 | (Actor as any)[key] = (Constructor as any)[key]; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/tuples/challenge.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Challenge, { digest } from './challenge'; 18 | import repository from '../test/repository'; 19 | 20 | const secret = Buffer.from('secret'); 21 | const expectedDigest = Buffer.from([ 22 | 229, 233, 250, 27, 163, 30, 205, 26, 232, 79, 117, 202, 170, 71, 79, 58, 23 | 102, 63, 5, 244 24 | ]); 25 | 26 | describe('create', () => { 27 | test('creates challenge', async () => { 28 | const challenge = await Challenge.create(repository, secret); 29 | 30 | expect(expectedDigest.equals(challenge.digest)).toBe(true); 31 | 32 | expect(repository.selectChallengeByDigest(expectedDigest)) 33 | .resolves 34 | .toBeInstanceOf(Challenge); 35 | }); 36 | }); 37 | 38 | describe('digest', () => { 39 | test('digests secret', () => 40 | expect(expectedDigest.equals(digest(secret))).toBe(true)); 41 | }); 42 | -------------------------------------------------------------------------------- /src/lib/tuples/challenge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { createHash } from 'crypto'; 18 | import Repository from '../repository'; 19 | 20 | export function digest(secret: Buffer): Buffer { 21 | const hash = createHash('sha1'); 22 | hash.update(secret); 23 | return hash.digest(); 24 | } 25 | 26 | export default class Challenge { 27 | readonly digest: Buffer; 28 | 29 | constructor({ digest }: { readonly digest: Buffer }) { 30 | this.digest = digest; 31 | } 32 | 33 | static create(repository: Repository, secret: Buffer) { 34 | return repository.insertChallenge(digest(secret)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/tuples/cookie.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateLocalAccount } from '../test/fabricator'; 19 | import repository from '../test/repository'; 20 | import Cookie, { digestToken } from './cookie'; 21 | import LocalAccount from './local_account'; 22 | 23 | const secret = Buffer.from('secret'); 24 | const expectedDigest = Buffer.from([ 25 | 229, 233, 250, 27, 163, 30, 205, 26, 232, 79, 117, 202, 170, 71, 79, 58, 26 | 102, 63, 5, 244 27 | ]); 28 | const token = 'c2VjcmV0'; 29 | 30 | describe('create', () => { 31 | test('creates cookie', async () => { 32 | const account = await fabricateLocalAccount(); 33 | const { signal } = new AbortController; 34 | const recover = jest.fn(); 35 | const cookie = await Cookie.create( 36 | repository, account, secret, signal, recover); 37 | 38 | expect(expectedDigest.equals(cookie.digest)).toBe(true); 39 | 40 | await expect(repository.selectLocalAccountByDigestOfCookie( 41 | expectedDigest, 42 | signal, 43 | recover 44 | )).resolves.toBeInstanceOf(LocalAccount); 45 | 46 | expect(recover).not.toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | describe('digestToken', () => { 51 | test('parses and digests token', () => 52 | expect(expectedDigest.equals(digestToken(token))).toBe(true)); 53 | }); 54 | -------------------------------------------------------------------------------- /src/lib/tuples/cookie.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { BinaryLike, createHash } from 'crypto'; 19 | import Repository from '../repository'; 20 | import LocalAccount from './local_account'; 21 | import Relation, { Reference } from './relation'; 22 | 23 | function digest(secret: BinaryLike) { 24 | const hash = createHash('sha1'); 25 | hash.update(secret); 26 | return hash.digest(); 27 | } 28 | 29 | interface BaseProperties { 30 | digest: Buffer; 31 | } 32 | 33 | interface AccountIdProperties { 34 | accountId: string; 35 | } 36 | 37 | interface AccountProperties { 38 | accountId?: string; 39 | account: LocalAccount; 40 | } 41 | 42 | interface References { 43 | account: LocalAccount | null; 44 | } 45 | 46 | type Properties = BaseProperties & (AccountIdProperties | AccountProperties); 47 | 48 | export function digestToken(token: string) { 49 | return digest(Buffer.from(token, 'base64')); 50 | } 51 | 52 | export function getToken(secret: Buffer) { 53 | return secret.toString('base64'); 54 | } 55 | 56 | export default class Cookie extends Relation { 57 | readonly account?: Reference; 58 | readonly accountId!: string; 59 | readonly digest!: Buffer; 60 | 61 | static create( 62 | repository: Repository, 63 | account: LocalAccount, 64 | secret: Buffer, 65 | signal: AbortSignal, 66 | recover: (error: Error & { name: string }) => unknown 67 | ) { 68 | return repository.insertCookie( 69 | { account, digest: digest(secret) }, 70 | signal, 71 | recover); 72 | } 73 | } 74 | 75 | Cookie.references = { 76 | account: { 77 | query: Cookie.withRepository('selectLocalAccountById'), 78 | id: 'accountId' 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/lib/tuples/delete.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import ParsedActivityStreams from '../parsed_activitystreams'; 18 | import Repository from '../repository'; 19 | import Actor from './actor'; 20 | 21 | export default class Delete { 22 | static createFromParsedActivityStreams( 23 | repository: Repository, 24 | activity: ParsedActivityStreams, 25 | actor: Actor): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/tuples/delete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController, AbortSignal } from 'abort-controller'; 18 | import ParsedActivityStreams from '../parsed_activitystreams'; 19 | import Repository from '../repository'; 20 | import { temporaryError } from '../transfer'; 21 | import Actor from './actor'; 22 | 23 | export const unexpectedType = Symbol(); 24 | 25 | export default class Delete { 26 | static async createFromParsedActivityStreams( 27 | repository: Repository, 28 | activity: ParsedActivityStreams, 29 | actor: Actor, 30 | signal: AbortSignal, 31 | recover: (error: Error & { 32 | [temporaryError]?: boolean; 33 | [unexpectedType]?: boolean; 34 | }) => unknown 35 | ) { 36 | const type = await activity.getType(signal, recover); 37 | 38 | if (!type || !type.has('Delete')) { 39 | throw recover(Object.assign( 40 | new Error('Unsupported type. Expected Delete.'), 41 | { [unexpectedType]: true })); 42 | } 43 | 44 | const object = await activity.getObject(signal, recover); 45 | if (!object) { 46 | throw recover(new Error('object unspecified.')); 47 | } 48 | 49 | const id = await object.getId(recover); 50 | if (typeof id != 'string') { 51 | throw recover(new Error('Unsupported id type. Expected string.')); 52 | } 53 | 54 | const uri = await repository.selectAllocatedURI(id, signal, recover); 55 | 56 | if (uri) { 57 | await repository.deleteStatusByUriAndAttributedTo( 58 | uri, actor, signal, recover); 59 | 60 | const documents = 61 | await repository.selectUnlinkedDocuments(signal, recover); 62 | const documentIds = documents.map(({ id }) => id); 63 | const controller = new AbortController; 64 | 65 | await repository.s3.service.deleteObjects({ 66 | Bucket: repository.s3.bucket, 67 | Delete: { 68 | Objects: documents.map(({ uuid, format }) => 69 | ({ Key: `${repository.s3.keyPrefix}${uuid}.${format}` })) 70 | } 71 | }).promise(); 72 | 73 | await repository.deleteUnlinkedDocumentsByIds( 74 | documentIds, controller.signal, recover); 75 | } 76 | 77 | return new this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/tuples/dirty_document.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Repository from '../repository'; 18 | 19 | interface Properties { 20 | readonly repository: Repository; 21 | readonly id: number; 22 | readonly uuid: string; 23 | readonly format: string; 24 | } 25 | 26 | export default class DirtyDocument { 27 | readonly repository: Repository; 28 | readonly id: number; 29 | readonly uuid: string; 30 | readonly format: string; 31 | 32 | constructor({ repository, id, uuid, format }: Properties) { 33 | this.repository = repository; 34 | this.id = id; 35 | this.uuid = uuid; 36 | this.format = format; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/tuples/hashtag.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import repository from '../test/repository'; 18 | import Hashtag from './hashtag'; 19 | 20 | describe('toActivityStreams', () => { 21 | test('resolves with ActivityStreams representation', () => { 22 | const hashtag = new Hashtag({ repository, name: '名前' }); 23 | 24 | return expect(hashtag.toActivityStreams()) 25 | .resolves 26 | .toEqual({ type: 'Hashtag', name: '名前' }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/tuples/hashtag.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { Hashtag as ActivityStreams } from '../generated_activitystreams'; 18 | import Repository from '../repository'; 19 | 20 | interface Properties { 21 | readonly repository: Repository; 22 | readonly name: string; 23 | } 24 | 25 | export default class Hashtag { 26 | readonly repository: Repository; 27 | readonly name: string; 28 | 29 | constructor({ repository, name }: Properties) { 30 | this.repository = repository; 31 | this.name = name; 32 | } 33 | 34 | async toActivityStreams(): Promise { 35 | return { type: 'Hashtag', name: this.name }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/tuples/mention.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateLocalAccount } from '../test/fabricator'; 19 | import repository from '../test/repository'; 20 | import { unwrap } from '../test/types'; 21 | import Mention from './mention'; 22 | 23 | describe('toActivityStreams', () => { 24 | test('resolves with ActivityStreams representation', async () => { 25 | const recover = jest.fn(); 26 | const { signal } = new AbortController; 27 | 28 | const account = 29 | await fabricateLocalAccount({ actor: { username: '行動者' } }); 30 | 31 | const mention = new Mention({ 32 | repository, 33 | href: unwrap(await account.select('actor', signal, recover)) 34 | }); 35 | 36 | await expect(mention.toActivityStreams(signal, recover)).resolves.toEqual({ 37 | type: 'Mention', 38 | href: 'https://xn--kgbechtv/@%E8%A1%8C%E5%8B%95%E8%80%85' 39 | }); 40 | 41 | expect(recover).not.toHaveBeenCalled(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/lib/tuples/mention.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Mention as ActivityStreams } from '../generated_activitystreams'; 19 | import Actor from './actor'; 20 | import Relation, { Reference } from './relation'; 21 | 22 | type Properties = { hrefId: string } | { hrefId?: string; href: Actor }; 23 | 24 | interface References { 25 | href: Actor | null; 26 | } 27 | 28 | export default class Mention extends Relation { 29 | readonly href?: Reference; 30 | readonly hrefId!: string; 31 | 32 | async toActivityStreams( 33 | signal: AbortSignal, 34 | recover: (error: Error & { name?: string }) => unknown 35 | ): Promise { 36 | const actor = await this.select('href', signal, recover); 37 | if (!actor) { 38 | throw recover(new Error('href not found.')); 39 | } 40 | 41 | const href = await actor.getUri(signal, recover); 42 | if (!href) { 43 | throw recover(new Error('href\'s uri not found.')); 44 | } 45 | 46 | return { type: 'Mention', href }; 47 | } 48 | } 49 | 50 | Mention.references = { 51 | href: { query: Mention.withRepository('selectActorById'), id: 'hrefId' } 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/tuples/ordered_collection.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import OrderedCollection from './ordered_collection'; 19 | 20 | describe('toActivityStreams', () => { 21 | const { signal } = new AbortController; 22 | 23 | test('catches an error which is not AbortError', async () => { 24 | const collection = new OrderedCollection({ 25 | orderedItems: [ 26 | { 27 | async toActivityStreams(_signal, recover) { 28 | throw recover(new Error); 29 | } 30 | } 31 | ] 32 | }); 33 | 34 | const recover = jest.fn(); 35 | 36 | await expect(collection.toActivityStreams(signal, recover)).resolves.toEqual({ 37 | type: 'OrderedCollection', 38 | orderedItems: [] 39 | }) 40 | 41 | expect(recover).not.toHaveBeenCalled(); 42 | }); 43 | 44 | test('propagates AbortError', async () => { 45 | const collection = new OrderedCollection({ 46 | orderedItems: [ 47 | { 48 | async toActivityStreams(_signal, recover) { 49 | throw recover(Object.assign(new Error, { name: 'AbortError' })); 50 | } 51 | } 52 | ] 53 | }); 54 | 55 | const recovery = {}; 56 | 57 | await expect(collection.toActivityStreams(signal, () => recovery)) 58 | .rejects.toBe(recovery); 59 | }); 60 | 61 | test('returns ActivityStreams representation', async () => { 62 | const item = { 63 | type: 'Announce', 64 | id: 'https://Id.إختبار/', 65 | published: new Date, 66 | object: 'https://ObJeCt.إختبار/' 67 | }; 68 | 69 | const collection = new OrderedCollection({ 70 | orderedItems: [ 71 | { 72 | async toActivityStreams() { 73 | return item; 74 | } 75 | } 76 | ] 77 | }); 78 | 79 | const recover = jest.fn(); 80 | const body = collection.toActivityStreams(signal, recover); 81 | 82 | await Promise.all([ 83 | expect(body).resolves.toHaveProperty('type', 'OrderedCollection'), 84 | expect(body).resolves.toHaveProperty('orderedItems', [item]) 85 | ]); 86 | 87 | expect(recover).not.toHaveBeenCalled(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/lib/tuples/ordered_collection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Any, OrderedCollection } from '../generated_activitystreams'; 19 | 20 | const recovery = {}; 21 | 22 | interface ToActivityStreams { 23 | toActivityStreams( 24 | signal: AbortSignal, 25 | recover: (error: Error) => unknown 26 | ): Promise; 27 | } 28 | 29 | export default class { 30 | readonly orderedItems: ToActivityStreams[]; 31 | 32 | constructor({ orderedItems }: { readonly orderedItems: ToActivityStreams[] }) { 33 | this.orderedItems = orderedItems; 34 | } 35 | 36 | async toActivityStreams( 37 | signal: AbortSignal, 38 | recover: (error: Error & { name: string }) => unknown 39 | ): Promise { 40 | return { 41 | type: 'OrderedCollection', 42 | orderedItems: (await Promise.all(this.orderedItems.map( 43 | item => item.toActivityStreams( 44 | signal, 45 | error => error.name == 'AbortError' ? recover(error) : recovery 46 | ).catch(error => { 47 | if (error != recovery) { 48 | throw error; 49 | } 50 | 51 | return recovery; 52 | }) 53 | ))).filter(item => item != recovery) as Any[] 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/tuples/ordered_collection_page.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import OrderedCollectionPage from './ordered_collection_page'; 19 | 20 | describe('toActivityStreams', () => { 21 | const { signal } = new AbortController; 22 | 23 | test('catches an error which is not AbortError', async () => { 24 | const collection = new OrderedCollectionPage({ 25 | orderedItems: [ 26 | { 27 | async toActivityStreams(_signal, recover) { 28 | throw recover(new Error); 29 | } 30 | } 31 | ] 32 | }); 33 | 34 | const recover = jest.fn(); 35 | 36 | await expect(collection.toActivityStreams(signal, recover)).resolves.toEqual({ 37 | type: 'OrderedCollectionPage', 38 | orderedItems: [] 39 | }) 40 | 41 | expect(recover).not.toHaveBeenCalled(); 42 | }); 43 | 44 | test('propagates AbortError', async () => { 45 | const collection = new OrderedCollectionPage({ 46 | orderedItems: [ 47 | { 48 | async toActivityStreams(_signal, recover) { 49 | throw recover(Object.assign(new Error, { name: 'AbortError' })); 50 | } 51 | } 52 | ] 53 | }); 54 | 55 | const recovery = {}; 56 | 57 | await expect(collection.toActivityStreams(signal, () => recovery)) 58 | .rejects.toBe(recovery); 59 | }); 60 | 61 | test('returns ActivityStreams representation', async () => { 62 | const item = { 63 | type: 'Announce', 64 | id: 'https://Id.إختبار/', 65 | published: new Date, 66 | object: 'https://ObJeCt.إختبار/' 67 | }; 68 | 69 | const collection = new OrderedCollectionPage({ 70 | orderedItems: [ 71 | { 72 | async toActivityStreams() { 73 | return item; 74 | } 75 | } 76 | ] 77 | }); 78 | 79 | const recover = jest.fn(); 80 | const body = collection.toActivityStreams(signal, recover); 81 | 82 | await Promise.all([ 83 | expect(body).resolves.toHaveProperty('type', 'OrderedCollectionPage'), 84 | expect(body).resolves.toHaveProperty('orderedItems', [item]) 85 | ]); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/lib/tuples/ordered_collection_page.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Any, OrderedCollectionPage } from '../generated_activitystreams'; 19 | 20 | const recovery = {}; 21 | 22 | interface ToActivityStreams { 23 | toActivityStreams( 24 | signal: AbortSignal, 25 | recover: (error: Error & { name?: string }) => unknown 26 | ): Promise; 27 | } 28 | 29 | export default class { 30 | readonly orderedItems: ToActivityStreams[]; 31 | 32 | constructor({ orderedItems }: { readonly orderedItems: ToActivityStreams[] }) { 33 | this.orderedItems = orderedItems; 34 | } 35 | 36 | async toActivityStreams( 37 | signal: AbortSignal, 38 | recover: (error: Error & { name: string }) => unknown 39 | ): Promise { 40 | return { 41 | type: 'OrderedCollectionPage', 42 | orderedItems: (await Promise.all(this.orderedItems.map( 43 | item => item.toActivityStreams( 44 | signal, 45 | error => error.name == 'AbortError' ? recover(error) : recovery 46 | ).catch(error => { 47 | if (error != recovery) { 48 | throw error; 49 | } 50 | 51 | return recovery; 52 | }) 53 | ))).filter(item => item != recovery) as Any[] 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/tuples/status.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { fabricateLocalAccount, fabricateNote } from '../test/fabricator'; 19 | import { unwrap } from '../test/types'; 20 | 21 | describe('getUri', () => { 22 | const { signal } = new AbortController; 23 | 24 | test('resolves with local URI', async () => { 25 | const recover = jest.fn(); 26 | 27 | const account = 28 | await fabricateLocalAccount({ actor: { username: 'attributed to' } }); 29 | 30 | const actor = unwrap(await account.select('actor', signal, recover)); 31 | const note = await fabricateNote({ status: { actor } }); 32 | const status = unwrap(await note.select('status', signal, recover)); 33 | 34 | await expect(status.getUri(signal, recover)).resolves.toMatch(/^https:\/\/xn--kgbechtv\/@attributed%20to\//); 35 | expect(recover).not.toHaveBeenCalled(); 36 | }); 37 | 38 | test('resolves with remote URI when it is resolved remote status', async () => { 39 | const recover = jest.fn(); 40 | 41 | const note = await fabricateNote( 42 | { status: { uri: 'https://NoTe.xn--kgbechtv/' } }); 43 | 44 | const status = unwrap(await note.select('status', signal, recover)); 45 | 46 | await expect(status.getUri(signal, recover)).resolves.toBe('https://NoTe.xn--kgbechtv/'); 47 | expect(recover).not.toHaveBeenCalled(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/lib/tuples/status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { domainToASCII } from 'url'; 19 | import Actor from './actor'; 20 | import Announce from './announce'; 21 | import Note from './note'; 22 | import Relation, { Reference } from './relation'; 23 | import URI, { encodeSegment } from './uri'; 24 | 25 | interface BaseProperties { 26 | id: string; 27 | published: Date; 28 | } 29 | 30 | interface ActorIdProperties { 31 | actorId: string; 32 | } 33 | 34 | interface ActorProperties { 35 | actorId?: string; 36 | actor: Actor; 37 | } 38 | 39 | interface References { 40 | extension: Announce | Note | null; 41 | actor: Actor | null; 42 | uri: URI | null; 43 | } 44 | 45 | type Properties = BaseProperties & (ActorIdProperties | ActorProperties); 46 | 47 | export default class Status extends Relation { 48 | readonly id!: string; 49 | readonly uri?: Reference; 50 | readonly actorId!: string; 51 | readonly actor?: Reference; 52 | readonly extension?: Reference; 53 | readonly published!: Date; 54 | 55 | async getUri( 56 | signal: AbortSignal, 57 | recover: (error: Error & { name?: string }) => unknown 58 | ) { 59 | const actor = await this.select('actor', signal, recover); 60 | if (!actor) { 61 | throw recover(new Error('actor not found.')); 62 | } 63 | 64 | if (actor.host) { 65 | const uri = await this.select('uri', signal, recover); 66 | if (!uri) { 67 | throw recover(new Error('uri not found.')); 68 | } 69 | 70 | return uri.uri; 71 | } 72 | 73 | const repositoryHost = domainToASCII(this.repository.host); 74 | const encodedUsername = encodeSegment(actor.username); 75 | 76 | return `https://${repositoryHost}/@${encodedUsername}/${this.id}`; 77 | } 78 | } 79 | 80 | Status.references = { 81 | extension: { id: 'id', inverseOf: 'status' }, 82 | actor: { id: 'actorId', query: Status.withRepository('selectActorById') }, 83 | uri: { id: 'id', query: Status.withRepository('selectURIById') } 84 | }; 85 | -------------------------------------------------------------------------------- /src/lib/tuples/uri.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { encodeSegment, encodeAcctUserpart, normalizeHost } from './uri'; 18 | 19 | /* 20 | RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax 21 | 3.3 Path 22 | https://tools.ietf.org/html/rfc3986#section-3.3 23 | */ 24 | describe('encodeSegment', () => { 25 | for (const [decoded, encoded] of [ 26 | ['//', '%2F%2F'], ['??', '%3F%3F'], ['##', '%23%23'] 27 | ]) { 28 | test(`encodes ${decoded} to ${encoded}`, () => 29 | expect(encodeSegment(decoded)).toBe(encoded)); 30 | } 31 | 32 | for (const character of 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~!$&\'()*+,;=:@') { 33 | test(`does not encode ${character}`, () => 34 | expect(encodeSegment(character)).toBe(character)); 35 | } 36 | }); 37 | 38 | /* 39 | RFC 7565 - The 'acct' URI Scheme 40 | 7. IANA Considerations 41 | https://tools.ietf.org/html/rfc7565#section-7 42 | */ 43 | describe('encodeAcctUserpart', () => { 44 | for (const [decoded, encoded] of [[':', '%3A'], ['@', '%40']]) { 45 | test(`encodes ${decoded} to ${encoded}`, () => 46 | expect(encodeAcctUserpart(decoded)).toBe(encoded)); 47 | } 48 | 49 | for (const character of 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~!$&\'()*+,;=') { 50 | test(`does not encode ${character}`, () => 51 | expect(encodeAcctUserpart(character)).toBe(character)); 52 | } 53 | }); 54 | 55 | describe('normalizeHost', () => { 56 | test('encodes in IDNA', () => 57 | expect(normalizeHost('إختبار')).toBe('xn--kgbechtv')); 58 | 59 | test('replaces uppercase ASCII characters with lowercase characters', () => 60 | expect(normalizeHost('EXAMPLE.COM')).toBe('example.com')); 61 | }); 62 | -------------------------------------------------------------------------------- /src/lib/tuples/uri.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { domainToASCII } from 'url'; 18 | import Repository from '../repository'; 19 | 20 | interface Properties { 21 | readonly repository: Repository; 22 | readonly id: string; 23 | readonly uri: string; 24 | readonly allocated: boolean; 25 | } 26 | 27 | export function encodeSegment(segment: string) { 28 | return encodeURI(segment) 29 | .replace(/\//g, '%2F') 30 | .replace(/\?/g, '%3F') 31 | .replace(/#/g, '%23'); 32 | } 33 | 34 | export function encodeAcctUserpart(userpart: string) { 35 | return encodeSegment(userpart).replace(/:/g, '%3A').replace(/@/g, '%40'); 36 | } 37 | 38 | export function normalizeHost(host: string) { 39 | return domainToASCII(host).toLowerCase(); 40 | } 41 | 42 | export default class { 43 | readonly id!: string; 44 | readonly uri!: string; 45 | readonly allocated!: boolean; 46 | 47 | constructor(properties: Properties) { 48 | Object.assign(this, properties); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import S3 = require('aws-sdk/clients/s3'); 18 | 19 | if (!process.env.AWS_S3_BUCKET) { 20 | throw new Error('AWS_S3_BUCKET environment variable not specified.'); 21 | } 22 | 23 | if (!process.env.AWS_S3_URL_PREFIX) { 24 | throw new Error('AWS_S3_URL_PREFIX environment variable not specified.'); 25 | } 26 | 27 | if (!process.env.HOST) { 28 | throw new Error('HOST environment variable not specified.'); 29 | } 30 | 31 | export default { 32 | analytics: { trackingId: process.env.ANALYTICS_TRACKING_ID }, 33 | console, 34 | content: { 35 | frame: { sourceList: process.env.CONTENT_FRAME_SOURCE_LIST }, 36 | image: { sourceList: process.env.CONTENT_IMAGE_SOURCE_LIST }, 37 | script: { 38 | sourceList: process.env.CONTENT_SCRIPT_SOURCE_LIST, 39 | sources: process.env.CONTENT_SCRIPT_SOURCES ? 40 | process.env.CONTENT_SCRIPT_SOURCES.split(';') : [] 41 | } 42 | }, 43 | fingerHost: process.env.FINGER_HOST, 44 | host: process.env.HOST, 45 | pg: { 46 | host: process.env.PGHOST || 'localhost', 47 | port: process.env.PGPORT && parseInt(process.env.PGPORT) || 5432 48 | }, 49 | redis: { 50 | prefix: process.env.REDIS_PREFIX, 51 | url: process.env.REDIS_URL 52 | }, 53 | s3: { 54 | service: new S3({ 55 | endpoint: process.env.AWS_ENDPOINT, 56 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 57 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 58 | region: process.env.AWS_REGION, 59 | signatureVersion: process.env.AWS_SIGNATURE_VERSION, 60 | s3BucketEndpoint: Boolean(process.env.AWS_S3_BUCKET_ENDPOINT), 61 | s3ForcePathStyle: Boolean(process.env.AWS_S3_FORCE_PATH_STYLE), 62 | }), 63 | bucket: process.env.AWS_S3_BUCKET, 64 | keyPrefix: process.env.AWS_S3_KEY_PREFIX || '', 65 | urlPrefix: process.env.AWS_S3_URL_PREFIX 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/processor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Repository from './lib/repository'; 18 | import options from './options'; 19 | import processJobs from './subsystems/processor'; 20 | 21 | const repository = new Repository(options); 22 | 23 | function terminate() { 24 | repository.end(); 25 | } 26 | 27 | process.on('SIGINT', terminate); 28 | process.on('SIGTERM', terminate); 29 | 30 | processJobs(repository); 31 | -------------------------------------------------------------------------------- /src/routes/.well-known/webfinger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Actor from '../../lib/tuples/actor'; 18 | import { normalizeHost } from '../../lib/tuples/uri'; 19 | import secure from '../_secure'; 20 | 21 | const recovery = {}; 22 | 23 | export const get = secure(async ({ query }, response) => { 24 | const lowerResource = query.resource; 25 | const parsed = /(?:acct:)?(.*)@(.*)/.exec(lowerResource); 26 | 27 | if (!parsed) { 28 | response.sendStatus(404); 29 | return; 30 | } 31 | 32 | const [, userpart, host] = parsed; 33 | const { repository } = response.app.locals; 34 | const { signal } = response.locals; 35 | let actor; 36 | 37 | try { 38 | if (host == normalizeHost(repository.fingerHost)) { 39 | actor = await repository.selectActorByUsernameAndNormalizedHost( 40 | decodeURI(userpart), null, signal, () => recovery); 41 | } else { 42 | actor = await Actor.fromUsernameAndNormalizedHost( 43 | repository, 44 | decodeURI(userpart), 45 | host, 46 | signal, 47 | () => recovery); 48 | } 49 | 50 | if (!actor) { 51 | response.sendStatus(404); 52 | return; 53 | } 54 | 55 | const account = await actor.select('account', signal, () => recovery); 56 | if (!account) { 57 | response.sendStatus(404); 58 | return; 59 | } 60 | 61 | response.json(await account.toWebFinger(signal, _error => recovery)); 62 | } catch (error) { 63 | if (error == recovery) { 64 | response.sendStatus(500); 65 | return;signal 66 | } 67 | 68 | throw error; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /src/routes/@[acct]/[id].ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { normalizeHost } from '../../lib/tuples/uri'; 18 | import secure from '../_secure'; 19 | import sendActivityStreams from '../_send_activitystreams'; 20 | 21 | const recovery = {}; 22 | 23 | export const get = secure(async (request, response) => { 24 | const { params } = request; 25 | const { repository } = response.app.locals; 26 | const { signal } = response.locals; 27 | const acct = decodeURIComponent(params.acct); 28 | const atIndex = acct.lastIndexOf('@'); 29 | let extension; 30 | let actor; 31 | let username; 32 | let normalizedHost; 33 | 34 | if (atIndex < 0) { 35 | username = acct; 36 | normalizedHost = null; 37 | } else { 38 | username = acct.slice(0, atIndex); 39 | normalizedHost = normalizeHost(acct.slice(atIndex + 1)); 40 | } 41 | 42 | try { 43 | const status = await repository.selectStatusIncludingExtensionById(params.id, signal, () => recovery); 44 | if (!status) { 45 | response.sendStatus(404); 46 | return; 47 | } 48 | 49 | [extension, actor] = await Promise.all([ 50 | status.select('extension', signal, () => recovery), 51 | status.select('actor', signal, () => recovery) 52 | ]); 53 | if (!extension || !actor || 54 | actor.username != username || 55 | (actor.host != normalizedHost && 56 | actor.host && 57 | normalizeHost(actor.host) != normalizedHost)) { 58 | response.sendStatus(404); 59 | return; 60 | } 61 | } catch (error) { 62 | if (error == recovery) { 63 | response.sendStatus(422); 64 | return; 65 | } 66 | 67 | throw error; 68 | } 69 | 70 | await sendActivityStreams(response, extension); 71 | }); 72 | -------------------------------------------------------------------------------- /src/routes/@[acct]/inbox.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { text } from 'body-parser'; 18 | import { Request, Response } from 'express'; 19 | import { parseRequest } from 'http-signature'; 20 | import { HttpSignatureError } from 'http-signature/lib/utils'; 21 | import { promisify } from 'util'; 22 | import secure from '../_secure'; 23 | 24 | const setBody = promisify(text({ 25 | type: ['application/activity+json', 'application/ld+json'] 26 | })) as unknown as (request: Request, response: Response) => Promise; 27 | 28 | /* 29 | ActivityPub 30 | 5.2 Inbox 31 | https://www.w3.org/TR/activitypub/#inbox 32 | > The inboxes of actors on federated servers accepts HTTP POST requests, with 33 | > behaviour described in Delivery. 34 | */ 35 | export const post = secure(async (request, response) => { 36 | let signature; 37 | 38 | request.headers.authorization = 'Signature ' + request.headers.signature; 39 | 40 | try { 41 | signature = parseRequest(request); 42 | } catch (error) { 43 | if (error instanceof HttpSignatureError) { 44 | response.sendStatus(403); 45 | return; 46 | } 47 | 48 | throw error; 49 | } 50 | 51 | await setBody(request, response); 52 | 53 | await response.app.locals.repository.queue.add({ 54 | type: 'processInbox', 55 | signature, 56 | body: request.body 57 | }, { timeout: 16384 }); 58 | 59 | response.sendStatus(202); 60 | }); 61 | -------------------------------------------------------------------------------- /src/routes/@[acct]/index.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |

{actor.preferredUsername}

18 | {#if $session.user && $session.user.id != actor.id} 19 | 20 | {/if} 21 |
    22 | {#each actor.outbox.orderedItems as item} 23 |
  1. {JSON.stringify(item)}
  2. 24 | {/each} 25 |
26 | 46 | 54 | -------------------------------------------------------------------------------- /src/routes/@[acct]/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { mediaType } from '@hapi/accept'; 18 | import Actor from '../../lib/tuples/actor'; 19 | import { normalizeHost } from '../../lib/tuples/uri'; 20 | import secure from '../_secure'; 21 | import sendActivityStreams from '../_send_activitystreams'; 22 | 23 | const recovery = {}; 24 | 25 | export const get = secure(async ({ params, headers }, response, next) => { 26 | /* 27 | ActivityPub 28 | https://www.w3.org/TR/activitypub/ 29 | > Servers MAY use HTTP content negotiation as defined in [RFC7231] to select 30 | > the type of data to return in response to a request, but MUST present the 31 | > ActivityStreams object representation in response to 32 | > application/ld+json; profile="https://www.w3.org/ns/activitystreams", 33 | > and SHOULD also present the ActivityStreams representation in response to 34 | > application/activity+json as well. 35 | */ 36 | /* 37 | Avoid accepts method of express.Response because parameters such as 38 | "profile" confuses it. 39 | */ 40 | const accepted = mediaType(headers.accept, [ 41 | 'text/html', 42 | 'application/activity+json', 43 | 'application/ld+json' 44 | ]); 45 | 46 | if (!accepted || accepted.startsWith('text/html')) { 47 | next(); 48 | return; 49 | } 50 | 51 | const acct = decodeURIComponent(params.acct); 52 | const atIndex = acct.lastIndexOf('@'); 53 | let actor; 54 | let username; 55 | let normalizedHost; 56 | 57 | if (atIndex < 0) { 58 | username = acct; 59 | normalizedHost = null; 60 | } else { 61 | username = acct.slice(0, atIndex); 62 | normalizedHost = normalizeHost(acct.slice(atIndex + 1)); 63 | } 64 | 65 | try { 66 | actor = await Actor.fromUsernameAndNormalizedHost( 67 | response.app.locals.repository, 68 | username, 69 | normalizedHost, 70 | response.locals.signal, 71 | () => recovery); 72 | } catch (error) { 73 | if (error == recovery) { 74 | response.sendStatus(422); 75 | return; 76 | } 77 | 78 | throw error; 79 | } 80 | 81 | if (actor) { 82 | await sendActivityStreams(response, actor); 83 | } else { 84 | response.sendStatus(404); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/routes/[uuid].ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2019 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Create from '../lib/tuples/create'; 18 | import secure from './_secure'; 19 | import sendActivityStreams from './_send_activitystreams'; 20 | 21 | const recovery = {}; 22 | 23 | export const get = secure(async ({ params }, response) => { 24 | const { repository } = response.app.locals; 25 | let object; 26 | 27 | try { 28 | object = await repository.selectDocumentByUUID( 29 | params.uuid, response.locals.signal, () => recovery); 30 | } catch (error) { 31 | if (error != recovery) { 32 | throw error; 33 | } 34 | 35 | response.sendStatus(500); 36 | return; 37 | } 38 | 39 | if (!object) { 40 | response.sendStatus(404); 41 | return; 42 | } 43 | 44 | await sendActivityStreams(response, new Create({ repository, object })); 45 | }); 46 | -------------------------------------------------------------------------------- /src/routes/_dashboard.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |

{$session.user.preferredUsername}

18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
    32 | {#each $inbox as item} 33 |
  1. {JSON.stringify(item)}
  2. 34 | {/each} 35 |
36 | 63 | -------------------------------------------------------------------------------- /src/routes/_error.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {status} 19 | 20 |

{status}

21 |

{error.message}

22 | {#if error.stack} 23 |
{error.stack}
24 | {/if} 25 | 29 | -------------------------------------------------------------------------------- /src/routes/_landing.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 | 60 | -------------------------------------------------------------------------------- /src/routes/_layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#each $session.scripts as script} 18 | 19 | {/each} 20 |
21 | {#if analytics.trackingId} 22 |

This site sends various data potentially private to third party. Check 23 | loaded scripts and block them if you don't want.

24 | {/if} 25 | 39 | -------------------------------------------------------------------------------- /src/routes/_secure.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { Request, NextFunction } from 'express'; 18 | import { Response } from '../subsystems/server'; 19 | 20 | interface Handler { 21 | (request: Request, response: Response, next: NextFunction): unknown; 22 | } 23 | 24 | export default (handle: Handler) => async (request: Request, response: Response, next: NextFunction) => { 25 | try { 26 | await handle(request, response, next); 27 | } catch (error) { 28 | response.app.locals.repository.console.error(error); 29 | response.sendStatus(500); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/routes/_send_activitystreams.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Response } from 'express'; 19 | 20 | const recovery = {}; 21 | 22 | export default async (response: Response, object: { 23 | toActivityStreams( 24 | signal: AbortSignal, 25 | recover: (error: Error) => unknown 26 | ): Promise<{ [key: string]: unknown }>; 27 | }) => { 28 | let message; 29 | 30 | try { 31 | message = await object.toActivityStreams(response.locals.signal, () => recovery); 32 | } catch (error) { 33 | if (error == recovery) { 34 | response.sendStatus(500); 35 | return; 36 | } 37 | 38 | throw error; 39 | } 40 | 41 | message['@context'] = [ 42 | 'https://miniverse.social/ns', 43 | 'https://w3id.org/security/v1', 44 | 'https://www.w3.org/ns/activitystreams' 45 | ]; 46 | 47 | /* 48 | Mastodon requires Content-Type to be application/activity+json: 49 | Hook up URL-based resource look-up to ActivityPub (#4589) · tootsuite/mastodon@4e75f0d 50 | https://github.com/tootsuite/mastodon/commit/4e75f0d88932511ad154773f4c77a485367ed36c#diff-328eab87b2768b5bd2b03f27e89a7dceR32 51 | Be aware that it does not accept the following type because of a bug: 52 | application/ld+json; profile="https://www.w3.org/ns/activitystreams" 53 | */ 54 | response.set('Content-Type', 'application/activity+json'); 55 | 56 | response.send(message); 57 | }; 58 | -------------------------------------------------------------------------------- /src/routes/api/_cookie.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { randomBytes } from 'crypto'; 18 | import { Response } from 'express'; 19 | import { promisify } from 'util'; 20 | import Cookie, { getToken } from '../../lib/tuples/cookie'; 21 | import LocalAccount from '../../lib/tuples/local_account'; 22 | import Repository from '../../lib/repository'; 23 | 24 | const promisifiedRandomBytes = promisify(randomBytes); 25 | const recovery = {}; 26 | 27 | export default async function(repository: Repository, account: LocalAccount, response: Response) { 28 | const secret = await promisifiedRandomBytes(64); 29 | 30 | await Cookie.create( 31 | repository, 32 | account, 33 | secret, 34 | response.locals.signal, 35 | () => recovery 36 | ).catch(error => { 37 | if (error == recovery) { 38 | response.sendStatus(500); 39 | return; 40 | } 41 | 42 | throw error; 43 | }); 44 | 45 | response.cookie('miniverse', getToken(secret), { 46 | httpOnly: true, 47 | sameSite: 'Lax', 48 | secure: true 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/routes/api/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import OrderedCollectionPage from '../../lib/tuples/ordered_collection_page'; 18 | import secure from '../_secure'; 19 | 20 | const recovery = {}; 21 | 22 | export const get = secure(async (request, response) => { 23 | const { repository } = response.app.locals; 24 | const { signal, user } = response.locals; 25 | let resolved; 26 | 27 | if (!user) { 28 | response.sendStatus(403); 29 | return; 30 | } 31 | 32 | response.setHeader('Content-Type', 'text/event-stream'); 33 | response.setHeader('Transfer-Encoding', 'chunked'); 34 | 35 | try { 36 | const statuses = await user.select('inbox', signal, () => recovery); 37 | 38 | const initialCollection = new OrderedCollectionPage({ 39 | orderedItems: (await Promise.all(statuses 40 | .reverse() 41 | .map(status => status.select('extension', signal, () => recovery)))) 42 | .filter(Boolean as unknown as (t: T | null) => t is T) 43 | }); 44 | 45 | resolved = await initialCollection.toActivityStreams( 46 | signal, () => recovery) as { [key: string]: unknown }; 47 | } catch (error) { 48 | if (error == recovery) { 49 | response.sendStatus(500); 50 | return; 51 | } 52 | 53 | throw error; 54 | } 55 | 56 | const subscribedChannel = repository.getInboxChannel(user); 57 | 58 | function listen(_publishedChannel: string, message: string) { 59 | response.write(`data:{"@context":"https://www.w3.org/ns/activitystreams","type":"OrderedCollectionPage","orderedItems":[${message}]}\n\n`); 60 | } 61 | 62 | resolved['@context'] = 'https://www.w3.org/ns/activitystreams'; 63 | response.write(`data:${JSON.stringify(resolved)}\n\n`); 64 | 65 | await repository.subscribe(subscribedChannel, listen); 66 | const heartbeat = setInterval(() => response.write(':\n'), 16384); 67 | 68 | request.on('close', () => { 69 | clearInterval(heartbeat); 70 | repository.unsubscribe(subscribedChannel, listen); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/routes/api/proxy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { Request, Response, urlencoded } from 'express'; 18 | import { promisify } from 'util'; 19 | import ParsedActivityStreams, { 20 | noHost 21 | } from '../../lib/parsed_activitystreams'; 22 | import Actor from '../../lib/tuples/actor'; 23 | import secure from '../_secure'; 24 | import sendActivityStreams from '../_send_activitystreams'; 25 | 26 | const recovery = {}; 27 | const setBody = promisify(urlencoded({ extended: false })) as 28 | unknown as 29 | (request: Request, response: Response) => Promise; 30 | 31 | export const post = secure(async (request, response) => { 32 | await setBody(request, response); 33 | 34 | const { body } = request; 35 | const { repository } = response.app.locals; 36 | const parsed = new ParsedActivityStreams(repository, body.id, noHost); 37 | let actor; 38 | 39 | try { 40 | actor = await Actor.fromParsedActivityStreams(repository, parsed, response.locals.signal, () => recovery); 41 | } catch (error) { 42 | if (error == recovery) { 43 | response.sendStatus(500); 44 | return; 45 | } 46 | 47 | throw error; 48 | } 49 | 50 | if (actor) { 51 | await sendActivityStreams(response, actor); 52 | } else { 53 | response.sendStatus(404); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/routes/api/signup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { Request, Response } from 'express'; 18 | import { raw } from 'body-parser'; 19 | import { promisify } from 'util'; 20 | import LocalAccount from '../../lib/tuples/local_account'; 21 | import secure from '../_secure'; 22 | import cookie from './_cookie'; 23 | 24 | const recovery = {}; 25 | const setBody = promisify(raw()) as 26 | unknown as 27 | (request: Request, response: Response) => Promise; 28 | 29 | export const post = secure(async (request, response) => { 30 | await setBody(request, response); 31 | 32 | const { body } = request; 33 | const { repository } = response.app.locals; 34 | const salt = body.slice(0, 64); 35 | const serverKey = body.slice(64, 96); 36 | const storedKey = body.slice(96, 128); 37 | const username = body.slice(128).toString(); 38 | let account; 39 | 40 | try { 41 | account = await LocalAccount.create(repository, { 42 | actor: { 43 | username, 44 | name: '', 45 | summary: '' 46 | }, 47 | admin: false, 48 | salt, 49 | serverKey, 50 | storedKey 51 | }, response.locals.signal, () => recovery); 52 | } catch (error) { 53 | if (error == recovery) { 54 | response.sendStatus(422); 55 | return; 56 | } 57 | 58 | throw error; 59 | } 60 | 61 | await cookie(repository, account, response); 62 | response.sendStatus(204); 63 | }); 64 | -------------------------------------------------------------------------------- /src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if $session.user} 17 | 18 | {:else} 19 | 20 | {/if} 21 | 28 | -------------------------------------------------------------------------------- /src/sapper.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@sapper/app' { 2 | function start(options: { target: HTMLElement }): void; 3 | } 4 | 5 | declare module '@sapper/server' { 6 | import { Handler, Request, Response } from 'express'; 7 | import Session from 'src/lib/session/types'; 8 | 9 | function middleware(options: { 10 | session(request: Request, response: Response): Session; 11 | }): Handler; 12 | } 13 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import Repository from './lib/repository'; 18 | import options from './options'; 19 | import processJobs from './subsystems/processor'; 20 | import serve from './subsystems/server'; 21 | 22 | const repository = new Repository(options); 23 | 24 | if (!process.env.NO_PROCESSOR) { 25 | processJobs(repository); 26 | } 27 | 28 | const server = serve(repository).listen(Number(process.env.PORT)); 29 | server.on('error', repository.console.error); 30 | 31 | function terminate() { 32 | const paused = repository.queue.pause(true); 33 | server.close(() => paused.then(() => repository.end())); 34 | } 35 | 36 | process.on('SIGINT', terminate); 37 | process.on('SIGTERM', terminate); 38 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | import { timestamp, files, shell, routes } from '@sapper/service-worker'; 2 | 3 | const ASSETS = `cache${timestamp}`; 4 | 5 | // `shell` is an array of all the files generated by the bundler, 6 | // `files` is an array of everything in the `static` directory 7 | const to_cache = shell.concat(files); 8 | const cached = new Set(to_cache); 9 | 10 | self.addEventListener('install', event => { 11 | event.waitUntil( 12 | caches 13 | .open(ASSETS) 14 | .then(cache => cache.addAll(to_cache)) 15 | .then(() => { 16 | self.skipWaiting(); 17 | }) 18 | ); 19 | }); 20 | 21 | self.addEventListener('activate', event => { 22 | event.waitUntil( 23 | caches.keys().then(async keys => { 24 | // delete old caches 25 | for (const key of keys) { 26 | if (key !== ASSETS) await caches.delete(key); 27 | } 28 | 29 | self.clients.claim(); 30 | }) 31 | ); 32 | }); 33 | 34 | self.addEventListener('fetch', event => { 35 | if (event.request.method !== 'GET' || event.request.headers.has('range')) return; 36 | 37 | const url = new URL(event.request.url); 38 | 39 | // don't try to handle e.g. data: URIs 40 | if (!url.protocol.startsWith('http')) return; 41 | 42 | // ignore dev server requests 43 | if (url.hostname === self.location.hostname && url.port !== self.location.port) return; 44 | 45 | if (url.pathname === '/api/events' || url.origin !== self.origin) return; 46 | 47 | // always serve static files and bundler-generated assets from cache 48 | if (url.host === self.location.host && cached.has(url.pathname)) { 49 | event.respondWith(caches.match(event.request)); 50 | return; 51 | } 52 | 53 | // for pages, you might want to serve a shell `service-worker-index.html` file, 54 | // which Sapper has generated for you. It's not right for every 55 | // app, but if it's right for yours then uncomment this section 56 | /* 57 | if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) { 58 | event.respondWith(caches.match('/service-worker-index.html')); 59 | return; 60 | } 61 | */ 62 | 63 | if (event.request.cache === 'only-if-cached') return; 64 | 65 | // for everything else, try the network first, falling back to 66 | // cache if the user is offline. (If the pages never change, you 67 | // might prefer a cache-first approach to a network-first one.) 68 | event.respondWith( 69 | caches 70 | .open(`offline${timestamp}`) 71 | .then(async cache => { 72 | try { 73 | const response = await fetch(event.request); 74 | cache.put(event.request, response.clone()); 75 | return response; 76 | } catch(err) { 77 | const response = await cache.match(event.request); 78 | if (response) return response; 79 | 80 | throw err; 81 | } 82 | }) 83 | ); 84 | }); 85 | -------------------------------------------------------------------------------- /src/subsystems/client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { start } from '@sapper/app'; 18 | 19 | type GA = ((...a: unknown[]) => void) & { q: unknown[] }; 20 | 21 | declare global { 22 | interface Window { 23 | ga: GA; 24 | } 25 | 26 | const ga: GA; 27 | } 28 | 29 | window.ga = function() { 30 | window.ga.q.push(arguments); 31 | } as GA; 32 | 33 | ga.q = []; 34 | 35 | addEventListener('error', ({ message, filename }) => { 36 | ga('send', 'event', 'Runtime script error', filename, message); 37 | }); 38 | 39 | addEventListener('unhandledrejection', ({ reason }) => { 40 | ga('send', 'event', 'Unhandled promise rejection', null, reason); 41 | }); 42 | 43 | document.addEventListener('securitypolicyviolation', ({ blockedURI, violatedDirective }) => { 44 | ga( 45 | 'send', 46 | 'event', 47 | 'Security Policy Violation', 48 | violatedDirective, 49 | blockedURI, 50 | { nonInteraction: true }); 51 | }); 52 | 53 | export default function(target: HTMLElement) { 54 | return start({ target }); 55 | } 56 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/accept.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { 19 | fabricateFollow, 20 | fabricateLocalAccount, 21 | fabricateRemoteAccount 22 | } from '../../../lib/test/fabricator'; 23 | import repository from '../../../lib/test/repository'; 24 | import { unwrap } from '../../../lib/test/types'; 25 | import accept from './accept'; 26 | import nock = require('nock'); 27 | 28 | test('delivers to remote account', async () => { 29 | const recover = jest.fn(); 30 | const { signal } = new AbortController; 31 | 32 | const [actor, object] = await Promise.all([ 33 | fabricateRemoteAccount({ inbox: { uri: 'https://AcToR.إختبار/?inbox' } }) 34 | .then(account => account.select('actor', signal, recover)) 35 | .then(unwrap), 36 | fabricateLocalAccount() 37 | .then(account => account.select('actor', signal, recover)) 38 | .then(unwrap) 39 | ]); 40 | 41 | const { id } = await fabricateFollow({ actor, object }); 42 | const job = await repository.queue.add({ objectId: id }); 43 | 44 | const post = nock('https://AcToR.إختبار').post('/?inbox').reply(200); 45 | 46 | try { 47 | await accept(repository, job, signal, recover); 48 | expect(post.isDone()).toBe(true); 49 | } finally { 50 | nock.cleanAll(); 51 | } 52 | 53 | expect(recover).not.toHaveBeenCalled(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/accept.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Job } from 'bull'; 19 | import Repository from '../../../lib/repository'; 20 | import { postToInbox, temporaryError } from '../../../lib/transfer'; 21 | import Accept from '../../../lib/tuples/accept'; 22 | import LocalAccount from '../../../lib/tuples/local_account'; 23 | import RemoteAccount from '../../../lib/tuples/remote_account'; 24 | 25 | interface Data { 26 | readonly objectId: string; 27 | } 28 | 29 | export default async function(repository: Repository, { data }: Job, signal: AbortSignal, recover: (error: Error & { [temporaryError]?: boolean }) => unknown) { 30 | const accept = new Accept({ objectId: data.objectId, repository }); 31 | 32 | const object = await accept.select('object', signal, recover); 33 | if (!object) { 34 | throw recover(new Error('object not found.')); 35 | } 36 | 37 | const [sender, inboxURI] = await Promise.all([ 38 | object.select('object', signal, recover).then(actor => { 39 | if (actor) { 40 | return actor.select('account', signal, recover); 41 | } 42 | 43 | throw recover(new Error('object\'s object not found.')); 44 | }), 45 | object.select('actor', signal, recover).then(async actor => { 46 | if (!actor) { 47 | throw recover(new Error('object\'s actor not found.')); 48 | } 49 | 50 | const account = await actor.select('account', signal, recover); 51 | if (!(account instanceof RemoteAccount)) { 52 | throw recover(new Error('object\'s actor\'s account invalid.')); 53 | } 54 | 55 | return account.select('inboxURI', signal, recover); 56 | }) 57 | ]); 58 | 59 | if (!(sender instanceof LocalAccount)) { 60 | throw recover(new Error('object\'s object\'s account invalid.')); 61 | } 62 | 63 | if (!inboxURI) { 64 | throw recover(new Error('object\'s actor\'s inboxURI not found.')); 65 | } 66 | 67 | await postToInbox(repository, sender, inboxURI, accept as any, signal, recover); 68 | } 69 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import accept from './accept'; 18 | import postFollow from './post_follow'; 19 | import postLike from './post_like'; 20 | import postStatus from './post_status'; 21 | import processInbox from './process_inbox'; 22 | 23 | export default { accept, postFollow, postLike, postStatus, processInbox }; 24 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_follow.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { 19 | fabricateFollow, 20 | fabricateLocalAccount, 21 | fabricateRemoteAccount 22 | } from '../../../lib/test/fabricator'; 23 | import repository from '../../../lib/test/repository'; 24 | import { unwrap } from '../../../lib/test/types'; 25 | import postFollow from './post_follow'; 26 | import nock = require('nock'); 27 | 28 | test('delivers to remote account', async () => { 29 | const recover = jest.fn(); 30 | const { signal } = new AbortController; 31 | 32 | const [actor, object] = await Promise.all([ 33 | fabricateLocalAccount() 34 | .then(account => account.select('actor', signal, recover)) 35 | .then(unwrap), 36 | fabricateRemoteAccount({ inbox: { uri: 'https://ObJeCt.إختبار/?inbox' } }) 37 | .then(account => account.select('actor', signal, recover)) 38 | .then(unwrap) 39 | ]); 40 | 41 | const follow = await fabricateFollow({ actor, object }); 42 | const job = await repository.queue.add({ id: follow.id }); 43 | const post = nock('https://ObJeCt.إختبار').post('/?inbox').reply(200); 44 | 45 | try { 46 | await postFollow(repository, job, signal, recover); 47 | expect(post.isDone()).toBe(true); 48 | } finally { 49 | nock.cleanAll(); 50 | } 51 | 52 | expect(recover).not.toHaveBeenCalled(); 53 | }); 54 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_follow.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Job } from 'bull'; 19 | import Repository from '../../../lib/repository'; 20 | import { postToInbox, temporaryError } from '../../../lib/transfer'; 21 | import LocalAccount from '../../../lib/tuples/local_account'; 22 | import RemoteAccount from '../../../lib/tuples/remote_account'; 23 | 24 | interface Data { 25 | readonly id: string; 26 | } 27 | 28 | export default async function (repository: Repository, { data }: Job, signal: AbortSignal, recover: (error: Error & { [temporaryError]?: boolean }) => unknown) { 29 | const follow = await repository.selectFollowIncludingActorAndObjectById(data.id, signal, recover); 30 | 31 | if (!follow) { 32 | throw recover(new Error('Follow not found.')); 33 | } 34 | 35 | const [sender, inboxURI] = await Promise.all([ 36 | follow.select('actor', signal, recover).then(actor => { 37 | if (!actor) { 38 | throw new Error('actor not found.'); 39 | } 40 | 41 | return actor.select('account', signal, recover); 42 | }), 43 | follow.select('object', signal, recover).then(async actor => { 44 | if (!actor) { 45 | throw new Error('object not found.'); 46 | } 47 | 48 | const account = await actor.select('account', signal, recover); 49 | if (!(account instanceof RemoteAccount)) { 50 | throw recover(new Error('object\'s account invalid')); 51 | } 52 | 53 | return account.select('inboxURI', signal, recover); 54 | }) 55 | ]); 56 | 57 | if (!(sender instanceof LocalAccount)) { 58 | throw recover(new Error('actor\'s account invalid')); 59 | } 60 | 61 | if (!inboxURI) { 62 | throw recover(new Error('object\'s inbox not found.')); 63 | } 64 | 65 | await postToInbox(repository, sender, inboxURI, follow as any, signal, recover); 66 | } 67 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_like.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { 19 | fabricateLike, 20 | fabricateLocalAccount, 21 | fabricateNote, 22 | fabricateRemoteAccount 23 | } from '../../../lib/test/fabricator'; 24 | import repository from '../../../lib/test/repository'; 25 | import { unwrap } from '../../../lib/test/types'; 26 | import postLike from './post_like'; 27 | import nock = require('nock'); 28 | 29 | test('delivers to remote account', async () => { 30 | const recover = jest.fn(); 31 | const { signal } = new AbortController; 32 | 33 | const [actor, object] = await Promise.all([ 34 | fabricateLocalAccount() 35 | .then(account => account.select('actor', signal, recover)) 36 | .then(unwrap), 37 | fabricateRemoteAccount({ inbox: { uri: 'https://ObJeCt.إختبار/?inbox' } }) 38 | .then(account => account.select('actor', signal, recover)) 39 | .then(unwrap) 40 | .then(actor => fabricateNote({ status: { actor } })) 41 | ]); 42 | 43 | const { id } = await fabricateLike({ actor, object }); 44 | const job = await repository.queue.add({ id }); 45 | 46 | const post = nock('https://ObJeCt.إختبار').post('/?inbox').reply(200); 47 | 48 | try { 49 | await postLike(repository, job, signal, recover); 50 | expect(post.isDone()).toBe(true); 51 | } finally { 52 | nock.cleanAll(); 53 | } 54 | 55 | expect(recover).not.toHaveBeenCalled(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_like.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Job } from 'bull'; 19 | import Repository from '../../../lib/repository'; 20 | import { postToInbox, temporaryError } from '../../../lib/transfer'; 21 | import LocalAccount from '../../../lib/tuples/local_account'; 22 | import RemoteAccount from '../../../lib/tuples/remote_account'; 23 | 24 | interface Data { 25 | readonly id: string; 26 | } 27 | 28 | export default async function(repository: Repository, { data }: Job, signal: AbortSignal, recover: (error: Error & { [temporaryError]?: boolean }) => unknown) { 29 | const like = await repository.selectLikeById(data.id, signal, recover); 30 | if (!like) { 31 | throw recover(new Error('Like not found.')); 32 | } 33 | 34 | const [sender, inboxURI] = await Promise.all([ 35 | like.select('actor', signal, recover).then(actor => { 36 | if (!actor) { 37 | throw recover(new Error('actor not found.')); 38 | } 39 | 40 | return actor.select('account', signal, recover); 41 | }), 42 | like.select('object', signal, recover).then(async note => { 43 | if (!note) { 44 | throw recover(new Error('object not found.')); 45 | } 46 | 47 | const status = await note.select('status', signal, recover); 48 | if (!status) { 49 | throw recover(new Error('object\'s status not found.')); 50 | } 51 | 52 | const actor = await status.select('actor', signal, recover); 53 | if (!actor) { 54 | throw recover(new Error('object\'s actor not found.')); 55 | } 56 | 57 | const account = await actor.select('account', signal, recover); 58 | if (!(account instanceof RemoteAccount)) { 59 | throw recover(new Error('object\'s actor\'s account invalid.')); 60 | } 61 | 62 | return account.select('inboxURI', signal, recover); 63 | }) 64 | ]); 65 | 66 | if (!(sender instanceof LocalAccount)) { 67 | throw recover(new Error('actor\'s account invalid.')); 68 | } 69 | 70 | if (!inboxURI) { 71 | throw recover(new Error('object\'s actor\'s inboxURI not found.')); 72 | } 73 | 74 | await postToInbox(repository, sender, inboxURI, like as any, signal, recover); 75 | } 76 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_status.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController } from 'abort-controller'; 18 | import { 19 | fabricateAnnounce, 20 | fabricateLocalAccount, 21 | fabricateNote, 22 | fabricateRemoteAccount 23 | } from '../../../lib/test/fabricator'; 24 | import repository from '../../../lib/test/repository'; 25 | import { unwrap } from '../../../lib/test/types'; 26 | import postStatus from './post_status'; 27 | import nock = require('nock'); 28 | 29 | const { signal } = new AbortController; 30 | 31 | test('delivers announce to remote account', async () => { 32 | const recover = jest.fn(); 33 | 34 | const [recipient, actor] = await Promise.all([ 35 | fabricateRemoteAccount( 36 | { inbox: { uri: 'https://ReCiPiEnT.إختبار/?inbox' } }), 37 | fabricateLocalAccount() 38 | .then(account => account.select('actor', signal, recover)) 39 | .then(unwrap) 40 | ]); 41 | 42 | const announce = await fabricateAnnounce({ status: { actor } }); 43 | 44 | const job = await repository.queue.add({ 45 | statusId: announce.id, 46 | inboxURIId: recipient.inboxURIId 47 | }); 48 | 49 | const post = nock('https://ReCiPiEnT.إختبار').post('/?inbox').reply(200); 50 | 51 | try { 52 | await postStatus(repository, job, signal, recover); 53 | expect(post.isDone()).toBe(true); 54 | } finally { 55 | nock.cleanAll(); 56 | } 57 | 58 | expect(recover).not.toHaveBeenCalled(); 59 | }); 60 | 61 | test('delivers note to remote account', async () => { 62 | const recover = jest.fn(); 63 | 64 | const [recipient, actor] = await Promise.all([ 65 | fabricateRemoteAccount( 66 | { inbox: { uri: 'https://ReCiPiEnT.إختبار/?inbox' } }), 67 | fabricateLocalAccount() 68 | .then(account => account.select('actor', signal, recover)) 69 | .then(unwrap) 70 | ]); 71 | 72 | const note = await fabricateNote({ status: { actor } }); 73 | 74 | const job = await repository.queue.add({ 75 | statusId: note.id, 76 | inboxURIId: recipient.inboxURIId 77 | }); 78 | 79 | const post = nock('https://ReCiPiEnT.إختبار').post('/?inbox').reply(200); 80 | 81 | try { 82 | await postStatus(repository, job, signal, recover); 83 | expect(post.isDone()).toBe(true); 84 | } finally { 85 | nock.cleanAll(); 86 | } 87 | 88 | expect(recover).not.toHaveBeenCalled(); 89 | }); 90 | -------------------------------------------------------------------------------- /src/subsystems/processor/handlers/post_status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortSignal } from 'abort-controller'; 18 | import { Job } from 'bull'; 19 | import Repository from '../../../lib/repository'; 20 | import { postToInbox, temporaryError } from '../../../lib/transfer'; 21 | import Create from '../../../lib/tuples/create'; 22 | import LocalAccount from '../../../lib/tuples/local_account'; 23 | import Note from '../../../lib/tuples/note'; 24 | 25 | interface Data { 26 | readonly statusId: string; 27 | readonly inboxURIId: string; 28 | } 29 | 30 | export default async function(repository: Repository, { data }: Job, signal: AbortSignal, recover: (error: Error & { [temporaryError]?: boolean }) => unknown) { 31 | const [[activity, sender], inboxURI] = await Promise.all([ 32 | repository.selectStatusIncludingExtensionById(data.statusId, signal, recover).then(status => { 33 | if (!status) { 34 | throw recover(new Error('Status not found.')); 35 | } 36 | 37 | return Promise.all([ 38 | status.select('extension', signal, recover).then(object => 39 | object instanceof Note ? new Create({ repository, object }) : object), 40 | status.select('actor', signal, recover).then(actor => { 41 | if (!actor) { 42 | throw recover(new Error('actor not found.')); 43 | } 44 | 45 | return actor.select('account', signal, recover); 46 | }) 47 | ]); 48 | }), 49 | repository.selectURIById(data.inboxURIId, signal, recover) 50 | ]); 51 | 52 | if (!activity) { 53 | throw new Error('Status extension not found.'); 54 | } 55 | 56 | if (!(sender instanceof LocalAccount)) { 57 | throw recover(new Error('actor\'s account invalid.')); 58 | } 59 | 60 | if (!inboxURI) { 61 | throw recover(new Error('actor\'s inboxURI not found.')); 62 | } 63 | 64 | await postToInbox(repository, sender, inboxURI, activity as any, signal, recover); 65 | } 66 | -------------------------------------------------------------------------------- /src/subsystems/processor/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Miniverse authors 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, version 3 of the License. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | */ 16 | 17 | import { AbortController, AbortSignal } from 'abort-controller'; 18 | import { Job } from 'bull'; 19 | import { globalAgent } from 'https'; 20 | import Repository from '../../lib/repository'; 21 | import { temporaryError } from '../../lib/transfer'; 22 | import handlers from './handlers'; 23 | 24 | export default function(repository: Repository) { 25 | repository.queue.process(globalAgent.maxFreeSockets, job => { 26 | const indexableHandlers = handlers as { 27 | readonly [key: string]: ( 28 | ( 29 | repository: Repository, 30 | job: Job, 31 | signal: AbortSignal, 32 | recover: (error: Error & { [temporaryError]?: boolean }) => unknown 33 | ) => Promise 34 | ) | undefined; 35 | }; 36 | 37 | const handle = indexableHandlers[job.data.type]; 38 | if (!handle) { 39 | repository.console.error(`Unrecoverable error caused by job ${job.id}.`); 40 | throw new Error('Invalid data type.'); 41 | } 42 | 43 | const controller = new AbortController; 44 | const recovery: { error?: Error } = {}; 45 | let discard = Promise.resolve(); 46 | 47 | const promise: Promise & { 48 | cancel?: () => unknown; 49 | } = handle(repository, job, controller.signal, error => { 50 | if (!error[temporaryError]) { 51 | discard = job.discard(); 52 | } 53 | 54 | recovery.error = error; 55 | return recovery; 56 | }).catch(error => discard.then(() => { 57 | if (error == recovery) { 58 | throw recovery.error; 59 | } 60 | 61 | repository.console.error(`Unrecoverable error caused by job ${job.id}.`); 62 | throw error; 63 | })); 64 | 65 | promise.cancel = () => controller.abort(); 66 | return promise; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/svelte.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'svelte/store' { 2 | export function readable(value: T, subscribe: (set: (value: T) => void) => () => void): { 3 | subscribe(): () => void; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | %sapper.base% 22 | %sapper.styles% 23 | %sapper.head% 24 | 25 | 26 |
%sapper.html%
27 |

Copyright (C) 2018 Miniverse authors. Licensed under AGPL-3.0.

28 |

See licenses of third parties as well.

29 | %sapper.scripts% 30 | 31 | 32 | -------------------------------------------------------------------------------- /test.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.tsconfig", 3 | "compilerOptions": { 4 | "checkJs": true, 5 | "lib": ["es2017"], 6 | "module": "CommonJS", 7 | "target": "ES2017", 8 | "types": ["jest"] 9 | }, 10 | "include": ["**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /travis/create-bucket.js: -------------------------------------------------------------------------------- 1 | const S3 = require('aws-sdk/clients/s3'); 2 | 3 | const s3 = new S3({ 4 | endpoint: process.env.AWS_ENDPOINT, 5 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 6 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 7 | signatureVersion: process.env.AWS_SIGNATURE_VERSION, 8 | s3ForcePathStyle: Boolean(process.env.AWS_S3_FORCE_PATH_STYLE), 9 | maxRetries: 9 10 | }); 11 | 12 | if (process.env.AWS_S3_BUCKET) { 13 | s3.createBucket({ Bucket: `${process.env.AWS_S3_BUCKET}-test-1` }, error => { 14 | if (error) { 15 | console.error(error); // eslint-disable-line no-console 16 | process.exitCode = 1; 17 | } 18 | }); 19 | } else { 20 | process.exitCode = 1; 21 | } 22 | -------------------------------------------------------------------------------- /ts.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:@typescript-eslint/recommended", 3 | "parserOptions": { "project": "./tsconfig.json" }, 4 | "rules": { 5 | "@typescript-eslint/camelcase": "off", 6 | "@typescript-eslint/explicit-function-return-type": "off", 7 | "@typescript-eslint/explicit-member-accessibility": "off", 8 | "@typescript-eslint/indent": ["error", 2], 9 | "@typescript-eslint/no-explicit-any": "off", 10 | "@typescript-eslint/no-use-before-define": [ 11 | "error", 12 | { "functions": false, "typedefs": false } 13 | ], 14 | "no-undef": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.tsconfig", 3 | "compilerOptions": { 4 | "lib": ["es2017", "dom"], 5 | "paths": { "isomorphism/*": ["src/lib/isomorphism/browser/*"] }, 6 | "types": ["jest", "webpack-env"] 7 | }, 8 | "include": ["src", "migrations", "webpack.config.js"], 9 | "exclude": ["src/service-worker.js"] 10 | } 11 | --------------------------------------------------------------------------------