2016-05-25 15:02:02 +02:00
|
|
|
|
2018-01-13 19:38:13 +01:00
|
|
|
import logging
|
2018-01-18 02:03:12 +01:00
|
|
|
import os
|
2017-11-16 00:55:49 +01:00
|
|
|
from typing import Any, Dict, Optional, Text, Union, cast
|
2013-10-17 22:55:09 +02:00
|
|
|
|
2017-11-16 00:55:49 +01:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
from django.utils import timezone
|
|
|
|
from django.utils.translation import ugettext as _
|
2018-01-13 19:38:13 +01:00
|
|
|
from django.shortcuts import render
|
|
|
|
from django.conf import settings
|
|
|
|
from django.views.decorators.http import require_GET
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
import stripe
|
|
|
|
from stripe.error import CardError, RateLimitError, InvalidRequestError, \
|
|
|
|
AuthenticationError, APIConnectionError, StripeError
|
|
|
|
|
|
|
|
from zerver.decorator import require_post, zulip_login_required
|
2017-10-28 00:07:31 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2018-01-18 02:03:12 +01:00
|
|
|
from zerver.lib.logging_util import log_to_file
|
2016-10-27 23:55:31 +02:00
|
|
|
from zerver.lib.push_notifications import send_android_push_notification, \
|
|
|
|
send_apple_push_notification
|
2017-11-16 00:55:49 +01:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2016-10-27 23:55:31 +02:00
|
|
|
from zerver.lib.response import json_error, json_success
|
2017-11-02 12:39:10 +01:00
|
|
|
from zerver.lib.validator import check_int
|
2018-01-30 02:02:32 +01:00
|
|
|
from zerver.models import UserProfile, Realm
|
2017-07-07 18:23:36 +02:00
|
|
|
from zerver.views.push_notifications import validate_token
|
2018-01-13 19:38:13 +01:00
|
|
|
from zilencer.models import RemotePushDeviceToken, RemoteZulipServer, Customer
|
|
|
|
from zproject.settings import get_secret
|
|
|
|
|
|
|
|
STRIPE_SECRET_KEY = get_secret('stripe_secret_key')
|
|
|
|
STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key')
|
|
|
|
stripe.api_key = STRIPE_SECRET_KEY
|
2013-10-17 22:55:09 +02:00
|
|
|
|
2018-01-18 02:03:12 +01:00
|
|
|
BILLING_LOG_PATH = os.path.join('/var/log/zulip'
|
|
|
|
if not settings.DEVELOPMENT
|
|
|
|
else settings.DEVELOPMENT_LOG_DIRECTORY,
|
|
|
|
'billing.log')
|
|
|
|
billing_logger = logging.getLogger('zilencer.stripe')
|
|
|
|
log_to_file(billing_logger, BILLING_LOG_PATH)
|
|
|
|
log_to_file(logging.getLogger('stripe'), BILLING_LOG_PATH)
|
|
|
|
|
2017-10-27 12:57:54 +02:00
|
|
|
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None:
|
2017-05-08 14:25:40 +02:00
|
|
|
if not isinstance(entity, RemoteZulipServer):
|
|
|
|
raise JsonableError(_("Must validate with valid Zulip server API key"))
|
|
|
|
|
2017-10-27 12:57:54 +02:00
|
|
|
def validate_bouncer_token_request(entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
token: bytes, kind: int) -> None:
|
2017-07-07 18:29:45 +02:00
|
|
|
if kind not in [RemotePushDeviceToken.APNS, RemotePushDeviceToken.GCM]:
|
|
|
|
raise JsonableError(_("Invalid token type"))
|
2017-05-08 14:25:40 +02:00
|
|
|
validate_entity(entity)
|
2017-07-07 18:23:36 +02:00
|
|
|
validate_token(token, kind)
|
2017-05-08 14:25:40 +02:00
|
|
|
|
2016-10-27 23:55:31 +02:00
|
|
|
@has_request_variables
|
2017-12-20 20:56:11 +01:00
|
|
|
def remote_server_register_push(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
user_id: int=REQ(), token: bytes=REQ(),
|
|
|
|
token_kind: int=REQ(validator=check_int),
|
|
|
|
ios_app_id: Optional[Text]=None) -> HttpResponse:
|
2017-07-07 18:23:36 +02:00
|
|
|
validate_bouncer_token_request(entity, token, token_kind)
|
2016-10-27 23:55:31 +02:00
|
|
|
server = cast(RemoteZulipServer, entity)
|
|
|
|
|
|
|
|
# If a user logged out on a device and failed to unregister,
|
|
|
|
# we should delete any other user associations for this token
|
|
|
|
# & RemoteServer pair
|
|
|
|
RemotePushDeviceToken.objects.filter(
|
|
|
|
token=token, kind=token_kind, server=server).exclude(user_id=user_id).delete()
|
|
|
|
|
|
|
|
# Save or update
|
|
|
|
remote_token, created = RemotePushDeviceToken.objects.update_or_create(
|
|
|
|
user_id=user_id,
|
|
|
|
server=server,
|
|
|
|
kind=token_kind,
|
|
|
|
token=token,
|
|
|
|
defaults=dict(
|
|
|
|
ios_app_id=ios_app_id,
|
|
|
|
last_updated=timezone.now()))
|
|
|
|
|
|
|
|
return json_success()
|
|
|
|
|
|
|
|
@has_request_variables
|
2017-12-20 20:56:11 +01:00
|
|
|
def remote_server_unregister_push(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
token: bytes=REQ(),
|
|
|
|
token_kind: int=REQ(validator=check_int),
|
|
|
|
ios_app_id: Optional[Text]=None) -> HttpResponse:
|
2017-07-07 18:23:36 +02:00
|
|
|
validate_bouncer_token_request(entity, token, token_kind)
|
2016-10-27 23:55:31 +02:00
|
|
|
server = cast(RemoteZulipServer, entity)
|
|
|
|
deleted = RemotePushDeviceToken.objects.filter(token=token,
|
|
|
|
kind=token_kind,
|
|
|
|
server=server).delete()
|
|
|
|
if deleted[0] == 0:
|
|
|
|
return json_error(_("Token does not exist"))
|
|
|
|
|
|
|
|
return json_success()
|
2017-05-08 13:48:16 +02:00
|
|
|
|
|
|
|
@has_request_variables
|
2017-12-20 20:56:11 +01:00
|
|
|
def remote_server_notify_push(request: HttpRequest, entity: Union[UserProfile, RemoteZulipServer],
|
|
|
|
payload: Dict[str, Any]=REQ(argument_type='body')) -> HttpResponse:
|
2017-05-08 13:48:16 +02:00
|
|
|
validate_entity(entity)
|
2017-05-09 10:31:47 +02:00
|
|
|
server = cast(RemoteZulipServer, entity)
|
|
|
|
|
|
|
|
user_id = payload['user_id']
|
|
|
|
gcm_payload = payload['gcm_payload']
|
|
|
|
apns_payload = payload['apns_payload']
|
|
|
|
|
|
|
|
android_devices = list(RemotePushDeviceToken.objects.filter(
|
|
|
|
user_id=user_id,
|
|
|
|
kind=RemotePushDeviceToken.GCM,
|
|
|
|
server=server
|
|
|
|
))
|
|
|
|
|
|
|
|
apple_devices = list(RemotePushDeviceToken.objects.filter(
|
|
|
|
user_id=user_id,
|
|
|
|
kind=RemotePushDeviceToken.APNS,
|
|
|
|
server=server
|
|
|
|
))
|
|
|
|
|
|
|
|
if android_devices:
|
2017-05-16 21:15:45 +02:00
|
|
|
send_android_push_notification(android_devices, gcm_payload, remote=True)
|
2017-05-09 10:31:47 +02:00
|
|
|
|
|
|
|
if apple_devices:
|
2017-08-19 01:38:11 +02:00
|
|
|
send_apple_push_notification(user_id, apple_devices, apns_payload)
|
2017-05-09 10:31:47 +02:00
|
|
|
|
2017-05-08 13:48:16 +02:00
|
|
|
return json_success()
|
2018-01-13 19:38:13 +01:00
|
|
|
|
2018-01-30 02:02:32 +01:00
|
|
|
def count_stripe_cards(realm: Realm) -> int:
|
|
|
|
try:
|
|
|
|
customer_obj = Customer.objects.get(realm=realm)
|
|
|
|
cards = stripe.Customer.retrieve(customer_obj.stripe_customer_id).sources.all(object="card")
|
|
|
|
return len(cards["data"])
|
|
|
|
except Customer.DoesNotExist:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
def save_stripe_token(user: UserProfile, token: str) -> int:
|
|
|
|
"""Returns total number of cards."""
|
|
|
|
# The card metadata doesn't show up in Dashboard but can be accessed
|
|
|
|
# using the API.
|
|
|
|
card_metadata = {"added_user_id": user.id, "added_user_email": user.email}
|
|
|
|
try:
|
|
|
|
customer_obj = Customer.objects.get(realm=user.realm)
|
|
|
|
customer = stripe.Customer.retrieve(customer_obj.stripe_customer_id)
|
|
|
|
billing_logger.info("Adding card on customer %s: source=%r, metadata=%r",
|
|
|
|
customer_obj.stripe_customer_id, token, card_metadata)
|
|
|
|
card = customer.sources.create(source=token, metadata=card_metadata)
|
|
|
|
customer.default_source = card.id
|
|
|
|
customer.save()
|
|
|
|
return len(customer.sources.all(object="card")["data"])
|
|
|
|
except Customer.DoesNotExist:
|
|
|
|
customer_metadata = {"string_id": user.realm.string_id}
|
|
|
|
# Description makes it easier to identify customers in Stripe dashboard
|
|
|
|
description = "{} ({})".format(user.realm.name, user.realm.string_id)
|
|
|
|
billing_logger.info("Creating customer: source=%r, description=%r, metadata=%r",
|
|
|
|
token, description, customer_metadata)
|
|
|
|
customer = stripe.Customer.create(source=token,
|
|
|
|
description=description,
|
|
|
|
metadata=customer_metadata)
|
|
|
|
|
|
|
|
card = customer.sources.all(object="card")["data"][0]
|
|
|
|
card.metadata = card_metadata
|
|
|
|
card.save()
|
|
|
|
Customer.objects.create(realm=user.realm, stripe_customer_id=customer.id)
|
|
|
|
return 1
|
2018-01-13 19:38:13 +01:00
|
|
|
|
|
|
|
@zulip_login_required
|
|
|
|
def add_payment_method(request: HttpRequest) -> HttpResponse:
|
|
|
|
user = request.user
|
|
|
|
ctx = {
|
|
|
|
"publishable_key": STRIPE_PUBLISHABLE_KEY,
|
|
|
|
"email": user.email,
|
|
|
|
} # type: Dict[str, Any]
|
2018-01-30 02:02:32 +01:00
|
|
|
|
|
|
|
def render_error(message: str) -> HttpResponse:
|
|
|
|
ctx["error_message"] = message
|
2018-01-30 01:30:02 +01:00
|
|
|
return render(request, 'zilencer/billing.html', context=ctx)
|
2018-01-30 02:02:32 +01:00
|
|
|
|
|
|
|
if not user.is_realm_admin:
|
|
|
|
return render_error(_("You should be an administrator of the organization %s to view this page.")
|
|
|
|
% (user.realm.name,))
|
2018-01-18 01:05:18 +01:00
|
|
|
if STRIPE_PUBLISHABLE_KEY is None:
|
|
|
|
# Dev-only message; no translation needed.
|
2018-01-30 02:02:32 +01:00
|
|
|
return render_error("Missing Stripe config. In dev, add to zproject/dev-secrets.conf .")
|
2018-01-13 19:38:13 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
if request.method == "GET":
|
2018-01-30 02:02:32 +01:00
|
|
|
ctx["num_cards"] = count_stripe_cards(user.realm)
|
2018-01-30 01:30:02 +01:00
|
|
|
return render(request, 'zilencer/billing.html', context=ctx)
|
2018-01-13 19:38:13 +01:00
|
|
|
if request.method == "POST":
|
|
|
|
token = request.POST.get("stripeToken", "")
|
2018-01-30 02:02:32 +01:00
|
|
|
ctx["num_cards"] = save_stripe_token(user, token)
|
2018-01-13 19:38:13 +01:00
|
|
|
ctx["payment_method_added"] = True
|
2018-01-30 01:30:02 +01:00
|
|
|
return render(request, 'zilencer/billing.html', context=ctx)
|
2018-01-18 03:05:27 +01:00
|
|
|
except StripeError as e:
|
|
|
|
billing_logger.error("Stripe error: %d %s", e.http_status, e.__class__.__name__)
|
|
|
|
if isinstance(e, CardError):
|
2018-01-30 02:02:32 +01:00
|
|
|
return render_error(e.json_body.get('error', {}).get('message'))
|
2018-01-18 03:05:27 +01:00
|
|
|
else:
|
2018-01-30 02:02:32 +01:00
|
|
|
return render_error(_("Something went wrong. Please try again or email us at %s.")
|
|
|
|
% (settings.ZULIP_ADMINISTRATOR,))
|
2018-01-13 19:38:13 +01:00
|
|
|
except Exception as e:
|
2018-01-18 03:05:27 +01:00
|
|
|
billing_logger.exception("Uncaught error in billing")
|
|
|
|
raise
|