mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge branch 'master' into 6376-refactor-cypress-upgrad-all-relevant-packages-to-current-versions
This commit is contained in:
commit
2c39e8f8a2
118
.github/workflows/test-e2e.yml
vendored
118
.github/workflows/test-e2e.yml
vendored
@ -2,8 +2,58 @@ name: ocelot.social end-to-end test CI
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
docker_preparation:
|
||||
name: Fullstack test preparation
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
pr-number: ${{ steps.pr.outputs.number }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Copy env files
|
||||
run: |
|
||||
cp webapp/.env.template webapp/.env
|
||||
cp backend/.env.template backend/.env
|
||||
|
||||
- name: Build docker images
|
||||
run: |
|
||||
mkdir /tmp/images
|
||||
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
||||
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar
|
||||
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
|
||||
docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar
|
||||
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar
|
||||
|
||||
- name: Install cypress requirements
|
||||
run: |
|
||||
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
|
||||
cd backend
|
||||
yarn install
|
||||
yarn build
|
||||
cd ..
|
||||
yarn install
|
||||
|
||||
- name: Get pr number
|
||||
id: pr
|
||||
uses: 8BitJonny/gh-get-current-pr@2.2.0
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache
|
||||
uses: actions/cache/save@v3.3.1
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
/home/runner/.cache/Cypress
|
||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||
/tmp/images/
|
||||
key: e2e-preparation-cache-pr${{ steps.pr.outputs.number }}
|
||||
|
||||
fullstack_tests:
|
||||
name: Fullstack tests
|
||||
if: success()
|
||||
needs: docker_preparation
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
jobs: 8
|
||||
@ -12,30 +62,56 @@ jobs:
|
||||
# run copies of the current job in parallel
|
||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
/home/runner/.cache/Cypress
|
||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||
/tmp/images/
|
||||
key: e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: webapp | copy env file
|
||||
run: cp webapp/.env.template webapp/.env
|
||||
|
||||
- name: backend | copy env file
|
||||
run: cp backend/.env.template backend/.env
|
||||
|
||||
- name: boot up test system | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
|
||||
|
||||
- name: cypress | Fullstack tests
|
||||
id: e2e-tests
|
||||
- name: Boot up test system | docker-compose
|
||||
run: |
|
||||
yarn install
|
||||
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||
chmod +x /opt/cucumber-json-formatter
|
||||
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
|
||||
docker load < /tmp/images/neo4j.tar
|
||||
docker load < /tmp/images/backend.tar
|
||||
docker load < /tmp/images/webapp.tar
|
||||
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
|
||||
sleep 90s
|
||||
|
||||
##########################################################################
|
||||
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
|
||||
##########################################################################
|
||||
- name: Full stack tests | if any test failed, upload screenshots
|
||||
- name: Full stack tests | run tests
|
||||
id: e2e-tests
|
||||
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||
|
||||
- name: Full stack tests | if tests failed, compile html report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
run: |
|
||||
cd cypress/
|
||||
node create-cucumber-html-report.js
|
||||
|
||||
- name: Full stack tests | if tests failed, upload report
|
||||
id: e2e-report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: cypress/screenshots/
|
||||
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: always()
|
||||
needs: [docker_preparation, fullstack_tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
8
.github/workflows/test-webapp.yml
vendored
8
.github/workflows/test-webapp.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.webapp == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
build_test_webapp:
|
||||
name: Docker Build Test - Webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||
needs: [files-changed, prepare]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
lint_webapp:
|
||||
name: Lint Webapp
|
||||
if: needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.webapp == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
unit_test_webapp:
|
||||
name: Unit Tests - Webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||
needs: [files-changed, build_test_webapp]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@ -4,8 +4,68 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [2.7.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.6.0...2.7.0)
|
||||
|
||||
- fix(webapp): fix event teaser date from start to end by new components [`#6385`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6385)
|
||||
- refactor(webapp): optimize create and update event form [`#6381`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6381)
|
||||
- feat(backend): show events not ended yet [`#6405`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6405)
|
||||
- feat(backend): migration to add postType property to existing posts [`#6396`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6396)
|
||||
- feat(backend): seed events [`#6391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6391)
|
||||
- feat(backend): seed posts as article [`#6227`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6227)
|
||||
- refactor(webapp): fix coverage [`#6361`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6361)
|
||||
- test(other): migrate cypress to v12 [`#6008`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6008)
|
||||
- refactor(backend): copy files in external script [`#6364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6364)
|
||||
- feat(webapp): alternative solution for filter and order posts [`#6367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6367)
|
||||
- refactor(webapp): changed color for event-ribbon. [`#6362`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6362)
|
||||
- fix(webapp): warnings in unit tests [`#6359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6359)
|
||||
- Bump metascraper-title from 5.33.5 to 5.34.7 in /backend [`#6372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6372)
|
||||
- Bump @babel/preset-env from 7.21.5 to 7.22.4 [`#6369`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6369)
|
||||
- fix(other): typescript fix regarding dist/build folder 2 [`#6366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6366)
|
||||
- fix(backend): corrected path in branded images for backend build folder(former dist) [`#6365`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6365)
|
||||
- chore(other): upgrade node version in '.nvmrc' files to v20.2.0 [`#6331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6331)
|
||||
- feat(backend): typescript [`#6321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6321)
|
||||
- fix(webapp): fix group list number to six [`#6319`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6319)
|
||||
- fix(webapp): fix notification menu comment hash [`#6335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6335)
|
||||
- feat(other): 🍰 epic events – master [`#6199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6199)
|
||||
- fix(other): fix avatar seeding [`#6260`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6260)
|
||||
- chore(other): set 'DEBUG=true' in backend '.env.template' to use GraphQL Playground [`#6333`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6333)
|
||||
- refactor(other): unused packages ocelot [`#6326`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6326)
|
||||
- refactor(backend): unused packages backend [`#6325`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6325)
|
||||
- docs(other): add description for script usage in deployment readme [`#6329`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6329)
|
||||
- fix(webapp): fix newsfeed layout [`#6154`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6154)
|
||||
- fix(webapp): adds white space after user handle in comment editor [`#6308`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6308)
|
||||
- Bump validator from 13.0.0 to 13.9.0 in /backend [`#6076`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6076)
|
||||
- Bump metascraper-soundcloud from 5.34.2 to 5.34.4 in /backend [`#6312`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6312)
|
||||
- Bump metascraper-audio from 5.33.5 to 5.34.4 in /backend [`#6287`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6287)
|
||||
- Bump node from 19.9.0-alpine3.17 to 20.2.0-alpine3.17 in /backend [`#6310`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6310)
|
||||
- docs(other): add missing todo in deployment readme 'TODO-next-update.md' [`#6324`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6324)
|
||||
- Bump node from 20.1.0-alpine3.17 to 20.2.0-alpine3.17 in /webapp [`#6309`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6309)
|
||||
- fix(backend): helmet fix [`#6318`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6318)
|
||||
- fix(backend): helmet + graphiql [`#6303`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6303)
|
||||
- Bump @babel/preset-env from 7.21.4 to 7.21.5 [`#6273`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6273)
|
||||
- Bump date-fns from 2.25.0 to 2.30.0 [`#6283`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6283)
|
||||
- Bump node from 19.9.0-alpine3.17 to 20.1.0-alpine3.17 in /webapp [`#6280`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6280)
|
||||
- Bump @babel/core from 7.21.4 to 7.21.8 [`#6284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6284)
|
||||
- Bump helmet from 3.22.0 to 7.0.0 in /backend [`#6296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6296)
|
||||
- fix(webapp): fix z layer of header elements [`#6279`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6279)
|
||||
- fix(webapp): properly render avatars in group settings [`#6289`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6289)
|
||||
- fix(backend): post type on notifications [`#6257`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6257)
|
||||
- fix(backend): recover missing commit [`#6262`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6262)
|
||||
- feat(backend): filter posts by post type [`#6255`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6255)
|
||||
- feat(backend): save location address [`#6240`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6240)
|
||||
- feat(backend): add further event params [`#6231`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6231)
|
||||
- feat(backend): event parameters [`#6198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6198)
|
||||
- feat(backend): create and update posts with labels [`#6197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6197)
|
||||
- feat(backend): add article label to posts [`#6196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6196)
|
||||
- Cypress: update packaage info [`b38769b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b38769b048e9cb9ca07862a61ea810f21b4ce82a)
|
||||
- update cypress related packageges in package.json [`692ec2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/692ec2a11555600647ec8d95b8296c9869948b02)
|
||||
- fixed coverage reporting [`540cd40`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/540cd40e10ec0461ef17379cb93d914839f3a84f)
|
||||
|
||||
#### [2.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.5.1...2.6.0)
|
||||
|
||||
> 27 April 2023
|
||||
|
||||
- chore(release): v2.6.0 [`#6271`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6271)
|
||||
- fix(other): docker-compose for rebranding deployment [`#6265`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6265)
|
||||
- feat(webapp): default categories of group for posts in group [`#6259`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6259)
|
||||
- refactor(webapp): make action radius select in group form a reusable component [`#6244`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6244)
|
||||
|
||||
@ -1,25 +1,219 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
es6: true,
|
||||
// es6: true,
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
parserOptions: {
|
||||
/* parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
},*/
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/],
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:prettier/recommended'
|
||||
// 'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
// 'plugin:import/recommended',
|
||||
// 'plugin:import/typescript',
|
||||
// 'plugin:security/recommended',
|
||||
// 'plugin:@eslint-community/eslint-comments/recommended',
|
||||
],
|
||||
plugins: [
|
||||
'jest'
|
||||
],
|
||||
rules: {
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
/* rules: {
|
||||
//'indent': [ 'error', 2 ],
|
||||
//'quotes': [ "error", "single"],
|
||||
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-console': ['error'],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'prettier/prettier': ['error'],
|
||||
> 'no-console': ['error'],
|
||||
> 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
> 'prettier/prettier': ['error'],
|
||||
}, */
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
camelcase: 'error',
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
},
|
||||
],
|
||||
// import
|
||||
// 'import/export': 'error',
|
||||
// 'import/no-deprecated': 'error',
|
||||
// 'import/no-empty-named-blocks': 'error',
|
||||
// 'import/no-extraneous-dependencies': 'error',
|
||||
// 'import/no-mutable-exports': 'error',
|
||||
// 'import/no-unused-modules': 'error',
|
||||
// 'import/no-named-as-default': 'error',
|
||||
// 'import/no-named-as-default-member': 'error',
|
||||
// 'import/no-amd': 'error',
|
||||
// 'import/no-commonjs': 'error',
|
||||
// 'import/no-import-module-exports': 'error',
|
||||
// 'import/no-nodejs-modules': 'off',
|
||||
// 'import/unambiguous': 'error',
|
||||
// 'import/default': 'error',
|
||||
// 'import/named': 'error',
|
||||
// 'import/namespace': 'error',
|
||||
// 'import/no-absolute-path': 'error',
|
||||
// 'import/no-cycle': 'error',
|
||||
// 'import/no-dynamic-require': 'error',
|
||||
// 'import/no-internal-modules': 'off',
|
||||
// 'import/no-relative-packages': 'error',
|
||||
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
|
||||
// 'import/no-self-import': 'error',
|
||||
// 'import/no-unresolved': 'error',
|
||||
// 'import/no-useless-path-segments': 'error',
|
||||
// 'import/no-webpack-loader-syntax': 'error',
|
||||
// 'import/consistent-type-specifier-style': 'error',
|
||||
// 'import/exports-last': 'off',
|
||||
// 'import/extensions': 'error',
|
||||
// 'import/first': 'error',
|
||||
// 'import/group-exports': 'off',
|
||||
// 'import/newline-after-import': 'error',
|
||||
// 'import/no-anonymous-default-export': 'error',
|
||||
// 'import/no-default-export': 'error',
|
||||
// 'import/no-duplicates': 'error',
|
||||
// 'import/no-named-default': 'error',
|
||||
// 'import/no-namespace': 'error',
|
||||
// 'import/no-unassigned-import': 'error',
|
||||
// 'import/order': [
|
||||
// 'error',
|
||||
// {
|
||||
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
// 'newlines-between': 'always',
|
||||
// pathGroups: [
|
||||
// {
|
||||
// pattern: '@?*/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: '@/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// ],
|
||||
// alphabetize: {
|
||||
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
|
||||
// caseInsensitive: true /* ignore case. Options: [true, false] */,
|
||||
// },
|
||||
// distinctGroup: true,
|
||||
// },
|
||||
// ],
|
||||
// 'import/prefer-default-export': 'off',
|
||||
// n
|
||||
// 'n/handle-callback-err': 'error',
|
||||
// 'n/no-callback-literal': 'error',
|
||||
// 'n/no-exports-assign': 'error',
|
||||
// 'n/no-extraneous-import': 'error',
|
||||
// 'n/no-extraneous-require': 'error',
|
||||
// 'n/no-hide-core-modules': 'error',
|
||||
// 'n/no-missing-import': 'off', // not compatible with typescript
|
||||
// 'n/no-missing-require': 'error',
|
||||
// 'n/no-new-require': 'error',
|
||||
// 'n/no-path-concat': 'error',
|
||||
// 'n/no-process-exit': 'error',
|
||||
// 'n/no-unpublished-bin': 'error',
|
||||
// 'n/no-unpublished-import': 'off', // TODO need to exclude seeds
|
||||
// 'n/no-unpublished-require': 'error',
|
||||
// 'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
|
||||
// 'n/no-unsupported-features/es-builtins': 'error',
|
||||
// 'n/no-unsupported-features/es-syntax': 'error',
|
||||
// 'n/no-unsupported-features/node-builtins': 'error',
|
||||
// 'n/process-exit-as-throw': 'error',
|
||||
// 'n/shebang': 'error',
|
||||
// 'n/callback-return': 'error',
|
||||
// 'n/exports-style': 'error',
|
||||
// 'n/file-extension-in-import': 'off',
|
||||
// 'n/global-require': 'error',
|
||||
// 'n/no-mixed-requires': 'error',
|
||||
// 'n/no-process-env': 'error',
|
||||
// 'n/no-restricted-import': 'error',
|
||||
// 'n/no-restricted-require': 'error',
|
||||
// 'n/no-sync': 'error',
|
||||
// 'n/prefer-global/buffer': 'error',
|
||||
// 'n/prefer-global/console': 'error',
|
||||
// 'n/prefer-global/process': 'error',
|
||||
// 'n/prefer-global/text-decoder': 'error',
|
||||
// 'n/prefer-global/text-encoder': 'error',
|
||||
// 'n/prefer-global/url': 'error',
|
||||
// 'n/prefer-global/url-search-params': 'error',
|
||||
// 'n/prefer-promises/dns': 'error',
|
||||
// 'n/prefer-promises/fs': 'error',
|
||||
// promise
|
||||
// 'promise/catch-or-return': 'error',
|
||||
// 'promise/no-return-wrap': 'error',
|
||||
// 'promise/param-names': 'error',
|
||||
// 'promise/always-return': 'error',
|
||||
// 'promise/no-native': 'off',
|
||||
// 'promise/no-nesting': 'warn',
|
||||
// 'promise/no-promise-in-callback': 'warn',
|
||||
// 'promise/no-callback-in-promise': 'warn',
|
||||
// 'promise/avoid-new': 'warn',
|
||||
// 'promise/no-new-statics': 'error',
|
||||
// 'promise/no-return-in-finally': 'warn',
|
||||
// 'promise/valid-params': 'warn',
|
||||
// 'promise/prefer-await-to-callbacks': 'error',
|
||||
// 'promise/no-multiple-resolved': 'error',
|
||||
// eslint comments
|
||||
// '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
|
||||
// '@eslint-community/eslint-comments/no-restricted-disable': 'error',
|
||||
// '@eslint-community/eslint-comments/no-use': 'off',
|
||||
// '@eslint-community/eslint-comments/require-description': 'off',
|
||||
},
|
||||
overrides: [
|
||||
// only for ts files
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: [
|
||||
// 'plugin:@typescript-eslint/recommended',
|
||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
// 'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
rules: {
|
||||
// allow explicitly defined dangling promises
|
||||
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
|
||||
'no-void': ['error', { allowAsStatement: true }],
|
||||
// ignore prefer-regexp-exec rule to allow string.match(regex)
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
|
||||
'import/unambiguous': 'off',
|
||||
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
// this is to properly reference the referenced project database without requirement of compiling it
|
||||
// eslint-disable-next-line camelcase
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.ts'],
|
||||
plugins: ['jest'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'jest/no-disabled-tests': 'error',
|
||||
'jest/no-focused-tests': 'error',
|
||||
'jest/no-identical-title': 'error',
|
||||
'jest/prefer-to-have-length': 'error',
|
||||
'jest/valid-expect': 'error',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
// 'jest/unbound-method': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'**/*.js',
|
||||
'**/*.ts',
|
||||
'!**/node_modules/**',
|
||||
'!**/test/**',
|
||||
'!**/build/**',
|
||||
'!**/src/**/?(*.)+(spec|test).js?(x)'
|
||||
'!**/src/**/?(*.)+(spec|test).ts?(x)'
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 57,
|
||||
lines: 67,
|
||||
},
|
||||
},
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/setup.js']
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/setup.ts']
|
||||
}
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "ocelot-social-backend",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.0",
|
||||
"description": "GraphQL Backend for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"main": "src/index.js",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/db/migrations --store ./build/db/migrate/store.js",
|
||||
"start": "node build/",
|
||||
"__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
|
||||
"start": "node build/src/",
|
||||
"build": "tsc && ./scripts/build.copy.files.sh",
|
||||
"dev": "nodemon --exec ts-node src/ -e js,ts,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts ./src",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
|
||||
"db:clean": "babel-node src/db/clean.js",
|
||||
"db:clean": "ts-node src/db/clean.ts",
|
||||
"db:reset": "yarn run db:clean",
|
||||
"db:seed": "babel-node src/db/seed.js",
|
||||
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
|
||||
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
|
||||
"db:seed": "ts-node src/db/seed.ts",
|
||||
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.ts",
|
||||
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "~7.8.4",
|
||||
@ -45,7 +45,6 @@
|
||||
"cheerio": "~1.0.0-rc.3",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~7.0.3",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^14.6.0",
|
||||
@ -76,7 +75,7 @@
|
||||
"metascraper-url": "^5.34.2",
|
||||
"metascraper-video": "^5.33.5",
|
||||
"metascraper-youtube": "^5.33.5",
|
||||
"migrate": "^1.7.0",
|
||||
"migrate": "^2.0.0",
|
||||
"mime-types": "^2.1.26",
|
||||
"minimatch": "^3.0.4",
|
||||
"mustache": "^4.2.0",
|
||||
@ -97,24 +96,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"apollo-server-testing": "~2.11.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~6.0.5",
|
||||
"eslint": "~6.8.0",
|
||||
"eslint-config-prettier": "~6.15.0",
|
||||
"eslint-config-standard": "~14.1.1",
|
||||
"eslint-plugin-import": "~2.20.2",
|
||||
"eslint-plugin-jest": "~23.8.2",
|
||||
"eslint-plugin-node": "~11.1.0",
|
||||
"eslint-plugin-prettier": "~3.4.1",
|
||||
"eslint-plugin-promise": "~4.3.1",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"jest": "29.4",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.4",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"prettier": "^2.8.7",
|
||||
"jest": "^27.2.4",
|
||||
"nodemon": "~2.0.2",
|
||||
"prettier": "~2.3.2",
|
||||
"rosie": "^2.0.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/**/fs-capacitor": "^6.2.0",
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# html files
|
||||
mkdir -p build/middleware/helpers/email/templates/
|
||||
cp -r src/middleware/helpers/email/templates/*.html build/middleware/helpers/email/templates/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/
|
||||
cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/
|
||||
|
||||
mkdir -p build/middleware/helpers/email/templates/en/
|
||||
cp -r src/middleware/helpers/email/templates/en/*.html build/middleware/helpers/email/templates/en/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/en/
|
||||
cp -r src/middleware/helpers/email/templates/en/*.html build/src/middleware/helpers/email/templates/en/
|
||||
|
||||
mkdir -p build/middleware/helpers/email/templates/de/
|
||||
cp -r src/middleware/helpers/email/templates/de/*.html build/middleware/helpers/email/templates/de/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/de/
|
||||
cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/
|
||||
|
||||
# gql files
|
||||
mkdir -p build/schema/types/
|
||||
cp -r src/schema/types/*.gql build/schema/types/
|
||||
mkdir -p build/src/schema/types/
|
||||
cp -r src/schema/types/*.gql build/src/schema/types/
|
||||
|
||||
mkdir -p build/schema/types/enum/
|
||||
cp -r src/schema/types/enum/*.gql build/schema/types/enum/
|
||||
mkdir -p build/src/schema/types/enum/
|
||||
cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/
|
||||
|
||||
mkdir -p build/schema/types/scalar/
|
||||
cp -r src/schema/types/scalar/*.gql build/schema/types/scalar/
|
||||
mkdir -p build/src/schema/types/scalar/
|
||||
cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/
|
||||
|
||||
mkdir -p build/schema/types/type/
|
||||
cp -r src/schema/types/type/*.gql build/schema/types/type/
|
||||
mkdir -p build/src/schema/types/type/
|
||||
cp -r src/schema/types/type/*.gql build/src/schema/types/type/
|
||||
@ -1,240 +0,0 @@
|
||||
// import { extractDomainFromUrl, signAndSend } from './utils'
|
||||
import { extractNameFromId, signAndSend } from './utils'
|
||||
import { isPublicAddressed } from './utils/activity'
|
||||
// import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
|
||||
import request from 'request'
|
||||
// import as from 'activitystrea.ms'
|
||||
import NitroDataSource from './NitroDataSource'
|
||||
import router from './routes'
|
||||
import Collections from './Collections'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import CONFIG from '../config'
|
||||
const debug = require('debug')('ea')
|
||||
|
||||
let activityPub = null
|
||||
|
||||
export { activityPub }
|
||||
|
||||
export default class ActivityPub {
|
||||
constructor(activityPubEndpointUri, internalGraphQlUri) {
|
||||
this.endpoint = activityPubEndpointUri
|
||||
this.dataSource = new NitroDataSource(internalGraphQlUri)
|
||||
this.collections = new Collections(this.dataSource)
|
||||
}
|
||||
|
||||
static init(server) {
|
||||
if (!activityPub) {
|
||||
activityPub = new ActivityPub(CONFIG.CLIENT_URI, CONFIG.GRAPHQL_URI)
|
||||
|
||||
// integrate into running graphql express server
|
||||
server.express.set('ap', activityPub)
|
||||
server.express.use(router)
|
||||
console.log('-> ActivityPub middleware added to the graphql express server') // eslint-disable-line no-console
|
||||
} else {
|
||||
console.log('-> ActivityPub middleware already added to the graphql express server') // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
// handleFollowActivity(activity) {
|
||||
// debug(`inside FOLLOW ${activity.actor}`)
|
||||
// const toActorName = extractNameFromId(activity.object)
|
||||
// const fromDomain = extractDomainFromUrl(activity.actor)
|
||||
// const dataSource = this.dataSource
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// request(
|
||||
// {
|
||||
// url: activity.actor,
|
||||
// headers: {
|
||||
// Accept: 'application/activity+json',
|
||||
// },
|
||||
// },
|
||||
// async (err, response, toActorObject) => {
|
||||
// if (err) return reject(err)
|
||||
// // save shared inbox
|
||||
// toActorObject = JSON.parse(toActorObject)
|
||||
// await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
||||
|
||||
// const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
|
||||
// activity.object,
|
||||
// )
|
||||
|
||||
// const followActivity = as
|
||||
// .follow()
|
||||
// .id(activity.id)
|
||||
// .actor(activity.actor)
|
||||
// .object(activity.object)
|
||||
|
||||
// // add follower if not already in collection
|
||||
// if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
||||
// debug('follower already in collection!')
|
||||
// debug(`inbox = ${toActorObject.inbox}`)
|
||||
// resolve(
|
||||
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// } else {
|
||||
// followersCollectionPage.orderedItems.push(activity.actor)
|
||||
// }
|
||||
// debug(`toActorObject = ${toActorObject}`)
|
||||
// toActorObject =
|
||||
// typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
||||
// debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
||||
// debug(`inbox = ${toActorObject.inbox}`)
|
||||
// debug(`outbox = ${toActorObject.outbox}`)
|
||||
// debug(`followers = ${toActorObject.followers}`)
|
||||
// debug(`following = ${toActorObject.following}`)
|
||||
|
||||
// try {
|
||||
// await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
||||
// debug('follow activity saved')
|
||||
// resolve(
|
||||
// sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// } catch (e) {
|
||||
// debug('followers update error!', e)
|
||||
// resolve(
|
||||
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
|
||||
handleUndoActivity(activity) {
|
||||
debug('inside UNDO')
|
||||
switch (activity.object.type) {
|
||||
case 'Follow': {
|
||||
const followActivity = activity.object
|
||||
return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object)
|
||||
}
|
||||
case 'Like': {
|
||||
return this.dataSource.deleteShouted(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateActivity(activity) {
|
||||
debug('inside create')
|
||||
switch (activity.object.type) {
|
||||
case 'Note': {
|
||||
const articleObject = activity.object
|
||||
if (articleObject.inReplyTo) {
|
||||
return this.dataSource.createComment(activity)
|
||||
} else {
|
||||
return this.dataSource.createPost(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteActivity(activity) {
|
||||
debug('inside delete')
|
||||
switch (activity.object.type) {
|
||||
case 'Article':
|
||||
case 'Note':
|
||||
return this.dataSource.deletePost(activity)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateActivity(activity) {
|
||||
debug('inside update')
|
||||
switch (activity.object.type) {
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
return this.dataSource.updatePost(activity)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeActivity(activity) {
|
||||
// TODO differ if activity is an Article/Note/etc.
|
||||
return this.dataSource.createShouted(activity)
|
||||
}
|
||||
|
||||
handleDislikeActivity(activity) {
|
||||
// TODO differ if activity is an Article/Note/etc.
|
||||
return this.dataSource.deleteShouted(activity)
|
||||
}
|
||||
|
||||
async handleAcceptActivity(activity) {
|
||||
debug('inside accept')
|
||||
switch (activity.object.type) {
|
||||
case 'Follow': {
|
||||
const followObject = activity.object
|
||||
const followingCollectionPage = await this.collections.getFollowingCollectionPage(
|
||||
followObject.actor,
|
||||
)
|
||||
followingCollectionPage.orderedItems.push(followObject.object)
|
||||
await this.dataSource.saveFollowingCollectionPage(followingCollectionPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActorObject(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(
|
||||
{
|
||||
url: url,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
(err, response, body) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(JSON.parse(body))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
generateStatusId(slug) {
|
||||
return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}`
|
||||
}
|
||||
|
||||
async sendActivity(activity) {
|
||||
delete activity.send
|
||||
const fromName = extractNameFromId(activity.actor)
|
||||
if (Array.isArray(activity.to) && isPublicAddressed(activity)) {
|
||||
debug('is public addressed')
|
||||
const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
|
||||
// serve shared inbox endpoints
|
||||
sharedInboxEndpoints.map((sharedInbox) => {
|
||||
return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox)
|
||||
})
|
||||
activity.to = activity.to.filter((recipient) => {
|
||||
return !isPublicAddressed({ to: recipient })
|
||||
})
|
||||
// serve the rest
|
||||
activity.to.map(async (recipient) => {
|
||||
debug('serve rest')
|
||||
const actorObject = await this.getActorObject(recipient)
|
||||
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
|
||||
})
|
||||
} else if (typeof activity.to === 'string') {
|
||||
debug('is string')
|
||||
const actorObject = await this.getActorObject(activity.to)
|
||||
return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox)
|
||||
} else if (Array.isArray(activity.to)) {
|
||||
activity.to.map(async (recipient) => {
|
||||
const actorObject = await this.getActorObject(recipient)
|
||||
return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async trySend(activity, fromName, host, url, tries = 5) {
|
||||
try {
|
||||
return await signAndSend(activity, fromName, host, url)
|
||||
} catch (e) {
|
||||
if (tries > 0) {
|
||||
setTimeout(function () {
|
||||
return this.trySend(activity, fromName, host, url, --tries)
|
||||
}, 20000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
export default class Collections {
|
||||
constructor(dataSource) {
|
||||
this.dataSource = dataSource
|
||||
}
|
||||
|
||||
getFollowersCollection(actorId) {
|
||||
return this.dataSource.getFollowersCollection(actorId)
|
||||
}
|
||||
|
||||
getFollowersCollectionPage(actorId) {
|
||||
return this.dataSource.getFollowersCollectionPage(actorId)
|
||||
}
|
||||
|
||||
getFollowingCollection(actorId) {
|
||||
return this.dataSource.getFollowingCollection(actorId)
|
||||
}
|
||||
|
||||
getFollowingCollectionPage(actorId) {
|
||||
return this.dataSource.getFollowingCollectionPage(actorId)
|
||||
}
|
||||
|
||||
getOutboxCollection(actorId) {
|
||||
return this.dataSource.getOutboxCollection(actorId)
|
||||
}
|
||||
|
||||
getOutboxCollectionPage(actorId) {
|
||||
return this.dataSource.getOutboxCollectionPage(actorId)
|
||||
}
|
||||
}
|
||||
@ -1,575 +0,0 @@
|
||||
import {
|
||||
throwErrorIfApolloErrorOccurred,
|
||||
extractIdFromActivityId,
|
||||
extractNameFromId,
|
||||
constructIdFromName,
|
||||
} from './utils'
|
||||
import { createOrderedCollection, createOrderedCollectionPage } from './utils/collection'
|
||||
import { createArticleObject, isPublicAddressed } from './utils/activity'
|
||||
import crypto from 'crypto'
|
||||
import gql from 'graphql-tag'
|
||||
import { createHttpLink } from 'apollo-link-http'
|
||||
import { setContext } from 'apollo-link-context'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import fetch from 'node-fetch'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import trunc from 'trunc-html'
|
||||
const debug = require('debug')('ea:datasource')
|
||||
|
||||
export default class NitroDataSource {
|
||||
constructor(uri) {
|
||||
this.uri = uri
|
||||
const defaultOptions = {
|
||||
query: {
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
}
|
||||
const link = createHttpLink({ uri: this.uri, fetch: fetch }) // eslint-disable-line
|
||||
const cache = new InMemoryCache()
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
// generate the authentication token (maybe from env? Which user?)
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw'
|
||||
// return the headers to the context so httpLink can read them
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: token ? `Bearer ${token}` : '',
|
||||
},
|
||||
}
|
||||
})
|
||||
this.client = new ApolloClient({
|
||||
link: authLink.concat(link),
|
||||
cache: cache,
|
||||
defaultOptions,
|
||||
})
|
||||
}
|
||||
|
||||
async getFollowersCollection(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`slug= ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${slug}") {
|
||||
followedByCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
debug('successfully fetched followers')
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followersCount = actor.followedByCount
|
||||
|
||||
const followersCollection = createOrderedCollection(slug, 'followers')
|
||||
followersCollection.totalItems = followersCount
|
||||
|
||||
return followersCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowersCollectionPage(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`getFollowersPage slug = ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
followedBy {
|
||||
slug
|
||||
}
|
||||
followedByCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followers = actor.followedBy
|
||||
const followersCount = actor.followedByCount
|
||||
|
||||
const followersCollection = createOrderedCollectionPage(slug, 'followers')
|
||||
followersCollection.totalItems = followersCount
|
||||
debug(`followers = ${JSON.stringify(followers, null, 2)}`)
|
||||
await Promise.all(
|
||||
followers.map(async (follower) => {
|
||||
followersCollection.orderedItems.push(constructIdFromName(follower.slug))
|
||||
}),
|
||||
)
|
||||
|
||||
return followersCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowingCollection(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
followingCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const followingCount = actor.followingCount
|
||||
|
||||
const followingCollection = createOrderedCollection(slug, 'following')
|
||||
followingCollection.totalItems = followingCount
|
||||
|
||||
return followingCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getFollowingCollectionPage(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
following {
|
||||
slug
|
||||
}
|
||||
followingCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const following = actor.following
|
||||
const followingCount = actor.followingCount
|
||||
|
||||
const followingCollection = createOrderedCollectionPage(slug, 'following')
|
||||
followingCollection.totalItems = followingCount
|
||||
|
||||
await Promise.all(
|
||||
following.map(async (user) => {
|
||||
followingCollection.orderedItems.push(await constructIdFromName(user.slug))
|
||||
}),
|
||||
)
|
||||
|
||||
return followingCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getOutboxCollection(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
content
|
||||
contentExcerpt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const posts = actor.contributions
|
||||
|
||||
const outboxCollection = createOrderedCollection(slug, 'outbox')
|
||||
outboxCollection.totalItems = posts.length
|
||||
|
||||
return outboxCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async getOutboxCollectionPage(actorId) {
|
||||
const slug = extractNameFromId(actorId)
|
||||
debug(`inside getting outbox collection page => ${slug}`)
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug:"${slug}") {
|
||||
actorId
|
||||
contributions {
|
||||
id
|
||||
activityId
|
||||
objectId
|
||||
title
|
||||
slug
|
||||
content
|
||||
contentExcerpt
|
||||
createdAt
|
||||
author {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
debug(result.data)
|
||||
if (result.data) {
|
||||
const actor = result.data.User[0]
|
||||
const posts = actor.contributions
|
||||
|
||||
const outboxCollection = createOrderedCollectionPage(slug, 'outbox')
|
||||
outboxCollection.totalItems = posts.length
|
||||
await Promise.all(
|
||||
posts.map(async (post) => {
|
||||
outboxCollection.orderedItems.push(
|
||||
await createArticleObject(
|
||||
post.activityId,
|
||||
post.objectId,
|
||||
post.content,
|
||||
post.author.slug,
|
||||
post.id,
|
||||
post.createdAt,
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
debug('after createNote')
|
||||
return outboxCollection
|
||||
} else {
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
}
|
||||
|
||||
async undoFollowActivity(fromActorId, toActorId) {
|
||||
const fromUserId = await this.ensureUser(fromActorId)
|
||||
const toUserId = await this.ensureUser(toActorId)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
RemoveUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async saveFollowersCollectionPage(followersCollection, onlyNewestItem = true) {
|
||||
debug('inside saveFollowers')
|
||||
let orderedItems = followersCollection.orderedItems
|
||||
const toUserName = extractNameFromId(followersCollection.id)
|
||||
const toUserId = await this.ensureUser(constructIdFromName(toUserName))
|
||||
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
|
||||
|
||||
return Promise.all(
|
||||
orderedItems.map(async (follower) => {
|
||||
debug(`follower = ${follower}`)
|
||||
const fromUserId = await this.ensureUser(follower)
|
||||
debug(`fromUserId = ${fromUserId}`)
|
||||
debug(`toUserId = ${toUserId}`)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
debug('saveFollowers: added follow edge successfully')
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async saveFollowingCollectionPage(followingCollection, onlyNewestItem = true) {
|
||||
debug('inside saveFollowers')
|
||||
let orderedItems = followingCollection.orderedItems
|
||||
const fromUserName = extractNameFromId(followingCollection.id)
|
||||
const fromUserId = await this.ensureUser(constructIdFromName(fromUserName))
|
||||
orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems
|
||||
return Promise.all(
|
||||
orderedItems.map(async (following) => {
|
||||
debug(`follower = ${following}`)
|
||||
const toUserId = await this.ensureUser(following)
|
||||
debug(`fromUserId = ${fromUserId}`)
|
||||
debug(`toUserId = ${toUserId}`)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserFollowing(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) {
|
||||
from { name }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`)
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
debug('saveFollowing: added follow edge successfully')
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async createPost(activity) {
|
||||
// TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient
|
||||
// createPost
|
||||
const postObject = activity.object
|
||||
if (!isPublicAddressed(postObject)) {
|
||||
return debug(
|
||||
'createPost: not send to public (sending to specific persons is not implemented yet)',
|
||||
)
|
||||
}
|
||||
const title = postObject.summary
|
||||
? postObject.summary
|
||||
: postObject.content.split(' ').slice(0, 5).join(' ')
|
||||
const postId = extractIdFromActivityId(postObject.id)
|
||||
debug('inside create post')
|
||||
let result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(
|
||||
postObject.content,
|
||||
120,
|
||||
)}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${
|
||||
activity.id
|
||||
}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
// ensure user and add author to post
|
||||
const userId = await this.ensureUser(postObject.attributedTo)
|
||||
debug(`userId = ${userId}`)
|
||||
debug(`postId = ${postId}`)
|
||||
result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async deletePost(activity) {
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
DeletePost(id: "${extractIdFromActivityId(activity.object.id)}") {
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async updatePost(activity) {
|
||||
const postObject = activity.object
|
||||
const postId = extractIdFromActivityId(postObject.id)
|
||||
const date = postObject.updated ? postObject.updated : new Date().toISOString()
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
UpdatePost(content: "${postObject.content}", contentExcerpt: "${
|
||||
trunc(postObject.content, 120).html
|
||||
}", id: "${postId}", updatedAt: "${date}") {
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
async createShouted(activity) {
|
||||
const userId = await this.ensureUser(activity.actor)
|
||||
const postId = extractIdFromActivityId(activity.object)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (!result.data.AddUserShouted) {
|
||||
debug('something went wrong shouting post')
|
||||
throw Error('User or Post not exists')
|
||||
}
|
||||
}
|
||||
|
||||
async deleteShouted(activity) {
|
||||
const userId = await this.ensureUser(activity.actor)
|
||||
const postId = extractIdFromActivityId(activity.object)
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
RemoveUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) {
|
||||
from {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (!result.data.AddUserShouted) {
|
||||
debug('something went wrong disliking a post')
|
||||
throw Error('User or Post not exists')
|
||||
}
|
||||
}
|
||||
|
||||
async getSharedInboxEndpoints() {
|
||||
const result = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
SharedInboxEndpoint {
|
||||
uri
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
return result.data.SharedInboxEnpoint
|
||||
}
|
||||
|
||||
async addSharedInboxEndpoint(uri) {
|
||||
try {
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateSharedInboxEndpoint(uri: "${uri}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async createComment(activity) {
|
||||
const postObject = activity.object
|
||||
let result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateComment(content: "${
|
||||
postObject.content
|
||||
}", activityId: "${extractIdFromActivityId(activity.id)}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
const toUserId = await this.ensureUser(activity.actor)
|
||||
const result2 = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result2)
|
||||
|
||||
const postId = extractIdFromActivityId(postObject.inReplyTo)
|
||||
result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will search for user existence and will create a disabled user with a random 16 bytes password when no user is found.
|
||||
*
|
||||
* @param actorId
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async ensureUser(actorId) {
|
||||
debug(`inside ensureUser = ${actorId}`)
|
||||
const name = extractNameFromId(actorId)
|
||||
const queryResult = await this.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
if (
|
||||
queryResult.data &&
|
||||
Array.isArray(queryResult.data.User) &&
|
||||
queryResult.data.User.length > 0
|
||||
) {
|
||||
debug('ensureUser: user exists.. return id')
|
||||
// user already exists.. return the id
|
||||
return queryResult.data.User[0].id
|
||||
} else {
|
||||
debug('ensureUser: user not exists.. createUser')
|
||||
// user does not exist.. create it
|
||||
const pw = crypto.randomBytes(16).toString('hex')
|
||||
const slug = name.toLowerCase().split(' ').join('-')
|
||||
const result = await this.client.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
CreateUser(password: "${pw}", slug:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
|
||||
return result.data.CreateUser.id
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import express from 'express'
|
||||
import { activityPub } from '../ActivityPub'
|
||||
|
||||
const debug = require('debug')('ea:inbox')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// Shared Inbox endpoint (federated Server)
|
||||
// For now its only able to handle Note Activities!!
|
||||
router.post('/', async function (req, res, next) {
|
||||
debug(`Content-Type = ${req.get('Content-Type')}`)
|
||||
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
|
||||
debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
|
||||
switch (req.body.type) {
|
||||
case 'Create':
|
||||
await activityPub.handleCreateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
// case 'Follow':
|
||||
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
// break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
/* eslint-disable */
|
||||
case 'Update':
|
||||
await activityPub.handleUpdateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Accept':
|
||||
await activityPub.handleAcceptActivity(req.body).catch(next)
|
||||
case 'Reject':
|
||||
// Do nothing
|
||||
break
|
||||
case 'Add':
|
||||
break
|
||||
case 'Remove':
|
||||
break
|
||||
case 'Like':
|
||||
await activityPub.handleLikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Dislike':
|
||||
await activityPub.handleDislikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Announce':
|
||||
debug('else!!')
|
||||
debug(JSON.stringify(req.body, null, 2))
|
||||
}
|
||||
/* eslint-enable */
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -1,29 +0,0 @@
|
||||
import user from './user'
|
||||
import inbox from './inbox'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import verify from './verify'
|
||||
|
||||
export default function () {
|
||||
const router = express.Router()
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
return router
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import { createActor } from '../utils/actor'
|
||||
const gql = require('graphql-tag')
|
||||
const debug = require('debug')('ea:serveUser')
|
||||
|
||||
export async function serveUser(req, res, next) {
|
||||
let name = req.params.name
|
||||
|
||||
if (name.startsWith('@')) {
|
||||
name = name.slice(1)
|
||||
}
|
||||
|
||||
debug(`name = ${name}`)
|
||||
const result = await req.app
|
||||
.get('ap')
|
||||
.dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.catch((reason) => {
|
||||
debug(`serveUser User fetch error: ${reason}`)
|
||||
})
|
||||
|
||||
if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) {
|
||||
const publicKey = result.data.User[0].publicKey
|
||||
const actor = createActor(name, publicKey)
|
||||
debug(`actor = ${JSON.stringify(actor, null, 2)}`)
|
||||
debug(
|
||||
`accepts json = ${req.accepts([
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/json',
|
||||
])}`,
|
||||
)
|
||||
if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) {
|
||||
return res.json(actor)
|
||||
} else if (req.accepts('text/html')) {
|
||||
// TODO show user's profile page instead of the actor object
|
||||
/* const outbox = JSON.parse(result.outbox)
|
||||
const posts = outbox.orderedItems.filter((el) => { return el.object.type === 'Note'})
|
||||
const actor = result.actor
|
||||
debug(posts) */
|
||||
// res.render('user', { user: actor, posts: JSON.stringify(posts)})
|
||||
return res.json(actor)
|
||||
}
|
||||
} else {
|
||||
debug(`error getting publicKey for actor ${name}`)
|
||||
next()
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
import { sendCollection } from '../utils/collection'
|
||||
import express from 'express'
|
||||
import { serveUser } from './serveUser'
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
const debug = require('debug')('ea:user')
|
||||
|
||||
router.get('/:name', async function (req, res, next) {
|
||||
debug('inside user.js -> serveUser')
|
||||
await serveUser(req, res, next)
|
||||
})
|
||||
|
||||
router.get('/:name/following', (req, res) => {
|
||||
debug('inside user.js -> serveFollowingCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'followingPage' : 'following'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:name/followers', (req, res) => {
|
||||
debug('inside user.js -> serveFollowersCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
return res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'followersPage' : 'followers'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:name/outbox', (req, res) => {
|
||||
debug('inside user.js -> serveOutboxCollection')
|
||||
const name = req.params.name
|
||||
if (!name) {
|
||||
return res.status(400).send('Bad request! Please specify a name.')
|
||||
} else {
|
||||
const collectionName = req.query.page ? 'outboxPage' : 'outbox'
|
||||
sendCollection(collectionName, req, res)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:name/inbox', verify, async function (req, res, next) {
|
||||
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
|
||||
debug(`actorId = ${req.body.actor}`)
|
||||
// const result = await saveActorId(req.body.actor)
|
||||
switch (req.body.type) {
|
||||
case 'Create':
|
||||
await activityPub.handleCreateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Undo':
|
||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||
break
|
||||
// case 'Follow':
|
||||
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||
// break
|
||||
case 'Delete':
|
||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||
break
|
||||
/* eslint-disable */
|
||||
case 'Update':
|
||||
await activityPub.handleUpdateActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Accept':
|
||||
await activityPub.handleAcceptActivity(req.body).catch(next)
|
||||
case 'Reject':
|
||||
// Do nothing
|
||||
break
|
||||
case 'Add':
|
||||
break
|
||||
case 'Remove':
|
||||
break
|
||||
case 'Like':
|
||||
await activityPub.handleLikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Dislike':
|
||||
await activityPub.handleDislikeActivity(req.body).catch(next)
|
||||
break
|
||||
case 'Announce':
|
||||
debug('else!!')
|
||||
debug(JSON.stringify(req.body, null, 2))
|
||||
}
|
||||
/* eslint-enable */
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -1,20 +0,0 @@
|
||||
import { verifySignature } from '../security'
|
||||
const debug = require('debug')('ea:verify')
|
||||
|
||||
export default async (req, res, next) => {
|
||||
debug(`actorId = ${req.body.actor}`)
|
||||
// TODO stop if signature validation fails
|
||||
if (
|
||||
await verifySignature(
|
||||
`${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`,
|
||||
req.headers,
|
||||
)
|
||||
) {
|
||||
debug('verify = true')
|
||||
next()
|
||||
} else {
|
||||
// throw Error('Signature validation failed!')
|
||||
debug('verify = false')
|
||||
next()
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import express from 'express'
|
||||
import CONFIG from '../../config/'
|
||||
import cors from 'cors'
|
||||
|
||||
const debug = require('debug')('ea:webfinger')
|
||||
const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/
|
||||
|
||||
const createWebFinger = (name) => {
|
||||
const { host } = new URL(CONFIG.CLIENT_URI)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function handler(req, res) {
|
||||
const { resource = '' } = req.query
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, name, domain] = resource.match(regex) || []
|
||||
if (!(name && domain))
|
||||
return res.status(400).json({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
|
||||
const session = req.app.get('driver').session()
|
||||
try {
|
||||
const [slug] = await session.readTransaction(async (t) => {
|
||||
const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', {
|
||||
slug: name,
|
||||
})
|
||||
return result.records.map((record) => record.get('slug'))
|
||||
})
|
||||
if (!slug)
|
||||
return res.status(404).json({
|
||||
error: `No record found for "${name}@${domain}".`,
|
||||
})
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} catch (error) {
|
||||
debug(error)
|
||||
return res.status(500).json({
|
||||
error: `Something went terribly wrong. Please visit ${CONFIG.SUPPORT_URL}`,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const router = express.Router()
|
||||
router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler)
|
||||
return router
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
import { handler } from './webfinger'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
let resource, res, json, status, contentType
|
||||
|
||||
const driver = getDriver()
|
||||
|
||||
const request = () => {
|
||||
json = jest.fn()
|
||||
status = jest.fn(() => ({ json }))
|
||||
contentType = jest.fn(() => ({ json }))
|
||||
res = { status, contentType }
|
||||
const req = {
|
||||
app: {
|
||||
get: (key) => {
|
||||
return {
|
||||
driver,
|
||||
}[key]
|
||||
},
|
||||
},
|
||||
query: {
|
||||
resource,
|
||||
},
|
||||
}
|
||||
return handler(req, res)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
driver.close()
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('webfinger', () => {
|
||||
describe('no ressource', () => {
|
||||
beforeEach(() => {
|
||||
resource = undefined
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('?resource query param', () => {
|
||||
describe('is missing acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'some-user@domain'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no domain', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@domain'
|
||||
})
|
||||
|
||||
it('returns error as json', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(404)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'No record found for "some-user@domain".',
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a user for acct', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('user', { slug: 'some-user' })
|
||||
})
|
||||
|
||||
it('returns user object', async () => {
|
||||
await request()
|
||||
expect(contentType).toHaveBeenCalledWith('application/jrd+json')
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
links: [
|
||||
{
|
||||
href: `${CONFIG.CLIENT_URI}/activitypub/users/some-user`,
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
},
|
||||
],
|
||||
subject: `acct:some-user@${new URL(CONFIG.CLIENT_URI).host}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,104 +0,0 @@
|
||||
import { generateRsaKeyPair, createSignature, verifySignature } from '.'
|
||||
import crypto from 'crypto'
|
||||
import request from 'request'
|
||||
jest.mock('request')
|
||||
|
||||
let privateKey
|
||||
let publicKey
|
||||
let headers
|
||||
const passphrase = 'a7dsf78sadg87ad87sfagsadg78'
|
||||
|
||||
describe('activityPub/security', () => {
|
||||
beforeEach(() => {
|
||||
const pair = generateRsaKeyPair({ passphrase })
|
||||
privateKey = pair.privateKey
|
||||
publicKey = pair.publicKey
|
||||
headers = {
|
||||
Date: '2019-03-08T14:35:45.759Z',
|
||||
Host: 'democracy-app.de',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
|
||||
describe('createSignature', () => {
|
||||
describe('returned http signature', () => {
|
||||
let signatureB64
|
||||
let httpSignature
|
||||
|
||||
beforeEach(() => {
|
||||
const signer = crypto.createSign('rsa-sha256')
|
||||
signer.update(
|
||||
'(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json',
|
||||
)
|
||||
signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
|
||||
httpSignature = createSignature({
|
||||
privateKey,
|
||||
keyId: 'https://human-connection.org/activitypub/users/lea#main-key',
|
||||
url: 'https://democracy-app.de/activitypub/users/max/inbox',
|
||||
headers,
|
||||
passphrase,
|
||||
})
|
||||
})
|
||||
|
||||
it('contains keyId', () => {
|
||||
expect(httpSignature).toContain(
|
||||
'keyId="https://human-connection.org/activitypub/users/lea#main-key"',
|
||||
)
|
||||
})
|
||||
|
||||
it('contains default algorithm "rsa-sha256"', () => {
|
||||
expect(httpSignature).toContain('algorithm="rsa-sha256"')
|
||||
})
|
||||
|
||||
it('contains headers', () => {
|
||||
expect(httpSignature).toContain('headers="(request-target) date host content-type"')
|
||||
})
|
||||
|
||||
it('contains signature', () => {
|
||||
expect(httpSignature).toContain('signature="' + signatureB64 + '"')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifySignature', () => {
|
||||
let httpSignature
|
||||
|
||||
beforeEach(() => {
|
||||
httpSignature = createSignature({
|
||||
privateKey,
|
||||
keyId: 'http://localhost:4001/activitypub/users/test-user#main-key',
|
||||
url: 'https://democracy-app.de/activitypub/users/max/inbox',
|
||||
headers,
|
||||
passphrase,
|
||||
})
|
||||
const body = {
|
||||
publicKey: {
|
||||
id: 'https://localhost:4001/activitypub/users/test-user#main-key',
|
||||
owner: 'https://localhost:4001/activitypub/users/test-user',
|
||||
publicKeyPem: publicKey,
|
||||
},
|
||||
}
|
||||
|
||||
const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body)))
|
||||
request.mockImplementation(mockedRequest)
|
||||
})
|
||||
|
||||
it('resolves false', async () => {
|
||||
await expect(
|
||||
verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
|
||||
).resolves.toEqual(false)
|
||||
})
|
||||
|
||||
describe('valid signature', () => {
|
||||
beforeEach(() => {
|
||||
headers.Signature = httpSignature
|
||||
})
|
||||
|
||||
it('resolves true', async () => {
|
||||
await expect(
|
||||
verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers),
|
||||
).resolves.toEqual(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,172 +0,0 @@
|
||||
// import dotenv from 'dotenv'
|
||||
// import { resolve } from 'path'
|
||||
import crypto from 'crypto'
|
||||
import request from 'request'
|
||||
import CONFIG from './../../config'
|
||||
const debug = require('debug')('ea:security')
|
||||
|
||||
// TODO Does this reference a local config? Why?
|
||||
// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
|
||||
|
||||
export function generateRsaKeyPair(options = {}) {
|
||||
const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
|
||||
return crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
cipher: 'aes-256-cbc',
|
||||
passphrase,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// signing
|
||||
export function createSignature(options) {
|
||||
const {
|
||||
privateKey,
|
||||
keyId,
|
||||
url,
|
||||
headers = {},
|
||||
algorithm = 'rsa-sha256',
|
||||
passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
|
||||
} = options
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
||||
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
|
||||
}
|
||||
const signer = crypto.createSign(algorithm)
|
||||
const signingString = constructSigningString(url, headers)
|
||||
signer.update(signingString)
|
||||
const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
|
||||
const headersString = Object.keys(headers).reduce((result, key) => {
|
||||
return result + ' ' + key.toLowerCase()
|
||||
}, '')
|
||||
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
|
||||
}
|
||||
|
||||
// verifying
|
||||
export function verifySignature(url, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const signatureHeader = headers.signature ? headers.signature : headers.Signature
|
||||
if (!signatureHeader) {
|
||||
debug('No Signature header present!')
|
||||
resolve(false)
|
||||
}
|
||||
debug(`Signature Header = ${signatureHeader}`)
|
||||
const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature')
|
||||
const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm')
|
||||
const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers')
|
||||
const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId')
|
||||
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
||||
debug('Unsupported hash algorithm specified!')
|
||||
resolve(false)
|
||||
}
|
||||
|
||||
const usedHeaders = headersString.split(' ')
|
||||
const verifyHeaders = {}
|
||||
Object.keys(headers).forEach((key) => {
|
||||
if (usedHeaders.includes(key.toLowerCase())) {
|
||||
verifyHeaders[key.toLowerCase()] = headers[key]
|
||||
}
|
||||
})
|
||||
const signingString = constructSigningString(url, verifyHeaders)
|
||||
debug(`keyId= ${keyId}`)
|
||||
request(
|
||||
{
|
||||
url: keyId,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
(err, response, body) => {
|
||||
if (err) reject(err)
|
||||
debug(`body = ${body}`)
|
||||
const actor = JSON.parse(body)
|
||||
const publicKeyPem = actor.publicKey.publicKeyPem
|
||||
resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// private: signing
|
||||
function constructSigningString(url, headers) {
|
||||
const urlObj = new URL(url)
|
||||
const signingString = `(request-target): post ${urlObj.pathname}${
|
||||
urlObj.search !== '' ? urlObj.search : ''
|
||||
}`
|
||||
return Object.keys(headers).reduce((result, key) => {
|
||||
return result + `\n${key.toLowerCase()}: ${headers[key]}`
|
||||
}, signingString)
|
||||
}
|
||||
|
||||
// private: verifying
|
||||
function httpVerify(pubKey, signature, signingString, algorithm) {
|
||||
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
||||
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
|
||||
}
|
||||
const verifier = crypto.createVerify(algorithm)
|
||||
verifier.update(signingString)
|
||||
return verifier.verify(pubKey, signature, 'base64')
|
||||
}
|
||||
|
||||
// private: verifying
|
||||
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
|
||||
// Just pass what you want as key
|
||||
function extractKeyValueFromSignatureHeader(signatureHeader, key) {
|
||||
const keyString = signatureHeader.split(',').filter((el) => {
|
||||
return !!el.startsWith(key)
|
||||
})[0]
|
||||
|
||||
let firstEqualIndex = keyString.search('=')
|
||||
// When headers are requested add 17 to the index to remove "(request-target) " from the string
|
||||
if (key === 'headers') {
|
||||
firstEqualIndex += 17
|
||||
}
|
||||
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
|
||||
}
|
||||
|
||||
// Obtained from invoking crypto.getHashes()
|
||||
export const SUPPORTED_HASH_ALGORITHMS = [
|
||||
'rsa-md4',
|
||||
'rsa-md5',
|
||||
'rsa-mdC2',
|
||||
'rsa-ripemd160',
|
||||
'rsa-sha1',
|
||||
'rsa-sha1-2',
|
||||
'rsa-sha224',
|
||||
'rsa-sha256',
|
||||
'rsa-sha384',
|
||||
'rsa-sha512',
|
||||
'blake2b512',
|
||||
'blake2s256',
|
||||
'md4',
|
||||
'md4WithRSAEncryption',
|
||||
'md5',
|
||||
'md5-sha1',
|
||||
'md5WithRSAEncryption',
|
||||
'mdc2',
|
||||
'mdc2WithRSA',
|
||||
'ripemd',
|
||||
'ripemd160',
|
||||
'ripemd160WithRSA',
|
||||
'rmd160',
|
||||
'sha1',
|
||||
'sha1WithRSAEncryption',
|
||||
'sha224',
|
||||
'sha224WithRSAEncryption',
|
||||
'sha256',
|
||||
'sha256WithRSAEncryption',
|
||||
'sha384',
|
||||
'sha384WithRSAEncryption',
|
||||
'sha512',
|
||||
'sha512WithRSAEncryption',
|
||||
'ssl3-md5',
|
||||
'ssl3-sha1',
|
||||
'whirlpool',
|
||||
]
|
||||
@ -1,117 +0,0 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import { throwErrorIfApolloErrorOccurred } from './index'
|
||||
// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
||||
|
||||
import crypto from 'crypto'
|
||||
// import as from 'activitystrea.ms'
|
||||
import gql from 'graphql-tag'
|
||||
// const debug = require('debug')('ea:utils:activity')
|
||||
|
||||
export function createNoteObject(text, name, id, published) {
|
||||
const createUuid = crypto.randomBytes(16).toString('hex')
|
||||
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}/status/${createUuid}`,
|
||||
type: 'Create',
|
||||
actor: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
object: {
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}/status/${id}`,
|
||||
type: 'Note',
|
||||
published: published,
|
||||
attributedTo: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
content: text,
|
||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function createArticleObject(activityId, objectId, text, name, id, published) {
|
||||
const actorId = await getActorId(name)
|
||||
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${activityId}`,
|
||||
type: 'Create',
|
||||
actor: `${actorId}`,
|
||||
object: {
|
||||
id: `${objectId}`,
|
||||
type: 'Article',
|
||||
published: published,
|
||||
attributedTo: `${actorId}`,
|
||||
content: text,
|
||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function getActorId(name) {
|
||||
const result = await activityPub.dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
actorId
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
throwErrorIfApolloErrorOccurred(result)
|
||||
if (Array.isArray(result.data.User) && result.data.User[0]) {
|
||||
return result.data.User[0].actorId
|
||||
} else {
|
||||
throw Error(`No user with name: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// export function sendAcceptActivity(theBody, name, targetDomain, url) {
|
||||
// as.accept()
|
||||
// .id(
|
||||
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
// crypto.randomBytes(16).toString('hex'),
|
||||
// )
|
||||
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
// .object(theBody)
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (!err) {
|
||||
// return signAndSend(doc, name, targetDomain, url)
|
||||
// } else {
|
||||
// debug(`error serializing Accept object: ${err}`)
|
||||
// throw new Error('error serializing Accept object')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// export function sendRejectActivity(theBody, name, targetDomain, url) {
|
||||
// as.reject()
|
||||
// .id(
|
||||
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||
// crypto.randomBytes(16).toString('hex'),
|
||||
// )
|
||||
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||
// .object(theBody)
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (!err) {
|
||||
// return signAndSend(doc, name, targetDomain, url)
|
||||
// } else {
|
||||
// debug(`error serializing Accept object: ${err}`)
|
||||
// throw new Error('error serializing Accept object')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
export function isPublicAddressed(postObject) {
|
||||
if (typeof postObject.to === 'string') {
|
||||
postObject.to = [postObject.to]
|
||||
}
|
||||
if (typeof postObject === 'string') {
|
||||
postObject.to = [postObject]
|
||||
}
|
||||
if (Array.isArray(postObject)) {
|
||||
postObject.to = postObject
|
||||
}
|
||||
return (
|
||||
postObject.to.includes('Public') ||
|
||||
postObject.to.includes('as:Public') ||
|
||||
postObject.to.includes('https://www.w3.org/ns/activitystreams#Public')
|
||||
)
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
|
||||
export function createActor(name, pubkey) {
|
||||
return {
|
||||
'@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
type: 'Person',
|
||||
preferredUsername: `${name}`,
|
||||
name: `${name}`,
|
||||
following: `${activityPub.endpoint}/activitypub/users/${name}/following`,
|
||||
followers: `${activityPub.endpoint}/activitypub/users/${name}/followers`,
|
||||
inbox: `${activityPub.endpoint}/activitypub/users/${name}/inbox`,
|
||||
outbox: `${activityPub.endpoint}/activitypub/users/${name}/outbox`,
|
||||
url: `${activityPub.endpoint}/activitypub/@${name}`,
|
||||
endpoints: {
|
||||
sharedInbox: `${activityPub.endpoint}/activitypub/inbox`,
|
||||
},
|
||||
publicKey: {
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}#main-key`,
|
||||
owner: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
publicKeyPem: pubkey,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import { constructIdFromName } from './index'
|
||||
const debug = require('debug')('ea:utils:collections')
|
||||
|
||||
export function createOrderedCollection(name, collectionName) {
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
|
||||
summary: `${name}s ${collectionName} collection`,
|
||||
type: 'OrderedCollection',
|
||||
first: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||
totalItems: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function createOrderedCollectionPage(name, collectionName) {
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}?page=true`,
|
||||
summary: `${name}s ${collectionName} collection`,
|
||||
type: 'OrderedCollectionPage',
|
||||
totalItems: 0,
|
||||
partOf: `${activityPub.endpoint}/activitypub/users/${name}/${collectionName}`,
|
||||
orderedItems: [],
|
||||
}
|
||||
}
|
||||
export function sendCollection(collectionName, req, res) {
|
||||
const name = req.params.name
|
||||
const id = constructIdFromName(name)
|
||||
|
||||
switch (collectionName) {
|
||||
case 'followers':
|
||||
attachThenCatch(activityPub.collections.getFollowersCollection(id), res)
|
||||
break
|
||||
|
||||
case 'followersPage':
|
||||
attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res)
|
||||
break
|
||||
|
||||
case 'following':
|
||||
attachThenCatch(activityPub.collections.getFollowingCollection(id), res)
|
||||
break
|
||||
|
||||
case 'followingPage':
|
||||
attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res)
|
||||
break
|
||||
|
||||
case 'outbox':
|
||||
attachThenCatch(activityPub.collections.getOutboxCollection(id), res)
|
||||
break
|
||||
|
||||
case 'outboxPage':
|
||||
attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res)
|
||||
break
|
||||
|
||||
default:
|
||||
res.status(500).end()
|
||||
}
|
||||
}
|
||||
|
||||
function attachThenCatch(promise, res) {
|
||||
return promise
|
||||
.then((collection) => {
|
||||
res.status(200).contentType('application/activity+json').send(collection)
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(`error getting a Collection: = ${err}`)
|
||||
res.status(500).end()
|
||||
})
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
import { activityPub } from '../ActivityPub'
|
||||
import gql from 'graphql-tag'
|
||||
import { createSignature } from '../security'
|
||||
import request from 'request'
|
||||
import CONFIG from './../../config'
|
||||
const debug = require('debug')('ea:utils')
|
||||
|
||||
export function extractNameFromId(uri) {
|
||||
const urlObject = new URL(uri)
|
||||
const pathname = urlObject.pathname
|
||||
const splitted = pathname.split('/')
|
||||
|
||||
return splitted[splitted.indexOf('users') + 1]
|
||||
}
|
||||
|
||||
export function extractIdFromActivityId(uri) {
|
||||
const urlObject = new URL(uri)
|
||||
const pathname = urlObject.pathname
|
||||
const splitted = pathname.split('/')
|
||||
|
||||
return splitted[splitted.indexOf('status') + 1]
|
||||
}
|
||||
export function constructIdFromName(name, fromDomain = activityPub.endpoint) {
|
||||
return `${fromDomain}/activitypub/users/${name}`
|
||||
}
|
||||
|
||||
export function extractDomainFromUrl(url) {
|
||||
return new URL(url).host
|
||||
}
|
||||
|
||||
export function throwErrorIfApolloErrorOccurred(result) {
|
||||
if (result.error && (result.error.message || result.error.errors)) {
|
||||
throw new Error(
|
||||
`${result.error.message ? result.error.message : result.error.errors[0].message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function signAndSend(activity, fromName, targetDomain, url) {
|
||||
// fix for development: replace with http
|
||||
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
|
||||
debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('inside signAndSend')
|
||||
// get the private key
|
||||
activityPub.dataSource.client
|
||||
.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${fromName}") {
|
||||
privateKey
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
reject(result.error)
|
||||
} else {
|
||||
// add security context
|
||||
const parsedActivity = JSON.parse(activity)
|
||||
if (Array.isArray(parsedActivity['@context'])) {
|
||||
parsedActivity['@context'].push('https://w3id.org/security/v1')
|
||||
} else {
|
||||
const context = [parsedActivity['@context']]
|
||||
context.push('https://w3id.org/security/v1')
|
||||
parsedActivity['@context'] = context
|
||||
}
|
||||
|
||||
// deduplicate context strings
|
||||
parsedActivity['@context'] = [...new Set(parsedActivity['@context'])]
|
||||
const privateKey = result.data.User[0].privateKey
|
||||
const date = new Date().toUTCString()
|
||||
|
||||
debug(`url = ${url}`)
|
||||
request(
|
||||
{
|
||||
url: url,
|
||||
headers: {
|
||||
Host: targetDomain,
|
||||
Date: date,
|
||||
Signature: createSignature({
|
||||
privateKey,
|
||||
keyId: `${activityPub.endpoint}/activitypub/users/${fromName}#main-key`,
|
||||
url,
|
||||
headers: {
|
||||
Host: targetDomain,
|
||||
Date: date,
|
||||
'Content-Type': 'application/activity+json',
|
||||
},
|
||||
}),
|
||||
'Content-Type': 'application/activity+json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(parsedActivity),
|
||||
},
|
||||
(error, response) => {
|
||||
if (error) {
|
||||
debug(`Error = ${JSON.stringify(error, null, 2)}`)
|
||||
reject(error)
|
||||
} else {
|
||||
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
|
||||
debug('Response Body:', JSON.stringify(response.body, null, 2))
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import dotenv from 'dotenv'
|
||||
import emails from './emails.js'
|
||||
import metadata from './metadata.js'
|
||||
import emails from './emails'
|
||||
import metadata from './metadata'
|
||||
|
||||
// Load env file
|
||||
if (require.resolve) {
|
||||
@ -15,10 +15,11 @@ if (require.resolve) {
|
||||
}
|
||||
|
||||
// Use Cypress env or process.env
|
||||
declare let Cypress: any | undefined
|
||||
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
|
||||
|
||||
const environment = {
|
||||
NODE_ENV: env.NODE_ENV || process.NODE_ENV,
|
||||
NODE_ENV: env.NODE_ENV || process.env.NODE_ENV,
|
||||
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
|
||||
TEST: env.NODE_ENV === 'test',
|
||||
PRODUCTION: env.NODE_ENV === 'production',
|
||||
@ -90,14 +91,12 @@ const options = {
|
||||
}
|
||||
|
||||
// Check if all required configs are present
|
||||
if (require.resolve) {
|
||||
// are we in a nodejs environment?
|
||||
Object.entries(required).map((entry) => {
|
||||
if (!entry[1]) {
|
||||
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
Object.entries(required).map((entry) => {
|
||||
if (!entry[1]) {
|
||||
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
||||
}
|
||||
return entry
|
||||
})
|
||||
|
||||
export default {
|
||||
...environment,
|
||||
@ -1,4 +1,4 @@
|
||||
// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
|
||||
// this file is duplicated in `backend/src/config/logos` and `webapp/constants/logos.js` and replaced on rebranding
|
||||
// this are the paths in the webapp
|
||||
export default {
|
||||
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
|
||||
@ -1,4 +1,4 @@
|
||||
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` and replaced on rebranding
|
||||
// this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` and replaced on rebranding
|
||||
export default {
|
||||
APPLICATION_NAME: 'ocelot.social',
|
||||
APPLICATION_SHORT_NAME: 'ocelot',
|
||||
@ -1,4 +1,4 @@
|
||||
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
|
||||
// this file is duplicated in `backend/src/constants/metadata` and `webapp/constants/metadata.js`
|
||||
export const CATEGORIES_MIN = 1
|
||||
export const CATEGORIES_MAX = 3
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
|
||||
// this file is duplicated in `backend/src/constants/group` and `webapp/constants/group.js`
|
||||
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 50 // with removed HTML tags
|
||||
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 250 // with removed HTML tags
|
||||
@ -1,5 +0,0 @@
|
||||
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
|
||||
export default {
|
||||
NONCE_LENGTH: 5,
|
||||
INVITE_CODE_LENGTH: 6,
|
||||
}
|
||||
5
backend/src/constants/registration.ts
Normal file
5
backend/src/constants/registration.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js`
|
||||
export default {
|
||||
NONCE_LENGTH: 5,
|
||||
INVITE_CODE_LENGTH: 6,
|
||||
}
|
||||
2
backend/src/db/compiler.ts
Normal file
2
backend/src/db/compiler.ts
Normal file
@ -0,0 +1,2 @@
|
||||
const tsNode = require('ts-node')
|
||||
module.exports = tsNode.register
|
||||
@ -4,8 +4,8 @@ import { hashSync } from 'bcryptjs'
|
||||
import { Factory } from 'rosie'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { getDriver, getNeode } from './neo4j'
|
||||
import CONFIG from '../config/index.js'
|
||||
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'
|
||||
import CONFIG from '../config/index'
|
||||
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -15,7 +15,7 @@ const uniqueImageUrl = (imageUrl) => {
|
||||
return newUrl.toString()
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
export const cleanDatabase = async (options: any = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const session = driver.session()
|
||||
try {
|
||||
@ -18,13 +18,13 @@ export function up(next) {
|
||||
rxSession
|
||||
.beginTransaction()
|
||||
.pipe(
|
||||
flatMap((txc) =>
|
||||
flatMap((txc: any) =>
|
||||
concat(
|
||||
txc
|
||||
.run('MATCH (email:EmailAddress) RETURN email {.email}')
|
||||
.records()
|
||||
.pipe(
|
||||
map((record) => {
|
||||
map((record: any) => {
|
||||
const { email } = record.get('email')
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
return { email, normalizedEmail }
|
||||
@ -45,7 +45,7 @@ export function up(next) {
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map((r) => ({
|
||||
map((r: any) => ({
|
||||
oldEmail: email,
|
||||
email: r.get('email'),
|
||||
user: r.get('user'),
|
||||
@ -12,7 +12,7 @@ export function up(next) {
|
||||
rxSession
|
||||
.beginTransaction()
|
||||
.pipe(
|
||||
flatMap((transaction) =>
|
||||
flatMap((transaction: any) =>
|
||||
concat(
|
||||
transaction
|
||||
.run(
|
||||
@ -23,7 +23,7 @@ export function up(next) {
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map((record) => {
|
||||
map((record: any) => {
|
||||
const { id: locationId } = record.get('location')
|
||||
return { locationId }
|
||||
}),
|
||||
@ -40,7 +40,7 @@ export function up(next) {
|
||||
)
|
||||
.records()
|
||||
.pipe(
|
||||
map((record) => ({
|
||||
map((record: any) => ({
|
||||
location: record.get('location'),
|
||||
updatedLocation: record.get('updatedLocation'),
|
||||
})),
|
||||
@ -3,7 +3,7 @@ import { existsSync, createReadStream } from 'fs'
|
||||
import path from 'path'
|
||||
import { S3 } from 'aws-sdk'
|
||||
import mime from 'mime-types'
|
||||
import { s3Configs } from '../../config'
|
||||
import s3Configs from '../../config'
|
||||
import https from 'https'
|
||||
|
||||
export const description = `
|
||||
@ -11,13 +11,13 @@ export async function up(next) {
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
`)
|
||||
// Those two indexes already exist
|
||||
// await transaction.run(`
|
||||
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
// `)
|
||||
// await transaction.run(`
|
||||
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
// `)
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
|
||||
`)
|
||||
@ -10,7 +10,7 @@ export async function up(next) {
|
||||
try {
|
||||
// Drop indexes if they exist because due to legacy code they might be set already
|
||||
const indexesResponse = await transaction.run(`CALL db.indexes()`)
|
||||
const indexes = indexesResponse.records.map((record) => record.get('indexName'))
|
||||
const indexes = indexesResponse.records.map((record) => record.get('name'))
|
||||
if (indexes.indexOf('user_fulltext_search') > -1) {
|
||||
await transaction.run(`CALL db.index.fulltext.drop("user_fulltext_search")`)
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = 'Add postType property Article to all posts'
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
await transaction.run(`
|
||||
MATCH (post:Post)
|
||||
SET post.postType = 'Article'
|
||||
RETURN post
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
await transaction.run(`
|
||||
MATCH (post:Post)
|
||||
REMOVE post.postType
|
||||
RETURN post
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
47
backend/src/graphql/messages.ts
Normal file
47
backend/src/graphql/messages.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createMessageMutation = () => {
|
||||
return gql`
|
||||
mutation ($roomId: ID!, $content: String!) {
|
||||
CreateMessage(roomId: $roomId, content: $content) {
|
||||
id
|
||||
content
|
||||
senderId
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const messageQuery = () => {
|
||||
return gql`
|
||||
query ($roomId: ID!, $first: Int, $offset: Int) {
|
||||
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
|
||||
_id
|
||||
id
|
||||
indexId
|
||||
content
|
||||
senderId
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const markMessagesAsSeen = () => {
|
||||
return gql`
|
||||
mutation ($messageIds: [String!]) {
|
||||
MarkMessagesAsSeen(messageIds: $messageIds)
|
||||
}
|
||||
`
|
||||
}
|
||||
65
backend/src/graphql/rooms.ts
Normal file
65
backend/src/graphql/rooms.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createRoomMutation = () => {
|
||||
return gql`
|
||||
mutation ($userId: ID!) {
|
||||
CreateRoom(userId: $userId) {
|
||||
id
|
||||
roomId
|
||||
roomName
|
||||
lastMessageAt
|
||||
unreadCount
|
||||
users {
|
||||
_id
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const roomQuery = () => {
|
||||
return gql`
|
||||
query Room($first: Int, $offset: Int, $id: ID) {
|
||||
Room(first: $first, offset: $offset, id: $id, orderBy: createdAt_desc) {
|
||||
id
|
||||
roomId
|
||||
roomName
|
||||
lastMessageAt
|
||||
unreadCount
|
||||
lastMessage {
|
||||
_id
|
||||
id
|
||||
content
|
||||
senderId
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
users {
|
||||
_id
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const unreadRoomsQuery = () => {
|
||||
return gql`
|
||||
query {
|
||||
UnreadRooms
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
* @property fieldName String
|
||||
* @property callback Function
|
||||
*/
|
||||
function walkRecursive(data, fields, fieldName, callback, _key) {
|
||||
function walkRecursive(data, fields, fieldName, callback, _key?) {
|
||||
if (!Array.isArray(fields)) {
|
||||
throw new Error('please provide an fields array for the walkRecursive helper')
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
import { generateRsaKeyPair } from '../activitypub/security'
|
||||
import { activityPub } from '../activitypub/ActivityPub'
|
||||
// import as from 'activitystrea.ms'
|
||||
|
||||
// const debug = require('debug')('backend:schema')
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
// CreatePost: async (resolve, root, args, context, info) => {
|
||||
// args.activityId = activityPub.generateStatusId(context.user.slug)
|
||||
// args.objectId = activityPub.generateStatusId(context.user.slug)
|
||||
|
||||
// const post = await resolve(root, args, context, info)
|
||||
|
||||
// const { user: author } = context
|
||||
// const actorId = author.actorId
|
||||
// debug(`actorId = ${actorId}`)
|
||||
// const createActivity = await new Promise((resolve, reject) => {
|
||||
// as.create()
|
||||
// .id(`${actorId}/status/${args.activityId}`)
|
||||
// .actor(`${actorId}`)
|
||||
// .object(
|
||||
// as
|
||||
// .article()
|
||||
// .id(`${actorId}/status/${post.id}`)
|
||||
// .content(post.content)
|
||||
// .to('https://www.w3.org/ns/activitystreams#Public')
|
||||
// .publishedNow()
|
||||
// .attributedTo(`${actorId}`),
|
||||
// )
|
||||
// .prettyWrite((err, doc) => {
|
||||
// if (err) {
|
||||
// reject(err)
|
||||
// } else {
|
||||
// debug(doc)
|
||||
// const parsedDoc = JSON.parse(doc)
|
||||
// parsedDoc.send = true
|
||||
// resolve(JSON.stringify(parsedDoc))
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// try {
|
||||
// await activityPub.sendActivity(createActivity)
|
||||
// } catch (e) {
|
||||
// debug(`error sending post activity\n${e}`)
|
||||
// }
|
||||
// return post
|
||||
// },
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const keys = generateRsaKeyPair()
|
||||
Object.assign(args, keys)
|
||||
args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
},
|
||||
}
|
||||
57
backend/src/middleware/chatMiddleware.ts
Normal file
57
backend/src/middleware/chatMiddleware.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { isArray } from 'lodash'
|
||||
|
||||
const setRoomProps = (room) => {
|
||||
if (room.users) {
|
||||
room.users.forEach((user) => {
|
||||
user._id = user.id
|
||||
})
|
||||
}
|
||||
if (room.lastMessage) {
|
||||
room.lastMessage._id = room.lastMessage.id
|
||||
}
|
||||
}
|
||||
|
||||
const setMessageProps = (message, context) => {
|
||||
message._id = message.id
|
||||
if (message.senderId !== context.user.id) {
|
||||
message.distributed = true
|
||||
}
|
||||
}
|
||||
|
||||
const roomProperties = async (resolve, root, args, context, info) => {
|
||||
const resolved = await resolve(root, args, context, info)
|
||||
if (resolved) {
|
||||
if (isArray(resolved)) {
|
||||
resolved.forEach((room) => {
|
||||
setRoomProps(room)
|
||||
})
|
||||
} else {
|
||||
setRoomProps(resolved)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
const messageProperties = async (resolve, root, args, context, info) => {
|
||||
const resolved = await resolve(root, args, context, info)
|
||||
if (resolved) {
|
||||
if (isArray(resolved)) {
|
||||
resolved.forEach((message) => {
|
||||
setMessageProps(message, context)
|
||||
})
|
||||
} else {
|
||||
setMessageProps(resolved, context)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Room: roomProperties,
|
||||
Message: messageProperties,
|
||||
},
|
||||
Mutation: {
|
||||
CreateRoom: roomProperties,
|
||||
},
|
||||
}
|
||||
@ -8,7 +8,7 @@ import { exec, build } from 'xregexp/xregexp-all.js'
|
||||
// 2. If it starts with a digit '0-9' than a unicode letter has to follow.
|
||||
const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
|
||||
export default function (content) {
|
||||
export default function (content?) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
||||
@ -18,7 +18,7 @@ export default function (content) {
|
||||
return $(el).attr('data-hashtag-id')
|
||||
})
|
||||
.get()
|
||||
const hashtags = []
|
||||
const hashtags: any = []
|
||||
ids.forEach((id) => {
|
||||
const match = exec(id, regX)
|
||||
if (match != null) {
|
||||
@ -1,12 +1,12 @@
|
||||
import CONFIG from '../../../config'
|
||||
import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js'
|
||||
import { cleanHtml } from '../../../middleware/helpers/cleanHtml'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { htmlToText } from 'nodemailer-html-to-text'
|
||||
|
||||
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
||||
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
||||
|
||||
let sendMailCallback = async () => {}
|
||||
let sendMailCallback: any = async () => {}
|
||||
if (!hasEmailConfig) {
|
||||
if (!CONFIG.TEST) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -29,7 +29,7 @@ if (!hasEmailConfig) {
|
||||
cleanHtml(templateArgs.html, 'dummyKey', {
|
||||
allowedTags: ['a'],
|
||||
allowedAttributes: { a: ['href'] },
|
||||
}).replace(/&/g, '&'),
|
||||
} as any).replace(/&/g, '&'),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import CONFIG from '../../../config'
|
||||
import logosWebapp from '../../../config/logos.js'
|
||||
import logosWebapp from '../../../config/logos'
|
||||
import {
|
||||
signupTemplate,
|
||||
emailVerificationTemplate,
|
||||
@ -1,7 +1,7 @@
|
||||
import mustache from 'mustache'
|
||||
import CONFIG from '../../../config'
|
||||
import metadata from '../../../config/metadata.js'
|
||||
import logosWebapp from '../../../config/logos.js'
|
||||
import metadata from '../../../config/metadata'
|
||||
import logosWebapp from '../../../config/logos'
|
||||
|
||||
import * as templates from './templates'
|
||||
import * as templatesEN from './templates/en'
|
||||
@ -1,7 +1,5 @@
|
||||
import { applyMiddleware } from 'graphql-middleware'
|
||||
import CONFIG from './../config'
|
||||
|
||||
import activityPub from './activityPubMiddleware'
|
||||
import softDelete from './softDelete/softDeleteMiddleware'
|
||||
import sluggify from './sluggifyMiddleware'
|
||||
import excerpt from './excerptMiddleware'
|
||||
@ -16,13 +14,13 @@ import login from './login/loginMiddleware'
|
||||
import sentry from './sentryMiddleware'
|
||||
import languages from './languages/languages'
|
||||
import userInteractions from './userInteractions'
|
||||
import chatMiddleware from './chatMiddleware'
|
||||
|
||||
export default (schema) => {
|
||||
const middlewares = {
|
||||
sentry,
|
||||
permissions,
|
||||
xss,
|
||||
activityPub,
|
||||
validation,
|
||||
sluggify,
|
||||
excerpt,
|
||||
@ -34,6 +32,7 @@ export default (schema) => {
|
||||
orderBy,
|
||||
languages,
|
||||
userInteractions,
|
||||
chatMiddleware,
|
||||
}
|
||||
|
||||
let order = [
|
||||
@ -52,6 +51,7 @@ export default (schema) => {
|
||||
'softDelete',
|
||||
'includedFields',
|
||||
'orderBy',
|
||||
'chatMiddleware',
|
||||
]
|
||||
|
||||
// add permisions middleware at the first position (unless we're seeding)
|
||||
@ -1,5 +1,5 @@
|
||||
import LanguageDetect from 'languagedetect'
|
||||
import { removeHtmlTags } from '../helpers/cleanHtml.js'
|
||||
import { removeHtmlTags } from '../helpers/cleanHtml'
|
||||
|
||||
const setPostLanguage = (text) => {
|
||||
const lngDetector = new LanguageDetect()
|
||||
@ -1,6 +1,6 @@
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
export default (content) => {
|
||||
export default (content?) => {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
const userIds = $('a.mention[data-mention-id]')
|
||||
@ -50,7 +50,7 @@ beforeAll(async () => {
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: neode,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
@ -576,7 +576,7 @@ describe('notifications', () => {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject(expected, { errors: undefined })
|
||||
).resolves.toMatchObject({ ...expected, errors: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,7 +15,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
|
||||
RETURN emailAddress {.email}
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const writeTxResultPromise = session.readTransaction(async (transaction) => {
|
||||
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
|
||||
notificationUserIds,
|
||||
})
|
||||
@ -140,16 +140,18 @@ const postAuthorOfComment = async (commentId, { context }) => {
|
||||
|
||||
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||
const cypher = `
|
||||
MATCH (user:User { id: $userId })
|
||||
MATCH (group:Group { id: $groupId })<-[membership:MEMBER_OF]-(owner:User)
|
||||
WHERE membership.role = 'owner'
|
||||
WITH owner, group
|
||||
WITH owner, group, user, membership
|
||||
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
|
||||
WITH group, owner, notification
|
||||
WITH group, owner, notification, user, membership
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
SET notification.relatedUserId = $userId
|
||||
RETURN notification {.*, from: group, to: properties(owner)}
|
||||
WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification
|
||||
RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -173,16 +175,20 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||
const notifyMemberOfGroup = async (groupId, userId, reason, context) => {
|
||||
const { user: owner } = context
|
||||
const cypher = `
|
||||
MATCH (owner:User { id: $ownerId })
|
||||
MATCH (user:User { id: $userId })
|
||||
MATCH (group:Group { id: $groupId })
|
||||
WITH user, group
|
||||
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group)
|
||||
WITH user, group, owner, membership
|
||||
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
WITH group, user, notification
|
||||
WITH group, user, notification, owner, membership
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
SET notification.relatedUserId = $ownerId
|
||||
RETURN notification {.*, from: group, to: properties(user)}
|
||||
WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup,
|
||||
notification, user, owner
|
||||
RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -238,11 +244,11 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
||||
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
|
||||
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
|
||||
WITH resource, user, notification, authors, posts,
|
||||
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
|
||||
resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
RETURN notification {.*, from: finalResource, to: properties(user)}
|
||||
RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -276,9 +282,14 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
WITH notification, postAuthor, post,
|
||||
WITH notification, postAuthor, post, commenter,
|
||||
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
|
||||
RETURN notification {.*, from: finalResource, to: properties(postAuthor)}
|
||||
RETURN notification {
|
||||
.*,
|
||||
from: finalResource,
|
||||
to: properties(postAuthor),
|
||||
relatedUser: properties(commenter)
|
||||
}
|
||||
`,
|
||||
{ commentId, postAuthorId, reason },
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user