mirror of
https://github.com/zulip/zulip.git
synced 2025-11-11 01:16:19 +00:00
stripe: Strengthen types using WildValue.
This commit is contained in:
committed by
Tim Abbott
parent
c5579cf15a
commit
9b7a91b49c
@@ -1,6 +1,6 @@
|
|||||||
# Webhooks for external integrations.
|
# Webhooks for external integrations.
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, Optional, Sequence, Tuple
|
from typing import Dict, Optional, Sequence, Tuple
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
@@ -9,6 +9,15 @@ from zerver.lib.exceptions import UnsupportedWebhookEventType
|
|||||||
from zerver.lib.request import REQ, has_request_variables
|
from zerver.lib.request import REQ, has_request_variables
|
||||||
from zerver.lib.response import json_success
|
from zerver.lib.response import json_success
|
||||||
from zerver.lib.timestamp import timestamp_to_datetime
|
from zerver.lib.timestamp import timestamp_to_datetime
|
||||||
|
from zerver.lib.validator import (
|
||||||
|
WildValue,
|
||||||
|
check_anything,
|
||||||
|
check_bool,
|
||||||
|
check_int,
|
||||||
|
check_none_or,
|
||||||
|
check_string,
|
||||||
|
to_wild_value,
|
||||||
|
)
|
||||||
from zerver.lib.webhooks.common import check_send_webhook_message
|
from zerver.lib.webhooks.common import check_send_webhook_message
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile
|
||||||
|
|
||||||
@@ -50,19 +59,23 @@ ALL_EVENT_TYPES = [
|
|||||||
def api_stripe_webhook(
|
def api_stripe_webhook(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
payload: Dict[str, Any] = REQ(argument_type="body"),
|
payload: WildValue = REQ(argument_type="body", converter=to_wild_value),
|
||||||
stream: str = REQ(default="test"),
|
stream: str = REQ(default="test"),
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
try:
|
try:
|
||||||
topic, body = topic_and_body(payload)
|
topic, body = topic_and_body(payload)
|
||||||
except SuppressedEvent: # nocoverage
|
except SuppressedEvent: # nocoverage
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
check_send_webhook_message(request, user_profile, topic, body, payload["type"])
|
check_send_webhook_message(
|
||||||
|
request, user_profile, topic, body, payload["type"].tame(check_string)
|
||||||
|
)
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
def topic_and_body(payload: WildValue) -> Tuple[str, str]:
|
||||||
event_type = payload["type"] # invoice.created, customer.subscription.created, etc
|
event_type = payload["type"].tame(
|
||||||
|
check_string
|
||||||
|
) # invoice.created, customer.subscription.created, etc
|
||||||
if len(event_type.split(".")) == 3:
|
if len(event_type.split(".")) == 3:
|
||||||
category, resource, event = event_type.split(".")
|
category, resource, event = event_type.split(".")
|
||||||
else:
|
else:
|
||||||
@@ -73,7 +86,7 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
|
|
||||||
# Set the topic to the customer_id when we can
|
# Set the topic to the customer_id when we can
|
||||||
topic = ""
|
topic = ""
|
||||||
customer_id = object_.get("customer", None)
|
customer_id = object_.get("customer").tame(check_none_or(check_string))
|
||||||
if customer_id is not None:
|
if customer_id is not None:
|
||||||
# Running into the 60 character topic limit.
|
# Running into the 60 character topic limit.
|
||||||
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (customer_id, customer_id)
|
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (customer_id, customer_id)
|
||||||
@@ -82,22 +95,24 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
|
|
||||||
def update_string(blacklist: Sequence[str] = []) -> str:
|
def update_string(blacklist: Sequence[str] = []) -> str:
|
||||||
assert "previous_attributes" in payload["data"]
|
assert "previous_attributes" in payload["data"]
|
||||||
previous_attributes = payload["data"]["previous_attributes"]
|
previous_attributes = {
|
||||||
for attribute in blacklist:
|
key: value
|
||||||
previous_attributes.pop(attribute, None)
|
for key, value in payload["data"]["previous_attributes"].items()
|
||||||
|
if key not in blacklist
|
||||||
|
}
|
||||||
if not previous_attributes: # nocoverage
|
if not previous_attributes: # nocoverage
|
||||||
raise SuppressedEvent()
|
raise SuppressedEvent()
|
||||||
return "".join(
|
return "".join(
|
||||||
"\n* "
|
"\n* "
|
||||||
+ attribute.replace("_", " ").capitalize()
|
+ attribute.replace("_", " ").capitalize()
|
||||||
+ " is now "
|
+ " is now "
|
||||||
+ stringify(object_[attribute])
|
+ stringify(object_[attribute].tame(check_anything))
|
||||||
for attribute in sorted(previous_attributes.keys())
|
for attribute in sorted(previous_attributes.keys())
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_body(update_blacklist: Sequence[str] = []) -> str:
|
def default_body(update_blacklist: Sequence[str] = []) -> str:
|
||||||
body = "{resource} {verbed}".format(
|
body = "{resource} {verbed}".format(
|
||||||
resource=linkified_id(object_["id"]), verbed=event.replace("_", " ")
|
resource=linkified_id(object_["id"].tame(check_string)), verbed=event.replace("_", " ")
|
||||||
)
|
)
|
||||||
if event == "updated":
|
if event == "updated":
|
||||||
return body + update_string(blacklist=update_blacklist)
|
return body + update_string(blacklist=update_blacklist)
|
||||||
@@ -124,23 +139,27 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
if not topic: # only in legacy fixtures
|
if not topic: # only in legacy fixtures
|
||||||
topic = "charges"
|
topic = "charges"
|
||||||
body = "{resource} for {amount} {verbed}".format(
|
body = "{resource} for {amount} {verbed}".format(
|
||||||
resource=linkified_id(object_["id"]),
|
resource=linkified_id(object_["id"].tame(check_string)),
|
||||||
amount=amount_string(object_["amount"], object_["currency"]),
|
amount=amount_string(
|
||||||
|
object_["amount"].tame(check_int), object_["currency"].tame(check_string)
|
||||||
|
),
|
||||||
verbed=event,
|
verbed=event,
|
||||||
)
|
)
|
||||||
if object_["failure_code"]: # nocoverage
|
if object_["failure_code"]: # nocoverage
|
||||||
body += ". Failure code: {}".format(object_["failure_code"])
|
body += ". Failure code: {}".format(object_["failure_code"].tame(check_string))
|
||||||
if resource == "dispute":
|
if resource == "dispute":
|
||||||
topic = "disputes"
|
topic = "disputes"
|
||||||
body = default_body() + ". Current status: {status}.".format(
|
body = default_body() + ". Current status: {status}.".format(
|
||||||
status=object_["status"].replace("_", " ")
|
status=object_["status"].tame(check_string).replace("_", " ")
|
||||||
)
|
)
|
||||||
if resource == "refund":
|
if resource == "refund":
|
||||||
topic = "refunds"
|
topic = "refunds"
|
||||||
body = "A {resource} for a {charge} of {amount} was updated.".format(
|
body = "A {resource} for a {charge} of {amount} was updated.".format(
|
||||||
resource=linkified_id(object_["id"], lower=True),
|
resource=linkified_id(object_["id"].tame(check_string), lower=True),
|
||||||
charge=linkified_id(object_["charge"], lower=True),
|
charge=linkified_id(object_["charge"].tame(check_string), lower=True),
|
||||||
amount=amount_string(object_["amount"], object_["currency"]),
|
amount=amount_string(
|
||||||
|
object_["amount"].tame(check_int), object_["currency"].tame(check_string)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if category == "checkout_beta": # nocoverage
|
if category == "checkout_beta": # nocoverage
|
||||||
# Not sure what this is
|
# Not sure what this is
|
||||||
@@ -152,20 +171,20 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
if resource == "customer":
|
if resource == "customer":
|
||||||
# Running into the 60 character topic limit.
|
# Running into the 60 character topic limit.
|
||||||
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (object_['id'], object_['id'])
|
# topic = '[{}](https://dashboard.stripe.com/customers/{})' % (object_['id'], object_['id'])
|
||||||
topic = object_["id"]
|
topic = object_["id"].tame(check_string)
|
||||||
body = default_body(update_blacklist=["delinquent", "currency", "default_source"])
|
body = default_body(update_blacklist=["delinquent", "currency", "default_source"])
|
||||||
if event == "created":
|
if event == "created":
|
||||||
if object_["email"]:
|
if object_["email"]:
|
||||||
body += "\nEmail: {}".format(object_["email"])
|
body += "\nEmail: {}".format(object_["email"].tame(check_string))
|
||||||
if object_["metadata"]: # nocoverage
|
if object_["metadata"]: # nocoverage
|
||||||
for key, value in object_["metadata"].items():
|
for key, value in object_["metadata"].items():
|
||||||
body += f"\n{key}: {value}"
|
body += f"\n{key}: {value}"
|
||||||
if resource == "discount":
|
if resource == "discount":
|
||||||
body = "Discount {verbed} ([{coupon_name}]({coupon_url})).".format(
|
body = "Discount {verbed} ([{coupon_name}]({coupon_url})).".format(
|
||||||
verbed=event.replace("_", " "),
|
verbed=event.replace("_", " "),
|
||||||
coupon_name=object_["coupon"]["name"],
|
coupon_name=object_["coupon"]["name"].tame(check_string),
|
||||||
coupon_url="https://dashboard.stripe.com/{}/{}".format(
|
coupon_url="https://dashboard.stripe.com/{}/{}".format(
|
||||||
"coupons", object_["coupon"]["id"]
|
"coupons", object_["coupon"]["id"].tame(check_string)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if resource == "source": # nocoverage
|
if resource == "source": # nocoverage
|
||||||
@@ -176,35 +195,40 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
DAY = 60 * 60 * 24 # seconds in a day
|
DAY = 60 * 60 * 24 # seconds in a day
|
||||||
# Basically always three: https://stripe.com/docs/api/python#event_types
|
# Basically always three: https://stripe.com/docs/api/python#event_types
|
||||||
body += " in {days} days".format(
|
body += " in {days} days".format(
|
||||||
days=int((object_["trial_end"] - time.time() + DAY // 2) // DAY)
|
days=int((object_["trial_end"].tame(check_int) - time.time() + DAY // 2) // DAY)
|
||||||
)
|
)
|
||||||
if event == "created":
|
if event == "created":
|
||||||
if object_["plan"]:
|
if object_["plan"]:
|
||||||
body += "\nPlan: [{plan_nickname}](https://dashboard.stripe.com/plans/{plan_id})".format(
|
body += "\nPlan: [{plan_nickname}](https://dashboard.stripe.com/plans/{plan_id})".format(
|
||||||
plan_nickname=object_["plan"]["nickname"], plan_id=object_["plan"]["id"]
|
plan_nickname=object_["plan"]["nickname"].tame(check_string),
|
||||||
|
plan_id=object_["plan"]["id"].tame(check_string),
|
||||||
)
|
)
|
||||||
if object_["quantity"]:
|
if object_["quantity"]:
|
||||||
body += "\nQuantity: {}".format(object_["quantity"])
|
body += "\nQuantity: {}".format(object_["quantity"].tame(check_int))
|
||||||
if "billing" in object_: # nocoverage
|
if "billing" in object_: # nocoverage
|
||||||
body += "\nBilling method: {}".format(object_["billing"].replace("_", " "))
|
body += "\nBilling method: {}".format(
|
||||||
|
object_["billing"].tame(check_string).replace("_", " ")
|
||||||
|
)
|
||||||
if category == "file": # nocoverage
|
if category == "file": # nocoverage
|
||||||
topic = "files"
|
topic = "files"
|
||||||
body = default_body() + " ({purpose}). \nTitle: {title}".format(
|
body = default_body() + " ({purpose}). \nTitle: {title}".format(
|
||||||
purpose=object_["purpose"].replace("_", " "), title=object_["title"]
|
purpose=object_["purpose"].tame(check_string).replace("_", " "),
|
||||||
|
title=object_["title"].tame(check_string),
|
||||||
)
|
)
|
||||||
if category == "invoice":
|
if category == "invoice":
|
||||||
if event == "upcoming": # nocoverage
|
if event == "upcoming": # nocoverage
|
||||||
body = "Upcoming invoice created"
|
body = "Upcoming invoice created"
|
||||||
elif (
|
elif (
|
||||||
event == "updated"
|
event == "updated"
|
||||||
and payload["data"]["previous_attributes"].get("paid", None) is False
|
and payload["data"]["previous_attributes"].get("paid").tame(check_none_or(check_bool))
|
||||||
and object_["paid"] is True
|
is False
|
||||||
and object_["amount_paid"] != 0
|
and object_["paid"].tame(check_bool) is True
|
||||||
and object_["amount_remaining"] == 0
|
and object_["amount_paid"].tame(check_int) != 0
|
||||||
|
and object_["amount_remaining"].tame(check_int) == 0
|
||||||
):
|
):
|
||||||
# We are taking advantage of logical AND short circuiting here since we need the else
|
# We are taking advantage of logical AND short circuiting here since we need the else
|
||||||
# statement below.
|
# statement below.
|
||||||
object_id = object_["id"]
|
object_id = object_["id"].tame(check_string)
|
||||||
invoice_link = f"https://dashboard.stripe.com/invoices/{object_id}"
|
invoice_link = f"https://dashboard.stripe.com/invoices/{object_id}"
|
||||||
body = f"[Invoice]({invoice_link}) is now paid"
|
body = f"[Invoice]({invoice_link}) is now paid"
|
||||||
else:
|
else:
|
||||||
@@ -221,15 +245,21 @@ def topic_and_body(payload: Dict[str, Any]) -> Tuple[str, str]:
|
|||||||
if event == "created":
|
if event == "created":
|
||||||
# Could potentially add link to invoice PDF here
|
# Could potentially add link to invoice PDF here
|
||||||
body += " ({reason})\nTotal: {total}\nAmount due: {due}".format(
|
body += " ({reason})\nTotal: {total}\nAmount due: {due}".format(
|
||||||
reason=object_["billing_reason"].replace("_", " "),
|
reason=object_["billing_reason"].tame(check_string).replace("_", " "),
|
||||||
total=amount_string(object_["total"], object_["currency"]),
|
total=amount_string(
|
||||||
due=amount_string(object_["amount_due"], object_["currency"]),
|
object_["total"].tame(check_int), object_["currency"].tame(check_string)
|
||||||
|
),
|
||||||
|
due=amount_string(
|
||||||
|
object_["amount_due"].tame(check_int), object_["currency"].tame(check_string)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if category == "invoiceitem":
|
if category == "invoiceitem":
|
||||||
body = default_body(update_blacklist=["description", "invoice"])
|
body = default_body(update_blacklist=["description", "invoice"])
|
||||||
if event == "created":
|
if event == "created":
|
||||||
body += " for {amount}".format(
|
body += " for {amount}".format(
|
||||||
amount=amount_string(object_["amount"], object_["currency"])
|
amount=amount_string(
|
||||||
|
object_["amount"].tame(check_int), object_["currency"].tame(check_string)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if category.startswith("issuing"): # nocoverage
|
if category.startswith("issuing"): # nocoverage
|
||||||
# Not implemented
|
# Not implemented
|
||||||
@@ -334,7 +364,7 @@ def linkified_id(object_id: str, lower: bool = False) -> str:
|
|||||||
return f"[{name}](https://dashboard.stripe.com/{url_prefix}/{object_id})"
|
return f"[{name}](https://dashboard.stripe.com/{url_prefix}/{object_id})"
|
||||||
|
|
||||||
|
|
||||||
def stringify(value: Any) -> str:
|
def stringify(value: object) -> str:
|
||||||
if isinstance(value, int) and value > 1500000000 and value < 2000000000:
|
if isinstance(value, int) and value > 1500000000 and value < 2000000000:
|
||||||
return timestamp_to_datetime(value).strftime("%b %d, %Y, %H:%M:%S %Z")
|
return timestamp_to_datetime(value).strftime("%b %d, %Y, %H:%M:%S %Z")
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|||||||
Reference in New Issue
Block a user