diff --git a/README.md b/README.md
index 9ffddc0..d061689 100644
--- a/README.md
+++ b/README.md
@@ -6,25 +6,7 @@
-## FAQ
-
-### Why did we create Freedium?
-
-In mid-June to mid-July 2023, Medium changed their paywall method, and all old paywall bypass methods we had stopped working. So I became obsessed with the idea of creating a service to bypass Medium's paywalled posts. Honestly I am not a big fan of Medium, but I sometimes read articles to improve my knowledge.
-
-### How does Freedium work?
-
-In the first version of Freedium, we reverse-engineered Medium.com's GraphQL endpoints and built our own parser and toolkits to show you unpaywalled Medium posts. Unfortunately, Medium closed this loophole and nowadays we just pay subscriptions and share access through Freedium. Sometimes we got a bugs because of the self-written parser, but we are working to make Freedium bug-free.
-
-### Wow! I would like to contribute to Freedium. How can I do that?
-
-We need volunteers who have Medium subscriptions because we might get banned by Medium. And if you developer you can start from the this (https://codeberg.org/Freedium-cfd/web) repository.
-
-### Plans, future?
-
-Speed up Freedium, add support for more services than just Medium and (probably) create open source Medium frontend (in next life)
-
-## Technologies:
+## Stack:
- Backend: Python 3.9+, Unicorn, FastAPI, Jinja2, Sentry
- Frontend: Tailwinds CSS v3
@@ -33,6 +15,13 @@ Speed up Freedium, add support for more services than just Medium and (probably)
## Local run:
+There is two profiles:
+
+- `min` - without 2 Cluster of Cloudflare WARP proxy, HAProxy proxy balancer, Plausible, Grafana.
+- `prod` - with all services for production.
+
+For local development, we recommend to use `min` profile.
+
Requirements:
- Docker
@@ -44,8 +33,8 @@ To configure your Freedium instance, follow these steps:
1. Clone the repository:
```
- git clone https://codeberg.org/Freedium-cfd/web/ ./web --depth 1
- cd ./web
+ git clone https://codeberg.org/Freedium-cfd/web/ ./freedium-web --depth 1
+ cd ./freedium-web
```
2. Create and configure the environment file:
@@ -56,24 +45,135 @@ To configure your Freedium instance, follow these steps:
Open the `.env` file and adjust the values as needed for your setup.
-3. Set up the Docker network:
+3. (Optional) Set up the Docker network:
```
- sudo docker network create caddy_net
+ sudo docker network create caddy_freedium_net
```
-4. Start the Freedium services:
+4. Change your hosts file:
+
```
- sudo docker compose -f ./docker-compose/docker-compose.yml up
+ sudo nano /etc/hosts
```
-And now you can access local instance of Freedium by opening browser and type `http://localhost:6752`.
+ Add the following line:
-These steps will set up and run your local Freedium instance.
+ ```
+ 127.0.0.1 freedium.local
+ ```
+
+5. Start the Freedium services (`min` profile):
+
+ ```
+ sudo docker compose --profile min -f ./docker-compose/docker-compose.yml up
+ ```
+
+ Stopping the services:
+
+ ```
+ sudo docker compose --profile min -f ./docker-compose/docker-compose.yml down
+ ```
+
+6. (Optional) Configure your reverse proxy (Caddy, Nginx, etc.) to use `freedium.local` as a host.
+
+If you use Dockerized reverse proxy, you can specify network `caddy_freedium_net` with `external: true` option in networks section of your reverse proxy container. Specify `caddy_freedium` hostname with port `80` (or `443`) in your reverse proxy configuration.
+
+And now you can access local instance of Freedium by opening browser and type `https://freedium.local`. There is would be a warning about insecure connection, because we use self-signed TLS certificate. Ignore it.
+
+## Architecture:
+
+```mermaid
+graph TB
+ subgraph "Main Application"
+ fw[freedium_web]
+ cdy[caddy_freedium]
+ end
+
+ subgraph "Database Layer"
+ rds[redis_service
DragonFlyDB]
+ pg[(postgres_freedium
PostgreSQL)]
+ pgadm[pgadmin4_freedium]
+ end
+
+ subgraph "Proxy Layer"
+ hpb[haproxy-proxy-balancer]
+
+ subgraph "WGCF Cluster 1"
+ wg1[wgcf1]
+ d1[dante_1]
+ wh1[wgcf1_healthcare_service]
+ end
+
+ subgraph "WGCF Cluster 2"
+ wg2[wgcf2]
+ d2[dante_2]
+ wh2[wgcf2_healthcare_service]
+ end
+ end
+
+ subgraph "Analytics"
+ pls[freedium_plausible]
+ pldb[(plausible_db)]
+ pledb[(plausible_events_db
ClickHouse)]
+ end
+
+ subgraph "Utility"
+ ah[autoheal]
+ end
+
+ %% Dependencies
+ fw -->|depends_on| hpb
+ d1 -->|depends_on| wg1
+ d2 -->|depends_on| wg2
+ wg2 -->|depends_on| wg1
+ hpb -->|depends_on| wg1
+ hpb -->|depends_on| wg2
+ pls -->|depends_on| pldb
+ pls -->|depends_on| pledb
+
+ %% Network Connections
+ subgraph "Networks"
+ fn[freedium_net]
+ cn[caddy_net]
+ pn[plausible_net]
+ end
+
+ fw ---|freedium_net| fn
+ cdy ---|freedium_net & caddy_net| fn
+ rds ---|freedium_net| fn
+ pg ---|freedium_net| fn
+ pgadm ---|freedium_net| fn
+ hpb ---|freedium_net| fn
+ wg1 ---|freedium_net| fn
+ wg2 ---|freedium_net| fn
+ wh1 ---|freedium_net| fn
+ wh2 ---|freedium_net| fn
+ pls ---|all networks| fn
+
+ %% Port Exposures
+ cdy -->|":6752"| ext1[External Access]
+ fw -->|":7080"| ext2[External Access]
+ pgadm -->|":5433"| ext3[External Access]
+
+ classDef service fill:#2ecc71,stroke:#27ae60,color:white
+ classDef network fill:#3498db,stroke:#2980b9,color:white
+ classDef database fill:#e74c3c,stroke:#c0392b,color:white
+ classDef proxy fill:#f1c40f,stroke:#f39c12,color:black
+ classDef utility fill:#9b59b6,stroke:#8e44ad,color:white
+ classDef external fill:#95a5a6,stroke:#7f8c8d,color:white
+
+ class fw,cdy,pls service
+ class rds,pg,pldb,pledb database
+ class hpb,wg1,wg2,d1,d2 proxy
+ class ah,wh1,wh2 utility
+ class fn,cn,pn network
+ class ext1,ext2,ext3 external
+```
## TODO:
-- Integrate library notifiers - https://github.com/liiight/notifiers
+- ~~Integrate library notifiers - https://github.com/liiight/notifiers~~ Use Graphana and Loki instead
- Do not use 'shturman/dante' image, because it is does not have updates for a long time. (Probably) Use https://hub.docker.com/r/vimagick/dante/
## Roadmap
diff --git a/caddy/CaddyfileDev b/caddy/Caddyfile
similarity index 88%
rename from caddy/CaddyfileDev
rename to caddy/Caddyfile
index afaee57..6a5a437 100644
--- a/caddy/CaddyfileDev
+++ b/caddy/Caddyfile
@@ -1,9 +1,9 @@
-:6752 {
- # header Server "nginx"
+(common) {
+ tls internal
encode gzip
header -Server
-
+
handle_path /android-chrome-192x192.png {
root * /static/android-chrome-192x192.png
file_server
@@ -200,11 +200,34 @@
respond "Access denied" 403
}
+}
- route /* {
- reverse_proxy freedium_web:7080 {
- lb_try_duration 30s
- lb_try_interval 1s
- }
+(header_up) {
+ header_up Host {host}
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+}
+
+(lb_try) {
+ lb_try_duration 30s
+ lb_try_interval 1s
+}
+
+plausible.freedium.local {
+ import common
+
+ reverse_proxy freedium_plausible:8000 {
+ import header_up
+ import lb_try
+ }
+}
+
+freedium.local {
+ import common
+
+ reverse_proxy freedium_web:7080 {
+ import header_up
+ import lb_try
}
}
\ No newline at end of file
diff --git a/caddy/CaddyfileDevTemplate b/caddy/CaddyfileDevTemplate
deleted file mode 100644
index 4f78960..0000000
--- a/caddy/CaddyfileDevTemplate
+++ /dev/null
@@ -1,14 +0,0 @@
-:6752 {
- # header Server "nginx"
- encode gzip
- header -Server
-
-{{ template }}
-
- route /* {
- reverse_proxy freedium_web:7080 {
- lb_try_duration 30s
- lb_try_interval 1s
- }
- }
-}
diff --git a/caddy/CaddyfileProd b/caddy/CaddyfileProd
deleted file mode 100644
index e0aa1b0..0000000
--- a/caddy/CaddyfileProd
+++ /dev/null
@@ -1,211 +0,0 @@
-# https://futurestud.io/tutorials/caddy-reverse-proxy-a-node-js-app
-freedium.cfd {
- # header Server "nginx"
- encode gzip
- header -Server
-
-
- handle_path /android-chrome-192x192.png {
- root * /static/android-chrome-192x192.png
- file_server
- }
-
-
- handle_path /android-chrome-512x512.png {
- root * /static/android-chrome-512x512.png
- file_server
- }
-
-
- handle_path /apple-touch-icon.png {
- root * /static/apple-touch-icon.png
- file_server
- }
-
-
- handle_path /browserconfig.xml {
- root * /static/browserconfig.xml
- file_server
- }
-
-
- handle_path /favicon-16x16.png {
- root * /static/favicon-16x16.png
- file_server
- }
-
-
- handle_path /favicon-32x32.png {
- root * /static/favicon-32x32.png
- file_server
- }
-
-
- handle_path /favicon.ico {
- root * /static/favicon.ico
- file_server
- }
-
-
- handle_path /humans.txt {
- root * /static/humans.txt
- file_server
- }
-
-
- handle_path /mstile-144x144.png {
- root * /static/mstile-144x144.png
- file_server
- }
-
-
- handle_path /mstile-150x150.png {
- root * /static/mstile-150x150.png
- file_server
- }
-
-
- handle_path /mstile-310x150.png {
- root * /static/mstile-310x150.png
- file_server
- }
-
-
- handle_path /mstile-310x310.png {
- root * /static/mstile-310x310.png
- file_server
- }
-
-
- handle_path /mstile-70x70.png {
- root * /static/mstile-70x70.png
- file_server
- }
-
-
- handle_path /robots.txt {
- root * /static/robots.txt
- file_server
- }
-
-
- handle_path /safari-pinned-tab.svg {
- root * /static/safari-pinned-tab.svg
- file_server
- }
-
-
- handle_path /security.txt {
- root * /static/security.txt
- file_server
- }
-
-
- handle_path /site.webmanifest {
- root * /static/site.webmanifest
- file_server
- }
-
-
- handle_path /websocket {
- respond "Access denied" 403
- }
-
-
- handle_path /meta.json {
- respond "Access denied" 403
- }
-
-
- handle_path /cdn-cgi/challenge-platform/scripts/jsd/main.js {
- respond "Access denied" 403
- }
-
-
- handle_path /cdn-cgi/rum {
- respond "Access denied" 403
- }
-
-
- handle_path /graphql/websocket {
- respond "Access denied" 403
- }
-
-
- handle_path /onboarding/* {
- respond "Access denied" 403
- }
-
-
- handle_path /wp-* {
- respond "Access denied" 403
- }
-
-
- handle_path /.env {
- respond "Access denied" 403
- }
-
-
- handle_path /api* {
- respond "Access denied" 403
- }
-
-
- handle_path /apple-touch-icon-precomposed.png {
- respond "Access denied" 403
- }
-
-
- handle_path /rss.xml {
- respond "Access denied" 403
- }
-
-
- handle_path /.git/* {
- respond "Access denied" 403
- }
-
-
- handle_path /apple-touch-icon-120x120.png {
- respond "Access denied" 403
- }
-
-
- handle_path /apple-touch-icon-120x120-precomposed.png {
- respond "Access denied" 403
- }
-
-
- handle_path /apple-touch-icon-152x152.png {
- respond "Access denied" 403
- }
-
-
- handle_path /apple-touch-icon-152x152-precomposed.png {
- respond "Access denied" 403
- }
-
-
- handle_path /.well-known/* {
- respond "Access denied" 403
- }
-
-
- handle_path /cdn-cgi/challenge-platform/h/b/orchestrate/chl_page/v1 {
- respond "Access denied" 403
- }
-
-
- handle_path /cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1 {
- respond "Access denied" 403
- }
-
-
- route /* {
- reverse_proxy freedium_web:7080 {
- lb_try_duration 30s
- lb_try_interval 1s
- }
- }
-}
\ No newline at end of file
diff --git a/caddy/CaddyfileProdTemplate b/caddy/CaddyfileProdTemplate
deleted file mode 100644
index 681ff9a..0000000
--- a/caddy/CaddyfileProdTemplate
+++ /dev/null
@@ -1,15 +0,0 @@
-# https://futurestud.io/tutorials/caddy-reverse-proxy-a-node-js-app
-freedium.cfd {
- # header Server "nginx"
- encode gzip
- header -Server
-
- {{ template }}
-
- route /* {
- reverse_proxy freedium_web:7080 {
- lb_try_duration 30s
- lb_try_interval 1s
- }
- }
-}
diff --git a/caddy/CaddyfileTemplate b/caddy/CaddyfileTemplate
new file mode 100644
index 0000000..592b14b
--- /dev/null
+++ b/caddy/CaddyfileTemplate
@@ -0,0 +1,37 @@
+(common) {
+ tls internal
+ encode gzip
+ header -Server
+
+ {{ template }}
+}
+
+(header_up) {
+ header_up Host {host}
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+}
+
+(lb_try) {
+ lb_try_duration 30s
+ lb_try_interval 1s
+}
+
+plausible.freedium.local {
+ import common
+
+ reverse_proxy freedium_plausible:8000 {
+ import header_up
+ import lb_try
+ }
+}
+
+freedium.local {
+ import common
+
+ reverse_proxy freedium_web:7080 {
+ import header_up
+ import lb_try
+ }
+}
diff --git a/caddy/generate_caddy_file.py b/caddy/generate_caddy_file.py
old mode 100644
new mode 100755
index 5788720..91cfe9c
--- a/caddy/generate_caddy_file.py
+++ b/caddy/generate_caddy_file.py
@@ -1,6 +1,9 @@
+#!/bin/env python3
+
from os import listdir
from os.path import isfile, join
-from typing import List, Dict
+from typing import Dict, List
+
from jinja2 import Template
# Constants
@@ -29,8 +32,7 @@ ACCESS_DENIED_PATHS: List[str] = [
]
CADDY_FILE_TEMPLATES: Dict[str, str] = {
- "CaddyfileDevTemplate": "CaddyfileDev",
- "CaddyfileProdTemplate": "CaddyfileProd",
+ "CaddyfileTemplate": "Caddyfile",
}
diff --git a/docker-compose/docker-compose.main.yml b/docker-compose/docker-compose.main.yml
index c5cad55..077020e 100644
--- a/docker-compose/docker-compose.main.yml
+++ b/docker-compose/docker-compose.main.yml
@@ -8,19 +8,23 @@ services:
dockerfile: Dockerfile
cap_add:
- NET_ADMIN
+ networks:
+ freedium_local_net:
+ ipv4_address: 177.28.0.5
+ freedium_net:
+ caddy_freedium_net: # caddy_net:
ports:
- - "6752:6752"
+ - "80:6752"
+ # - "80:80"
+ # - "443:443"
volumes:
- - ../caddy/CaddyfileDev:/etc/caddy/Caddyfile
+ - ../caddy/Caddyfile:/etc/caddy/Caddyfile
- freedium_caddy_data:/data
- freedium_caddy_config:/config
- ../caddy/static:/static
- networks:
- - freedium_net
- - caddy_net
restart: always
healthcheck:
- test: [ "CMD-SHELL", "curl -f http://localhost:6752/ --max-time 80 --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15'" ]
+ test: [ "CMD-SHELL", "curl -f http://localhost:80/ --max-time 80 --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15'" ]
interval: 30s
start_period: 20s
timeout: 80s
@@ -62,6 +66,14 @@ volumes:
networks:
- caddy_net:
- external: true
+ # caddy_net:
+ # external: true
freedium_net:
+ caddy_freedium_net:
+ freedium_local_net:
+ driver: bridge
+ ipam:
+ driver: default
+ config:
+ - subnet: 177.28.0.0/16
+ gateway: 177.28.0.1
diff --git a/docker-compose/docker-compose.plausible.yml b/docker-compose/docker-compose.plausible.yml
new file mode 100644
index 0000000..5c02283
--- /dev/null
+++ b/docker-compose/docker-compose.plausible.yml
@@ -0,0 +1,110 @@
+version: '3.7'
+
+# Based on: https://github.com/plausible/community-edition/blob/v2.1.4/compose.yml
+services:
+ plausible_db:
+ image: postgres:16.3-alpine3.20
+ profiles: [ prod ]
+ restart: always
+ networks:
+ - plausible_net
+ volumes:
+ - plausible-db-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_PASSWORD=postgres
+ healthcheck:
+ test: [ "CMD-SHELL", "pg_isready -U postgres" ]
+ start_period: 1m
+
+ plausible_events_db:
+ image: clickhouse/clickhouse-server:24.3.3.102-alpine
+ restart: always
+ profiles: [ prod ]
+ networks:
+ - plausible_net
+ volumes:
+ - plausible-event-data:/var/lib/clickhouse
+ - plausible-event-logs:/var/log/clickhouse-server
+ - ../plausible/clickhouse/logs.xml:/etc/clickhouse-server/config.d/logs.xml:ro
+ - ../plausible/clickhouse/docker.xml:/etc/clickhouse-server/config.d/docker.xml:ro
+ # This makes ClickHouse bind to IPv4 only, since Docker doesn't enable IPv6 in bridge networks by default.
+ # Fixes "Listen [::]:9000 failed: Address family for hostname not supported" warnings.
+ - ../plausible/clickhouse/ipv4-only.xml:/etc/clickhouse-server/config.d/ipv4-only.xml:ro
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
+ healthcheck:
+ test: [ "CMD-SHELL", "wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1" ]
+ start_period: 1m
+
+ freedium_plausible:
+ image: ghcr.io/plausible/community-edition:v2.1.4
+ restart: always
+ profiles: [ prod ]
+ command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
+ networks:
+ - plausible_net
+ - caddy_freedium_net
+ - freedium_net
+ depends_on:
+ plausible_db:
+ condition: service_healthy
+ plausible_events_db:
+ condition: service_healthy
+ volumes:
+ - plausible-data:/var/lib/plausible
+ ulimits:
+ nofile:
+ soft: 65535
+ hard: 65535
+ environment:
+ - TMPDIR=/var/lib/plausible/tmp
+ # required: https://github.com/plausible/community-edition/wiki/configuration#required
+ - BASE_URL=${PLAUSIBLE_BASE_URL:-https://plausible.freedium.local}
+ - SECRET_KEY_BASE=${PLAUSIBLE_SECRET_KEY_BASE}
+ - DISABLE_REGISTRATION=${PLAUSIBLE_DISABLE_REGISTRATION}
+ # - DISABLE_REGISTRATION
+ # optional: https://github.com/plausible/community-edition/wiki/configuration#optional
+ # registration: https://github.com/plausible/community-edition/wiki/configuration#registration
+ # - TOTP_VAULT_KEY=${PLAUSIBLE_TOTP_VAULT_KEY}
+ # - ENABLE_EMAIL_VERIFICATION=${PLAUSIBLE_ENABLE_EMAIL_VERIFICATION}
+ # web: https://github.com/plausible/community-edition/wiki/configuration#web
+ - HTTP_PORT=${PLAUSIBLE_HTTP_PORT:-8000} # Default: 8000
+ # - HTTPS_PORT
+ # databases: https://github.com/plausible/community-edition/wiki/configuration#database
+ - DATABASE_URL=postgres://postgres:postgres@plausible_db:5432/plausible_db
+ - CLICKHOUSE_DATABASE_URL=http://plausible_events_db:8123/plausible_events_db
+ # Google: https://github.com/plausible/community-edition/wiki/configuration#google
+ # - GOOGLE_CLIENT_ID
+ # - GOOGLE_CLIENT_SECRET
+ # geolocation: https://github.com/plausible/community-edition/wiki/configuration#ip-geolocation
+ # - IP_GEOLOCATION_DB
+ # - GEONAMES_SOURCE_FILE
+ # - MAXMIND_LICENSE_KEY
+ # - MAXMIND_EDITION
+ # email: https://github.com/plausible/community-edition/wiki/configuration#email
+ # - MAILER_ADAPTER
+ # - MAILER_EMAIL
+ # - MAILER_NAME
+ # - SMTP_HOST_ADDR
+ # - SMTP_HOST_PORT
+ # - SMTP_USER_NAME
+ # - SMTP_USER_PWD
+ # - SMTP_HOST_SSL_ENABLED
+ # - POSTMARK_API_KEY
+ # - MAILGUN_API_KEY
+ # - MAILGUN_DOMAIN
+ # - MAILGUN_BASE_URI
+ # - MANDRILL_API_KEY
+ # - SENDGRID_API_KEY
+
+volumes:
+ plausible-db-data:
+ plausible-event-data:
+ plausible-event-logs:
+ plausible-data:
+
+
+networks:
+ plausible_net:
diff --git a/docker-compose/docker-compose.proxy.yml b/docker-compose/docker-compose.proxy.yml
index 9657e78..3d3f020 100644
--- a/docker-compose/docker-compose.proxy.yml
+++ b/docker-compose/docker-compose.proxy.yml
@@ -7,8 +7,6 @@ services:
dockerfile: Dockerfile
container_name: haproxy-proxy-balancer
hostname: haproxy-pb
- # ports:
- # - "1080:1080"
volumes:
- ../proxy-balancer/haproxy:/usr/local/etc/haproxy/
restart: always
diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml
index 3b64906..f44825f 100644
--- a/docker-compose/docker-compose.yml
+++ b/docker-compose/docker-compose.yml
@@ -6,3 +6,4 @@ include:
- docker-compose.db.yml
- docker-compose.utility.yml
- docker-compose.proxy.yml
+ - docker-compose.plausible.yml
diff --git a/plausible/clickhouse/docker.xml b/plausible/clickhouse/docker.xml
new file mode 100644
index 0000000..a9f3181
--- /dev/null
+++ b/plausible/clickhouse/docker.xml
@@ -0,0 +1,3 @@
+
+ 0
+
\ No newline at end of file
diff --git a/plausible/clickhouse/ipv4-only.xml b/plausible/clickhouse/ipv4-only.xml
new file mode 100644
index 0000000..e4f4b22
--- /dev/null
+++ b/plausible/clickhouse/ipv4-only.xml
@@ -0,0 +1,3 @@
+
+ 0.0.0.0
+
\ No newline at end of file
diff --git a/plausible/clickhouse/logs.xml b/plausible/clickhouse/logs.xml
new file mode 100644
index 0000000..d1c0a62
--- /dev/null
+++ b/plausible/clickhouse/logs.xml
@@ -0,0 +1,36 @@
+
+
+ warning
+ true
+
+
+
+ system
+
+ 7500
+
+ ENGINE = MergeTree
+ PARTITION BY event_date
+ ORDER BY (event_time)
+ TTL event_date + interval 30 day
+ SETTINGS ttl_only_drop_parts=1
+
+
+
+
+ event_date
+ event_date + INTERVAL 30 DAY DELETE
+
+
+
+ event_date
+ event_date + INTERVAL 30 DAY DELETE
+
+
+
+
+
+
+
+
+
\ No newline at end of file