Merging Human-Connection to master

This commit is contained in:
Matt Rider 2019-07-03 08:47:30 -03:00
commit 089b07c3e0
559 changed files with 57465 additions and 0 deletions

View File

@ -0,0 +1,169 @@
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:
# layout options are quite limited in v4.x - there have been way more options in v1.0
layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags
behavior: new # default = posts once then update, posts new if delete
# once = post once then updates
# new = delete old, post new
# spammy = post new
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: no # [yes :: must have a head report to post]
branches: null # branch names that can post comment
flags: null
paths: null

View File

@ -0,0 +1,3 @@
structure:
readme: README.md
summary: SUMMARY.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

@ -0,0 +1,11 @@
<!--
Please take a look at the issue templates at https://github.com/Human-Connection/Human-Connection/issues/new/choose
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.
Thanks!
-->
## 💬 Issue
<!-- Describe your Issue in detail. -->
<!-- Attach screenshots and drawings if needed. -->

View File

@ -0,0 +1,32 @@
---
name: 🐛 Bug report
about: Create a report to help us improve
labels: bug
title: 🐛 [Bug]
---
## :bug: Bugreport
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->
### Steps to reproduce the behavior
1.
2.
3.
4. ...
5. Profit
### Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
### Version & Environment
Type: [] <!-- [Desktop|Smartphone] -->
- OS: [] <!-- [e.g. iOS8.1 or Windows] -->
- Browser: [] <!-- [e.g. stock browser, safari, chrome] -->
- Version [] <!-- [e.g. 22] -->
- Device: [] <!-- [e.g. iPhone6] -->
### Additional context
<!-- Add any other context about the problem here. -->

View File

@ -0,0 +1,16 @@
---
name: 🚀 Feature request
about: Suggest an idea for this project
labels: feature
title: 🚀 [Feature]
---
## :rocket: Feature
<!-- Describe the Feature. Use Screenshots if possible. -->
### Design & Layout
<!-- Attach Screenshots and Drawings. -->
### Additional context
<!-- Add any other context or screenshots about the feature request here.-->

View File

@ -0,0 +1,12 @@
---
name: 💬 Question
about: If you need help understanding HumanConnection.
labels: question
title: 💬 [Question]
---
<!-- Chat with Team HumanConnection -->
<!-- If you need an answer right away, visit the HumanConnection Discord:
https://discord.gg/Q3mpcgr -->
## :speech_balloon: Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

View File

@ -0,0 +1,13 @@
## 🍰 Pullrequest
<!-- Describe the Pullrequest. Use Screenshots if possible. -->
### Issues
<!-- Which Issues does this fix, which are related?
- fixes #XXX
- relates #XXX
-->
- None
### Todo
<!-- In case some parts are still missing, list them here. -->
- [X] None

19
Human-Connection/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
.env
.idea
*.iml
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn-integrity
.eslintcache
kubeconfig.yaml
node_modules/
cypress/videos
cypress/screenshots/
cypress.env.json
!.gitkeep
**/coverage

View File

@ -0,0 +1,70 @@
dist: xenial
language: generic
addons:
apt:
packages:
- libgconf-2-4
snaps:
- docker
- chromium
before_install:
- yarn global add wait-on
# Install Codecov
- yarn global add codecov
- yarn install
- cp cypress.env.template.json cypress.env.json
install:
- docker-compose -f docker-compose.yml -f docker-compose.travis.yml up --build -d
# avoid "Database constraints have changed after this transaction started"
- wait-on http://localhost:7474
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"
# Backend
- docker-compose exec backend yarn run lint
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
- docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed
- docker-compose exec backend yarn run test:cucumber --tags "not @wip"
- docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed
# Frontend
- docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
- docker-compose exec -d backend yarn run test:before:seeder
# Fullstack
- yarn run cypress:run
# Coverage
- 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:
- ./scripts/setup_kubernetes.sh
deploy:
- provider: script
script: scripts/docker_push.sh
on:
branch: master
- provider: script
script: scripts/deploy.sh
on:
branch: master

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"octref.vetur",
"gruntfuggly.todo-tree",
]
}

12
Human-Connection/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true
}

View File

@ -0,0 +1,44 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at developer@human-connection.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/)

View File

@ -0,0 +1,80 @@
# CONTRIBUTING
Thanks so much for thinking of contributing to the Human Connection project, we really appreciate it! :-\)
## Getting Set Up
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/)
We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
![](https://dl.dropbox.com/s/vbmcihkduy9dhko/Screenshot%202019-01-03%2015.50.11.png?dl=0)
Here are some general notes on our development flow:
## Development
* Currently operating in two week sprints
* We are using ZenHub to coordinate
* estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
* "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A"good+first+issue)
* ordering on ZenHub not necessarily reflected on github projects
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
* Core team
* all the people who are hired by HC non-profit corporation
* you can Meet-the-team [every two weeks in German](https://human-connection.org/veranstaltungen/) and [every month in English](https://human-connection.org/en/events/).
* 9 people
* 2 core developers \(Robert [@roschaefer](https://github.com/roschaefer) and Greg [@appinteractive](https://github.com/appinteractive)\)
* 3 marketeers Jasi, Dennis and Sensi
* Hardy doing business development
* Martin head of IT and previously data protection officer
* Victor doing accounting and controlling
* Nicolas is the community manager \(reviews content in the network\) reflects community opinion back to the core team
* when can folks pair with Robert
* 10am UTC until 5pm UTC every working day
## Philosophy
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
* anyone can start working on anyone elses code
* we avoid blocking because someone else isn't working on something
* however it's sometimes good to leave something in order to create successful education experience
* everyone should always push their code to branches so others can see it
Everyone feel free to request merges or answers to issues from the project managers
But what do we do when waiting for merge into master \(wanting to keep PRs small\) --&gt; Robert recommends creating a pull request for each step
* programming is also about thinking about other people - empathy for your co-workers
* but what about when you are waiting for merge?
* solutions
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
* 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
### Code Review
* Github setting in place - at least one review is required to merge
- in principle anyone (who is not the PR owner) can review
- but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?)
- once there is a review, and presuming no requested changes, PR opener can merge
* CI/tests
- the CI needs to pass
- linting <-- autofix?
- tests (unit, feature) (backend, frontend)
- codecoverage
## Notes
question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --&gt; Robert says you can always ask in discord - group channels are the best
Robert shares: [Zenhub board](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/boards?repos=112590397,152252353,152252578,157710732,163305928) Robert says the order of tickets are preserved in ZenHub and reflect their priority \(most important at the top\) and so check out the current milestones
Matt - question about who can work on [ticket 100](https://app.zenhub.com/workspaces/nitro-embed-5c0154ecc699f60fc92cf11f/issues/human-connection/human-connection/100) --&gt; Robert - in rare occasions it might be exclusive to someone with admin permissions Robert: notes greg just pushed this today: [https://github.com/Human-Connection/Nitro-Deployment](https://github.com/Human-Connection/Nitro-Deployment)
Matt makes point that new stories will have to be taken off the "New Issues" and Robert says that's fine, if you don't like the first one, then you can take the next one. Volunteeers have no commitment except their own self development and their awesomeness by contributing to free and open-source software projects.
Robert notes that everyone is invited to join the kickoff meetings
Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)

View File

@ -0,0 +1,12 @@
# LICENSE
MIT License
Copyright \(c\) 2018 Human-Connection gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files \(the "Software"\), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,58 @@
# Human-Connection
[![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection)
[![Codecov Coverage](https://img.shields.io/codecov/c/github/Human-Connection/Human-Connection/master.svg?style=flat-square)](https://codecov.io/gh/Human-Connection/Human-Connection/)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)
Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
[![Human-Connection](.gitbook/assets/lets_get_together.png)](https://human-connection.org)
**Technology Stack**
* [VueJS](https://vuejs.org/)
* [NuxtJS](https://nuxtjs.org/)
* [GraphQL](https://graphql.org/)
* [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/)
## Live demo
Try out our deployed [staging environment](https://nitro-staging.human-connection.org/).
Logins:
| email | password | role |
| :--- | :--- | :--- |
| `user@example.org` | 1234 | user |
| `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin |
## Documentation
Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/human-connection/) :mag_right:
## Translations
You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
Thank you lokalise for providing us with a premium account :raised_hands:.
## Developer Chat
Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
Just introduce yourself at `#user-presentation` and mention `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
## Attributions
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/)
## License
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).

View File

@ -0,0 +1,40 @@
# Table of contents
* [Introduction](README.md)
* [Edit this Documentation](edit-this-documentation.md)
* [Installation](installation.md)
* [Neo4J](neo4j/README.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
* [Webapp](webapp/README.md)
* [COMPONENTS](webapp/components.md)
* [PLUGINS](webapp/plugins.md)
* [STORE](webapp/store.md)
* [PAGES](webapp/pages.md)
* [ASSETS](webapp/assets.md)
* [LAYOUTS](webapp/layouts.md)
* [Styleguide](webapp/styleguide.md)
* [STATIC](webapp/static.md)
* [MIDDLEWARE](webapp/middleware.md)
* [Testing Guide](testing.md)
* [End-to-end tests](cypress/README.md)
* [Frontend tests](webapp/testing.md)
* [Backend tests](backend/testing.md)
* [Contributing](CONTRIBUTING.md)
* [Kubernetes Deployment](deployment/README.md)
* [Minikube](deployment/minikube/README.md)
* [Digital Ocean](deployment/digital-ocean/README.md)
* [Kubernetes Dashboard](deployment/digital-ocean/dashboard/README.md)
* [HTTPS](deployment/digital-ocean/https/README.md)
* [Human Connection](deployment/human-connection/README.md)
* [Mailserver](deployment/human-connection/mailserver/README.md)
* [Volumes](deployment/volumes/README.md)
* [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md)
* [Legacy Migration](deployment/legacy-migration/README.md)
* [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md)
* [License](LICENSE.md)

View File

@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "10"
}
}
]
],
"plugins": [
"@babel/plugin-proposal-throw-expressions"
]
}

View File

@ -0,0 +1,2 @@
coverage:
range: "60...100"

View File

@ -0,0 +1,22 @@
.vscode/
.nyc_output/
.github/
.travis.yml
.graphqlconfig
.env
Dockerfile
docker-compose*.yml
./*.png
./*.log
node_modules/
scripts/
dist/
maintenance-worker/
neo4j/
public/uploads/*
!.gitkeep

View File

@ -0,0 +1,17 @@
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCKS=false
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true
SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"

View File

@ -0,0 +1,25 @@
module.exports = {
env: {
es6: true,
node: true,
jest: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'standard',
'plugin:prettier/recommended'
],
plugins: [
'jest'
],
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'],
},
};

13
Human-Connection/backend/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
node_modules/
.env
.vscode
.idea
yarn-error.log
dist/*
coverage.lcov
.nyc_output/
public/uploads/*
!.gitkeep
# Apple macOS folder attribute file
.DS_Store

View File

@ -0,0 +1,3 @@
{
"schemaPath": "./src/schema.graphql"
}

View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true
};

View File

@ -0,0 +1,28 @@
FROM node:12.5-alpine as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000
ARG BUILD_COMMIT
ENV BUILD_COMMIT=$BUILD_COMMIT
ARG WORKDIR=/nitro-backend
RUN mkdir -p $WORKDIR
WORKDIR $WORKDIR
RUN apk --no-cache add git
COPY package.json yarn.lock ./
COPY .env.template .env
CMD ["yarn", "run", "start"]
FROM base as builder
RUN yarn install --frozen-lockfile --non-interactive
COPY . .
RUN cp .env.template .env
RUN yarn run build
# reduce image size with a multistage build
FROM base as production
ENV NODE_ENV=production
COPY --from=builder /nitro-backend/dist ./dist
COPY ./public/img/ ./public/img/
RUN yarn install --frozen-lockfile --non-interactive

View File

@ -0,0 +1,130 @@
# Backend
## Installation with Docker
Run the following command to install everything through docker.
The installation takes a bit longer on the first pass or on rebuild ...
```bash
$ docker-compose up
# rebuild the containers for a cleanup
$ docker-compose up --build
```
Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/).
## Installation without Docker
For the local installation you need a recent version of [node](https://nodejs.org/en/)
(&gt;= `v10.12.0`).
Install node dependencies with [yarn](https://yarnpkg.com/en/):
```bash
$ cd backend
$ yarn install
```
Copy Environment Variables:
```bash
# in backend/
$ cp .env.template .env
```
Configure the new file according to your needs and your local setup. Make sure
a [local Neo4J](http://localhost:7474) instance is up and running.
Start the backend for development with:
```bash
$ yarn run dev
```
or start the backend in production environment with:
```bash
yarn run start
```
For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
your `.env` configuration file.
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.
![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
#### Seed Database
If you want your backend to return anything else than an empty response, you
need to seed your database:
{% tabs %}
{% tab title="Docker" %}
In another terminal run:
```bash
$ docker-compose exec backend yarn run db:seed
```
To reset the database run:
```bash
$ docker-compose exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v
```
{% endtab %}
{% tab title="Without Docker" %}
Run:
```bash
$ yarn run db:seed
```
To reset the database run:
```bash
$ yarn run db:reset
```
{% endtab %}
{% endtabs %}
# Testing
**Beware**: We have no multiple database setup at the moment. We clean the
database after each test, running the tests will wipe out all your data!
{% tabs %}
{% tab title="Docker" %}
Run the _**jest**_ tests:
```bash
$ docker-compose exec backend yarn run test:jest
```
Run the _**cucumber**_ features:
```bash
$ docker-compose exec backend yarn run test:cucumber
```
{% endtab %}
{% tab title="Without Docker" %}
Run the _**jest**_ tests:
```bash
$ yarn run test:jest
```
Run the _**cucumber**_ features:
```bash
$ yarn run test:cucumber
```
{% endtab %}
{% endtabs %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -0,0 +1,18 @@
# GraphQL with Apollo
GraphQL is a data query language which provides an alternative to REST and ad-hoc web service architectures. It allows clients to define the structure of the data required, and exactly the same structure of the data is returned from the server.
![GraphQL Playground](../../../.gitbook/assets/graphql-playground%20%281%29.png)
## Middleware keeps resolvers clean
![](../.gitbook/assets/grafik-4.png)
A well-organized codebase is key for the ability to maintain and easily introduce changes into an app. Figuring out the right structure for your code remains a continuous challenge - especially as an application grows and more developers are joining a project.
A common problem in GraphQL servers is that resolvers often get cluttered with business logic, making the entire resolver system harder to understand and maintain.
GraphQL Middleware uses the [_middleware pattern_](https://dzone.com/articles/understanding-middleware-pattern-in-expressjs) \(well-known from Express.js\) to pull out repetitive code from resolvers and execute it before or after one of your resolvers is invoked. This improves code modularity and keeps your resolvers clean and simple.

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -0,0 +1,112 @@
{
"name": "human-connection-backend",
"version": "0.0.1",
"description": "GraphQL Backend for Human Connection",
"main": "src/index.js",
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"start": "node dist/",
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
"lint": "eslint src --config .eslintrc.js",
"test": "run-s test:jest test:cucumber",
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
"test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",
"db:reset": "cross-env babel-node src/seed/reset-db.js",
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
},
"author": "Human Connection gGmbH",
"license": "MIT",
"jest": {
"verbose": true,
"collectCoverageFrom": [
"**/*.js",
"!**/node_modules/**",
"!**/test/**",
"!**/dist/**",
"!**/src/**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
],
"testMatch": [
"**/src/**/?(*.)+(spec|test).js?(x)"
]
},
"dependencies": {
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.3",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.6.7",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-beta.2",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
"express": "~4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "~14.4.0",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~6.0.2",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.18.0",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
"merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "^2.6.3",
"node-fetch": "~2.6.0",
"nodemailer": "^6.2.1",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
"slug": "~1.1.0",
"trunc-html": "~1.1.2",
"uuid": "~3.3.2",
"wait-on": "~3.2.0"
},
"devDependencies": {
"@babel/cli": "~7.4.4",
"@babel/core": "~7.4.5",
"@babel/node": "~7.4.5",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.6.7",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~6.0.1",
"eslint-config-prettier": "~6.0.0",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.18.0",
"eslint-plugin-jest": "~22.7.1",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.8.0",
"nodemon": "~1.19.1",
"prettier": "~1.18.2",
"supertest": "~4.0.2"
}
}

View File

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M126.284 211.948c102.42-35.097 185.118-48.813 243.947 3.76-87.197-24.763-166.785-19.12-243.947-3.76zm56.557 37.817c83.2 8.086 125.85 7.018 185.85-18.187-78.351 41.912-114.88 34.814-185.85 18.187zm102.729 20.872c0-1.67 1.563-3.027 3.492-3.027 1.927 0 3.493 1.357 3.493 3.027 0 1.67-1.566 3.026-3.493 3.026-1.93 0-3.492-1.355-3.492-3.026zm-12.265.16c0-1.67 1.562-3.027 3.49-3.027 1.93 0 3.493 1.356 3.493 3.026 0 1.671-1.564 3.026-3.493 3.026-1.928 0-3.49-1.355-3.49-3.026zm-12.426.158c0-1.671 1.567-3.026 3.492-3.026 1.93 0 3.493 1.355 3.493 3.026 0 1.672-1.563 3.026-3.493 3.026-1.925 0-3.492-1.354-3.492-3.026zm59.115-4.762l-4.274 14.591-55.034-.141 44.12-5.849 15.188-8.601zm-186.512-44.348v-5.807c13.51-1.104 33.205-1.49 52.186 2.728l-52.186 3.08zm248.469-4.061c0-26.937-46.004-48.784-112.812-48.784-29.018 0-62.094 4.123-90.233 10.995l-46.092-5.762 3.41 20.454C122.504 201.562 114 209.429 114 217.784c0 8.419 8.628 16.34 22.526 23.251l-3.47 20.823 47.352-5.918a373.092 373.092 0 0 0 24.043 4.893v6.96h20.215l1.573-3.841a400.647 400.647 0 0 0 22.102 1.965l4.608 11.551.326 6.43h66.102l7.206-20.662h-14.52c42.795-7.105 69.888-24.776 69.888-45.452z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 .056h291.913v103.538H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><path d="M247.532 256.288c26.553 0 49.62 4.608 61.301 11.379-10-8.244-33.676-14.039-61.3-14.039-27.625 0-51.301 5.795-61.302 14.039 11.684-6.77 34.75-11.379 61.301-11.379" fill="#E5ECD6"/><path d="M248.467 272.148c28.174 0 52.65 6.146 65.046 15.173-10.611-10.993-35.735-18.718-65.046-18.718-29.31 0-54.433 7.725-65.044 18.718 12.395-9.027 36.872-15.173 65.044-15.173" fill="#B5CB88"/><path d="M248.467 291.044c30.201 0 56.439 6.45 69.725 15.93-11.372-11.54-38.304-19.653-69.725-19.653-31.419 0-58.35 8.113-69.723 19.653 13.286-9.48 39.524-15.93 69.723-15.93" fill="#E5ECD6"/><path d="M248.424 312.164c33.088 0 61.833 6.994 76.39 17.272-12.46-12.512-41.964-21.31-76.39-21.31-34.423 0-63.929 8.798-76.39 21.31 14.558-10.278 43.303-17.272 76.39-17.272" fill="#B5CB88"/><g transform="translate(102 146)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M145.956 28.306c-56.672 0-105.907 7.595-130.842 18.756C36.458 33.475 86.994 23.92 145.956 23.92c58.963 0 109.5 9.556 130.843 23.143-24.937-11.16-74.17-18.756-130.843-18.756m69.48 26.622c-4.413 2.168-9.05 1.749-10.362-.937-1.308-2.684 1.205-6.62 5.617-8.787 4.412-2.166 9.05-1.745 10.36.94 1.31 2.683-1.204 6.618-5.616 8.784m-69.479 36.038c-33.854 0-61.301-7.914-61.301-17.676 0-9.762 27.447-17.678 61.301-17.678 33.855 0 61.302 7.916 61.302 17.678s-27.447 17.676-61.302 17.676m54.659 2.678c-.498 1.453-2.839 1.968-5.228 1.145-2.39-.822-3.925-2.668-3.43-4.122.498-1.455 2.839-1.966 5.23-1.145 2.389.821 3.924 2.665 3.428 4.122m-100.66 0c-.497 1.453-2.838 1.968-5.227 1.145-2.39-.822-3.926-2.668-3.43-4.122.5-1.455 2.837-1.966 5.228-1.145 2.39.821 3.927 2.665 3.43 4.122M77.101 55.188c-4.647-1.597-7.631-5.186-6.667-8.015.967-2.828 5.517-3.826 10.163-2.228 4.648 1.597 7.632 5.187 6.666 8.014-.966 2.828-5.515 3.828-10.162 2.23m148.591-28.937C214.793 11.05 183.223.056 145.956.056 108.689.056 77.12 11.05 66.22 26.252 26.35 33.765 0 46.746 0 61.505c0 23.244 65.347 42.089 145.956 42.089 80.61 0 145.957-18.845 145.957-42.09 0-14.758-26.351-27.739-66.22-35.252" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255-33L476 95v256L255 479 34 351V95z"/><path id="c" d="M0 0h132.6v218.216H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(181.645 120)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M11.221 66.902C6.636 33.096 34.24 9.57 62.304 9.57c.985 0 1.966.033 2.945.091-26.822 5.83-50.335 27.206-54.028 57.242M67.378.001C30.569 0-5.633 32.784.733 79.817c3.475 25.675 37.881 81.413 65.736 83.034a.02.02 0 0 1-.004.01c-.257.111-.492.482-.524.857-.001-.006-.005-.01-.005-.017a1.132 1.132 0 0 0 .003.205.84.84 0 0 0 .04.173c-.582 1.261-1.33 2.487-1.804 4.159-.67 2.366 3.474 5.2 7.465 2.42 2.523-1.76-1.758-3.08-1.977-4.84a9.117 9.117 0 0 0-.116-.674c3.67 2.972 10.917 10.466 7.945 21.534-1.91 7.111-5.973 11.194-9.56 14.794-4.17 4.19-8.109 8.146-6.837 15.37a1.662 1.662 0 0 0 1.924 1.349 1.664 1.664 0 0 0 1.346-1.927c-.972-5.52 1.918-8.423 5.919-12.442 3.687-3.702 8.275-8.311 10.415-16.28 3.49-12.99-5.024-21.714-9.236-25.105 27.033-5.322 57.021-57.103 60.365-81.804C138.19 33.6 104.183.001 67.378.001" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M230.035 250.385c.86.402 1.793.598 2.712.593a6.087 6.087 0 0 0 2.66-.63c1.828-.903 3.288-2.82 3.642-5.787-.02-.147-.024-.306-.039-.458h-12.88c-.013.143-.01.303-.026.445.408 2.984 1.992 4.939 3.931 5.837M237.234 99h-12.187c-23.28 13.57-56.759 56.364.884 141.217h11.366c-30.487-66-23.202-120.912-.063-141.217zM259.063 99h12.187c23.28 13.57 56.891 56.364-.751 141.217h-12.167c30.487-66 23.87-120.912.73-141.217zM273.348 244.102c-.01.12-.007.25-.022.363.407 2.987 1.993 4.944 3.93 5.842.863.402 1.794.601 2.713.594a6.115 6.115 0 0 0 2.662-.628 5.906 5.906 0 0 0 1.59-1.179c.157-.176.314-.36.472-.538.85-1.094 1.448-2.571 1.607-4.454h-12.952zM223.388 244.34l-.02-.237h-12.902c.04.311.1.603.165.89a728.456 728.456 0 0 0 4.557 5.28c.63.199 1.277.333 1.924.329a6.091 6.091 0 0 0 2.662-.628c1.795-.887 3.228-2.76 3.614-5.633M245.652 250.096a6.352 6.352 0 0 0 2.711.593 6.138 6.138 0 0 0 2.666-.63c1.859-.92 3.337-2.896 3.654-5.957h-12.897c-.009.129-.013.261-.022.387.455 2.85 2.003 4.73 3.888 5.607M284.012 96.298c-10.502-4.528-22.225-7.073-34.6-7.073-12.373 0-24.095 2.545-34.597 7.073h69.197z"/><path d="M220.209 240.285c-70.207-81.191-29.736-127.722-2.779-141.217h-9.395C184.08 112.061 168 135.971 168 163.328c0 25.886 19.326 53.366 39.087 76.957h13.122zM277 240.285c70.207-81.191 29.736-127.722 2.778-141.217h9.395c23.957 12.993 40.036 36.903 40.036 64.26 0 25.886-19.326 53.366-39.087 76.957H277zM237.045 278.261l2.188-15.376h2.898v4.38h14.439v-4.38h3.45l2.183 15.376h-25.158zm-.603 27.055l2.329-20.592h3.614l2.059 16.025 14.905 5.014-22.907-.447zm43.804-51.73c-.09.002-.177.016-.267.016a9.026 9.026 0 0 1-3.852-.839c-1.689-.784-3.168-2.126-4.188-3.978-.942 1.62-2.257 2.798-3.746 3.535a8.84 8.84 0 0 1-3.846.903 9.036 9.036 0 0 1-3.853-.835c-1.71-.795-3.205-2.163-4.225-4.051-.95 1.929-2.381 3.323-4.049 4.146a8.76 8.76 0 0 1-3.843.906 9.04 9.04 0 0 1-3.857-.838c-1.587-.739-2.997-1.963-4.008-3.646-.95 1.785-2.326 3.083-3.91 3.867a8.782 8.782 0 0 1-3.845.905 9.006 9.006 0 0 1-3.855-.836c-1.687-.785-3.166-2.13-4.186-3.98-.94 1.62-2.256 2.8-3.747 3.536a8.74 8.74 0 0 1-3.173.862c2.917 3.326 5.77 6.544 8.484 9.626h10.363l-2.185 15.376h-4.61v6.463h3.426l-3.516 21.696h.03l3.911 4.67h31.626l3.913-4.67h.008v-.009l.01-.012h-.015l-3.508-21.675h2.891v-6.463h-3.833l-2.186-15.376h9.33c2.664-2.983 5.457-6.092 8.311-9.3zM248.011 99h5.029c16.71 35.173 10.995 57.883 1.544 141.217h-12.886c-10.733-70.609-14.48-113.26 1.544-141.217h4.769z"/><path d="M261.625 249.933a6.384 6.384 0 0 0 2.71.592 6.17 6.17 0 0 0 2.664-.628c1.793-.888 3.225-2.76 3.61-5.636l-.013-.159h-4.205c-.011.025-.02.05-.032.075l-.172-.075h-8.494c.412 2.978 1.995 4.93 3.932 5.831"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M126 146l94.405 71.42-38.172-25.45-27.501-3.282 95.226 49.254-28.733 91.941 37.763-51.306-17.24 4.515 87.017-41.867 8.209-41.045 2.463 10.67 30.373-3.282-29.553-20.111-40.225 15.186 18.881 26.269-64.852-66.494z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h218.936v174.69H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(124 138)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M104.134 27.903L5.11 0l-.592.604 97.681 29.448 87.491 33.51L4.473.651 0 5.219l100.81 83.795-39.555 15.02 46.152-9.598L218.08 70.762l.02.055-72.984 20.65-6.912 36.332 17.764 10.131c.265-6.47-6.641-9.97-5.288-17.874a31.598 31.598 0 0 0 1.546-2.977l.12.018a8.186 8.186 0 0 0 2.182-16.225 8.147 8.147 0 0 0-6.094 1.646 8.12 8.12 0 0 1 6.565-2.006c.27.035.532.087.794.139l-1.385-11.538 2.93-.353 1.561 12.991c6.621 3.448 8.733 12.552 1.896 15.793l3.072 25.581.35 2.918-2.55-1.456-7.14-4.072-.178.06c-.01-.006-.019-.015-.029-.021l.173-.059-18.673-10.648-.916-.524.197-1.033 6.836-35.923-31.345 8.87 10.75 16.354c-6.477 5.957-3.536 3.9-10.95 9.05l-15.685 4.519c-1.989 5.116-9.56 10.71 3.271 13.775 14.892-5.856 18.646-6.024 32.718-13.112l27.452 41.764 12.621 1.104 21.208-35.327 26.96-68.784-114.803-42.677z" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M235.643 286.994c-7.337-3.551-12.277-8.112-15.098-13.57l-116.648-18.373-4.258-23.658 13.038 4.518 10.197 14.959-4.824 2.328 99.665 9.536c-.513-9.03 2.799-19.675 9.271-31.656-1.464 20.596 1.8 37.705 15.463 47.156 23.486 6.045 48.729 7.994 74.61 8.465-28.956 4.908-55.646 3.822-81.416.295zm69.532-27.415l-9.773-8.359-33.12-.45-10.792-3.35 2.282-14.323 57.564 4.523c-14.163-9.303-33.081-17.106-50.387-19.464v-5.573l.071-.34 147.07-6.467-19.661-7.346-126.49 9.388 2.19-10.52h-15.855l1.777 10.453-125.741-9.33-19.66 7.344 146.156 6.427.067.391v4.842c-.049 0-.098-.004-.145-.004-29.507 0-40.107 20.221-42.641 37.652l-79.63-4.934-9.978-19.48-29.413-6.494 4.3 32.913-4.3.502v9.3l42.598 6.01 86.112 18.034h10.441a78.766 78.766 0 0 0 5.452 2.24l1.33 8.006c-2.342 1.298-3.945 3.766-3.945 6.631a7.612 7.612 0 0 0 15.222 0c0-3.778-2.76-6.89-6.366-7.486l-.9-5.403c17.834 5.241 39.833 5.613 67.932 2.156l-.573 3.444c-3.433.736-6.01 3.781-6.01 7.435a7.611 7.611 0 1 0 15.22 0c0-2.995-1.747-5.563-4.265-6.803l.841-5.07 5.734-5.15h5.136c6.532-7.727 10.336-8.41 10.336-26.918 0-1.424-.303-2.907-.843-4.427h-27.343zM223.91 109.559c4.673-.533 9.103.431 13.206 2.575 1.986 1.039 2.466.165 2.88-1.414 2.241-3.537 4.34-7.236 8.198-9.306 11.677-6.263 26.247.19 30.135 13.27 1.77 5.957 1.12 11.827-.688 17.605-1.005 3.22-2.371 6.327-3.573 9.483-4.357 9.065-9.837 17.545-13.952 26.727-.854 1.906-1.6 1.957-3.322 1.064-2.773-1.438-5.53-2.964-8.534-3.923-4.288-1.848-8.602-3.644-12.86-5.56-6.696-3.015-13.344-6.187-18.823-11.203-7.401-6.774-10.928-14.974-7.87-25 2.305-7.555 7.88-11.94 15.204-14.318"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255-33L476 95v256L255 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#84A939" transform="rotate(30 255 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M128.748 228.578l61.936 31.273 188.772-121.85zM215.634 274.035l-14.938 27.141 24.659-22.233zM217.273 271.672l70.296 35.496L382.634 138z"/><path d="M217.495 267.595L358.438 153.66 190.882 264.248l5.072 37.632 16.98-30.852 4.563-3.434z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h280.118v187.763H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(108 134)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M163.39 149.606c-15.27-3.601-29.887-9.802-42.72-18.873 27.116 9.276 59.092 11.735 86.94 4.313 5.732 9.119 12.64 13.725 19.001 17.29 3.803 2.266 5.487 3.529 6.159 6.883l.958 4.714c.827 4.517-.43 6.509-3.67 8.897l-4.463 3.087c-5.162 3.798-5.112 11.852 2.063 11.846h21.003c7.333-.043 15.421-8.131 17.356-16.9l13.312-76.946c4.428-27.484-10.356-51.243-29.436-68.604C229.875 7.108 203.244-5.11 177.288 2.105 133.059 14.76 125.194 3.303 106.21 3.8c-12.501.33-18.655 3.835-29.956 8.434-9.583 3.902-19.977 6.67-29.195 11.133 4.37-5.153 9.852-9.154 15.82-12.316-3.005-4.873-11.102-6.727-14.773-3.955-4.665 3.508-8.828 13.264-7.669 19.958-3.305 2.128-6.357 4.614-9.042 7.629-6.095 6.848-8.07 13.003-8.703 16.488-.556 2.559-.892 3.309-2.648 4.437L2.633 66.225c-2.825 1.8-3.648 5.47-1.12 9.4 7.115 11.115 18.142 9.597 23.066 8.214 2.105-.551 2.823-.201 3.986 1.183 4.264 4.786 14.15 8.82 28.89 6.746 4.917-.653 8.087 2.192 7.681 7.185L60.73 154.9c-1.517 10.845-3.575 14.505-11.227 16.699l-15.68 4.317c-6.09 2.032-8.685 11.852-.744 11.846h34.72c6.472-.155 11.16-1.741 15.407-6.341 16.097-17.618 20.644-41.42 26.332-53.78 14.2 12.144 29.457 23.05 33.438 32.4 1.971 4.63.537 9.656-4.626 12.454-4.123 2.115-4.885 10.306 2.794 9.585l18.273-.01c5.423-.116 7.822-4.267 7.524-11.521-.305-6.631-1.65-13.762-3.55-20.944" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M194.65 255.66c4.047 4.982 9.571 3.188 15.086-1.194 8.055-6.402 16.476-17.269 19.764-26.627 2.495-7.093.732-16.373-5.516-20.253-6.478-4.02-13.564.416-16.274 5.952-4.439 9.062-6.906 12.694-13.617 18.74-7.817 6.823-6.24 14.631.557 23.381zm107.461 0c6.797-8.75 8.374-16.559.558-23.382-6.712-6.046-9.178-9.678-13.614-18.74-2.713-5.536-9.796-9.972-16.277-5.952-6.248 3.88-8.011 13.16-5.517 20.253 3.288 9.358 11.709 20.225 19.765 26.627 5.515 4.382 11.041 6.176 15.085 1.193zm-27.558-26.665c4.408 4.596 13.593 2.09 12.798-5.04-.078-.677-.316-1.14-.681-1.583-5.136-6.53-10.59-4.72-12.941 2.797-.51 1.73.058 3.004.824 3.826zm-52.345 0c.766-.822 1.338-2.097.828-3.822-2.354-7.522-7.808-9.332-12.944-2.8-.366.442-.604.905-.682 1.581-.795 7.13 8.393 9.637 12.798 5.041zm-46.864-42.102a134.897 134.897 0 0 1 11.14-16.112c5.049-6.26 10.538-11.57 16.521-15.894l-3.069-3.96c-12.943-16.678-35.918 14.46-24.592 35.966zm118.413-32.006c5.982 4.323 11.472 9.633 16.523 15.894a135.717 135.717 0 0 1 11.14 16.112c11.326-21.507-11.652-52.644-24.595-35.966l-3.068 3.96zm-12.793 148.086c-9.533 6.024-22.681 7.08-32.584-.319-9.899 7.4-23.05 6.343-32.584.32 11.038 26.404 54.131 26.404 65.168 0zm-77.322-162.669c-17.905-23.078-50.233 22.52-30.874 51.153-6.196 11.49-10.812 24.013-13.066 36.701-1.673 4.842-5.068 10.247-8.702 12.538 0 0 3.541 1.435 7.232 1.614.023 3.57.214 6.986.558 10.262-.592 3.486-1.754 6.857-3.452 9.046 0 0 2.194.077 4.793-.356 1.887 9.088 5.145 16.903 9.583 23.556 1.129 3.366 1.695 6.9 1.225 9.64 0 0 2.069-1.012 4.214-2.693.309.336.619.67.936 1.001 2.25 4.433 4.814 11.088 2.906 15.704 0 0 6.777-1.493 9.759-5.2 1.319 2.512 2.001 5.115 1.247 7.41 0 0 2.882-1.73 5.193-4.288 5.679 2.733 11.894 4.871 18.563 6.465-1.526-3.76-2.325-8.128-2.168-13.093-4.717-4.3-7.891-10.057-8.221-16.612-.381-7.577 1.707-13.788 5.644-19.447-.755 2.826-1.166 5.664-1.121 8.508.147 11.827 6.973 20.626 15.905 24.124 8.105 3.18 17.946 1.991 23.146-5.26-.971-.228-1.944-.68-2.916-1.363-5.113-3.562-11.209-5.599-14.235-9.004-2.945-3.307-2.691-8.974 2.608-9.427 3.835-.332 9.649 1.588 15.981 1.588 6.334 0 12.144-1.92 15.984-1.588 5.298.453 5.548 6.12 2.606 9.427-3.029 3.405-9.124 5.442-14.235 9.004-.972.682-1.945 1.135-2.916 1.362 5.201 7.252 15.041 8.44 23.147 5.261 8.93-3.498 15.757-12.297 15.908-24.124.044-2.909-.388-5.813-1.179-8.704 3.995 5.714 6.083 11.978 5.698 19.643-.328 6.555-3.5 12.311-8.219 16.612.159 4.965-.643 9.33-2.17 13.09 6.669-1.594 12.884-3.729 18.564-6.462 2.31 2.558 5.196 4.287 5.196 4.287-.757-2.294-.072-4.897 1.243-7.407 2.986 3.705 9.759 5.198 9.759 5.198-1.907-4.616.657-11.27 2.907-15.704.317-.327.626-.662.936-1 2.141 1.68 4.213 2.692 4.213 2.692-.47-2.74.097-6.274 1.224-9.64 4.443-6.648 7.7-14.468 9.584-23.556 2.599.436 4.793.356 4.793.356-1.698-2.189-2.86-5.555-3.452-9.044.345-3.276.536-6.692.561-10.264 3.688-.179 7.229-1.612 7.229-1.612-3.634-2.293-7.029-7.698-8.702-12.54-2.254-12.688-6.87-25.21-13.065-36.7 19.358-28.634-12.97-74.232-30.875-51.154l-7.396 9.534c-10.869-5.877-23.226-8.89-37.343-8.89-14.113 0-26.473 3.013-37.341 8.89l-7.397-9.534z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h170.423v190.223H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(163 133)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M166.489 131.825c-1.473-1.058-3.205-1.59-5.055-1.64-.457 6.738-2.37 13.122-5.414 19.051.17-3.725.102-7.574.139-11.43.365-38.024-25.043-68.757-55.531-70.73-15.427-.998-24.246-2.62-32.522-17.258-4.209-7.476-8.694-13.01-14.506-15.995 5.49.17 9.962 1.896 13.843 4.935 7.902-9.387 12.119-22.148 11.271-34.633-.266-3.669-2.423-4.599-5.458-3.92C55.139 4.406 48.259 17.56 45.958 31.511c-2.137-.257-4.432-.262-6.922.011-18.57 2.036-29.837 12.815-38.007 30.464-6.125 13.231 16.172 29.308 38.519 26.05-4.534 3.588-11.775 5.888-19.235 6.687 3.513 34.395 5.22 55.208 23.367 73.087 2.47 2.45 1.558 5.335-1.846 5.937-7.459 1.338-16.863 4.724-16.792 13.26.079 2.416 1.348 3.198 3.909 3.216h30.665c6.359.03 9.516-1.617 12.461-6.109 5.481-9.447 7.894-13.456 21.287-15.267 4.308-.58 5.463-3.02 4.227-7.052-10.239-33.426 12.827-44.218 29.58-42.314-14.528 2.398-33.529 13.087-21.94 46.98 1.143 3.323.858 6.253-3.664 6.865-7.692.985-19.784 4.091-19.534 13.88.126 2.27 1.351 2.985 3.49 3.017h30.7c7-.122 9.536-2.118 12.4-6.023 5.81-8.472 13.706-8.113 19.675-13.87 3.003-2.897 4.851-6.236 5.996-9.864 4.231-.685 8.758-3.59 12.054-8.185 5.377-7.5 5.442-16.658.141-20.456" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M96 134h304.563v187.156H96z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M233.572 271.01c-6.76-4.6-11.25-9.185-15.986-13.274 3.792 1.377 8.023 3.209 13.55 5.875 4.745 2.288 10.686 2.315 14.2-3.954 6.953-12.513 6.215-24.384 2.593-35.755 7.358 12.029 11.76 24.825 3.788 39.516 24.56 8.805 51.835 8.101 80.266-2.195-10.935-20.436-10.743-44.23-1.969-63.405-5.608 24.92.452 53.337 22.912 72.526 3.892 3.067 10.577 7.094 19.964 2.115 3.8-2.017 7.911-5.055 11.643-8.464-6.773 10.094-16.673 16.925-26.168 17.712.62 5.349 1.587 9.12 4.62 10.944 1.496.913 1.996 2.159 1.705 4.097l-1.14 7.26c-.613 3.047-1.516 4.663-4.944 5.458-13.168 3.534-10.77 11.695-3.248 11.69h15.154c5.555-.036 7.675-2.274 9.697-5.745l10.8-20.05c2.724-5.373 3.196-9.243 1.618-16.325l-3.724-12.733c-.969-3.3-.899-5.469 1.3-8.149 4.538-5.501 6.385-10.606 7.854-19.371 1.993-12.962 4.435-36.15.066-53.626-5.476-29.484-25.04-49.834-55.373-44.32-58.384 12.407-79.885-1.403-97.514-3.75-21.65-2.876-31.163 8.51-54.29 18.17 6.589-7.674 4.7-17.8 4.358-21.257-17.537.597-19.418 11.694-22.262 24.364-7.197-.895-12.441 2.044-14.888 10.181-6.707 19.633-21.913 21.532-29.577 20.886-1.769-.16-3.044-.723-4.335-2.073-3.484-3.843-5-9.245-4.752-17.793.144-2.482-2.28-2.809-4.11-1.32-4.522 3.993-9.938 13.102-11.465 27.708-.33 2.956-.517 3.703-2.258 6.166-3.195 4.77-5.135 10.542-5.635 16.787-.1 1.517.113 2.431 1.218 3.599 3.305 3.358 7.055 4.495 11.96 4.276 2.502-.089 3.088.005 4.891 1.84 12.791 13.16 20.004 6.143 42.201 8.222 22.875 2.137 39.078-5.802 46.26-22.102-1.034 12.313-6.938 22.012-17.133 28.431 2.766 4.267 6.674 7.578 13.054 9.51 1.803.57 2.582 1.412 2.535 3.635-.1 11.664 1.936 19.135 3.291 26.036 1.909 10.214 4.25 24.27-7.288 27.113-13.167 3.534-10.773 11.695-3.246 11.69h15.153c5.552-.094 9.04-1.625 9.691-5.745 1.826-11.108 3.626-14.96 7.916-24.43 2.928-6.469 5.201-12.769 7.047-19.97" fill="#FEFEFE" mask="url(#d)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M149 133h197.237v189.77H149z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M271.983 301.966c-8.364.88-17.497-1.828-24.363-9.108-6.87 7.276-16.007 9.988-24.373 9.113-9.277 7.49-8.469 20.8 4.571 20.8h39.6c13.044 0 13.847-13.317 4.565-20.805m-5.28-75.342c-.039 1.26 1.02 1.754 2.108 1.212 8.188-4.17 15.895-2.294 20.334-18.03.253-.933-.016-1.463-1.016-1.665-4.606-.938-11.352-2.32-15.98 1.017-4.78 3.45-5.047 11.206-5.446 17.466m-38.173 0c-.399-6.26-.663-14.016-5.447-17.466-4.624-3.337-11.37-1.955-15.975-1.017-1 .202-1.27.732-1.018 1.665 4.44 15.736 12.148 13.86 20.334 18.03 1.09.542 2.15.048 2.106-1.212m19.087-52.888c10.23 0 20.464 1.781 30.69 5.34 2.265.695 3.222-1.32 1.772-2.685-9.789-8.417-21.04-12.329-32.462-12.26-11.42-.069-22.672 3.843-32.46 12.26-1.449 1.365-.494 3.38 1.771 2.685 10.227-3.559 20.458-5.34 30.69-5.34m-25.682 22.118c14.401-3.159 36.964-3.159 51.363 0 2.24.462 2.76-1.326 1.487-2.249-14.989-10.87-39.347-10.87-54.335 0-1.274.923-.754 2.711 1.485 2.25m84.907 91.212c18.226-11.169 27.342-44.08 6.566-76.568-1.112-1.646-3.009-.658-2.366 1.69 5.31 18.81 9.856 44.55-6.773 72.111-1.37 2.442.575 3.904 2.573 2.767m-118.45 0c1.997 1.137 3.945-.325 2.572-2.767-16.632-27.562-12.082-53.302-6.77-72.112.642-2.347-1.256-3.335-2.368-1.689-20.778 32.487-11.66 65.4 6.565 76.568m109.691-72.166c-1.47-1.436-3.266-.266-2.617 1.638 4.863 13.282 5.382 21.047 2.34 34.861-.702 3.111 2.083 4.15 3.734 2.05 8.518-11.839 6.568-27.957-3.457-38.549m-100.929 0c-10.029 10.592-11.98 26.71-3.459 38.548 1.652 2.101 4.436 1.062 3.731-2.049-3.038-13.814-2.519-21.579 2.344-34.86.65-1.905-1.148-3.075-2.616-1.639m10.684 99.41c-.587-4.866 2.393-10.63 8.83-13.823-7.377-2.622-13.304-8.265-15.13-16.082-3.725-15.93 2.586-27.452 9.781-36.574-3.448 8.332-6.012 17.802-4.478 28.098 2.651 17.814 23.762 22.192 38.972 4.347a5.937 5.937 0 0 1-1.229-.718l-10.733-8.355c-4.53-3.792.198-11.802 7.68-8.432 1.526.658 2.075 1.026 3.837 1.01h4.5c1.761.016 2.31-.352 3.838-1.01 7.478-3.37 12.21 4.64 7.68 8.432l-10.738 8.355a5.855 5.855 0 0 1-1.224.718c15.205 17.85 36.32 13.472 38.971-4.347 1.525-10.234-1.002-19.658-4.42-27.952 7.173 9.168 13.435 20.724 9.721 36.428-1.847 7.811-7.772 13.453-15.142 16.076 6.448 3.19 9.431 8.96 8.84 13.83 13.604-4.805 25.07-12.445 33.402-23.119 3.515 2.727 7.033 8.127 8.982 16.097 7.267-11.59 5.723-25.319 2.934-33.122 2.974 1.391 6.955 5.561 9.13 12.083 4.117-12.372 1.098-24.299-4.144-31.306 3.438.722 6.39 3.253 8.212 6.202 1.823-13.045-4.316-26.077-10.38-32.4 2.431.021 5.142 1.17 6.873 2.732-3.695-15.869-13.086-25.616-16.297-28.566a100.124 100.124 0 0 0-5.95-10.615c-3.995-5.673-3.172-11.237 1.441-15.836 18.95-19.27-6.892-57.465-26.66-38.16l-10.595 10.768c-5.095 5.18-9.29 6.324-16.369 4.498a82.25 82.25 0 0 0-20.356-2.532c-7.098 0-13.9.887-20.354 2.532-7.079 1.826-11.274.682-16.37-4.498l-10.598-10.769c-19.765-19.304-45.61 18.89-26.657 38.161 4.615 4.6 5.436 10.163 1.443 15.836a98.73 98.73 0 0 0-5.95 10.615c-3.21 2.95-12.603 12.697-16.298 28.566 1.728-1.563 4.443-2.71 6.873-2.733-6.064 6.324-12.203 19.356-10.38 32.401 1.824-2.949 4.774-5.48 8.213-6.202-5.245 7.007-8.26 18.934-4.148 31.306 2.178-6.522 6.156-10.692 9.13-12.083-2.789 7.803-4.329 21.532 2.934 33.122 1.953-7.97 5.47-13.37 8.985-16.097 8.334 10.67 19.795 18.314 33.398 23.118" fill="#FEFEFE" mask="url(#d)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)" fill="#FEFEFE"><path d="M188.74 245.033c-3.953 0-7.161 1.979-7.161 4.426 0 2.444 3.208 4.427 7.162 4.427 3.954 0 7.161-1.983 7.161-4.427 0-2.447-3.207-4.426-7.161-4.426m21.144 8.09c-7.708-.69-10.217 6.19-21.144 6.19-10.924 0-13.433-6.88-21.144-6.19 5.133 3.257 8.929 12.703 21.144 12.703 12.215 0 16.018-9.446 21.144-12.703m147.05-30.274c2.975-9.662 5.02-12.315 11.313-17.998 31.238-27.646 30.43-51.658 24.545-70.851-5.557 18.187-33.065 4.357-40.66 28.398-13.773-21.315-34.758.35-49.452-13.277 1.532 27.69 25.526 33.642 38.183 48.287 4.444 5.152 3.989 11.73-1.956 15.517-49.149 31.406-100.128 1.906-149.571 1.017-29.169-.523-58.305 6.71-79.76 14.678-9.511 3.825-11.391 10.684-8.035 19.454 13.923 32.119 49.884 52.038 90.795 58.184-16.337.539-32.45-1.076-47.332-4.852-5.706 5.575-12.046 11.423-19.477 17.533 32.45 7.447 57.723.387 78.558-11.304a189.342 189.342 0 0 0 40.502-.944c32.71 10.862 73.437 22.47 117.684-7.714-51.504-16.794-68.083-38.293-121.901-33.977 30.804-9.268 54.943-2.28 83.925 7.978 14.89-12.964 26.424-29.613 32.64-50.129"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M255.5-33L477 95v256L255.5 479 34 351V95z"/><path id="c" d="M0 0h197.01v186.749H0z"/></defs><g transform="translate(1 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#1E70B7" transform="rotate(30 255.5 223)" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(149 135)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M49.467 17.814c-5.418-4.322-11.367-7.31-17.501-7.373-4.208-.017-5.188-.143-7.916-2.47C19.304 3.894 16.122 1.23 11.564.16 9.108-.35 8.214.324 7.7 2.604c-1.15 5.252-2.128 7.936-.598 15.377.496 2.086-1.204 2.514-3.507 1.135-3.663-2.276-5.075 3.447-1.612 7.282 3.112 3.439 7.559 6.192 8.223 15.72.26 3.761.578 6.618.995 9.014.483 2.634.568 3.038.113 5.636-3.691 19.968 4.455 21.12 9.908 33.044 4.086 8.933 8.75 17.055 14.4 22.549 2.416 2.293 3.74 3.88 5.048 7.015 4.922 11.494 9.567 28.374 8.574 50.046-.3 6.38-3.776 10.275-10.251 8.8-3.782-.86-7.311.24-7.3 4.123.006 1.734.618 4.185 3.927 4.405h14.67c3.519-.014 6.028-1.376 7.004-4.792 5.766-21.198 7.343-36.803 8.017-52.548.4-6.025 2.717-9.076 9.089-9.544 15.108-.951 32.244-3.475 46.518-6.29-2.863-9.972-4.225-20.618-2.973-33.955 4.204 27.358 11.447 31.555 28.017 55.235 8.376 11.97 7.227 30.77-4.102 41.189-.913.84-1.938 1.534-3.082 1.988-2.16.862-2.996.672-5.11.189-3.78-.858-7.31.24-7.3 4.123.008 1.734.618 4.185 3.927 4.405h14.67c3.53-.053 5.39-1.316 6.736-3.811l13.022-24.778c3.375-6.477 2.747-11.292-.021-17.974-1.057-2.462-2.313-6.117-3.357-9.33-1.78-5.84-.413-9.097 2.269-13.242 4.059-6.362 10.075-10.92 14.302-20.597-.55 6.769-3.346 12.22-6.521 17.24-3.15 5.21-3.77 8.492-2.05 14.373 3.522 12.064 7.018 22.587 8.536 26.661 3.067 7.749 7.642 9.631 10.662 12.84 1.363 1.376 2.358 1.154 3.126-.572 1.67-4.08 5.263-7.486 5.34-15.825-.115-7.849-1.454-36.989-6.296-61.132-5.165-25.71-17.168-45.135-48.56-44.38-23.8.995-50.62 5.209-69.908-6.633-7.2 7.161-16.35 8.217-25.05 2.631 9.129 2.359 17.167-1.261 22.149-9.938 1.434-2.602 1.067-5.016-1.773-6.18l-13.266-5.427c-1.73-.726-3.018-1.351-4.84-2.862" fill="#FEFEFE" mask="url(#d)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274zM329.011 135.763a5.232 5.232 0 0 0-5.223 5.23 5.232 5.232 0 0 0 5.223 5.23 5.236 5.236 0 0 0 5.231-5.23 5.236 5.236 0 0 0-5.23-5.23m0 40.237C309.705 176 294 160.297 294 140.993 294 121.698 309.706 106 329.011 106 348.303 106 364 121.698 364 140.993 364 160.297 348.303 176 329.011 176" fill="#FFF"/><path d="M330.511 101C308.173 101 290 119.164 290 141.492 290 163.828 308.173 182 330.511 182 352.836 182 371 163.828 371 141.492 371 119.164 352.836 101 330.511 101m0 51.022c5.823 0 10.531-4.716 10.531-10.53a10.517 10.517 0 0 0-10.53-10.529 10.51 10.51 0 0 0-10.523 10.529c0 5.814 4.7 10.53 10.522 10.53m0-40.496c16.563 0 29.963 13.406 29.963 29.966 0 16.555-13.4 29.982-29.963 29.982-16.555 0-29.985-13.427-29.985-29.982 0-16.56 13.43-29.966 29.985-29.966" fill="#AD245D"/><path d="M331 106.209c-20.305 0-36.825 16.06-36.825 35.799 0 19.747 16.52 35.813 36.825 35.813 20.306 0 36.827-16.066 36.827-35.813 0-19.74-16.521-35.8-36.827-35.8zM314.287 215l-4.11-21.345c-.324-.129-.648-.265-.972-.404l-18.012 12.169-23.607-23.609 12.186-18.009a63.31 63.31 0 0 1-.403-.968L258 158.712v-33.383l21.361-4.13c.131-.327.267-.652.407-.979l-12.18-18.025 23.608-23.612 18.015 12.198c.322-.137.643-.27.964-.4L314.287 69h33.416l4.13 21.387c.319.13.638.26.956.396l18.024-12.2 23.608 23.612-12.186 18.031c.139.324.273.648.402.971L404 125.33v33.381l-21.37 4.124c-.13.32-.262.64-.398.96l12.19 18.017-23.606 23.609-18.021-12.171c-.32.137-.642.27-.964.402L347.701 215h-33.414z" fill="#FFF"/><path d="M330 171.448c-17.342 0-31.45-13.656-31.45-30.44 0-16.778 14.108-30.427 31.45-30.427 17.341 0 31.449 13.649 31.449 30.426 0 16.785-14.108 30.441-31.45 30.441zM350.979 63h-41.97l-1.64 8.517-1.953 10.156-8.55-5.788-7.18-4.862-6.132 6.132-17.399 17.4-6.126 6.126 4.85 7.178 5.8 8.583-10.173 1.966-8.506 1.646v41.932l8.51 1.643 10.16 1.962-5.787 8.553-4.858 7.179 6.13 6.13 17.399 17.399 6.126 6.126 7.177-4.85 8.56-5.782 1.954 10.141 1.64 8.513H350.975l1.645-8.507 1.964-10.15 8.566 5.787 7.178 4.846 6.124-6.124 17.4-17.399 6.13-6.13-4.858-7.18-5.788-8.554 10.153-1.96 8.51-1.643v-41.932l-8.507-1.646-10.165-1.965 5.8-8.584 4.85-7.178-6.125-6.126-17.4-17.4-6.13-6.13-7.18 4.858-8.558 5.792-1.964-10.166L350.98 63zm-20.98 118.948c23.176 0 41.95-18.318 41.95-40.94 0-22.607-18.774-40.927-41.95-40.927-23.174 0-41.948 18.32-41.948 40.926 0 22.623 18.774 40.941 41.949 40.941zM342.313 73.5l3.855 19.963a47.184 47.184 0 0 1 6.037 2.502l16.824-11.386 17.4 17.4-11.362 16.818a53.171 53.171 0 0 1 2.502 6.066l19.932 3.855v24.601l-19.932 3.848a56.644 56.644 0 0 1-2.502 6.066l11.362 16.795-17.4 17.4-16.824-11.364a44.931 44.931 0 0 1-6.037 2.504l-3.855 19.932H317.68l-3.84-19.932a43.821 43.821 0 0 1-6.043-2.504l-16.818 11.364-17.4-17.4 11.364-16.795a53.759 53.759 0 0 1-2.51-6.066l-19.933-3.848v-24.601l19.933-3.855a50.617 50.617 0 0 1 2.51-6.066l-11.364-16.818 17.4-17.4 16.818 11.386a45.957 45.957 0 0 1 6.043-2.502l3.84-19.963h24.632z" fill="#AD245D"/></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><g fill="#FFF"><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274z"/><text font-family="Impact" font-size="118" font-style="condensed" font-weight="700" transform="translate(1 -1)"><tspan x="256" y="208">&lt;/&gt;</tspan></text></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384 .297L511.392 221.65l-128 221.702-255.392.352L.608 222.35 128.608.65z"/><g fill="#FFF"><path d="M34.944 367.726c-.081-21.169-.205-53.162 21.215-71.332 3.81-3.253 9.91-7.497 17.288-9.224 2.57-.523 4.946-.756 7.247-.979 4.43-.431 8.254-.804 12.03-2.9l4.945-2.846c9.851-5.655 19.157-10.996 28.171-17.377 5.075-3.632 6.713-15.73 6.082-25.428-.213-2.792-1.888-5.7-3.662-8.777-1.095-1.901-2.228-3.867-3.059-5.916l-.073-.199a57.061 57.061 0 0 1-.42-1.443c-1.194-4.205-1.93-6.378-2.382-7.476-7.015-.944-11.778-8.647-12.864-21.006l-.03-.557c-.644-12.785.806-16.13 2.31-17.716.241-.254.505-.475.783-.666-1.75-16.051 3.11-32.521 13.331-44.704 9.296-11.079 21.912-17.18 35.524-17.18 3.722 0 7.535.458 11.332 1.362 25.58 6.228 41.597 30.27 39.983 59.227.53.251 1.016.61 1.439 1.066 2.745 2.964 2.464 10.97 2.215 14.276l-.024.41c-.335 5.236-.683 10.65-3.046 15.73-1.736 3.918-4.397 6.242-6.747 8.29-2.391 2.089-4.28 3.735-5.284 6.885-.698 2.416-1.642 4.866-2.554 7.235-1.749 4.538-3.4 8.827-3.534 13.244-.425 10.222 1.17 18.391 4.165 21.359 5.087 5.163 12.977 9.391 19.939 13.121 1.597.855 3.16 1.692 4.645 2.517 9.262 5.052 16.038 7.915 25.259 8.557 9.1.849 18.02 5.193 24.706 11.97.735.641 1.817 1.744 3.687 3.648 4.408 4.492 4.408 4.492 4.417 5.852l.007.758c10.762 17.702 11.12 40.656 11.392 58.169l.05 3.28-3.271.028c-41.968.363-83.894.677-125.812.993-42.038.314-84.067.632-126.12.994l-3.267.029-.013-3.274zM332.387 115.763h15.318v86.734h-15.318v.513l-22.127-17.64v.12h-10.21V211h-22.128v-25.51h-20.424v-52.722h52.762v-.508l22.127-17.065v.568zm34.313 72.093l-7.988-4.591c15.803-27.453 1.717-46.642 1.106-47.443l7.304-5.607c.774.993 18.547 24.675-.422 57.641zm27.361 21.216l-13.866-7.975c27.437-47.66 2.98-80.973 1.918-82.36L394.795 109c1.343 1.723 32.2 42.839-.734 100.072z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 .89h265.71v170.52H0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M210.84 346.697l2.17.412c3.654-5.4 7.304-10.797 11.167-16.514l-13.948-4.158c.216 7.17.415 13.715.61 20.26" fill="#FEFEFE"/><g transform="translate(124 96.872)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M265.476 135.466c2.28-16.36-12.373-28.485-33.146-37.925-3.264-1.498-32.042-13.807-71.066-11.423-.493-.025-.99-.055-1.485-.077-4.622-.21-8.12-1.25-9.72-6.232 8.024-2.458 14.575-6.522 17.408-15.188 2.705-8.281.812-14.61-5.396-18.966 3.091-4.423 7.693-7.442 11.87-10.785 5.529-4.425 11.32-7.711 18.427-8.009.24.227.53.414.906.525 2.803.837 4.097-3.51 1.295-4.345-1.034-.309-1.896.11-2.368.801-6.4.293-11.767 2.709-17.154 6.329-5.428 3.635-11.634 8.205-15.427 13.712-.48-1.289-2.718-6.195-9.466-8.056-4.63-1.276-7.919-.39-9.835.541 1.488-12.829-8.348-26.169-18.146-33.456-.048-.826-.524-1.61-1.556-1.917-2.804-.835-4.097 3.502-1.295 4.337.41.121.772.127 1.11.054 9.044 6.741 19.157 19.93 16.758 31.839-.028.13-.005.243-.002.363-16.899-.786-23.471 21.104-10.53 35.477-4.684 2.295-5.616 2.748-8.613 1.177-32.087-24.754-64.257-30.463-67.89-31.01C27.605 39.755 8.707 41.88 1.662 56.82-2.182 65.082.903 73.342 8.21 80.396A46.714 46.714 0 0 0 10.342 95.9c13.312 41.39 67.248 17.494 97.338 5.981 1.043 2.698 2.299 5.946 3.627 9.388l1.733.513c-12.762 1.374-25.1 8.753-34.245 20.253 11.938-.127 26.762 2.062 42.317 6.698 15.322 4.568 28.756 10.738 38.637 17.268-1.399-14.62-7.72-27.531-17.685-35.634l.046.013 9.111-6.463c18.754 25.75 51.289 77.278 85.606 49.502a46.669 46.669 0 0 0 10.267-11.807c9.978-1.902 17.079-7.125 18.382-16.146" fill="#FEFEFE" mask="url(#b)"/></g><path d="M239.142 255.648c-17.848-5.32-34.719-7.408-47.378-6.481a74.024 74.024 0 0 0-.923 2.876 70.569 70.569 0 0 0-2.728 14.609c12.465-.643 28.687 1.501 45.785 6.598 16.902 5.04 31.502 12.025 41.573 19.298a70.533 70.533 0 0 0 6.48-16.67c-10.089-7.605-25.188-14.976-42.809-20.23" fill="#E5ECD6"/><path d="M189.62 286.635c3.972 16.568 14.648 29.797 29.836 34.325 15.22 4.537 31.434-.721 43.835-12.47-9.486-5.756-21.669-11.116-35.364-15.198-13.858-4.13-27.135-6.319-38.307-6.657" fill="#FEFEFE"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.073.459h124.425v172.749H.073z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M232.409 254.848c.268-7.727 7.016-33.547 7.258-39.582.838-19.569-19.295-37.316-35.404-44.459-20.896-9.313-46.108-15.054-68.428-7.619-17.536 5.837-23.16 21.744-17.995 38.456 5.905 18.931 25.96 23.883 43.23 27.984-2.735 6.685-13.588 12.887-16.81 23.005-2.78 8.729-2.97 17.367-1.227 26.318.333 1.703 1.236 3.314 1.819 4.836 5.689 14.863 40.24 34.999 49.693 39.224 3.09 1.366 18.603 9.7 24.605 9.519.476-.016.962-.047 1.453-.096 12.115-1.246 17.577-9.799 19.811-20.828.36-1.791-8.229-49.896-8.005-56.758" fill="#E5ECD6"/><path d="M268.885 204.496c-.268-7.83-8.948-10.453-13.935-7.422-2.966.291-5.702 2.075-6.746 5.894-4.237 15.355-1.235 30.542-.916 46.19.358 17.416-.37 34.49 2.024 51.804 1.158 8.493 14.876 8.837 16.773.612 3.27-14.248 1.438-28.557 1.233-42.969-.291-18.067 2.216-36.006 1.567-54.11" fill="#FEFEFE"/><path d="M268.54 185.138c3.437-13.134 12.433-23.528 20.018-34.472 9.29-13.399 19.905-24.457 35.392-29.791.675.343 1.439.565 2.342.565 6.73 0 6.73-10.44 0-10.44-2.483 0-4.111 1.491-4.696 3.324-13.922 4.858-24.18 13.72-33.687 25.246-10.257 12.408-21.876 27.757-26.012 43.492-1.119 4.336 5.523 6.417 6.643 2.076M251.284 184.303c-2.824-31.263-34.996-55.942-62.636-66.026-.649-1.789-2.215-3.207-4.696-3.207-6.732 0-6.732 10.42 0 10.42.979 0 1.782-.23 2.481-.612 24.382 8.926 55.359 31.37 57.89 59.222.381 4.386 7.37 4.679 6.961.203" fill="#BBD094"/><g transform="translate(273.18 159.325)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M8.09 95.524C7.822 87.797 1.074 61.977.832 55.94c-.838-19.568 19.294-37.316 35.404-44.458 20.896-9.313 46.106-15.055 68.427-7.62C122.2 9.702 127.824 25.608 122.66 42.32c-5.905 18.932-25.961 23.884-43.23 27.984 2.734 6.685 13.588 12.887 16.81 23.005 2.78 8.73 2.97 17.367 1.226 26.318-.334 1.704-1.236 3.315-1.818 4.836-5.69 14.863-40.24 35-49.695 39.225-3.088 1.366-18.601 9.7-24.604 9.518a21.536 21.536 0 0 1-1.453-.095C7.78 171.864 2.32 163.31.085 152.282c-.36-1.792 8.228-49.896 8.005-56.758" fill="#E5ECD6" mask="url(#b)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.474.589h222.082V155.85H.474z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M106 295.77c0-8.772 4.114-14.323 7.973-17.622 3.405-2.91 6.612-4.072 6.612-4.072s.991-13.569 10.898-22.434c.097-.086.18-.179.278-.265 3.804-3.31 8.891-5.92 15.692-6.854.112-.015.214-.039.33-.053.545-.07 1.087-.144 1.654-.193 3.884-.328 7.329-.025 10.403.706 18.218 4.333 22.977 24.233 22.977 24.233s9.721-6.152 19.125.324c9.397 6.48 7.454 15.87 7.454 15.87 17.507-2.266 26.092 4.209 26.092 18.623 0 14.412-17.015 17.001-17.015 17.001h-93.672s-.333-.157-.89-.452C120.203 318.614 106 310.138 106 295.771" fill="#FEFEFE"/><path d="M333.747 275.73c0-55.854-45.477-101.293-101.378-101.293-43.712 0-80.995 27.807-95.194 66.632a36.758 36.758 0 0 0-9.437 5.96l-.479.442c-5.63 5.087-8.934 11.15-10.906 16.39 5.97-58.709 55.707-104.681 116.016-104.681 64.32 0 116.648 52.284 116.648 116.55 0 16.978-3.683 33.105-10.245 47.668h-16.984a100.603 100.603 0 0 0 11.96-47.669" fill="#E5ECD6"/><path d="M320.29 275.73c0-48.44-39.44-87.85-87.923-87.85-34.819 0-64.929 20.35-79.154 49.752-.883.022-1.766.048-2.66.124-.625.051-1.223.127-1.818.205l-.773.112c-3.737.513-7.249 1.487-10.519 2.889 14.22-38.649 51.374-66.313 94.924-66.313 55.784 0 101.17 45.345 101.17 101.08a100.39 100.39 0 0 1-11.987 47.67h-15.386c8.92-13.742 14.127-30.106 14.127-47.67" fill="#CCDBAE"/><path d="M308.526 275.73c0-41.957-34.164-76.092-76.156-76.092-28.736 0-53.744 16.016-66.704 39.555-.62-.182-1.233-.366-1.877-.518-3.268-.778-6.708-1.125-10.244-1.06 14.209-29.225 44.179-49.44 78.825-49.44 48.315 0 87.626 39.278 87.626 87.554 0 17.574-5.227 33.94-14.183 47.67h-14.145c10.533-13.055 16.858-29.635 16.858-47.67" fill="#B5CB88"/><g transform="translate(182.963 98.98)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M104.288 16.014c56.52-.178 102.65 45.62 102.829 102.093.04 13.318-2.485 26.05-7.103 37.743h16.444c3.993-11.874 6.14-24.585 6.097-37.791C222.35 53.079 169.273.382 104.241.588 59.286.73 20.245 26.132.474 63.257c7.3-3.05 14.945-5.433 22.867-7.06 18.668-24.318 47.967-40.079 80.947-40.183" fill="#E5ECD6" mask="url(#b)"/></g><path d="M376.475 217.131c.042 13.462-2.936 26.238-8.287 37.7h14.566c4.626-11.69 7.153-24.425 7.11-37.742-.178-56.354-46.21-102.058-102.612-101.88-32.822.104-61.988 15.75-80.621 39.905 6.557-1.328 13.306-2.117 20.192-2.376 15.83-14.881 37.068-24.076 60.472-24.15 49.019-.155 89.023 39.565 89.18 88.543" fill="#CCDBAE"/><path d="M364.578 217.169c.042 13.669-3.534 26.506-9.78 37.663h13.049a87.88 87.88 0 0 0 8.33-37.7c-.155-48.812-40.028-88.4-88.881-88.246-23.205.076-44.275 9.145-60.024 23.84 1.434-.048 2.867-.108 4.313-.108 4.568 0 9.074.26 13.516.738 12.112-8.07 26.607-12.831 42.23-12.881 42.461-.134 77.112 34.271 77.247 76.694" fill="#B5CB88"/><path d="M354.798 254.83h-.377c.03.174.072.345.102.519.098-.17.18-.349.275-.518" fill="#CABAA1"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M238.234 309.762c-1.375.258-2.792 1.55-3.708 2.736-1.608 2.081-3.007 1.861-5.165.778-8.43-4.235-16.65-1.328-20.991 6.88-.792 1.496-2.614 2.848-4.257 3.43-6.343 2.24-10.08 6.638-10.719 13.498h98.273c-.897-5.657-3.359-9.528-8.075-12.241-2.12-1.22-4.18-3.107-5.416-5.194-4.232-7.157-10.743-9.897-18.71-7.382-3.109.983-9.193-2.98-10.331-6.279h-6.066c-.46 1.829-1.749 3.193-4.835 3.774" fill="#FEFEFE"/><path d="M252.144 200.674c-2.639 3.425-9.63 13.408-11.283 24.964l-.87 5.505c-2.211-5.534-6.18-10.496-11.796-14.408-11.918-8.303-25.534-11.544-39.636-13.142-6.885-.78-13.842-.944-21.559-1.436 4.949 20.969 11.08 40.049 25.452 55.231 6.342 6.698 14.362 10.992 23.85 10.166 5.9-.514 11.66-2.65 17.862-4.149 4.946 11.405 7.394 24.204 8.876 37.28.143 1.266.255 2.46.227 3.56h5.569c-.882-7.237-1.761-14.484-2.983-21.665-.397-2.335-.79-4.38-.575-5.91.299-1.79.748-1.79 1.582-3.072.884-1.193 2.198-2.26 4.092-3.402 2.143 1.826 3.998 3.807 6.216 5.223 4.423 2.827 7.262 5.105 14.637 5.803 2.418.234 5.006.276 7.187-.337 4.318-1.207 8.743-2.716 12.51-5.075 14.244-8.905 24.206-21.803 32.765-35.977-.592-.457-.912-.851-1.319-.994-14.417-5.058-29.212-8.309-44.538-6.92-8.774.796-17.394 3.209-23.143 10.763-3.56 4.676-6.985 9.634-5.063 16.138-.412 3.335-1.967 5.828-4.244 7.79-1.402-12.036-2.767-39.862 11.142-58.371" fill="#E5ECD6"/><path d="M258.188 206.85s7.083 18.602 21.241 17.487c0 0 20.87.371 21.987-27.161 0 0 21.198 8.399 32.42-4.093 0 0 15.653-18.974-8.567-34.6 0 0 18.63-8.93 11.921-30.137 0 0-6.707-19.347-35.028-7.812 0 0-.746-26.045-23.107-26.788 0 0-23.538.821-21.239 27.53 0 0-23.85-13.022-35.03 5.582 0 0-10.062 18.973 10.435 31.996 0 0-22.36 17.115-7.826 35.719 0 0 10.435 12.649 27.762 4.836m25.716-27.533c-6.827 0-12.36-5.525-12.36-12.342 0-6.816 5.533-12.342 12.36-12.342 6.826 0 12.36 5.526 12.36 12.342 0 6.817-5.534 12.342-12.36 12.342" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M.318.89h249.647v250.352H.318z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#84A939" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><g transform="translate(135.784 96.866)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M124.802 234.855c59.98 0 108.778-48.803 108.778-108.788 0-59.988-48.797-108.79-108.778-108.79-46.9 0-86.909 29.866-102.143 71.563a39.335 39.335 0 0 0-10.123 6.401l-.513.474C5.98 101.181 2.435 107.691.318 113.318 6.725 50.265 60.092.89 124.802.89c69.014 0 125.163 56.153 125.163 125.177 0 69.021-56.15 125.175-125.163 125.175-46.925 0-87.849-25.989-109.267-64.306l19.208-.021c19.576 28.89 52.615 47.94 90.059 47.94" fill="#E5ECD6" mask="url(#b)"/></g><path d="M260.586 114.37c59.852 0 108.55 48.701 108.55 108.562 0 59.86-48.698 108.561-108.55 108.561-37.305 0-70.211-18.96-89.753-47.711l17.75-.02c17.315 20.476 43.151 33.52 72.003 33.52 52.019 0 94.34-42.325 94.34-94.35 0-52.025-42.321-94.35-94.34-94.35-37.36 0-69.668 21.853-84.936 53.43-.947.027-1.892.055-2.851.136-.673.056-1.31.135-1.949.22l-.834.12c-4.005.55-7.774 1.598-11.285 3.104 15.257-41.51 55.121-71.222 101.855-71.222" fill="#CCDBAE"/><path d="M260.586 128.899c51.845 0 94.023 42.182 94.023 94.033s-42.178 94.033-94.023 94.033c-28.657 0-54.333-12.913-71.588-33.202l17.15-.02c14.463 12.977 33.527 20.913 54.438 20.913 45.057 0 81.715-36.662 81.715-81.724 0-45.062-36.658-81.724-81.715-81.724-30.832 0-57.664 17.202-71.57 42.483-.663-.194-1.324-.392-2.016-.556-3.508-.834-7.195-1.207-10.99-1.138 15.245-31.39 47.403-53.098 84.576-53.098" fill="#B5CB88"/><path d="M127 247.046c0-9.422 4.415-15.384 8.555-18.926 3.653-3.125 7.094-4.374 7.094-4.374s1.065-14.572 11.693-24.095c.104-.091.195-.192.299-.285 4.083-3.554 9.543-6.356 16.837-7.361.12-.016.231-.04.354-.057.584-.073 1.167-.154 1.773-.207 4.169-.351 7.867-.026 11.166.76 19.546 4.653 24.653 26.027 24.653 26.027s10.43-6.608 20.522.347c10.082 6.958 7.997 17.045 7.997 17.045 18.783-2.436 27.997 4.52 27.997 20 0 15.478-18.256 18.259-18.256 18.259H147.17s-.357-.167-.954-.485C142.24 271.58 127 262.478 127 247.046" fill="#FEFEFE"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,236 @@
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
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 uuid from 'uuid/v4'
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}`)
let toActorName = extractNameFromId(activity.object)
let 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)
let 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)
default:
}
}
handleCreateActivity(activity) {
debug('inside create')
switch (activity.object.type) {
case 'Article':
case 'Note':
const articleObject = activity.object
if (articleObject.inReplyTo) {
return this.dataSource.createComment(activity)
} else {
return this.dataSource.createPost(activity)
}
default:
}
}
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

@ -0,0 +1,28 @@
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

@ -0,0 +1,579 @@
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:nitro-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

@ -0,0 +1,54 @@
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

@ -0,0 +1,27 @@
import user from './user'
import inbox from './inbox'
import webFinger from './webFinger'
import express from 'express'
import cors from 'cors'
import verify from './verify'
const router = express.Router()
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
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,
)
export default router

View File

@ -0,0 +1,54 @@
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

@ -0,0 +1,92 @@
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

@ -0,0 +1,20 @@
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

@ -0,0 +1,43 @@
import express from 'express'
import { createWebFinger } from '../utils/actor'
import gql from 'graphql-tag'
const router = express.Router()
router.get('/', async function(req, res) {
const resource = req.query.resource
if (!resource || !resource.includes('acct:')) {
return res
.status(400)
.send(
'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
)
} else {
const nameAndDomain = resource.replace('acct:', '')
const name = nameAndDomain.split('@')[0]
let result
try {
result = await req.app.get('ap').dataSource.client.query({
query: gql`
query {
User(slug: "${name}") {
slug
}
}
`,
})
} catch (error) {
return res.status(500).json({ error })
}
if (result.data && result.data.User.length > 0) {
const webFinger = createWebFinger(name)
return res.contentType('application/jrd+json').json(webFinger)
} else {
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
}
}
})
export default router

View File

@ -0,0 +1,104 @@
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

@ -0,0 +1,172 @@
// 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)
let 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

@ -0,0 +1,116 @@
import { activityPub } from '../ActivityPub'
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

@ -0,0 +1,38 @@
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,
},
}
}
export function createWebFinger(name) {
const { host } = new URL(activityPub.endpoint)
return {
subject: `acct:${name}@${host}`,
links: [
{
rel: 'self',
type: 'application/activity+json',
href: `${activityPub.endpoint}/activitypub/users/${name}`,
},
],
}
}

View File

@ -0,0 +1,73 @@
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

@ -0,0 +1,109 @@
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(async (resolve, reject) => {
debug('inside signAndSend')
// get the private key
const result = await activityPub.dataSource.client.query({
query: gql`
query {
User(slug: "${fromName}") {
privateKey
}
}
`,
})
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

@ -0,0 +1,12 @@
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective,
} from 'graphql-custom-directives'
export default function applyDirectives(augmentedSchema) {
const directives = [GraphQLLowerCaseDirective, GraphQLTrimDirective, GraphQLDefaultToDirective]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
return augmentedSchema
}

View File

@ -0,0 +1,16 @@
import { v1 as neo4j } from 'neo4j-driver'
import CONFIG from './../config'
let driver
export function getDriver(options = {}) {
const {
uri = CONFIG.NEO4J_URI,
username = CONFIG.NEO4J_USERNAME,
password = CONFIG.NEO4J_PASSWORD,
} = options
if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
}
return driver
}

View File

@ -0,0 +1,9 @@
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from 'graphql-iso-date'
export default function applyScalars(augmentedSchema) {
augmentedSchema._typeMap.Date = GraphQLDate
augmentedSchema._typeMap.Time = GraphQLTime
augmentedSchema._typeMap.DateTime = GraphQLDateTime
return augmentedSchema
}

View File

@ -0,0 +1,46 @@
import dotenv from 'dotenv'
dotenv.config()
const {
MAPBOX_TOKEN,
JWT_SECRET,
PRIVATE_KEY_PASSPHRASE,
SMTP_IGNORE_TLS = true,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
NEO4J_URI = 'bolt://localhost:7687',
NEO4J_USERNAME = 'neo4j',
NEO4J_PASSWORD = 'neo4j',
GRAPHQL_PORT = 4000,
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
} = process.env
export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,
SMTP_IGNORE_TLS,
SMTP_USERNAME,
SMTP_PASSWORD,
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
MOCKS: process.env.MOCKS === 'true',
DISABLED_MIDDLEWARES:
(process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
}
export default {
...requiredConfigs,
...smtpConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,
}

View File

@ -0,0 +1,14 @@
/**
* Provide a way to iterate for each element in an array while waiting for async functions to finish
*
* @param array
* @param callback
* @returns {Promise<void>}
*/
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
export default asyncForEach

View File

@ -0,0 +1,28 @@
/**
* iterate through all fields and replace it with the callback result
* @property data Array
* @property fields Array
* @property callback Function
*/
function walkRecursive(data, fields, 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
data = callback(data, _key)
} else if (data && Array.isArray(data)) {
// go into the rabbit hole and dig through that array
data.forEach((res, index) => {
data[index] = walkRecursive(data[index], fields, callback, index)
})
} else if (data && typeof data === 'object') {
// lets get some keys and stir them
Object.keys(data).forEach(k => {
data[k] = walkRecursive(data[k], fields, callback, k)
})
}
return data
}
export default walkRecursive

View File

@ -0,0 +1,18 @@
import createServer from './server'
import ActivityPub from './activitypub/ActivityPub'
import CONFIG from './config'
const serverConfig = {
port: CONFIG.GRAPHQL_PORT,
// cors: {
// credentials: true,
// origin: [CONFIG.CLIENT_URI] // your frontend url.
// }
}
const server = createServer()
server.start(serverConfig, options => {
/* eslint-disable-next-line no-console */
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
ActivityPub.init(server)
})

View File

@ -0,0 +1,16 @@
import { request } from 'graphql-request'
// this is the to-be-tested server host
// not to be confused with the seeder host
export const host = 'http://127.0.0.1:4123'
export async function login({ email, password }) {
const mutation = `
mutation {
login(email:"${email}", password:"${password}")
}`
const response = await request(host, mutation)
return {
authorization: `Bearer ${response.login}`,
}
}

View File

@ -0,0 +1,31 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null
const token = authorizationHeader.replace('Bearer ', '')
let id = null
try {
const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
id = decoded.sub
} catch (err) {
return null
}
const session = driver.session()
const query = `
MATCH (user:User {id: {id} })
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
LIMIT 1
`
const result = await session.run(query, { id })
session.close()
const [currentUser] = await result.records.map(record => {
return record.get('user')
})
if (!currentUser) return null
if (currentUser.disabled) return null
return {
token,
...currentUser,
}
}

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