def test_wrong_data(self): result = parse_active_membership({}) self.assertIsNone(result) result = parse_active_membership({"data": {}}) # no included self.assertIsNone(result) result = parse_active_membership({"included": {}}) # no data self.assertIsNone(result)
def test_successful_god_id(self): with self.settings(PATREON_GOD_IDS=['12345689']): result: Membership = parse_active_membership( self.stub_patreon_response_oauth_identity) self.assertIsNotNone(result) self.assertTrue(isinstance(result, Membership)) self.assertEqual(result.platform, "patreon") self.assertEqual(result.user_id, "12345689") self.assertEqual(result.full_name, "FullName With Space") self.assertEqual(result.email, "*****@*****.**") self.assertEqual(result.image, 'https://url.example') self.assertGreaterEqual(result.started_at, datetime.utcnow() - timedelta(seconds=5)) self.assertLessEqual(result.started_at, datetime.utcnow() + timedelta(seconds=5)) self.assertGreaterEqual( result.expires_at, datetime.utcnow() + timedelta(days=100 * 365) - timedelta(seconds=5)) self.assertLessEqual( result.expires_at, datetime.utcnow() + timedelta(days=100 * 365) + timedelta(seconds=5)) self.assertEqual(result.lifetime_support_cents, -1) self.assertEqual(result.currently_entitled_amount_cents, 0)
def test_successful_parsed(self): result: Membership = parse_active_membership( self.stub_patreon_response_oauth_identity) self.assertIsNotNone(result) self.assertTrue(isinstance(result, Membership)) self.assertEqual(result.platform, "patreon") self.assertEqual(result.user_id, "12345689") self.assertEqual(result.full_name, "FullName With Space") self.assertEqual(result.email, "*****@*****.**") self.assertEqual(result.image, None) self.assertEqual(result.started_at, datetime(2018, 4, 1, 0, 0)) self.assertEqual(result.expires_at, datetime(2018, 5, 16, 0, 0)) self.assertEqual(result.lifetime_support_cents, 400) self.assertEqual(result.currently_entitled_amount_cents, 100)
def handle(self, *args, **options): days_before = options["days_before"] days_after = options["days_after"] expiring_users = User.objects\ .filter( membership_expires_at__gte=datetime.utcnow() - timedelta(days=days_before), membership_expires_at__lte=datetime.utcnow() + timedelta(days=days_after), )\ .all() for user in expiring_users: if user.membership_platform_type == User.MEMBERSHIP_PLATFORM_PATREON: if not user.membership_platform_data or "refresh_token" not in user.membership_platform_data: log.warning(f"No auth data for user: {user.slug}") continue self.stdout.write(f"Renewing for user {user.slug}") # refresh user data id needed try: auth_data = patreon.refresh_auth_data( user.membership_platform_data["refresh_token"]) user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } except PatreonException as ex: log.warning(f"Can't refresh user data: {user.slug}: {ex}") pass # fetch user pledge status try: user_data = fetch_user_data( user.membership_platform_data["access_token"]) except PatreonException as ex: log.exception(f"Something went wrong for user {user.slug}") continue # check the new expiration date membership = patreon.parse_active_membership(user_data) if membership: if membership.expires_at >= user.membership_expires_at: user.membership_expires_at = membership.expires_at user.balance = membership.lifetime_support_cents / 100 # TODO: ^^^ remove when the real money comes in self.stdout.write( f"New expiration date for user {user.slug} — {membership.expires_at}" ) else: Session.objects.filter(user=user).delete() user.save() self.stdout.write(f"User processed: {user.slug}") else: self.stderr.write( f"No renewing scenario for the platform: {user.membership_platform_type}" ) self.stdout.write("Done 🥙")
def patreon_oauth_callback(request): code = request.GET.get("code") if not code: return render( request, "error.html", { "title": "Что-то сломалось между нами и патреоном", "message": "Так бывает. Попробуйте залогиниться еще раз" }) try: auth_data = patreon.fetch_auth_data(code) user_data = patreon.fetch_user_data(auth_data["access_token"]) except PatreonException as ex: if "invalid_grant" in str(ex): return render( request, "error.html", { "title": "Тут такое дело 😭", "message": "Авторизация патреона — говно. " "Она не сразу понимает, что вы стали патроном и отдаёт " "статус «отказано» в первые несколько минут, а иногда и часов. " "Я уже написал им в саппорт, но пока вам надо немного подождать и авторизоваться снова. " "Если долго не будет пускать — напишите мне в личку на патреоне." }) return render( request, "error.html", { "message": "Не получилось загрузить ваш профиль с серверов патреона. " "Попробуйте еще раз, наверняка оно починится. " f"Но если нет, то вот текст ошибки, с которым можно пожаловаться мне в личку:", "data": str(ex) }) membership = patreon.parse_active_membership(user_data) if not membership: return render( request, "error.html", { "title": "Надо быть патроном, чтобы состоять в Клубе", "message": "Кажется, вы не патроните <a href=\"https://www.patreon.com/join/vas3k\">@vas3k</a>. " "А это одно из основных требований для входа в Клуб.<br><br>" "Ещё иногда бывает, что ваш банк отказывает патреону в снятии денег. " "Проверьте, всё ли там у них в порядке." }) now = datetime.utcnow() user, is_created = User.objects.get_or_create( membership_platform_type=User.MEMBERSHIP_PLATFORM_PATREON, membership_platform_id=membership.user_id, defaults=dict( email=membership.email, full_name=membership.full_name[:120], avatar=upload_image_from_url(membership.image) if membership.image else None, membership_started_at=membership.started_at, membership_expires_at=membership.expires_at, created_at=now, updated_at=now, is_email_verified=False, is_profile_complete=False, # redirect new users to an intro page ), ) if is_created: user.balance = membership.lifetime_support_cents / 100 else: user.membership_expires_at = membership.expires_at user.balance = membership.lifetime_support_cents / 100 # TODO: remove when the real money comes in user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } user.save() session = Session.objects.create( user=user, token=random_string(length=32), created_at=now, expires_at=user.membership_expires_at, ) redirect_to = reverse("profile", args=[user.slug]) state = request.GET.get("state") if state: redirect_to += f"?{state}" response = redirect(redirect_to) response.set_cookie( key="token", value=session.token, max_age=settings.SESSION_COOKIE_AGE, httponly=True, secure=not settings.DEBUG, ) return response
def patreon_oauth_callback(request): code = request.GET.get("code") if not code: return render(request, "error.html", { "title": "Что-то сломалось между нами и патреоном", "message": "Так бывает. Попробуйте залогиниться еще раз" }) try: auth_data = patreon.fetch_auth_data(code) user_data = patreon.fetch_user_data(auth_data["access_token"]) except PatreonException as ex: if "invalid_grant" in str(ex): return render(request, "error.html", { "title": "Тут такое дело 😭", "message": "Авторизация патреона — говно. " "Она не сразу понимает, что вы стали патроном и отдаёт " "статус «отказано» в первые несколько минут, а иногда и часов. " "Я уже написал им в саппорт, но пока вам надо немного подождать и авторизоваться снова. " "Если долго не будет пускать — напишите мне в личку на патреоне." }) return render(request, "error.html", { "message": "Не получилось загрузить ваш профиль с серверов патреона. " "Попробуйте еще раз, наверняка оно починится. " f"Но если нет, то вот текст ошибки, с которым можно пожаловаться мне в личку:", "data": str(ex) }) membership = patreon.parse_active_membership(user_data) if not membership: return render(request, "error.html", { "title": "Надо быть патроном, чтобы состоять в Клубе", "message": "Кажется, вы не патроните <a href=\"https://www.patreon.com/join/vas3k\">@vas3k</a>. " "А это одно из основных требований для входа в Клуб.<br><br>" "Ещё иногда бывает, что ваш банк отказывает патреону в снятии денег. " "Проверьте, всё ли там у них в порядке." }) now = datetime.utcnow() # get user by patreon_id or email user = User.objects.filter(Q(patreon_id=membership.user_id) | Q(email=membership.email.lower())).first() if not user: # user is new, create it try: user = User.objects.create( patreon_id=membership.user_id, email=membership.email.lower(), full_name=membership.full_name[:120], avatar=upload_image_from_url(membership.image) if membership.image else None, membership_platform_type=User.MEMBERSHIP_PLATFORM_PATREON, membership_started_at=membership.started_at, membership_expires_at=membership.expires_at, balance=membership.lifetime_support_cents / 100, created_at=now, updated_at=now, is_email_verified=False, ) except IntegrityError: return render(request, "error.html", { "title": "💌 Придётся войти через почту", "message": "Пользователь с таким имейлом уже зарегистрирован, но не через патреон. " "Чтобы защититься от угона аккаунтов через подделку почты на патреоне, " "нам придётся сейчас попросить вас войти через почту." }) else: # user exists if user.deleted_at: return render(request, "error.html", { "title": "💀 Аккаунт был удалён", "message": "Войти через этот патреон больше не получится" }) # update membership dates user.balance = membership.lifetime_support_cents / 100 # TODO: remove when the real money comes in if membership.expires_at > user.membership_expires_at: user.membership_expires_at = membership.expires_at user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } user.save() # create a new session token to authorize the user session = Session.create_for_user(user) redirect_to = reverse("profile", args=[user.slug]) state = request.GET.get("state") if state: state_dict = dict(parse_qsl(state)) if "goto" in state_dict: redirect_to = state_dict["goto"] response = redirect(redirect_to) return set_session_cookie(response, user, session)
def patreon_oauth_callback(request): code = request.GET.get("code") if not code: return render( request, "error.html", { "message": "Что-то сломалось между нами и патреоном. Так бывает. Попробуйте залогиниться еще раз." }) try: auth_data = patreon.fetch_auth_data(code) user_data = patreon.fetch_user_data(auth_data["access_token"]) except PatreonException as ex: if "invalid_grant" in str(ex): return render( request, "error.html", { "message": "Тут такое дело. Авторизация патреона — говно. " "Она не сразу понимает, что вы стали моим патроном и отдаёт мне ошибку. " "Я уже написал им в саппорт, но пока вам надо немного подождать и авторизоваться снова. " "Обычно тогда срабатывает. Если нет — напишите мне в личные сообщения на патреоне." }) return render( request, "error.html", { "message": "Не получилось загрузить ваш профиль с серверов патреона. " "Попробуйте еще раз, наверняка оно починится. " f"Но если нет, то вот текст ошибки, с которым можно пожаловаться мне в личку:", "data": str(ex) }) membership = patreon.parse_active_membership(user_data) if not membership: return render( request, "error.html", { "message": "Надо быть патроном чтобы состоять в клубе.<br>" '<a href="https://www.patreon.com/join/vas3k">Станьте им здесь!</a>' }) now = datetime.utcnow() user, is_created = User.objects.get_or_create( membership_platform_type=User.MEMBERSHIP_PLATFORM_PATREON, membership_platform_id=membership.user_id, defaults=dict( email=membership.email, full_name=membership.full_name[:120], avatar=upload_image_from_url(membership.image) if membership.image else None, membership_started_at=membership.started_at, membership_expires_at=membership.expires_at, created_at=now, updated_at=now, is_email_verified=False, is_profile_complete=False, # redirect new users to an intro page ), ) if is_created: user.balance = membership.lifetime_support_cents / 100 else: user.membership_expires_at = membership.expires_at user.balance = membership.lifetime_support_cents / 100 # TODO: remove when the real money comes in user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } user.save() session = Session.objects.create( user=user, token=random_string(length=32), created_at=now, expires_at=first_day_of_next_month(now), ) redirect_to = reverse("profile", args=[user.slug]) state = request.GET.get("state") if state: redirect_to += f"?{state}" response = redirect(redirect_to) response.set_cookie( key="token", value=session.token, max_age=settings.SESSION_COOKIE_AGE, httponly=True, secure=not settings.DEBUG, ) return response
def handle(self, *args, **options): if options.get("email"): self.stdout.write( f"Selecting a user with email: {options['email']}") expiring_users = User.objects.filter(email=options["email"]) else: self.stdout.write( f"Selecting users with expired subscriptions " f"between {options['days_before']} days before and {options['days_after']} days after" ) expiring_users = User.objects\ .filter( membership_platform_type=User.MEMBERSHIP_PLATFORM_PATREON, membership_expires_at__gte=datetime.utcnow() - timedelta(days=options["days_before"]), membership_expires_at__lte=datetime.utcnow() + timedelta(days=options["days_after"]), )\ .all() for user in expiring_users: self.stdout.write(f"Checking user: {user.slug}") if user.membership_platform_type == User.MEMBERSHIP_PLATFORM_PATREON: if not user.membership_platform_data or "refresh_token" not in user.membership_platform_data: self.stdout.write(f"No auth data for user: {user.slug}") continue # refresh user data id needed try: auth_data = patreon.refresh_auth_data( user.membership_platform_data["refresh_token"]) user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } except PatreonException as ex: self.stdout.write( f"Can't refresh user data {user.slug}: {ex}. Cleaning up active sessions..." ) Session.objects.filter(user=user).delete() # fetch user pledge status try: user_data = fetch_user_data( user.membership_platform_data["access_token"]) self.stdout.write(f"Pledge status: {user_data}") except PatreonException as ex: self.stdout.write( f"Invalid patreon credentials for user {user.slug}: {ex}" ) Session.objects.filter(user=user).delete() continue # check the new expiration date membership = patreon.parse_active_membership(user_data) if membership: if membership.expires_at >= user.membership_expires_at: user.membership_expires_at = membership.expires_at user.balance = membership.lifetime_support_cents / 100 # TODO: ^^^ remove when the real money comes in self.stdout.write( f"New expiration date for user {user.slug} — {membership.expires_at}" ) else: Session.objects.filter(user=user).delete() user.save() self.stdout.write(f"User processed: {user.slug}") else: self.stderr.write( f"No renewing scenario for the platform: {user.membership_platform_type}" ) self.stdout.write("Done 🥙")
def patreon_oauth_callback(request): code = request.GET.get("code") if not code: return render( request, "error.html", { "title": "Что-то сломалось между нами и патреоном", "message": "Так бывает. Попробуйте залогиниться еще раз" }, status=500) try: auth_data = patreon.fetch_auth_data(code) user_data = patreon.fetch_user_data(auth_data["access_token"]) except PatreonException as ex: if "invalid_grant" in str(ex): return render( request, "error.html", { "title": "Тут такое дело 😭", "message": "Авторизация патреона — говно. " "Она не сразу понимает, что вы стали патроном и отдаёт " "статус «отказано» в первые несколько минут, а иногда и часов. " "Я уже написал им в саппорт, но пока вам надо немного подождать и авторизоваться снова. " "Если долго не будет пускать — напишите мне в личку на патреоне." }, status=503) return render( request, "error.html", { "message": "Не получилось загрузить ваш профиль с серверов патреона. " "Попробуйте еще раз, наверняка оно починится. " f"Но если нет, то вот текст ошибки, с которым можно пожаловаться мне в личку:", "data": str(ex) }, status=504) membership = patreon.parse_active_membership(user_data) if not membership: return render( request, "error.html", { "title": "Надо быть патроном, чтобы состоять в Клубе", "message": "Кажется, вы не патроните <a href=\"https://www.patreon.com/join/vas3k\">@vas3k</a>. " "А это одно из основных требований для входа в Клуб.<br><br>" "Ещё иногда бывает, что ваш банк отказывает патреону в снятии денег. " "Проверьте, всё ли там у них в порядке." }, status=402) now = datetime.utcnow() # get user by patreon_id or email user = User.objects.filter( Q(patreon_id=membership.user_id) | Q(email=membership.email.lower())).first() if not user: # user is new, do not allow patreon users to register return render( request, "error.html", { "title": "🤕 Регистрироваться через Патреон больше нельзя", "message": "Возможность входа через Патреон осталась только для легаси-юзеров, " "но создавать новые аккаунты в Клубе через него больше нельзя. " "Через Патреон регистрируется очень много виртуалов и прочих анонимов, " "так как им это дешево. Мы же устали их ловить и выгонять, " "потому решили полностью прикрыть регистрацию." }, status=400) else: # user exists if user.deleted_at: return render( request, "error.html", { "title": "💀 Аккаунт был удалён", "message": "Войти через этот патреон больше не получится" }, status=404) # update membership dates user.balance = membership.lifetime_support_cents / 100 if membership.expires_at > user.membership_expires_at: user.membership_expires_at = membership.expires_at user.membership_platform_data = { "access_token": auth_data["access_token"], "refresh_token": auth_data["refresh_token"], } user.save() # create a new session token to authorize the user session = Session.create_for_user(user) redirect_to = reverse("profile", args=[user.slug]) state = request.GET.get("state") if state: state_dict = dict(parse_qsl(state)) if "goto" in state_dict: redirect_to = state_dict["goto"] response = redirect(redirect_to) return set_session_cookie(response, user, session)