Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 6179-add-script-to-set-neo4j-in-offline-mode-and-back

This commit is contained in:
Wolfgang Huß 2023-10-25 13:19:12 +02:00
commit 202353bcb4
689 changed files with 19858 additions and 17439 deletions

View File

@ -1,157 +0,0 @@
codecov:
#token: uuid # Your private repository token
#url: "http" # for Codecov Enterprise customers
#slug: "owner/repo" # for Codecov Enterprise customers
#branch: master # override the default branch
#bot: username # set user whom will be the consumer of oauth requests
#ci: # Custom CI domains if Codecov does not identify them automatically
# - ci.domain.com
# - !provider # ignore these providers when checking if CI passed
# # ex. You may test on Travis, Circle, and AppVeyor, but only need
# # to check if Travis passes. Therefore add: !circle and !appveyor
notify:
#after_n_builds: null # number of expected builds to recieve before sending notifications
# # after: check ci status unless disabled via require_ci_to_pass
require_ci_to_pass: yes # yes: will delay sending notifications until all ci is finished
# no: will send notifications without checking ci status and wait till "after_n_builds" are uploaded
#countdown: null # number of seconds to wait before first ci build check
#delay: null # number of seconds to wait between ci build checks
coverage:
precision: 2 # 2 = xx.xx%, 0 = xx%
round: nearest # down|up|nearest - default down
# range: 50...60 # default 70...90. red...green
#notify:
# irc:
# default:
# server: "chat.freenode.net"|encrypted
# branches: null # all branches by default
# threshold: 1%
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# flags: null
# paths: null
#
# slack:
# default:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# attachments: "sunburst, diff"
# only_pulls: false
# flags: null
# paths: null
#
# email:
# default:
# to:
# - example@domain.com
# - &author
# threshold: 1%
# only_pulls: false
# layout: header, diff, trends
# flags: null
# paths: null
#
# hipchat:
# default:
# url: "http"|encrypted
# room: name|id
# threshold: 1%
# token: encrypted
# branches: null # all branches by default
# notify: false # if the hipchat message is silent or loud (default false)
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
# flags: null
# paths: null
#
# gitter:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
# message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message
#
# webhooks:
# _name_:
# url: "http"|encrypted
# threshold: 1%
# branches: null # all branches by default
status:
project:
default: false # disable the default status that measures entire project
backend: # declare a new status context "backend"
against: parent
target: auto
threshold: null
#threshold: 1%
base: auto
if_no_uploads: error
if_not_found: success
if_ci_failed: error
only_pulls: false
#branches:
# - master
#flags:
# - integration
paths:
- backend/ # only include coverage in "backend/" folder
webapp: # declare a new status context "frontend"
against: parent
target: auto
threshold: null
#threshold: 1%
base: auto
if_no_uploads: error
if_not_found: success
if_ci_failed: error
only_pulls: false
#branches:
# - master
#flags:
# - integration
paths:
- webapp/ # only include coverage in "webapp/" folder
patch:
default: false
# against: parent
# target: 80%
# branches: null
# if_no_uploads: success
# if_not_found: success
# if_ci_failed: error
# only_pulls: false
# flags:
# - integration
# paths:
# - folder
#changes:
# default:
# against: parent
# branches: null
# if_no_uploads: error
# if_not_found: success
# if_ci_failed: error
# only_pulls: false
# flags:
# - integration
# paths:
# - folder
#flags:
# integration:
# branches:
# - master
# ignore:
# - app/ui
#ignore: # files and folders for processing
# - tests/*
#fixes:
# - "old_path::new_path"
comment: off

13
.github/file-filters.yml vendored Normal file
View File

@ -0,0 +1,13 @@
backend: &backend
- '.github/workflows/test-backend.yml'
- 'backend/**/*'
- 'neo4j/**/*'
- 'package.json'
docker: &docker
- 'docker-compose.*'
webapp: &webapp
- '.github/workflows/test-webapp.yml'
- 'webapp/**/*'
- 'package.json'

View File

@ -0,0 +1,42 @@
###############################################################################
# A Github repo has max 10 GB of cache.
# https://github.blog/changelog/2021-11-23-github-actions-cache-size-is-now-increased-to-10gb-per-repository/
#
# To avoid "cache thrashing" by their cache eviction policy it is recommended
# to apply a cache cleanup workflow at PR closing to dele cache leftovers of
# the current branch:
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
###############################################################################
name: ocelot.social cache cleanup on pr closing
on:
pull_request:
types:
- closed
jobs:
clean-branch-cache:
name: Cleanup branch cache
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
@ -96,7 +96,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
@ -145,7 +145,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
@ -243,7 +243,7 @@ jobs:
needs: [upload_to_dockerhub]
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full History for changelog
- name: Setup env
@ -283,7 +283,7 @@ jobs:
needs: [github_tag]
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full History for changelog
- name: Setup env
@ -306,4 +306,12 @@ jobs:
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
event-type: trigger-ocelot-build-success
repository: 'Ocelot-Social-Community/stage.ocelot.social'
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
- name: Repository Dispatch stage.yunite.me
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository
event-type: trigger-ocelot-build-success
repository: 'Yunite-Net/stage.yunite.me'
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'

141
.github/workflows/test-backend.yml vendored Normal file
View File

@ -0,0 +1,141 @@
name: ocelot.social backend test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Backend
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.changes.outputs.backend }}
docker: ${{ steps.changes.outputs.docker }}
steps:
- uses: actions/checkout@v4
- name: Check for backend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test_neo4j:
name: Docker Build Test - Neo4J
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Neo4J | Build 'community' image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
- name: Cache docker images
id: cache-neo4j
uses: actions/cache/save@v3.3.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
build_test_backend:
name: Docker Build Test - Backend
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: backend | Build 'test' image
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
- name: Cache docker images
id: cache-backend
uses: actions/cache/save@v3.3.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
lint_backend:
name: Lint Backend
if: needs.files-changed.outputs.backend == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: backend | Lint
run: cd backend && yarn && yarn run lint
unit_test_backend:
name: Unit tests - Backend
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true'
needs: [files-changed, build_test_neo4j, build_test_backend]
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Restore Neo4J cache
uses: actions/cache/restore@v3.3.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
fail-on-cache-miss: true
- name: Restore Backend cache
uses: actions/cache/restore@v3.3.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
fail-on-cache-miss: true
- name: Load Docker Images
run: |
docker load < /tmp/neo4j.tar
docker load < /tmp/backend.tar
- name: backend | copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
- name: backend | Initialize Database
run: docker-compose exec -T backend yarn db:migrate init
- name: backend | Migrate Database Up
run: docker-compose exec -T backend yarn db:migrate up
- name: backend | Unit test incl. coverage check
run: docker-compose exec -T backend yarn test
cleanup:
name: Cleanup
if: ${{ needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' }}
needs: [files-changed, unit_test_backend]
runs-on: ubuntu-latest
permissions: write-all
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-backend-neo4j-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
KEY="${{ github.run_id }}-backend-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

113
.github/workflows/test-e2e.yml vendored Normal file
View File

@ -0,0 +1,113 @@
name: ocelot.social end-to-end test CI
on: push
jobs:
docker_preparation:
name: Fullstack test preparation
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- 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: Cache docker images
id: cache
uses: actions/cache/save@v3.3.2
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: ${{ github.run_id }}-e2e-preparation-cache
fullstack_tests:
name: Fullstack tests
if: success()
needs: docker_preparation
runs-on: ubuntu-latest
env:
jobs: 8
strategy:
matrix:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Restore cache
uses: actions/cache/restore@v3.3.2
id: cache
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: ${{ github.run_id }}-e2e-preparation-cache
fail-on-cache-miss: true
- name: Boot up test system | docker-compose
run: |
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
- 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: 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
needs: [docker_preparation, fullstack_tests]
runs-on: ubuntu-latest
permissions: write-all
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-e2e-preparation-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

116
.github/workflows/test-webapp.yml vendored Normal file
View File

@ -0,0 +1,116 @@
name: ocelot.social webapp test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Webapp
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.changes.outputs.docker }}
webapp: ${{ steps.changes.outputs.webapp }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
prepare:
name: Prepare
if: needs.files-changed.outputs.webapp == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check translation files
run: |
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
build_test_webapp:
name: Docker Build Test - Webapp
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
needs: [files-changed, prepare]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Webapp | Build 'test' image
run: |
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Cache docker image
uses: actions/cache/save@v3.3.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
lint_webapp:
name: Lint Webapp
if: needs.files-changed.outputs.webapp == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: webapp | Lint
run: cd webapp && yarn && yarn run lint
unit_test_webapp:
name: Unit Tests - 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:
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Restore webapp cache
uses: actions/cache/restore@v3.3.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
- name: Copy env files
run: |
cp webapp/.env.template webapp/.env
cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
- name: webapp | Unit tests incl. coverage check
run: docker-compose exec -T webapp yarn test
cleanup:
name: Cleanup
if: ${{ needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' }}
needs: [files-changed, unit_test_webapp]
runs-on: ubuntu-latest
permissions: write-all
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-webapp-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm

View File

@ -1,344 +0,0 @@
name: ocelot.social test CI
on: [push]
jobs:
##############################################################################
# JOB: PREPARE #####################################################
##############################################################################
prepare:
name: Prepare
runs-on: ubuntu-latest
# needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# TODO: DO STUFF ??? #####################################################
##########################################################################
- name: Check translation files
run: |
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
##############################################################################
# JOB: DOCKER BUILD TEST NEO4J ###############################################
##############################################################################
build_test_neo4j:
name: Docker Build Test - Neo4J
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# NEO4J ##################################################################
##########################################################################
- name: Neo4J | Build `community` image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-neo4j-image
path: /tmp/neo4j.tar
##############################################################################
# JOB: DOCKER BUILD TEST BACKEND #############################################
##############################################################################
build_test_backend:
name: Docker Build Test - Backend
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD BACKEND DOCKER IMAGE (build) #####################################
##########################################################################
- name: backend | Build `test` image
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-backend-test
path: /tmp/backend.tar
##############################################################################
# JOB: DOCKER BUILD TEST WEBAPP ##############################################
##############################################################################
build_test_webapp:
name: Docker Build Test - WebApp
runs-on: ubuntu-latest
needs: [prepare]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
##########################################################################
- name: webapp | Build `test` image
run: |
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-webapp-test
path: /tmp/webapp.tar
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################
lint_backend:
name: Lint backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: docker run --rm ocelotsocialnetwork/backend:test yarn run lint
##############################################################################
# JOB: LINT WEBAPP ###########################################################
##############################################################################
lint_webapp:
name: Lint webapp
runs-on: ubuntu-latest
needs: [build_test_webapp]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Webapp)
uses: actions/download-artifact@v3
with:
name: docker-webapp-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
# LINT WEBAPP ############################################################
##########################################################################
- name: webapp | Lint
run: docker run --rm ocelotsocialnetwork/webapp:test yarn run lint
##############################################################################
# JOB: UNIT TEST BACKEND #####################################################
##############################################################################
unit_test_backend:
name: Unit tests - backend
runs-on: ubuntu-latest
needs: [build_test_neo4j,build_test_backend]
permissions:
checks: write
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Neo4J)
uses: actions/download-artifact@v3
with:
name: docker-neo4j-image
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
- name: backend | Initialize Database
run: docker-compose exec -T backend yarn db:migrate init
- name: backend | Migrate Database Up
run: docker-compose exec -T backend yarn db:migrate up
- name: backend | Unit test
run: docker-compose exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################
- name: backend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Backend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 57
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST WEBAPP ######################################################
##############################################################################
unit_test_webapp:
name: Unit tests - webapp
runs-on: ubuntu-latest
needs: [build_test_webapp]
permissions:
checks: write
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Webapp)
uses: actions/download-artifact@v3
with:
name: docker-webapp-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
# UNIT TESTS WEBAPP ######################################################
##########################################################################
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
- name: webapp | Unit tests
run: docker-compose exec -T webapp yarn test
##########################################################################
# COVERAGE REPORT FRONTEND ################################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
##########################################################################
# COVERAGE CHECK WEBAPP ##################################################
##########################################################################
- name: webapp | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Webapp
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 83
token: ${{ github.token }}
##############################################################################
# JOB: FULLSTACK TESTS #######################################################
##############################################################################
fullstack_tests:
name: Fullstack tests
runs-on: ubuntu-latest
needs: [build_test_webapp, build_test_backend, build_test_neo4j]
env:
jobs: 8
strategy:
matrix:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Neo4J)
uses: actions/download-artifact@v3
with:
name: docker-neo4j-image
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Webapp)
uses: actions/download-artifact@v3
with:
name: docker-webapp-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
# FULLSTACK TESTS CYPRESS ################################################
##########################################################################
- name: webapp | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
run: cp backend/.env.template backend/.env
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
- name: cypress | Fullstack tests
id: e2e-tests
run: |
yarn install
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
##########################################################################
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
##########################################################################
- name: Full stack tests | if any test failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: cypress/screenshots/

2
.nvmrc
View File

@ -1 +1 @@
v19.4.0
v20.2.0

View File

@ -1,84 +0,0 @@
dist: xenial
language: node_js
node_js: lts/*
cache:
yarn: false
npm: false
addons:
apt:
packages:
- libgconf-2-4
snaps:
- docker
firefox: "latest-esr"
install:
- yarn global add wait-on
# Install Codecov
- yarn install --frozen-lockfile
- cp backend/.env.template backend/.env
before_script:
- docker-compose -f docker-compose.yml build --parallel
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
- wait-on http://localhost:7474
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec backend yarn run db:migrate init
script:
- export CYPRESS_RETRIES=1
- export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
- echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
# Miscellaneous
- ./scripts/translations/sort.sh
- ./scripts/translations/missing-keys.sh
# Backend
- docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
- docker-compose exec backend yarn run db:seed
- docker-compose exec backend yarn run db:reset
# Frontend
- docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
# Fullstack
- docker-compose down
- docker-compose -f docker-compose.yml up -d
- wait-on http://localhost:7474
# disable for last deploy, because of flakiness!
# - yarn run cypress:run --record
# - yarn run cucumber
# Coverage
# disable this uneffective thing for last deploy, because of easyness!
# - yarn run codecov
after_success:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
- chmod +x send.sh
- ./send.sh success $WEBHOOK_URL
- if [ $TRAVIS_BRANCH == "master" ] && [ $TRAVIS_EVENT_TYPE == "push" ]; then
wget https://raw.githubusercontent.com/Human-Connection/Discord-Bot/develop/tester.sh &&
chmod +x tester.sh &&
./tester.sh staging $WEBHOOK_URL;
fi
after_failure:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
- chmod +x send.sh
- ./send.sh failure $WEBHOOK_URL
before_deploy:
- go get -u github.com/tcnksm/ghr
# stop deployment to kubernetes until we have set it up
# - ./scripts/setup_kubernetes.sh
deploy:
- provider: script
script: bash scripts/docker_push.sh
on:
branch: master
# stop deployment to kubernetes until we have set it up
# - provider: script
# script: bash scripts/deploy.sh
# on:
# branch: master

View File

@ -4,8 +4,310 @@ 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).
#### [3.1.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.1.0...3.1.2)
- fix(other): kubernetes error by degrading node version from v20.7.0 to v20.2.0 [`#6779`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6779)
- fix(backend): cypher statement in user locales unit test [`#6780`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6780)
- chore(release): release v3.1.1 move dkim to secrets and fix search etc. [`#6763`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6763)
- chore(other): move kubrnetes dkim from confmap to secret yaml [`#6761`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6761)
- build(other): bump @babel/preset-env from 7.22.9 to 7.22.20 in /backend [`#6741`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6741)
- build(other): bump @babel/register from 7.22.5 to 7.22.15 [`#6745`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6745)
- chore(other): add tail command as template comment to `docker-compose.yml` [`#6755`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6755)
- chore(other): upgrade node version in '.nvmrc' files to v20.6.0 [`#6758`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6758)
- build(other): bump @babel/plugin-proposal-throw-expressions from 7.8.3 to 7.22.5 in /backend [`#6742`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6742)
- build(other): bump @faker-js/faker from 8.0.2 to 8.1.0 [`#6743`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6743)
- fix(webapp): chat-only show counter if unread messages exist [`#6752`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6752)
- build(other): bump node from 20.6.0-alpine3.17 to 20.7.0-alpine3.17 in /webapp [`#6749`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6749)
- build(other): bump node from 20.6.0-alpine3.17 to 20.7.0-alpine3.17 in /backend [`#6748`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6748)
- fix(backend): post type as array in search posts query [`#6756`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6756)
- refactor(backend): remove redundant package metascraper-audio from backend [`#6724`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6724)
- test(other): update cypress related packages [`#6382`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6382)
- build(other): bump @storybook/vue from 6.3.6 to 7.4.0 in /webapp [`#6708`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6708)
- build(other): bump chai from 4.2.0 to 4.3.8 in /backend [`#6738`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6738)
- build(other): bump eslint-config-prettier from 8.8.0 to 9.0.0 in /backend [`#6737`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6737)
- build(other): bump slug from 6.1.0 to 8.2.3 [`#6716`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6716)
- build(other): bump jsonwebtoken from 8.5.1 to 9.0.2 [`#6707`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6707)
- build(other): bump expect from 29.6.1 to 29.6.4 [`#6699`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6699)
- build(other): bump node from 20.2.0-alpine3.17 to 20.6.0-alpine3.17 in /webapp [`#6709`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6709)
- build(other): bump node from 20.2.0-alpine3.17 to 20.6.0-alpine3.17 in /backend [`#6710`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6710)
- build(other): bump actions/cache from 3.3.1 to 3.3.2 [`#6714`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6714)
- build(other): bump actions/checkout from 3 to 4 [`#6713`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6713)
- Bump @storybook/vue from 6.3.6 to 7.4.0 in /webapp [`8113d47`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8113d47dfcbfa0a63ae1f035d9d24a4c548d2089)
- Revert "update cypress packages" [`2229baf`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2229baff6fb74d92ed395fb90a1421f994368a20)
- update cypress packages [`dbedcf9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dbedcf901d3040d1a6a8db62170bdea86d020bb1)
#### [3.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.4...3.1.0)
> 13 September 2023
- chore(release): release v3.1.0 use of dkim in deployment [`#6733`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6733)
- chore(other): use dkim values in helm script of backend [`#6731`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6731)
- fix(other): add global package.json to workflow file filters [`#6706`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6706)
- Release v3.1.0 [`6276d87`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/6276d87c6a427438c5fbf56c0f7bc414293e671d)
- Fix super fluid spaces [`ecdaf83`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ecdaf83ac57bd4186f064d891d2570ce2438d47f)
- Use DKIM values in Helm script of backend [`2ea98b1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2ea98b1900c1b33712a2456dde6ba3dfb6d95b00)
#### [3.0.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.3...3.0.4)
> 12 September 2023
- chore(other): release v3.0.4 fix group link on map, embed backend shot down and bug on post language detect [`#6725`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6725)
- fix(other): fix backup script for neo4j v4 [`#6662`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6662)
- fix(other): fix workflow cache cleanup fail [`#6722`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6722)
- fix(backend): fix post language detection [`#6720`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6720)
- fix(webapp): add jsconfig.json to navigate in webapp. [`#6645`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6645)
- fix(backend): metascraper crash [`#6704`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6704)
- build(other): bump aws-sdk from 2.652.0 to 2.1425.0 in /backend [`#6651`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6651)
- build(other): bump @babel/preset-env from 7.9.5 to 7.22.9 in /backend [`#6578`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6578)
- build(other): bump @babel/preset-env from 7.22.7 to 7.22.9 [`#6572`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6572)
- build(other): bump dotenv from 8.6.0 to 16.3.1 [`#6483`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6483)
- Bump @babel/preset-env from 7.9.5 to 7.22.9 in /backend [`ae0c6f1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ae0c6f12bd86c39b33a2b48c2eeb0eccb880821c)
- Revert "Revert "Revert "update metascraper packages""" [`7fdc5e8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7fdc5e8f5ed8e191763acfcd74c510138145f612)
- Revert "Revert "update metascraper packages"" [`d5c1421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d5c142129291264d5508b3f32b8500c2451a5f39)
#### [3.0.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.2...3.0.3)
> 4 September 2023
- chore(release): release v3.0.3 fix chat avatar error, adjust layout of filter and enable e-mail dkim [`#6702`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6702)
- build(other): bump @babel/core from 7.22.8 to 7.22.9 [`#6573`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6573)
- feat(backend): implement dkim config for nodemailer [`#6692`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6692)
- fix(webapp): filtermenu mobile bug [`#6694`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6694)
- fix(webapp): fix proxyapiurl in chat rooms for the avatars [`#6693`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6693)
- fix(webapp): fix the group link in the map [`#6698`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6698)
- Bump @babel/core from 7.22.8 to 7.22.9 [`209390a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/209390a7e026b03ee92f7b1ecb3b1a7b3b2e0232)
- Write documentation for DKIM e-mail setting [`9f5d32e`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9f5d32e527151854f1888614c59e0aa5f2b504d0)
- Release v3.0.3 [`7d761c2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7d761c262a6e7bf6382cc49148c55ac8037db12a)
#### [3.0.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.1...3.0.2)
> 11 August 2023
- chore(release): release v3.0.2 fix chat avatar error and wrong font in network [`#6674`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6674)
- fix(webapp): fix wrong font in whole network comming from chat component [`#6672`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6672)
- fix(webapp): fix 'm.avatar is null' error message [`#6671`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6671)
- Release v3.0.2 [`7bcad81`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7bcad81785de39115ba03aa37fb26caeff7775f2)
- Remove font 'Quicksand' from the chat [`5b95419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5b954197dc93bd5715f657539ca1b9b32d7d557a)
- Fix 'm.avatar is null' error message [`2c12331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2c123313ed6267bb1c88470ba7ae3f4f693cfc15)
#### [3.0.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.0...3.0.1)
> 9 August 2023
- chore(release): release v3.0.1 fix chat avatars [`#6667`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6667)
- fix(webapp): try to fix avatars [`#6660`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6660)
- Releasde v3.0.1 fix chat avatars [`0daeb5b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0daeb5b957e11d206ceddc3e8b275929e1850d18)
- conditional url replacement [`5c1ab88`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5c1ab880127a5f4c1b457598fafd3ae9fe1a0d12)
- try to fix avatars [`6f43321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/6f43321abad945602529b962e536546ab541cd70)
### [3.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.7.0...3.0.0)
> 7 August 2023
- chore(other): release v3.0.0 [`#6658`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6658)
- fix(webapp): add missing locales [`#6652`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6652)
- fix(webapp): fix wrong labels in filter menu buttons [`#6656`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6656)
- refactor(webapp): refactor filter menu [`#6535`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6535)
- fix(webapp): correct chat usertag profile link [`#6646`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6646)
- fix(webapp): fix create event typing issues [`#6643`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6643)
- fix(webapp): mini chat: dynamic header buttons [`#6641`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6641)
- fix(webapp): start search of user after three characters (chat new room, add group member) [`#6639`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6639)
- refactor(webapp): updated/refactored ui of create post page [`#6559`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6559)
- fix(webapp): fix code in editor component [`#6624`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6624)
- fix(backend): security subscriptions [`#6621`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6621)
- fix(webapp): change the background color of my messages to the same color as side menu [`#6620`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6620)
- feat(webapp): change general search hint [`#6619`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6619)
- feat(webapp): chat component + button open search [`#6618`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6618)
- feat(webapp): change color red to green on cancel buttons of post and comments [`#6565`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6565)
- fix(webapp): groups highlighting [`#6608`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6608)
- feat(webapp): chat link to chat page [`#6617`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6617)
- fix(webapp): fix lastMessage to contain proper values [`#6615`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6615)
- fix(backend): chat create message - only take 2000 chat message characters [`#6613`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6613)
- feat(webapp): chat component can now show clickable urls [`#6614`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6614)
- fix(webapp): highlight username functionality for chat [`#6609`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6609)
- fix(webapp): fix chat behaviour [`#6611`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6611)
- feat(webapp): chat seen check style [`#6612`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6612)
- feat(webapp): on open chat room page close or on change profile and click chat change small chat [`#6605`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6605)
- feat(webapp): change text for chat room search to `Filter chat rooms` [`#6601`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6601)
- fix(backend): convert indexId to string (neo4j number problem) [`#6603`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6603)
- fix(other): github cache in workflow [`#6604`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6604)
- feat(webapp): show last message and unread count in room list [`#6595`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6595)
- refactor(other): cache docker images for backend webapp e2e testing [`#6585`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6585)
- feat(backend): chat message added subscription [`#6586`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6586)
- refactor(backend): removed promise all from seed [`#6599`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6599)
- feat(webapp): add headline to chat page [`#6563`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6563)
- fix(other): set workflow not to fail while deleting cache [`#6594`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6594)
- feat(backend): room count subscription [`#6584`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6584)
- fix(backend): profile query fix [`#6587`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6587)
- feat(webapp): mark messages as seen [`#6567`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6567)
- feat(webapp): changed usertag in chat messages to user-slug [`#6555`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6555)
- feat(backend): more room properties [`#6564`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6564)
- refactor(backend): chat messages - order by indexId instead of createdAt [`#6562`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6562)
- feat(webapp): chat language is reactive [`#6560`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6560)
- feat(backend): unread rooms query [`#6566`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6566)
- feat(webapp): chat last seen in webapp [`#6580`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6580)
- feat(webapp): chat paginate rooms [`#6579`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6579)
- refactor(other): use cached docker images in github e2e flow to decrease run time of e2e test job runs [`#6582`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6582)
- fix(webapp): show room header images. [`#6581`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6581)
- feat(backend): chat seed [`#6561`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6561)
- feat(webapp): chat message pagination [`#6549`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6549)
- feat(webapp): chat remove reply smileys [`#6553`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6553)
- feat(webapp): remove message actions for chat [`#6552`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6552)
- feat(backend): more message props [`#6547`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6547)
- refactor(backend): fix seed to not use promise all where easily refactored [`#6520`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6520)
- refactor(other): add proper reporting to cypress tests [`#6540`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6540)
- refactor(other): cypress - migrate from cypress-file-upload to .selectFile() [`#6528`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6528)
- feat(webapp): change chat style and lang [`#6517`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6517)
- feat(webapp): show events with locations on the map [`#6518`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6518)
- feat(webapp): refine chat notification in header menu and chat button on user profile [`#6550`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6550)
- refactor(other): disable cypress test retries [`#6527`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6527)
- feat(webapp): event creation page -better placeholder texts for venue and city inputs [`#6525`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6525)
- refactor(other): refine webapp github workflow conditions [`#6526`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6526)
- docs(other): update TODO-next-update.md for 2.7.0-470 [`#6532`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6532)
- fix(backend): chat - do not allow to create room with self [`#6530`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6530)
- feat(webapp): chat message capability [`#6524`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6524)
- chore(backend): bump @faker-js/faker from 5.1.0 to 8.0.2 [`#6368`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6368)
- chore(other): bump cypress from 12.14.0 to 12.17.0 [`#6509`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6509)
- feat(webapp): chat rooms [`#6519`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6519)
- fix(other): fix branded build - rename files to js for the webapp [`#6516`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6516)
- fix(backend): fix branded build (migration) [`#6508`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6508)
- fix(backend): fix branded build [`#6507`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6507)
- feat(webapp): create a component chat scaffholding [`#6503`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6503)
- fix(webapp): fix create post & notifications [`#6505`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6505)
- feat(backend): typescript eslint backend [`#6478`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6478)
- fix(backend): subscriptions [`#6477`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6477)
- fix(backend): seed reports [`#6472`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6472)
- refactor(backend): remove activity pub [`#6470`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6470)
- fix(webapp): change registration `terms and conditions`, `data privacy` links to use `page-params-link` [`#6441`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6441)
- feat(backend): message properties [`#6471`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6471)
- feat(backend): room properties [`#6451`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6451)
- fix(backend): create message mutation [`#6469`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6469)
- feat(backend): messages [`#6450`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6450)
- feat(backend): rooms [`#6444`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6444)
- fix(other): neo4j config map & node version [`#6464`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6464)
- Bump @babel/core from 7.22.1 to 7.22.5 [`#6413`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6413)
- refactor(database): update neo4j to 4.4 [`#6306`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6306)
- Bump cypress from 12.13.0 to 12.14.0 [`#6414`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6414)
- Bump node from 20.2.0-alpine3.17 to 20.3.0-alpine3.17 in /backend [`#6409`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6409)
- Bump node from 20.2.0-alpine3.17 to 20.3.0-alpine3.17 in /webapp [`#6408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6408)
- fix(backend): typescript fix [`#6448`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6448)
- refactor(backend): migrate completely to typescript [`#6434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6434)
- remove package cypress-file-upload from e2e testing [`73f6bc6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/73f6bc642194b0c73769d4e8d8e53645b6e80adf)
- fixed cypress [`5f545f3`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5f545f3b8fc6927954e036b27ad2e123bcd36149)
- fix seed to not use promise all where easily refactored [`1b0f512`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1b0f5124159033214f99bfbc4cebe9dfaa7dd76e)
#### [2.7.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.6.0...2.7.0)
> 14 June 2023
- chore(release): v2.7.0 [`#6440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6440)
- 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)
- fix(webapp): show avatar for group members [`#6258`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6258)
- fix(webapp): fix search for 3 chars [`#6256`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6256)
- fix(backend): group posts cannot be pinned [`#6242`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6242)
- Bump metascraper-soundcloud from 5.33.5 to 5.34.2 in /backend [`#6213`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6213)
- refactor(other): refactor test workflows [`#6151`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6151)
- fix(other): deployment fix typo, update stage.ocelot.social reference [`#6230`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6230)
- fix(other): bump metascraper-url from 5.33.5 to 5.34.2 in /backend [`#6217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6217)
- fix(other): workflow typo [`#6235`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6235)
- feat(other): publish stage.yunite.me hook [`#6234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6234)
- fix(other): reduce kubernetes memory limits [`#6229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6229)
- fix(other): deployment for branded image with custom names [`#6228`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6228)
- Bump node from 19.8.1-alpine3.17 to 19.9.0-alpine3.17 in /backend [`#6219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6219)
- Bump node from 19.8.1-alpine3.17 to 19.9.0-alpine3.17 in /webapp [`#6220`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6220)
- feat(other): deployment pod resources [`#6132`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6132)
- Bump @babel/core from 7.12.17 to 7.21.4 in /webapp [`#6215`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6215)
- Bump jsonwebtoken from 8.5.1 to 9.0.0 in /webapp [`#6079`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6079)
- Bump jest from 29.4.2 to 29.5.0 in /webapp [`#6094`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6094)
- Bump babel-jest from 29.4.2 to 29.5.0 in /webapp [`#6095`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6095)
- Bump neode from 0.4.8 to 0.4.9 in /backend [`#6075`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6075)
- Bump node from 19.4.0-alpine3.17 to 19.8.1-alpine3.17 in /webapp [`#6155`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6155)
- Bump node from 19.4.0-alpine3.17 to 19.8.1-alpine3.17 in /backend [`#6156`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6156)
- Bump @babel/core from 7.9.0 to 7.21.4 [`#6200`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6200)
- Bump @babel/preset-env from 7.12.7 to 7.21.4 [`#6204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6204)
- Bump expect from 25.3.0 to 29.5.0 [`#6098`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6098)
- separate test workflows [`3533a36`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3533a36cdc811c0e1dae218fbc2184f7c4bc3951)
- get it working [`8df7d5d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8df7d5d265b0c5ba16f167a213631d765d2f985e)
- feat(webapp): group categories on posts [`3244f3f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3244f3f86d1e8c09e0fd49f43c49f0a3aa8b85ab)
#### [2.5.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.5.0...2.5.1)
> 23 March 2023
- chore(other): release v2.5.1 fix filter menu width [`#6180`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6180)
- feat(webapp): add tooltips to all menu icons [`#6185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6185)
- fix(webapp): popup filter max-width [`#6177`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6177)
- Add tooltip to header notifications menu [`28505a5`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/28505a5b181008ebcde6fa58b7a4a8459a492018)
- Add tooltip to header avatar menu [`4c0469f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4c0469f61a3c2fae23e50c6a5a2a91b63fac149a)
- Release v2.5.1 - fix filter menu width [`08def14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/08def14cafef7816d8e43f1896430400bda9635d)
#### [2.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.4.0...2.5.0)
> 21 March 2023
- chore(other): release v2.5.0 [`#6172`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6172)
- fix(other): publish transmit github run number properly [`#6171`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6171)
- feat(other): deployment include GitHub run number on branded images [`#6170`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6170)
- fix(backend): new migration to create search indexes [`#6167`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6167)

View File

@ -8,9 +8,11 @@
[ocelot.social](https://ocelot.social) is free and open source software program code to run social networks. Its development is supported by a community of programmers and interested network operators.
<!-- markdownlint-disable MD033 -->
<p align="center">
<a href="https://ocelot.social" target="_blank"><img src="webapp/static/img/custom/logo-squared.svg" alt="ocelot.social" width="40%" height="40%"></a>
</p>
<!-- markdownlint-enable MD033 -->
Our goal is to enable people to participate fairly and equally in online social networks. The equality of opportunity applies both to the fundamental equality of all people and to the possibility of letting their diverse voices be heard.

View File

@ -16,7 +16,7 @@
* [Frontend tests](webapp/testing.md)
* [Backend tests](backend/testing.md)
* [Docker More Closely](DOCKER_MORE_CLOSELY.md)
* [Deployment](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/blob/master/deployment/README.md)
* [Deployment](deployment/README.md)
* [Contributing](CONTRIBUTING.md)
* [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md)

View File

@ -12,8 +12,7 @@ docker-compose*.yml
./*.log
node_modules/
scripts/
dist/
build/
maintenance-worker/
neo4j/

View File

@ -1,13 +1,23 @@
DEBUG=true
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
# EMail
EMAIL_SUPPORT="devops@ocelot.social"
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURE="false" # true for 465, false for other ports
SMTP_DKIM_DOMAINNAME=
SMTP_DKIM_KEYSELECTOR=
SMTP_DKIM_PRIVATKEY=
JWT_SECRET="b/&&7b78BF&fv/Vd"
JWT_EXPIRES="2y"
@ -26,7 +36,4 @@ AWS_ENDPOINT=
AWS_REGION=
AWS_BUCKET=
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
EMAIL_SUPPORT="devops@ocelot.social"
CATEGORIES_ACTIVE=false

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',
},
},
],
};

2
backend/.gitignore vendored
View File

@ -3,7 +3,7 @@ node_modules/
.vscode
.idea
yarn-error.log
dist/*
build/*
coverage.lcov
.nyc_output/
public/uploads/*

View File

@ -1 +1 @@
v19.4.0
v20.2.0

View File

@ -1,7 +1,7 @@
##################################################################################
# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:19.4.0-alpine3.17 as base
FROM node:20.2.0-alpine3.17 as base
# ENVs
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
@ -74,7 +74,7 @@ FROM code as build
# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
RUN yarn run build
RUN /bin/sh -c "yarn run build"
##################################################################################
# TEST ###########################################################################
@ -90,7 +90,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist
COPY --from=build ${DOCKER_WORKDIR}/build ./build
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files
# TODO - externalize the uploads so we can copy the whole folder

View File

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

View File

@ -1,35 +1,34 @@
{
"name": "ocelot-social-backend",
"version": "2.5.0",
"version": "3.1.2",
"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 ./dist/db/migrations --store ./dist/db/migrate/store.js",
"start": "node dist/",
"build": "babel src/ -d dist/ --copy-files",
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"__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,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",
"@babel/core": "~7.9.0",
"@babel/node": "~7.8.7",
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.9.5",
"@babel/plugin-proposal-throw-expressions": "^7.22.5",
"@babel/preset-env": "~7.22.20",
"@babel/register": "^7.9.0",
"@hapi/joi": "^17.1.1",
"@sentry/node": "^5.15.4",
"apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8",
@ -37,7 +36,7 @@
"apollo-link-http": "~1.5.17",
"apollo-server": "~2.14.2",
"apollo-server-express": "^2.14.2",
"aws-sdk": "^2.652.0",
"aws-sdk": "^2.1425.0",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.1.0",
"babel-jest": "~25.2.6",
@ -46,19 +45,15 @@
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~7.0.3",
"date-fns": "2.22.1",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"graphql": "^14.6.0",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1",
"graphql-redis-subscriptions": "^2.2.1",
"graphql-shield": "~7.2.2",
"graphql-tag": "~2.10.3",
"helmet": "~3.22.0",
"helmet": "~7.0.0",
"ioredis": "^4.16.1",
"jsonwebtoken": "~8.5.1",
"languagedetect": "^2.0.0",
@ -66,9 +61,7 @@
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.33.5",
"metascraper-audio": "^5.33.5",
"metascraper-author": "^5.33.5",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.33.5",
"metascraper-description": "^5.33.5",
"metascraper-image": "^5.33.5",
@ -76,51 +69,56 @@
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.33.5",
"metascraper-publisher": "^5.33.5",
"metascraper-soundcloud": "^5.33.5",
"metascraper-title": "^5.33.5",
"metascraper-url": "^5.33.5",
"metascraper-soundcloud": "^5.34.4",
"metascraper-title": "^5.34.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",
"neo4j-driver": "^4.0.2",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.4.8",
"neode": "^0.4.9",
"node-fetch": "~2.6.1",
"nodemailer": "^6.4.4",
"nodemailer-html-to-text": "^3.2.0",
"npm-run-all": "~4.1.5",
"request": "~2.88.2",
"sanitize-html": "~1.22.0",
"slug": "~6.0.0",
"subscriptions-transport-ws": "^0.9.19",
"trunc-html": "~1.1.2",
"uuid": "~8.3.2",
"validator": "^13.0.0",
"wait-on": "~4.0.1",
"validator": "^13.9.0",
"xregexp": "^4.3.0"
},
"devDependencies": {
"@faker-js/faker": "5.1.0",
"@faker-js/faker": "7.6.0",
"@types/jest": "^27.0.2",
"@types/node": "^20.2.5",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"apollo-server-testing": "~2.11.0",
"chai": "~4.2.0",
"chai": "~4.3.8",
"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": "^9.0.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",
"jest": "^27.2.4",
"nodemon": "~2.0.2",
"prettier": "~2.3.2",
"prettier": "^2.8.7",
"rosie": "^2.0.1",
"supertest": "~4.0.2"
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",

View File

@ -0,0 +1,24 @@
#!/bin/sh
# html files
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/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/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/src/schema/types/
cp -r src/schema/types/*.gql build/src/schema/types/
mkdir -p build/src/schema/types/enum/
cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/
mkdir -p build/src/schema/types/scalar/
cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/
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',
@ -39,6 +40,8 @@ const server = {
JWT_EXPIRES: env.JWT_EXPIRES || '2y',
}
const hasDKIMData = env.SMTP_DKIM_DOMAINNAME && env.SMTP_DKIM_KEYSELECTOR && env.SMTP_DKIM_PRIVATKEY
const smtp = {
SMTP_HOST: env.SMTP_HOST,
SMTP_PORT: env.SMTP_PORT,
@ -46,6 +49,10 @@ const smtp = {
SMTP_SECURE: env.SMTP_SECURE === 'true',
SMTP_USERNAME: env.SMTP_USERNAME,
SMTP_PASSWORD: env.SMTP_PASSWORD,
SMTP_DKIM_DOMAINNAME: hasDKIMData && env.SMTP_DKIM_DOMAINNAME,
SMTP_DKIM_KEYSELECTOR: hasDKIMData && env.SMTP_DKIM_KEYSELECTOR,
// PEM format: https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html
SMTP_DKIM_PRIVATKEY: hasDKIMData && env.SMTP_DKIM_PRIVATKEY.replace(/\\n/g, '\n'), // replace all "\n" in .env string by real line break
}
const neo4j = {
@ -90,14 +97,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

@ -2,10 +2,10 @@ import { v4 as uuid } from 'uuid'
import slugify from 'slug'
import { hashSync } from 'bcryptjs'
import { Factory } from 'rosie'
import faker from '@faker-js/faker'
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 {
@ -63,7 +63,7 @@ Factory.define('basicUser')
.option('password', '1234')
.attrs({
id: uuid,
name: faker.name.findName,
name: faker.name.fullName,
password: '1234',
role: 'user',
termsAndConditionsAgreedVersion: '0.0.1',
@ -165,7 +165,7 @@ Factory.define('post')
})
.after(async (buildObject, options) => {
const [post, author, image, /* categories, */ tags] = await Promise.all([
neode.create('Post', buildObject),
neode.create('Article', buildObject),
options.author,
options.image,
// options.categories,

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 to all existing posts the Article label'
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: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: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()
}
}

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

View File

@ -23,6 +23,7 @@ export function getNeode(options = {}) {
if (!neodeInstance) {
const { uri, username, password } = { ...defaultOptions, ...options }
neodeInstance = new Neode(uri, username, password).with(models)
neodeInstance.extend('Post', 'Article', {})
return neodeInstance
}
return neodeInstance

File diff suppressed because it is too large Load Diff

1574
backend/src/db/seed.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
# GraphQL Playground
***Attention:** For using the GraphQL Playground set `DEBUG=true` in your backend `.env`, see `.env.template`!*
To use GraphQL Playground, we need to know some basics:
## How To Login?

View File

@ -0,0 +1,50 @@
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
author {
id
}
username
avatar
date
saved
distributed
seen
}
}
`
}
export const markMessagesAsSeen = () => {
return gql`
mutation ($messageIds: [String!]) {
MarkMessagesAsSeen(messageIds: $messageIds)
}
`
}

View File

@ -11,6 +11,8 @@ export const createPostMutation = () => {
$content: String!
$categoryIds: [ID]
$groupId: ID
$postType: PostType
$eventInput: _EventInput
) {
CreatePost(
id: $id
@ -19,11 +21,31 @@ export const createPostMutation = () => {
content: $content
categoryIds: $categoryIds
groupId: $groupId
postType: $postType
eventInput: $eventInput
) {
id
slug
title
content
disabled
deleted
postType
author {
name
}
categories {
id
}
eventStart
eventEnd
eventLocationName
eventVenue
eventIsOnline
eventLocation {
lng
lat
}
}
}
`
@ -50,6 +72,7 @@ export const filterPosts = () => {
id
title
content
eventStart
}
}
`

View File

@ -0,0 +1,67 @@
import gql from 'graphql-tag'
export const createRoomMutation = () => {
return gql`
mutation ($userId: ID!) {
CreateRoom(userId: $userId) {
id
roomId
roomName
lastMessageAt
unreadCount
#avatar
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: lastMessageAt_desc) {
id
roomId
roomName
avatar
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,14 +5,13 @@
* @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')
}
if (data && typeof data === 'string' && fields.includes(_key)) {
// well we found what we searched for, lets replace the value with our callback result
const key = _key.split('!')
if (key.length === 1 || key[1] !== fieldName) data = callback(data, key[0])
const fieldDef = fields.find((f) => f.field === _key)
if (data && typeof data === 'string' && fieldDef) {
if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key)
} else if (data && Array.isArray(data)) {
// go into the rabbit hole and dig through that array
data.forEach((res, index) => {

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,60 @@
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,
},
Subscription: {
chatMessageAdded: messageProperties,
},
}

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

@ -30,6 +30,7 @@ const standardSanitizeHtmlOptions = {
'strike',
'span',
'blockquote',
'usertag',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],

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