mirror of
https://github.com/tildeclub/fosspay.git
synced 2026-10-27 16:55:38 +00:00
Source incoming :)
This commit is contained in:
1 parent
415f50ce1e
commit
907bf2206f
50 files changed
+2918
No files matched your search
+14
@@ -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
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Bootstrap v3.3.5 (http://getbootstrap.com)
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026Line truncated
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Bootstrap v3.3.5 (http://getbootstrap.com)
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under the MIT license
|
||||
*/
|
||||
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&cLine truncated
|
||||
d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.5",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 593 B |
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */
|
||||
!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^Line truncated
|
||||
|
||||
return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var
Line truncated
|
||||
return new Za.prototype.init(a,b,c,d,e)}m.Tween=Za,Za.prototype={constructor:Za,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")},cur:function(){var a=Za.propHooks[this.prop];return a&&a.get?a.get(this):Za.propHooks._default.get(this)},run:function(a){var b,c=Za.propHooks[this.prop];return this.options.duration?this.pos=b=m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Za.propHooks._default.set(this),this}},Za.prototype.init.prototype=Za.prototype,Za.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Za.propHooks.scrollTop=Za.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Za.prototype.init,m.fx.step={};var $a,_a,ab=/^(?:toggle|show|hide)$/,bb=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cb=/queueHooks$/,db=[ib],eb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bb.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bb.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fb(){return setTimeout(function(){$a=void 0}),$a=m.now()}function gb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hb(a,b,c){for(var d,e=(eb[b]||[]).concat(eb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fa(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fa(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ab.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fa(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hb(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=db.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$a||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$a||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);g>f;f++)if(d=db[f].call(j,a,k,j.opts))return d;return m.map(k,hb,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kb,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],eb[cLine truncated
|
||||
//# sourceMappingURL=jquery.min.map
|
||||
@@ -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/;
|
||||
}
|
||||
}
|
||||
Executable
+78
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user