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