diff --git a/.gitignore b/.gitignore index 924eaa6..80cdf2f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ storage/ pip-selfcheck.json .sass-cache/ overrides/ +venv/ diff --git a/cronjob.py b/cronjob.py index 6032f59..c749d25 100755 --- a/cronjob.py +++ b/cronjob.py @@ -5,29 +5,37 @@ 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 +import sys # ← required for sys.exit below ❱❱ ADDED 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() +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)) + print(f"Charging {donation}") user = donation.user - customer = stripe.Customer.retrieve(user.stripe_customer) + + # ── guard against missing customer IDs ───────────────────────── + if not user.stripe_customer: + print(" • no stripe_customer on record → skipped") + continue + + customer = stripe.Customer.retrieve( # ❱❱ FIXED + user.stripe_customer, + expand=["sources"] # ensure .sources exists + ) try: charge = stripe.Charge.create( amount=donation.amount, @@ -35,31 +43,34 @@ for donation in donations: customer=user.stripe_customer, description="Donation to " + _cfg("your-name") ) - except stripe.error.CardError as e: + except stripe.error.CardError: donation.active = False db.commit() send_declined(user, donation.amount) - print("Declined") + print(" • declined") continue - send_thank_you(user, donation.amount, donation.type == DonationType.monthly) + send_thank_you(user, donation.amount, True) donation.updated = datetime.now() donation.payments += 1 db.commit() else: - print("Skipping {}".format(donation)) + print(f"Skipping {donation}") -print("{} records processed.".format(len(donations))) +print(f"{len(donations)} records processed.") +# ───────────────────────── Patreon token refresh ────────────────────── 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") - }) + 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) @@ -71,6 +82,7 @@ if _cfg("patreon-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") diff --git a/fosspay/blueprints/html.py b/fosspay/blueprints/html.py index f1379ca..74c331c 100644 --- a/fosspay/blueprints/html.py +++ b/fosspay/blueprints/html.py @@ -71,42 +71,54 @@ def index(): lp_count = 0 lp_sum = 0 + + # ── GitHub Sponsors totals ──────────────────────────────────────── github_token = _cfg("github-token") if github_token: - query = """ - { - viewer { - login - sponsorsListing { - tiers(first:100) { - nodes { - monthlyPriceInCents - adminInfo { - sponsorships(includePrivate:true) { - totalCount + try: + 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"] + """ + r = requests.post( + "https://api.github.com/graphql", + json={"query": query}, + headers={"Authorization": f"bearer {github_token}"} + ) + result = r.json() + viewer = (result.get("data") or {}).get("viewer") or {} + gh_user = viewer.get("login") + listing = viewer.get("sponsorsListing") or {} + tiers = (listing.get("tiers") or {}).get("nodes") or [] + + cnt = lambda n: n["adminInfo"]["sponsorships"]["totalCount"] + gh_count = sum(cnt(n) for n in tiers) + gh_sum = sum(n["monthlyPriceInCents"] * cnt(n) for n in tiers) + + except Exception: + gh_count = 0 + gh_sum = 0 + gh_user = None else: gh_count = 0 - gh_sum = 0 - gh_user = None + gh_sum = 0 + gh_user = None + return render_template("index.html", projects=projects, avatar=avatar, selected_project=selected_project, @@ -236,7 +248,7 @@ def donate(): db.add(user) else: customer = stripe.Customer.retrieve(user.stripe_customer) - new_source = customer.sources.create(source=stripe_token) + new_source = customer.create_source(user.stripe_customer, source=stripe_token) customer.default_source = new_source.id customer.save() diff --git a/fosspay/config.py b/fosspay/config.py index 730b896..c240f7d 100644 --- a/fosspay/config.py +++ b/fosspay/config.py @@ -1,10 +1,12 @@ import logging +import os +from pathlib import Path try: from configparser import ConfigParser except ImportError: # Python 2 support - from ConfigParser import ConfigParser + from ConfigParser import ConfigParser # type: ignore logger = logging.getLogger("fosspay") logger.setLevel(logging.DEBUG) @@ -19,13 +21,49 @@ logger.addHandler(sh) # scss logger logging.getLogger("scss").addHandler(sh) -env = 'dev' +env = "dev" config = None + +def _read_config_file(cfg, path: Path) -> None: + """ + Read config.ini using the best available API. + Python 3.2+ prefers read_file(); Python 2 uses readfp(). + """ + with path.open("r", encoding="utf-8") as f: + if hasattr(cfg, "read_file"): + cfg.read_file(f) + else: + # Python 2 + cfg.readfp(f) # type: ignore[attr-defined] + + def load_config(): global config config = ConfigParser() - config.readfp(open('config.ini')) + + # Keep existing behavior: look in current working directory first. + candidates = [Path(os.getcwd()) / "config.ini"] + + # Fallback: look in project root (parent of the fosspay package dir). + # /home/fosspay/app/fosspay/config.py -> /home/fosspay/app/config.ini + candidates.append(Path(__file__).resolve().parent.parent / "config.ini") + + # Optional override, doesn't break defaults: + # export FOSSPAY_CONFIG=/path/to/config.ini + override = os.environ.get("FOSSPAY_CONFIG") + if override: + candidates.insert(0, Path(override)) + + for p in candidates: + if p.is_file(): + _read_config_file(config, p) + logger.debug("Loaded config.ini from %s", str(p)) + return + + logger.error("Could not find config.ini. Tried: %s", ", ".join(str(p) for p in candidates)) + raise FileNotFoundError("config.ini not found") + load_config() diff --git a/fosspay/email.py b/fosspay/email.py index ac350bb..4f364c3 100644 --- a/fosspay/email.py +++ b/fosspay/email.py @@ -1,3 +1,8 @@ +# fosspay/email.py +# ─────────────────────────────────────────────────────────────── +# Only change: smarter SMTP opener that works with either SMTPS (465) +# or STARTTLS (587/25). All calling functions now use it. +# ─────────────────────────────────────────────────────────────── import smtplib import os import html.parser @@ -12,12 +17,28 @@ from fosspay.objects import User, DonationType from fosspay.config import _cfg, _cfgi from fosspay.currency import currency -def send_thank_you(user, amount, monthly): + +# helper: open & log in using the right TLS mode +def _open_smtp(): if _cfg("smtp-host") == "": - return - smtp = smtplib.SMTP_SSL(_cfg("smtp-host"), _cfgi("smtp-port")) + return None + host = _cfg("smtp-host") + port = _cfgi("smtp-port") + if port == 465: # implicit TLS + smtp = smtplib.SMTP_SSL(host, port) + else: # STARTTLS pathway + smtp = smtplib.SMTP(host, port) + smtp.ehlo() + smtp.starttls() smtp.ehlo() smtp.login(_cfg("smtp-user"), _cfg("smtp-password")) + return smtp + + +def send_thank_you(user, amount, monthly): + smtp = _open_smtp() + if smtp is None: + return with open("emails/thank-you") as f: tmpl = Template(f.read()) message = MIMEText(tmpl.substitute(**{ @@ -31,15 +52,14 @@ def send_thank_you(user, amount, monthly): 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.sendmail(_cfg("smtp-from"), [user.email], message.as_string()) smtp.quit() + def send_password_reset(user): - if _cfg("smtp-host") == "": + smtp = _open_smtp() + if smtp is None: 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(**{ @@ -52,15 +72,14 @@ def send_password_reset(user): 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.sendmail(_cfg("smtp-from"), [user.email], message.as_string()) smtp.quit() + def send_declined(user, amount): - if _cfg("smtp-host") == "": + smtp = _open_smtp() + if smtp is None: 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(**{ @@ -72,15 +91,14 @@ def send_declined(user, amount): 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.sendmail(_cfg("smtp-from"), [user.email], message.as_string()) smtp.quit() + def send_new_donation(user, donation): - if _cfg("smtp-host") == "": + smtp = _open_smtp() + if smtp is None: 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(**{ @@ -96,15 +114,14 @@ def send_new_donation(user, donation): 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.sendmail(_cfg("smtp-from"), [_cfg('your-email')], message.as_string()) smtp.quit() + def send_cancellation_notice(user, donation): - if _cfg("smtp-host") == "": + smtp = _open_smtp() + if smtp is None: 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(**{ @@ -118,5 +135,5 @@ def send_cancellation_notice(user, donation): 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.sendmail(_cfg("smtp-from"), [_cfg('your-email')], message.as_string()) smtp.quit() diff --git a/templates/goal.html b/templates/goal.html index 2483649..a2d0551 100644 --- a/templates/goal.html +++ b/templates/goal.html @@ -20,33 +20,41 @@ {% set gh_progress = gh_sum / adjusted_goal %} {% set progress = total_sum / goal %}
Your donation here supports tilde.club, and any other services that are run by tilde.club.
+{# templates/summary.html — combined quick‑pitch + full donate info #} -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.
+Your donation keeps our community servers, IRC nodes, and hosted +services online. Current costs run about $200 / month for +hosting plus $150 / year in domains.
-My hosting costs are around $200/month and domains are around $150/year.
++ Payments are processed securely by Stripe; card details never touch our + servers. Stripe charges 2.9 % + 30¢ per transaction. +
-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.
+NOTICE: Tilde Club is not a registered legal entity + or charity. Donations are not tax‑deductible.
+All funds offset hosting (servers, domains).
+ +
+
+
+
+
+
+
+
+
+
+
+
+
Note: Email root@tilde.club after donating so we can add you to the Gold‑Star Supporters list.
The best way to support us is to join the community: open pull + requests on GitHub, chat on IRC, build cool web pages, or develop + software with the tools on tilde.club.
+ +Ask on IRC if you have questions!
+