Beispiel #1
0
def post_donate_stripe(request: Request, amount: int, currency: str,
                       interval: str) -> dict:
    """Process a Stripe donation."""
    try:
        stripe.api_key = request.registry.settings["api_keys.stripe.secret"]
        publishable_key = request.registry.settings[
            "api_keys.stripe.publishable"]
        product_id = request.registry.settings[
            "stripe.recurring_donation_product_id"]
        site_name = request.registry.settings["tildes.site_name"]
    except KeyError:
        raise HTTPInternalServerError

    incr_counter("donation_initiations", type="stripe")

    if interval == "onetime":
        session = stripe.checkout.Session.create(
            payment_method_types=["card"],
            line_items=[{
                "name": f"One-time donation - {site_name}",
                "amount": int(amount * 100),
                "currency": currency,
                "quantity": 1,
            }],
            submit_type="donate",
            success_url=f"https://{site_name}/donate_success",
            cancel_url=f"https://docs.{site_name}/donate",
        )
    else:
        product = stripe.Product.retrieve(product_id)
        existing_plans = stripe.Plan.list(product=product,
                                          active=True,
                                          limit=100)

        # look through existing plans to see if there's already a matching one, or
        # create a new plan if not
        for existing_plan in existing_plans:
            if (existing_plan.amount == int(amount * 100)
                    and existing_plan.currency == currency.lower()
                    and existing_plan.interval == interval):
                plan = existing_plan
                break
        else:
            plan = stripe.Plan.create(
                amount=int(amount * 100),
                currency=currency,
                interval=interval,
                product=product,
            )

        session = stripe.checkout.Session.create(
            payment_method_types=["card"],
            subscription_data={"items": [{
                "plan": plan.id
            }]},
            success_url=f"https://{site_name}/donate_success",
            cancel_url=f"https://docs.{site_name}/donate",
        )

    return {"publishable_key": publishable_key, "session_id": session.id}
Beispiel #2
0
    def __init__(self, conversation: MessageConversation, sender: User, markdown: str):
        """Add a new reply to a message conversation."""
        self.conversation_id = conversation.conversation_id
        self.sender_id = sender.user_id
        self.markdown = markdown
        self.rendered_html = convert_markdown_to_safe_html(markdown)

        incr_counter("messages", type="reply")
Beispiel #3
0
def get_donate_success(request: Request) -> dict:
    """Display a message after a successful donation."""
    # pylint: disable=unused-argument

    # incrementing this metric on page-load and hard-coding Stripe isn't ideal, but it
    # should do the job for now
    incr_counter("donations", type="stripe")

    return {}
Beispiel #4
0
def post_register(
    request: Request,
    username: str,
    password: str,
    password_confirm: str,
    invite_code: str,
) -> HTTPFound:
    """Process a registration request."""
    if not request.params.get("accepted_terms"):
        raise HTTPUnprocessableEntity(
            "Terms of Use and Privacy Policy must be accepted.")

    if password != password_confirm:
        raise HTTPUnprocessableEntity(
            "Password and confirmation do not match.")

    code_row = _handle_invite_code(request, invite_code)

    # create the user and set inviter to the owner of the invite code
    user = User(username, password)
    if code_row:
        user.inviter_id = code_row.user_id

    # flush the user insert to db, will fail if username is already taken
    request.db_session.add(user)
    try:
        request.db_session.flush()
    except IntegrityError:
        raise HTTPUnprocessableEntity(
            "That username has already been registered.")

    # the flush above will generate the new user's ID, so use that to update the invite
    # code with info about the user that registered with it
    if code_row:
        code_row.invitee_id = user.user_id

    # subscribe the new user to all groups except ~test
    all_groups = request.query(Group).all()
    for group in all_groups:
        if group.path == "test":
            continue
        request.db_session.add(GroupSubscription(user, group))

    _send_welcome_message(user, request)

    incr_counter("registrations")

    # log the user in to the new account
    remember(request, user.user_id)

    # set request.user before logging so the user is associated with the event
    request.user = user
    request.db_session.add(Log(LogEventType.USER_REGISTER, request))

    # redirect to the front page
    raise HTTPFound(location="/")
Beispiel #5
0
    def __init__(self, sender: User, recipient: User, subject: str, markdown: str):
        """Create a new message conversation between two users."""
        self.sender_id = sender.user_id
        self.recipient_id = recipient.user_id
        self.unread_user_ids = [self.recipient_id]
        self.subject = subject
        self.markdown = markdown
        self.rendered_html = convert_markdown_to_safe_html(markdown)

        incr_counter("messages", type="conversation")
Beispiel #6
0
def get_comment_reply(request: Request) -> dict:
    """Get the reply form for a comment with Intercooler."""
    wait_mins = _reply_wait_minutes(request, request.user,
                                    request.context.user)
    if wait_mins:
        incr_counter("comment_back_and_forth_warnings")
        raise HTTPUnprocessableEntity(
            f"You can't reply to this user yet. Please wait {wait_mins} minutes."
        )

    return {"parent_comment": request.context}
Beispiel #7
0
    def create_text_topic(
        cls, group: Group, author: User, title: str, markdown: str = ""
    ) -> "Topic":
        """Create a new text topic."""
        new_topic = cls._create_base_topic(group, author, title)
        new_topic.topic_type = TopicType.TEXT
        new_topic.markdown = markdown

        incr_counter("topics", type="text")

        return new_topic
Beispiel #8
0
    def create_link_topic(
        cls, group: Group, author: User, title: str, link: str
    ) -> "Topic":
        """Create a new link topic."""
        new_topic = cls._create_base_topic(group, author, title)
        new_topic.topic_type = TopicType.LINK
        new_topic.link = link
        new_topic.original_url = link

        incr_counter("topics", type="link")

        return new_topic
Beispiel #9
0
    def create_link_topic(
        cls,
        group: Group,
        author: User,
        title: str,
        link: str,
    ) -> 'Topic':
        """Create a new link topic."""
        new_topic = cls._create_base_topic(group, author, title)
        new_topic.topic_type = TopicType.LINK
        new_topic.link = link

        incr_counter('topics', type='link')

        return new_topic
Beispiel #10
0
    def create_text_topic(
        cls,
        group: Group,
        author: User,
        title: str,
        markdown: str = '',
    ) -> 'Topic':
        """Create a new text topic."""
        new_topic = cls._create_base_topic(group, author, title)
        new_topic.topic_type = TopicType.TEXT
        new_topic.markdown = markdown

        incr_counter('topics', type='text')

        return new_topic
Beispiel #11
0
    def __init__(
        self,
        comment: Comment,
        user: User,
        label: CommentLabelOption,
        weight: float,
        reason: Optional[str] = None,
    ):
        """Add a new label to a comment."""
        # pylint: disable=too-many-arguments
        self.comment_id = comment.comment_id
        self.user_id = user.user_id
        self.label = label
        self.weight = weight
        self.reason = reason

        incr_counter("comment_labels", label=label.name)
Beispiel #12
0
    def __init__(
        self,
        topic: Topic,
        author: User,
        markdown: str,
        parent_comment: Optional["Comment"] = None,
    ):
        """Create a new comment."""
        self.topic = topic
        self.user_id = author.user_id
        if parent_comment:
            self.parent_comment_id = parent_comment.comment_id
        else:
            self.parent_comment_id = None

        self.markdown = markdown

        incr_counter("comments")
Beispiel #13
0
    def theme_cookie_tween(request: Request) -> Response:
        """Set the theme cookie if needed.

        Will only set a cookie if the user has a default theme set for their account
        but doesn't already have a theme cookie. This is necessary so that their default
        theme will apply to the Blog and Docs sites as well, since those sites are
        static and can't look up the user's default theme in the database.

        Temporarily, this tween is also being used to convert old theme cookies with
        "light" or "dark" values to the new "solarized-light" and "solarized-dark" ones.
        """
        response = handler(request)

        # only set the cookie on GET requests
        if request.method.upper() != "GET":
            return response

        current_theme = request.cookies.get("theme", "")

        # if they already have a valid theme cookie, we don't need to do anything
        if current_theme and current_theme not in ("light", "dark"):
            return response

        if current_theme in ("light", "dark"):
            # add the "solarized-" prefix to "light" / "dark" values
            new_theme = "solarized-" + current_theme
        elif request.user and request.user.theme_default:
            # otherwise (no theme cookie), set as the user's default
            new_theme = request.user.theme_default
        else:
            # if the user isn't logged in or doesn't have a default, do nothing
            return response

        response.set_cookie(
            "theme",
            new_theme,
            max_age=315360000,
            secure=True,
            domain="." + request.domain,
        )
        incr_counter("theme_cookie_tween_sets")

        return response
Beispiel #14
0
def _handle_invite_code(request: Request,
                        invite_code: str) -> Optional[UserInviteCode]:
    # invite code not required -- leave empty
    if request.registry.settings["tildes.open_registration"]:
        return None

    # attempt to fetch and lock the row for the specified invite code (lock prevents
    # concurrent requests from using the same invite code)
    lookup_code = UserInviteCode.prepare_code_for_lookup(invite_code)
    code_row = (
        request.query(UserInviteCode).filter(
            UserInviteCode.code == lookup_code,
            UserInviteCode.invitee_id == None,  # noqa
        ).with_for_update(skip_locked=True).one_or_none())

    if not code_row:
        incr_counter("invite_code_failures")
        raise HTTPUnprocessableEntity("Invalid invite code")
    return code_row
Beispiel #15
0
def post_login(
    request: Request, username: str, password: str, from_url: str
) -> Response:
    """Process a log in request."""
    incr_counter("logins")

    # Look up the user for the supplied username
    user = (
        request.query(User)
        .undefer_all_columns()
        .filter(User.username == username)
        .one_or_none()
    )

    # If that user doesn't exist or the password was wrong, error out
    if not user or not user.is_correct_password(password):
        incr_counter("login_failures")

        # log the failure - need to manually commit because of the exception
        log_entry = Log(LogEventType.USER_LOG_IN_FAIL, request, {"username": username})
        request.db_session.add(log_entry)
        request.tm.commit()

        raise HTTPUnprocessableEntity("Incorrect username or password")

    # Don't allow banned users to log in
    if user.is_banned:
        raise HTTPUnprocessableEntity("This account has been banned")

    # If 2FA is enabled, save username to session and make user enter code
    if user.two_factor_enabled:
        request.session["two_factor_username"] = username
        return render_to_response(
            "tildes:templates/intercooler/login_two_factor.jinja2",
            {"keep": request.params.get("keep"), "from_url": from_url},
            request=request,
        )

    raise finish_login(request, user, from_url)
Beispiel #16
0
def post_login(request: Request, username: str, password: str) -> HTTPFound:
    """Process a log in request."""
    incr_counter('logins')

    # Look up the user for the supplied username
    user = (request.query(User).undefer_all_columns().filter(
        User.username == username).one_or_none())

    # If that user doesn't exist or the password was wrong, error out
    if not user or not user.is_correct_password(password):
        incr_counter('login_failures')

        # log the failure - need to manually commit because of the exception
        log_entry = Log(LogEventType.USER_LOG_IN_FAIL, request,
                        {'username': username})
        request.db_session.add(log_entry)
        request.tm.commit()

        raise HTTPUnprocessableEntity('Incorrect username or password')

    # Don't allow banned users to log in
    if user.is_banned:
        raise HTTPUnprocessableEntity('This account has been banned')

    # Username/password were correct - attach the user_id to the session
    remember(request, user.user_id)

    # Depending on "keep me logged in", set session timeout to 1 year or 1 day
    if request.params.get('keep'):
        request.session.adjust_timeout_for_session(31_536_000)
    else:
        request.session.adjust_timeout_for_session(86_400)

    # set request.user before logging so the user is associated with the event
    request.user = user
    request.db_session.add(Log(LogEventType.USER_LOG_IN, request))

    raise HTTPFound(location='/')
Beispiel #17
0
def post_comment_reply(request: Request, markdown: str) -> dict:
    """Post a reply to a comment with Intercooler."""
    parent_comment = request.context

    wait_mins = _reply_wait_minutes(request, request.user, parent_comment.user)
    if wait_mins:
        incr_counter("comment_back_and_forth_warnings")
        raise HTTPUnprocessableEntity(
            f"You can't reply to this user yet. Please wait {wait_mins} minutes."
        )

    new_comment = Comment(
        topic=parent_comment.topic,
        author=request.user,
        markdown=markdown,
        parent_comment=parent_comment,
    )
    request.db_session.add(new_comment)

    request.db_session.add(
        LogComment(LogEventType.COMMENT_POST, request, new_comment))

    if CommentNotification.should_create_reply_notification(new_comment):
        notification = CommentNotification(
            parent_comment.user, new_comment,
            CommentNotificationType.COMMENT_REPLY)
        request.db_session.add(notification)

    _mark_comment_read_from_interaction(request, parent_comment)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (request.query(Comment).join_all_relationships().filter_by(
        comment_id=new_comment.comment_id).one())

    return {"comment": new_comment}
Beispiel #18
0
    def __init__(self, user: User, topic: Topic):
        """Create a new vote on a topic."""
        self.user = user
        self.topic = topic

        incr_counter("votes", target_type="topic")
Beispiel #19
0
    def __init__(self, user: User, topic: Topic) -> None:
        """Create a new vote on a topic."""
        self.user = user
        self.topic = topic

        incr_counter('votes', target_type='topic')
Beispiel #20
0
 def _update_creation_metric(self) -> None:
     incr_counter("topics", type=self.topic_type.name.lower())
Beispiel #21
0
 def _update_creation_metric(self) -> None:
     incr_counter("messages", type="conversation")
Beispiel #22
0
 def _update_creation_metric(self) -> None:
     incr_counter("votes", target_type="topic")
Beispiel #23
0
 def _update_creation_metric(self) -> None:
     incr_counter("comment_labels", label=self.label.name)
Beispiel #24
0
    def __init__(self, user: User, comment: Comment):
        """Create a new vote on a comment."""
        self.user = user
        self.comment = comment

        incr_counter("votes", target_type="comment")
Beispiel #25
0
def post_login(request: Request, username: str, password: str,
               from_url: str) -> Response:
    """Process a log in request."""
    incr_counter("logins")

    # Look up the user for the supplied username
    user = (request.query(User).undefer_all_columns().filter(
        User.username == username).one_or_none())

    # If the username doesn't exist, tell them so - usually this isn't considered a good
    # practice, but it's completely trivial to check if a username exists on Tildes
    # anyway (by visiting /user/<username>), so it's better to just let people know if
    # they're trying to log in with the wrong username
    if not user:
        incr_counter("login_failures")

        # log the failure - need to manually commit because of the exception
        log_entry = Log(
            LogEventType.USER_LOG_IN_FAIL,
            request,
            {
                "username": username,
                "reason": "Nonexistent username"
            },
        )
        request.db_session.add(log_entry)
        request.tm.commit()

        raise HTTPUnprocessableEntity("That username does not exist")

    if not user.is_correct_password(password):
        incr_counter("login_failures")

        # log the failure - need to manually commit because of the exception
        log_entry = Log(
            LogEventType.USER_LOG_IN_FAIL,
            request,
            {
                "username": username,
                "reason": "Incorrect password"
            },
        )
        request.db_session.add(log_entry)
        request.tm.commit()

        raise HTTPUnprocessableEntity("Incorrect password")

    # Don't allow banned users to log in
    if user.is_banned:
        if user.ban_expiry_time:
            # add an hour to the ban's expiry time since the cronjob runs hourly
            unban_time = user.ban_expiry_time + timedelta(hours=1)
            unban_time = unban_time.strftime("%Y-%m-%d %H:%M (UTC)")

            raise HTTPUnprocessableEntity(
                "That account is temporarily banned. "
                f"The ban will be lifted at {unban_time}")

        raise HTTPUnprocessableEntity("That account has been banned")

    # If 2FA is enabled, save username to session and make user enter code
    if user.two_factor_enabled:
        request.session["two_factor_username"] = username
        return render_to_response(
            "tildes:templates/intercooler/login_two_factor.jinja2",
            {
                "keep": request.params.get("keep"),
                "from_url": from_url
            },
            request=request,
        )

    raise finish_login(request, user, from_url)
Beispiel #26
0
 def _update_creation_metric(self) -> None:
     incr_counter("messages", type="reply")
Beispiel #27
0
 def _update_creation_metric(self) -> None:
     incr_counter("subscriptions")
Beispiel #28
0
    def __init__(self, user: User, group: Group) -> None:
        """Create a new subscription to a group."""
        self.user = user
        self.group = group

        incr_counter('subscriptions')
Beispiel #29
0
def post_register(
    request: Request,
    username: str,
    password: str,
    password_confirm: str,
    invite_code: str,
) -> HTTPFound:
    """Process a registration request."""
    if not request.params.get("accepted_terms"):
        raise HTTPUnprocessableEntity(
            "Terms of Use and Privacy Policy must be accepted.")

    if password != password_confirm:
        raise HTTPUnprocessableEntity(
            "Password and confirmation do not match.")

    # attempt to fetch and lock the row for the specified invite code (lock prevents
    # concurrent requests from using the same invite code)
    lookup_code = UserInviteCode.prepare_code_for_lookup(invite_code)
    code_row = (
        request.query(UserInviteCode).filter(
            UserInviteCode.code == lookup_code,
            UserInviteCode.invitee_id == None,  # noqa
        ).with_for_update(skip_locked=True).one_or_none())

    if not code_row:
        incr_counter("invite_code_failures")
        raise HTTPUnprocessableEntity("Invalid invite code")

    # create the user and set inviter to the owner of the invite code
    user = User(username, password)
    user.inviter_id = code_row.user_id

    # flush the user insert to db, will fail if username is already taken
    request.db_session.add(user)
    try:
        request.db_session.flush()
    except IntegrityError:
        raise HTTPUnprocessableEntity(
            "That username has already been registered.")

    # the flush above will generate the new user's ID, so use that to update the invite
    # code with info about the user that registered with it
    code_row.invitee_id = user.user_id

    # subscribe the new user to all groups except ~test
    all_groups = request.query(Group).all()
    for group in all_groups:
        if group.path == "test":
            continue
        request.db_session.add(GroupSubscription(user, group))

    _send_welcome_message(user, request)

    incr_counter("registrations")

    # log the user in to the new account
    remember(request, user.user_id)

    # set request.user before logging so the user is associated with the event
    request.user = user
    request.db_session.add(Log(LogEventType.USER_REGISTER, request))

    # redirect to the front page
    raise HTTPFound(location="/")
Beispiel #30
0
 def _update_creation_metric(self) -> None:
     incr_counter("comments")