Source incoming :)

This commit is contained in:
deepend 2021-12-12 07:06:51 +00:00
parent 415f50ce1e
commit 907bf2206f
50 changed files with 2918 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
*.pyc
bin/
config.ini
alembic.ini
include/
local/
lib/
static/
*.swp
*.rdb
storage/
pip-selfcheck.json
.sass-cache/
overrides/

19
LICENSE Normal file
View File

@ -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.

45
Makefile Normal file
View File

@ -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

5
_static/bootstrap/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
_static/bootstrap/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
_static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

6
_static/jquery/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

16
app.py Executable file
View File

@ -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)

75
config.ini.example Normal file
View File

@ -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)

15
contrib/fosspay.service Normal file
View File

@ -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

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

24
contrib/nginx.conf Normal file
View File

@ -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/;
}
}

78
cronjob.py Executable file
View File

@ -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)

3
emails/cancelled Normal file
View File

@ -0,0 +1,3 @@
Hi $your_name!
Unfortunately, $email has chosen to cancel their monthly donation of $amount.

12
emails/declined Normal file
View File

@ -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

5
emails/new_donation Normal file
View File

@ -0,0 +1,5 @@
Hi $your_name!
Good news: $email just donated $amount$frequency!
$comment

13
emails/reset-password Normal file
View File

@ -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

14
emails/thank-you Normal file
View File

@ -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

80
fosspay/app.py Normal file
View File

@ -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
}

684
fosspay/blacklist.py Normal file
View File

@ -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",
]

345
fosspay/blueprints/html.py Normal file
View File

@ -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)

103
fosspay/common.py Normal file
View File

@ -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

33
fosspay/config.py Normal file
View File

@ -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))

21
fosspay/currency.py Normal file
View File

@ -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")]

14
fosspay/database.py Normal file
View File

@ -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)

122
fosspay/email.py Normal file
View File

@ -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()

19
fosspay/network.py Normal file
View File

@ -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

114
fosspay/objects.py Normal file
View File

@ -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()

6
fosspay/stripe.py Normal file
View File

@ -0,0 +1,6 @@
from fosspay.config import _cfg
import stripe
if _cfg("stripe-secret") != "":
stripe.api_key = _cfg("stripe-secret")

20
invoice Executable file
View File

@ -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}")

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
stripe
Flask
Jinja2
Flask-Misaka
Flask-Login
psycopg2
requests
bcrypt
gunicorn
SQLAlchemy
SQLAlchemy-Utils

122
scripts/index.js Normal file
View File

@ -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();
});
})();

47
scripts/invoice.js Normal file
View File

@ -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();
});
})();

167
templates/admin.html Normal file
View File

@ -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 &amp; 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">&times;</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">&times;</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>[![Donate with fosspay]({{root}}/static/donate-with-fosspay.png)]({{root}})</pre>
<p><strong>HTML</strong></p>
<pre>&lt;a href="{{root}}"&gt;&lt;img src="{{root}}/static/donate-with-fosspay.png" alt="Donate with fosspay" /&gt;&lt;/a&gt;</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Dismiss
</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,4 @@
{#
This is a little blurb you can write to explain why your goal is set to what
it is.
#}

117
templates/goal.html Normal file
View File

@ -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 %}

160
templates/index.html Normal file
View File

@ -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 %}

View File

@ -0,0 +1,5 @@
{% extends "layout.html" %}
{% block container %}
<h1>500 Internal Error</h1>
<p><a href="/">Trying to donate?</a></p>
{% endblock %}

60
templates/invoice.html Normal file
View File

@ -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 %}

24
templates/layout.html Normal file
View File

@ -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>

32
templates/login.html Normal file
View File

@ -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 %}

5
templates/not_found.html Normal file
View File

@ -0,0 +1,5 @@
{% extends "layout.html" %}
{% block container %}
<h1>404 Not Found</h1>
<p><a href="/">Trying to donate?</a></p>
{% endblock %}

62
templates/panel.html Normal file
View File

@ -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 %}

View File

@ -0,0 +1,2 @@
<h3>Thanks!</h3>
<p>Have a great day!</p>

43
templates/reset.html Normal file
View File

@ -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 %}

87
templates/setup.html Normal file
View File

@ -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 %}

13
templates/summary.html Normal file
View File

@ -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>