Merge branch 'master' into 6376-refactor-cypress-upgrad-all-relevant-packages-to-current-versions

This commit is contained in:
mahula 2023-07-17 16:22:14 +02:00 committed by GitHub
commit 2c39e8f8a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
286 changed files with 7285 additions and 4793 deletions

View File

@ -2,8 +2,58 @@ name: ocelot.social end-to-end test CI
on: push
jobs:
docker_preparation:
name: Fullstack test preparation
runs-on: ubuntu-latest
outputs:
pr-number: ${{ steps.pr.outputs.number }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: Build docker images
run: |
mkdir /tmp/images
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar
- name: Install cypress requirements
run: |
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
cd backend
yarn install
yarn build
cd ..
yarn install
- name: Get pr number
id: pr
uses: 8BitJonny/gh-get-current-pr@2.2.0
- name: Cache docker images
id: cache
uses: actions/cache/save@v3.3.1
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ steps.pr.outputs.number }}
fullstack_tests:
name: Fullstack tests
if: success()
needs: docker_preparation
runs-on: ubuntu-latest
env:
jobs: 8
@ -12,30 +62,56 @@ jobs:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Restore cache
uses: actions/cache/restore@v3.3.1
id: cache
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}
fail-on-cache-miss: true
- name: webapp | copy env file
run: cp webapp/.env.template webapp/.env
- name: backend | copy env file
run: cp backend/.env.template backend/.env
- name: boot up test system | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: cypress | Fullstack tests
id: e2e-tests
- name: Boot up test system | docker-compose
run: |
yarn install
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
chmod +x /opt/cucumber-json-formatter
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
docker load < /tmp/images/neo4j.tar
docker load < /tmp/images/backend.tar
docker load < /tmp/images/webapp.tar
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
sleep 90s
##########################################################################
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
##########################################################################
- name: Full stack tests | if any test failed, upload screenshots
- name: Full stack tests | run tests
id: e2e-tests
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
- name: Full stack tests | if tests failed, compile html report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
run: |
cd cypress/
node create-cucumber-html-report.js
- name: Full stack tests | if tests failed, upload report
id: e2e-report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: cypress/screenshots/
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
cleanup:
name: Cleanup
if: always()
needs: [docker_preparation, fullstack_tests]
runs-on: ubuntu-latest
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="e2e-preparation-cache-pr${{ needs.docker_preparation.outputs.pr-number }}"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

View File

@ -23,7 +23,7 @@ jobs:
prepare:
name: Prepare
if: needs.files-changed.outputs.webapp
if: needs.files-changed.outputs.webapp == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
@ -37,7 +37,7 @@ jobs:
build_test_webapp:
name: Docker Build Test - Webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
needs: [files-changed, prepare]
runs-on: ubuntu-latest
steps:
@ -57,7 +57,7 @@ jobs:
lint_webapp:
name: Lint Webapp
if: needs.files-changed.outputs.webapp
if: needs.files-changed.outputs.webapp == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
@ -69,7 +69,7 @@ jobs:
unit_test_webapp:
name: Unit Tests - Webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
needs: [files-changed, build_test_webapp]
runs-on: ubuntu-latest
permissions:

View File

@ -4,8 +4,68 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [2.7.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.6.0...2.7.0)
- fix(webapp): fix event teaser date from start to end by new components [`#6385`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6385)
- refactor(webapp): optimize create and update event form [`#6381`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6381)
- feat(backend): show events not ended yet [`#6405`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6405)
- feat(backend): migration to add postType property to existing posts [`#6396`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6396)
- feat(backend): seed events [`#6391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6391)
- feat(backend): seed posts as article [`#6227`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6227)
- refactor(webapp): fix coverage [`#6361`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6361)
- test(other): migrate cypress to v12 [`#6008`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6008)
- refactor(backend): copy files in external script [`#6364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6364)
- feat(webapp): alternative solution for filter and order posts [`#6367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6367)
- refactor(webapp): changed color for event-ribbon. [`#6362`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6362)
- fix(webapp): warnings in unit tests [`#6359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6359)
- Bump metascraper-title from 5.33.5 to 5.34.7 in /backend [`#6372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6372)
- Bump @babel/preset-env from 7.21.5 to 7.22.4 [`#6369`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6369)
- fix(other): typescript fix regarding dist/build folder 2 [`#6366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6366)
- fix(backend): corrected path in branded images for backend build folder(former dist) [`#6365`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6365)
- chore(other): upgrade node version in '.nvmrc' files to v20.2.0 [`#6331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6331)
- feat(backend): typescript [`#6321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6321)
- fix(webapp): fix group list number to six [`#6319`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6319)
- fix(webapp): fix notification menu comment hash [`#6335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6335)
- feat(other): 🍰 epic events master [`#6199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6199)
- fix(other): fix avatar seeding [`#6260`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6260)
- chore(other): set 'DEBUG=true' in backend '.env.template' to use GraphQL Playground [`#6333`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6333)
- refactor(other): unused packages ocelot [`#6326`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6326)
- refactor(backend): unused packages backend [`#6325`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6325)
- docs(other): add description for script usage in deployment readme [`#6329`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6329)
- fix(webapp): fix newsfeed layout [`#6154`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6154)
- fix(webapp): adds white space after user handle in comment editor [`#6308`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6308)
- Bump validator from 13.0.0 to 13.9.0 in /backend [`#6076`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6076)
- Bump metascraper-soundcloud from 5.34.2 to 5.34.4 in /backend [`#6312`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6312)
- Bump metascraper-audio from 5.33.5 to 5.34.4 in /backend [`#6287`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6287)
- Bump node from 19.9.0-alpine3.17 to 20.2.0-alpine3.17 in /backend [`#6310`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6310)
- docs(other): add missing todo in deployment readme 'TODO-next-update.md' [`#6324`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6324)
- Bump node from 20.1.0-alpine3.17 to 20.2.0-alpine3.17 in /webapp [`#6309`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6309)
- fix(backend): helmet fix [`#6318`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6318)
- fix(backend): helmet + graphiql [`#6303`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6303)
- Bump @babel/preset-env from 7.21.4 to 7.21.5 [`#6273`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6273)
- Bump date-fns from 2.25.0 to 2.30.0 [`#6283`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6283)
- Bump node from 19.9.0-alpine3.17 to 20.1.0-alpine3.17 in /webapp [`#6280`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6280)
- Bump @babel/core from 7.21.4 to 7.21.8 [`#6284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6284)
- Bump helmet from 3.22.0 to 7.0.0 in /backend [`#6296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6296)
- fix(webapp): fix z layer of header elements [`#6279`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6279)
- fix(webapp): properly render avatars in group settings [`#6289`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6289)
- fix(backend): post type on notifications [`#6257`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6257)
- fix(backend): recover missing commit [`#6262`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6262)
- feat(backend): filter posts by post type [`#6255`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6255)
- feat(backend): save location address [`#6240`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6240)
- feat(backend): add further event params [`#6231`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6231)
- feat(backend): event parameters [`#6198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6198)
- feat(backend): create and update posts with labels [`#6197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6197)
- feat(backend): add article label to posts [`#6196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6196)
- Cypress: update packaage info [`b38769b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b38769b048e9cb9ca07862a61ea810f21b4ce82a)
- update cypress related packageges in package.json [`692ec2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/692ec2a11555600647ec8d95b8296c9869948b02)
- fixed coverage reporting [`540cd40`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/540cd40e10ec0461ef17379cb93d914839f3a84f)
#### [2.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.5.1...2.6.0)
> 27 April 2023
- chore(release): v2.6.0 [`#6271`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6271)
- fix(other): docker-compose for rebranding deployment [`#6265`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6265)
- feat(webapp): default categories of group for posts in group [`#6259`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6259)
- refactor(webapp): make action radius select in group form a reusable component [`#6244`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6244)

View File

@ -1,25 +1,219 @@
module.exports = {
root: true,
env: {
es6: true,
// es6: true,
node: true,
jest: true
},
parserOptions: {
/* parserOptions: {
parser: 'babel-eslint'
},
},*/
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/],
extends: [
'standard',
'plugin:prettier/recommended'
// 'eslint:recommended',
'plugin:prettier/recommended',
// 'plugin:import/recommended',
// 'plugin:import/typescript',
// 'plugin:security/recommended',
// 'plugin:@eslint-community/eslint-comments/recommended',
],
plugins: [
'jest'
],
rules: {
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
},
node: true,
},
},
/* rules: {
//'indent': [ 'error', 2 ],
//'quotes': [ "error", "single"],
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'prettier/prettier': ['error'],
> 'no-console': ['error'],
> 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
> 'prettier/prettier': ['error'],
}, */
rules: {
'no-console': 'error',
camelcase: 'error',
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
// import
// 'import/export': 'error',
// 'import/no-deprecated': 'error',
// 'import/no-empty-named-blocks': 'error',
// 'import/no-extraneous-dependencies': 'error',
// 'import/no-mutable-exports': 'error',
// 'import/no-unused-modules': 'error',
// 'import/no-named-as-default': 'error',
// 'import/no-named-as-default-member': 'error',
// 'import/no-amd': 'error',
// 'import/no-commonjs': 'error',
// 'import/no-import-module-exports': 'error',
// 'import/no-nodejs-modules': 'off',
// 'import/unambiguous': 'error',
// 'import/default': 'error',
// 'import/named': 'error',
// 'import/namespace': 'error',
// 'import/no-absolute-path': 'error',
// 'import/no-cycle': 'error',
// 'import/no-dynamic-require': 'error',
// 'import/no-internal-modules': 'off',
// 'import/no-relative-packages': 'error',
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
// 'import/no-self-import': 'error',
// 'import/no-unresolved': 'error',
// 'import/no-useless-path-segments': 'error',
// 'import/no-webpack-loader-syntax': 'error',
// 'import/consistent-type-specifier-style': 'error',
// 'import/exports-last': 'off',
// 'import/extensions': 'error',
// 'import/first': 'error',
// 'import/group-exports': 'off',
// 'import/newline-after-import': 'error',
// 'import/no-anonymous-default-export': 'error',
// 'import/no-default-export': 'error',
// 'import/no-duplicates': 'error',
// 'import/no-named-default': 'error',
// 'import/no-namespace': 'error',
// 'import/no-unassigned-import': 'error',
// 'import/order': [
// 'error',
// {
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
// 'newlines-between': 'always',
// pathGroups: [
// {
// pattern: '@?*/**',
// group: 'external',
// position: 'after',
// },
// {
// pattern: '@/**',
// group: 'external',
// position: 'after',
// },
// ],
// alphabetize: {
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
// caseInsensitive: true /* ignore case. Options: [true, false] */,
// },
// distinctGroup: true,
// },
// ],
// 'import/prefer-default-export': 'off',
// n
// 'n/handle-callback-err': 'error',
// 'n/no-callback-literal': 'error',
// 'n/no-exports-assign': 'error',
// 'n/no-extraneous-import': 'error',
// 'n/no-extraneous-require': 'error',
// 'n/no-hide-core-modules': 'error',
// 'n/no-missing-import': 'off', // not compatible with typescript
// 'n/no-missing-require': 'error',
// 'n/no-new-require': 'error',
// 'n/no-path-concat': 'error',
// 'n/no-process-exit': 'error',
// 'n/no-unpublished-bin': 'error',
// 'n/no-unpublished-import': 'off', // TODO need to exclude seeds
// 'n/no-unpublished-require': 'error',
// 'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
// 'n/no-unsupported-features/es-builtins': 'error',
// 'n/no-unsupported-features/es-syntax': 'error',
// 'n/no-unsupported-features/node-builtins': 'error',
// 'n/process-exit-as-throw': 'error',
// 'n/shebang': 'error',
// 'n/callback-return': 'error',
// 'n/exports-style': 'error',
// 'n/file-extension-in-import': 'off',
// 'n/global-require': 'error',
// 'n/no-mixed-requires': 'error',
// 'n/no-process-env': 'error',
// 'n/no-restricted-import': 'error',
// 'n/no-restricted-require': 'error',
// 'n/no-sync': 'error',
// 'n/prefer-global/buffer': 'error',
// 'n/prefer-global/console': 'error',
// 'n/prefer-global/process': 'error',
// 'n/prefer-global/text-decoder': 'error',
// 'n/prefer-global/text-encoder': 'error',
// 'n/prefer-global/url': 'error',
// 'n/prefer-global/url-search-params': 'error',
// 'n/prefer-promises/dns': 'error',
// 'n/prefer-promises/fs': 'error',
// promise
// 'promise/catch-or-return': 'error',
// 'promise/no-return-wrap': 'error',
// 'promise/param-names': 'error',
// 'promise/always-return': 'error',
// 'promise/no-native': 'off',
// 'promise/no-nesting': 'warn',
// 'promise/no-promise-in-callback': 'warn',
// 'promise/no-callback-in-promise': 'warn',
// 'promise/avoid-new': 'warn',
// 'promise/no-new-statics': 'error',
// 'promise/no-return-in-finally': 'warn',
// 'promise/valid-params': 'warn',
// 'promise/prefer-await-to-callbacks': 'error',
// 'promise/no-multiple-resolved': 'error',
// eslint comments
// '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
// '@eslint-community/eslint-comments/no-restricted-disable': 'error',
// '@eslint-community/eslint-comments/no-use': 'off',
// '@eslint-community/eslint-comments/require-description': 'off',
},
overrides: [
// only for ts files
{
files: ['*.ts', '*.tsx'],
extends: [
// 'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
// 'plugin:@typescript-eslint/strict',
],
rules: {
// allow explicitly defined dangling promises
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
// ignore prefer-regexp-exec rule to allow string.match(regex)
'@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off',
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
'@typescript-eslint/no-unnecessary-condition': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
{
files: ['*.spec.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'error',
'jest/valid-expect': 'error',
'@typescript-eslint/unbound-method': 'off',
// 'jest/unbound-method': 'error',
},
},
],
};

View File

@ -1,18 +1,19 @@
module.exports = {
verbose: true,
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: [
'**/*.js',
'**/*.ts',
'!**/node_modules/**',
'!**/test/**',
'!**/build/**',
'!**/src/**/?(*.)+(spec|test).js?(x)'
'!**/src/**/?(*.)+(spec|test).ts?(x)'
],
coverageThreshold: {
global: {
lines: 57,
lines: 67,
},
},
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
setupFilesAfterEnv: ['<rootDir>/test/setup.js']
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
setupFilesAfterEnv: ['<rootDir>/test/setup.ts']
}

View File

@ -1,26 +1,26 @@
{
"name": "ocelot-social-backend",
"version": "2.6.0",
"version": "2.7.0",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
"license": "MIT",
"private": false,
"main": "src/index.js",
"main": "src/index.ts",
"scripts": {
"__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
"prod:migrate": "migrate --migrations-dir ./build/db/migrations --store ./build/db/migrate/store.js",
"start": "node build/",
"__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations",
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
"start": "node build/src/",
"build": "tsc && ./scripts/build.copy.files.sh",
"dev": "nodemon --exec ts-node src/ -e js,ts,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql",
"lint": "eslint --max-warnings=0 --ext .js,.ts ./src",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
"db:clean": "babel-node src/db/clean.js",
"db:clean": "ts-node src/db/clean.ts",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
"db:seed": "ts-node src/db/seed.ts",
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.ts",
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create"
},
"dependencies": {
"@babel/cli": "~7.8.4",
@ -45,7 +45,6 @@
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~7.0.3",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"graphql": "^14.6.0",
@ -76,7 +75,7 @@
"metascraper-url": "^5.34.2",
"metascraper-video": "^5.33.5",
"metascraper-youtube": "^5.33.5",
"migrate": "^1.7.0",
"migrate": "^2.0.0",
"mime-types": "^2.1.26",
"minimatch": "^3.0.4",
"mustache": "^4.2.0",
@ -97,24 +96,30 @@
},
"devDependencies": {
"@faker-js/faker": "7.6.0",
"@types/jest": "^27.0.2",
"@types/node": "^20.2.5",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"apollo-server-testing": "~2.11.0",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
"eslint": "~6.8.0",
"eslint-config-prettier": "~6.15.0",
"eslint-config-standard": "~14.1.1",
"eslint-plugin-import": "~2.20.2",
"eslint-plugin-jest": "~23.8.2",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-prettier": "~3.4.1",
"eslint-plugin-promise": "~4.3.1",
"eslint-plugin-standard": "~4.0.1",
"jest": "29.4",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-import-resolver-typescript": "^3.5.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^1.7.1",
"prettier": "^2.8.7",
"jest": "^27.2.4",
"nodemon": "~2.0.2",
"prettier": "~2.3.2",
"rosie": "^2.0.1",
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
"typescript": "^4.9.4"
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",

View File

@ -1,24 +1,24 @@
#!/bin/sh
# html files
mkdir -p build/middleware/helpers/email/templates/
cp -r src/middleware/helpers/email/templates/*.html build/middleware/helpers/email/templates/
mkdir -p build/src/middleware/helpers/email/templates/
cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/
mkdir -p build/middleware/helpers/email/templates/en/
cp -r src/middleware/helpers/email/templates/en/*.html build/middleware/helpers/email/templates/en/
mkdir -p build/src/middleware/helpers/email/templates/en/
cp -r src/middleware/helpers/email/templates/en/*.html build/src/middleware/helpers/email/templates/en/
mkdir -p build/middleware/helpers/email/templates/de/
cp -r src/middleware/helpers/email/templates/de/*.html build/middleware/helpers/email/templates/de/
mkdir -p build/src/middleware/helpers/email/templates/de/
cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/
# gql files
mkdir -p build/schema/types/
cp -r src/schema/types/*.gql build/schema/types/
mkdir -p build/src/schema/types/
cp -r src/schema/types/*.gql build/src/schema/types/
mkdir -p build/schema/types/enum/
cp -r src/schema/types/enum/*.gql build/schema/types/enum/
mkdir -p build/src/schema/types/enum/
cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/
mkdir -p build/schema/types/scalar/
cp -r src/schema/types/scalar/*.gql build/schema/types/scalar/
mkdir -p build/src/schema/types/scalar/
cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/
mkdir -p build/schema/types/type/
cp -r src/schema/types/type/*.gql build/schema/types/type/
mkdir -p build/src/schema/types/type/
cp -r src/schema/types/type/*.gql build/src/schema/types/type/

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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}`,
})
})
})
})
})
})

View File

@ -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)
})
})
})
})

View File

@ -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',
]

View File

@ -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')
)
}

View File

@ -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,
},
}
}

View File

@ -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()
})
}

View File

@ -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()
}
},
)
}
})
})
}

View File

@ -1,6 +1,6 @@
import dotenv from 'dotenv'
import emails from './emails.js'
import metadata from './metadata.js'
import emails from './emails'
import metadata from './metadata'
// Load env file
if (require.resolve) {
@ -15,10 +15,11 @@ if (require.resolve) {
}
// Use Cypress env or process.env
declare let Cypress: any | undefined
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
const environment = {
NODE_ENV: env.NODE_ENV || process.NODE_ENV,
NODE_ENV: env.NODE_ENV || process.env.NODE_ENV,
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
TEST: env.NODE_ENV === 'test',
PRODUCTION: env.NODE_ENV === 'production',
@ -90,14 +91,12 @@ const options = {
}
// Check if all required configs are present
if (require.resolve) {
// are we in a nodejs environment?
Object.entries(required).map((entry) => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
}
Object.entries(required).map((entry) => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
return entry
})
export default {
...environment,

View File

@ -1,4 +1,4 @@
// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
// this file is duplicated in `backend/src/config/logos` and `webapp/constants/logos.js` and replaced on rebranding
// this are the paths in the webapp
export default {
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',

View File

@ -1,4 +1,4 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` and replaced on rebranding
// this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` and replaced on rebranding
export default {
APPLICATION_NAME: 'ocelot.social',
APPLICATION_SHORT_NAME: 'ocelot',

View File

@ -1,4 +1,4 @@
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
// this file is duplicated in `backend/src/constants/metadata` and `webapp/constants/metadata.js`
export const CATEGORIES_MIN = 1
export const CATEGORIES_MAX = 3

View File

@ -1,3 +1,3 @@
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
// this file is duplicated in `backend/src/constants/group` and `webapp/constants/group.js`
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 50 // with removed HTML tags
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 250 // with removed HTML tags

View File

@ -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,
}

View 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,
}

View File

@ -0,0 +1,2 @@
const tsNode = require('ts-node')
module.exports = tsNode.register

View File

@ -4,8 +4,8 @@ import { hashSync } from 'bcryptjs'
import { Factory } from 'rosie'
import { faker } from '@faker-js/faker'
import { getDriver, getNeode } from './neo4j'
import CONFIG from '../config/index.js'
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'
import CONFIG from '../config/index'
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode'
const neode = getNeode()
@ -15,7 +15,7 @@ const uniqueImageUrl = (imageUrl) => {
return newUrl.toString()
}
export const cleanDatabase = async (options = {}) => {
export const cleanDatabase = async (options: any = {}) => {
const { driver = getDriver() } = options
const session = driver.session()
try {

View File

@ -18,13 +18,13 @@ export function up(next) {
rxSession
.beginTransaction()
.pipe(
flatMap((txc) =>
flatMap((txc: any) =>
concat(
txc
.run('MATCH (email:EmailAddress) RETURN email {.email}')
.records()
.pipe(
map((record) => {
map((record: any) => {
const { email } = record.get('email')
const normalizedEmail = normalizeEmail(email)
return { email, normalizedEmail }
@ -45,7 +45,7 @@ export function up(next) {
)
.records()
.pipe(
map((r) => ({
map((r: any) => ({
oldEmail: email,
email: r.get('email'),
user: r.get('user'),

View File

@ -12,7 +12,7 @@ export function up(next) {
rxSession
.beginTransaction()
.pipe(
flatMap((transaction) =>
flatMap((transaction: any) =>
concat(
transaction
.run(
@ -23,7 +23,7 @@ export function up(next) {
)
.records()
.pipe(
map((record) => {
map((record: any) => {
const { id: locationId } = record.get('location')
return { locationId }
}),
@ -40,7 +40,7 @@ export function up(next) {
)
.records()
.pipe(
map((record) => ({
map((record: any) => ({
location: record.get('location'),
updatedLocation: record.get('updatedLocation'),
})),

View File

@ -3,7 +3,7 @@ import { existsSync, createReadStream } from 'fs'
import path from 'path'
import { S3 } from 'aws-sdk'
import mime from 'mime-types'
import { s3Configs } from '../../config'
import s3Configs from '../../config'
import https from 'https'
export const description = `

View File

@ -11,13 +11,13 @@ export async function up(next) {
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(`
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
`)
await transaction.run(`
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
`)
// Those two indexes already exist
// await transaction.run(`
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
// `)
// await transaction.run(`
// CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
// `)
await transaction.run(`
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
`)

View File

@ -10,7 +10,7 @@ export async function up(next) {
try {
// Drop indexes if they exist because due to legacy code they might be set already
const indexesResponse = await transaction.run(`CALL db.indexes()`)
const indexes = indexesResponse.records.map((record) => record.get('indexName'))
const indexes = indexesResponse.records.map((record) => record.get('name'))
if (indexes.indexOf('user_fulltext_search') > -1) {
await transaction.run(`CALL db.index.fulltext.drop("user_fulltext_search")`)
}

View File

@ -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

View 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)
}
`
}

View 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
}
`
}

View File

@ -5,7 +5,7 @@
* @property fieldName String
* @property callback Function
*/
function walkRecursive(data, fields, fieldName, callback, _key) {
function walkRecursive(data, fields, fieldName, callback, _key?) {
if (!Array.isArray(fields)) {
throw new Error('please provide an fields array for the walkRecursive helper')
}

View File

@ -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)
},
},
}

View 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,
},
}

View File

@ -8,7 +8,7 @@ import { exec, build } from 'xregexp/xregexp-all.js'
// 2. If it starts with a digit '0-9' than a unicode letter has to follow.
const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
export default function (content) {
export default function (content?) {
if (!content) return []
const $ = cheerio.load(content)
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
@ -18,7 +18,7 @@ export default function (content) {
return $(el).attr('data-hashtag-id')
})
.get()
const hashtags = []
const hashtags: any = []
ids.forEach((id) => {
const match = exec(id, regX)
if (match != null) {

View File

@ -1,12 +1,12 @@
import CONFIG from '../../../config'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
let sendMailCallback = async () => {}
let sendMailCallback: any = async () => {}
if (!hasEmailConfig) {
if (!CONFIG.TEST) {
// eslint-disable-next-line no-console
@ -29,7 +29,7 @@ if (!hasEmailConfig) {
cleanHtml(templateArgs.html, 'dummyKey', {
allowedTags: ['a'],
allowedAttributes: { a: ['href'] },
}).replace(/&amp;/g, '&'),
} as any).replace(/&amp;/g, '&'),
)
}
}

View File

@ -1,5 +1,5 @@
import CONFIG from '../../../config'
import logosWebapp from '../../../config/logos.js'
import logosWebapp from '../../../config/logos'
import {
signupTemplate,
emailVerificationTemplate,

View File

@ -1,7 +1,7 @@
import mustache from 'mustache'
import CONFIG from '../../../config'
import metadata from '../../../config/metadata.js'
import logosWebapp from '../../../config/logos.js'
import metadata from '../../../config/metadata'
import logosWebapp from '../../../config/logos'
import * as templates from './templates'
import * as templatesEN from './templates/en'

View File

@ -1,7 +1,5 @@
import { applyMiddleware } from 'graphql-middleware'
import CONFIG from './../config'
import activityPub from './activityPubMiddleware'
import softDelete from './softDelete/softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import excerpt from './excerptMiddleware'
@ -16,13 +14,13 @@ import login from './login/loginMiddleware'
import sentry from './sentryMiddleware'
import languages from './languages/languages'
import userInteractions from './userInteractions'
import chatMiddleware from './chatMiddleware'
export default (schema) => {
const middlewares = {
sentry,
permissions,
xss,
activityPub,
validation,
sluggify,
excerpt,
@ -34,6 +32,7 @@ export default (schema) => {
orderBy,
languages,
userInteractions,
chatMiddleware,
}
let order = [
@ -52,6 +51,7 @@ export default (schema) => {
'softDelete',
'includedFields',
'orderBy',
'chatMiddleware',
]
// add permisions middleware at the first position (unless we're seeding)

View File

@ -1,5 +1,5 @@
import LanguageDetect from 'languagedetect'
import { removeHtmlTags } from '../helpers/cleanHtml.js'
import { removeHtmlTags } from '../helpers/cleanHtml'
const setPostLanguage = (text) => {
const lngDetector = new LanguageDetect()

View File

@ -1,6 +1,6 @@
import cheerio from 'cheerio'
export default (content) => {
export default (content?) => {
if (!content) return []
const $ = cheerio.load(content)
const userIds = $('a.mention[data-mention-id]')

View File

@ -50,7 +50,7 @@ beforeAll(async () => {
context: () => {
return {
user: authenticatedUser,
neode: neode,
neode,
driver,
}
},
@ -576,7 +576,7 @@ describe('notifications', () => {
read: false,
},
}),
).resolves.toMatchObject(expected, { errors: undefined })
).resolves.toMatchObject({ ...expected, errors: undefined })
})
})

View File

@ -15,7 +15,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
RETURN emailAddress {.email}
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const writeTxResultPromise = session.readTransaction(async (transaction) => {
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
notificationUserIds,
})
@ -140,16 +140,18 @@ const postAuthorOfComment = async (commentId, { context }) => {
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
const cypher = `
MATCH (user:User { id: $userId })
MATCH (group:Group { id: $groupId })<-[membership:MEMBER_OF]-(owner:User)
WHERE membership.role = 'owner'
WITH owner, group
WITH owner, group, user, membership
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
WITH group, owner, notification
WITH group, owner, notification, user, membership
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
SET notification.relatedUserId = $userId
RETURN notification {.*, from: group, to: properties(owner)}
WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification
RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) }
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -173,16 +175,20 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
const notifyMemberOfGroup = async (groupId, userId, reason, context) => {
const { user: owner } = context
const cypher = `
MATCH (owner:User { id: $ownerId })
MATCH (user:User { id: $userId })
MATCH (group:Group { id: $groupId })
WITH user, group
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group)
WITH user, group, owner, membership
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH group, user, notification
WITH group, user, notification, owner, membership
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
SET notification.relatedUserId = $ownerId
RETURN notification {.*, from: group, to: properties(user)}
WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup,
notification, user, owner
RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) }
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -238,11 +244,11 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
WITH resource, user, notification, authors, posts,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
RETURN notification {.*, from: finalResource, to: properties(user)}
RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) }
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -276,9 +282,14 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont
SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime())
WITH notification, postAuthor, post,
WITH notification, postAuthor, post, commenter,
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
RETURN notification {.*, from: finalResource, to: properties(postAuthor)}
RETURN notification {
.*,
from: finalResource,
to: properties(postAuthor),
relatedUser: properties(commenter)
}
`,
{ commentId, postAuthorId, reason },
)

Some files were not shown because too many files have changed in this diff Show More