kopia lustrzana https://github.com/fediversespace/fediverse.space
Porównaj commity
48 Commity
Autor | SHA1 | Data |
---|---|---|
Tao Bojlén | 6efce18c3b | |
Tao Bojlén | ed0c28f24e | |
Tao Bojlén | de97f6d843 | |
Tao Bojlén | 9c0bf93420 | |
Tao Bojlén | a4eaf75c70 | |
Tao Bojlén | 8f4193e43f | |
Tao Bojlén | 12b035780e | |
Tao Bojlén | 0970e39dea | |
Tao Bojlén | b054f78197 | |
Tao Bojlén | ffa0e50966 | |
Tao Bojlén | f7a5dbc9d5 | |
Tao Bojlén | f6c754a4ac | |
Tao Bojlén | 96a35c5d9a | |
Tao Bojlén | ca831b3831 | |
Tao Bojlén | 01ff551516 | |
Tao Bojlén | 4cdd03dd6c | |
Tao Bojlén | 1ccb2a84a1 | |
Tao Bojlén | ba024c9357 | |
Tao Bojlén | 9cae5e58b2 | |
Tao Bojlén | 4d4193ff49 | |
Tao Bojlén | b7cb7fa685 | |
Tao Bojlén | 332d12e1a4 | |
Tao Bojlén | 98b7448291 | |
Tao Bojlén | 43745881dd | |
Tao Bojlén | 0234a465f2 | |
Tao Bojlén | e7a6c0a988 | |
Tao Bojlén | 9115d29a88 | |
Inex Code | aacc8574d8 | |
Inex Code | 7bb8bd0a8a | |
Tao Bojlén | 2e87b53024 | |
Tao Bojlén | 0dd39e6984 | |
Tao Bojlén | 15ad5f1615 | |
Tao Bojlén | 153af37ccf | |
Tao Bojlén | 5f8bd7f891 | |
Tao Bojlén | e1d48e70c4 | |
Tao Bojlén | db04c3a67a | |
Tao Bojlén | 55994be8bc | |
Tao Bojlén | 4691b8dfd7 | |
Tao Bojlén | 3148578a7d | |
Tao Bojlén | 41f5461386 | |
Tao Bojlén | a276eccba4 | |
Tao Bojlén | 49b74c189b | |
Tao Bojlén | 6f75565061 | |
Tao Bojlén | c1d939e7b3 | |
Tao Bojlén | 2eb7ea98d1 | |
Tao Bojlén | f5daa648f7 | |
Tao Bojlén | 3cdc0dc49a | |
Tao Bojlén | 15e0d982e9 |
|
@ -0,0 +1,42 @@
|
|||
name: Build image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
# Permissions to use OIDC token authentication
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
# Allows pushing to the GitHub Container Registry
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: depot/setup-action@v1
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
- name: Build & push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: rktsv8c4sk
|
||||
context: backend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -0,0 +1,43 @@
|
|||
name: Elixir CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Elixir
|
||||
uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24
|
||||
with:
|
||||
elixir-version: '1.12.2' # Define the elixir version [required]
|
||||
otp-version: '24.0.4' # Define the OTP version [required]
|
||||
- name: Restore dependencies cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
working-directory: ./backend
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
- name: Install dependencies
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
mix local.hex --force
|
||||
mix local.rebar --force
|
||||
mix deps.get
|
||||
- name: Compile dependencies
|
||||
working-directory: ./backend
|
||||
run: mix deps.compile
|
||||
- name: Run Credo
|
||||
working-directory: ./backend
|
||||
run: mix credo --strict
|
||||
- name: Run sobelow
|
||||
working-directory: ./backend
|
||||
run: mix sobelow --config
|
|
@ -0,0 +1,37 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.3.0
|
||||
|
||||
- name: Setup deps
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./frontend
|
||||
run: npm run lint
|
|
@ -1,10 +1,12 @@
|
|||
*.csv
|
||||
.idea/
|
||||
*.gexf
|
||||
data/
|
||||
*.class
|
||||
|
||||
backend/.sobelow
|
||||
node_modules/
|
||||
/frontend/build/
|
||||
/frontend/dist/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
|
|
|
@ -5,19 +5,19 @@ test-frontend:
|
|||
- cd frontend
|
||||
stage: test
|
||||
script:
|
||||
- yarn install
|
||||
- yarn lint
|
||||
- npm install
|
||||
- npm run lint
|
||||
cache:
|
||||
paths:
|
||||
- frontend/node_modules/
|
||||
- frontend/.yarn
|
||||
- .npm/
|
||||
only:
|
||||
changes:
|
||||
- frontend/**/*
|
||||
|
||||
test-backend:
|
||||
stage: test
|
||||
image: elixir:1.9
|
||||
image: elixir:1.14
|
||||
variables:
|
||||
MIX_ENV: test
|
||||
only:
|
||||
|
@ -36,53 +36,3 @@ test-backend:
|
|||
paths:
|
||||
- backend/deps/
|
||||
- backend/_build/
|
||||
|
||||
deploy-backend-develop:
|
||||
stage: deploy
|
||||
environment:
|
||||
name: develop
|
||||
url: https://phoenix.api-develop.fediverse.space
|
||||
image: ilyasemenov/gitlab-ci-git-push
|
||||
only:
|
||||
- develop
|
||||
except:
|
||||
- schedules
|
||||
script:
|
||||
- git-push dokku@api-develop.fediverse.space:phoenix develop
|
||||
|
||||
deploy-gephi-develop:
|
||||
stage: deploy
|
||||
image: ilyasemenov/gitlab-ci-git-push
|
||||
environment:
|
||||
name: develop
|
||||
only:
|
||||
- develop
|
||||
except:
|
||||
- schedules
|
||||
script:
|
||||
- git-push dokku@api-develop.fediverse.space:gephi develop
|
||||
|
||||
deploy-backend-production:
|
||||
stage: deploy
|
||||
environment:
|
||||
name: production
|
||||
url: https://phoenix.api.fediverse.space
|
||||
image: ilyasemenov/gitlab-ci-git-push
|
||||
only:
|
||||
- master
|
||||
except:
|
||||
- schedules
|
||||
script:
|
||||
- git-push dokku@api.fediverse.space:phoenix master
|
||||
|
||||
deploy-gephi-production:
|
||||
stage: deploy
|
||||
image: ilyasemenov/gitlab-ci-git-push
|
||||
environment:
|
||||
name: production
|
||||
only:
|
||||
- master
|
||||
except:
|
||||
- schedules
|
||||
script:
|
||||
- git-push dokku@api.fediverse.space:gephi master
|
|
@ -5,12 +5,15 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"type": "mix_task",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/frontend/src",
|
||||
"runtimeExecutable": "/usr/bin/chromium-browser"
|
||||
}
|
||||
"name": "phx.server",
|
||||
"task": "phx.server",
|
||||
"taskArgs": [],
|
||||
"projectDir": "${workspaceRoot}/backend",
|
||||
"env": {
|
||||
"SKIP_CRAWL": "1"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
|
@ -4,28 +4,30 @@ This is an overview of the external software components (libraries, etc.) that
|
|||
are used in fediverse.space.
|
||||
|
||||
## Backend
|
||||
|
||||
### Crawler and API
|
||||
* [Elixir](https://elixir-lang.org/) (the language)
|
||||
* [Phoenix](https://phoenixframework.org/) (the web framework)
|
||||
* See [/backend/mix.env](/backend/mix.env) for a complete overview of
|
||||
|
||||
- [Elixir](https://elixir-lang.org/) (the language)
|
||||
- [Phoenix](https://phoenixframework.org/) (the web framework)
|
||||
- See [/backend/mix.env](/backend/mix.env) for a complete overview of
|
||||
dependencies
|
||||
|
||||
### Graph layout
|
||||
* Java (the language)
|
||||
* Gradle (to build)
|
||||
* [Gephi toolkit](https://gephi.org/toolkit/)
|
||||
|
||||
- Java (the language)
|
||||
- Gradle (to build)
|
||||
- [Gephi toolkit](https://gephi.org/toolkit/)
|
||||
|
||||
## Frontend
|
||||
* [React](https://reactjs.org/) (the UI framework)
|
||||
* [Blueprint](https://blueprintjs.com/) (a collection of pre-existing UI components)
|
||||
* [Cytoscape.js](http://js.cytoscape.org/) (for graph visualization)
|
||||
* See [/frontend/package.json](/frontend/package.json) for a complete overview
|
||||
|
||||
- [React](https://reactjs.org/) (the UI framework)
|
||||
- [Blueprint](https://blueprintjs.com/) (a collection of pre-existing UI components)
|
||||
- [Cytoscape.js](http://js.cytoscape.org/) (for graph visualization)
|
||||
- See [/frontend/package.json](/frontend/package.json) for a complete overview
|
||||
of dependencies
|
||||
|
||||
## Other
|
||||
* [Docker](https://www.docker.com/) and
|
||||
[docker-compose](https://docs.docker.com/compose/overview/)
|
||||
* The backend is deployed using [Dokku](http://dokku.viewdocs.io/dokku/).
|
||||
* The frontend is hosted on [Netlify](https://www.netlify.com/)
|
||||
* [GitLab](https://gitlab.com/) and GitLab CI/CD are used for project management and CI/CD.
|
||||
|
||||
- [Docker](https://www.docker.com/) and
|
||||
[docker-compose](https://docs.docker.com/compose/overview/)
|
||||
- [GitLab](https://gitlab.com/) and GitLab CI/CD are used for project management and CI/CD.
|
||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -19,6 +19,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Security
|
||||
|
||||
## [2.9.6 - 2020-10-13]
|
||||
|
||||
### Added
|
||||
|
||||
- Added link to personal website on About page.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow `data:` images in Netlify CSP.
|
||||
- Fix inability to DM login links in releases (#104).
|
||||
|
||||
## [2.9.5 - 2020-10-11]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed crawler not finding API in some cases
|
||||
|
||||
## [2.9.4 - 2020-10-09]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CSP issues for Plausible analytics
|
||||
|
||||
## [2.9.3 - 2020-10-09]
|
||||
|
||||
### Added
|
||||
|
||||
- Allow Plausible privacy-preserving analytics in CSP
|
||||
|
||||
### Changed
|
||||
|
||||
- Update dependencies
|
||||
- Update to Elixir 1.10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed CSP headers for data: images
|
||||
|
||||
## [2.9.2 - 2020-08-31]
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove staging server
|
||||
|
||||
## [2.9.1 - 2020-08-31]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added AppSignal logo to "Special thanks" section
|
||||
|
||||
## [2.9.0 - 2020-06-19]
|
||||
|
||||
### Changed
|
||||
|
|
33
README.md
33
README.md
|
@ -20,37 +20,31 @@ Read the latest updates on Mastodon: [@fediversespace](https://mastodon.social/@
|
|||
|
||||
## Requirements
|
||||
|
||||
Note: examples here use `podman`. In most cases you should be able to replace `podman` with `docker`.
|
||||
|
||||
Though containerized, backend development is easiest if you have the following installed.
|
||||
You'll need the following to work on fediverse.space:
|
||||
|
||||
- For the crawler + API:
|
||||
- Elixir
|
||||
- Postgres
|
||||
- Elasticsearch
|
||||
- For laying out the graph:
|
||||
- Java
|
||||
- For the frontend:
|
||||
- Node.js
|
||||
- Yarn
|
||||
|
||||
## Running it
|
||||
|
||||
### Backend
|
||||
|
||||
- `cp example.env .env` and modify environment variables as required
|
||||
- `podman build gephi && podman build phoenix`
|
||||
- `podman run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.8.9`
|
||||
- If you've `run` this container previously, use `podman start elasticsearch`
|
||||
- `podman run --name postgres -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 postgres:12`
|
||||
- `podman-compose -f compose.backend-services.yml -f compose.phoenix.yml`
|
||||
- `docker-compose up`
|
||||
- Create the elasticsearch index:
|
||||
- `iex -S mix app.start`
|
||||
- `Elasticsearch.Index.hot_swap(Backend.Elasticsearch.Cluster, :instances)`
|
||||
|
||||
### Frontend
|
||||
|
||||
- `cd frontend && yarn install`
|
||||
- `yarn start`
|
||||
- `cd frontend && npm install`
|
||||
- `npm start`
|
||||
|
||||
## Commands
|
||||
|
||||
|
@ -60,7 +54,7 @@ Though containerized, backend development is easiest if you have the following i
|
|||
|
||||
### Frontend
|
||||
|
||||
- `yarn build` creates an optimized build for deployment
|
||||
- `npm run build` creates an optimized build for deployment
|
||||
|
||||
## Privacy
|
||||
|
||||
|
@ -88,14 +82,19 @@ You don't have to follow these instructions, but it's one way to set up a contin
|
|||
- `dokku elasticsearch:create fediverse`
|
||||
- `dokku elasticsearch:link fediverse phoenix`
|
||||
|
||||
6. Update the backend configuration. In particular, change the `user_agent` in [config.exs](/backend/config/config.exs) to something descriptive.
|
||||
7. Push the apps, e.g. `git push dokku@<DOMAIN>:phoenix` (note that the first push cannot be from the CD pipeline).
|
||||
8. Set up SSL for the Phoenix app
|
||||
6. Set the build dirs
|
||||
|
||||
- `dokku letsencrypt phoenix`
|
||||
- `dokku builder:set phoenix build-dir backend`
|
||||
- `dokku builder:set gephi build-dir gephi`
|
||||
|
||||
7. Update the backend configuration. In particular, change the `user_agent` in [config.exs](/backend/config/config.exs) to something descriptive.
|
||||
8. Push the apps, e.g. `git push dokku@<DOMAIN>:phoenix` (note that the first push cannot be from the CD pipeline).
|
||||
9. Set up SSL for the Phoenix app
|
||||
|
||||
- `dokku letsencrypt:enable phoenix`
|
||||
- `dokku letsencrypt:cron-job --add`
|
||||
|
||||
9. Set up a cron job for the graph layout (use the `dokku` user). E.g.
|
||||
10. Set up a cron job for the graph layout (use the `dokku` user). E.g.
|
||||
|
||||
```
|
||||
SHELL=/bin/bash
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
# This file contains the configuration for Credo and you are probably reading
|
||||
# this after creating it with `mix credo.gen.config`.
|
||||
#
|
||||
# If you find anything wrong or unclear in this file, please report an
|
||||
# issue on GitHub: https://github.com/rrrene/credo/issues
|
||||
#
|
||||
%{
|
||||
#
|
||||
# You can have as many configs as you like in the `configs:` field.
|
||||
configs: [
|
||||
%{
|
||||
#
|
||||
# Run any config using `mix credo -C <name>`. If no config name is given
|
||||
# "default" is used.
|
||||
#
|
||||
name: "default",
|
||||
#
|
||||
# These are the files included in the analysis:
|
||||
files: %{
|
||||
#
|
||||
# You can give explicit globs or simply directories.
|
||||
# In the latter case `**/*.{ex,exs}` will be used.
|
||||
#
|
||||
included: [
|
||||
"**/*.{ex,exs}"
|
||||
],
|
||||
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||
},
|
||||
#
|
||||
# Load and configure plugins here:
|
||||
#
|
||||
plugins: [],
|
||||
#
|
||||
# If you create your own checks, you must specify the source files for
|
||||
# them here, so they can be loaded by Credo before running the analysis.
|
||||
#
|
||||
requires: [],
|
||||
#
|
||||
# If you want to enforce a style guide and need a more traditional linting
|
||||
# experience, you can change `strict` to `true` below:
|
||||
#
|
||||
strict: true,
|
||||
#
|
||||
# To modify the timeout for parsing files, change this value:
|
||||
#
|
||||
parse_timeout: 5000,
|
||||
#
|
||||
# If you want to use uncolored output by default, you can change `color`
|
||||
# to `false` below:
|
||||
#
|
||||
color: true,
|
||||
#
|
||||
# You can customize the parameters of any check by adding a second element
|
||||
# to the tuple.
|
||||
#
|
||||
# To disable a check put `false` as second element:
|
||||
#
|
||||
# {Credo.Check.Design.DuplicatedCode, false}
|
||||
#
|
||||
checks: %{
|
||||
enabled: [
|
||||
#
|
||||
## Consistency Checks
|
||||
#
|
||||
{Credo.Check.Consistency.ExceptionNames, []},
|
||||
{Credo.Check.Consistency.LineEndings, []},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||
|
||||
#
|
||||
## Design Checks
|
||||
#
|
||||
# You can customize the priority of any check
|
||||
# Priority values are: `low, normal, high, higher`
|
||||
#
|
||||
{Credo.Check.Design.AliasUsage,
|
||||
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||
# You can also customize the exit_status of each check.
|
||||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
#
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||
{Credo.Check.Design.TagFIXME, []},
|
||||
|
||||
#
|
||||
## Readability Checks
|
||||
#
|
||||
{Credo.Check.Readability.AliasOrder, []},
|
||||
{Credo.Check.Readability.FunctionNames, []},
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||
{Credo.Check.Readability.Semicolons, []},
|
||||
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||
{Credo.Check.Readability.StringSigils, []},
|
||||
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||
{Credo.Check.Readability.VariableNames, []},
|
||||
{Credo.Check.Readability.WithSingleClause, []},
|
||||
|
||||
#
|
||||
## Refactoring Opportunities
|
||||
#
|
||||
{Credo.Check.Refactor.Apply, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, []},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.MapJoin, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterCount, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
{Credo.Check.Refactor.RejectReject, []},
|
||||
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||
|
||||
#
|
||||
## Warnings
|
||||
#
|
||||
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||
{Credo.Check.Warning.Dbg, []},
|
||||
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||
{Credo.Check.Warning.IExPry, []},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||
{Credo.Check.Warning.SpecWithStruct, []},
|
||||
{Credo.Check.Warning.WrongTestFileExtension, []},
|
||||
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||
{Credo.Check.Warning.UnusedListOperation, []},
|
||||
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||
{Credo.Check.Warning.UnsafeExec, []}
|
||||
],
|
||||
disabled: [
|
||||
#
|
||||
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
|
||||
|
||||
#
|
||||
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
|
||||
# and be sure to use `mix credo --strict` to see low priority checks)
|
||||
#
|
||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||
{Credo.Check.Consistency.UnusedVariableNames, []},
|
||||
{Credo.Check.Design.DuplicatedCode, []},
|
||||
{Credo.Check.Design.SkipTestWithoutComment, []},
|
||||
{Credo.Check.Readability.AliasAs, []},
|
||||
{Credo.Check.Readability.BlockPipe, []},
|
||||
{Credo.Check.Readability.ImplTrue, []},
|
||||
{Credo.Check.Readability.MultiAlias, []},
|
||||
{Credo.Check.Readability.NestedFunctionCalls, []},
|
||||
{Credo.Check.Readability.OneArityFunctionInPipe, []},
|
||||
{Credo.Check.Readability.SeparateAliasRequire, []},
|
||||
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
{Credo.Check.Readability.Specs, []},
|
||||
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||
{Credo.Check.Readability.WithCustomTaggedTuple, []},
|
||||
{Credo.Check.Readability.OnePipePerLine, []},
|
||||
{Credo.Check.Refactor.ABCSize, []},
|
||||
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||
{Credo.Check.Refactor.DoubleBooleanNegation, []},
|
||||
{Credo.Check.Refactor.FilterReject, []},
|
||||
{Credo.Check.Refactor.IoPuts, []},
|
||||
{Credo.Check.Refactor.MapMap, []},
|
||||
{Credo.Check.Refactor.ModuleDependencies, []},
|
||||
{Credo.Check.Refactor.NegatedIsNil, []},
|
||||
{Credo.Check.Refactor.PassAsyncInTestCases, []},
|
||||
{Credo.Check.Refactor.PipeChainStart, []},
|
||||
{Credo.Check.Refactor.RejectFilter, []},
|
||||
{Credo.Check.Refactor.VariableRebinding, []},
|
||||
{Credo.Check.Warning.LazyLogging, []},
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||
{Credo.Check.Warning.MixEnv, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||
|
||||
# {Credo.Check.Refactor.MapInto, []},
|
||||
|
||||
#
|
||||
# Custom checks can be created using `mix credo.gen.check`.
|
||||
#
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
[
|
||||
import_deps: [:ecto, :phoenix],
|
||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/health OK
|
|
@ -1,7 +1,7 @@
|
|||
FROM elixir:1.9.0-alpine as build
|
||||
FROM elixir:1.14-alpine as build
|
||||
|
||||
# install build dependencies
|
||||
RUN apk add --update git build-base
|
||||
RUN apk add --update git build-base
|
||||
|
||||
# prepare build dir
|
||||
RUN mkdir /app
|
||||
|
@ -36,8 +36,8 @@ COPY rel rel
|
|||
RUN mix release
|
||||
|
||||
# prepare release image
|
||||
FROM alpine:3.9 AS app
|
||||
RUN apk add --update bash openssl
|
||||
FROM alpine:3.17 AS app
|
||||
RUN apk add --update bash openssl libstdc++ build-base
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
@ -46,6 +46,7 @@ ENV APP_NAME=backend
|
|||
|
||||
COPY --from=build /app/_build/prod/rel/${APP_NAME} ./
|
||||
COPY Procfile ./
|
||||
COPY CHECKS ./
|
||||
RUN chown -R nobody: /app
|
||||
USER nobody
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Notes
|
||||
|
||||
- This project requires Elixir >= 1.9.
|
||||
- This project requires Elixir >= 1.14.
|
||||
- Run with `SKIP_CRAWL=true` to just run the server (useful for working on the API without also crawling)
|
||||
- This project is automatically scanned for potential vulnerabilities with [Sobelow](https://sobelow.io/).
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@ config :backend,
|
|||
# Configures the endpoint
|
||||
config :backend, BackendWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "XL4NKGBN9lZMrQbMEI1KJOlwAt8S7younVJl90TdAgzmwyapr3g7BRYSNYvX0sZ9",
|
||||
render_errors: [view: BackendWeb.ErrorView, accepts: ~w(json)],
|
||||
pubsub: [name: Backend.PubSub, adapter: Phoenix.PubSub.PG2],
|
||||
instrumenters: [Appsignal.Phoenix.Instrumenter]
|
||||
secret_key_base: System.get_env("SECRET_KEY_BASE"),
|
||||
render_errors: [view: BackendWeb.ErrorView, accepts: ~w(json)]
|
||||
|
||||
config :backend, :http, Backend.Http
|
||||
|
||||
config :backend, Backend.Repo, queue_target: 5000
|
||||
|
||||
|
@ -43,13 +43,15 @@ config :backend, Graph.Cache,
|
|||
# 1 hour
|
||||
gc_interval: 3600
|
||||
|
||||
config :ex_twilio,
|
||||
account_sid: System.get_env("TWILIO_ACCOUNT_SID"),
|
||||
auth_token: System.get_env("TWILIO_AUTH_TOKEN")
|
||||
|
||||
config :backend, Backend.Mailer,
|
||||
adapter: Swoosh.Adapters.Sendgrid,
|
||||
api_key: System.get_env("SENDGRID_API_KEY")
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: System.get_env("MAILER_RELAY"),
|
||||
username: System.get_env("MAILER_USERNAME"),
|
||||
password: System.get_env("MAILER_PASSWORD"),
|
||||
ssl: true,
|
||||
tls: :always,
|
||||
auth: :always,
|
||||
port: 465
|
||||
|
||||
config :backend, Mastodon.Messenger,
|
||||
domain: System.get_env("MASTODON_DOMAIN"),
|
||||
|
@ -57,15 +59,23 @@ config :backend, Mastodon.Messenger,
|
|||
|
||||
config :backend, :crawler,
|
||||
status_age_limit_days: 28,
|
||||
status_count_limit: 5000,
|
||||
status_count_limit: 1000,
|
||||
personal_instance_threshold: 10,
|
||||
crawl_interval_mins: 30,
|
||||
crawl_interval_mins: 60,
|
||||
crawl_workers: 100,
|
||||
blacklist: [
|
||||
# spam
|
||||
"gab.best",
|
||||
# spam
|
||||
"4chan.icu",
|
||||
# spam
|
||||
"activitypub-troll.cf",
|
||||
# spam
|
||||
"misskey-forkbomb.cf",
|
||||
# spam
|
||||
"repl.co",
|
||||
# malicious?
|
||||
"ignorelist.com",
|
||||
# *really* doesn't want to be listed on fediverse.space
|
||||
"pleroma.site",
|
||||
# dummy instances used for pleroma CI
|
||||
|
@ -74,7 +84,6 @@ config :backend, :crawler,
|
|||
user_agent: "fediverse.space crawler",
|
||||
require_bidirectional_mentions: false,
|
||||
admin_phone: System.get_env("ADMIN_PHONE"),
|
||||
twilio_phone: System.get_env("TWILIO_PHONE"),
|
||||
admin_email: System.get_env("ADMIN_EMAIL")
|
||||
|
||||
config :backend, Backend.Scheduler,
|
||||
|
@ -91,6 +100,8 @@ config :backend, Backend.Scheduler,
|
|||
{"0 */3 * * *", {Backend.Scheduler, :check_for_spam_instances, []}}
|
||||
]
|
||||
|
||||
config :backend, :environment, Mix.env()
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
|
@ -17,7 +17,11 @@ config :backend, Backend.Repo,
|
|||
config :backend, Backend.Elasticsearch.Cluster,
|
||||
url: System.get_env("ELASTICSEARCH_URL") || "http://localhost:9200"
|
||||
|
||||
config :appsignal, :config, revision: System.get_env("GIT_REV")
|
||||
config :appsignal, :config,
|
||||
otp_app: :backend,
|
||||
name: "fediverse.space",
|
||||
active: true,
|
||||
revision: System.get_env("GIT_REV")
|
||||
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
|
||||
|
@ -28,16 +32,21 @@ config :backend, BackendWeb.Endpoint,
|
|||
secret_key_base: System.get_env("SECRET_KEY_BASE"),
|
||||
server: true
|
||||
|
||||
config :ex_twilio,
|
||||
account_sid: System.get_env("TWILIO_ACCOUNT_SID"),
|
||||
auth_token: System.get_env("TWILIO_AUTH_TOKEN")
|
||||
|
||||
config :backend, :crawler,
|
||||
admin_phone: System.get_env("ADMIN_PHONE"),
|
||||
twilio_phone: System.get_env("TWILIO_PHONE"),
|
||||
admin_email: System.get_env("ADMIN_EMAIL"),
|
||||
frontend_domain: System.get_env("FRONTEND_DOMAIN")
|
||||
frontend_domain: System.get_env("FRONTEND_DOMAIN"),
|
||||
crawl_workers: String.to_integer(System.get_env("CRAWL_WORKERS") || "100")
|
||||
|
||||
config :backend, Backend.Mailer,
|
||||
adapter: Swoosh.Adapters.Sendgrid,
|
||||
api_key: System.get_env("SENDGRID_API_KEY")
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: System.get_env("MAILER_RELAY"),
|
||||
username: System.get_env("MAILER_USERNAME"),
|
||||
password: System.get_env("MAILER_PASSWORD"),
|
||||
ssl: true,
|
||||
auth: :always,
|
||||
port: 465
|
||||
|
||||
config :backend, Mastodon.Messenger,
|
||||
domain: System.get_env("MASTODON_DOMAIN"),
|
||||
token: System.get_env("MASTODON_TOKEN")
|
||||
|
|
|
@ -16,3 +16,7 @@ config :backend, Backend.Repo,
|
|||
database: "backend_test",
|
||||
hostname: "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
config :appsignal, :config, active: false
|
||||
|
||||
config :backend, :crawler, status_count_limit: 5
|
||||
|
|
|
@ -7,13 +7,6 @@ defmodule Backend.Application do
|
|||
import Backend.Util
|
||||
|
||||
def start(_type, _args) do
|
||||
:telemetry.attach(
|
||||
"appsignal-ecto",
|
||||
[:backend, :repo, :query],
|
||||
&Appsignal.Ecto.handle_event/4,
|
||||
nil
|
||||
)
|
||||
|
||||
crawl_worker_count = get_config(:crawl_workers)
|
||||
|
||||
children = [
|
||||
|
@ -38,9 +31,11 @@ defmodule Backend.Application do
|
|||
]
|
||||
|
||||
children =
|
||||
case Enum.member?(["true", 1, "1"], System.get_env("SKIP_CRAWL")) do
|
||||
true -> children
|
||||
false -> children ++ [Backend.Crawler.StaleInstanceManager]
|
||||
if Enum.member?(["true", 1, "1"], System.get_env("SKIP_CRAWL")) or
|
||||
Application.get_env(:backend, :environment) == :test do
|
||||
children
|
||||
else
|
||||
children ++ [Backend.Crawler.StaleInstanceManager]
|
||||
end
|
||||
|
||||
add_appsignal_probes()
|
||||
|
|
|
@ -18,7 +18,8 @@ defmodule Backend.Crawler.ApiCrawler do
|
|||
# {domain, type} e.g. {"gab.com", "reject"}
|
||||
@type federation_restriction :: {String.t(), String.t()}
|
||||
|
||||
@type instance_type :: :mastodon | :pleroma | :gab | :misskey | :gnusocial
|
||||
@type instance_type ::
|
||||
:mastodon | :pleroma | :gab | :misskey | :gnusocial | :smithereen | :friendica
|
||||
|
||||
defstruct [
|
||||
:version,
|
||||
|
|
|
@ -117,8 +117,8 @@ defmodule Backend.Crawler do
|
|||
try do
|
||||
%Crawler{state | result: curr.crawl(domain, result), found_api?: true}
|
||||
rescue
|
||||
e in HTTPoison.Error ->
|
||||
Map.put(state, :error, "HTTPoison error: " <> HTTPoison.Error.message(e))
|
||||
e in Backend.HttpBehaviour.Error ->
|
||||
Map.put(state, :error, "HTTP error: " <> e.message)
|
||||
|
||||
e in Jason.DecodeError ->
|
||||
Map.put(state, :error, "Jason DecodeError: " <> Jason.DecodeError.message(e))
|
||||
|
@ -237,9 +237,12 @@ defmodule Backend.Crawler do
|
|||
Enum.map(result.federation_restrictions, fn {domain, _restriction_type} -> domain end)
|
||||
)
|
||||
|> Enum.map(&%{domain: &1, inserted_at: now, updated_at: now, next_crawl: now})
|
||||
|> Enum.chunk_every(5000)
|
||||
|
||||
Instance
|
||||
|> Repo.insert_all(new_instances, on_conflict: :nothing, conflict_target: :domain)
|
||||
new_instances
|
||||
|> Enum.each(fn chunk ->
|
||||
Repo.insert_all(Instance, chunk, on_conflict: :nothing, conflict_target: :domain)
|
||||
end)
|
||||
|
||||
Repo.transaction(fn ->
|
||||
## Save peer relationships ##
|
||||
|
@ -276,9 +279,10 @@ defmodule Backend.Crawler do
|
|||
updated_at: now
|
||||
}
|
||||
)
|
||||
|> Enum.chunk_every(5000)
|
||||
|
||||
InstancePeer
|
||||
|> Repo.insert_all(new_instance_peers)
|
||||
new_instance_peers
|
||||
|> Enum.each(fn chunk -> Repo.insert_all(InstancePeer, chunk) end)
|
||||
end)
|
||||
|
||||
## Save federation restrictions ##
|
||||
|
|
|
@ -51,7 +51,7 @@ defmodule Backend.Crawler.Crawlers.Friendica do
|
|||
|> Map.merge(nodeinfo_result)
|
||||
|
||||
peers =
|
||||
case get_and_decode("https://#{domain}/poco/@server") do
|
||||
case http_client().get_and_decode("https://#{domain}/poco/@server") do
|
||||
{:ok, p} -> p
|
||||
{:error, _err} -> []
|
||||
end
|
||||
|
@ -71,7 +71,7 @@ defmodule Backend.Crawler.Crawlers.Friendica do
|
|||
end
|
||||
|
||||
defp get_statistics(domain) do
|
||||
get_and_decode("https://#{domain}/statistics.json")
|
||||
http_client().get_and_decode("https://#{domain}/statistics.json")
|
||||
end
|
||||
|
||||
defp to_domain(url) do
|
||||
|
|
|
@ -14,7 +14,7 @@ defmodule Backend.Crawler.Crawlers.GnuSocial do
|
|||
if nodeinfo_result != nil do
|
||||
Map.get(nodeinfo_result, :instance_type) == :gnusocial
|
||||
else
|
||||
case get_and_decode("https://#{domain}/api/statuses/public_timeline.json") do
|
||||
case http_client().get_and_decode("https://#{domain}/api/statuses/public_timeline.json") do
|
||||
{:ok, statuses} -> is_list(statuses)
|
||||
{:error, _other} -> false
|
||||
end
|
||||
|
@ -86,7 +86,7 @@ defmodule Backend.Crawler.Crawlers.GnuSocial do
|
|||
|
||||
Logger.debug("Crawling #{endpoint}")
|
||||
|
||||
statuses = get_and_decode!(endpoint)
|
||||
statuses = http_client().get_and_decode!(endpoint)
|
||||
|
||||
# Filter to statuses that are in the correct timeframe
|
||||
filtered_statuses =
|
||||
|
|
|
@ -12,10 +12,19 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
@impl ApiCrawler
|
||||
def is_instance_type?(domain, result) do
|
||||
# We might already know that this is a Pleroma instance from nodeinfo
|
||||
if result != nil and Map.get(result, :instance_type) == :pleroma do
|
||||
true
|
||||
if result != nil do
|
||||
cond do
|
||||
# for pleroma and smithereen, the instance_type will get overwritten
|
||||
# with the correct value -- but we still want to return true here
|
||||
# since they are compatible with the mastodon API
|
||||
Map.get(result, :instance_type) == :pleroma -> true
|
||||
Map.get(result, :instance_type) == :smithereen -> true
|
||||
Map.get(result, :instance_type) == :mastodon -> true
|
||||
Map.get(result, :instance_type) == :friendica -> false
|
||||
true -> false
|
||||
end
|
||||
else
|
||||
case get_and_decode("https://#{domain}/api/v1/instance") do
|
||||
case http_client().get_and_decode("https://#{domain}/api/v1/instance") do
|
||||
{:ok, %{"title" => _title}} -> true
|
||||
_other -> false
|
||||
end
|
||||
|
@ -35,7 +44,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
|
||||
@impl ApiCrawler
|
||||
def crawl(domain, nodeinfo) do
|
||||
instance = get_and_decode!("https://#{domain}/api/v1/instance")
|
||||
instance = http_client().get_and_decode!("https://#{domain}/api/v1/instance")
|
||||
user_count = get_in(instance, ["stats", "user_count"])
|
||||
|
||||
if is_above_user_threshold?(user_count) or has_opted_in?(domain) do
|
||||
|
@ -59,9 +68,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
{interactions, statuses_seen} = get_interactions(domain)
|
||||
|
||||
Logger.debug(
|
||||
"#{domain}: found #{
|
||||
interactions |> Map.values() |> Enum.reduce(0, fn count, acc -> count + acc end)
|
||||
} mentions in #{statuses_seen} statuses."
|
||||
"#{domain}: found #{interactions |> Map.values() |> Enum.reduce(0, fn count, acc -> count + acc end)} mentions in #{statuses_seen} statuses."
|
||||
)
|
||||
|
||||
Map.merge(
|
||||
|
@ -93,16 +100,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
interactions \\ %{},
|
||||
statuses_seen \\ 0
|
||||
) do
|
||||
# If `statuses_seen == 0`, it's the first call of this function, which means we want to query the database for the
|
||||
# most recent status we have.
|
||||
min_timestamp =
|
||||
if statuses_seen == 0 do
|
||||
get_last_crawl_timestamp(domain)
|
||||
else
|
||||
min_timestamp
|
||||
end
|
||||
|
||||
endpoint = "https://#{domain}/api/v1/timelines/public?local=true"
|
||||
endpoint = "https://#{domain}/api/v1/timelines/public?local=true&limit=40"
|
||||
|
||||
endpoint =
|
||||
if max_id do
|
||||
|
@ -113,7 +111,26 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
|
||||
Logger.debug("Crawling #{endpoint}")
|
||||
|
||||
statuses = get_and_decode!(endpoint)
|
||||
case http_client().get_and_decode(endpoint) do
|
||||
{:ok, statuses} ->
|
||||
handle_statuses(statuses, domain, min_timestamp, interactions, statuses_seen)
|
||||
|
||||
# if there's an error (e.g. because the timeline prevents unauthenticated access)
|
||||
# then stop here
|
||||
{:error, _} ->
|
||||
{interactions, statuses_seen}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_statuses(statuses, domain, min_timestamp, interactions, statuses_seen) do
|
||||
# If `statuses_seen == 0`, it's the first call of this function, which means we want to query the database for the
|
||||
# most recent status we have.
|
||||
min_timestamp =
|
||||
if statuses_seen == 0 do
|
||||
get_last_crawl_timestamp(domain)
|
||||
else
|
||||
min_timestamp
|
||||
end
|
||||
|
||||
filtered_statuses =
|
||||
statuses
|
||||
|
@ -157,7 +174,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
|
||||
defp get_peers(domain) do
|
||||
# servers may not publish peers
|
||||
case get_and_decode("https://#{domain}/api/v1/instance/peers") do
|
||||
case http_client().get_and_decode("https://#{domain}/api/v1/instance/peers") do
|
||||
{:ok, peers} -> peers
|
||||
{:error, _err} -> []
|
||||
end
|
||||
|
@ -178,8 +195,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
|
||||
fields =
|
||||
account["fields"]
|
||||
|> Enum.map(fn %{"name" => name, "value" => value} -> name <> value end)
|
||||
|> Enum.join("")
|
||||
|> Enum.map_join("", fn %{"name" => name, "value" => value} -> name <> value end)
|
||||
|
||||
# this also means that any users who mentioned ethnobotany in their profiles will be excluded lol ¯\_(ツ)_/¯
|
||||
(account["note"] <> fields)
|
||||
|
@ -230,6 +246,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do
|
|||
defp get_instance_type(instance_stats) do
|
||||
cond do
|
||||
Map.get(instance_stats, "version") |> String.downcase() =~ "pleroma" -> :pleroma
|
||||
Map.get(instance_stats, "version") |> String.downcase() =~ "smithereen" -> :smithereen
|
||||
is_gab?(instance_stats) -> :gab
|
||||
true -> :mastodon
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
Crawler for Misskey servers.
|
||||
"""
|
||||
alias Backend.Crawler.ApiCrawler
|
||||
alias Backend.Http
|
||||
|
||||
@behaviour ApiCrawler
|
||||
import Backend.Crawler.Util
|
||||
|
@ -37,7 +38,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
@impl ApiCrawler
|
||||
def crawl(domain, nodeinfo) do
|
||||
with {:ok, %{"originalUsersCount" => user_count, "originalNotesCount" => status_count}} <-
|
||||
post_and_decode("https://#{domain}/api/stats") do
|
||||
http_client().post_and_decode("https://#{domain}/api/stats") do
|
||||
if is_above_user_threshold?(user_count) or has_opted_in?(domain) do
|
||||
Map.merge(nodeinfo, crawl_large_instance(domain, user_count, status_count))
|
||||
else
|
||||
|
@ -97,7 +98,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
endpoint = "https://#{domain}/api/notes/local-timeline"
|
||||
|
||||
params = %{
|
||||
limit: 20
|
||||
limit: 100
|
||||
}
|
||||
|
||||
params =
|
||||
|
@ -109,7 +110,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
|
||||
Logger.debug("Crawling #{endpoint} with untilId=#{until_id}")
|
||||
|
||||
statuses = post_and_decode!(endpoint, Jason.encode!(params))
|
||||
statuses = http_client().post_and_decode!(endpoint, params)
|
||||
|
||||
filtered_statuses =
|
||||
statuses
|
||||
|
@ -153,9 +154,9 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
end
|
||||
|
||||
@spec get_version_and_description(String.t()) ::
|
||||
{:ok, {String.t(), String.t()}} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
|
||||
{:ok, {String.t(), String.t()}} | {:error, Jason.DecodeError.t() | Http.Error.t()}
|
||||
defp get_version_and_description(domain) do
|
||||
case post_and_decode("https://#{domain}/api/meta") do
|
||||
case http_client().post_and_decode("https://#{domain}/api/meta") do
|
||||
{:ok, %{"version" => version, "description" => description}} ->
|
||||
{:ok, {version, description}}
|
||||
|
||||
|
@ -166,7 +167,7 @@ defmodule Backend.Crawler.Crawlers.Misskey do
|
|||
|
||||
@spec get_peers(String.t()) :: {:ok, [String.t()]} | {:error, Jason.DecodeError.t()}
|
||||
defp get_peers(domain) do
|
||||
case get_and_decode("https://#{domain}/api/v1/instance/peers") do
|
||||
case http_client().get_and_decode("https://#{domain}/api/v1/instance/peers") do
|
||||
{:ok, peers} -> {:ok, peers}
|
||||
{:error, _} -> {:ok, []}
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
"""
|
||||
|
||||
alias Backend.Crawler.ApiCrawler
|
||||
alias Backend.Http
|
||||
require Logger
|
||||
import Backend.Util
|
||||
import Backend.Crawler.Util
|
||||
|
@ -13,7 +14,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
@impl ApiCrawler
|
||||
def allows_crawling?(domain) do
|
||||
[
|
||||
".well-known/nodeinfo"
|
||||
"/.well-known/nodeinfo"
|
||||
]
|
||||
|> Enum.map(fn endpoint -> "https://#{domain}#{endpoint}" end)
|
||||
|> urls_are_crawlable?()
|
||||
|
@ -36,26 +37,40 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
end
|
||||
|
||||
@spec get_nodeinfo_url(String.t()) ::
|
||||
{:ok, String.t()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
|
||||
{:ok, String.t()} | {:error, Jason.DecodeError.t() | Http.Error.t() | :invalid_body}
|
||||
defp get_nodeinfo_url(domain) do
|
||||
case get_and_decode("https://#{domain}/.well-known/nodeinfo") do
|
||||
{:ok, response} -> {:ok, process_nodeinfo_url(response)}
|
||||
{:error, err} -> {:error, err}
|
||||
with {:ok, response} <-
|
||||
http_client().get_and_decode("https://#{domain}/.well-known/nodeinfo"),
|
||||
{:ok, nodeinfo_url} <- process_nodeinfo_url(response) do
|
||||
{:ok, nodeinfo_url}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
:error -> {:error, :invalid_body}
|
||||
end
|
||||
end
|
||||
|
||||
@spec process_nodeinfo_url(any()) :: String.t()
|
||||
@spec process_nodeinfo_url(any()) :: {:ok, String.t()} | :error
|
||||
defp process_nodeinfo_url(response) do
|
||||
response
|
||||
|> Map.get("links")
|
||||
|> Enum.filter(fn %{"rel" => rel} -> is_compatible_nodeinfo_version?(rel) end)
|
||||
|> Kernel.hd()
|
||||
|> Map.get("href")
|
||||
links =
|
||||
response
|
||||
|> Map.get("links", [])
|
||||
|> Enum.filter(fn %{"rel" => rel} -> is_compatible_nodeinfo_version?(rel) end)
|
||||
|
||||
if Enum.empty?(links) do
|
||||
:error
|
||||
else
|
||||
href =
|
||||
links
|
||||
|> Kernel.hd()
|
||||
|> Map.get("href")
|
||||
|
||||
{:ok, href}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_nodeinfo(String.t()) :: ApiCrawler.t()
|
||||
defp get_nodeinfo(nodeinfo_url) do
|
||||
case get_and_decode(nodeinfo_url) do
|
||||
case http_client().get_and_decode(nodeinfo_url) do
|
||||
{:ok, nodeinfo} -> {:ok, process_nodeinfo(nodeinfo)}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
|
@ -70,7 +85,9 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
description =
|
||||
[
|
||||
get_in(nodeinfo, ["metadata", "description"]),
|
||||
get_in(nodeinfo, ["metadata", "nodeDescription"])
|
||||
get_in(nodeinfo, ["metadata", "nodeDescription"]),
|
||||
# pixelfed
|
||||
get_in(nodeinfo, ["metadata", "config", "site", "description"])
|
||||
]
|
||||
|> Enum.filter(fn d -> d != nil end)
|
||||
|> Enum.at(0)
|
||||
|
@ -81,8 +98,8 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
ApiCrawler.get_default(),
|
||||
%{
|
||||
description: description,
|
||||
user_count: user_count,
|
||||
status_count: get_in(nodeinfo, ["usage", "localPosts"]),
|
||||
user_count: handle_count(user_count),
|
||||
status_count: nodeinfo |> get_in(["usage", "localPosts"]) |> handle_count(),
|
||||
instance_type: type,
|
||||
version: get_in(nodeinfo, ["software", "version"]),
|
||||
federation_restrictions: get_federation_restrictions(nodeinfo)
|
||||
|
@ -129,6 +146,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
"accept"
|
||||
])
|
||||
|> Enum.flat_map(fn {type, domains} ->
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|
||||
Enum.map(domains, fn domain -> {domain, type} end)
|
||||
end)
|
||||
|> Enum.concat(quarantined_domains)
|
||||
|
@ -136,4 +154,14 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do
|
|||
quarantined_domains
|
||||
end
|
||||
end
|
||||
|
||||
# handle a count that may be formatted as a string or an integer
|
||||
defp handle_count(count) do
|
||||
if is_integer(count) do
|
||||
count
|
||||
else
|
||||
{count, _rem} = Integer.parse(count)
|
||||
count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Backend.Crawler.StaleInstanceManager do
|
|||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("Starting crawler manager...")
|
||||
Backend.Repo.start_link()
|
||||
|
||||
instance_count =
|
||||
Instance
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
defmodule Backend.Http do
|
||||
@moduledoc """
|
||||
A wrapper around HTTPoison. Using this wrapper makes it easy for us
|
||||
to mock web responses in tests, and we can easily switch out HTTPoison for
|
||||
another library if we want to.
|
||||
"""
|
||||
@behaviour Backend.HttpBehaviour
|
||||
alias Backend.HttpBehaviour.Error
|
||||
|
||||
import Backend.Util
|
||||
|
||||
@doc """
|
||||
GETs from the given URL and returns the JSON-decoded response.
|
||||
If the response is unsuccessful and a default value is given, this returns the default value.
|
||||
Otherwise, unsuccessful responses return an error.
|
||||
"""
|
||||
@impl true
|
||||
def get_and_decode(url, pool \\ :default, timeout \\ 15_000, default \\ nil) do
|
||||
case HTTPoison.get(url, [{"User-Agent", get_config(:user_agent)}],
|
||||
hackney: [pool: pool],
|
||||
recv_timeout: timeout,
|
||||
timeout: timeout
|
||||
) do
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: status_code}}
|
||||
when status_code >= 200 and status_code <= 299 ->
|
||||
decode_body(body)
|
||||
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: status_code}} ->
|
||||
if not is_nil(default) do
|
||||
{:ok, default}
|
||||
else
|
||||
{:error,
|
||||
%Error{
|
||||
message: "HTTP request failed with status code #{status_code}",
|
||||
status_code: status_code,
|
||||
body: body
|
||||
}}
|
||||
end
|
||||
|
||||
{:error, %HTTPoison.Error{} = error} ->
|
||||
{:error, %Error{message: HTTPoison.Error.message(error)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def get_and_decode!(url, pool \\ :default, timeout \\ 15_000, default \\ nil) do
|
||||
case get_and_decode(url, pool, timeout, default) do
|
||||
{:ok, decoded} -> decoded
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
POSTs to the given URL with the given body and returns the JSON-decoded response.
|
||||
The given body is JSON-encoded before sending.
|
||||
"""
|
||||
@impl true
|
||||
def post_and_decode(url, body \\ %{}) do
|
||||
case HTTPoison.post(url, Jason.encode!(body), [
|
||||
{"User-Agent", get_config(:user_agent)},
|
||||
{"Content-Type", "application/json"}
|
||||
]) do
|
||||
{:ok, %HTTPoison.Response{body: body}} ->
|
||||
decode_body(body)
|
||||
|
||||
{:error, %HTTPoison.Error{} = error} ->
|
||||
{:error, %Error{message: HTTPoison.Error.message(error)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def post_and_decode!(url, body \\ %{}) do
|
||||
case post_and_decode(url, body) do
|
||||
{:ok, decoded} ->
|
||||
decoded
|
||||
|
||||
{:error, error} ->
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_body(body) do
|
||||
with {:ok, decoded} <- Jason.decode(body) do
|
||||
if is_map(decoded) and (Map.has_key?(decoded, "errors") or Map.has_key?(decoded, "error")) do
|
||||
{:error, %Error{message: "API error: " <> body}}
|
||||
else
|
||||
{:ok, decoded}
|
||||
end
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Backend.HttpBehaviour do
|
||||
@moduledoc """
|
||||
This module defines the behavior for HTTP requests.
|
||||
"""
|
||||
|
||||
defmodule Error do
|
||||
defstruct message: nil, status_code: nil, body: nil
|
||||
@type t :: %__MODULE__{message: String.t(), status_code: integer | nil, body: term | nil}
|
||||
end
|
||||
|
||||
@type response :: {:ok, Response.t()} | {:error, __MODULE__.Error.t() | Jason.DecodeError.t()}
|
||||
|
||||
@callback get_and_decode(String.t()) :: response
|
||||
@callback get_and_decode(String.t(), Atom.t(), Integer.t(), any()) :: response
|
||||
|
||||
@callback get_and_decode!(String.t()) :: Response.t()
|
||||
@callback get_and_decode!(String.t(), Atom.t(), Integer.t(), any()) :: Response.t()
|
||||
|
||||
@callback post_and_decode(String.t()) :: response()
|
||||
@callback post_and_decode(String.t(), String.t()) :: response()
|
||||
@callback post_and_decode!(String.t()) :: Response.t()
|
||||
@callback post_and_decode!(String.t(), String.t()) :: Response.t()
|
||||
end
|
|
@ -14,7 +14,7 @@ defmodule Backend.Release do
|
|||
]
|
||||
|
||||
# Ecto repos to start, if any
|
||||
@repos Application.get_env(:backend, :ecto_repos, [])
|
||||
@repos Application.compile_env(:backend, :ecto_repos, [])
|
||||
# Elasticsearch clusters to start
|
||||
@clusters [Backend.Elasticsearch.Cluster]
|
||||
# Elasticsearch indexes to build
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Backend.Scheduler do
|
|||
This module runs recurring tasks.
|
||||
"""
|
||||
|
||||
use Quantum.Scheduler, otp_app: :backend
|
||||
use Quantum, otp_app: :backend
|
||||
|
||||
alias Backend.{Crawl, CrawlInteraction, Edge, FederationRestriction, Instance, Repo}
|
||||
alias Backend.Mailer.AdminEmail
|
||||
|
@ -269,18 +269,14 @@ defmodule Backend.Scheduler do
|
|||
if length(potential_spam_instances) > 0 do
|
||||
message =
|
||||
potential_spam_instances
|
||||
|> Enum.map(fn %{count: count, base_domain: base_domain} ->
|
||||
|> Enum.map_join("\n", fn %{count: count, base_domain: base_domain} ->
|
||||
"* #{count} new at #{base_domain}"
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
|> (fn lines ->
|
||||
"fediverse.space detected the following potential spam domains from the last #{
|
||||
hour_range
|
||||
} hours:\n#{lines}"
|
||||
"fediverse.space detected the following potential spam domains from the last #{hour_range} hours:\n#{lines}"
|
||||
end).()
|
||||
|
||||
Logger.info(message)
|
||||
send_admin_sms(message)
|
||||
AdminEmail.send("Potential spam", message)
|
||||
else
|
||||
Logger.debug("Did not find potential spam instances.")
|
||||
|
|
|
@ -113,21 +113,6 @@ defmodule Backend.Util do
|
|||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an SMS to the admin phone number if configured.
|
||||
"""
|
||||
def send_admin_sms(body) do
|
||||
if get_config(:admin_phone) != nil and get_config(:twilio_phone) != nil do
|
||||
ExTwilio.Message.create(
|
||||
to: get_config(:admin_phone),
|
||||
from: get_config(:twilio_phone),
|
||||
body: body
|
||||
)
|
||||
else
|
||||
Logger.info("Could not send SMS to admin; not configured.")
|
||||
end
|
||||
end
|
||||
|
||||
@spec clean_domain(String.t()) :: String.t()
|
||||
def clean_domain(domain) do
|
||||
cleaned =
|
||||
|
@ -158,58 +143,12 @@ defmodule Backend.Util do
|
|||
map |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets and decodes a HTTP response.
|
||||
"""
|
||||
@spec get_and_decode(String.t(), Atom.t(), Integer.t()) ::
|
||||
{:ok, any()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
|
||||
def get_and_decode(url, pool \\ :crawler, timeout \\ 15_000) do
|
||||
case HTTPoison.get(url, [{"User-Agent", get_config(:user_agent)}],
|
||||
hackney: [pool: pool],
|
||||
recv_timeout: timeout,
|
||||
timeout: timeout
|
||||
) do
|
||||
{:ok, %{status_code: 200, body: body}} -> Jason.decode(body)
|
||||
{:ok, _} -> {:error, %HTTPoison.Error{reason: "Non-200 response"}}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_and_decode!(String.t()) :: any()
|
||||
def get_and_decode!(url) do
|
||||
case get_and_decode(url) do
|
||||
{:ok, decoded} -> decoded
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
POSTS to a HTTP endpoint and decodes the JSON response.
|
||||
"""
|
||||
@spec post_and_decode(String.t(), String.t()) ::
|
||||
{:ok, any()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()}
|
||||
def post_and_decode(url, body \\ "") do
|
||||
case HTTPoison.post(url, body, [{"User-Agent", get_config(:user_agent)}],
|
||||
hackney: [pool: :crawler],
|
||||
recv_timeout: 15_000,
|
||||
timeout: 15_000
|
||||
) do
|
||||
{:ok, %{status_code: 200, body: response_body}} -> Jason.decode(response_body)
|
||||
{:ok, _} -> {:error, %HTTPoison.Error{reason: "Non-200 response"}}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec post_and_decode!(String.t(), String.t()) :: any()
|
||||
def post_and_decode!(url, body \\ "") do
|
||||
case post_and_decode(url, body) do
|
||||
{:ok, decoded} -> decoded
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_valid_domain?(String.t()) :: boolean
|
||||
def is_valid_domain?(domain) do
|
||||
Regex.match?(~r/^[\pL\d\.\-_]+\.[a-zA-Z]+$/, domain)
|
||||
end
|
||||
|
||||
def http_client() do
|
||||
Application.get_env(:backend, :http, Backend.Http)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,8 @@ defmodule BackendWeb do
|
|||
and import those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: BackendWeb
|
||||
|
@ -24,6 +26,8 @@ defmodule BackendWeb do
|
|||
import Plug.Conn
|
||||
import BackendWeb.Gettext
|
||||
alias BackendWeb.Router.Helpers, as: Routes
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -39,6 +43,8 @@ defmodule BackendWeb do
|
|||
import BackendWeb.ErrorHelpers
|
||||
import BackendWeb.Gettext
|
||||
alias BackendWeb.Router.Helpers, as: Routes
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -57,6 +63,15 @@ defmodule BackendWeb do
|
|||
end
|
||||
end
|
||||
|
||||
def verified_routes do
|
||||
quote do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: BackendWeb.Endpoint,
|
||||
router: BackendWeb.Router,
|
||||
statics: BackendWeb.static_paths()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate controller/view/etc.
|
||||
"""
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule BackendWeb.AdminLoginController do
|
|||
alias Backend.Mailer.UserEmail
|
||||
alias Mastodon.Messenger
|
||||
|
||||
action_fallback BackendWeb.FallbackController
|
||||
action_fallback(BackendWeb.FallbackController)
|
||||
|
||||
@doc """
|
||||
Given an instance, looks up the login types (email or admin account) and returns them. The user can then
|
||||
|
@ -24,7 +24,7 @@ defmodule BackendWeb.AdminLoginController do
|
|||
[error: "It is only possible to administer Mastodon and Pleroma instances."]
|
||||
|
||||
true ->
|
||||
case get_and_decode("https://#{cleaned_domain}/api/v1/instance") do
|
||||
case http_client().get_and_decode("https://#{cleaned_domain}/api/v1/instance") do
|
||||
{:ok, instance_data} ->
|
||||
[instance_data: instance_data, cleaned_domain: cleaned_domain]
|
||||
|
||||
|
@ -40,7 +40,7 @@ defmodule BackendWeb.AdminLoginController do
|
|||
cleaned_domain = clean_domain(domain)
|
||||
|
||||
{data_state, instance_data} =
|
||||
get_and_decode("https://#{cleaned_domain}/api/v1/instance",
|
||||
http_client().get_and_decode("https://#{cleaned_domain}/api/v1/instance",
|
||||
pool: :admin_login,
|
||||
timeout: 20_000
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule BackendWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :backend
|
||||
use Appsignal.Phoenix
|
||||
|
||||
plug BackendWeb.Healthcheck
|
||||
|
||||
socket("/socket", BackendWeb.UserSocket,
|
||||
websocket: true,
|
||||
|
@ -15,7 +16,7 @@ defmodule BackendWeb.Endpoint do
|
|||
at: "/",
|
||||
from: :backend,
|
||||
gzip: false,
|
||||
only: ~w(css fonts images js favicon.ico robots.txt)
|
||||
only: BackendWeb.static_paths()
|
||||
)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
|
@ -46,7 +47,11 @@ defmodule BackendWeb.Endpoint do
|
|||
)
|
||||
|
||||
plug(Corsica,
|
||||
origins: ["http://localhost:3000", ~r{^https://(.*\.?)fediverse\.space$}, ~r{^https://(.*\.?)fediverse-space\.netlify\.app$}],
|
||||
origins: [
|
||||
"http://localhost:3000",
|
||||
~r{^https://(.*\.?)fediverse\.space$},
|
||||
~r{^https://(.*\.?)fediverse-space\.netlify\.app$}
|
||||
],
|
||||
allow_headers: ["content-type", "token"]
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
defmodule BackendWeb.Healthcheck do
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
|
||||
conn
|
||||
|> send_resp(200, "OK")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
def call(conn, _opts), do: conn
|
||||
end
|
|
@ -11,7 +11,8 @@ defmodule BackendWeb.RateLimiter do
|
|||
|
||||
def rate_limit(conn, options \\ []) do
|
||||
case check_rate(conn, options) do
|
||||
{:ok, _count} -> conn # Do nothing, allow execution to continue
|
||||
# Do nothing, allow execution to continue
|
||||
{:ok, _count} -> conn
|
||||
{:error, _count} -> render_error(conn)
|
||||
end
|
||||
end
|
||||
|
@ -23,6 +24,7 @@ defmodule BackendWeb.RateLimiter do
|
|||
else
|
||||
Map.get(conn.params, "domain")
|
||||
end
|
||||
|
||||
options = Keyword.put(options, :bucket_name, "authorization: #{domain}")
|
||||
rate_limit(conn, options)
|
||||
end
|
||||
|
@ -40,7 +42,7 @@ defmodule BackendWeb.RateLimiter do
|
|||
# "127.0.0.1:/api/v1/authorizations"
|
||||
defp bucket_name(conn) do
|
||||
path = Enum.join(conn.path_info, "/")
|
||||
ip = conn.remote_ip |> Tuple.to_list |> Enum.join(".")
|
||||
ip = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
|
||||
"#{ip}:#{path}"
|
||||
end
|
||||
|
||||
|
@ -48,6 +50,7 @@ defmodule BackendWeb.RateLimiter do
|
|||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Rate limit exceeded."})
|
||||
|> halt # Stop execution of further plugs, return response now
|
||||
# Stop execution of further plugs, return response now
|
||||
|> halt
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,8 @@ defmodule BackendWeb.Router do
|
|||
|
||||
pipeline :api do
|
||||
plug(:accepts, ["json"])
|
||||
plug(:rate_limit, max_requests: 5, interval_seconds: 10) # requests to the same endpoint
|
||||
# requests to the same endpoint
|
||||
plug(:rate_limit, max_requests: 5, interval_seconds: 10)
|
||||
end
|
||||
|
||||
pipeline :api_admin do
|
||||
|
|
|
@ -28,7 +28,7 @@ defmodule Graph.Cache do
|
|||
nodes = Api.list_nodes(domain)
|
||||
edges = Api.list_edges(domain)
|
||||
# Cache for 10 minutes
|
||||
Cache.set(key, %{nodes: nodes, edges: edges}, ttl: 600)
|
||||
Cache.put(key, %{nodes: nodes, edges: edges}, ttl: 600)
|
||||
%{nodes: nodes, edges: edges}
|
||||
|
||||
data ->
|
||||
|
@ -48,7 +48,7 @@ defmodule Graph.Cache do
|
|||
Logger.debug("Instance cache: miss")
|
||||
instance = Api.get_instance_with_relationships(domain)
|
||||
# Cache for five minutes
|
||||
Cache.set(key, instance, ttl: 300)
|
||||
Cache.put(key, instance, ttl: 300)
|
||||
instance
|
||||
|
||||
data ->
|
||||
|
@ -82,7 +82,8 @@ defmodule Graph.Cache do
|
|||
|> Repo.one()
|
||||
|
||||
# Cache for five minutes
|
||||
Cache.set(key, crawl, ttl: 300)
|
||||
Cache.put(key, crawl, ttl: 300)
|
||||
crawl
|
||||
|
||||
data ->
|
||||
Appsignal.increment_counter("most_recent_crawl_cache.hits", 1)
|
||||
|
|
|
@ -7,7 +7,6 @@ defmodule Backend.MixProject do
|
|||
version: "2.8.2",
|
||||
elixir: "~> 1.5",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
|
@ -23,11 +22,10 @@ defmodule Backend.MixProject do
|
|||
extra_applications: [
|
||||
:logger,
|
||||
:runtime_tools,
|
||||
:mnesia,
|
||||
:gollum,
|
||||
:ex_twilio,
|
||||
:elasticsearch,
|
||||
:appsignal
|
||||
:appsignal,
|
||||
:swoosh,
|
||||
:gen_smtp
|
||||
]
|
||||
]
|
||||
end
|
||||
|
@ -41,35 +39,41 @@ defmodule Backend.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.4.3"},
|
||||
{:phoenix_pubsub, "~> 1.1"},
|
||||
{:phoenix_ecto, "~> 4.0"},
|
||||
{:phoenix_view, "~> 2.0"},
|
||||
{:phoenix, "~> 1.7.0"},
|
||||
{:phoenix_live_view, "~> 0.18.18"},
|
||||
{:phoenix_live_dashboard, "~> 0.7.2"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
{:telemetry_poller, "~> 0.5"},
|
||||
{:phoenix_pubsub, "~> 2.1.1"},
|
||||
{:phoenix_ecto, "~> 4.4.0"},
|
||||
{:ecto_sql, "~> 3.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:gettext, "~> 0.11"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:httpoison, "~> 1.5"},
|
||||
{:plug_cowboy, "~> 2.1"},
|
||||
{:httpoison, "~> 2.1", override: true},
|
||||
{:timex, "~> 3.5"},
|
||||
{:honeydew, "~> 1.4.3"},
|
||||
{:quantum, "~> 2.3"},
|
||||
{:corsica, "~> 1.1.2"},
|
||||
{:honeydew, "~> 1.5.0"},
|
||||
{:quantum, "~> 3.3"},
|
||||
{:corsica, "~> 1.3"},
|
||||
{:sobelow, "~> 0.8", only: [:dev, :test]},
|
||||
{:gollum, "~> 0.3.2"},
|
||||
{:public_suffix, "~> 0.6.0"},
|
||||
{:idna, "~> 5.1.2", override: true},
|
||||
{:swoosh, "~> 0.23.3"},
|
||||
{:ex_twilio, "~> 0.7.0"},
|
||||
{:public_suffix, git: "https://github.com/axelson/publicsuffix-elixir"},
|
||||
{:swoosh, "~> 1.0"},
|
||||
{:gen_smtp, "~> 1.2"},
|
||||
{:elasticsearch, "~> 1.0"},
|
||||
{:appsignal, "~> 1.10.1"},
|
||||
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
|
||||
{:nebulex, "~> 1.1"},
|
||||
{:appsignal, "~> 2.7"},
|
||||
{:appsignal_phoenix, "~> 2.3"},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:nebulex, "~> 2.4.2"},
|
||||
{:hunter, "~> 0.5.1"},
|
||||
{:poison, "~> 4.0", override: true},
|
||||
{:scrivener_ecto, "~> 2.2"},
|
||||
{:recase, "~> 0.6.0"},
|
||||
{:ex_rated, "~> 1.3"},
|
||||
{:html_sanitize_ex, "~> 1.4"}
|
||||
{:recase, "~> 0.7"},
|
||||
{:ex_rated, "~> 2.1"},
|
||||
{:html_sanitize_ex, "~> 1.4"},
|
||||
{:mox, "~> 1.0", only: [:test]}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
133
backend/mix.lock
133
backend/mix.lock
|
@ -1,70 +1,81 @@
|
|||
%{
|
||||
"appsignal": {:hex, :appsignal, "1.10.1", "582238fd95cef54d1440ce9b2ef47c243e7d176200156164b2d924c29c7cef24", [:make, :mix], [{:decorator, "~> 1.2.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "686bf26b678d958a366215923de605492f99ca6ad7715a89a4abaf9caf6ea45d"},
|
||||
"artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"},
|
||||
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
|
||||
"appsignal": {:hex, :appsignal, "2.7.3", "5cd234052e49014c1590458627eebb68b9250ef13653734a0cf607736e05233b", [:make, :mix], [{:decorator, "~> 1.2.3 or ~> 1.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c24b96512ae7892b075d06d7294aadf89fb18b6e419ace6f3816783831e30d35"},
|
||||
"appsignal_phoenix": {:hex, :appsignal_phoenix, "2.3.2", "80b2405fa8c4b8c27401b3dda32f90d380fb3f63d67efd6162d9489b8e409b06", [:mix], [{:appsignal, ">= 2.5.1 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:appsignal_plug, ">= 2.0.13 and < 3.0.0", [hex: :appsignal_plug, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.11 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.9", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8543c90688e309a503d7c3425558bf8182730b3efab546d84ec2f5c78eeaebfe"},
|
||||
"appsignal_plug": {:hex, :appsignal_plug, "2.0.13", "daea31daec103248532c2facbe01098f53914ddecba47263a66574f3b322ac57", [:mix], [{:appsignal, ">= 2.5.1 and < 3.0.0", [hex: :appsignal, repo: "hexpm", optional: false]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "704417abf28391ab3f6783ecf75b10242bf240554ddd8819d80b18b131cc2076"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||
"corsica": {:hex, :corsica, "1.1.3", "5f1de40bc9285753aa03afbdd10c364dac79b2ddbf2ba9c5c9c47b397ec06f40", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8156b3a14a114a346262871333a931a1766b2597b56bf994fcfcb65443a348ad"},
|
||||
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
|
||||
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
|
||||
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
|
||||
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"},
|
||||
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"decorator": {:hex, :decorator, "1.2.4", "31dfff6143d37f0b68d0bffb3b9f18ace14fea54d4f1b5e4f86ead6f00d9ff6e", [:mix], [], "hexpm", "6c393a3aada02d0eaa6bde725e1816d2b122d7d0fb06c6dd8ebd92d33826396b"},
|
||||
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.4.3", "c552aa8a7ccff2b64024f835503b3155d8e73452c180298527fbdbcd6e79710b", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec9e59d6fa3f8cfda9963ada371e9e6659167c2338a997bd7ea23b10b245842b"},
|
||||
"elasticsearch": {:hex, :elasticsearch, "1.0.0", "626d3fb8e7554d9c93eb18817ae2a3d22c2a4191cc903c4644b1334469b15374", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6.0", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm", "9fa0b717ad57a54c28451b3eb10c5121211c29a7b33615d2bcc7e2f3c9418b2e"},
|
||||
"ex2ms": {:hex, :ex2ms, "1.6.0", "f39bbd9ff1b0f27b3f707bab2d167066dd8965e7df1149b962d94c74615d0e09", [:mix], [], "hexpm", "0d1ab5e08421af5cd69146efb408dbb1ff77f38a2f4df5f086f2512dc8cf65bf"},
|
||||
"ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "26817cf0927b7a2e7bc2e14b4dab66a329fafa4520b513a8a4025532ac5a7cbf"},
|
||||
"ex_twilio": {:hex, :ex_twilio, "0.7.0", "d7ce624ef4661311ae28c3e3aa060ecb66a9f4843184d7400c29072f7d3f5a4a", [:mix], [{:httpoison, ">= 0.9.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:inflex, "~> 1.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "6be84f1508ed47d443d18cdc4ea0561f8ad4095b69791dd9be5f2fe14b1dafc5"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"},
|
||||
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"},
|
||||
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"corsica": {:hex, :corsica, "1.3.0", "bbec02ccbeca1fdf44ee23b25a8ae32f7c6c28fc127ef8836dd8420e8f65bd9b", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8847ec817554047e9aa6d9933539cacb10c4ee60b58e0c15c3b380c5b737b35f"},
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
|
||||
"crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"},
|
||||
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
|
||||
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
|
||||
"elasticsearch": {:hex, :elasticsearch, "1.0.1", "8339538d90af6b280f10ecd02b1eae372f09373e629b336a13461babf7366495", [:mix], [{:httpoison, ">= 0.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sigaws, "~> 0.7", [hex: :sigaws, repo: "hexpm", optional: true]}, {:vex, "~> 0.6", [hex: :vex, repo: "hexpm", optional: false]}], "hexpm", "83e7d8b8bee3e7e19a06ab4d357d24845ac1da894e79678227fd52c0b7f71867"},
|
||||
"ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"},
|
||||
"ex_rated": {:hex, :ex_rated, "2.1.0", "d40e6fe35097b10222df2db7bb5dd801d57211bac65f29063de5f201c2a6aebc", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "936c155337253ed6474f06d941999dd3a9cf0fe767ec99a59f2d2989dc2cc13f"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
||||
"gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
|
||||
"gollum": {:hex, :gollum, "0.3.3", "25ebb47700b9236bc4e5382bf91b72e4cdaf9bae3556172eff27e770735a198f", [:mix], [{:httpoison, "~> 1.5.1", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm", "39268eeaf4f0adb6fdebe4f8c36b10a277881ab2eee3419c9b6727759e2f5a5d"},
|
||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
|
||||
"honeydew": {:hex, :honeydew, "1.4.5", "03818730602274ef0119652d664b92ddf733256e857d29899ce6841e01345bd1", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "62633858ce7e82f67689b4d2b4024bd87fa00dc6a11a227614b816d868a1529d"},
|
||||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.0", "0310d27d7bafb662f30bff22ec732a72414799c83eaf44239781fd23b96216c0", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c5d79626be0b6e50c19ecdfb783ee26e85bd3a77436b488379ce6dc104ec4593"},
|
||||
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "191a3b6329c917de4e7ca68431919a59bf19e60694b313a69bc1f56a4cb160bf"},
|
||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||
"honeydew": {:hex, :honeydew, "1.5.0", "53088c1d87399efa5c0939adc8d32a9713b8fe6ce00a77c6769d2d363abac6bc", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "f71669e25f6a972e970ecbd79c34c4ad4b28369be78e4f8164fe8d0c5a674907"},
|
||||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.2", "c479398b6de798c03eb5d04a0a9a9159d73508f83f6590a00b8eacba3619cf4c", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "aef6c28585d06a9109ad591507e508854c5559561f950bbaea773900dd369b0e"},
|
||||
"httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
|
||||
"hunter": {:hex, :hunter, "0.5.1", "374dc4a800e2c340659657f8875e466075c7ea532e0d7a7787665f272b410150", [:mix], [{:httpoison, "~> 1.5", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "209b2cca7e4d51d5ff7ee4a0ab6cdc4c6ad23ddd61c9e12ceeee6f7ffbeae9c8"},
|
||||
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8fddb3aec4692c71647d67de72536254bce9069851754e370a99f2aae69fbdf4"},
|
||||
"inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm", "7b5ccb9b720c26516f5962dc4565fc26f083ca107b0f6c167048506a125d2df3"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
|
||||
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
|
||||
"libring": {:hex, :libring, "1.5.0", "44313eb6862f5c9168594a061e9d5f556a9819da7c6444706a9e2da533396d70", [:mix], [], "hexpm", "04e843d4fdcff49a62d8e03778d17c6cb2a03fe2d14020d3825a1761b55bd6cc"},
|
||||
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"joken": {:hex, :joken, "2.3.0", "62a979c46f2c81dcb8ddc9150453b60d3757d1ac393c72bb20fc50a7b0827dc6", [:mix], [{:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "57b263a79c0ec5d536ac02d569c01e6b4de91bd1cb825625fe90eab4feb7bc1e"},
|
||||
"jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"},
|
||||
"nebulex": {:hex, :nebulex, "1.1.1", "4117e18e614ecbd078e19558b7b9c58f11d666c4dca584b9382b02913f13ad8a", [:mix], [{:shards, "~> 0.6", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm", "cf3a04f9bfb8fcb8f070ab049c3fab54dd31c72d13430360a1c908c3cacb9196"},
|
||||
"paginator": {:hex, :paginator, "0.6.0", "bc2c01abdd98281ff39b6a7439cf540091122a7927bdaabc167c61d4508f9cbb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
|
||||
"plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"},
|
||||
"mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
|
||||
"nebulex": {:hex, :nebulex, "2.4.2", "b3d2d86d57b15896fb8e6d6dd49b4a9dee2eedd6eddfb3b69bfdb616a09c2817", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c9f888e5770fd47614c95990d0a02c3515216d51dc72e3c830eaf28f5649ba52"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.3", "4d8eca2c020c9ed81a28e7a8c60e0a4f6f9f7f6e12eb91dfd01301eac07424c1", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6b1bc308758f95ecf3e0d795389440a2ca88a903e0fda1f921c780918c16d640"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.2", "a4950b63ace57720b0fc1c6da083c53346b36f99021de89595cc4f026288ff51", [:mix], [], "hexpm", "45741676a94c71f9afdfed9d22d49b6856c026ff504db04e3dc03a1d86f8201c"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
||||
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
|
||||
"postgrex": {:hex, :postgrex, "0.15.4", "5d691c25fc79070705a2ff0e35ce0822b86a0ee3c6fdb7a4fb354623955e1aed", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "306515b9d975fcb2478dc337a1d27dc3bf8af7cd71017c333fe9db3a3d211b0a"},
|
||||
"public_suffix": {:hex, :public_suffix, "0.6.0", "100cfe86f13f9f6f0cf67e743b1b83c78dd1223a2c422fa03ebf4adff514cbc3", [:mix], [{:idna, ">= 1.2.0 and < 6.0.0", [hex: :idna, repo: "hexpm", optional: false]}], "hexpm", "663f29209e7930680cb1656cf144cc7484b37fe261f4417fec280d9a20363bfc"},
|
||||
"quantum": {:hex, :quantum, "2.4.0", "f2ad4b20988f848455d35ed0e884ba0c7629a27ee86cbec6a6e0fc214b6e69cf", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: true]}], "hexpm", "a125a9e65a5af740a1198f3b05c1a736fce3942f5e0dc2901e0f9be5745bea99"},
|
||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||
"recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm", "8712e318420a228eb2e6366ada230148ed3a4316a798319edd5512f64d78c990"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm", "30da36a427f2519cf75993271fb7c5aad1759682a70f90d880a85c3d743d2c57"},
|
||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.3.0", "057f9dd3c77315f0a470263c3565353860d0294404aed611b3524c6df9044189", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "dfa43ca660651da63239e5d4acbfd9c57c5759bbf3a2bdc16cd70777c9bc7e0d"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"},
|
||||
"public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "89372422ab8b433de508519ef474e39699fd11ca", []},
|
||||
"quantum": {:hex, :quantum, "3.5.0", "8d2c5ba68c55991e8975aca368e3ab844ba01f4b87c4185a7403280e2c99cf34", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "cab737d1d9779f43cb1d701f46dd05ea58146fd96238d91c9e0da662c1982bb6"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||
"shards": {:hex, :shards, "0.6.2", "e05d05537883220c3b8a8f9d40d5c8ba7ff6064c63ebb6b23046972f6863b2d1", [:make, :rebar3], [], "hexpm", "58afa3712f1f1256a2a15e39fa95b7cd758087aaa7a25beaf786daabd87890f0"},
|
||||
"sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
|
||||
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"},
|
||||
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
|
||||
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm", "da1d9bef8a092cc7e1e51f1298037a5ddfb0f657fe862dfe7ba4c5807b551c29"},
|
||||
"vex": {:hex, :vex, "0.6.0", "4e79b396b2ec18cd909eed0450b19108d9631842598d46552dc05031100b7a56", [:mix], [], "hexpm", "7e4d9b50dd72cf931b52aba3470513686007f2ad54832de37cdb659cc85ba73e"},
|
||||
"sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"swoosh": {:hex, :swoosh, "1.11.0", "00b4fff8c08347a47cc5cbe67d64df5aae0607a7a51171944f5b89216e2d62f5", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5e7c49b6259e50a5ed756517e23a7f916c0b73eb0752e864b1d83b28e2c204d9"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||
"telemetry_registry": {:hex, :telemetry_registry, "0.2.1", "fe648a691f2128e4279d993cd010994c67f282354dc061e697bf070d4b87b480", [:mix, :rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4221cefbcadd0b3e7076960339223742d973f1371bc20f3826af640257bc3690"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"vex": {:hex, :vex, "0.9.0", "613ea5eb3055662e7178b83e25b2df0975f68c3d8bb67c1645f0573e1a78d606", [:mix], [], "hexpm", "c69fff44d5c8aa3f1faee71bba1dcab05dd36364c5a629df8bb11751240c857f"},
|
||||
"websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
defmodule Backend.Crawler.Crawlers.MastodonTest do
|
||||
use Backend.DataCase
|
||||
|
||||
alias Backend.Crawler.Crawlers.Mastodon
|
||||
alias Backend.Crawler.ApiCrawler
|
||||
alias Backend.HttpBehaviour
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "is_instance_type?/2" do
|
||||
test "returns true for pleroma and smithereen" do
|
||||
assert Mastodon.is_instance_type?("example.com", %{instance_type: :pleroma})
|
||||
assert Mastodon.is_instance_type?("example.com", %{instance_type: :smithereen})
|
||||
end
|
||||
|
||||
test "returns true for mastodon instance" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance" ->
|
||||
{:ok, TestHelpers.load_json("mastodon/instance.json")}
|
||||
end)
|
||||
|
||||
assert Mastodon.is_instance_type?("example.com", nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "crawl/2" do
|
||||
test "does nothing for small instances" do
|
||||
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
|
||||
TestHelpers.load_json("mastodon/instance.json")
|
||||
|> Map.merge(%{"stats" => %{"user_count" => 1}})
|
||||
end)
|
||||
|
||||
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
|
||||
|
||||
assert result ==
|
||||
ApiCrawler.get_default() |> Map.merge(%{instance_type: :mastodon, user_count: 1})
|
||||
end
|
||||
|
||||
test "crawls large instance" do
|
||||
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
|
||||
TestHelpers.load_json("mastodon/instance.json")
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
|
||||
{:ok, TestHelpers.load_json("mastodon/peers.json")}
|
||||
end)
|
||||
|
||||
expect(
|
||||
HttpMock,
|
||||
:get_and_decode,
|
||||
fn "https://example.com/api/v1/timelines/public?local=true&limit=40" ->
|
||||
{:ok, TestHelpers.load_json("mastodon/timeline.json")}
|
||||
end
|
||||
)
|
||||
|
||||
expect(
|
||||
HttpMock,
|
||||
:get_and_decode,
|
||||
4,
|
||||
fn "https://example.com/api/v1/timelines/public?local=true&limit=40&max_id=123" ->
|
||||
{:ok, TestHelpers.load_json("mastodon/timeline.json")}
|
||||
end
|
||||
)
|
||||
|
||||
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
|
||||
|
||||
assert result == %{
|
||||
description: "long description",
|
||||
federation_restrictions: [],
|
||||
instance_type: :mastodon,
|
||||
interactions: %{},
|
||||
peers: ["other.com"],
|
||||
user_count: 100,
|
||||
status_count: 100,
|
||||
statuses_seen: 5,
|
||||
version: "1.2.3"
|
||||
}
|
||||
end
|
||||
|
||||
test "handles timelines that require auth" do
|
||||
expect(HttpMock, :get_and_decode!, fn "https://example.com/api/v1/instance" ->
|
||||
TestHelpers.load_json("mastodon/instance.json")
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
|
||||
{:ok, TestHelpers.load_json("mastodon/peers.json")}
|
||||
end)
|
||||
|
||||
expect(
|
||||
HttpMock,
|
||||
:get_and_decode,
|
||||
fn "https://example.com/api/v1/timelines/public?local=true&limit=40" ->
|
||||
{:error,
|
||||
%HttpBehaviour.Error{
|
||||
message: "HTTP request failed with status code 422",
|
||||
status_code: 422,
|
||||
body: "{\"error\":\"This method requires an authenticated user\"}"
|
||||
}}
|
||||
end
|
||||
)
|
||||
|
||||
result = Mastodon.crawl("example.com", ApiCrawler.get_default())
|
||||
|
||||
assert result == %{
|
||||
description: "long description",
|
||||
federation_restrictions: [],
|
||||
instance_type: :mastodon,
|
||||
interactions: %{},
|
||||
peers: ["other.com"],
|
||||
user_count: 100,
|
||||
status_count: 100,
|
||||
statuses_seen: 0,
|
||||
version: "1.2.3"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,68 @@
|
|||
defmodule Backend.Crawler.Crawlers.MisskeyTest do
|
||||
use Backend.DataCase
|
||||
|
||||
alias Backend.Crawler.Crawlers.Misskey
|
||||
alias Backend.Crawler.ApiCrawler
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "is_instance_type?/2" do
|
||||
test "returns true for misskey instance" do
|
||||
expect(HttpMock, :post_and_decode, fn "https://example.com/api/meta" ->
|
||||
{:ok, TestHelpers.load_json("misskey/meta.json")}
|
||||
end)
|
||||
|
||||
assert Misskey.is_instance_type?("example.com", nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "crawl/2" do
|
||||
test "does nothing for small instances" do
|
||||
expect(HttpMock, :post_and_decode, fn "https://example.com/api/stats" ->
|
||||
stats =
|
||||
TestHelpers.load_json("misskey/stats.json") |> Map.merge(%{"originalUsersCount" => 1})
|
||||
|
||||
{:ok, stats}
|
||||
end)
|
||||
|
||||
result = Misskey.crawl("example.com", ApiCrawler.get_default())
|
||||
|
||||
assert result == ApiCrawler.get_default() |> Map.merge(%{type: :misskey, user_count: 1})
|
||||
end
|
||||
|
||||
test "crawls large instances" do
|
||||
expect(HttpMock, :post_and_decode, fn "https://example.com/api/stats" ->
|
||||
{:ok, TestHelpers.load_json("misskey/stats.json")}
|
||||
end)
|
||||
|
||||
expect(HttpMock, :post_and_decode, fn "https://example.com/api/meta" ->
|
||||
{:ok, TestHelpers.load_json("misskey/meta.json")}
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://example.com/api/v1/instance/peers" ->
|
||||
{:ok, TestHelpers.load_json("misskey/peers.json")}
|
||||
end)
|
||||
|
||||
# status_count_limit is 5, response has 1 post per page, so we expect 5 requests
|
||||
expect(HttpMock, :post_and_decode!, 5, fn "https://example.com/api/notes/local-timeline",
|
||||
%{limit: 100} ->
|
||||
TestHelpers.load_json("misskey/notes.json")
|
||||
end)
|
||||
|
||||
result = Misskey.crawl("example.com", ApiCrawler.get_default())
|
||||
|
||||
assert result == %{
|
||||
description: "some description",
|
||||
federation_restrictions: [],
|
||||
instance_type: :misskey,
|
||||
interactions: %{},
|
||||
peers: ["other.com"],
|
||||
status_count: 20,
|
||||
statuses_seen: 5,
|
||||
user_count: 20,
|
||||
version: "13.12.2"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,193 @@
|
|||
defmodule Backend.Crawler.Crawlers.NodeinfoTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Backend.Crawler.Crawlers.Nodeinfo
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "crawl/2" do
|
||||
test "handles valid nodeinfo" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
|
||||
{:ok,
|
||||
%{
|
||||
"links" => [
|
||||
%{
|
||||
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href" => "https://mastodon.social/nodeinfo/2.0"
|
||||
}
|
||||
]
|
||||
}}
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/nodeinfo/2.0" ->
|
||||
{:ok,
|
||||
%{
|
||||
"version" => "2.0",
|
||||
"software" => %{
|
||||
"name" => "Mastodon",
|
||||
"version" => "1.2.3"
|
||||
},
|
||||
"protocols" => ["activitypub"],
|
||||
"services" => %{
|
||||
"inbound" => [],
|
||||
"outbound" => []
|
||||
},
|
||||
"usage" => %{
|
||||
"users" => %{
|
||||
"total" => 100,
|
||||
"activeMonth" => 1,
|
||||
"activeHalfYear" => 2
|
||||
},
|
||||
"localPosts" => 3
|
||||
},
|
||||
"openRegistrations" => true,
|
||||
"metadata" => %{}
|
||||
}}
|
||||
end)
|
||||
|
||||
result = Nodeinfo.crawl("mastodon.social", %{})
|
||||
|
||||
assert result == %{
|
||||
description: nil,
|
||||
user_count: 100,
|
||||
status_count: 3,
|
||||
statuses_seen: 0,
|
||||
instance_type: :mastodon,
|
||||
version: "1.2.3",
|
||||
federation_restrictions: [],
|
||||
interactions: %{},
|
||||
peers: []
|
||||
}
|
||||
end
|
||||
|
||||
test "handles small instances" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
|
||||
{:ok,
|
||||
%{
|
||||
"links" => [
|
||||
%{
|
||||
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href" => "https://mastodon.social/nodeinfo/2.0"
|
||||
}
|
||||
]
|
||||
}}
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/nodeinfo/2.0" ->
|
||||
{:ok,
|
||||
%{
|
||||
"version" => "2.0",
|
||||
"software" => %{
|
||||
"name" => "Mastodon",
|
||||
"version" => "1.2.3"
|
||||
},
|
||||
"protocols" => ["activitypub"],
|
||||
"services" => %{
|
||||
"inbound" => [],
|
||||
"outbound" => []
|
||||
},
|
||||
"usage" => %{
|
||||
"users" => %{
|
||||
"total" => 1,
|
||||
"activeMonth" => 1,
|
||||
"activeHalfYear" => 1
|
||||
},
|
||||
"localPosts" => 3
|
||||
},
|
||||
"openRegistrations" => true,
|
||||
"metadata" => %{}
|
||||
}}
|
||||
end)
|
||||
|
||||
result = Nodeinfo.crawl("mastodon.social", %{})
|
||||
|
||||
assert result == %{
|
||||
description: nil,
|
||||
user_count: 1,
|
||||
status_count: nil,
|
||||
statuses_seen: 0,
|
||||
instance_type: nil,
|
||||
version: nil,
|
||||
federation_restrictions: [],
|
||||
interactions: %{},
|
||||
peers: []
|
||||
}
|
||||
end
|
||||
|
||||
test "handles missing nodeinfo" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
|
||||
{:ok, %{}}
|
||||
end)
|
||||
|
||||
result = Nodeinfo.crawl("mastodon.social", %{})
|
||||
|
||||
assert result == %{
|
||||
description: nil,
|
||||
user_count: nil,
|
||||
status_count: nil,
|
||||
statuses_seen: 0,
|
||||
instance_type: nil,
|
||||
version: nil,
|
||||
federation_restrictions: [],
|
||||
interactions: %{},
|
||||
peers: []
|
||||
}
|
||||
end
|
||||
|
||||
test "handles non-200 response" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://mastodon.social/.well-known/nodeinfo" ->
|
||||
{:error, %Backend.HttpBehaviour.Error{status_code: 401}}
|
||||
end)
|
||||
|
||||
result = Nodeinfo.crawl("mastodon.social", %{})
|
||||
|
||||
assert result == %{
|
||||
description: nil,
|
||||
user_count: nil,
|
||||
status_count: nil,
|
||||
statuses_seen: 0,
|
||||
instance_type: nil,
|
||||
version: nil,
|
||||
federation_restrictions: [],
|
||||
interactions: %{},
|
||||
peers: []
|
||||
}
|
||||
end
|
||||
|
||||
# don't know why some pixelfed instances return numbers as strings
|
||||
# but i've seen it in the wild, so we need to handle it
|
||||
test "handles nodeinfo with some numbers stringified (pixelfed)" do
|
||||
expect(HttpMock, :get_and_decode, fn "https://pixelfed.social/.well-known/nodeinfo" ->
|
||||
{:ok,
|
||||
%{
|
||||
"links" => [
|
||||
%{
|
||||
"rel" => "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href" => "https://pixelfed.social/nodeinfo/2.0.json"
|
||||
}
|
||||
]
|
||||
}}
|
||||
end)
|
||||
|
||||
expect(HttpMock, :get_and_decode, fn "https://pixelfed.social/nodeinfo/2.0.json" ->
|
||||
{:ok, TestHelpers.load_json("nodeinfo/pixelfed.json")}
|
||||
end)
|
||||
|
||||
result = Nodeinfo.crawl("pixelfed.social", %{})
|
||||
|
||||
assert result == %{
|
||||
description:
|
||||
"Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.",
|
||||
user_count: 16,
|
||||
status_count: 60,
|
||||
statuses_seen: 0,
|
||||
instance_type: :pixelfed,
|
||||
version: "0.11.2",
|
||||
federation_restrictions: [],
|
||||
interactions: %{},
|
||||
peers: []
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,104 +0,0 @@
|
|||
defmodule BackendWeb.GraphControllerTest do
|
||||
use BackendWeb.ConnCase
|
||||
|
||||
alias Backend.Api
|
||||
alias Backend.Api.Graph
|
||||
|
||||
@create_attrs %{
|
||||
id: "some id",
|
||||
label: "some label",
|
||||
size: 120.5,
|
||||
x: 120.5,
|
||||
y: 120.5
|
||||
}
|
||||
@update_attrs %{
|
||||
id: "some updated id",
|
||||
label: "some updated label",
|
||||
size: 456.7,
|
||||
x: 456.7,
|
||||
y: 456.7
|
||||
}
|
||||
@invalid_attrs %{id: nil, label: nil, size: nil, x: nil, y: nil}
|
||||
|
||||
def fixture(:graph) do
|
||||
{:ok, graph} = Api.create_graph(@create_attrs)
|
||||
graph
|
||||
end
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, conn: put_req_header(conn, "accept", "application/json")}
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
test "lists all nodes", %{conn: conn} do
|
||||
conn = get(conn, Routes.graph_path(conn, :index))
|
||||
assert json_response(conn, 200)["data"] == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "create graph" do
|
||||
test "renders graph when data is valid", %{conn: conn} do
|
||||
conn = post(conn, Routes.graph_path(conn, :create), graph: @create_attrs)
|
||||
assert %{"id" => id} = json_response(conn, 201)["data"]
|
||||
|
||||
conn = get(conn, Routes.graph_path(conn, :show, id))
|
||||
|
||||
assert %{
|
||||
"id" => id,
|
||||
"id" => "some id",
|
||||
"label" => "some label",
|
||||
"size" => 120.5,
|
||||
"x" => 120.5,
|
||||
"y" => 120.5
|
||||
} = json_response(conn, 200)["data"]
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn} do
|
||||
conn = post(conn, Routes.graph_path(conn, :create), graph: @invalid_attrs)
|
||||
assert json_response(conn, 422)["errors"] != %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update graph" do
|
||||
setup [:create_graph]
|
||||
|
||||
test "renders graph when data is valid", %{conn: conn, graph: %Graph{id: id} = graph} do
|
||||
conn = put(conn, Routes.graph_path(conn, :update, graph), graph: @update_attrs)
|
||||
assert %{"id" => ^id} = json_response(conn, 200)["data"]
|
||||
|
||||
conn = get(conn, Routes.graph_path(conn, :show, id))
|
||||
|
||||
assert %{
|
||||
"id" => id,
|
||||
"id" => "some updated id",
|
||||
"label" => "some updated label",
|
||||
"size" => 456.7,
|
||||
"x" => 456.7,
|
||||
"y" => 456.7
|
||||
} = json_response(conn, 200)["data"]
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, graph: graph} do
|
||||
conn = put(conn, Routes.graph_path(conn, :update, graph), graph: @invalid_attrs)
|
||||
assert json_response(conn, 422)["errors"] != %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete graph" do
|
||||
setup [:create_graph]
|
||||
|
||||
test "deletes chosen graph", %{conn: conn, graph: graph} do
|
||||
conn = delete(conn, Routes.graph_path(conn, :delete, graph))
|
||||
assert response(conn, 204)
|
||||
|
||||
assert_error_sent 404, fn ->
|
||||
get(conn, Routes.graph_path(conn, :show, graph))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_graph(_) do
|
||||
graph = fixture(:graph)
|
||||
{:ok, graph: graph}
|
||||
end
|
||||
end
|
|
@ -1,91 +0,0 @@
|
|||
defmodule BackendWeb.InstanceControllerTest do
|
||||
use BackendWeb.ConnCase
|
||||
|
||||
alias Backend.Api
|
||||
alias Backend.Api.Instance
|
||||
|
||||
@create_attrs %{
|
||||
name: "some name"
|
||||
}
|
||||
@update_attrs %{
|
||||
name: "some updated name"
|
||||
}
|
||||
@invalid_attrs %{name: nil}
|
||||
|
||||
def fixture(:instance) do
|
||||
{:ok, instance} = Api.create_instance(@create_attrs)
|
||||
instance
|
||||
end
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, conn: put_req_header(conn, "accept", "application/json")}
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
test "lists all instances", %{conn: conn} do
|
||||
conn = get(conn, Routes.instance_path(conn, :index))
|
||||
assert json_response(conn, 200)["data"] == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "create instance" do
|
||||
test "renders instance when data is valid", %{conn: conn} do
|
||||
conn = post(conn, Routes.instance_path(conn, :create), instance: @create_attrs)
|
||||
assert %{"id" => id} = json_response(conn, 201)["data"]
|
||||
|
||||
conn = get(conn, Routes.instance_path(conn, :show, id))
|
||||
|
||||
assert %{
|
||||
"id" => id,
|
||||
"name" => "some name"
|
||||
} = json_response(conn, 200)["data"]
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn} do
|
||||
conn = post(conn, Routes.instance_path(conn, :create), instance: @invalid_attrs)
|
||||
assert json_response(conn, 422)["errors"] != %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update instance" do
|
||||
setup [:create_instance]
|
||||
|
||||
test "renders instance when data is valid", %{
|
||||
conn: conn,
|
||||
instance: %Instance{id: id} = instance
|
||||
} do
|
||||
conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @update_attrs)
|
||||
assert %{"id" => ^id} = json_response(conn, 200)["data"]
|
||||
|
||||
conn = get(conn, Routes.instance_path(conn, :show, id))
|
||||
|
||||
assert %{
|
||||
"id" => id,
|
||||
"name" => "some updated name"
|
||||
} = json_response(conn, 200)["data"]
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, instance: instance} do
|
||||
conn = put(conn, Routes.instance_path(conn, :update, instance), instance: @invalid_attrs)
|
||||
assert json_response(conn, 422)["errors"] != %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete instance" do
|
||||
setup [:create_instance]
|
||||
|
||||
test "deletes chosen instance", %{conn: conn, instance: instance} do
|
||||
conn = delete(conn, Routes.instance_path(conn, :delete, instance))
|
||||
assert response(conn, 204)
|
||||
|
||||
assert_error_sent 404, fn ->
|
||||
get(conn, Routes.instance_path(conn, :show, instance))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_instance(_) do
|
||||
instance = fixture(:instance)
|
||||
{:ok, instance: instance}
|
||||
end
|
||||
end
|
|
@ -19,11 +19,14 @@ defmodule BackendWeb.ConnCase do
|
|||
using do
|
||||
quote do
|
||||
# Import conveniences for testing with connections
|
||||
use Phoenix.ConnTest
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
alias BackendWeb.Router.Helpers, as: Routes
|
||||
|
||||
# The default endpoint for testing
|
||||
@endpoint BackendWeb.Endpoint
|
||||
|
||||
use BackendWeb, :verified_routes
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"uri": "mastodon.social",
|
||||
"title": "Mastodon",
|
||||
"short_description": "short description",
|
||||
"description": "long description",
|
||||
"email": "staff@mastodon.social",
|
||||
"version": "1.2.3",
|
||||
"urls": {
|
||||
"streaming_api": "wss://streaming.mastodon.social"
|
||||
},
|
||||
"stats": {
|
||||
"user_count": 100,
|
||||
"status_count": 100,
|
||||
"domain_count": 55958
|
||||
},
|
||||
"thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
|
||||
"languages": ["en"],
|
||||
"registrations": true,
|
||||
"approval_required": false,
|
||||
"invites_enabled": true,
|
||||
"configuration": {
|
||||
"accounts": {
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"statuses": {
|
||||
"max_characters": 500,
|
||||
"max_media_attachments": 4,
|
||||
"characters_reserved_per_url": 23
|
||||
},
|
||||
"media_attachments": {
|
||||
"supported_mime_types": [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg",
|
||||
"audio/wave",
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/x-pn-wave",
|
||||
"audio/vnd.wave",
|
||||
"audio/ogg",
|
||||
"audio/vorbis",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/flac",
|
||||
"audio/aac",
|
||||
"audio/m4a",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"audio/3gpp",
|
||||
"video/x-ms-asf"
|
||||
],
|
||||
"image_size_limit": 16777216,
|
||||
"image_matrix_limit": 33177600,
|
||||
"video_size_limit": 103809024,
|
||||
"video_frame_rate_limit": 120,
|
||||
"video_matrix_limit": 8294400
|
||||
},
|
||||
"polls": {
|
||||
"max_options": 4,
|
||||
"max_characters_per_option": 50,
|
||||
"min_expiration": 300,
|
||||
"max_expiration": 2629746
|
||||
}
|
||||
},
|
||||
"contact_account": {
|
||||
"id": "13179",
|
||||
"username": "Mastodon",
|
||||
"acct": "Mastodon",
|
||||
"display_name": "Mastodon",
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": true,
|
||||
"group": false,
|
||||
"created_at": "2016-11-23T00:00:00.000Z",
|
||||
"note": "<p>Official account of the Mastodon project. News, releases, announcements! Learn more on our website!</p>",
|
||||
"url": "https://mastodon.social/@Mastodon",
|
||||
"avatar": "https://files.mastodon.social/accounts/avatars/000/013/179/original/b4ceb19c9c54ec7e.png",
|
||||
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/013/179/original/b4ceb19c9c54ec7e.png",
|
||||
"header": "https://files.mastodon.social/accounts/headers/000/013/179/original/878f382e7dd9fb84.png",
|
||||
"header_static": "https://files.mastodon.social/accounts/headers/000/013/179/original/878f382e7dd9fb84.png",
|
||||
"followers_count": 778859,
|
||||
"following_count": 8,
|
||||
"statuses_count": 237,
|
||||
"last_status_at": "2023-05-13",
|
||||
"noindex": false,
|
||||
"emojis": [],
|
||||
"roles": [],
|
||||
"fields": [
|
||||
{
|
||||
"name": "Homepage",
|
||||
"value": "<a href=\"https://joinmastodon.org\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">joinmastodon.org</span><span class=\"invisible\"></span></a>",
|
||||
"verified_at": "2018-10-31T04:11:00.076+00:00"
|
||||
},
|
||||
{
|
||||
"name": "Patreon",
|
||||
"value": "<a href=\"https://patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>",
|
||||
"verified_at": null
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"value": "<a href=\"https://github.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">github.com/mastodon</span><span class=\"invisible\"></span></a>",
|
||||
"verified_at": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "No incitement of violence or promotion of violent ideologies"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "No harassment, dogpiling or doxxing of other users"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"text": "Do not share intentionally false or misleading information"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
["other.com"]
|
|
@ -0,0 +1,55 @@
|
|||
[
|
||||
{
|
||||
"id": "123",
|
||||
"created_at": "2023-06-10T18:59:36.207Z",
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "public",
|
||||
"language": "de",
|
||||
"uri": "https://mastodon.social/users/someuser/statuses/110521455489577427",
|
||||
"url": "https://mastodon.social/@someuser/110521455489577427",
|
||||
"replies_count": 0,
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"edited_at": null,
|
||||
"content": "<p>New post</p>",
|
||||
"reblog": null,
|
||||
"application": {
|
||||
"name": "IFTTT",
|
||||
"website": "https://www.ifttt.com"
|
||||
},
|
||||
"account": {
|
||||
"id": "108265572384945996",
|
||||
"username": "someuser",
|
||||
"acct": "someuser",
|
||||
"display_name": "Some User",
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": true,
|
||||
"group": false,
|
||||
"created_at": "2022-05-08T00:00:00.000Z",
|
||||
"note": "<p>My account</p>",
|
||||
"url": "https://mastodon.social/@someuser",
|
||||
"avatar": "https://example.com/picture.jpg",
|
||||
"avatar_static": "https://example.com/picture.jpg",
|
||||
"header": "https://example.com/picture.jpg",
|
||||
"header_static": "https://example.com/picture.jpg",
|
||||
"followers_count": 7,
|
||||
"following_count": 73,
|
||||
"statuses_count": 256,
|
||||
"last_status_at": "2023-06-10",
|
||||
"noindex": false,
|
||||
"emojis": [],
|
||||
"roles": [],
|
||||
"fields": []
|
||||
},
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"tags": [],
|
||||
"emojis": [],
|
||||
"card": {},
|
||||
"poll": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,170 @@
|
|||
{
|
||||
"maintainerName": "MisskeyHQ",
|
||||
"maintainerEmail": "https://go.misskey.io/support",
|
||||
"version": "13.12.2",
|
||||
"name": "Misskey.io",
|
||||
"uri": "https://misskey.io",
|
||||
"description": "some description",
|
||||
"langs": ["ja", "en", "zh", "ko", "fr", "de"],
|
||||
"tosUrl": "http://go.misskey.io/tos",
|
||||
"repositoryUrl": "https://github.com/syuilo/misskey",
|
||||
"feedbackUrl": "https://github.com/syuilo/misskey/issues/new",
|
||||
"disableRegistration": false,
|
||||
"emailRequiredForSignup": true,
|
||||
"enableHcaptcha": false,
|
||||
"hcaptchaSiteKey": "95d75440-7e37-4419-a693-8f52c377f1c5",
|
||||
"enableRecaptcha": false,
|
||||
"recaptchaSiteKey": "6LfW8qQUAAAAAI_1WMThmcj6zO39laasAoEJHfFF",
|
||||
"enableTurnstile": true,
|
||||
"turnstileSiteKey": "0x4AAAAAAACJmZyh3LCvo-uf",
|
||||
"swPublickey": "BHqCPVsCM8pMUo26Fenl6fuLPfuqQTNeo2Rpvt6KFxFEKznKAXZBHI2nk1aAanlJ1Me_PSr-MVkW3ho4RaYmZpk",
|
||||
"themeColor": "#86b300",
|
||||
"mascotImageUrl": "/assets/ai.png",
|
||||
"bannerUrl": "https://s3.arkjp.net/misskey/65b25d3c-2ae4-474f-b1c0-050c8c8962e1.jpg",
|
||||
"errorImageUrl": "https://s3.arkjp.net/misskey/94aab3c5-0b26-42a7-9fa9-83a69d7253cd.png",
|
||||
"iconUrl": "https://s3.arkjp.net/misskey/webpublic-0c66b1ca-b8c0-4eaa-9827-47674f4a1580.png",
|
||||
"backgroundImageUrl": "https://s3.arkjp.net/misskey/e23f6837-c477-4f40-bbc7-b8a06e3bc1cc.jpg",
|
||||
"logoImageUrl": "https://s3.arkjp.net/misskey/31240fa8-98fa-4750-bfd4-767753d1c48d.png",
|
||||
"maxNoteTextLength": 3000,
|
||||
"defaultLightTheme": null,
|
||||
"defaultDarkTheme": null,
|
||||
"ads": [
|
||||
{
|
||||
"id": "8riz9d7mt0",
|
||||
"url": "http://go.misskey.io/nextdns",
|
||||
"place": "horizontal",
|
||||
"ratio": 3,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/03992473-790d-4e50-9f70-f12ed1a5aabb.png"
|
||||
},
|
||||
{
|
||||
"id": "8rkte84ghf",
|
||||
"url": "https://go.misskey.io/vultr",
|
||||
"place": "horizontal",
|
||||
"ratio": 3,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/fa1421c3-fabc-4dbc-a688-52dfb7491660.webp"
|
||||
},
|
||||
{
|
||||
"id": "97crkngnt7",
|
||||
"url": "https://go.misskey.io/ads",
|
||||
"place": "horizontal",
|
||||
"ratio": 1,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/d85e0e31-522b-4779-8479-b7acb65c86dc.png"
|
||||
},
|
||||
{
|
||||
"id": "97crxz63al",
|
||||
"url": "https://go.misskey.io/ads",
|
||||
"place": "horizontal",
|
||||
"ratio": 1,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/d73e0b21-5910-42f7-9f1b-6983026ee1db.png"
|
||||
},
|
||||
{
|
||||
"id": "97cspskh3f",
|
||||
"url": "https://go.misskey.io/ads",
|
||||
"place": "horizontal",
|
||||
"ratio": 1,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/36fe91e6-5d11-4f12-8bc8-426ab0ebd885.png"
|
||||
},
|
||||
{
|
||||
"id": "9clihjru6p",
|
||||
"url": "https://go.misskey.io/maZC",
|
||||
"place": "horizontal",
|
||||
"ratio": 40,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/ee74935a-a1ec-4385-bb36-2377387118b8.png"
|
||||
},
|
||||
{
|
||||
"id": "9e5idmskrv",
|
||||
"url": "https://go.misskey.io/LfNP",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 20,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/4e03fca5-b25f-4950-974a-313a7d958b6d.png"
|
||||
},
|
||||
{
|
||||
"id": "9eo9im87s6",
|
||||
"url": "https://go.misskey.io/jHVi",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 40,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/7e903951-e7e1-4277-badf-0ea5bc9ab07a.png"
|
||||
},
|
||||
{
|
||||
"id": "9f00tkswhg",
|
||||
"url": "https://go.misskey.io/wD6e",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 20,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/015a3335-b21e-4897-a958-d6879b2a82f1.png"
|
||||
},
|
||||
{
|
||||
"id": "9f3stos3s7",
|
||||
"url": "https://go.misskey.io/iMxB",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 40,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/098ceb69-8238-4c3c-8f99-f9752294cb96.png"
|
||||
},
|
||||
{
|
||||
"id": "9fgxdm8mpd",
|
||||
"url": "https://go.misskey.io/dwRP",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 60,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/c4650ddc-687e-46c0-932a-c1f5ca8c9f83.png"
|
||||
},
|
||||
{
|
||||
"id": "9fhdbdyevw",
|
||||
"url": "https://go.misskey.io/pwYv",
|
||||
"place": "horizontal",
|
||||
"ratio": 40,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/e2f6c692-0d16-4fe9-90c4-25eac1b31731.png"
|
||||
},
|
||||
{
|
||||
"id": "9fgjjfdr3s",
|
||||
"url": "https://go.misskey.io/VwEm",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 60,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/951bda53-3707-480e-ab5a-0000ca9c7578.png"
|
||||
},
|
||||
{
|
||||
"id": "9fnpd9tsgs",
|
||||
"url": "https://go.misskey.io/QwId",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 80,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/261f7323-7a5e-4734-92bc-6ad69a4226df.jpg"
|
||||
},
|
||||
{
|
||||
"id": "9fmru4ok7f",
|
||||
"url": "https://go.misskey.io/pwYv",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 20,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/563a709e-6d5e-4952-9cb5-ac6897f80990.png"
|
||||
},
|
||||
{
|
||||
"id": "9ew2fhwyfc",
|
||||
"url": "https://go.misskey.io/sjxJ",
|
||||
"place": "horizontal-big",
|
||||
"ratio": 60,
|
||||
"imageUrl": "https://s3.arkjp.net/misskey/8dff6f2d-444f-459f-80ff-02cad454be91.png"
|
||||
}
|
||||
],
|
||||
"enableEmail": true,
|
||||
"enableServiceWorker": true,
|
||||
"translatorAvailable": true,
|
||||
"serverRules": [],
|
||||
"policies": {
|
||||
"gtlAvailable": true,
|
||||
"ltlAvailable": true,
|
||||
"canPublicNote": true,
|
||||
"canInvite": false,
|
||||
"canManageCustomEmojis": false,
|
||||
"canSearchNotes": false,
|
||||
"canHideAds": false,
|
||||
"driveCapacityMb": 10240,
|
||||
"alwaysMarkNsfw": false,
|
||||
"pinLimit": 3,
|
||||
"antennaLimit": 5,
|
||||
"wordMuteLimit": 200,
|
||||
"webhookLimit": 3,
|
||||
"clipLimit": 10,
|
||||
"noteEachClipsLimit": 50,
|
||||
"userListLimit": 5,
|
||||
"userEachUserListsLimit": 20,
|
||||
"rateLimitFactor": 2
|
||||
},
|
||||
"mediaProxy": "https://nos3.arkjp.net"
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
[
|
||||
{
|
||||
"id": "9ftvwz5kx8no7aeb",
|
||||
"createdAt": "2023-06-10T18:18:57.656Z",
|
||||
"userId": "9badbj0vp9",
|
||||
"user": {
|
||||
"id": "9badbj0vp9",
|
||||
"name": "Some Name",
|
||||
"username": "username",
|
||||
"host": null,
|
||||
"avatarUrl": "https://example.com/image.png",
|
||||
"avatarBlurhash": "foobar",
|
||||
"avatarColor": null,
|
||||
"speakAsCat": true,
|
||||
"emojis": [],
|
||||
"onlineStatus": "online",
|
||||
"driveCapacityOverrideMb": null
|
||||
},
|
||||
"text": "My post",
|
||||
"cw": null,
|
||||
"visibility": "public",
|
||||
"renoteCount": 0,
|
||||
"repliesCount": 0,
|
||||
"reactions": {},
|
||||
"reactionEmojis": [],
|
||||
"emojis": [],
|
||||
"tags": ["post"],
|
||||
"fileIds": [],
|
||||
"files": [],
|
||||
"replyId": null,
|
||||
"renoteId": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
["other.com"]
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"notesCount": 10,
|
||||
"originalNotesCount": 20,
|
||||
"usersCount": 10,
|
||||
"originalUsersCount": 20,
|
||||
"reactionsCount": 64569657,
|
||||
"instances": 21184,
|
||||
"driveUsageLocal": 0,
|
||||
"driveUsageRemote": 0
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"metadata": {
|
||||
"nodeName": "Pixelfed",
|
||||
"software": {
|
||||
"homepage": "https://pixelfed.org",
|
||||
"repo": "https://github.com/pixelfed/pixelfed"
|
||||
},
|
||||
"config": {
|
||||
"open_registration": true,
|
||||
"uploader": {
|
||||
"max_photo_size": "15000",
|
||||
"max_caption_length": "500",
|
||||
"album_limit": "4",
|
||||
"image_quality": 80,
|
||||
"max_collection_length": 18,
|
||||
"optimize_image": true,
|
||||
"optimize_video": true,
|
||||
"media_types": "image/jpeg,image/png,image/gif",
|
||||
"enforce_account_limit": true
|
||||
},
|
||||
"activitypub": {
|
||||
"enabled": true,
|
||||
"remote_follow": true
|
||||
},
|
||||
"ab": {
|
||||
"lc": false,
|
||||
"rec": false,
|
||||
"loops": false,
|
||||
"top": false,
|
||||
"polls": false,
|
||||
"cached_public_timeline": false,
|
||||
"gps": false,
|
||||
"spa": true,
|
||||
"emc": false
|
||||
},
|
||||
"site": {
|
||||
"name": "Pixelfe",
|
||||
"domain": "pixelfed.example.com",
|
||||
"url": "https://pixelfed.example.com",
|
||||
"description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms."
|
||||
},
|
||||
"username": {
|
||||
"remote": {
|
||||
"formats": ["@", "from", "custom"],
|
||||
"format": "@",
|
||||
"custom": null
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"mobile_apis": true,
|
||||
"circles": false,
|
||||
"stories": false,
|
||||
"video": false,
|
||||
"import": {
|
||||
"instagram": false,
|
||||
"mastodon": false,
|
||||
"pixelfed": false
|
||||
},
|
||||
"label": {
|
||||
"covid": {
|
||||
"enabled": true,
|
||||
"org": "visit the WHO website",
|
||||
"url": "https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"protocols": ["activitypub"],
|
||||
"services": {
|
||||
"inbound": [],
|
||||
"outbound": []
|
||||
},
|
||||
"software": {
|
||||
"name": "pixelfed",
|
||||
"version": "0.11.2"
|
||||
},
|
||||
"usage": {
|
||||
"localPosts": "60",
|
||||
"localComments": 0,
|
||||
"users": {
|
||||
"total": "16",
|
||||
"activeHalfyear": 16,
|
||||
"activeMonth": 2
|
||||
}
|
||||
},
|
||||
"version": "2.0",
|
||||
"openRegistrations": true
|
||||
}
|
|
@ -1,2 +1,14 @@
|
|||
Mox.defmock(HttpMock, for: Backend.HttpBehaviour)
|
||||
Application.put_env(:backend, :http, HttpMock)
|
||||
|
||||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Backend.Repo, :manual)
|
||||
|
||||
defmodule TestHelpers do
|
||||
@spec load_json(String.t()) :: any()
|
||||
def load_json(path) do
|
||||
Path.join([__DIR__, "support", "data", "json", path])
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
version: "2"
|
||||
|
||||
networks:
|
||||
space:
|
||||
external: false
|
||||
|
||||
services:
|
||||
phoenix:
|
||||
build: backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- space
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
gephi:
|
||||
build: gephi
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
networks:
|
||||
- space
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
|
||||
elastic:
|
||||
image: elasticsearch:8.7.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
networks:
|
||||
- space
|
|
@ -5,20 +5,30 @@ module.exports = {
|
|||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
plugins: ["@typescript-eslint", "prettier"],
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"prettier",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"react/prop-types": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unsafe-assignment": ["off"],
|
||||
"@typescript-eslint/no-unsafe-argument": ["off"],
|
||||
"@typescript-eslint/no-unsafe-call": ["off"],
|
||||
"@typescript-eslint/no-unsafe-member-access": ["off"],
|
||||
"@typescript-eslint/no-unsafe-return": ["off"],
|
||||
"@typescript-eslint/restrict-template-expressions": ["off"],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend
|
||||
npx lint-staged
|
Plik diff jest za duży
Load Diff
|
@ -11,7 +11,7 @@
|
|||
<!-- Open Graph -->
|
||||
<meta property="og:site_name" content="fediverse.space" />
|
||||
<meta property="og:description" content="" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/preview.png" />
|
||||
<meta property="og:image" content="/preview.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="914" />
|
||||
<meta property="og:image:height" content="679" />
|
||||
|
@ -20,25 +20,13 @@
|
|||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="fediverse.space">
|
||||
<meta name="twitter:description" content="A tool to visualize decentralized social networks.">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.png">
|
||||
<meta name="twitter:image" content="/preview.png">
|
||||
<meta name="twitter:image:alt" content="A screenshot of fediverse.space. Shows a graph of fediverse instances." />
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
<title>fediverse.space</title>
|
||||
|
||||
<script defer data-domain="fediverse.space" data-api="https://btao.org/workers/btao/event" src="https://btao.org/workers/btao/script.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -46,6 +34,8 @@
|
|||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<a rel="me" href="https://mastodon.social/@fediversespace" style="display: none">Mastodon</a>
|
||||
</body>
|
||||
|
||||
</html>
|
Plik diff jest za duży
Load Diff
|
@ -3,85 +3,79 @@
|
|||
"version": "2.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=development react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"start": "vite",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"serve": "vite preview",
|
||||
"typecheck": "tsc --noemit",
|
||||
"lint": "yarn typecheck && yarn eslint src/ --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "npm run lint --fix",
|
||||
"pretty": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||
"test": "yarn lint && react-scripts test --ci",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
"prepare": "cd .. && husky install frontend/.husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"yarn pretty",
|
||||
"yarn lint:fix"
|
||||
"npm run pretty",
|
||||
"npm run lint:fix"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.26.1",
|
||||
"@blueprintjs/icons": "^3.16.0",
|
||||
"@blueprintjs/select": "^3.12.3",
|
||||
"classnames": "^2.2.6",
|
||||
"connected-react-router": "^6.5.2",
|
||||
"cross-fetch": "^3.0.4",
|
||||
"cytoscape": "^3.15.0",
|
||||
"cytoscape-popper": "^1.0.7",
|
||||
"inflection": "^1.12.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.25.3",
|
||||
"normalize.css": "^8.0.0",
|
||||
"@blueprintjs/core": "^4.20.1",
|
||||
"@blueprintjs/icons": "^4.16.0",
|
||||
"@blueprintjs/select": "^4.9.22",
|
||||
"classnames": "^2.3.2",
|
||||
"connected-react-router": "^6.9.3",
|
||||
"cross-fetch": "^3.1.6",
|
||||
"cytoscape": "^3.25.0",
|
||||
"cytoscape-popper": "^2.0.0",
|
||||
"inflection": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"normalize.css": "^8.0.1",
|
||||
"numeral": "^2.0.6",
|
||||
"react": "^16.10.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-sigma": "^1.2.30",
|
||||
"react-virtualized": "^9.21.1",
|
||||
"redux": "^4.0.4",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sanitize-html": "^1.20.1",
|
||||
"styled-components": "^5.1.0",
|
||||
"tippy.js": "^4.3.5"
|
||||
"react": "^17",
|
||||
"react-dom": "^17",
|
||||
"react-redux": "^7",
|
||||
"react-router-dom": "^5",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"redux": "^4.2.1",
|
||||
"redux-first-history": "^5.1.1",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/cytoscape": "^3.8.3",
|
||||
"@types/inflection": "^1.5.28",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/node": "^14.0.1",
|
||||
"@types/numeral": "^0.0.28",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-axe": "^3.1.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/sanitize-html": "^1.23.0",
|
||||
"@types/styled-components": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.24.0",
|
||||
"@typescript-eslint/parser": "^2.34.0",
|
||||
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.2",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.4",
|
||||
"prettier": "^2.0.5",
|
||||
"react-axe": "3.3.0",
|
||||
"typescript": "^3.9.2"
|
||||
"@types/classnames": "^2.3.0",
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"@types/inflection": "^1.13.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.2.6",
|
||||
"@types/numeral": "^2.0.2",
|
||||
"@types/react": "^17",
|
||||
"@types/react-dom": "^17",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
||||
"@typescript-eslint/parser": "^5.59.9",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^5.1.3",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import { Classes } from "@blueprintjs/core";
|
||||
|
||||
import { ConnectedRouter } from "connected-react-router";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Nav } from "./components/organisms";
|
||||
import {
|
||||
AboutScreen,
|
||||
|
@ -20,11 +20,23 @@ const AppRouter: React.FC = () => (
|
|||
<div className={`${Classes.DARK} App`}>
|
||||
<Nav />
|
||||
<main role="main">
|
||||
<Route path="/instances" exact component={TableScreen} />
|
||||
<Route path="/about" exact component={AboutScreen} />
|
||||
<Route path="/admin/login" exact component={LoginScreen} />
|
||||
<Route path="/admin/verify" exact component={VerifyLoginScreen} />
|
||||
<Route path="/admin" exact component={AdminScreen} />
|
||||
<Switch>
|
||||
<Route path="/instances" exact>
|
||||
<TableScreen />
|
||||
</Route>
|
||||
<Route path="/about" exact>
|
||||
<AboutScreen />
|
||||
</Route>
|
||||
<Route path="/admin/login" exact>
|
||||
<LoginScreen />
|
||||
</Route>
|
||||
<Route path="/admin/verify" exact>
|
||||
<VerifyLoginScreen />
|
||||
</Route>
|
||||
<Route path="/admin" exact>
|
||||
<AdminScreen />
|
||||
</Route>
|
||||
</Switch>
|
||||
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
||||
<GraphScreen />
|
||||
</main>
|
||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 5.2 KiB |
|
@ -1,30 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 603.8 104.9" enable-background="new 0 0 603.8 104.9" xml:space="preserve">
|
||||
<g>
|
||||
<polygon fill="#4988A2" points="481.8,16.9 481.8,80.7 561.3,80.7 "/>
|
||||
<polygon fill="#7A4198" points="538.3,80.7 602.1,7.4 602.1,80.7 "/>
|
||||
<polygon fill="#478B60" points="481.9,80.7 513.5,1.1 561,80.7 "/>
|
||||
<polygon fill="#135F66" points="488.3,80.7 515.3,43.9 547.7,69.9 538.3,80.7 "/>
|
||||
<polygon fill="#2B7B82" points="488.3,80.7 481.9,80.7 501.1,32.4 515.3,43.9 "/>
|
||||
<polygon fill="#194A7F" points="547.7,69.9 538.3,80.7 561.1,80.7 "/>
|
||||
<polyline fill="#5E2B7C" points="551.7,65.3 547.7,69.9 561.1,80.7 551.7,65.3 "/>
|
||||
<polygon fill="#7A2980" points="602.1,80.7 561.1,80.7 551.7,65.3 575.6,37.8 "/>
|
||||
<polygon fill="#456630" points="515.3,43.9 528.6,26.4 551.7,65.3 547.7,69.9 "/>
|
||||
<polygon fill="#E77A45" points="528.6,26.4 535.4,16.9 551.3,39.2 566.9,23.9 575.6,37.8 551.7,65.3 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#6F8087" d="M61.7,80.7H49.5l-6.9-22.6H19.1l-6.6,22.6H0.5L24,4h14.2L61.7,80.7z M40.6,49.2l-6-20 c-1.4-5-2.7-10.4-3.8-15.1h-0.2c-1.1,4.7-2.3,10.3-3.6,15l-6.1,20.1H40.6z"/>
|
||||
<path fill="#6F8087" d="M82.3,33.9c3.8-6.2,9.8-9.6,17.5-9.6c12.4,0,21.8,11,21.8,28.2c0,20.3-11.7,29.4-23.4,29.4 c-6.6,0-12-3.1-14.7-7.6h-0.2v28.9H71.7V43.3c0-7.2-0.2-12.9-0.4-17.8h10.2l0.6,8.4H82.3z M83.3,58.4c0,9.9,6.8,14.1,12.5,14.1 c9,0,13.9-8.3,13.9-19.8c0-10.5-4.7-19.2-13.6-19.2c-6.9,0-12.8,6.5-12.8,14.6V58.4z"/>
|
||||
<path fill="#6F8087" d="M143.7,33.9c3.8-6.2,9.8-9.6,17.5-9.6c12.4,0,21.8,11,21.8,28.2c0,20.3-11.7,29.4-23.4,29.4 c-6.6,0-12-3.1-14.7-7.6h-0.2v28.9h-11.6V43.3c0-7.2-0.2-12.9-0.4-17.8h10.2l0.6,8.4H143.7z M144.7,58.4c0,9.9,6.8,14.1,12.5,14.1 c9,0,13.9-8.3,13.9-19.8c0-10.5-4.7-19.2-13.6-19.2c-6.9,0-12.8,6.5-12.8,14.6V58.4z"/>
|
||||
<g>
|
||||
<path fill="#404D5C" d="M230.9,15.9c-2.5-1.4-7.3-3.4-13.6-3.4c-9.1,0-12.9,5.3-12.9,10.2c0,6.5,4.3,9.7,14,13.9 c12.3,5.4,18.2,12.1,18.2,22.7c0,12.8-9.5,22.5-26.8,22.5c-7.2,0-14.8-2.1-18.5-4.6l2.6-9.7C198,70,204.3,72,210.6,72 c9.1,0,14.2-4.7,14.2-11.6c0-6.5-3.9-10.5-13-14.3c-11.4-4.6-19.1-11.5-19.1-22c0-12.1,9.7-21.3,25-21.3c7.3,0,12.8,1.8,16.1,3.5 L230.9,15.9z"/>
|
||||
<path fill="#404D5C" d="M254.3,17.7c-4,0-6.7-3-6.7-6.7c0-3.9,2.8-6.8,6.8-6.8c4,0,6.7,2.9,6.7,6.8 C261.1,14.7,258.5,17.7,254.3,17.7z M260.1,80.7h-11.6V25.4h11.6V80.7z"/>
|
||||
<path fill="#404D5C" d="M320.4,25.5c-0.3,3.9-0.5,8.5-0.5,15.9v31.5c0,11-0.9,31.4-27.1,31.4c-6.4,0-13.1-1.4-17.4-4l2.6-9 c3.4,2,8.7,3.9,14.9,3.9c9,0,15.4-5,15.4-17.5v-5.3h-0.2c-2.8,4.7-8.2,8.1-15.3,8.1c-13,0-22-11.5-22-27 c0-18.7,11.3-29.2,23.4-29.2c8.2,0,12.8,4.2,15.1,8.6h0.2l0.5-7.4H320.4z M308.2,46.8c0-7.3-4.8-13.3-11.9-13.3 c-8,0-13.6,7.6-13.6,19.4c0,10.8,4.9,18.5,13.5,18.5c6,0,12-4.6,12-13.8V46.8z"/>
|
||||
<path fill="#404D5C" d="M379.5,80.7h-11.6V48.3c0-7.8-2.6-14.4-10.5-14.4c-5.7,0-11.8,4.7-11.8,13.5v33.3h-11.6V41 c0-6.1-0.2-10.8-0.4-15.5h10.1l0.6,8.2h0.3c2.6-4.7,8.4-9.4,16.7-9.4c8.6,0,18.2,5.6,18.2,22.7V80.7z"/>
|
||||
<path fill="#404D5C" d="M432,67.6c0,4.7,0.1,9.5,0.8,13.1h-10.5l-0.8-6.4h-0.3c-3.1,4.3-8.5,7.5-15.4,7.5 c-10.3,0-16.2-7.6-16.2-16.1c0-13.7,11.9-20.6,30.9-20.6c0-4.1,0-12.3-11.1-12.3c-4.9,0-9.9,1.5-13.4,3.8l-2.4-7.7 c3.9-2.5,10.4-4.6,17.8-4.6c16.2,0,20.8,10.7,20.8,22.5V67.6z M420.7,52.9c-9.1,0-19.6,1.7-19.6,11.5c0,6.1,3.8,8.8,8.1,8.8 c6.3,0,11.5-4.8,11.5-11.2V52.9z"/>
|
||||
<path fill="#404D5C" d="M457.8,80.7h-11.1V7.4l11.1-6.3V80.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170.08 170.08"><defs><style>.cls-1{fill:#04246e;}.cls-2{fill:#fff;}</style></defs><title>square-mark-white</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><rect class="cls-1" width="170.08" height="170.08"/><path class="cls-2" d="M127.34,58c-8.4,0-14.41,7.4-20.6,15.65-3.84-17.51-8.11-35-21.7-35s-17.86,17.5-21.71,35C57.15,65.37,51.13,58,42.74,58c-5.35,0-14.39,3.63-14.39,17.25L28.41,100c0,13.36,7.14,16.64,11.42,17.76h0c7.39,1.91,25.17,3.69,45.18,3.69s37.8-1.78,45.18-3.69h0c4.28-1.12,11.42-4.4,11.42-17.76l.06-24.79c0-13.62-9-17.25-14.39-17.25M85,46.33c8.15,0,11.24,16.06,15.57,35.33C95.86,87.5,90.83,92.21,85,92.21S74.21,87.5,69.47,81.66c4-18,7.17-35.33,15.57-35.33M36.13,100l-.06-24.79c0-8.6,4.67-9.53,6.67-9.53,5.92,0,12.28,9.88,18.36,17.83-4.29,18-8.83,29-19.56,26.72-2.78-.77-5.41-2.53-5.41-10.23m21,12.53c4.75-5.16,7.78-13.21,10.08-21.73,5,5.19,10.69,9.13,17.8,9.13S97.86,96,102.83,90.81c2.31,8.52,5.33,16.57,10.07,21.73-8.29.75-18.28,1.21-27.86,1.21s-19.59-.46-27.88-1.21M134,100c0,7.7-2.63,9.46-5.42,10.23-10.73,2.27-15.26-8.72-19.56-26.72,6.08-7.94,12.45-17.83,18.37-17.83,2,0,6.67.93,6.67,9.53Z"/></g></g></svg>
|
Przed Szerokość: | Wysokość: | Rozmiar: 3.7 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
|
@ -3,7 +3,7 @@ import { isEqual } from "lodash";
|
|||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import styled from "styled-components";
|
||||
import tippy, { Instance } from "tippy.js";
|
||||
import tippy from "tippy.js";
|
||||
import {
|
||||
DEFAULT_NODE_COLOR,
|
||||
HOVERED_NODE_COLOR,
|
||||
|
@ -65,10 +65,10 @@ class Cytoscape extends React.PureComponent<CytoscapeProps> {
|
|||
trigger: "manual",
|
||||
});
|
||||
n.on("mouseover", () => {
|
||||
(t as Instance).show();
|
||||
(t as any).show();
|
||||
});
|
||||
n.on("mouseout", () => {
|
||||
(t as Instance).hide();
|
||||
(t as any).hide();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ interface NavState {
|
|||
const graphIsActive = (currMatch: match<InstanceDomainPath>, location: Location) =>
|
||||
location.pathname === "/" || location.pathname.startsWith("/instance/");
|
||||
|
||||
class Nav extends React.Component<{}, NavState> {
|
||||
class Nav extends React.Component<Record<string, never>, NavState> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { aboutIsOpen: false };
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Classes, Code, H1, H2, H3 } from "@blueprintjs/core";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
// import appsignalLogo from "../../assets/appsignal.svg";
|
||||
import * as gitlabLogo from "../../assets/gitlab.png";
|
||||
import * as nlnetLogo from "../../assets/nlnet.png";
|
||||
import appsignalLogo from "../../assets/appsignal.png";
|
||||
import gitlabLogo from "../../assets/gitlab.png";
|
||||
import nlnetLogo from "../../assets/nlnet.png";
|
||||
import { Page } from "../atoms";
|
||||
|
||||
const SponsorContainer = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
const Sponsor = styled.div`
|
||||
margin: 10px;
|
||||
margin: 10px 40px 10px 0;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
|
@ -30,6 +30,10 @@ const AboutScreen: React.FC = () => (
|
|||
<a href="https://mastodon.social/@fediversespace" target="_blank" rel="noopener noreferrer">
|
||||
Mastodon
|
||||
</a>
|
||||
, and read more about what I'm up to on my{" "}
|
||||
<a href="https://www.btao.org" target="_blank" rel="noopener noreferrer">
|
||||
website
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
|
@ -75,11 +79,11 @@ const AboutScreen: React.FC = () => (
|
|||
</a>
|
||||
</Sponsor>
|
||||
<br />
|
||||
{/* <Sponsor>
|
||||
<Sponsor>
|
||||
<a href="https://appsignal.com" target="_blank" rel="noopener noreferrer">
|
||||
<img src={appsignalLogo} alt="Appsignal logo" height={40} />
|
||||
<img src={appsignalLogo} alt="Appsignal logo" height={50} />
|
||||
</a>
|
||||
</Sponsor> */}
|
||||
</Sponsor>
|
||||
<Sponsor>
|
||||
<a href="https://gitlab.com" target="_blank" rel="noopener noreferrer">
|
||||
<img src={gitlabLogo} alt="GitLab logo" height={40} />
|
||||
|
|
|
@ -3,13 +3,15 @@ import { connect } from "react-redux";
|
|||
import { Dispatch } from "redux";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
|
||||
import { Route, Switch } from "react-router";
|
||||
import { InstanceScreen, SearchScreen } from ".";
|
||||
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
||||
import { loadInstance } from "../../redux/actions";
|
||||
import { AppState } from "../../redux/types";
|
||||
import { domainMatchSelector, isSmallScreen } from "../../util";
|
||||
import { Graph, SidebarContainer } from "../organisms";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import type { Location } from "history";
|
||||
|
||||
const GraphContainer = styled.div`
|
||||
display: flex;
|
||||
|
@ -24,7 +26,8 @@ const FullDiv = styled.div`
|
|||
right: 0;
|
||||
`;
|
||||
|
||||
interface GraphScreenProps extends RouteComponentProps {
|
||||
interface GraphScreenProps {
|
||||
location: Location;
|
||||
currentInstanceName: string | null;
|
||||
pathname: string;
|
||||
graphLoadError: boolean;
|
||||
|
@ -79,8 +82,12 @@ class GraphScreenImpl extends React.Component<GraphScreenProps, GraphScreenState
|
|||
{isSmallScreen || !this.state.hasBeenViewed || <Graph />}
|
||||
<SidebarContainer>
|
||||
<Switch>
|
||||
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
||||
<Route exact path="/" component={SearchScreen} />
|
||||
<Route path={INSTANCE_DOMAIN_PATH}>
|
||||
<InstanceScreen />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<SearchScreen />
|
||||
</Route>
|
||||
</Switch>
|
||||
</SidebarContainer>
|
||||
</GraphContainer>
|
||||
|
@ -106,4 +113,9 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|||
loadInstance: (domain: string | null) => dispatch(loadInstance(domain) as any),
|
||||
});
|
||||
const GraphScreen = connect(mapStateToProps, mapDispatchToProps)(GraphScreenImpl);
|
||||
export default withRouter(GraphScreen);
|
||||
const Component = (props: Omit<React.ComponentProps<typeof GraphScreen>, "location">) => {
|
||||
const location = useLocation();
|
||||
return <GraphScreen {...props} location={location} />;
|
||||
};
|
||||
Component.displayName = "GraphScreen";
|
||||
export default Component;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
H2,
|
||||
HTMLTable,
|
||||
Icon,
|
||||
IconSize,
|
||||
NonIdealState,
|
||||
Position,
|
||||
Spinner,
|
||||
|
@ -233,11 +234,11 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
</StyledTabs>
|
||||
<StyledLinkToFdNetwork>
|
||||
<AnchorButton
|
||||
href={`https://fediverse.network/${this.props.instanceName}`}
|
||||
href={`https://fedidb.org/network/instance?domain=${this.props.instanceName}`}
|
||||
minimal
|
||||
rightIcon={IconNames.SHARE}
|
||||
target="_blank"
|
||||
text="See more statistics at fediverse.network"
|
||||
text="See more statistics at fedidb.org"
|
||||
/>
|
||||
</StyledLinkToFdNetwork>
|
||||
</>
|
||||
|
@ -296,7 +297,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
statusesPerUserPerDay,
|
||||
} = this.props.instanceDetails;
|
||||
return (
|
||||
<StyledHTMLTable small striped>
|
||||
<StyledHTMLTable condensed striped>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
|
@ -329,7 +330,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
position={Position.TOP}
|
||||
className={Classes.DARK}
|
||||
>
|
||||
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
||||
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"}</td>
|
||||
|
@ -349,7 +350,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
position={Position.TOP}
|
||||
className={Classes.DARK}
|
||||
>
|
||||
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
||||
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>{(statusesPerDay && numeral.default(statusesPerDay).format("0.0")) || "Unknown"}</td>
|
||||
|
@ -369,7 +370,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
position={Position.TOP}
|
||||
className={Classes.DARK}
|
||||
>
|
||||
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
||||
<Icon icon={IconNames.HELP} iconSize={IconSize.STANDARD} />
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>{(statusesPerUserPerDay && numeral.default(statusesPerUserPerDay).format("0.000")) || "Unknown"}</td>
|
||||
|
@ -420,7 +421,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
|
||||
instance, and vice versa.
|
||||
</p>
|
||||
<StyledHTMLTable small striped interactive={false}>
|
||||
<StyledHTMLTable condensed striped interactive={false}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instance</th>
|
||||
|
@ -456,7 +457,7 @@ class InstanceScreenImpl extends React.PureComponent<InstanceScreenProps, Instan
|
|||
<p className={Classes.TEXT_MUTED}>
|
||||
All the instances, past and present, that {this.props.instanceName} knows about.
|
||||
</p>
|
||||
<StyledHTMLTable small striped interactive={false} className="fediverse-sidebar-table">
|
||||
<StyledHTMLTable condensed striped interactive={false} className="fediverse-sidebar-table">
|
||||
<tbody>{peerRows}</tbody>
|
||||
</StyledHTMLTable>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,7 @@ interface LoginScreenState {
|
|||
selectedLoginType?: "email" | "fediverseAccount";
|
||||
error: boolean;
|
||||
}
|
||||
class LoginScreen extends React.PureComponent<{}, LoginScreenState> {
|
||||
class LoginScreen extends React.PureComponent<Record<string, never>, LoginScreenState> {
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Callout, H1, InputGroup, Intent, NonIdealState, Spinner } from "@blueprintjs/core";
|
||||
import { Button, Callout, H1, InputGroup, Intent, NonIdealState, Spinner, SpinnerSize } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import { push } from "connected-react-router";
|
||||
import { get, isEqual } from "lodash";
|
||||
|
@ -90,7 +90,7 @@ class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenSt
|
|||
onMouseLeave={this.onMouseLeave}
|
||||
/>
|
||||
))}
|
||||
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
|
||||
{isLoadingResults && <StyledSpinner size={SpinnerSize.SMALL} />}
|
||||
{!isLoadingResults && hasMoreResults && (
|
||||
<Button onClick={this.search} minimal>
|
||||
Load more results
|
||||
|
@ -102,7 +102,7 @@ class SearchScreen extends React.PureComponent<SearchScreenProps, SearchScreenSt
|
|||
|
||||
let rightSearchBarElement;
|
||||
if (isLoadingResults) {
|
||||
rightSearchBarElement = <Spinner size={Spinner.SIZE_SMALL} />;
|
||||
rightSearchBarElement = <Spinner size={SpinnerSize.SMALL} />;
|
||||
} else if (query || error) {
|
||||
rightSearchBarElement = <Button minimal icon={IconNames.CROSS} onClick={this.clearQuery} aria-label="Search" />;
|
||||
} else {
|
||||
|
|
|
@ -20,6 +20,7 @@ export const QUALITATIVE_COLOR_SCHEME = [
|
|||
"#AD99FF",
|
||||
"#0E5A8A",
|
||||
"#0A6640",
|
||||
"#AAB42F",
|
||||
"#A66321",
|
||||
"#A82A2A",
|
||||
];
|
||||
|
@ -56,4 +57,5 @@ export const INSTANCE_TYPES = [
|
|||
"hubzilla",
|
||||
"plume",
|
||||
"wordpress",
|
||||
"smithereen",
|
||||
];
|
||||
|
|
|
@ -19,11 +19,6 @@ import { createBrowserHistory } from "history";
|
|||
import AppRouter from "./AppRouter";
|
||||
import createRootReducer from "./redux/reducers";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const axe = require("react-axe"); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
axe(React, ReactDOM, 1000);
|
||||
}
|
||||
|
||||
// https://blueprintjs.com/docs/#core/accessibility.focus-management
|
||||
FocusStyleManager.onlyShowFocusOnTabs();
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
// / <reference types="react-scripts" />
|
|
@ -85,38 +85,36 @@ export const loadInstance = (instanceName: string | null) => (dispatch: Dispatch
|
|||
.catch(() => dispatch(instanceLoadFailed()));
|
||||
};
|
||||
|
||||
export const updateSearch = (query: string, filters: SearchFilter[]) => (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
query = query.trim();
|
||||
export const updateSearch =
|
||||
(query: string, filters: SearchFilter[]) => (dispatch: Dispatch, getState: () => AppState) => {
|
||||
query = query.trim();
|
||||
|
||||
if (!query) {
|
||||
dispatch(resetSearch());
|
||||
return;
|
||||
}
|
||||
if (!query) {
|
||||
dispatch(resetSearch());
|
||||
return;
|
||||
}
|
||||
|
||||
const prevQuery = getState().search.query;
|
||||
const prevFilters = getState().search.filters;
|
||||
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
|
||||
const prevQuery = getState().search.query;
|
||||
const prevFilters = getState().search.filters;
|
||||
const isNewQuery = prevQuery !== query || !isEqual(prevFilters, filters);
|
||||
|
||||
const { next } = getState().search;
|
||||
let url = `search/?query=${query}`;
|
||||
if (!isNewQuery && next) {
|
||||
url += `&after=${next}`;
|
||||
}
|
||||
const { next } = getState().search;
|
||||
let url = `search/?query=${query}`;
|
||||
if (!isNewQuery && next) {
|
||||
url += `&after=${next}`;
|
||||
}
|
||||
|
||||
// Add filters
|
||||
// The format is e.g. type_eq=mastodon or user_count_gt=1000
|
||||
filters.forEach((filter) => {
|
||||
url += `&${filter.field}_${filter.relation}=${filter.value}`;
|
||||
});
|
||||
// Add filters
|
||||
// The format is e.g. type_eq=mastodon or user_count_gt=1000
|
||||
filters.forEach((filter) => {
|
||||
url += `&${filter.field}_${filter.relation}=${filter.value}`;
|
||||
});
|
||||
|
||||
dispatch(requestSearchResult(query, filters));
|
||||
return getFromApi(url)
|
||||
.then((result) => dispatch(receiveSearchResults(result)))
|
||||
.catch(() => dispatch(searchFailed()));
|
||||
};
|
||||
dispatch(requestSearchResult(query, filters));
|
||||
return getFromApi(url)
|
||||
.then((result) => dispatch(receiveSearchResults(result)))
|
||||
.catch(() => dispatch(searchFailed()));
|
||||
};
|
||||
|
||||
export const fetchGraph = () => (dispatch: Dispatch) => {
|
||||
dispatch(requestGraph());
|
||||
|
@ -125,22 +123,20 @@ export const fetchGraph = () => (dispatch: Dispatch) => {
|
|||
.catch(() => dispatch(graphLoadFailed()));
|
||||
};
|
||||
|
||||
export const loadInstanceList = (page?: number, sort?: InstanceSort) => (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
sort = sort || getState().data.instanceListSort;
|
||||
dispatch(requestInstanceList(sort));
|
||||
const params: string[] = [];
|
||||
if (page) {
|
||||
params.push(`page=${page}`);
|
||||
}
|
||||
if (sort) {
|
||||
params.push(`sortField=${sort.field}`);
|
||||
params.push(`sortDirection=${sort.direction}`);
|
||||
}
|
||||
const path = params ? `instances?${params.join("&")}` : "instances";
|
||||
return getFromApi(path)
|
||||
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
|
||||
.catch(() => dispatch(instanceListLoadFailed()));
|
||||
};
|
||||
export const loadInstanceList =
|
||||
(page?: number, sort?: InstanceSort) => (dispatch: Dispatch, getState: () => AppState) => {
|
||||
sort = sort || getState().data.instanceListSort;
|
||||
dispatch(requestInstanceList(sort));
|
||||
const params: string[] = [];
|
||||
if (page) {
|
||||
params.push(`page=${page}`);
|
||||
}
|
||||
if (sort) {
|
||||
params.push(`sortField=${sort.field}`);
|
||||
params.push(`sortDirection=${sort.direction}`);
|
||||
}
|
||||
const path = params ? `instances?${params.join("&")}` : "instances";
|
||||
return getFromApi(path)
|
||||
.then((instancesListResponse) => dispatch(receiveInstanceList(instancesListResponse)))
|
||||
.catch(() => dispatch(instanceListLoadFailed()));
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ export interface SearchFilter {
|
|||
type SearchFilterField = "type" | "user_count";
|
||||
const searchFilterFieldTranslations = {
|
||||
type: "Instance type",
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
user_count: "User count",
|
||||
};
|
||||
const searchFilterRelationTranslations = {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
declare module "cytoscape-popper" {
|
||||
const prototype: {};
|
||||
const prototype: unknown;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import { DESKTOP_WIDTH_THRESHOLD, InstanceDomainPath, INSTANCE_DOMAIN_PATH } fro
|
|||
import { AppState } from "./redux/types";
|
||||
|
||||
let API_ROOT = "http://localhost:4000/api/";
|
||||
if (["true", true, 1, "1"].includes(process.env.REACT_APP_STAGING || "")) {
|
||||
if (["true", true, 1, "1"].includes(import.meta.env.VITE_STAGING || "")) {
|
||||
API_ROOT = "https://phoenix.api-develop.fediverse.space/api/";
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
} else if (import.meta.env.PROD) {
|
||||
API_ROOT = "https://phoenix.api.fediverse.space/api/";
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"types": ["vite/client"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr()],
|
||||
});
|
11810
frontend/yarn.lock
11810
frontend/yarn.lock
Plik diff jest za duży
Load Diff
29
netlify.toml
29
netlify.toml
|
@ -1,29 +0,0 @@
|
|||
[build]
|
||||
base = "frontend/"
|
||||
publish = "frontend/build/"
|
||||
|
||||
[build.environment]
|
||||
INLINE_RUNTIME_CHUNK = "false"
|
||||
|
||||
[context.develop.environment]
|
||||
REACT_APP_STAGING = "true"
|
||||
|
||||
[context.branch-deploy.environment]
|
||||
REACT_APP_STAGING = "true"
|
||||
|
||||
[context.deploy-preview.environment]
|
||||
REACT_APP_STAGING = "true"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Content-Type-Options = "nosniff"
|
||||
X-Frame-Options = "DENY"
|
||||
X-XSS-Protection = "1"
|
||||
Content-Security-Policy = "default-src 'self' https://*.fediverse.space; style-src 'self' 'unsafe-inline'"
|
||||
|
Ładowanie…
Reference in New Issue