mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +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
|
on: push
|
||||||
|
|
||||||
jobs:
|
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:
|
fullstack_tests:
|
||||||
name: Fullstack tests
|
name: Fullstack tests
|
||||||
|
if: success()
|
||||||
|
needs: docker_preparation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
jobs: 8
|
jobs: 8
|
||||||
@ -12,30 +62,56 @@ jobs:
|
|||||||
# run copies of the current job in parallel
|
# run copies of the current job in parallel
|
||||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Restore cache
|
||||||
uses: actions/checkout@v3
|
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
|
- name: Boot up test system | docker-compose
|
||||||
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
|
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
chmod +x /opt/cucumber-json-formatter
|
||||||
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
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
|
||||||
|
|
||||||
##########################################################################
|
- name: Full stack tests | run tests
|
||||||
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
|
id: e2e-tests
|
||||||
##########################################################################
|
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||||
- name: Full stack tests | if any test failed, upload screenshots
|
|
||||||
|
- 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' }}
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||||
path: cypress/screenshots/
|
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:
|
prepare:
|
||||||
name: Prepare
|
name: Prepare
|
||||||
if: needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
build_test_webapp:
|
build_test_webapp:
|
||||||
name: Docker 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]
|
needs: [files-changed, prepare]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
lint_webapp:
|
lint_webapp:
|
||||||
name: Lint Webapp
|
name: Lint Webapp
|
||||||
if: needs.files-changed.outputs.webapp
|
if: needs.files-changed.outputs.webapp == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
unit_test_webapp:
|
unit_test_webapp:
|
||||||
name: Unit Tests - 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]
|
needs: [files-changed, build_test_webapp]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
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).
|
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)
|
#### [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)
|
- 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)
|
- 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)
|
- 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 = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
es6: true,
|
// es6: true,
|
||||||
node: true,
|
node: true,
|
||||||
jest: true
|
|
||||||
},
|
},
|
||||||
parserOptions: {
|
/* parserOptions: {
|
||||||
parser: 'babel-eslint'
|
parser: 'babel-eslint'
|
||||||
},
|
},*/
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/],
|
||||||
extends: [
|
extends: [
|
||||||
'standard',
|
'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: [
|
settings: {
|
||||||
'jest'
|
'import/parsers': {
|
||||||
],
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
rules: {
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* rules: {
|
||||||
//'indent': [ 'error', 2 ],
|
//'indent': [ 'error', 2 ],
|
||||||
//'quotes': [ "error", "single"],
|
//'quotes': [ "error", "single"],
|
||||||
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'no-console': ['error'],
|
> 'no-console': ['error'],
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
> 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'prettier/prettier': ['error'],
|
> '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 = {
|
module.exports = {
|
||||||
verbose: true,
|
verbose: true,
|
||||||
|
preset: 'ts-jest',
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
'**/*.js',
|
'**/*.ts',
|
||||||
'!**/node_modules/**',
|
'!**/node_modules/**',
|
||||||
'!**/test/**',
|
'!**/test/**',
|
||||||
'!**/build/**',
|
'!**/build/**',
|
||||||
'!**/src/**/?(*.)+(spec|test).js?(x)'
|
'!**/src/**/?(*.)+(spec|test).ts?(x)'
|
||||||
],
|
],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 57,
|
lines: 67,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
|
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/setup.js']
|
setupFilesAfterEnv: ['<rootDir>/test/setup.ts']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "ocelot-social-backend",
|
"name": "ocelot-social-backend",
|
||||||
"version": "2.6.0",
|
"version": "2.7.0",
|
||||||
"description": "GraphQL Backend for ocelot.social",
|
"description": "GraphQL Backend for ocelot.social",
|
||||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||||
"author": "ocelot.social Community",
|
"author": "ocelot.social Community",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"main": "src/index.js",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
|
"__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations",
|
||||||
"prod:migrate": "migrate --migrations-dir ./build/db/migrations --store ./build/db/migrate/store.js",
|
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
|
||||||
"start": "node build/",
|
"start": "node build/src/",
|
||||||
"build": "tsc && ./scripts/build.copy.files.sh",
|
"build": "tsc && ./scripts/build.copy.files.sh",
|
||||||
"dev": "nodemon --exec ts-node src/ -e js,ts,gql",
|
"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",
|
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql",
|
||||||
"lint": "eslint src --config .eslintrc.js",
|
"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",
|
"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:reset": "yarn run db:clean",
|
||||||
"db:seed": "babel-node src/db/seed.js",
|
"db:seed": "ts-node src/db/seed.ts",
|
||||||
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
|
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.ts",
|
||||||
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
|
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "~7.8.4",
|
"@babel/cli": "~7.8.4",
|
||||||
@ -45,7 +45,6 @@
|
|||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"debug": "~4.1.1",
|
|
||||||
"dotenv": "~8.2.0",
|
"dotenv": "~8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graphql": "^14.6.0",
|
"graphql": "^14.6.0",
|
||||||
@ -76,7 +75,7 @@
|
|||||||
"metascraper-url": "^5.34.2",
|
"metascraper-url": "^5.34.2",
|
||||||
"metascraper-video": "^5.33.5",
|
"metascraper-video": "^5.33.5",
|
||||||
"metascraper-youtube": "^5.33.5",
|
"metascraper-youtube": "^5.33.5",
|
||||||
"migrate": "^1.7.0",
|
"migrate": "^2.0.0",
|
||||||
"mime-types": "^2.1.26",
|
"mime-types": "^2.1.26",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
@ -97,24 +96,30 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "7.6.0",
|
"@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",
|
"apollo-server-testing": "~2.11.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~6.0.5",
|
"cucumber": "~6.0.5",
|
||||||
"eslint": "~6.8.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-prettier": "~6.15.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-config-standard": "~14.1.1",
|
"eslint-config-standard": "^17.0.0",
|
||||||
"eslint-plugin-import": "~2.20.2",
|
"eslint-import-resolver-typescript": "^3.5.4",
|
||||||
"eslint-plugin-jest": "~23.8.2",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-node": "~11.1.0",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"eslint-plugin-prettier": "~3.4.1",
|
"eslint-plugin-n": "^15.7.0",
|
||||||
"eslint-plugin-promise": "~4.3.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-standard": "~4.0.1",
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"jest": "29.4",
|
"eslint-plugin-security": "^1.7.1",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"jest": "^27.2.4",
|
||||||
"nodemon": "~2.0.2",
|
"nodemon": "~2.0.2",
|
||||||
"prettier": "~2.3.2",
|
|
||||||
"rosie": "^2.0.1",
|
"rosie": "^2.0.1",
|
||||||
|
"ts-jest": "^27.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/**/fs-capacitor": "^6.2.0",
|
"**/**/fs-capacitor": "^6.2.0",
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# html files
|
# html files
|
||||||
mkdir -p build/middleware/helpers/email/templates/
|
mkdir -p build/src/middleware/helpers/email/templates/
|
||||||
cp -r src/middleware/helpers/email/templates/*.html build/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/
|
mkdir -p build/src/middleware/helpers/email/templates/en/
|
||||||
cp -r src/middleware/helpers/email/templates/en/*.html build/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/
|
mkdir -p build/src/middleware/helpers/email/templates/de/
|
||||||
cp -r src/middleware/helpers/email/templates/de/*.html build/middleware/helpers/email/templates/de/
|
cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/
|
||||||
|
|
||||||
# gql files
|
# gql files
|
||||||
mkdir -p build/schema/types/
|
mkdir -p build/src/schema/types/
|
||||||
cp -r src/schema/types/*.gql build/schema/types/
|
cp -r src/schema/types/*.gql build/src/schema/types/
|
||||||
|
|
||||||
mkdir -p build/schema/types/enum/
|
mkdir -p build/src/schema/types/enum/
|
||||||
cp -r src/schema/types/enum/*.gql build/schema/types/enum/
|
cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/
|
||||||
|
|
||||||
mkdir -p build/schema/types/scalar/
|
mkdir -p build/src/schema/types/scalar/
|
||||||
cp -r src/schema/types/scalar/*.gql build/schema/types/scalar/
|
cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/
|
||||||
|
|
||||||
mkdir -p build/schema/types/type/
|
mkdir -p build/src/schema/types/type/
|
||||||
cp -r src/schema/types/type/*.gql build/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 dotenv from 'dotenv'
|
||||||
import emails from './emails.js'
|
import emails from './emails'
|
||||||
import metadata from './metadata.js'
|
import metadata from './metadata'
|
||||||
|
|
||||||
// Load env file
|
// Load env file
|
||||||
if (require.resolve) {
|
if (require.resolve) {
|
||||||
@ -15,10 +15,11 @@ if (require.resolve) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use Cypress env or process.env
|
// 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 env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
|
||||||
|
|
||||||
const environment = {
|
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,
|
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
|
||||||
TEST: env.NODE_ENV === 'test',
|
TEST: env.NODE_ENV === 'test',
|
||||||
PRODUCTION: env.NODE_ENV === 'production',
|
PRODUCTION: env.NODE_ENV === 'production',
|
||||||
@ -90,14 +91,12 @@ const options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if all required configs are present
|
// Check if all required configs are present
|
||||||
if (require.resolve) {
|
Object.entries(required).map((entry) => {
|
||||||
// are we in a nodejs environment?
|
if (!entry[1]) {
|
||||||
Object.entries(required).map((entry) => {
|
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
||||||
if (!entry[1]) {
|
}
|
||||||
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
return entry
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...environment,
|
...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
|
// this are the paths in the webapp
|
||||||
export default {
|
export default {
|
||||||
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
|
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 {
|
export default {
|
||||||
APPLICATION_NAME: 'ocelot.social',
|
APPLICATION_NAME: 'ocelot.social',
|
||||||
APPLICATION_SHORT_NAME: 'ocelot',
|
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_MIN = 1
|
||||||
export const CATEGORIES_MAX = 3
|
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_WITHOUT_HTML_LENGTH_MIN = 50 // with removed HTML tags
|
||||||
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 250 // 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 { Factory } from 'rosie'
|
||||||
import { faker } from '@faker-js/faker'
|
import { faker } from '@faker-js/faker'
|
||||||
import { getDriver, getNeode } from './neo4j'
|
import { getDriver, getNeode } from './neo4j'
|
||||||
import CONFIG from '../config/index.js'
|
import CONFIG from '../config/index'
|
||||||
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'
|
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode'
|
||||||
|
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const uniqueImageUrl = (imageUrl) => {
|
|||||||
return newUrl.toString()
|
return newUrl.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options: any = {}) => {
|
||||||
const { driver = getDriver() } = options
|
const { driver = getDriver() } = options
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
try {
|
try {
|
||||||
@ -18,13 +18,13 @@ export function up(next) {
|
|||||||
rxSession
|
rxSession
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.pipe(
|
.pipe(
|
||||||
flatMap((txc) =>
|
flatMap((txc: any) =>
|
||||||
concat(
|
concat(
|
||||||
txc
|
txc
|
||||||
.run('MATCH (email:EmailAddress) RETURN email {.email}')
|
.run('MATCH (email:EmailAddress) RETURN email {.email}')
|
||||||
.records()
|
.records()
|
||||||
.pipe(
|
.pipe(
|
||||||
map((record) => {
|
map((record: any) => {
|
||||||
const { email } = record.get('email')
|
const { email } = record.get('email')
|
||||||
const normalizedEmail = normalizeEmail(email)
|
const normalizedEmail = normalizeEmail(email)
|
||||||
return { email, normalizedEmail }
|
return { email, normalizedEmail }
|
||||||
@ -45,7 +45,7 @@ export function up(next) {
|
|||||||
)
|
)
|
||||||
.records()
|
.records()
|
||||||
.pipe(
|
.pipe(
|
||||||
map((r) => ({
|
map((r: any) => ({
|
||||||
oldEmail: email,
|
oldEmail: email,
|
||||||
email: r.get('email'),
|
email: r.get('email'),
|
||||||
user: r.get('user'),
|
user: r.get('user'),
|
||||||
@ -12,7 +12,7 @@ export function up(next) {
|
|||||||
rxSession
|
rxSession
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.pipe(
|
.pipe(
|
||||||
flatMap((transaction) =>
|
flatMap((transaction: any) =>
|
||||||
concat(
|
concat(
|
||||||
transaction
|
transaction
|
||||||
.run(
|
.run(
|
||||||
@ -23,7 +23,7 @@ export function up(next) {
|
|||||||
)
|
)
|
||||||
.records()
|
.records()
|
||||||
.pipe(
|
.pipe(
|
||||||
map((record) => {
|
map((record: any) => {
|
||||||
const { id: locationId } = record.get('location')
|
const { id: locationId } = record.get('location')
|
||||||
return { locationId }
|
return { locationId }
|
||||||
}),
|
}),
|
||||||
@ -40,7 +40,7 @@ export function up(next) {
|
|||||||
)
|
)
|
||||||
.records()
|
.records()
|
||||||
.pipe(
|
.pipe(
|
||||||
map((record) => ({
|
map((record: any) => ({
|
||||||
location: record.get('location'),
|
location: record.get('location'),
|
||||||
updatedLocation: record.get('updatedLocation'),
|
updatedLocation: record.get('updatedLocation'),
|
||||||
})),
|
})),
|
||||||
@ -3,7 +3,7 @@ import { existsSync, createReadStream } from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { S3 } from 'aws-sdk'
|
import { S3 } from 'aws-sdk'
|
||||||
import mime from 'mime-types'
|
import mime from 'mime-types'
|
||||||
import { s3Configs } from '../../config'
|
import s3Configs from '../../config'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
|
|
||||||
export const description = `
|
export const description = `
|
||||||
@ -11,13 +11,13 @@ export async function up(next) {
|
|||||||
const transaction = session.beginTransaction()
|
const transaction = session.beginTransaction()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Implement your migration here.
|
// Those two indexes already exist
|
||||||
await transaction.run(`
|
// await transaction.run(`
|
||||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||||
`)
|
// `)
|
||||||
await transaction.run(`
|
// await transaction.run(`
|
||||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||||
`)
|
// `)
|
||||||
await transaction.run(`
|
await transaction.run(`
|
||||||
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
|
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
|
||||||
`)
|
`)
|
||||||
@ -10,7 +10,7 @@ export async function up(next) {
|
|||||||
try {
|
try {
|
||||||
// Drop indexes if they exist because due to legacy code they might be set already
|
// Drop indexes if they exist because due to legacy code they might be set already
|
||||||
const indexesResponse = await transaction.run(`CALL db.indexes()`)
|
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) {
|
if (indexes.indexOf('user_fulltext_search') > -1) {
|
||||||
await transaction.run(`CALL db.index.fulltext.drop("user_fulltext_search")`)
|
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 fieldName String
|
||||||
* @property callback Function
|
* @property callback Function
|
||||||
*/
|
*/
|
||||||
function walkRecursive(data, fields, fieldName, callback, _key) {
|
function walkRecursive(data, fields, fieldName, callback, _key?) {
|
||||||
if (!Array.isArray(fields)) {
|
if (!Array.isArray(fields)) {
|
||||||
throw new Error('please provide an fields array for the walkRecursive helper')
|
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.
|
// 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]*))$')
|
const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||||
|
|
||||||
export default function (content) {
|
export default function (content?) {
|
||||||
if (!content) return []
|
if (!content) return []
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
// 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')
|
return $(el).attr('data-hashtag-id')
|
||||||
})
|
})
|
||||||
.get()
|
.get()
|
||||||
const hashtags = []
|
const hashtags: any = []
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
const match = exec(id, regX)
|
const match = exec(id, regX)
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import CONFIG from '../../../config'
|
import CONFIG from '../../../config'
|
||||||
import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js'
|
import { cleanHtml } from '../../../middleware/helpers/cleanHtml'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { htmlToText } from 'nodemailer-html-to-text'
|
import { htmlToText } from 'nodemailer-html-to-text'
|
||||||
|
|
||||||
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
||||||
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
||||||
|
|
||||||
let sendMailCallback = async () => {}
|
let sendMailCallback: any = async () => {}
|
||||||
if (!hasEmailConfig) {
|
if (!hasEmailConfig) {
|
||||||
if (!CONFIG.TEST) {
|
if (!CONFIG.TEST) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -29,7 +29,7 @@ if (!hasEmailConfig) {
|
|||||||
cleanHtml(templateArgs.html, 'dummyKey', {
|
cleanHtml(templateArgs.html, 'dummyKey', {
|
||||||
allowedTags: ['a'],
|
allowedTags: ['a'],
|
||||||
allowedAttributes: { a: ['href'] },
|
allowedAttributes: { a: ['href'] },
|
||||||
}).replace(/&/g, '&'),
|
} as any).replace(/&/g, '&'),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import CONFIG from '../../../config'
|
import CONFIG from '../../../config'
|
||||||
import logosWebapp from '../../../config/logos.js'
|
import logosWebapp from '../../../config/logos'
|
||||||
import {
|
import {
|
||||||
signupTemplate,
|
signupTemplate,
|
||||||
emailVerificationTemplate,
|
emailVerificationTemplate,
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import mustache from 'mustache'
|
import mustache from 'mustache'
|
||||||
import CONFIG from '../../../config'
|
import CONFIG from '../../../config'
|
||||||
import metadata from '../../../config/metadata.js'
|
import metadata from '../../../config/metadata'
|
||||||
import logosWebapp from '../../../config/logos.js'
|
import logosWebapp from '../../../config/logos'
|
||||||
|
|
||||||
import * as templates from './templates'
|
import * as templates from './templates'
|
||||||
import * as templatesEN from './templates/en'
|
import * as templatesEN from './templates/en'
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import { applyMiddleware } from 'graphql-middleware'
|
import { applyMiddleware } from 'graphql-middleware'
|
||||||
import CONFIG from './../config'
|
import CONFIG from './../config'
|
||||||
|
|
||||||
import activityPub from './activityPubMiddleware'
|
|
||||||
import softDelete from './softDelete/softDeleteMiddleware'
|
import softDelete from './softDelete/softDeleteMiddleware'
|
||||||
import sluggify from './sluggifyMiddleware'
|
import sluggify from './sluggifyMiddleware'
|
||||||
import excerpt from './excerptMiddleware'
|
import excerpt from './excerptMiddleware'
|
||||||
@ -16,13 +14,13 @@ import login from './login/loginMiddleware'
|
|||||||
import sentry from './sentryMiddleware'
|
import sentry from './sentryMiddleware'
|
||||||
import languages from './languages/languages'
|
import languages from './languages/languages'
|
||||||
import userInteractions from './userInteractions'
|
import userInteractions from './userInteractions'
|
||||||
|
import chatMiddleware from './chatMiddleware'
|
||||||
|
|
||||||
export default (schema) => {
|
export default (schema) => {
|
||||||
const middlewares = {
|
const middlewares = {
|
||||||
sentry,
|
sentry,
|
||||||
permissions,
|
permissions,
|
||||||
xss,
|
xss,
|
||||||
activityPub,
|
|
||||||
validation,
|
validation,
|
||||||
sluggify,
|
sluggify,
|
||||||
excerpt,
|
excerpt,
|
||||||
@ -34,6 +32,7 @@ export default (schema) => {
|
|||||||
orderBy,
|
orderBy,
|
||||||
languages,
|
languages,
|
||||||
userInteractions,
|
userInteractions,
|
||||||
|
chatMiddleware,
|
||||||
}
|
}
|
||||||
|
|
||||||
let order = [
|
let order = [
|
||||||
@ -52,6 +51,7 @@ export default (schema) => {
|
|||||||
'softDelete',
|
'softDelete',
|
||||||
'includedFields',
|
'includedFields',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'chatMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
// add permisions middleware at the first position (unless we're seeding)
|
// add permisions middleware at the first position (unless we're seeding)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import LanguageDetect from 'languagedetect'
|
import LanguageDetect from 'languagedetect'
|
||||||
import { removeHtmlTags } from '../helpers/cleanHtml.js'
|
import { removeHtmlTags } from '../helpers/cleanHtml'
|
||||||
|
|
||||||
const setPostLanguage = (text) => {
|
const setPostLanguage = (text) => {
|
||||||
const lngDetector = new LanguageDetect()
|
const lngDetector = new LanguageDetect()
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
|
|
||||||
export default (content) => {
|
export default (content?) => {
|
||||||
if (!content) return []
|
if (!content) return []
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
const userIds = $('a.mention[data-mention-id]')
|
const userIds = $('a.mention[data-mention-id]')
|
||||||
@ -50,7 +50,7 @@ beforeAll(async () => {
|
|||||||
context: () => {
|
context: () => {
|
||||||
return {
|
return {
|
||||||
user: authenticatedUser,
|
user: authenticatedUser,
|
||||||
neode: neode,
|
neode,
|
||||||
driver,
|
driver,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -576,7 +576,7 @@ describe('notifications', () => {
|
|||||||
read: false,
|
read: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject(expected, { errors: undefined })
|
).resolves.toMatchObject({ ...expected, errors: undefined })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
|
|||||||
RETURN emailAddress {.email}
|
RETURN emailAddress {.email}
|
||||||
`
|
`
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.readTransaction(async (transaction) => {
|
||||||
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
|
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
|
||||||
notificationUserIds,
|
notificationUserIds,
|
||||||
})
|
})
|
||||||
@ -140,16 +140,18 @@ const postAuthorOfComment = async (commentId, { context }) => {
|
|||||||
|
|
||||||
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||||
const cypher = `
|
const cypher = `
|
||||||
|
MATCH (user:User { id: $userId })
|
||||||
MATCH (group:Group { id: $groupId })<-[membership:MEMBER_OF]-(owner:User)
|
MATCH (group:Group { id: $groupId })<-[membership:MEMBER_OF]-(owner:User)
|
||||||
WHERE membership.role = 'owner'
|
WHERE membership.role = 'owner'
|
||||||
WITH owner, group
|
WITH owner, group, user, membership
|
||||||
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
|
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
|
||||||
WITH group, owner, notification
|
WITH group, owner, notification, user, membership
|
||||||
SET notification.read = FALSE
|
SET notification.read = FALSE
|
||||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||||
SET notification.updatedAt = toString(datetime())
|
SET notification.updatedAt = toString(datetime())
|
||||||
SET notification.relatedUserId = $userId
|
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 session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
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 notifyMemberOfGroup = async (groupId, userId, reason, context) => {
|
||||||
const { user: owner } = context
|
const { user: owner } = context
|
||||||
const cypher = `
|
const cypher = `
|
||||||
|
MATCH (owner:User { id: $ownerId })
|
||||||
MATCH (user:User { id: $userId })
|
MATCH (user:User { id: $userId })
|
||||||
MATCH (group:Group { id: $groupId })
|
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)
|
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
WITH group, user, notification
|
WITH group, user, notification, owner, membership
|
||||||
SET notification.read = FALSE
|
SET notification.read = FALSE
|
||||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||||
SET notification.updatedAt = toString(datetime())
|
SET notification.updatedAt = toString(datetime())
|
||||||
SET notification.relatedUserId = $ownerId
|
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 session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
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)<-[:WROTE]-(author:User) | author {.*}] AS authors,
|
||||||
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
|
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
|
||||||
WITH resource, user, notification, authors, 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.read = FALSE
|
||||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||||
SET notification.updatedAt = 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 session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
@ -276,9 +282,14 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont
|
|||||||
SET notification.read = FALSE
|
SET notification.read = FALSE
|
||||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||||
SET notification.updatedAt = 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
|
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 },
|
{ 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