mirror of https://github.com/tildeclub/fosspay.git
Source incoming :)
This commit is contained in:
parent
415f50ce1e
commit
907bf2206f
|
@ -0,0 +1,14 @@
|
||||||
|
*.pyc
|
||||||
|
bin/
|
||||||
|
config.ini
|
||||||
|
alembic.ini
|
||||||
|
include/
|
||||||
|
local/
|
||||||
|
lib/
|
||||||
|
static/
|
||||||
|
*.swp
|
||||||
|
*.rdb
|
||||||
|
storage/
|
||||||
|
pip-selfcheck.json
|
||||||
|
.sass-cache/
|
||||||
|
overrides/
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2015 Drew DeVault
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Builds static assets
|
||||||
|
# Depends on:
|
||||||
|
# - scss
|
||||||
|
# - inotify-tools
|
||||||
|
# Run `make` to compile static assets
|
||||||
|
# Run `make watch` to recompile whenever a change is made
|
||||||
|
|
||||||
|
.PHONY: all static watch clean
|
||||||
|
|
||||||
|
STYLES:=$(patsubst styles/%.scss,static/%.css,$(wildcard styles/*.scss))
|
||||||
|
STYLES+=$(patsubst styles/%.css,static/%.css,$(wildcard styles/*.css))
|
||||||
|
SCRIPTS:=$(patsubst scripts/%.js,static/%.js,$(wildcard scripts/*.js))
|
||||||
|
_STATIC:=$(patsubst _static/%,static/%,$(wildcard _static/*))
|
||||||
|
|
||||||
|
static/%: _static/%
|
||||||
|
@mkdir -p static/
|
||||||
|
cp -r $< $@
|
||||||
|
|
||||||
|
static/%.css: styles/%.css
|
||||||
|
@mkdir -p static/
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
static/%.css: styles/%.scss
|
||||||
|
@mkdir -p static/
|
||||||
|
scss -I styles/ $< $@
|
||||||
|
|
||||||
|
static/%.js: scripts/%.js
|
||||||
|
@mkdir -p static/
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
static: $(STYLES) $(SCRIPTS) $(_STATIC)
|
||||||
|
|
||||||
|
all: static
|
||||||
|
echo $(STYLES)
|
||||||
|
echo $(SCRIPTS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf static
|
||||||
|
|
||||||
|
watch:
|
||||||
|
while inotifywait \
|
||||||
|
-e close_write scripts/ \
|
||||||
|
-e close_write styles/ \
|
||||||
|
-e close_write _static/; \
|
||||||
|
do make; done
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 593 B |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from fosspay.app import app
|
||||||
|
from fosspay.config import _cfg, _cfgi, load_config
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
app.static_folder = os.path.join(os.getcwd(), "static")
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
|
signal.signal(signal.SIGHUP, lambda *args: load_config())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host=_cfg("debug-host"), port=_cfgi('debug-port'), debug=True)
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
[dev]
|
||||||
|
# Change this to the actual location of your site
|
||||||
|
# You can include a path in the domain if it's a subdirectory
|
||||||
|
# i.e. domain=drewdevault.com/donate
|
||||||
|
# omit the trailing slash
|
||||||
|
protocol=http
|
||||||
|
domain=localhost:5000
|
||||||
|
# Change this value to something random and secret
|
||||||
|
secret-key=hello world
|
||||||
|
|
||||||
|
# On the debug server, this lets you choose what to bind to
|
||||||
|
debug-host=0.0.0.0
|
||||||
|
debug-port=5000
|
||||||
|
|
||||||
|
# Fill out these details with your mail server
|
||||||
|
smtp-host=mail.example.org
|
||||||
|
smtp-port=587
|
||||||
|
smtp-user=you
|
||||||
|
smtp-password=password
|
||||||
|
smtp-from=donate@example.org
|
||||||
|
|
||||||
|
# Your information
|
||||||
|
your-name=Joe Bloe
|
||||||
|
your-email=joe@example.org
|
||||||
|
# ^ you should have a gravatar that works with this email
|
||||||
|
|
||||||
|
# SQL connection string
|
||||||
|
connection-string=postgresql://postgres@localhost/fosspay
|
||||||
|
|
||||||
|
# Stripe API info: https://dashboard.stripe.com/account/apikeys
|
||||||
|
stripe-secret=
|
||||||
|
stripe-publish=
|
||||||
|
|
||||||
|
# Currency to use
|
||||||
|
# "usd" for dollar, "eur" for euro
|
||||||
|
# refer to stripe documentation for details : https://stripe.com/docs/currencies
|
||||||
|
currency=usd
|
||||||
|
|
||||||
|
# Separate with spaces
|
||||||
|
default-amounts=3 5 10 20
|
||||||
|
# Which one to pick when they arrive?
|
||||||
|
default-amount=5
|
||||||
|
|
||||||
|
# Pick between "monthly" and "once"
|
||||||
|
default-type=monthly
|
||||||
|
|
||||||
|
# Display monthly donations publicly
|
||||||
|
public-income=yes
|
||||||
|
|
||||||
|
# How much are you hoping to earn monthly, in cents
|
||||||
|
goal=500
|
||||||
|
|
||||||
|
# Optional Patreon integration
|
||||||
|
# Register a client here: https://www.patreon.com/portal/registration/register-clients
|
||||||
|
# And put in the "Creator's Access Token" here:
|
||||||
|
patreon-access-token=
|
||||||
|
# And the "Creator's Refresh Token" here:
|
||||||
|
patreon-refresh-token=
|
||||||
|
# Client ID
|
||||||
|
patreon-client-id=
|
||||||
|
# Client secret
|
||||||
|
patreon-client-secret=
|
||||||
|
# And the Patreon campaign you want to connect with:
|
||||||
|
patreon-campaign=
|
||||||
|
|
||||||
|
# Optional LiberaPay integration, fill in your username here
|
||||||
|
liberapay-campaign=
|
||||||
|
|
||||||
|
# Optional GitHub sponsors integration
|
||||||
|
# Generate personal access key at https://github.com/settings/tokens
|
||||||
|
# Must have "user" access.
|
||||||
|
github-token=
|
||||||
|
|
||||||
|
# Command to reload fosspay (send it a SIGHUP)
|
||||||
|
reload-command=kill -HUP $(pgrep -xf '/usr/bin/python3.*app.py' | tail -n 1)
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=fosspay website
|
||||||
|
Wants=network.target
|
||||||
|
Wants=postgresql.target
|
||||||
|
Before=network.target
|
||||||
|
Before=postgresql.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/home/sircmpwn/fosspay/
|
||||||
|
ExecStart=/usr/local/bin/gunicorn app:app -b 127.0.0.1:5000
|
||||||
|
ExecStop=/usr/bin/pkill gunicorn
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,10 @@
|
||||||
|
Une tentative de prélèvement de votre carte bancaire d'un montant de {{amount}}€ vient d'échouer.
|
||||||
|
|
||||||
|
Le don récurrent a donc été annulé. Si vous voulez encore nous donner, vous pouvez le faire à nouveau sur notre page dédiée :
|
||||||
|
{{root}}
|
||||||
|
|
||||||
|
Merci !
|
||||||
|
|
||||||
|
--
|
||||||
|
{{your_name}}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Quelqu'un (probablement vous), a tenté de réinitialiser votre mot de passe.
|
||||||
|
|
||||||
|
Pour valider cette demande, cliquez sur ce lien :
|
||||||
|
{{root}}/password-reset/{{user.password_reset}}
|
||||||
|
|
||||||
|
Il expirera après 24 heures.
|
||||||
|
Si vous n'avez pas demandé à changer votre mot de passe, ignorez cet e-mail.
|
||||||
|
|
||||||
|
Si vous avez des questions, envoyez-nous un e-mail : {{your_email}}
|
||||||
|
|
||||||
|
--
|
||||||
|
{{your_name}}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
Merci pour votre don !
|
||||||
|
|
||||||
|
Votre reçu :
|
||||||
|
|
||||||
|
{{#monthly}}
|
||||||
|
Don mensuel {{amount}}€
|
||||||
|
{{/monthly}}
|
||||||
|
{{^monthly}}
|
||||||
|
Don unique {{amount}}€
|
||||||
|
{{/monthly}}
|
||||||
|
|
||||||
|
Vous pouvez visualiser et gérer vos dons ici :
|
||||||
|
{{root}}/panel
|
||||||
|
|
||||||
|
Merci encore !
|
||||||
|
Si vous avez des questions, vous pouvez nous contacter à l'adresse : {{your_email}}.
|
||||||
|
|
||||||
|
--
|
||||||
|
{{your_name}}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# This is my nginx configuration
|
||||||
|
# Yours will look different. This is just an example.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name drewdevault.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl spdy;
|
||||||
|
listen [::]:443 ssl spdy default_server ipv6only=on;
|
||||||
|
server_name drewdevault.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://sircmpwn.github.io;
|
||||||
|
proxy_redirect http:// https://;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /donate/ {
|
||||||
|
proxy_pass http://127.0.0.1:5000/;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from fosspay.objects import *
|
||||||
|
from fosspay.database import db
|
||||||
|
from fosspay.config import _cfg
|
||||||
|
from fosspay.email import send_thank_you, send_declined
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import stripe
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
stripe.api_key = _cfg("stripe-secret")
|
||||||
|
|
||||||
|
currency = _cfg("currency")
|
||||||
|
|
||||||
|
print("Processing monthly donations at " + str(datetime.utcnow()))
|
||||||
|
|
||||||
|
donations = Donation.query \
|
||||||
|
.filter(Donation.type == DonationType.monthly) \
|
||||||
|
.filter(Donation.active) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
limit = datetime.now() - timedelta(days=30)
|
||||||
|
|
||||||
|
for donation in donations:
|
||||||
|
if donation.updated < limit:
|
||||||
|
print("Charging {}".format(donation))
|
||||||
|
user = donation.user
|
||||||
|
customer = stripe.Customer.retrieve(user.stripe_customer)
|
||||||
|
try:
|
||||||
|
charge = stripe.Charge.create(
|
||||||
|
amount=donation.amount,
|
||||||
|
currency=currency,
|
||||||
|
customer=user.stripe_customer,
|
||||||
|
description="Donation to " + _cfg("your-name")
|
||||||
|
)
|
||||||
|
except stripe.error.CardError as e:
|
||||||
|
donation.active = False
|
||||||
|
db.commit()
|
||||||
|
send_declined(user, donation.amount)
|
||||||
|
print("Declined")
|
||||||
|
continue
|
||||||
|
|
||||||
|
send_thank_you(user, donation.amount, donation.type == DonationType.monthly)
|
||||||
|
donation.updated = datetime.now()
|
||||||
|
donation.payments += 1
|
||||||
|
db.commit()
|
||||||
|
else:
|
||||||
|
print("Skipping {}".format(donation))
|
||||||
|
|
||||||
|
print("{} records processed.".format(len(donations)))
|
||||||
|
|
||||||
|
if _cfg("patreon-refresh-token"):
|
||||||
|
print("Updating Patreon API token")
|
||||||
|
|
||||||
|
r = requests.post('https://www.patreon.com/api/oauth2/token', params={
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': _cfg("patreon-refresh-token"),
|
||||||
|
'client_id': _cfg("patreon-client-id"),
|
||||||
|
'client_secret': _cfg("patreon-client-secret")
|
||||||
|
})
|
||||||
|
if r.status_code != 200:
|
||||||
|
print("Failed to update Patreon API token")
|
||||||
|
sys.exit(1)
|
||||||
|
resp = r.json()
|
||||||
|
with open("config.ini") as f:
|
||||||
|
config = f.read()
|
||||||
|
config = config.replace(_cfg("patreon-access-token"), resp["access_token"])
|
||||||
|
config = config.replace(_cfg("patreon-refresh-token"), resp["refresh_token"])
|
||||||
|
with open("config.ini", "w") as f:
|
||||||
|
f.write(config)
|
||||||
|
print("Refreshed Patreon API token")
|
||||||
|
reload_cmd = _cfg("reload-command")
|
||||||
|
if not reload_cmd:
|
||||||
|
print("Cannot reload application, add reload-command to config.ini")
|
||||||
|
else:
|
||||||
|
subprocess.run(reload_cmd, shell=True, check=True)
|
|
@ -0,0 +1,3 @@
|
||||||
|
Hi $your_name!
|
||||||
|
|
||||||
|
Unfortunately, $email has chosen to cancel their monthly donation of $amount.
|
|
@ -0,0 +1,12 @@
|
||||||
|
An attempt was just made to charge your card for your monthly donation to
|
||||||
|
$your_name for $amount. Unfortunately, your card was declined.
|
||||||
|
|
||||||
|
The donation has been disabled. If you would like to, you may create a new
|
||||||
|
recurring donation here:
|
||||||
|
|
||||||
|
$root
|
||||||
|
|
||||||
|
Thanks!
|
||||||
|
|
||||||
|
--
|
||||||
|
$your_name
|
|
@ -0,0 +1,5 @@
|
||||||
|
Hi $your_name!
|
||||||
|
|
||||||
|
Good news: $email just donated $amount$frequency!
|
||||||
|
|
||||||
|
$comment
|
|
@ -0,0 +1,13 @@
|
||||||
|
Someone, probably you, wants to reset your donor password.
|
||||||
|
|
||||||
|
To proceed, click this link:
|
||||||
|
|
||||||
|
$root/password-reset/$password_reset
|
||||||
|
|
||||||
|
This link expires in 24 hours. If you don't want to change your password or you
|
||||||
|
weren't expecting this email, just ignore it.
|
||||||
|
|
||||||
|
If you have questions, send an email to $your_email.
|
||||||
|
|
||||||
|
--
|
||||||
|
$your_name
|
|
@ -0,0 +1,14 @@
|
||||||
|
Thank you for donating!
|
||||||
|
|
||||||
|
Receipt:
|
||||||
|
|
||||||
|
$summary $amount
|
||||||
|
|
||||||
|
You can view and manage your donations online here:
|
||||||
|
|
||||||
|
$root/panel
|
||||||
|
|
||||||
|
Thanks again! If you have questions, contact $your_email.
|
||||||
|
|
||||||
|
--
|
||||||
|
$your_name
|
|
@ -0,0 +1,80 @@
|
||||||
|
from flask import Flask, render_template, request, g, Response, redirect, url_for
|
||||||
|
from flask_login import LoginManager, current_user
|
||||||
|
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import locale
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
from fosspay.config import _cfg, _cfgi
|
||||||
|
from fosspay.database import db, init_db
|
||||||
|
from fosspay.objects import User
|
||||||
|
from fosspay.common import *
|
||||||
|
from fosspay.network import *
|
||||||
|
|
||||||
|
from fosspay.blueprints.html import html
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = _cfg("secret-key")
|
||||||
|
app.jinja_env.cache = None
|
||||||
|
init_db()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
|
||||||
|
app.jinja_loader = ChoiceLoader([
|
||||||
|
FileSystemLoader("overrides"),
|
||||||
|
FileSystemLoader("templates"),
|
||||||
|
])
|
||||||
|
|
||||||
|
stripe.api_key = _cfg("stripe-secret")
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(email):
|
||||||
|
return User.query.filter(User.email == email).first()
|
||||||
|
|
||||||
|
login_manager.anonymous_user = lambda: None
|
||||||
|
|
||||||
|
app.register_blueprint(html)
|
||||||
|
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, 'en_US')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not app.debug:
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def handle_500(e):
|
||||||
|
# shit
|
||||||
|
try:
|
||||||
|
db.rollback()
|
||||||
|
db.close()
|
||||||
|
except:
|
||||||
|
# shit shit
|
||||||
|
print("We're very borked, letting init system kick us back up")
|
||||||
|
sys.exit(1)
|
||||||
|
return render_template("internal_error.html"), 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def handle_404(e):
|
||||||
|
return render_template("not_found.html"), 404
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject():
|
||||||
|
return {
|
||||||
|
'root': _cfg("protocol") + "://" + _cfg("domain"),
|
||||||
|
'domain': _cfg("domain"),
|
||||||
|
'protocol': _cfg("protocol"),
|
||||||
|
'len': len,
|
||||||
|
'any': any,
|
||||||
|
'request': request,
|
||||||
|
'locale': locale,
|
||||||
|
'url_for': url_for,
|
||||||
|
'file_link': file_link,
|
||||||
|
'user': current_user,
|
||||||
|
'_cfg': _cfg,
|
||||||
|
'_cfgi': _cfgi,
|
||||||
|
'debug': app.debug,
|
||||||
|
'str': str,
|
||||||
|
'int': int
|
||||||
|
}
|
|
@ -0,0 +1,684 @@
|
||||||
|
# https://gist.github.com/michenriksen/8710649
|
||||||
|
email_blacklist = [
|
||||||
|
"0815.ru",
|
||||||
|
"0wnd.net",
|
||||||
|
"0wnd.org",
|
||||||
|
"10minutemail.co.za",
|
||||||
|
"10minutemail.com",
|
||||||
|
"123-m.com",
|
||||||
|
"1fsdfdsfsdf.tk",
|
||||||
|
"1pad.de",
|
||||||
|
"20minutemail.com",
|
||||||
|
"21cn.com",
|
||||||
|
"2fdgdfgdfgdf.tk",
|
||||||
|
"2prong.com",
|
||||||
|
"30minutemail.com",
|
||||||
|
"33mail.com",
|
||||||
|
"3trtretgfrfe.tk",
|
||||||
|
"4gfdsgfdgfd.tk",
|
||||||
|
"4warding.com",
|
||||||
|
"5ghgfhfghfgh.tk",
|
||||||
|
"6hjgjhgkilkj.tk",
|
||||||
|
"6paq.com",
|
||||||
|
"7tags.com",
|
||||||
|
"9ox.net",
|
||||||
|
"a-bc.net",
|
||||||
|
"agedmail.com",
|
||||||
|
"ama-trade.de",
|
||||||
|
"amilegit.com",
|
||||||
|
"amiri.net",
|
||||||
|
"amiriindustries.com",
|
||||||
|
"anonmails.de",
|
||||||
|
"anonymbox.com",
|
||||||
|
"antichef.com",
|
||||||
|
"antichef.net",
|
||||||
|
"antireg.ru",
|
||||||
|
"antispam.de",
|
||||||
|
"antispammail.de",
|
||||||
|
"armyspy.com",
|
||||||
|
"artman-conception.com",
|
||||||
|
"awdrt.net",
|
||||||
|
"azmeil.tk",
|
||||||
|
"baxomale.ht.cx",
|
||||||
|
"beefmilk.com",
|
||||||
|
"bigstring.com",
|
||||||
|
"binkmail.com",
|
||||||
|
"bio-muesli.net",
|
||||||
|
"bobmail.info",
|
||||||
|
"bodhi.lawlita.com",
|
||||||
|
"bofthew.com",
|
||||||
|
"bootybay.de",
|
||||||
|
"boun.cr",
|
||||||
|
"bouncr.com",
|
||||||
|
"breakthru.com",
|
||||||
|
"brefmail.com",
|
||||||
|
"bsnow.net",
|
||||||
|
"bspamfree.org",
|
||||||
|
"bugmenot.com",
|
||||||
|
"bund.us",
|
||||||
|
"burstmail.info",
|
||||||
|
"buymoreplays.com",
|
||||||
|
"byom.de",
|
||||||
|
"c2.hu",
|
||||||
|
"card.zp.ua",
|
||||||
|
"casualdx.com",
|
||||||
|
"cek.pm",
|
||||||
|
"centermail.com",
|
||||||
|
"centermail.net",
|
||||||
|
"chammy.info",
|
||||||
|
"childsavetrust.org",
|
||||||
|
"chogmail.com",
|
||||||
|
"choicemail1.com",
|
||||||
|
"clixser.com",
|
||||||
|
"cmail.net",
|
||||||
|
"cmail.org",
|
||||||
|
"coldemail.info",
|
||||||
|
"cool.fr.nf",
|
||||||
|
"courriel.fr.nf",
|
||||||
|
"courrieltemporaire.com",
|
||||||
|
"crapmail.org",
|
||||||
|
"cust.in",
|
||||||
|
"cuvox.de",
|
||||||
|
"d3p.dk",
|
||||||
|
"dacoolest.com",
|
||||||
|
"dandikmail.com",
|
||||||
|
"dayrep.com",
|
||||||
|
"dcemail.com",
|
||||||
|
"deadaddress.com",
|
||||||
|
"deadspam.com",
|
||||||
|
"delikkt.de",
|
||||||
|
"despam.it",
|
||||||
|
"despammed.com",
|
||||||
|
"devnullmail.com",
|
||||||
|
"dfgh.net",
|
||||||
|
"digitalsanctuary.com",
|
||||||
|
"dingbone.com",
|
||||||
|
"disposableaddress.com",
|
||||||
|
"disposableemailaddresses.com",
|
||||||
|
"disposableinbox.com",
|
||||||
|
"dispose.it",
|
||||||
|
"dispostable.com",
|
||||||
|
"dodgeit.com",
|
||||||
|
"dodgit.com",
|
||||||
|
"donemail.ru",
|
||||||
|
"dontreg.com",
|
||||||
|
"dontsendmespam.de",
|
||||||
|
"drdrb.net",
|
||||||
|
"dump-email.info",
|
||||||
|
"dumpandjunk.com",
|
||||||
|
"dumpyemail.com",
|
||||||
|
"e-mail.com",
|
||||||
|
"e-mail.org",
|
||||||
|
"e4ward.com",
|
||||||
|
"easytrashmail.com",
|
||||||
|
"einmalmail.de",
|
||||||
|
"einrot.com",
|
||||||
|
"eintagsmail.de",
|
||||||
|
"emailgo.de",
|
||||||
|
"emailias.com",
|
||||||
|
"emaillime.com",
|
||||||
|
"emailsensei.com",
|
||||||
|
"emailtemporanea.com",
|
||||||
|
"emailtemporanea.net",
|
||||||
|
"emailtemporar.ro",
|
||||||
|
"emailtemporario.com.br",
|
||||||
|
"emailthe.net",
|
||||||
|
"emailtmp.com",
|
||||||
|
"emailwarden.com",
|
||||||
|
"emailx.at.hm",
|
||||||
|
"emailxfer.com",
|
||||||
|
"emeil.in",
|
||||||
|
"emeil.ir",
|
||||||
|
"emz.net",
|
||||||
|
"ero-tube.org",
|
||||||
|
"evopo.com",
|
||||||
|
"explodemail.com",
|
||||||
|
"express.net.ua",
|
||||||
|
"eyepaste.com",
|
||||||
|
"fakeinbox.com",
|
||||||
|
"fakeinformation.com",
|
||||||
|
"fansworldwide.de",
|
||||||
|
"fantasymail.de",
|
||||||
|
"fightallspam.com",
|
||||||
|
"filzmail.com",
|
||||||
|
"fivemail.de",
|
||||||
|
"fleckens.hu",
|
||||||
|
"frapmail.com",
|
||||||
|
"friendlymail.co.uk",
|
||||||
|
"fuckingduh.com",
|
||||||
|
"fudgerub.com",
|
||||||
|
"fyii.de",
|
||||||
|
"garliclife.com",
|
||||||
|
"gehensiemirnichtaufdensack.de",
|
||||||
|
"get2mail.fr",
|
||||||
|
"getairmail.com",
|
||||||
|
"getmails.eu",
|
||||||
|
"getonemail.com",
|
||||||
|
"giantmail.de",
|
||||||
|
"girlsundertheinfluence.com",
|
||||||
|
"gishpuppy.com",
|
||||||
|
"gmial.com",
|
||||||
|
"goemailgo.com",
|
||||||
|
"gotmail.net",
|
||||||
|
"gotmail.org",
|
||||||
|
"gotti.otherinbox.com",
|
||||||
|
"great-host.in",
|
||||||
|
"greensloth.com",
|
||||||
|
"grr.la",
|
||||||
|
"gsrv.co.uk",
|
||||||
|
"guerillamail.biz",
|
||||||
|
"guerillamail.com",
|
||||||
|
"guerrillamail.biz",
|
||||||
|
"guerrillamail.com",
|
||||||
|
"guerrillamail.de",
|
||||||
|
"guerrillamail.info",
|
||||||
|
"guerrillamail.net",
|
||||||
|
"guerrillamail.org",
|
||||||
|
"guerrillamailblock.com",
|
||||||
|
"gustr.com",
|
||||||
|
"harakirimail.com",
|
||||||
|
"hat-geld.de",
|
||||||
|
"hatespam.org",
|
||||||
|
"herp.in",
|
||||||
|
"hidemail.de",
|
||||||
|
"hidzz.com",
|
||||||
|
"hmamail.com",
|
||||||
|
"hopemail.biz",
|
||||||
|
"ieh-mail.de",
|
||||||
|
"ikbenspamvrij.nl",
|
||||||
|
"imails.info",
|
||||||
|
"inbax.tk",
|
||||||
|
"inbox.si",
|
||||||
|
"inboxalias.com",
|
||||||
|
"inboxclean.com",
|
||||||
|
"inboxclean.org",
|
||||||
|
"infocom.zp.ua",
|
||||||
|
"instant-mail.de",
|
||||||
|
"ip6.li",
|
||||||
|
"irish2me.com",
|
||||||
|
"iwi.net",
|
||||||
|
"jetable.com",
|
||||||
|
"jetable.fr.nf",
|
||||||
|
"jetable.net",
|
||||||
|
"jetable.org",
|
||||||
|
"jnxjn.com",
|
||||||
|
"jourrapide.com",
|
||||||
|
"jsrsolutions.com",
|
||||||
|
"kasmail.com",
|
||||||
|
"kaspop.com",
|
||||||
|
"killmail.com",
|
||||||
|
"killmail.net",
|
||||||
|
"klassmaster.com",
|
||||||
|
"klzlk.com",
|
||||||
|
"koszmail.pl",
|
||||||
|
"kurzepost.de",
|
||||||
|
"lawlita.com",
|
||||||
|
"letthemeatspam.com",
|
||||||
|
"lhsdv.com",
|
||||||
|
"lifebyfood.com",
|
||||||
|
"link2mail.net",
|
||||||
|
"litedrop.com",
|
||||||
|
"lol.ovpn.to",
|
||||||
|
"lolfreak.net",
|
||||||
|
"lookugly.com",
|
||||||
|
"lortemail.dk",
|
||||||
|
"lr78.com",
|
||||||
|
"lroid.com",
|
||||||
|
"lukop.dk",
|
||||||
|
"m21.cc",
|
||||||
|
"mail-filter.com",
|
||||||
|
"mail-temporaire.fr",
|
||||||
|
"mail.by",
|
||||||
|
"mail.mezimages.net",
|
||||||
|
"mail.zp.ua",
|
||||||
|
"mail1a.de",
|
||||||
|
"mail21.cc",
|
||||||
|
"mail2rss.org",
|
||||||
|
"mail333.com",
|
||||||
|
"mailbidon.com",
|
||||||
|
"mailbiz.biz",
|
||||||
|
"mailblocks.com",
|
||||||
|
"mailbucket.org",
|
||||||
|
"mailcat.biz",
|
||||||
|
"mailcatch.com",
|
||||||
|
"mailde.de",
|
||||||
|
"mailde.info",
|
||||||
|
"maildrop.cc",
|
||||||
|
"maileimer.de",
|
||||||
|
"mailexpire.com",
|
||||||
|
"mailfa.tk",
|
||||||
|
"mailforspam.com",
|
||||||
|
"mailfreeonline.com",
|
||||||
|
"mailguard.me",
|
||||||
|
"mailin8r.com",
|
||||||
|
"mailinater.com",
|
||||||
|
"mailinator.com",
|
||||||
|
"mailinator.net",
|
||||||
|
"mailinator.org",
|
||||||
|
"mailinator2.com",
|
||||||
|
"mailincubator.com",
|
||||||
|
"mailismagic.com",
|
||||||
|
"mailme.lv",
|
||||||
|
"mailme24.com",
|
||||||
|
"mailmetrash.com",
|
||||||
|
"mailmoat.com",
|
||||||
|
"mailms.com",
|
||||||
|
"mailnesia.com",
|
||||||
|
"mailnull.com",
|
||||||
|
"mailorg.org",
|
||||||
|
"mailpick.biz",
|
||||||
|
"mailrock.biz",
|
||||||
|
"mailscrap.com",
|
||||||
|
"mailshell.com",
|
||||||
|
"mailsiphon.com",
|
||||||
|
"mailtemp.info",
|
||||||
|
"mailtome.de",
|
||||||
|
"mailtothis.com",
|
||||||
|
"mailtrash.net",
|
||||||
|
"mailtv.net",
|
||||||
|
"mailtv.tv",
|
||||||
|
"mailzilla.com",
|
||||||
|
"makemetheking.com",
|
||||||
|
"manybrain.com",
|
||||||
|
"mbx.cc",
|
||||||
|
"mega.zik.dj",
|
||||||
|
"meinspamschutz.de",
|
||||||
|
"meltmail.com",
|
||||||
|
"messagebeamer.de",
|
||||||
|
"mezimages.net",
|
||||||
|
"ministry-of-silly-walks.de",
|
||||||
|
"mintemail.com",
|
||||||
|
"misterpinball.de",
|
||||||
|
"moncourrier.fr.nf",
|
||||||
|
"monemail.fr.nf",
|
||||||
|
"monmail.fr.nf",
|
||||||
|
"monumentmail.com",
|
||||||
|
"mt2009.com",
|
||||||
|
"mt2014.com",
|
||||||
|
"mycard.net.ua",
|
||||||
|
"mycleaninbox.net",
|
||||||
|
"mymail-in.net",
|
||||||
|
"mypacks.net",
|
||||||
|
"mypartyclip.de",
|
||||||
|
"myphantomemail.com",
|
||||||
|
"mysamp.de",
|
||||||
|
"mytempemail.com",
|
||||||
|
"mytempmail.com",
|
||||||
|
"mytrashmail.com",
|
||||||
|
"nabuma.com",
|
||||||
|
"neomailbox.com",
|
||||||
|
"nepwk.com",
|
||||||
|
"nervmich.net",
|
||||||
|
"nervtmich.net",
|
||||||
|
"netmails.com",
|
||||||
|
"netmails.net",
|
||||||
|
"neverbox.com",
|
||||||
|
"nice-4u.com",
|
||||||
|
"nincsmail.hu",
|
||||||
|
"nnh.com",
|
||||||
|
"no-spam.ws",
|
||||||
|
"noblepioneer.com",
|
||||||
|
"nomail.pw",
|
||||||
|
"nomail.xl.cx",
|
||||||
|
"nomail2me.com",
|
||||||
|
"nomorespamemails.com",
|
||||||
|
"nospam.ze.tc",
|
||||||
|
"nospam4.us",
|
||||||
|
"nospamfor.us",
|
||||||
|
"nospammail.net",
|
||||||
|
"notmailinator.com",
|
||||||
|
"nowhere.org",
|
||||||
|
"nowmymail.com",
|
||||||
|
"nurfuerspam.de",
|
||||||
|
"nus.edu.sg",
|
||||||
|
"objectmail.com",
|
||||||
|
"obobbo.com",
|
||||||
|
"odnorazovoe.ru",
|
||||||
|
"oneoffemail.com",
|
||||||
|
"onewaymail.com",
|
||||||
|
"onlatedotcom.info",
|
||||||
|
"online.ms",
|
||||||
|
"opayq.com",
|
||||||
|
"ordinaryamerican.net",
|
||||||
|
"otherinbox.com",
|
||||||
|
"ovpn.to",
|
||||||
|
"owlpic.com",
|
||||||
|
"pancakemail.com",
|
||||||
|
"pcusers.otherinbox.com",
|
||||||
|
"pjjkp.com",
|
||||||
|
"plexolan.de",
|
||||||
|
"poczta.onet.pl",
|
||||||
|
"politikerclub.de",
|
||||||
|
"poofy.org",
|
||||||
|
"pookmail.com",
|
||||||
|
"privacy.net",
|
||||||
|
"privatdemail.net",
|
||||||
|
"proxymail.eu",
|
||||||
|
"prtnx.com",
|
||||||
|
"putthisinyourspamdatabase.com",
|
||||||
|
"putthisinyourspamdatabase.com",
|
||||||
|
"qq.com",
|
||||||
|
"quickinbox.com",
|
||||||
|
"rcpt.at",
|
||||||
|
"reallymymail.com",
|
||||||
|
"realtyalerts.ca",
|
||||||
|
"recode.me",
|
||||||
|
"recursor.net",
|
||||||
|
"reliable-mail.com",
|
||||||
|
"rhyta.com",
|
||||||
|
"rmqkr.net",
|
||||||
|
"royal.net",
|
||||||
|
"rtrtr.com",
|
||||||
|
"s0ny.net",
|
||||||
|
"safe-mail.net",
|
||||||
|
"safersignup.de",
|
||||||
|
"safetymail.info",
|
||||||
|
"safetypost.de",
|
||||||
|
"saynotospams.com",
|
||||||
|
"schafmail.de",
|
||||||
|
"schrott-email.de",
|
||||||
|
"secretemail.de",
|
||||||
|
"secure-mail.biz",
|
||||||
|
"senseless-entertainment.com",
|
||||||
|
"services391.com",
|
||||||
|
"sharklasers.com",
|
||||||
|
"shieldemail.com",
|
||||||
|
"shiftmail.com",
|
||||||
|
"shitmail.me",
|
||||||
|
"shitware.nl",
|
||||||
|
"shmeriously.com",
|
||||||
|
"shortmail.net",
|
||||||
|
"sibmail.com",
|
||||||
|
"sinnlos-mail.de",
|
||||||
|
"slapsfromlastnight.com",
|
||||||
|
"slaskpost.se",
|
||||||
|
"smashmail.de",
|
||||||
|
"smellfear.com",
|
||||||
|
"snakemail.com",
|
||||||
|
"sneakemail.com",
|
||||||
|
"sneakmail.de",
|
||||||
|
"snkmail.com",
|
||||||
|
"sofimail.com",
|
||||||
|
"solvemail.info",
|
||||||
|
"sogetthis.com",
|
||||||
|
"soodonims.com",
|
||||||
|
"spam4.me",
|
||||||
|
"spamail.de",
|
||||||
|
"spamarrest.com",
|
||||||
|
"spambob.net",
|
||||||
|
"spambog.ru",
|
||||||
|
"spambox.us",
|
||||||
|
"spamcannon.com",
|
||||||
|
"spamcannon.net",
|
||||||
|
"spamcon.org",
|
||||||
|
"spamcorptastic.com",
|
||||||
|
"spamcowboy.com",
|
||||||
|
"spamcowboy.net",
|
||||||
|
"spamcowboy.org",
|
||||||
|
"spamday.com",
|
||||||
|
"spamex.com",
|
||||||
|
"spamfree.eu",
|
||||||
|
"spamfree24.com",
|
||||||
|
"spamfree24.de",
|
||||||
|
"spamfree24.org",
|
||||||
|
"spamgoes.in",
|
||||||
|
"spamgourmet.com",
|
||||||
|
"spamgourmet.net",
|
||||||
|
"spamgourmet.org",
|
||||||
|
"spamherelots.com",
|
||||||
|
"spamherelots.com",
|
||||||
|
"spamhereplease.com",
|
||||||
|
"spamhereplease.com",
|
||||||
|
"spamhole.com",
|
||||||
|
"spamify.com",
|
||||||
|
"spaml.de",
|
||||||
|
"spammotel.com",
|
||||||
|
"spamobox.com",
|
||||||
|
"spamslicer.com",
|
||||||
|
"spamspot.com",
|
||||||
|
"spamthis.co.uk",
|
||||||
|
"spamtroll.net",
|
||||||
|
"speed.1s.fr",
|
||||||
|
"spoofmail.de",
|
||||||
|
"stuffmail.de",
|
||||||
|
"super-auswahl.de",
|
||||||
|
"supergreatmail.com",
|
||||||
|
"supermailer.jp",
|
||||||
|
"superrito.com",
|
||||||
|
"superstachel.de",
|
||||||
|
"suremail.info",
|
||||||
|
"talkinator.com",
|
||||||
|
"teewars.org",
|
||||||
|
"teleworm.com",
|
||||||
|
"teleworm.us",
|
||||||
|
"temp-mail.org",
|
||||||
|
"temp-mail.ru",
|
||||||
|
"tempe-mail.com",
|
||||||
|
"tempemail.co.za",
|
||||||
|
"tempemail.com",
|
||||||
|
"tempemail.net",
|
||||||
|
"tempemail.net",
|
||||||
|
"tempinbox.co.uk",
|
||||||
|
"tempinbox.com",
|
||||||
|
"tempmail.eu",
|
||||||
|
"tempmaildemo.com",
|
||||||
|
"tempmailer.com",
|
||||||
|
"tempmailer.de",
|
||||||
|
"tempomail.fr",
|
||||||
|
"temporaryemail.net",
|
||||||
|
"temporaryforwarding.com",
|
||||||
|
"temporaryinbox.com",
|
||||||
|
"temporarymailaddress.com",
|
||||||
|
"tempthe.net",
|
||||||
|
"thankyou2010.com",
|
||||||
|
"thc.st",
|
||||||
|
"thelimestones.com",
|
||||||
|
"thisisnotmyrealemail.com",
|
||||||
|
"thismail.net",
|
||||||
|
"throwawayemailaddress.com",
|
||||||
|
"tilien.com",
|
||||||
|
"tittbit.in",
|
||||||
|
"tizi.com",
|
||||||
|
"tmailinator.com",
|
||||||
|
"toomail.biz",
|
||||||
|
"topranklist.de",
|
||||||
|
"tradermail.info",
|
||||||
|
"trash-mail.at",
|
||||||
|
"trash-mail.com",
|
||||||
|
"trash-mail.de",
|
||||||
|
"trash2009.com",
|
||||||
|
"trashdevil.com",
|
||||||
|
"trashemail.de",
|
||||||
|
"trashmail.at",
|
||||||
|
"trashmail.com",
|
||||||
|
"trashmail.de",
|
||||||
|
"trashmail.me",
|
||||||
|
"trashmail.net",
|
||||||
|
"trashmail.org",
|
||||||
|
"trashymail.com",
|
||||||
|
"trialmail.de",
|
||||||
|
"trillianpro.com",
|
||||||
|
"twinmail.de",
|
||||||
|
"tyldd.com",
|
||||||
|
"uggsrock.com",
|
||||||
|
"umail.net",
|
||||||
|
"uroid.com",
|
||||||
|
"us.af",
|
||||||
|
"venompen.com",
|
||||||
|
"veryrealemail.com",
|
||||||
|
"viditag.com",
|
||||||
|
"viralplays.com",
|
||||||
|
"vpn.st",
|
||||||
|
"vsimcard.com",
|
||||||
|
"vubby.com",
|
||||||
|
"wasteland.rfc822.org",
|
||||||
|
"webemail.me",
|
||||||
|
"weg-werf-email.de",
|
||||||
|
"wegwerf-emails.de",
|
||||||
|
"wegwerfadresse.de",
|
||||||
|
"wegwerfemail.com",
|
||||||
|
"wegwerfemail.de",
|
||||||
|
"wegwerfmail.de",
|
||||||
|
"wegwerfmail.info",
|
||||||
|
"wegwerfmail.net",
|
||||||
|
"wegwerfmail.org",
|
||||||
|
"wh4f.org",
|
||||||
|
"whyspam.me",
|
||||||
|
"willhackforfood.biz",
|
||||||
|
"willselfdestruct.com",
|
||||||
|
"winemaven.info",
|
||||||
|
"wronghead.com",
|
||||||
|
"www.e4ward.com",
|
||||||
|
"www.mailinator.com",
|
||||||
|
"wwwnew.eu",
|
||||||
|
"x.ip6.li",
|
||||||
|
"xagloo.com",
|
||||||
|
"xemaps.com",
|
||||||
|
"xents.com",
|
||||||
|
"xmaily.com",
|
||||||
|
"xoxy.net",
|
||||||
|
"yep.it",
|
||||||
|
"yogamaven.com",
|
||||||
|
"yopmail.com",
|
||||||
|
"yopmail.fr",
|
||||||
|
"yopmail.net",
|
||||||
|
"yourdomain.com",
|
||||||
|
"yuurok.com",
|
||||||
|
"z1p.biz",
|
||||||
|
"za.com",
|
||||||
|
"zehnminuten.de",
|
||||||
|
"zehnminutenmail.de",
|
||||||
|
"zippymail.info",
|
||||||
|
"zoemail.net",
|
||||||
|
"zomg.info",
|
||||||
|
|
||||||
|
# Additions:
|
||||||
|
"mailto.plus",
|
||||||
|
"fexpost.com",
|
||||||
|
"fexbos.ru",
|
||||||
|
"fexbox.org",
|
||||||
|
"rover.info",
|
||||||
|
"inpwa.com",
|
||||||
|
"intopwa.org",
|
||||||
|
"intopwa.net",
|
||||||
|
"intopwa.com",
|
||||||
|
"mailbox.in.ua",
|
||||||
|
"btc.glass",
|
||||||
|
"1secmail.com",
|
||||||
|
"1secmail.org",
|
||||||
|
"1secmail.net",
|
||||||
|
"relay.firefox.com",
|
||||||
|
"miucce.com",
|
||||||
|
"upived.o",
|
||||||
|
"biyac.com",
|
||||||
|
"nucleant.org",
|
||||||
|
"temporary-mail.net",
|
||||||
|
"tempr.email",
|
||||||
|
"discard.email",
|
||||||
|
"discardmail.com",
|
||||||
|
"discardmail.de",
|
||||||
|
"spambog.com",
|
||||||
|
"spambog.de",
|
||||||
|
"spambog.ru",
|
||||||
|
"0815.ru",
|
||||||
|
"knol-power.nl",
|
||||||
|
"freundin.ru",
|
||||||
|
"smashmail.de",
|
||||||
|
"s0ny.net",
|
||||||
|
"pecinan.net",
|
||||||
|
"budaya-tionghoa.com",
|
||||||
|
"lajoska.pe.hu",
|
||||||
|
"1mail.x24hr.com",
|
||||||
|
"from.onmypc.info",
|
||||||
|
"now.mefound.com",
|
||||||
|
"mowgli.jungleheart.com",
|
||||||
|
"pecinan.org",
|
||||||
|
"budayationghoa.com",
|
||||||
|
"CR.cloudns.asia",
|
||||||
|
"TLS.cloudns.asia",
|
||||||
|
"MSFT.cloudns.asia",
|
||||||
|
"B.cr.cloUdnS.asia",
|
||||||
|
"ssl.tls.cloudns.ASIA",
|
||||||
|
"sweetxxx.de",
|
||||||
|
"DVD.dns-cloud.net",
|
||||||
|
"DVD.dnsabr.com",
|
||||||
|
"BD.dns-cloud.net",
|
||||||
|
"YX.dns-cloud.net",
|
||||||
|
"SHIT.dns-cloud.net",
|
||||||
|
"SHIT.dnsabr.com",
|
||||||
|
"eu.dns-cloud.net",
|
||||||
|
"eu.dnsabr.com",
|
||||||
|
"asia.dnsabr.com",
|
||||||
|
"8.dnsabr.com",
|
||||||
|
"pw.8.dnsabr.com",
|
||||||
|
"mm.8.dnsabr.com",
|
||||||
|
"23.8.dnsabr.com",
|
||||||
|
"pecinan.com",
|
||||||
|
"disposable-email.ml",
|
||||||
|
"pw.epac.to",
|
||||||
|
"postheo.de",
|
||||||
|
"sexy.camdvr.org",
|
||||||
|
"Disposable.ml",
|
||||||
|
"888.dnS-clouD.NET",
|
||||||
|
"adult-work.info",
|
||||||
|
"trap-mail.de",
|
||||||
|
"gmaile.design",
|
||||||
|
"tempes.gq",
|
||||||
|
"cpmail.life",
|
||||||
|
"tempemail.info",
|
||||||
|
"coolmailcool.com",
|
||||||
|
"notmyemail.tech",
|
||||||
|
"m.cloudns.cl",
|
||||||
|
"twitter-sign-in.cf",
|
||||||
|
"anonymized.org",
|
||||||
|
"you.has.dating",
|
||||||
|
"t.woeishyang.com",
|
||||||
|
"blackturtle.xyz",
|
||||||
|
"mailg.ml",
|
||||||
|
"media.motornation.buzz",
|
||||||
|
"badlion.co.uk",
|
||||||
|
"mrdeeps.ml",
|
||||||
|
"fouadps.cf",
|
||||||
|
"fshare.ootech.vn",
|
||||||
|
"pflege-schoene-haut.de",
|
||||||
|
"corona.is.bullsht.dedyn.io",
|
||||||
|
"dristypat.com",
|
||||||
|
"smack.email",
|
||||||
|
"techwizardent.me",
|
||||||
|
"mrgamin.ml",
|
||||||
|
"mrgamin.gq",
|
||||||
|
"mrgamin.cf",
|
||||||
|
"tempmail.wizardmail.tech",
|
||||||
|
"mail.mrgamin.ml",
|
||||||
|
"kaaaxcreators.tk",
|
||||||
|
"mail.kaaaxcreators.tk",
|
||||||
|
"mail.igosad.me",
|
||||||
|
"maa.567map.xyz",
|
||||||
|
"32core.live",
|
||||||
|
"tokyoto.site",
|
||||||
|
"hidemyass.fun",
|
||||||
|
"solpatu.space",
|
||||||
|
"igosad.tech",
|
||||||
|
"99email.xyz",
|
||||||
|
"ketoblazepro.com",
|
||||||
|
"kost.party",
|
||||||
|
"0hio0ak.com",
|
||||||
|
"4dentalsolutions.com",
|
||||||
|
"ondemandemail.top",
|
||||||
|
"kittenemail.xyz",
|
||||||
|
"geneseeit.com",
|
||||||
|
"safeemail.xyz",
|
||||||
|
"virtual-generations.com",
|
||||||
|
"historictheology.com",
|
||||||
|
"speedfocus.biz",
|
||||||
|
"chapedia.net",
|
||||||
|
"meantinc.com",
|
||||||
|
"powerencry.com",
|
||||||
|
"chapedia.org",
|
||||||
|
"truthfinderlogin.com",
|
||||||
|
"chasefreedomactivate.com",
|
||||||
|
"wellsfargocomcardholders.com",
|
||||||
|
"qq.com",
|
||||||
|
"hostux.ninja",
|
||||||
|
"chitthi.in",
|
||||||
|
]
|
|
@ -0,0 +1,345 @@
|
||||||
|
from flask import Blueprint, render_template, abort, request, redirect, session, url_for, send_file, Response
|
||||||
|
from flask_login import current_user, login_user, logout_user
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fosspay.blacklist import email_blacklist
|
||||||
|
from fosspay.objects import *
|
||||||
|
from fosspay.database import db
|
||||||
|
from fosspay.common import *
|
||||||
|
from fosspay.config import _cfg, load_config
|
||||||
|
from fosspay.email import send_thank_you, send_password_reset
|
||||||
|
from fosspay.email import send_new_donation, send_cancellation_notice
|
||||||
|
from fosspay.currency import currency
|
||||||
|
|
||||||
|
import os
|
||||||
|
import locale
|
||||||
|
import bcrypt
|
||||||
|
import hashlib
|
||||||
|
import stripe
|
||||||
|
import binascii
|
||||||
|
import requests
|
||||||
|
|
||||||
|
encoding = locale.getdefaultlocale()[1]
|
||||||
|
html = Blueprint('html', __name__, template_folder='../../templates')
|
||||||
|
|
||||||
|
@html.route("/")
|
||||||
|
def index():
|
||||||
|
if User.query.count() == 0:
|
||||||
|
load_config()
|
||||||
|
return render_template("setup.html")
|
||||||
|
projects = sorted(Project.query.all(), key=lambda p: p.name)
|
||||||
|
avatar = "//www.gravatar.com/avatar/" + hashlib.md5(_cfg("your-email").encode("utf-8")).hexdigest()
|
||||||
|
selected_project = request.args.get("project")
|
||||||
|
if selected_project:
|
||||||
|
try:
|
||||||
|
selected_project = int(selected_project)
|
||||||
|
except:
|
||||||
|
selected_project = None
|
||||||
|
active_recurring = (Donation.query
|
||||||
|
.filter(Donation.type == DonationType.monthly)
|
||||||
|
.filter(Donation.active == True)
|
||||||
|
.filter(Donation.hidden == False))
|
||||||
|
recurring_count = active_recurring.count()
|
||||||
|
recurring_sum = sum([d.amount for d in active_recurring])
|
||||||
|
|
||||||
|
access_token = _cfg("patreon-access-token")
|
||||||
|
campaign = _cfg("patreon-campaign")
|
||||||
|
if access_token and campaign:
|
||||||
|
try:
|
||||||
|
import patreon
|
||||||
|
client = patreon.API(access_token)
|
||||||
|
campaign = client.fetch_campaign()
|
||||||
|
attrs = campaign.json_data["data"][0]["attributes"]
|
||||||
|
patreon_count = attrs["patron_count"]
|
||||||
|
patreon_sum = attrs["pledge_sum"]
|
||||||
|
except:
|
||||||
|
patreon_count = 0
|
||||||
|
patreon_sum = 0
|
||||||
|
else:
|
||||||
|
patreon_count = 0
|
||||||
|
patreon_sum = 0
|
||||||
|
|
||||||
|
liberapay = _cfg("liberapay-campaign")
|
||||||
|
if liberapay:
|
||||||
|
lp = (requests
|
||||||
|
.get("https://liberapay.com/{}/public.json".format(liberapay))
|
||||||
|
).json()
|
||||||
|
lp_count = lp['npatrons']
|
||||||
|
lp_sum = int(float(lp['receiving']['amount']) * 100)
|
||||||
|
# Convert from weekly to monthly
|
||||||
|
lp_sum = lp_sum * 52 // 12
|
||||||
|
else:
|
||||||
|
lp_count = 0
|
||||||
|
lp_sum = 0
|
||||||
|
|
||||||
|
github_token = _cfg("github-token")
|
||||||
|
if github_token:
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
viewer {
|
||||||
|
login
|
||||||
|
sponsorsListing {
|
||||||
|
tiers(first:100) {
|
||||||
|
nodes {
|
||||||
|
monthlyPriceInCents
|
||||||
|
adminInfo {
|
||||||
|
sponsorships(includePrivate:true) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
r = requests.post("https://api.github.com/graphql", json={
|
||||||
|
"query": query
|
||||||
|
}, headers={
|
||||||
|
"Authorization": f"bearer {github_token}"
|
||||||
|
})
|
||||||
|
result = r.json()
|
||||||
|
nodes = result["data"]["viewer"]["sponsorsListing"]["tiers"]["nodes"]
|
||||||
|
cnt = lambda n: n["adminInfo"]["sponsorships"]["totalCount"]
|
||||||
|
gh_count = sum(cnt(n) for n in nodes)
|
||||||
|
gh_sum = sum(n["monthlyPriceInCents"] * cnt(n) for n in nodes)
|
||||||
|
gh_user = result["data"]["viewer"]["login"]
|
||||||
|
else:
|
||||||
|
gh_count = 0
|
||||||
|
gh_sum = 0
|
||||||
|
gh_user = None
|
||||||
|
|
||||||
|
return render_template("index.html", projects=projects,
|
||||||
|
avatar=avatar, selected_project=selected_project,
|
||||||
|
recurring_count=recurring_count, recurring_sum=recurring_sum,
|
||||||
|
patreon_count=patreon_count, patreon_sum=patreon_sum,
|
||||||
|
lp_count=lp_count, lp_sum=lp_sum,
|
||||||
|
gh_count=gh_count, gh_sum=gh_sum, gh_user=gh_user,
|
||||||
|
currency=currency)
|
||||||
|
|
||||||
|
@html.route("/setup", methods=["POST"])
|
||||||
|
def setup():
|
||||||
|
if not User.query.count() == 0:
|
||||||
|
abort(400)
|
||||||
|
email = request.form.get("email")
|
||||||
|
password = request.form.get("password")
|
||||||
|
if not email or not password:
|
||||||
|
return redirect("..") # TODO: Tell them what they did wrong (i.e. being stupid)
|
||||||
|
user = User(email, password)
|
||||||
|
user.admin = True
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
login_user(user)
|
||||||
|
return redirect("admin?first-run=1")
|
||||||
|
|
||||||
|
@html.route("/admin")
|
||||||
|
@adminrequired
|
||||||
|
def admin():
|
||||||
|
first = request.args.get("first-run") is not None
|
||||||
|
projects = Project.query.all()
|
||||||
|
unspecified = Donation.query.filter(Donation.project == None).all()
|
||||||
|
donations = Donation.query.order_by(Donation.created.desc()).all()
|
||||||
|
return render_template("admin.html",
|
||||||
|
first=first,
|
||||||
|
projects=projects,
|
||||||
|
donations=donations,
|
||||||
|
currency=currency,
|
||||||
|
one_times=lambda p: sum([d.amount for d in p.donations if d.type == DonationType.one_time]),
|
||||||
|
recurring=lambda p: sum([d.amount for d in p.donations if d.type == DonationType.monthly and d.active]),
|
||||||
|
recurring_ever=lambda p: sum([d.amount * d.payments for d in p.donations if d.type == DonationType.monthly]),
|
||||||
|
unspecified_one_times=sum([d.amount for d in unspecified if d.type == DonationType.one_time]),
|
||||||
|
unspecified_recurring=sum([d.amount for d in unspecified if d.type == DonationType.monthly and d.active]),
|
||||||
|
unspecified_recurring_ever=sum([d.amount * d.payments for d in unspecified if d.type == DonationType.monthly]),
|
||||||
|
total_one_time=sum([d.amount for d in Donation.query.filter(Donation.type == DonationType.one_time)]),
|
||||||
|
total_recurring=sum([d.amount for d in Donation.query.filter(Donation.type == DonationType.monthly, Donation.active == True)]),
|
||||||
|
total_recurring_ever=sum([d.amount * d.payments for d in Donation.query.filter(Donation.type == DonationType.monthly)]),
|
||||||
|
)
|
||||||
|
|
||||||
|
@html.route("/create-project", methods=["POST"])
|
||||||
|
@adminrequired
|
||||||
|
def create_project():
|
||||||
|
name = request.form.get("name")
|
||||||
|
project = Project(name)
|
||||||
|
db.add(project)
|
||||||
|
db.commit()
|
||||||
|
return redirect("admin")
|
||||||
|
|
||||||
|
@html.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if current_user:
|
||||||
|
if current_user.admin:
|
||||||
|
return redirect("admin")
|
||||||
|
return redirect("panel")
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template("login.html")
|
||||||
|
email = request.form.get("email")
|
||||||
|
password = request.form.get("password")
|
||||||
|
if not email or not password:
|
||||||
|
return render_template("login.html", errors=True)
|
||||||
|
user = User.query.filter(User.email == email).first()
|
||||||
|
if not user:
|
||||||
|
return render_template("login.html", errors=True)
|
||||||
|
if not bcrypt.hashpw(password.encode('UTF-8'), user.password.encode('UTF-8')) == user.password.encode('UTF-8'):
|
||||||
|
return render_template("login.html", errors=True)
|
||||||
|
login_user(user)
|
||||||
|
if user.admin:
|
||||||
|
return redirect("admin")
|
||||||
|
return redirect("panel")
|
||||||
|
|
||||||
|
@html.route("/logout")
|
||||||
|
@loginrequired
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(_cfg("protocol") + "://" + _cfg("domain"))
|
||||||
|
|
||||||
|
@html.route("/donate", methods=["POST"])
|
||||||
|
@json_output
|
||||||
|
def donate():
|
||||||
|
email = request.form.get("email")
|
||||||
|
stripe_token = request.form.get("stripe_token")
|
||||||
|
amount = request.form.get("amount")
|
||||||
|
type = request.form.get("type")
|
||||||
|
comment = request.form.get("comment")
|
||||||
|
project_id = request.form.get("project")
|
||||||
|
|
||||||
|
# validate and rejigger the form inputs
|
||||||
|
if not email or not stripe_token or not amount or not type:
|
||||||
|
return { "success": False, "reason": "Invalid request" }, 400
|
||||||
|
try:
|
||||||
|
if project_id is None or project_id == "null":
|
||||||
|
project = None
|
||||||
|
else:
|
||||||
|
project_id = int(project_id)
|
||||||
|
project = Project.query.filter(Project.id == project_id).first()
|
||||||
|
|
||||||
|
if type == "once":
|
||||||
|
type = DonationType.one_time
|
||||||
|
else:
|
||||||
|
type = DonationType.monthly
|
||||||
|
|
||||||
|
amount = int(amount)
|
||||||
|
except:
|
||||||
|
return { "success": False, "reason": "Invalid request" }, 400
|
||||||
|
|
||||||
|
[_, domain] = email.split("@")
|
||||||
|
if domain in email_blacklist:
|
||||||
|
return { "success": True, "new_account": False }
|
||||||
|
|
||||||
|
new_account = False
|
||||||
|
user = User.query.filter(User.email == email).first()
|
||||||
|
if not user:
|
||||||
|
new_account = True
|
||||||
|
user = User(email, binascii.b2a_hex(os.urandom(20)).decode("utf-8"))
|
||||||
|
user.password_reset = binascii.b2a_hex(os.urandom(20)).decode("utf-8")
|
||||||
|
user.password_reset_expires = datetime.now() + timedelta(days=1)
|
||||||
|
customer = stripe.Customer.create(email=user.email, card=stripe_token)
|
||||||
|
user.stripe_customer = customer.id
|
||||||
|
db.add(user)
|
||||||
|
else:
|
||||||
|
customer = stripe.Customer.retrieve(user.stripe_customer)
|
||||||
|
new_source = customer.sources.create(source=stripe_token)
|
||||||
|
customer.default_source = new_source.id
|
||||||
|
customer.save()
|
||||||
|
|
||||||
|
donation = Donation(user, type, amount, project, comment)
|
||||||
|
db.add(donation)
|
||||||
|
|
||||||
|
try:
|
||||||
|
charge = stripe.Charge.create(
|
||||||
|
amount=amount,
|
||||||
|
currency=_cfg("currency"),
|
||||||
|
customer=user.stripe_customer,
|
||||||
|
description="Donation to " + _cfg("your-name")
|
||||||
|
)
|
||||||
|
except stripe.error.CardError as e:
|
||||||
|
db.rollback()
|
||||||
|
db.close()
|
||||||
|
return { "success": False, "reason": "Your card was declined." }
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
send_thank_you(user, amount, type == DonationType.monthly)
|
||||||
|
send_new_donation(user, donation)
|
||||||
|
|
||||||
|
if new_account:
|
||||||
|
return { "success": True, "new_account": new_account, "password_reset": user.password_reset }
|
||||||
|
else:
|
||||||
|
return { "success": True, "new_account": new_account }
|
||||||
|
|
||||||
|
def issue_password_reset(email):
|
||||||
|
user = User.query.filter(User.email == email).first()
|
||||||
|
if not user:
|
||||||
|
return render_template("reset.html", errors="No one with that email found.")
|
||||||
|
user.password_reset = binascii.b2a_hex(os.urandom(20)).decode("utf-8")
|
||||||
|
user.password_reset_expires = datetime.now() + timedelta(days=1)
|
||||||
|
send_password_reset(user)
|
||||||
|
db.commit()
|
||||||
|
return render_template("reset.html", done=True)
|
||||||
|
|
||||||
|
@html.route("/password-reset", methods=['GET', 'POST'], defaults={'token': None})
|
||||||
|
@html.route("/password-reset/<token>", methods=['GET', 'POST'])
|
||||||
|
def reset_password(token):
|
||||||
|
if request.method == "GET" and not token:
|
||||||
|
return render_template("reset.html")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
token = request.form.get("token")
|
||||||
|
email = request.form.get("email")
|
||||||
|
|
||||||
|
if email:
|
||||||
|
return issue_password_reset(email)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return redirect("..")
|
||||||
|
|
||||||
|
user = User.query.filter(User.password_reset == token).first()
|
||||||
|
if not user:
|
||||||
|
return render_template("reset.html", errors="This link has expired.")
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
if user.password_reset_expires == None or user.password_reset_expires < datetime.now():
|
||||||
|
return render_template("reset.html", errors="This link has expired.")
|
||||||
|
if user.password_reset != token:
|
||||||
|
redirect("..")
|
||||||
|
return render_template("reset.html", token=token)
|
||||||
|
else:
|
||||||
|
if user.password_reset_expires == None or user.password_reset_expires < datetime.now():
|
||||||
|
abort(401)
|
||||||
|
if user.password_reset != token:
|
||||||
|
abort(401)
|
||||||
|
password = request.form.get('password')
|
||||||
|
if not password:
|
||||||
|
return render_template("reset.html", token=token, errors="You need to type a new password.")
|
||||||
|
user.set_password(password)
|
||||||
|
user.password_reset = None
|
||||||
|
user.password_reset_expires = None
|
||||||
|
db.commit()
|
||||||
|
login_user(user)
|
||||||
|
return redirect("../panel")
|
||||||
|
|
||||||
|
@html.route("/panel")
|
||||||
|
@loginrequired
|
||||||
|
def panel():
|
||||||
|
return render_template("panel.html",
|
||||||
|
one_times=lambda u: [d for d in u.donations if d.type == DonationType.one_time],
|
||||||
|
recurring=lambda u: [d for d in u.donations if d.type == DonationType.monthly and d.active],
|
||||||
|
currency=currency)
|
||||||
|
|
||||||
|
@html.route("/cancel/<id>")
|
||||||
|
@loginrequired
|
||||||
|
def cancel(id):
|
||||||
|
donation = Donation.query.filter(Donation.id == id).first()
|
||||||
|
if donation.user != current_user:
|
||||||
|
abort(401)
|
||||||
|
if donation.type != DonationType.monthly:
|
||||||
|
abort(400)
|
||||||
|
donation.active = False
|
||||||
|
db.commit()
|
||||||
|
send_cancellation_notice(current_user, donation)
|
||||||
|
return redirect("../panel")
|
||||||
|
|
||||||
|
@html.route("/invoice/<id>")
|
||||||
|
def invoice(id):
|
||||||
|
invoice = Invoice.query.filter(Invoice.external_id == id).first()
|
||||||
|
if not invoice:
|
||||||
|
abort(404)
|
||||||
|
return render_template("invoice.html", invoice=invoice)
|
|
@ -0,0 +1,103 @@
|
||||||
|
from flask import session, jsonify, redirect, request, Response, abort
|
||||||
|
from flask_login import current_user
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from functools import wraps
|
||||||
|
from fosspay.objects import User
|
||||||
|
from fosspay.database import db, Base
|
||||||
|
from fosspay.config import _cfg
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def firstparagraph(text):
|
||||||
|
try:
|
||||||
|
para = text.index("\n\n")
|
||||||
|
return text[:para + 2]
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
para = text.index("\r\n\r\n")
|
||||||
|
return text[:para + 4]
|
||||||
|
except:
|
||||||
|
return text
|
||||||
|
|
||||||
|
def with_session(f):
|
||||||
|
@wraps(f)
|
||||||
|
def go(*args, **kw):
|
||||||
|
try:
|
||||||
|
ret = f(*args, **kw)
|
||||||
|
db.commit()
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
db.rollback()
|
||||||
|
db.close()
|
||||||
|
raise
|
||||||
|
return go
|
||||||
|
|
||||||
|
def loginrequired(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not current_user:
|
||||||
|
return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
|
||||||
|
else:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def adminrequired(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not current_user:
|
||||||
|
return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
|
||||||
|
else:
|
||||||
|
if not current_user.admin:
|
||||||
|
abort(401)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def json_output(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
def jsonify_wrap(obj):
|
||||||
|
jsonification = json.dumps(obj)
|
||||||
|
return Response(jsonification, mimetype='application/json')
|
||||||
|
|
||||||
|
result = f(*args, **kwargs)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
return jsonify_wrap(result[0]), result[1]
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return jsonify_wrap(result)
|
||||||
|
if isinstance(result, list):
|
||||||
|
return jsonify_wrap(result)
|
||||||
|
|
||||||
|
# This is a fully fleshed out response, return it immediately
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def cors(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
res = f(*args, **kwargs)
|
||||||
|
if request.headers.get('x-cors-status', False):
|
||||||
|
if isinstance(res, tuple):
|
||||||
|
json_text = res[0].data
|
||||||
|
code = res[1]
|
||||||
|
else:
|
||||||
|
json_text = res.data
|
||||||
|
code = 200
|
||||||
|
|
||||||
|
o = json.loads(json_text)
|
||||||
|
o['x-status'] = code
|
||||||
|
|
||||||
|
return jsonify(o)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def file_link(path):
|
||||||
|
return _cfg("protocol") + "://" + _cfg("domain") + "/" + path
|
||||||
|
|
||||||
|
def disown_link(path):
|
||||||
|
return _cfg("protocol") + "://" + _cfg("domain") + "/disown?filename=" + path
|
|
@ -0,0 +1,33 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
try:
|
||||||
|
from configparser import ConfigParser
|
||||||
|
except ImportError:
|
||||||
|
# Python 2 support
|
||||||
|
from ConfigParser import ConfigParser
|
||||||
|
|
||||||
|
logger = logging.getLogger("fosspay")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
sh = logging.StreamHandler()
|
||||||
|
sh.setLevel(logging.DEBUG)
|
||||||
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
sh.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(sh)
|
||||||
|
|
||||||
|
# scss logger
|
||||||
|
logging.getLogger("scss").addHandler(sh)
|
||||||
|
|
||||||
|
env = 'dev'
|
||||||
|
config = None
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
global config
|
||||||
|
config = ConfigParser()
|
||||||
|
config.readfp(open('config.ini'))
|
||||||
|
|
||||||
|
load_config()
|
||||||
|
|
||||||
|
_cfg = lambda k: config.get(env, k)
|
||||||
|
_cfgi = lambda k: int(_cfg(k))
|
|
@ -0,0 +1,21 @@
|
||||||
|
from fosspay.config import _cfg
|
||||||
|
|
||||||
|
class Currency:
|
||||||
|
def __init__(self, symbol, position):
|
||||||
|
self.symbol = symbol
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def amount(self, amount):
|
||||||
|
if self.position == "right":
|
||||||
|
return amount + self.symbol
|
||||||
|
else:
|
||||||
|
return self.symbol + amount
|
||||||
|
|
||||||
|
currencies = {
|
||||||
|
'usd' : Currency("$", "left"),
|
||||||
|
'eur' : Currency("€", "right")
|
||||||
|
# ... More currencies can be added here
|
||||||
|
}
|
||||||
|
|
||||||
|
currency = currencies[_cfg("currency")]
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from .config import _cfg, _cfgi
|
||||||
|
engine = create_engine(_cfg('connection-string'))
|
||||||
|
db = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
Base.query = db.query_property()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
import fosspay.objects
|
||||||
|
Base.metadata.create_all(bind=engine)
|
|
@ -0,0 +1,122 @@
|
||||||
|
import smtplib
|
||||||
|
import os
|
||||||
|
import html.parser
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import localtime, format_datetime
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from flask import url_for
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
from fosspay.database import db
|
||||||
|
from fosspay.objects import User, DonationType
|
||||||
|
from fosspay.config import _cfg, _cfgi
|
||||||
|
from fosspay.currency import currency
|
||||||
|
|
||||||
|
def send_thank_you(user, amount, monthly):
|
||||||
|
if _cfg("smtp-host") == "":
|
||||||
|
return
|
||||||
|
smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port"))
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(_cfg("smtp-user"), _cfg("smtp-password"))
|
||||||
|
with open("emails/thank-you") as f:
|
||||||
|
tmpl = Template(f.read())
|
||||||
|
message = MIMEText(tmpl.substitute(**{
|
||||||
|
"root": _cfg("protocol") + "://" + _cfg("domain"),
|
||||||
|
"your_name": _cfg("your-name"),
|
||||||
|
"summary": ("Monthly donation" if monthly else "One-time donation"),
|
||||||
|
"amount": currency.amount("{:.2f}".format(amount / 100)),
|
||||||
|
"your_email": _cfg("your-email")
|
||||||
|
}))
|
||||||
|
message['Subject'] = "Thank you for your donation!"
|
||||||
|
message['From'] = _cfg("smtp-from")
|
||||||
|
message['To'] = user.email
|
||||||
|
message['Date'] = format_datetime(localtime())
|
||||||
|
smtp.sendmail(_cfg("smtp-from"), [ user.email ], message.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
def send_password_reset(user):
|
||||||
|
if _cfg("smtp-host") == "":
|
||||||
|
return
|
||||||
|
smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port"))
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(_cfg("smtp-user"), _cfg("smtp-password"))
|
||||||
|
with open("emails/reset-password") as f:
|
||||||
|
tmpl = Template(f.read())
|
||||||
|
message = MIMEText(tmpl.substitute(**{
|
||||||
|
"password_reset": user.password_reset,
|
||||||
|
"root": _cfg("protocol") + "://" + _cfg("domain"),
|
||||||
|
"your_name": _cfg("your-name"),
|
||||||
|
"your_email": _cfg("your-email")
|
||||||
|
}))
|
||||||
|
message['Subject'] = "Reset your donor password"
|
||||||
|
message['From'] = _cfg("smtp-from")
|
||||||
|
message['To'] = user.email
|
||||||
|
message['Date'] = format_datetime(localtime())
|
||||||
|
smtp.sendmail(_cfg("smtp-from"), [ user.email ], message.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
def send_declined(user, amount):
|
||||||
|
if _cfg("smtp-host") == "":
|
||||||
|
return
|
||||||
|
smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port"))
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(_cfg("smtp-user"), _cfg("smtp-password"))
|
||||||
|
with open("emails/declined") as f:
|
||||||
|
tmpl = Template(f.read())
|
||||||
|
message = MIMEText(tmpl.substitute(**{
|
||||||
|
"root": _cfg("protocol") + "://" + _cfg("domain"),
|
||||||
|
"your_name": _cfg("your-name"),
|
||||||
|
"amount": currency.amount("{:.2f}".format(amount / 100))
|
||||||
|
}))
|
||||||
|
message['Subject'] = "Your monthly donation was declined."
|
||||||
|
message['From'] = _cfg("smtp-from")
|
||||||
|
message['To'] = user.email
|
||||||
|
message['Date'] = format_datetime(localtime())
|
||||||
|
smtp.sendmail(_cfg("smtp-from"), [ user.email ], message.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
def send_new_donation(user, donation):
|
||||||
|
if _cfg("smtp-host") == "":
|
||||||
|
return
|
||||||
|
smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port"))
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(_cfg("smtp-user"), _cfg("smtp-password"))
|
||||||
|
with open("emails/new_donation") as f:
|
||||||
|
tmpl = Template(f.read())
|
||||||
|
message = MIMEText(tmpl.substitute(**{
|
||||||
|
"email": user.email,
|
||||||
|
"your_name": _cfg("your-name"),
|
||||||
|
"amount": currency.amount("{:.2f}".format(
|
||||||
|
donation.amount / 100)),
|
||||||
|
"frequency": (" per month"
|
||||||
|
if donation.type == DonationType.monthly else ""),
|
||||||
|
"comment": donation.comment or "",
|
||||||
|
}))
|
||||||
|
message['Subject'] = "New donation on fosspay!"
|
||||||
|
message['From'] = _cfg("smtp-from")
|
||||||
|
message['To'] = f"{_cfg('your-name')} <{_cfg('your-email')}>"
|
||||||
|
message['Date'] = format_datetime(localtime())
|
||||||
|
smtp.sendmail(_cfg("smtp-from"), [ _cfg('your-email') ], message.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
def send_cancellation_notice(user, donation):
|
||||||
|
if _cfg("smtp-host") == "":
|
||||||
|
return
|
||||||
|
smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port"))
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(_cfg("smtp-user"), _cfg("smtp-password"))
|
||||||
|
with open("emails/cancelled") as f:
|
||||||
|
tmpl = Template(f.read())
|
||||||
|
message = MIMEText(tmpl.substitute(**{
|
||||||
|
"email": user.email,
|
||||||
|
"root": _cfg("protocol") + "://" + _cfg("domain"),
|
||||||
|
"your_name": _cfg("your-name"),
|
||||||
|
"amount": currency.amount("{:.2f}".format(
|
||||||
|
donation.amount / 100)),
|
||||||
|
}))
|
||||||
|
message['Subject'] = "A monthly donation on fosspay has been cancelled"
|
||||||
|
message['From'] = _cfg("smtp-from")
|
||||||
|
message['To'] = f"{_cfg('your-name')} <{_cfg('your-email')}>"
|
||||||
|
message['Date'] = format_datetime(localtime())
|
||||||
|
smtp.sendmail(_cfg("smtp-from"), [ _cfg('your-email') ], message.as_string())
|
||||||
|
smtp.quit()
|
|
@ -0,0 +1,19 @@
|
||||||
|
def makeMask(n):
|
||||||
|
"return a mask of n bits as a long integer"
|
||||||
|
return (2 << n - 1) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def dottedQuadToNum(ip):
|
||||||
|
"convert decimal dotted quad string to long integer"
|
||||||
|
parts = ip.split(".")
|
||||||
|
return int(parts[0]) | (int(parts[1]) << 8) | (int(parts[2]) << 16) | (int(parts[3]) << 24)
|
||||||
|
|
||||||
|
|
||||||
|
def networkMask(ip, bits):
|
||||||
|
"Convert a network address to a long integer"
|
||||||
|
return dottedQuadToNum(ip) & makeMask(bits)
|
||||||
|
|
||||||
|
|
||||||
|
def addressInNetwork(ip, net):
|
||||||
|
"Is an address in a network"
|
||||||
|
return ip & net == net
|
|
@ -0,0 +1,114 @@
|
||||||
|
from sqlalchemy import Column, Integer, String, Unicode, Boolean, DateTime
|
||||||
|
from sqlalchemy import ForeignKey, Table, UnicodeText, Text, text
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
from sqlalchemy_utils import ChoiceType
|
||||||
|
|
||||||
|
from .database import Base
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
import bcrypt
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
class DonationType(Enum):
|
||||||
|
one_time = "one_time"
|
||||||
|
monthly = "monthly"
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
email = Column(String(256), nullable=False, index=True)
|
||||||
|
admin = Column(Boolean())
|
||||||
|
password = Column(String)
|
||||||
|
created = Column(DateTime)
|
||||||
|
password_reset = Column(String(128))
|
||||||
|
password_reset_expires = Column(DateTime)
|
||||||
|
stripe_customer = Column(String(256))
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password = bcrypt.hashpw(password.encode("utf-8"),
|
||||||
|
bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
def __init__(self, email, password):
|
||||||
|
self.email = email
|
||||||
|
self.admin = False
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.set_password(password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<User {}>".format(self.email)
|
||||||
|
|
||||||
|
# Flask.Login stuff
|
||||||
|
# We don't use most of these features
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
def is_active(self):
|
||||||
|
return True
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
def get_id(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
class Donation(Base):
|
||||||
|
__tablename__ = 'donations'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
user = relationship("User", backref=backref("donations"))
|
||||||
|
project_id = Column(Integer, ForeignKey("projects.id"))
|
||||||
|
project = relationship("Project", backref=backref("donations"))
|
||||||
|
type = Column(ChoiceType(DonationType, impl=String()))
|
||||||
|
amount = Column(Integer, nullable=False)
|
||||||
|
created = Column(DateTime, nullable=False)
|
||||||
|
updated = Column(DateTime, nullable=False)
|
||||||
|
comment = Column(String(512))
|
||||||
|
active = Column(Boolean)
|
||||||
|
payments = Column(Integer)
|
||||||
|
hidden = Column(Boolean, server_default='f', nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, user, type, amount, project=None, comment=None):
|
||||||
|
self.user = user
|
||||||
|
self.type = type
|
||||||
|
self.amount = amount
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.updated = datetime.now()
|
||||||
|
self.emailed_about = False
|
||||||
|
self.comment = comment
|
||||||
|
self.active = True
|
||||||
|
self.payments = 1
|
||||||
|
if project:
|
||||||
|
self.project_id = project.id
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Donation {} from {}: ${} ({})>".format(
|
||||||
|
self.id,
|
||||||
|
self.user.email,
|
||||||
|
"{:.2f}".format(self.amount / 100),
|
||||||
|
self.type
|
||||||
|
)
|
||||||
|
|
||||||
|
class Project(Base):
|
||||||
|
__tablename__ = 'projects'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
created = Column(DateTime, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.created = datetime.now()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Project {} {}>".format(self.id, self.name)
|
||||||
|
|
||||||
|
class Invoice(Base):
|
||||||
|
__tablename__ = 'invoices'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
created = Column(DateTime, nullable=False)
|
||||||
|
external_id = Column(String(16), index=True)
|
||||||
|
amount = Column(Integer, nullable=False)
|
||||||
|
comment = Column(String(512), nullable=False)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.external_id = binascii.hexlify(os.urandom(8)).decode()
|
||||||
|
self.created = datetime.now()
|
|
@ -0,0 +1,6 @@
|
||||||
|
from fosspay.config import _cfg
|
||||||
|
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
if _cfg("stripe-secret") != "":
|
||||||
|
stripe.api_key = _cfg("stripe-secret")
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from fosspay.database import db
|
||||||
|
from fosspay.objects import Invoice
|
||||||
|
from fosspay.config import _cfg
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(f"Usage: {sys.argv[0]} <amount in cents> <comment>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
amount = int(sys.argv[1])
|
||||||
|
comment = sys.argv[2]
|
||||||
|
|
||||||
|
invoice = Invoice()
|
||||||
|
invoice.amount = amount
|
||||||
|
invoice.comment = comment
|
||||||
|
db.add(invoice)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f"{_cfg('protocol')}://{_cfg('domain')}/invoice/{invoice.external_id}")
|
|
@ -0,0 +1,11 @@
|
||||||
|
stripe
|
||||||
|
Flask
|
||||||
|
Jinja2
|
||||||
|
Flask-Misaka
|
||||||
|
Flask-Login
|
||||||
|
psycopg2
|
||||||
|
requests
|
||||||
|
bcrypt
|
||||||
|
gunicorn
|
||||||
|
SQLAlchemy
|
||||||
|
SQLAlchemy-Utils
|
|
@ -0,0 +1,122 @@
|
||||||
|
(function() {
|
||||||
|
var donation = {
|
||||||
|
type: window.default_type,
|
||||||
|
amount: window.default_amount * 100, // cents
|
||||||
|
project: null,
|
||||||
|
comment: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectAmount(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector(".amounts .active").classList.remove("active");
|
||||||
|
e.target.classList.add("active");
|
||||||
|
var custom = document.querySelector("#custom-amount");
|
||||||
|
var amount = e.target.dataset.amount;
|
||||||
|
if (amount === "custom") {
|
||||||
|
custom.classList.remove("hidden");
|
||||||
|
donation.amount = +document.querySelector("#custom-amount-text").value * 100;
|
||||||
|
} else {
|
||||||
|
custom.classList.add("hidden");
|
||||||
|
donation.amount = +e.target.dataset.amount * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFrequency(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector(".frequencies .active").classList.remove("active");
|
||||||
|
e.target.classList.add("active");
|
||||||
|
donation.type = e.target.dataset.frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
var amounts = document.querySelectorAll(".amounts button");
|
||||||
|
for (var i = 0; i < amounts.length; i++) {
|
||||||
|
amounts[i].addEventListener("click", selectAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
var frequencies = document.querySelectorAll(".frequencies button");
|
||||||
|
for (var i = 0; i < frequencies.length; i++) {
|
||||||
|
frequencies[i].addEventListener("click", selectFrequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("custom-amount-text").addEventListener("change", function(e) {
|
||||||
|
var value = +e.target.value;
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = 1;
|
||||||
|
}
|
||||||
|
if (value <= 0) {
|
||||||
|
value = 1;
|
||||||
|
}
|
||||||
|
e.target.value = value;
|
||||||
|
donation.amount = value * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
var project = document.getElementById("project")
|
||||||
|
if (project) {
|
||||||
|
project.addEventListener("change", function(e) {
|
||||||
|
if (e.target.value === "null") {
|
||||||
|
donation.project = null;
|
||||||
|
} else {
|
||||||
|
donation.project = e.target.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("donate-button").addEventListener("click", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.getAttribute("disabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
donation.comment = document.getElementById("comments").value;
|
||||||
|
|
||||||
|
var handler = StripeCheckout.configure({
|
||||||
|
name: your_name,
|
||||||
|
email: window.email,
|
||||||
|
key: window.stripe_key,
|
||||||
|
image: window.avatar,
|
||||||
|
locale: 'auto',
|
||||||
|
description: donation.type == "monthly" ? i18n["Monthly Donation"] : i18n["One-time Donation"],
|
||||||
|
panelLabel: i18n["Donate "] + "{{amount}}",
|
||||||
|
amount: donation.amount,
|
||||||
|
currency: currency,
|
||||||
|
token: function(token) {
|
||||||
|
e.target.setAttribute("disabled", "");
|
||||||
|
e.target.textContent = i18n["Submitting..."];
|
||||||
|
|
||||||
|
var data = new FormData();
|
||||||
|
data.append("stripe_token", token.id);
|
||||||
|
data.append("email", token.email);
|
||||||
|
data.append("amount", donation.amount);
|
||||||
|
data.append("type", donation.type);
|
||||||
|
if (donation.comment !== null) {
|
||||||
|
data.append("comment", donation.comment);
|
||||||
|
}
|
||||||
|
if (donation.project !== null) {
|
||||||
|
data.append("project", donation.project);
|
||||||
|
}
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "donate");
|
||||||
|
xhr.onload = function() {
|
||||||
|
var res = JSON.parse(this.responseText);
|
||||||
|
if (res.success) {
|
||||||
|
document.getElementById("donation-stuff").classList.add("hidden");
|
||||||
|
document.getElementById("thanks").classList.remove("hidden");
|
||||||
|
if (res.new_account) {
|
||||||
|
document.getElementById("new-donor-password").classList.remove("hidden");
|
||||||
|
document.getElementById("reset-token").value = res.password_reset;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var errors = document.getElementById("errors");
|
||||||
|
errors.classList.remove("hidden");
|
||||||
|
errors.querySelector("p").textContent = res.reason;
|
||||||
|
e.target.removeAttribute("disabled");
|
||||||
|
e.target.textContent = i18n["Donate"];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.open();
|
||||||
|
});
|
||||||
|
})();
|
|
@ -0,0 +1,47 @@
|
||||||
|
(function() {
|
||||||
|
document.getElementById("submit").addEventListener("click", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.getAttribute("disabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler = StripeCheckout.configure({
|
||||||
|
name: your_name,
|
||||||
|
key: window.stripe_key,
|
||||||
|
locale: 'auto',
|
||||||
|
description: "Invoice " + invoice,
|
||||||
|
panelLabel: "Pay {{amount}}",
|
||||||
|
amount: amount,
|
||||||
|
token: function(token) {
|
||||||
|
e.target.setAttribute("disabled", "");
|
||||||
|
e.target.textContent = "Submitting...";
|
||||||
|
|
||||||
|
var data = new FormData();
|
||||||
|
data.append("stripe_token", token.id);
|
||||||
|
data.append("email", token.email);
|
||||||
|
data.append("amount", amount);
|
||||||
|
data.append("type", "once");
|
||||||
|
data.append("comment", comment);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "../donate");
|
||||||
|
xhr.onload = function() {
|
||||||
|
var res = JSON.parse(this.responseText);
|
||||||
|
if (res.success) {
|
||||||
|
document.getElementById("donation-stuff").classList.add("hidden");
|
||||||
|
document.getElementById("thanks").classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
var errors = document.getElementById("errors");
|
||||||
|
errors.classList.remove("hidden");
|
||||||
|
errors.querySelector("p").textContent = res.reason;
|
||||||
|
e.target.removeAttribute("disabled");
|
||||||
|
e.target.textContent = "Submit payment";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handler.open();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}
|
||||||
|
<title>Donation Admin</title>
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<p class="pull-right">
|
||||||
|
<a class="btn btn-primary" href="#" data-toggle="modal" data-target="#donation-button-modal">
|
||||||
|
Get donation button
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-default" href="logout">Log out</a>
|
||||||
|
</p>
|
||||||
|
<h1>Donation Admin</h1>
|
||||||
|
<p>Combine this with your <a href="https://dashboard.stripe.com">Stripe
|
||||||
|
dashboard</a> for the full effect.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
{% if first %}
|
||||||
|
<div class="well">
|
||||||
|
<p>
|
||||||
|
You're set up and ready to go! This is your admin panel. Next steps:
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<strong>Set up a cron job to handle monthly donations.</strong>
|
||||||
|
<a href="https://github.com/ddevault/fosspay/wiki/Recurring-donations-cronjob">
|
||||||
|
Relevant documentation
|
||||||
|
</a>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Add some projects. Donors can tell you which project they want to support
|
||||||
|
when they donate.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Customize the look & feel. Look at the contents of the <code>templates</code>
|
||||||
|
directory - you can copy and paste any of these templates into the
|
||||||
|
<code>overrides</code> directory and change it to suit your needs.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://drewdevault.com/donate?project=fosspay">Donate to fosspay upstream</a>?
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.sr.ht/~sircmpwn/fosspay">Contribute code to fosspay upstream</a>?
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<h2>Projects</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 10%"></th>
|
||||||
|
<th>Project Name</th>
|
||||||
|
<th>One-time</th>
|
||||||
|
<th>Recurring (active)</th>
|
||||||
|
<th>Recurring (total paid)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for project in projects %}
|
||||||
|
<tr>
|
||||||
|
<td><button type="button" class="close">×</button></td>
|
||||||
|
<td>{{ project.name }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(one_times(project) / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(recurring(project) / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(recurring_ever(project) / 100)) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>(not specified)</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(unspecified_one_times / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(unspecified_recurring / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(unspecified_recurring_ever / 100)) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><strong>Total</strong></td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(total_one_time / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(total_recurring / 100)) }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(total_recurring_ever / 100)) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 well">
|
||||||
|
<h4>Add Project</h4>
|
||||||
|
<p>Donors will not be given a choice of project unless you have at least 2.</p>
|
||||||
|
<form method="POST" action="{{root}}/create-project">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" type="text" placeholder="Project name" name="name" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Add" class="btn btn-default pull-right" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Donations <small>50 most recent</small></h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="min-width: 10rem">Date</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Project</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Payments</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for donation in donations %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ donation.created.strftime("%Y-%m-%d") }}</td>
|
||||||
|
<td><a href="mailto:{{ donation.user.email }}">{{ donation.user.email }}</a></td>
|
||||||
|
<td>{{ donation.project.name if donation.project else "" }}</td>
|
||||||
|
<td title="{{ donation.comment }}">{{ donation.comment if donation.comment else "" }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(donation.amount / 100)) }}</td>
|
||||||
|
<td>
|
||||||
|
{{ "Once" if str(donation.type) == "DonationType.one_time" else "Monthly" }}
|
||||||
|
{{ "(cancelled)" if not donation.active else "" }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{donation.payments}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="donation-button-modal" tabindex="-1" role="dialog" aria-labelledby="donation-modal-label">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title" id="donation-modal-label">Donation buttons</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
You can include a donation button in various places to
|
||||||
|
drive people to your donation page. Here's how it looks:
|
||||||
|
<a href="{{root}}">
|
||||||
|
<img src="{{root}}/static/donate-with-fosspay.png" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>If you add <code>?project=1</code> to your URL, it will pre-select that project
|
||||||
|
(where 1 is the 1st project you have listed on this page) when users arrive to donate.</p>
|
||||||
|
<p><strong>Markdown</strong></p>
|
||||||
|
<pre>[]({{root}})</pre>
|
||||||
|
<p><strong>HTML</strong></p>
|
||||||
|
<pre><a href="{{root}}"><img src="{{root}}/static/donate-with-fosspay.png" alt="Donate with fosspay" /></a></pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{#
|
||||||
|
This is a little blurb you can write to explain why your goal is set to what
|
||||||
|
it is.
|
||||||
|
#}
|
|
@ -0,0 +1,117 @@
|
||||||
|
{% if _cfg("public-income") == "yes" %}
|
||||||
|
<div class="container" style="padding-top: 5rem">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
{% set total_sum = recurring_sum + patreon_sum + lp_sum + gh_sum %}
|
||||||
|
{% set total_count = recurring_count + patreon_count + lp_count + gh_count %}
|
||||||
|
{% if _cfg("goal") %}
|
||||||
|
{% set goal = int(_cfg("goal")) %}
|
||||||
|
|
||||||
|
{% if goal < total_sum %}
|
||||||
|
{# Make the graph still make sense if we exceeded the goal #}
|
||||||
|
{% set adjusted_goal = total_sum %}
|
||||||
|
{% else %}
|
||||||
|
{% set adjusted_goal = goal %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set recurring_progress = recurring_sum / adjusted_goal %}
|
||||||
|
{% set patreon_progress = patreon_sum / adjusted_goal %}
|
||||||
|
{% set lp_progress = lp_sum / adjusted_goal %}
|
||||||
|
{% set gh_progress = gh_sum / adjusted_goal %}
|
||||||
|
{% set progress = total_sum / goal %}
|
||||||
|
<div class="progress" style="height: 3rem">
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-primary"
|
||||||
|
style="width: {{ recurring_progress * 100 }}%; line-height: 2.5"
|
||||||
|
>
|
||||||
|
<span>{{ currency.amount("{:.0f}".format(recurring_sum / 100)) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-info"
|
||||||
|
style="width: {{ patreon_progress * 100 }}%; line-height: 2.5"
|
||||||
|
>
|
||||||
|
<span>{{ currency.amount("{:.0f}".format(patreon_sum / 100)) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-warning"
|
||||||
|
style="width: {{ lp_progress * 100 }}%; line-height: 2.5"
|
||||||
|
>
|
||||||
|
<span>{{ currency.amount("{:.0f}".format(lp_sum / 100)) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-primary"
|
||||||
|
style="width: {{ gh_progress * 100 }}%; line-height: 2.5"
|
||||||
|
>
|
||||||
|
<span>{{ currency.amount("{:.0f}".format(gh_sum / 100)) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
{% if patreon_count or lp_count %}
|
||||||
|
<p>
|
||||||
|
{{ currency.amount("{:.2f}".format(recurring_sum / 100)) }}/mo
|
||||||
|
via <strong class="text-primary">{{ domain }}</strong>
|
||||||
|
({{ recurring_count }} supporter{{ "s" if recurring_count != 1 else "" }})
|
||||||
|
</p>
|
||||||
|
{% if patreon_count %}
|
||||||
|
<p>
|
||||||
|
{{ currency.amount("{:.2f}".format(patreon_sum / 100)) }}/mo
|
||||||
|
via
|
||||||
|
<strong><a
|
||||||
|
href="https://patreon.com/{{ _cfg("patreon-campaign") }}"
|
||||||
|
style="color: #51acc7">
|
||||||
|
Patreon <i class="glyphicon glyphicon-share"></i>
|
||||||
|
</a></strong> ({{ patreon_count }} supporter{{ "s" if patreon_count != 1 else "" }})
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if lp_count %}
|
||||||
|
<p>
|
||||||
|
{{ currency.amount("{:.2f}".format(lp_sum / 100)) }}/mo
|
||||||
|
via
|
||||||
|
<strong><a
|
||||||
|
class="text-warning"
|
||||||
|
href="https://liberapay.com/{{ _cfg("liberapay-campaign") }}">
|
||||||
|
Liberapay <i class="glyphicon glyphicon-share"></i>
|
||||||
|
</a></strong> ({{ lp_count }} supporter{{ "s" if lp_count != 1 else "" }})
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if gh_count %}
|
||||||
|
<p>
|
||||||
|
{{ currency.amount("{:.2f}".format(gh_sum / 100)) }}/mo
|
||||||
|
via
|
||||||
|
<strong><a
|
||||||
|
class="text-primary"
|
||||||
|
href="https://github.com/{{gh_user}}">
|
||||||
|
GitHub <i class="glyphicon glyphicon-share"></i>
|
||||||
|
</a></strong> ({{ gh_count }} supporter{{ "s" if lp_count != 1 else "" }})
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if goal %}
|
||||||
|
<p class="{{ "text-center" if not patreon_sum else "" }}">
|
||||||
|
{{ currency.amount("{:.2f}".format(total_sum / 100))}}/mo
|
||||||
|
of
|
||||||
|
{{ currency.amount("{:.2f}".format(goal / 100)) }}/mo
|
||||||
|
goal
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
Supported with {{ currency.amount("{:.2f}".format(total_sum / 100)) }}
|
||||||
|
from {{ total_count }} supporters!
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
{% include "goal-summary.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,160 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
window.stripe_key = "{{ _cfg("stripe-publish") }}";
|
||||||
|
window.avatar = "{{ avatar }}";
|
||||||
|
window.your_name = "{{ _cfg("your-name") }}";
|
||||||
|
window.default_amount = {{ _cfg("default-amount") }};
|
||||||
|
window.default_type = "{{ _cfg("default-type") }}";
|
||||||
|
|
||||||
|
// Array used for translation of index.js sentences. See contrib/fr/overrides/index.html for example use
|
||||||
|
const i18n = {
|
||||||
|
"Monthly Donation": "Monthly Donation",
|
||||||
|
"One-time Donation": "One-time Donation",
|
||||||
|
"Donate ": "Donate ",
|
||||||
|
"Submitting...": "Submitting...",
|
||||||
|
"Donate": "Donate"
|
||||||
|
};
|
||||||
|
// End of translation of index.js
|
||||||
|
|
||||||
|
const currency = "{{ _cfg("currency") }}";
|
||||||
|
|
||||||
|
{% if user %}
|
||||||
|
window.email = "{{user.email}}";
|
||||||
|
{% endif %}
|
||||||
|
</script>
|
||||||
|
<script src="//checkout.stripe.com/checkout.js"></script>
|
||||||
|
<script src="static/index.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<img
|
||||||
|
class="pull-right"
|
||||||
|
src="{{ avatar }}?s=129"
|
||||||
|
style="border-radius: 5px; margin-left: 1rem"
|
||||||
|
width="128" height="128" />
|
||||||
|
<h1>Donate to {{ _cfg("your-name") }}</h1>
|
||||||
|
{% include "summary.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>This page requires Javascript. It's necessary to send your credit card number to
|
||||||
|
<a href="https://stripe.com/">Stripe</a> directly, so you don't need to trust me with it.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div class="container text-center hidden" id="thanks">
|
||||||
|
{% include "post-donation-message.html" %}
|
||||||
|
<form id="new-donor-password" class="hidden" action="{{root}}/password-reset" method="POST">
|
||||||
|
<p>Set a password now if you want to manage your donations later:</p>
|
||||||
|
<input type="password" placeholder="Password" name="password" />
|
||||||
|
<input type="hidden" name="token" id="reset-token" />
|
||||||
|
<button class="btn btn-primary btn-sm">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="container text-center" id="donation-stuff">
|
||||||
|
<h3>How much?</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<div class="btn-group btn-group-justified amounts" role="group" aria-label="...">
|
||||||
|
{% for amt in _cfg("default-amounts").split(" ") %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button data-amount="{{ amt }}" type="button"
|
||||||
|
class="btn btn-default {{"active" if _cfg("default-amount") == amt else ""}}"
|
||||||
|
>{{ currency.amount(amt) }}</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button data-amount="custom" type="button" class="btn btn-default">Custom</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row hidden" id="custom-amount" style="margin-top: 20px;">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon">{{ currency.symbol }}</span>
|
||||||
|
<input id="custom-amount-text" type="text" value="13.37"
|
||||||
|
class="form-control" placeholder="Amount" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>How often?</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="btn-group btn-group-justified frequencies" role="group" aria-label="...">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button data-frequency="once" type="button"
|
||||||
|
class="btn btn-default {{"active" if _cfg("default-type")=="once" else ""}}"
|
||||||
|
>Once</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button data-frequency="monthly" type="button"
|
||||||
|
class="btn btn-default {{"active" if _cfg("default-type")=="monthly" else ""}}"
|
||||||
|
>Monthly</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if len(projects) > 1 %}
|
||||||
|
<h3>What project?</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<select id="project" name="project" class="form-control">
|
||||||
|
<option value="null"
|
||||||
|
{{ "selected" if selected_project == None else "" }}>None in particular</option>
|
||||||
|
{% for project in projects %}
|
||||||
|
<option value="{{ project.id }}"
|
||||||
|
{{ "selected" if selected_project == project.id else "" }}>{{ project.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="comments" class="form-control" placeholder="Any comments?" maxlength="512" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="alert alert-danger hidden" id="errors">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-block btn-success" id="donate-button">Donate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "goal.html" %}
|
||||||
|
<hr />
|
||||||
|
<div class="container text-center">
|
||||||
|
{% if not user %}
|
||||||
|
<p>
|
||||||
|
<small class="text-muted">
|
||||||
|
Been here before? <a href="login">Log in</a> to view your donation
|
||||||
|
history, edit recurring donations, and so on.
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<small class="text-muted">
|
||||||
|
Powered by <a href="https://git.sr.ht/~sircmpwn/fosspay">fosspay</a>.
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block container %}
|
||||||
|
<h1>500 Internal Error</h1>
|
||||||
|
<p><a href="/">Trying to donate?</a></p>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
window.stripe_key = "{{ _cfg("stripe-publish") }}";
|
||||||
|
window.your_name = "{{ _cfg("your-name") }}";
|
||||||
|
window.amount = {{invoice.amount}};
|
||||||
|
window.invoice = "{{invoice.external_id}}";
|
||||||
|
window.comment = "{{invoice.comment}}";
|
||||||
|
const currency = "{{ _cfg("currency") }}";
|
||||||
|
</script>
|
||||||
|
<script src="//checkout.stripe.com/checkout.js"></script>
|
||||||
|
<script src="../static/invoice.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<h1>Invoice to {{ _cfg("your-name") }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>This page requires Javascript. It's necessary to send your credit card number to
|
||||||
|
<a href="https://stripe.com/">Stripe</a> directly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div class="container text-center hidden" id="thanks">
|
||||||
|
<p>Thank you for your payment - it has been processed successfully.</p>
|
||||||
|
</div>
|
||||||
|
<div class="container text-center" id="donation-stuff">
|
||||||
|
<h3>Invoice for ${{"{:.2f}".format(invoice.amount / 100)}}</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<p>
|
||||||
|
{{invoice.comment}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="alert alert-danger hidden" id="errors"><p></p></div>
|
||||||
|
<button class="btn btn-block btn-success" id="submit">
|
||||||
|
Submit Payment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container text-center">
|
||||||
|
<p>
|
||||||
|
<small class="text-muted">
|
||||||
|
Powered by <a href="https://github.com/ddevault/fosspay">fosspay</a>.
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="static/icon.png" type="image/png" />
|
||||||
|
{% block title %}
|
||||||
|
<title>Donate to {{_cfg("your-name")}}</title>
|
||||||
|
{% endblock %}
|
||||||
|
<link rel="stylesheet" href="{{root}}/static/club.css" />
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
{% block container %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
<script src="{{root}}/static/jquery/jquery.min.js"></script>
|
||||||
|
<script src="{{root}}/static/bootstrap/bootstrap.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Donate to {{ _cfg("your-name") }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<h1>Log In</h1>
|
||||||
|
{% if errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>
|
||||||
|
Username or password incorrect.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form action="{{root}}/login" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" type="text" name="email" placeholder="your@example.org" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" type="password" name="password" placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Log in" class="btn btn-primary" />
|
||||||
|
<a style="margin-left: 20px" href="password-reset">Reset password</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block container %}
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
<p><a href="/">Trying to donate?</a></p>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,62 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<p class="pull-right">
|
||||||
|
<a class="btn btn-primary" href="..">Donate again</a>
|
||||||
|
<a class="btn btn-default" href="logout">Log out</a>
|
||||||
|
</p>
|
||||||
|
<h1>Your Donations</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
{% if any(recurring(user)) %}
|
||||||
|
<h2>Monthly Donations</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 10%"></th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Project</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for donation in recurring(user) %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<form method="DELETE" action="{{root}}/cancel/{{ donation.id }}">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>{{ donation.created.strftime("%Y-%m-%d") }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(donation.amount / 100)) }}</td>
|
||||||
|
<td>{{ donation.project.name if donation.project else "Not specified" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% if any(one_times(user)) %}
|
||||||
|
<h2>One-time Donations</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Project</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for donation in one_times(user) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ donation.created.strftime("%Y-%m-%d") }}</td>
|
||||||
|
<td>{{ currency.amount("{:.2f}".format(donation.amount / 100)) }}</td>
|
||||||
|
<td>{{ donation.project.name if donation.project else "Not specified" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<h3>Thanks!</h3>
|
||||||
|
<p>Have a great day!</p>
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="well">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Donate to {{ _cfg("your-name") }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<h1>Reset Password</h1>
|
||||||
|
{% if errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>
|
||||||
|
{{ errors }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if done %}
|
||||||
|
<p>
|
||||||
|
An email should arrive shortly. If you need help, contact
|
||||||
|
<a href="mailto:{{_cfg("your-email")}}">{{_cfg("your-email")}}</a>.
|
||||||
|
</p>
|
||||||
|
{% elif token %}
|
||||||
|
<form action="{{root}}/password-reset" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" type="password" name="password" placeholder="New password" />
|
||||||
|
<input type="hidden" name="token" value="{{ token }}" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" class="btn btn-primary" />
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="{{root}}/password-reset" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" type="text" name="email" placeholder="your@example.org" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" class="btn btn-primary" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,87 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block container %}
|
||||||
|
<h1>FossPay Setup</h1>
|
||||||
|
<p>Congrats! You have FossPay up and running.</p>
|
||||||
|
|
||||||
|
<h2>config.ini</h2>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>
|
||||||
|
{% if _cfg("secret-key") == "hello world" %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
You need to change the secret key to something other than "hello world".
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Your secret key looks good.
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if _cfg("domain") == "localhost:5000" %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
You should change your domain to something other than localhost.
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Your domain is set to "{{_cfg("domain")}}".
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if _cfg("protocol") != "https" %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
Stripe requires your website to use HTTPS.
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Stripe requires your website to use HTTPS.
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if not _cfg("smtp-host") %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
You should configure an SMTP server to send emails with.
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Your email configuration looks good.
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if not _cfg("stripe-secret") or not _cfg("stripe-publish") %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
Your Stripe API keys are not in your config file.
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Your Stripe API keys look good.
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% if not _cfg("patreon-access-token") or not _cfg("patreon-campaign") %}
|
||||||
|
<span class="glyphicon glyphicon-remove text-danger"></span>
|
||||||
|
Your Patreon access token and campaign are not configured (optional).
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-ok text-success"></span>
|
||||||
|
Your Patreon integration looks good. We'll integrate with
|
||||||
|
<a
|
||||||
|
href="https://patreon.com/{{ _cfg("patreon-campaign") }}"
|
||||||
|
target="_blank" rel="noopener noreferrer"
|
||||||
|
>{{ _cfg("patreon-campaign") }}</a>'s campaign.
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>You can make changes and refresh this page if you like.</p>
|
||||||
|
|
||||||
|
<h2>Admin Account</h2>
|
||||||
|
<p>Enter your details for the admin account:</p>
|
||||||
|
<form class="form" action="{{root}}/setup" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" name="email"
|
||||||
|
placeholder="Email" value="{{_cfg("your-email")}}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" class="form-control" name="password" placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Continue" class="btn btn-primary" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{#
|
||||||
|
Try to keep this text short. Too long and it distracts from
|
||||||
|
the donation UI and will reduce the number of conversions you
|
||||||
|
actually get.
|
||||||
|
#}
|
||||||
|
<p>Your donation here supports <a href="https://tilde.club">tilde.club</a>, and any other services that are run by tilde.club.</p>
|
||||||
|
|
||||||
|
<p> I pay for hosting and domain costs out of pocket and spend far more time than I should running these services. Anything you can pitch in helps keep everything up and running.</p>
|
||||||
|
|
||||||
|
<p>My hosting costs are around $200/month and domains are around $150/year.</p>
|
||||||
|
|
||||||
|
<p>Donations are securely processed through Stripe, and your payment information is not stored on my servers. I am charged a 2.9%+30c fee per transaction.</p>
|
||||||
|
|
Loading…
Reference in New Issue