Esempio n. 1
0
    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 🥙")
Esempio n. 2
0
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
Esempio n. 3
0
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)
Esempio n. 4
0
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 🥙")
Esempio n. 6
0
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)