Пример #1
0
class GoldApiController(RedditController):
    @validatedForm(
        VUser(),
        VGold(),
        VModhash(),
        public=VBoolean("public"),
        snoo_color=VSnooColor("snoo_color"),
        unvalidated_components=VJSON("components"),
    )
    def POST_snoovatar(self, form, jquery, public, snoo_color, unvalidated_components):
        if form.has_errors("components",
                           errors.NO_TEXT,
                           errors.TOO_LONG,
                           errors.BAD_STRING,
                          ):
            return
        if form.has_errors("snoo_color", errors.BAD_CSS_COLOR):
            return

        try:
            tailors = g.plugins["gold"].tailors_data
            validated = {}

            for tailor in tailors:
                tailor_name = tailor["name"]

                component = unvalidated_components.get(tailor_name)

                # if the tailor requires a selection, ensure there is one
                if not tailor["allow_clear"]:
                    require(component)

                # ensure this dressing exists
                dressing = component.get("dressingName")
                if dressing:
                    for d in tailor["dressings"]:
                        if dressing == d["name"]:
                            break
                    else:
                        raise RequirementException

                validated[tailor_name] = component
        except RequirementException:
            c.errors.add(errors.INVALID_SNOOVATAR, field="components")
            form.has_errors("components", errors.INVALID_SNOOVATAR)
            return

        SnoovatarsByAccount.save(
            user=c.user,
            name="snoo",
            public=public,
            snoo_color=snoo_color,
            components=validated,
        )
Пример #2
0
class FreeToPlayApiController(RedditController):
    @validate(VUser(),
              VModhash(),
              item_name=VRequired('item', errors.NO_NAME),
              target=VByName('target'))
    def POST_use_item(self, item_name, target):
        try:
            inventory.consume_item(c.user, item_name)
        except inventory.NoSuchItemError:
            abort(400)

        c.user.f2p = "participated"
        c.user._commit()

        item = items.get_item(item_name)
        if not item.is_target_valid(target):
            abort(400)
        item.on_use(c.user, target)

        return json.dumps(c.state_changes)
Пример #3
0
class BetaModeController(RedditController):
    @validate(
        VUser(),
        name=VPrintable('name', 15),
    )
    def GET_beta(self, name):
        user_exempt = beta_user_exempt(c.user)

        if name != g.beta_name or (g.beta_require_admin and not user_exempt):
            abort(404)

        content = BetaSettings(
            beta_name=g.beta_name,
            beta_title=g.beta_title,
            description_md=g.beta_description_md[0],
            feedback_sr=g.beta_feedback_sr,
            enabled=c.beta,
            require_gold=g.beta_require_gold and not user_exempt,
            has_gold=c.user_is_loggedin and c.user.gold,
        )

        return BoringPage(
            pagename=g.beta_title,
            content_id='beta-settings',
            content=content,
            show_sidebar=False,
        ).render()

    def GET_disable(self, **kwargs):
        # **kwargs included above to swallow pylons env arguments passed in
        # due to argspec inspection of decorator **kwargs.

        return BoringPage(
            pagename=_('disabling beta'),
            content_id='beta-disable',
            content=BetaDisable(),
            show_sidebar=False,
        ).render()
Пример #4
0
class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    def pre(self):
        set_extension(request.environ, "json")
        RedditController.pre(self)

    @require_oauth2_scope("read")
    @validate(VUser())
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self):
        """Fetch a list of multis belonging to the current user."""
        multis = LabeledMulti.by_owner(c.user)
        wrapped = wrap_things(*multis)
        resp = [w.render() for w in wrapped]
        return self.api_wrapper(resp)

    def _format_multi(self, multi):
        resp = wrap_things(multi)[0].render()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(multi=VMultiByPath("multipath", require_view=True))
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi)

    def _check_new_multi_path(self, path_info):
        if path_info['username'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        multi.visibility = data['visibility']

        multi.clear_srs()
        try:
            self._add_multi_srs(multi, data['subreddits'])
        except:
            multi._revert()
            raise

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info):
        self._check_new_multi_path(to_path_info)

        to_owner = Account._by_name(to_path_info['username'])

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        to_multi = self._copy_multi(from_multi, to_path_info)
        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        to_multi._commit()
        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info):
        """Rename a multi."""

        to_multi = self._copy_multi(from_multi, to_path_info)
        from_multi.delete()
        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
class PlaceController(RedditController):
    def pre(self):
        RedditController.pre(self)

        if not PLACE_SUBREDDIT.can_view(c.user):
            self.abort403()

        if c.user.in_timeout:
            self.abort403()

        if c.user._spam:
            self.abort403()

    @validate(
        is_embed=VBoolean("is_embed"),
        is_webview=VBoolean("webview", default=False),
        is_palette_hidden=VBoolean('hide_palette', default=False),
    )
    @allow_oauth2_access
    def GET_canvasse(self, is_embed, is_webview, is_palette_hidden):
        # oauth will try to force the response into json
        # undo that here by hacking extension, content_type, and render_style
        try:
            del(request.environ['extension'])
        except:
            pass
        request.environ['content_type'] = "text/html; charset=UTF-8"
        request.environ['render_style'] = "html"
        set_content_type()

        websocket_url = websockets.make_url("/place", max_age=3600)

        content = PlaceCanvasse()

        js_config = {
            "place_websocket_url": websocket_url,
            "place_canvas_width": CANVAS_WIDTH,
            "place_canvas_height": CANVAS_HEIGHT,
            "place_cooldown": 0 if c.user_is_admin else PIXEL_COOLDOWN_SECONDS,
            "place_fullscreen": is_embed or is_webview,
            "place_hide_ui": is_palette_hidden,
        }

        if c.user_is_loggedin and not c.user_is_admin:
            js_config["place_wait_seconds"] = get_wait_seconds(c.user)

        # this is a sad duplication of the same from reddit_base :(
        # if c.user_is_loggedin:
        #     PLACE_SUBREDDIT.record_visitor_activity("logged_in", c.user._fullname)
        # elif c.loid.serializable:
        #     PLACE_SUBREDDIT.record_visitor_activity("logged_out", c.loid.loid)

        try:
            js_config["place_active_visitors"] = get_activity_count()
        except ActivityError:
            pass

        if is_embed:
            # ensure we're off the cookie domain before allowing embedding
            if request.host != g.media_domain:
                abort(404)
            c.allow_framing = True

        if is_embed or is_webview:
            return PlaceEmbedPage(
                title="place",
                content=content,
                extra_js_config=js_config,
            ).render()
        else:
            return PlacePage(
                title="place",
                content=content,
                extra_js_config=js_config,
            ).render()

    @json_validate(
        VUser(),    # NOTE: this will respond with a 200 with an error body
        VModhash(),
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
        color=VInt("color", min=0, max=15),
    )
    @allow_oauth2_access
    def POST_draw(self, responder, x, y, color):

       #if c.user._date >= ACCOUNT_CREATION_CUTOFF:
       #    self.abort403()

        if PLACE_SUBREDDIT.is_banned(c.user):
            self.abort403()

        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if color is None:
            c.errors.add(errors.BAD_COLOR, field="color")

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER) or
                responder.has_errors("color", errors.BAD_COLOR)):
            # TODO: return 400 with parsable error message?
            return

        if c.user_is_admin:
            wait_seconds = 0
        else:
            wait_seconds = get_wait_seconds(c.user)

        if wait_seconds > 2:
            response.status = 429
            request.environ['extra_error_data'] = {
                "error": 429,
                "wait_seconds": wait_seconds,
            }
            return

        Pixel.create(c.user, color, x, y)

        c.user.set_flair(
            subreddit=PLACE_SUBREDDIT,
            text="({x},{y}) {time}".format(x=x, y=y, time=time.time()),
            css_class="place-%s" % color,
        )

        websockets.send_broadcast(
            namespace="/place",
            type="place",
            payload={
                "author": c.user.name,
                "x": x,
                "y": y,
                "color": color,
            }
        )

        events.place_pixel(x, y, color)
        cooldown = 0 if c.user_is_admin else PIXEL_COOLDOWN_SECONDS
        return {
            'wait_seconds': cooldown,
        }

    @json_validate(
        VUser(),    # NOTE: this will respond with a 200 with an error body
        VAdmin(),
        VModhash(),
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
        width=VInt("width", min=1, max=ADMIN_RECT_DRAW_MAX_SIZE,
                   coerce=True, num_default=1),
        height=VInt("height", min=1, max=ADMIN_RECT_DRAW_MAX_SIZE,
                    coerce=True, num_default=1),
    )
    @allow_oauth2_access
    def POST_drawrect(self, responder, x, y, width, height):
        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER)):
            # TODO: return 400 with parsable error message?
            return

        # prevent drawing outside of the canvas
        width = min(CANVAS_WIDTH - x, width)
        height = min(CANVAS_HEIGHT - y, height)

        batch_payload = []

        for _x in xrange(x, x + width):
            for _y in xrange(y, y + height):
                pixel = Pixel.create(None, 0, _x, _y)
                payload = {
                    "author": '',
                    "x": _x,
                    "y": _y,
                    "color": 0,
                }
                batch_payload.append(payload)

        websockets.send_broadcast(
            namespace="/place",
            type="batch-place",
            payload=batch_payload,
        )

    @json_validate(
        VUser(),
    )
    @allow_oauth2_access
    def GET_time_to_wait(self, responder):
        if c.user._date >= ACCOUNT_CREATION_CUTOFF:
            self.abort403()

        if c.user_is_admin:
            wait_seconds = 0
        else:
            wait_seconds = get_wait_seconds(c.user)

        return {
            "wait_seconds": wait_seconds,
        }

    @json_validate(
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
    )
    @allow_oauth2_access
    def GET_pixel(self, responder, x, y):
        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER)):
            return

        pixel = Pixel.get_pixel_at(x, y)
        if pixel and pixel["user_name"]:
            # pixels blanked out by admins will not have a user_name set
            return pixel
Пример #6
0
class QrCodeController(RedditController):
    @validate(
        meetup=validators.VMeetup("codename"), )
    def GET_portal(self, meetup):
        if meetup.state != "closed":
            if c.user_is_loggedin:
                content = pages.MeetupPortal(meetup=meetup)
            else:
                content = pages.LoggedOutMeetupPortal(meetup=meetup)
        else:
            content = pages.ClosedMeetupPortal(meetup=meetup)

        return pages.MeatspacePage(content=content,
                                   page_classes=["meatspace-portal"]).render()

    @validate(
        VUser(),
        meetup=validators.VMeetup("codename"),
    )
    def GET_configure_badge(self, meetup):
        if meetup.state not in BADGE_STATES:
            return redirect_to("/meetup/%s" % str(meetup._id))

        content = pages.ConversationStarterSelector(meetup, c.user)
        return pages.MeatspacePage(content=content).render()

    @validate(
        VUser(),
        meetup=validators.VMeetup("codename"),
        topic=validators.VConversationStarter("topic"),
    )
    def GET_badge(self, meetup, topic):
        if meetup.state not in BADGE_STATES:
            return redirect_to("/meetup/%s" % str(meetup._id))

        content = pages.QrCodeBadge(meetup, c.user, topic)
        return pages.MeatspaceBadgePage(content=content).render()

    @validate(
        VUser(),
        meetup=validators.VMeetup("codename"),
    )
    def GET_mobile_badge(self, meetup):
        if meetup.state not in BADGE_STATES:
            return redirect_to("/meetup/%s" % str(meetup._id))

        content = pages.MobileQrCodeBadge(meetup, c.user)
        return content.render()

    @validate(
        VUser(),
        meetup=validators.VMeetup("codename"),
        other=VExistingUname("user"),
        connected_with=VExistingUname("connected-with"),
        code=VInt("code"),
    )
    def GET_connect(self, meetup, other, code, connected_with):
        if meetup.state not in CONNECT_STATES:
            self.abort404()

        content = pages.QrCodeForm(
            meetup=meetup,
            other=other,
            code=code,
            connected_with=connected_with,
        )
        return pages.MeatspacePage(content=content).render()

    @validatedForm(
        VUser(),
        meetup=validators.VMeetup("codename"),
        other=VExistingUname("username"),
        code=VInt("code"),
    )
    def POST_connect(self, form, jquery, meetup, other, code):
        if meetup.state not in CONNECT_STATES:
            self.abort403()

        jquery("body .connection-success").hide()

        if form.has_errors("username", errors.NO_USER,
                           errors.USER_DOESNT_EXIST):
            return

        if c.user == other:
            c.errors.add(errors.MEETUP_NOT_WITH_SELF, field="username")
            form.set_error(errors.MEETUP_NOT_WITH_SELF, "username")
            return

        expected_code = utils.make_secret_code(meetup, other)
        if code != expected_code:
            g.log.warning("%r just tried an invalid code on %r", c.user.name,
                          other.name)
            c.errors.add(errors.MEETUP_INVALID_CODE, field="code")
            form.set_error(errors.MEETUP_INVALID_CODE, "code")
            return

        models.MeetupConnections._connect(meetup, c.user, other)
        models.MeetupConnectionsByAccount._connect(meetup, c.user, other)
        g.stats.simple_event("meetup.connection")

        form.redirect("/meetup/%s/connect?connected-with=%s" %
                      (meetup._id, other.name))

    @validate(
        VUser(),
        meetup=validators.VMeetup("codename"),
    )
    def GET_connections(self, meetup):
        all_connections = models.MeetupConnectionsByAccount._connections(
            meetup, c.user)
        connections = [a for a in all_connections if not a._deleted]

        content = pages.QrCodeConnections(
            meetup=meetup,
            connections=connections,
        )
        return pages.MeatspacePage(content=content).render()

    @validate(meetup=validators.VMeetup("codename"))
    def GET_connect_shortlink(self, meetup, user, code):
        if meetup.state not in CONNECT_STATES:
            self.abort404()

        params = urllib.urlencode({
            "user": user,
            "code": code,
        })
        return redirect_to("/meetup/%s/connect?%s" % (str(meetup._id), params),
                           _code=301)
Пример #7
0
class APIv1GoldController(OAuth2OnlyController):
    def _gift_using_creddits(self, recipient, months=1, thing_fullname=None,
            proxying_for=None):
        with creddits_lock(c.user):
            if not c.user.employee and c.user.gold_creddits < months:
                err = RedditError("INSUFFICIENT_CREDDITS")
                self.on_validation_error(err)

            note = None
            buyer = c.user
            if c.user.name.lower() in g.live_config["proxy_gilding_accounts"]:
                note = "proxy-%s" % c.user.name
                if proxying_for:
                    try:
                        buyer = Account._by_name(proxying_for)
                    except NotFound:
                        pass

            send_gift(
                buyer=buyer,
                recipient=recipient,
                months=months,
                days=months * 31,
                signed=False,
                giftmessage=None,
                thing_fullname=thing_fullname,
                note=note,
            )

            if not c.user.employee:
                c.user.gold_creddits -= months
                c.user._commit()

    @require_oauth2_scope("creddits")
    @validate(
        VUser(),
        target=VByName("fullname"),
    )
    @api_doc(
        api_section.gold,
        uri="/api/v1/gold/gild/{fullname}",
    )
    def POST_gild(self, target):
        if not isinstance(target, (Comment, Link)):
            err = RedditError("NO_THING_ID")
            self.on_validation_error(err)

        if target.subreddit_slow.quarantine:
            err = RedditError("GILDING_NOT_ALLOWED")
            self.on_validation_error(err)

        self._gift_using_creddits(
            recipient=target.author_slow,
            thing_fullname=target._fullname,
            proxying_for=request.POST.get("proxying_for"),
        )

    @require_oauth2_scope("creddits")
    @validate(
        VUser(),
        user=VAccountByName("username"),
        months=VInt("months", min=1, max=36),
    )
    @api_doc(
        api_section.gold,
        uri="/api/v1/gold/give/{username}",
    )
    def POST_give(self, user, months):
        self._gift_using_creddits(
            recipient=user,
            months=months,
            proxying_for=request.POST.get("proxying_for"),
        )
Пример #8
0
class DonateController(RedditController):
    def GET_closed(self):
        return pages.DonatePage(
            title=_("reddit donate"),
            content=pages.DonateClosed(),
        ).render()

    @validate(
        eligible=VAccountEligible(),
        organization=VOrganization("organization"),
    )
    def GET_landing(self, eligible, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if c.user_is_loggedin:
            nomination_count = DonationNominationsByAccount.count(c.user)
        else:
            nomination_count = None

        if organization:
            wrapped_organization = inject_nomination_status([organization])
        else:
            wrapped_organization = None

        content = pages.DonateLanding(eligible=eligible, )

        og_data = {
            "site_name": "reddit.com",
        }

        if organization:
            og_data[
                "title"] = "reddit donate: vote for %s" % organization.data[
                    "DisplayName"]
            og_data["url"] = join_urls(
                g.origin, "donate?organization=%s" % organization.data["EIN"])
        else:
            og_data["title"] = "reddit donate: giving 10% back"
            og_data["url"] = join_urls(g.origin, "donate")

        return pages.DonatePage(
            title=_("reddit donate"),
            content=content,
            og_data=og_data,
            extra_js_config={
                "unloadedNominations": nomination_count,
                "accountIsEligible": eligible,
                "organization": wrapped_organization,
            },
        ).render()

    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="donate_nominate_"),
        VAccountEligible(),
        organization=VOrganization("organization"),
    )
    def POST_nominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        if form.has_errors("eligible", errors.DONATE_ACCOUNT_NOT_ELIGIBLE):
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return
        else:
            VRatelimit.ratelimit(
                rate_user=True,
                prefix="donate_nominate_",
                seconds=NOMINATION_COOLDOWN,
            )

        DonationNominationsByAccount.nominate(
            c.user,
            organization,
        )

    @validatedForm(
        VUser(),
        VModhash(),
        organization=VOrganization("organization"),
    )
    def POST_unnominate(self, form, jquery, organization):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        DonationNominationsByAccount.unnominate(
            c.user,
            organization,
        )

    @json_validate(
        organization=VOrganization("organization"), )
    def GET_organization(self, responder, organization):
        """Look up a single org by EIN."""

        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if responder.has_errors("organization",
                                errors.DONATE_UNKNOWN_ORGANIZATION):
            return

        wrapped = inject_nomination_status([organization])
        return wrapped[0]

    @json_validate(
        prefix=VLength("prefix", min_length=3, max_length=100), )
    def GET_search(self, responder, prefix):
        """Get organizations by display-name prefix."""

        if not feature.is_enabled('reddit_donate'):
            return self.abort404()

        if responder.has_errors("prefix", errors.TOO_LONG, errors.TOO_SHORT):
            return

        organizations = DonationOrganizationsByPrefix.byPrefix(prefix)
        return inject_nomination_status(organizations)

    @json_validate(
        VUser(), )
    def GET_nominations(self, responder):
        if not feature.is_enabled('reddit_donate'):
            return self.abort404()
        nominated_org_ids = DonationNominationsByAccount.get_for(c.user)
        orgs = DonationOrganization.byEIN(nominated_org_ids)
        wrapped = inject_nomination_status(orgs, assume_nominated=True)
        return wrapped
Пример #9
0
class StripeController(GoldPaymentController):
    name = 'stripe'
    webhook_secret = g.STRIPE_WEBHOOK_SECRET
    event_type_mappings = {
        'charge.succeeded': 'succeeded',
        'charge.failed': 'failed',
        'charge.refunded': 'refunded',
        'customer.created': 'noop',
        'transfer.created': 'noop',
        'transfer.paid': 'noop',
    }

    @classmethod
    def process_response(cls):
        event_dict = json.loads(request.body)
        event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY)
        status = event.type
        if cls.event_type_mappings.get(status) == 'noop':
            return status, None, None, None, None

        charge = event.data.object
        description = charge.description
        try:
            passthrough, buyer_name = description.split('-', 1)
        except ValueError:
            g.log.error('stripe_error on charge: %s', charge)
            raise
        transaction_id = 'S%s' % charge.id
        pennies = charge.amount
        months, days = months_and_days_from_pennies(pennies)
        return status, passthrough, transaction_id, pennies, months

    @validatedForm(VUser(),
                   token=nop('stripeToken'),
                   passthrough=VPrintable("passthrough", max_length=50),
                   pennies=VInt('pennies'),
                   months=VInt("months"))
    def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months):
        """
        Submit charge to stripe.

        Called by GoldPayment form. This submits the charge to stripe, and gold
        will be applied once we receive a webhook from stripe.

        """

        try:
            payment_blob = validate_blob(passthrough)
        except GoldException as e:
            # This should never happen. All fields in the payment_blob
            # are validated on creation
            form.set_html('.status',
                          _('something bad happened, try again later'))
            g.log.debug('POST_goldcharge: %s' % e.message)
            return

        penny_months, days = months_and_days_from_pennies(pennies)
        if not months or months != penny_months:
            form.set_html('.status', _('stop trying to trick the form'))
            return

        stripe.api_key = g.STRIPE_SECRET_KEY

        try:
            customer = stripe.Customer.create(card=token)

            if (customer['active_card']['address_line1_check'] == 'fail' or
                customer['active_card']['address_zip_check'] == 'fail'):
                form.set_html('.status',
                              _('error: address verification failed'))
                form.find('.stripe-submit').removeAttr('disabled').end()
                return

            if customer['active_card']['cvc_check'] == 'fail':
                form.set_html('.status', _('error: cvc check failed'))
                form.find('.stripe-submit').removeAttr('disabled').end()
                return

            charge = stripe.Charge.create(
                amount=pennies,
                currency="usd",
                customer=customer['id'],
                description='%s-%s' % (passthrough, c.user.name)
            )
        except stripe.CardError as e:
            form.set_html('.status', 'error: %s' % e.message)
            form.find('.stripe-submit').removeAttr('disabled').end()
        except stripe.InvalidRequestError as e:
            form.set_html('.status', _('invalid request'))
        except stripe.APIConnectionError as e:
            form.set_html('.status', _('api error'))
        except stripe.AuthenticationError as e:
            form.set_html('.status', _('connection error'))
        except stripe.StripeError as e:
            form.set_html('.status', _('error'))
            g.log.error('stripe error: %s' % e)
        else:
            form.set_html('.status', _('payment submitted'))

            # webhook usually sends near instantly, send a message in case
            subject = _('gold payment')
            msg = _('your payment is being processed and gold will be'
                    ' delivered shortly')
            send_system_message(c.user, subject, msg)
Пример #10
0
class OAuth2FrontendController(RedditController):
    def check_for_bearer_token(self):
        pass

    def pre(self):
        RedditController.pre(self)
        require_https()

    def _abort_oauth_error(self, error):
        g.stats.simple_event('oauth2.errors.%s' % error)
        abort(BadRequestError(error))

    def _check_redirect_uri(self, client, redirect_uri):
        if (errors.OAUTH2_INVALID_CLIENT, 'client_id') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_CLIENT)

        if not redirect_uri or redirect_uri != client.redirect_uri:
            self._abort_oauth_error(errors.OAUTH2_INVALID_REDIRECT_URI)

    def _check_response_type_and_scope(self, response_type, scope):
        if (errors.INVALID_OPTION, 'response_type') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_RESPONSE_TYPE)

        if (errors.OAUTH2_INVALID_SCOPE, 'scope') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_SCOPE)

    def _check_client_type_and_duration(self, response_type, client, duration):
        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            self._abort_oauth_error(errors.OAUTH2_CONFIDENTIAL_TOKEN)

        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            self._abort_oauth_error(errors.OAUTH2_NO_REFRESH_TOKENS_ALLOWED)

    def _error_response(self, state, redirect_uri, as_fragment=False):
        """Return an error redirect."""
        resp = {"state": state}

        if (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
            resp["error"] = "access_denied"
        elif (errors.INVALID_MODHASH, None) in c.errors:
            resp["error"] = "access_denied"
        else:
            resp["error"] = "invalid_request"

        final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment)
        return self.redirect(final_redirect, code=302)

    def _check_employee_grants(self, client, scope):
        if not c.user.employee or not client or not scope:
            return
        if client._id in g.employee_approved_clients:
            return
        if client._id in g.mobile_auth_allowed_clients:
            return
        # The identity scope doesn't leak much, and we don't mind if employees
        # prove their identity to some external service
        if scope.scopes == {"identity"}:
            return
        error_page = RedditError(
            title=_('this app has not been approved for use with employee accounts'),
            message="",
        )
        request.environ["usable_error_content"] = error_page.render()
        self.abort403()

    @validate(VUser(),
              response_type = VOneOf("response_type", ("code", "token")),
              client = VOAuth2ClientID(),
              redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
              scope = VOAuth2Scope(),
              state = VRequired("state", errors.NO_TEXT),
              duration = VOneOf("duration", ("temporary", "permanent"),
                                default="temporary"))
    def GET_authorize(self, response_type, client, redirect_uri, scope, state,
                      duration):
        """
        First step in [OAuth 2.0](http://oauth.net/2/) authentication.
        End users will be prompted for their credentials (username/password)
        and asked if they wish to authorize the application identified by
        the **client_id** parameter with the permissions specified by the
        **scope** parameter.  They are then redirected to the endpoint on
        the client application's side specified by **redirect_uri**.

        If the user granted permission to the application, the response will
        contain a **code** parameter with a temporary authorization code
        which can be exchanged for an access token at
        [/api/v1/access_token](#api_method_access_token).

        **redirect_uri** must match the URI configured for the client in the
        [app preferences](/prefs/apps).  All errors will show a 400 error
        page along with some information on what option was wrong.
        """
        self._check_employee_grants(client, scope)

        # Check redirect URI first; it will ensure client exists
        self._check_redirect_uri(client, redirect_uri)

        self._check_response_type_and_scope(response_type, scope)

        self._check_client_type_and_duration(response_type, client, duration)

        if not c.errors:
            return OAuth2AuthorizationPage(client, redirect_uri, scope, state,
                                           duration, response_type).render()
        else:
            self._abort_oauth_error(errors.INVALID_OPTION)

    @validate(VUser(),
              VModhash(fatal=False),
              client = VOAuth2ClientID(),
              redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
              scope = VOAuth2Scope(),
              state = VRequired("state", errors.NO_TEXT),
              duration = VOneOf("duration", ("temporary", "permanent"),
                                default="temporary"),
              authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED),
              response_type = VOneOf("response_type", ("code", "token"),
                                     default="code"))
    def POST_authorize(self, authorize, client, redirect_uri, scope, state,
                       duration, response_type):
        """Endpoint for OAuth2 authorization."""

        self._check_employee_grants(client, scope)

        self._check_redirect_uri(client, redirect_uri)

        self._check_response_type_and_scope(response_type, scope)

        self._check_client_type_and_duration(response_type, client, duration)

        if c.errors:
            return self._error_response(state, redirect_uri,
                                        as_fragment=(response_type == "token"))

        if response_type == "code":
            code = OAuth2AuthorizationCode._new(client._id, redirect_uri,
                                            c.user._id36, scope,
                                            duration == "permanent")
            resp = {"code": code._id, "state": state}
            final_redirect = _update_redirect_uri(redirect_uri, resp)
            g.stats.simple_event('oauth2.POST_authorize.authorization_code_create')
        elif response_type == "token":
            device_id = get_device_id(client)
            token = OAuth2AccessToken._new(
                client_id=client._id,
                user_id=c.user._id36,
                scope=scope,
                device_id=device_id,
            )
            resp = OAuth2AccessController._make_new_token_response(token)
            resp["state"] = state
            final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment=True)
            g.stats.simple_event('oauth2.POST_authorize.access_token_create')

        # If this is the first time the user is logging in with an official
        # mobile app, gild them
        if (g.live_config.get('mobile_gild_first_login') and
                not c.user.has_used_mobile_app and
                client._id in g.mobile_auth_gild_clients):
            buyer = Account.system_user()
            admintools.adjust_gold_expiration(
                c.user, days=g.mobile_auth_gild_time)
            create_gift_gold(
                buyer._id, c.user._id, g.mobile_auth_gild_time,
                datetime.now(g.tz), signed=True, note='first_mobile_auth')
            subject = 'Let there be gold! Reddit just sent you Reddit gold!'
            message = (
                "Thank you for using the Reddit mobile app!  As a thank you "
                "for logging in during launch week, you've been gifted %s of "
                "Reddit Gold.\n\n"
                "Reddit Gold is Reddit's premium membership program, which "
                "grants you: \n"
                "An ads-free experience in Reddit's mobile apps, and\n"
                "Extra site features on desktop\n\n"
                "Discuss and get help on the features and perks at "
                "r/goldbenefits."
            ) % g.mobile_auth_gild_message
            message += '\n\n' + strings.gold_benefits_msg
            send_system_message(c.user, subject, message, add_to_sent=False)
            c.user.has_used_mobile_app = True
            c.user._commit()

        return self.redirect(final_redirect, code=302)
class LiveUpdateEventsController(RedditController):
    def GET_home(self):
        return pages.LiveUpdateMetaPage(
            title=_("reddit live"),
            content=pages.LiveUpdateHome(),
            page_classes=["liveupdate-home"],
        ).render()

    @validate(
        VEmployee(),
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateEvent("after"),
        before=VLiveUpdateEvent("before"),
        count=VCount("count"),
    )
    def GET_listing(self, filter, num, after, before, count):
        reverse = False
        if before:
            after = before
            reverse = True

        builder_cls = LiveUpdateEventBuilder
        wrapper = Wrapped
        listing_cls = Listing

        if filter == "open":
            title = _("live threads")
            query = queries.get_live_events("new", "all")
        elif filter == "closed":
            title = _("closed threads")
            query = queries.get_complete_events("new", "all")
        elif filter == "active":
            title = _("most active threads")
            query = queries.get_active_events()
        elif filter == "reported":
            if not c.user_is_admin:
                self.abort403()

            title = _("reported threads")
            query = queries.get_reported_events()
            builder_cls = LiveUpdateReportedEventBuilder
            wrapper = pages.LiveUpdateReportedEventRow
            listing_cls = pages.LiveUpdateReportedEventListing
        else:
            self.abort404()

        builder = builder_cls(
            query,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
            wrap=wrapper,
            skip=True,
        )

        listing = listing_cls(builder)

        return pages.LiveUpdateMetaPage(
            title=title,
            content=listing.listing(),
        ).render()

    @validate(
        VUser(),
    )
    def GET_create(self):
        return pages.LiveUpdateMetaPage(
            title=_("create live thread"),
            content=pages.LiveUpdateCreate(),
        ).render()

    @require_oauth2_scope("submit")
    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="liveupdate_create_"),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
        uri="/api/live/create",
    )
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
Пример #12
0
class GoldApiController(RedditController):
    @json_validate(VUser(), VGold(), VModhash(), deal=VLength('deal', 100))
    def POST_claim_gold_partner_deal_code(self, responder, deal):
        try:
            return {'code': GoldPartnerDealCode.claim_code(c.user, deal)}
        except GoldPartnerCodesExhaustedError:
            return {
                'error': 'GOLD_PARTNER_CODES_EXHAUSTED',
                'explanation': _("sorry, we're out of codes!")
            }

    @validatedForm(
        VUser(),
        VGold(),
        VModhash(),
        public=VBoolean("public"),
        snoo_color=VSnooColor("snoo_color"),
        unvalidated_components=VJSON("components"),
    )
    def POST_snoovatar(self, form, jquery, public, snoo_color,
                       unvalidated_components):
        if form.has_errors(
                "components",
                errors.NO_TEXT,
                errors.TOO_LONG,
                errors.BAD_STRING,
        ):
            return
        if form.has_errors("snoo_color", errors.BAD_CSS_COLOR):
            return

        try:
            tailors = g.plugins["gold"].tailors_data
            validated = {}

            for tailor in tailors:
                tailor_name = tailor["name"]

                component = unvalidated_components.get(tailor_name)

                # if the tailor requires a selection, ensure there is one
                if not tailor["allow_clear"]:
                    require(component)

                # ensure this dressing exists
                dressing = component.get("dressingName")
                if dressing:
                    for d in tailor["dressings"]:
                        if dressing == d["name"]:
                            break
                    else:
                        raise RequirementException

                validated[tailor_name] = component
        except RequirementException:
            c.errors.add(errors.INVALID_SNOOVATAR, field="components")
            form.has_errors("components", errors.INVALID_SNOOVATAR)
            return

        SnoovatarsByAccount.save(
            user=c.user,
            name="snoo",
            public=public,
            snoo_color=snoo_color,
            components=validated,
        )
Пример #13
0
class IpnController(RedditController):
    # Used when buying gold with creddits
    @validatedForm(VUser(),
                   months=VInt("months"),
                   passthrough=VPrintable("passthrough", max_length=50))
    def POST_spendcreddits(self, form, jquery, months, passthrough):
        if months is None or months < 1:
            form.set_html(".status", _("nice try."))
            return

        days = months * 31

        if not passthrough:
            raise ValueError("/spendcreddits got no passthrough?")

        blob_key, payment_blob = get_blob(passthrough)
        if payment_blob["goldtype"] != "gift":
            raise ValueError("/spendcreddits payment_blob %s has goldtype %s" %
                             (passthrough, payment_blob["goldtype"]))

        signed = payment_blob["signed"]
        giftmessage = _force_unicode(payment_blob["giftmessage"])
        recipient_name = payment_blob["recipient"]

        if payment_blob["account_id"] != c.user._id:
            fmt = ("/spendcreddits payment_blob %s has userid %d " +
                   "but c.user._id is %d")
            raise ValueError(fmt % passthrough, payment_blob["account_id"],
                             c.user._id)

        try:
            recipient = Account._by_name(recipient_name)
        except NotFound:
            raise ValueError(
                "Invalid username %s in spendcreddits, buyer = %s" %
                (recipient_name, c.user.name))

        if recipient._deleted:
            form.set_html(".status", _("that user has deleted their account"))
            return

        if not c.user.employee:
            if months > c.user.gold_creddits:
                raise ValueError(
                    "%s is trying to sneak around the creddit check" %
                    c.user.name)

            c.user.gold_creddits -= months
            c.user.gold_creddit_escrow += months
            c.user._commit()

        comment_id = payment_blob.get("comment")
        comment = send_gift(c.user, recipient, months, days, signed,
                            giftmessage, comment_id)

        if not c.user.employee:
            c.user.gold_creddit_escrow -= months
            c.user._commit()

        payment_blob["status"] = "processed"
        g.hardcache.set(blob_key, payment_blob, 86400 * 30)

        form.set_html(".status", _("the gold has been delivered!"))
        form.find("button").hide()

        if comment:
            gilding_message = make_comment_gold_message(comment,
                                                        user_gilded=True)
            jquery.gild_comment(comment_id, gilding_message, comment.gildings)

    @textresponse(paypal_secret=VPrintable('secret', 50),
                  payment_status=VPrintable('payment_status', 20),
                  txn_id=VPrintable('txn_id', 20),
                  paying_id=VPrintable('payer_id', 50),
                  payer_email=VPrintable('payer_email', 250),
                  mc_currency=VPrintable('mc_currency', 20),
                  mc_gross=VFloat('mc_gross'),
                  custom=VPrintable('custom', 50))
    def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id,
                 payer_email, mc_currency, mc_gross, custom):

        parameters = request.POST.copy()

        # Make sure it's really PayPal
        if paypal_secret != g.PAYPAL_SECRET:
            log_text("invalid IPN secret",
                     "%s guessed the wrong IPN secret" % request.ip, "warning")
            raise ValueError

        # Return early if it's an IPN class we don't care about
        response, psl = check_payment_status(payment_status)
        if response:
            return response

        # Return early if it's a txn_type we don't care about
        response, subscription = check_txn_type(parameters['txn_type'], psl)
        if subscription is None:
            subscr_id = None
        elif subscription == "new":
            subscr_id = parameters['subscr_id']
        elif subscription == "cancel":
            cancel_subscription(parameters['subscr_id'])
        else:
            raise ValueError("Weird subscription: %r" % subscription)

        if response:
            return response

        # Check for the debug flag, and if so, dump the IPN dict
        if g.cache.get("ipn-debug"):
            g.cache.delete("ipn-debug")
            dump_parameters(parameters)

        if mc_currency != 'USD':
            raise ValueError("Somehow got non-USD IPN %r" % mc_currency)

        if not (txn_id and paying_id and payer_email and mc_gross):
            dump_parameters(parameters)
            raise ValueError("Got incomplete IPN")

        pennies = int(mc_gross * 100)
        months, days = months_and_days_from_pennies(pennies)

        # Special case: autorenewal payment
        existing = existing_subscription(subscr_id, paying_id, custom)
        if existing:
            if existing != "deleted account":
                create_claimed_gold("P" + txn_id, payer_email, paying_id,
                                    pennies, days, None, existing._id,
                                    c.start_time, subscr_id)
                admintools.engolden(existing, days)

                g.log.info("Just applied IPN renewal for %s, %d days" %
                           (existing.name, days))
            return "Ok"

        # More sanity checks that all non-autorenewals should pass:

        if not custom:
            dump_parameters(parameters)
            raise ValueError("Got IPN with txn_id=%s and no custom" % txn_id)

        self.finish(parameters, "P" + txn_id, payer_email, paying_id,
                    subscr_id, custom, pennies, months, days)

    def finish(self, parameters, txn_id, payer_email, paying_id, subscr_id,
               custom, pennies, months, days):

        blob_key, payment_blob = get_blob(custom)

        buyer_id = payment_blob.get('account_id', None)
        if not buyer_id:
            dump_parameters(parameters)
            raise ValueError("No buyer_id in IPN with custom='%s'" % custom)
        try:
            buyer = Account._byID(buyer_id)
        except NotFound:
            dump_parameters(parameters)
            raise ValueError("Invalid buyer_id %d in IPN with custom='%s'" %
                             (buyer_id, custom))

        if subscr_id:
            buyer.gold_subscr_id = subscr_id

        instagift = False
        if payment_blob['goldtype'] in ('autorenew', 'onetime'):
            admintools.engolden(buyer, days)

            subject = _("Eureka! Thank you for investing in reddit gold!")

            message = _("Thank you for buying reddit gold. Your patronage "
                        "supports the site and makes future development "
                        "possible. For example, one month of reddit gold "
                        "pays for 5 instance hours of reddit's servers.")
            message += "\n\n" + strings.gold_benefits_msg
            if g.lounge_reddit:
                message += "\n* " + strings.lounge_msg
        elif payment_blob['goldtype'] == 'creddits':
            buyer._incr("gold_creddits", months)
            buyer._commit()
            subject = _("Eureka! Thank you for investing in reddit gold "
                        "creddits!")

            message = _("Thank you for buying creddits. Your patronage "
                        "supports the site and makes future development "
                        "possible. To spend your creddits and spread reddit "
                        "gold, visit [/gold](/gold) or your favorite "
                        "person's user page.")
            message += "\n\n" + strings.gold_benefits_msg + "\n\n"
            message += _("Thank you again for your support, and have fun "
                         "spreading gold!")
        elif payment_blob['goldtype'] == 'gift':
            recipient_name = payment_blob.get('recipient', None)
            try:
                recipient = Account._by_name(recipient_name)
            except NotFound:
                dump_parameters(parameters)
                raise ValueError(
                    "Invalid recipient_name %s in IPN/GC with custom='%s'" %
                    (recipient_name, custom))
            signed = payment_blob.get("signed", False)
            giftmessage = _force_unicode(payment_blob.get("giftmessage", ""))
            comment_id = payment_blob.get("comment")
            send_gift(buyer, recipient, months, days, signed, giftmessage,
                      comment_id)
            instagift = True
            subject = _("Thanks for giving the gift of reddit gold!")
            message = _("Your classy gift to %s has been delivered.\n\n"
                        "Thank you for gifting reddit gold. Your patronage "
                        "supports the site and makes future development "
                        "possible.") % recipient.name
            message += "\n\n" + strings.gold_benefits_msg + "\n\n"
            message += _("Thank you again for your support, and have fun "
                         "spreading gold!")
        else:
            dump_parameters(parameters)
            raise ValueError("Got status '%s' in IPN/GC" %
                             payment_blob['status'])

        # Reuse the old "secret" column as a place to record the goldtype
        # and "custom", just in case we need to debug it later or something
        secret = payment_blob['goldtype'] + "-" + custom

        if instagift:
            status = "instagift"
        else:
            status = "processed"

        create_claimed_gold(txn_id,
                            payer_email,
                            paying_id,
                            pennies,
                            days,
                            secret,
                            buyer_id,
                            c.start_time,
                            subscr_id,
                            status=status)

        message = append_random_bottlecap_phrase(message)

        send_system_message(buyer, subject, message, distinguished='gold-auto')

        payment_blob["status"] = "processed"
        g.hardcache.set(blob_key, payment_blob, 86400 * 30)
Пример #14
0
class ButtonApiController(ApiController):
    @validate(
        VUser(),
        VModhash(),
        seconds_remaining=VInt('seconds', min=0, max=60),
        previous_seconds=VInt('prev_seconds'),
        tick_time=nop('tick_time'),
        tick_mac=nop('tick_mac'),
    )
    def POST_press_button(self, seconds_remaining, previous_seconds, tick_time, tick_mac):
        if not g.live_config['thebutton_is_active']:
            return

        if c.user._date > ACCOUNT_CREATION_CUTOFF:
            return

        user_has_pressed = ButtonPressByUser.has_pressed(c.user)

        if user_has_pressed and not c.user.employee:
            return

        if has_timer_expired():
            # time has expired: no longer possible to press the button
            return

        has_started = has_timer_started()

        if not has_started:
            # the timer can only be started through reddit-shell
            return

        cheater = False
        if (seconds_remaining is None or
                previous_seconds is None or
                tick_time is None or
                tick_mac is None):
            # incomplete info from client, just let them press it anyways
            seconds_remaining = max(0, int(get_seconds_left()))
        elif not check_tick_mac(previous_seconds, tick_time, tick_mac):
            # can't trust the values sent by the client
            seconds_remaining = max(0, int(get_seconds_left()))
            cheater = True
        else:
            # client sent a valid mac so we can trust:
            # previous_seconds - the timer value at the last tick
            # tick_time - the datetime at the last tick

            # check to make sure tick_time wasn't too long ago
            then = str_to_datetime(tick_time)
            now = datetime.now(g.tz)
            if then and (now - then).total_seconds() > 60:
                # client sent an old (but potentially valid) mac, etc.
                seconds_remaining = max(0, int(get_seconds_left()))
                cheater = True

            # GOTCHA: the client actually sends the same value for
            # previous_seconds and seconds_remaining so make sure those match.
            # If the client sent down its own ticking down timer as
            # seconds_remaining we would want to compare to previous_seconds to
            # make sure they weren't too far apart
            if previous_seconds != seconds_remaining:
                seconds_remaining = max(0, int(get_seconds_left()))
                cheater = True

        press_button(c.user)
        g.stats.simple_event("thebutton.press")
        if cheater:
            g.stats.simple_event("thebutton.cheater")

        # don't flair on first press (the starter)
        if not has_started:
            return

        if user_has_pressed:
            # don't flair on multiple employee presses
            return

        if cheater:
            flair_css = "cheater"
        elif seconds_remaining > 51:
            flair_css = "press-6"
        elif seconds_remaining > 41:
            flair_css = "press-5"
        elif seconds_remaining > 31:
            flair_css = "press-4"
        elif seconds_remaining > 21:
            flair_css = "press-3"
        elif seconds_remaining > 11:
            flair_css = "press-2"
        else:
            flair_css = "press-1"

        flair_text = "%ss" % seconds_remaining

        setattr(c.user, 'flair_%s_text' % g.live_config["thebutton_srid"], flair_text)
        setattr(c.user, 'flair_%s_css_class' % g.live_config["thebutton_srid"], flair_css)
        c.user._commit()
Пример #15
0
class APIv1GoldController(OAuth2ResourceController):
    handles_csrf = True

    def pre(self):
        OAuth2ResourceController.pre(self)
        if request.method != "OPTIONS":
            self.authenticate_with_token()
            self.set_up_user_context()
        self.run_sitewide_ratelimits()

    def try_pagecache(self):
        pass

    @staticmethod
    def on_validation_error(error):
        abort_with_error(error, error.code or 400)

    def _gift_using_creddits(self,
                             recipient,
                             months=1,
                             thing_fullname=None,
                             proxying_for=None):
        with creddits_lock(c.user):
            if not c.user.employee and c.user.gold_creddits < months:
                err = RedditError("INSUFFICIENT_CREDDITS")
                self.on_validation_error(err)

            note = None
            buyer = c.user
            if c.user.name.lower() in g.live_config["proxy_gilding_accounts"]:
                note = "proxy-%s" % c.user.name
                if proxying_for:
                    try:
                        buyer = Account._by_name(proxying_for)
                    except NotFound:
                        pass

            send_gift(
                buyer=buyer,
                recipient=recipient,
                months=months,
                days=months * 31,
                signed=False,
                giftmessage=None,
                thing_fullname=thing_fullname,
                note=note,
            )

            if not c.user.employee:
                c.user.gold_creddits -= months
                c.user._commit()

    @require_oauth2_scope("creddits")
    @validate(
        VUser(),
        target=VByName("fullname"),
    )
    @api_doc(
        api_section.gold,
        uri="/api/v1/gold/gild/{fullname}",
    )
    def POST_gild(self, target):
        if not isinstance(target, (Comment, Link)):
            err = RedditError("NO_THING_ID")
            self.on_validation_error(err)

        self._gift_using_creddits(
            recipient=target.author_slow,
            thing_fullname=target._fullname,
            proxying_for=request.POST.get("proxying_for"),
        )

    @require_oauth2_scope("creddits")
    @validate(
        VUser(),
        user=VAccountByName("username"),
        months=VInt("months", min=1, max=36),
    )
    @api_doc(
        api_section.gold,
        uri="/api/v1/gold/give/{username}",
    )
    def POST_give(self, user, months):
        self._gift_using_creddits(
            recipient=user,
            months=months,
            proxying_for=request.POST.get("proxying_for"),
        )
Пример #16
0
class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    def pre(self):
        set_extension(request.environ, "json")
        RedditController.pre(self)

    def _format_multi_list(self, multis, viewer, expand_srs):
        templ = LabeledMultiJsonTemplate(expand_srs)
        resp = [
            templ.render(multi).finalize() for multi in multis
            if multi.can_view(viewer)
        ]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName("username"),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(api_section.multis, uri="/api/multi/user/{username}")
    def GET_list_multis(self, user, expand_srs):
        """Fetch a list of public multis belonging to `username`"""
        multis = LabeledMulti.by_owner(user)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(
        sr=VSRByName('srname'),
        expand_srs=VBoolean("expand_srs"),
    )
    def GET_list_sr_multis(self, sr, expand_srs):
        """Fetch a list of public multis belonging to subreddit `srname`"""
        multis = LabeledMulti.by_owner(sr)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(VUser(), expand_srs=VBoolean("expand_srs"))
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self, expand_srs):
        """Fetch a list of multis belonging to the current user."""
        multis = LabeledMulti.by_owner(c.user)
        return self._format_multi_list(multis, c.user, expand_srs)

    def _format_multi(self, multi, expand_sr_info=False):
        multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi)
        return self.api_wrapper(multi_info.finalize())

    @require_oauth2_scope("read")
    @validate(
        multi=VMultiByPath("multipath", require_view=True),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi, expand_srs):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi, expand_srs)

    def _check_new_multi_path(self, path_info):
        if path_info['prefix'] == 'r':
            return self._check_sr_multi_path(path_info)

        return self._check_user_multi_path(path_info)

    def _check_user_multi_path(self, path_info):
        if path_info['owner'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')
        return c.user

    def _check_sr_multi_path(self, path_info):
        try:
            sr = Subreddit._by_name(path_info['owner'])
        except NotFound:
            raise RedditError('SUBREDDIT_NOEXIST', code=404)

        if (not sr.is_moderator_with_perms(c.user, 'config')
                and not c.user_is_admin):
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

        return sr

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        srs = data.pop('subreddits', None)
        if srs is not None:
            multi.clear_srs()
            try:
                self._add_multi_srs(multi, srs)
            except:
                multi._revert()
                raise

        if 'icon_name' in data:
            try:
                multi.set_icon_by_name(data.pop('icon_name'))
            except:
                multi._revert()
                raise

        for key, val in data.iteritems():
            if key in WRITABLE_MULTI_FIELDS:
                setattr(multi, key, val)

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath", required=False),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        if not path_info and "path" in data:
            path_info = VMultiPath("").run(data["path"])
        elif 'display_name' in data:
            # if path not provided, create multi for user
            path = LabeledMulti.slugify(c.user, data['display_name'])
            path_info = VMultiPath("").run(path)

        if not path_info:
            raise RedditError('BAD_MULTI_PATH', code=400)

        owner = self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        owner = self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info, rename=False):
        """Copy a multi to a user account."""

        to_owner = self._check_new_multi_path(to_path_info)

        # rename requires same owner
        if rename and from_multi.owner != to_owner:
            raise RedditError('MULTI_CANNOT_EDIT', code=400)

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info, display_name):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        if not to_path_info:
            if display_name:
                # if path not provided, copy multi to same owner
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info)

        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        if display_name:
            to_multi.display_name = display_name
        to_multi._commit()

        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info, display_name):
        """Rename a multi."""
        if not to_path_info:
            if display_name:
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info, rename=True)

        if display_name:
            to_multi.display_name = display_name
            to_multi._commit()
        from_multi.delete()

        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
Пример #17
0
class APIv1UserController(OAuth2OnlyController):
    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(api_section.account)
    def GET_me(self):
        """Returns the identity of the user currently authenticated via OAuth."""
        resp = IdentityJsonTemplate().data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("identity")
    @validate(
        VUser(),
        fields=VList(
            "fields",
            choices=PREFS_JSON_SPEC.spec.keys(),
            error=errors.errors.NON_PREFERENCE,
        ),
    )
    @api_doc(api_section.account, uri='/api/v1/me/prefs')
    def GET_prefs(self, fields):
        """Return the preference settings of the logged in user"""
        resp = PrefsJsonTemplate(fields).data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName('username'), )
    @api_doc(
        section=api_section.users,
        uri='/api/v1/user/{username}/trophies',
    )
    def GET_usertrophies(self, user):
        """Return a list of trophies for the a given user."""
        return self.api_wrapper(get_usertrophies(user))

    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/trophies',
    )
    def GET_trophies(self):
        """Return a list of trophies for the current user."""
        return self.api_wrapper(get_usertrophies(c.oauth_user))

    @require_oauth2_scope("mysubreddits")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/karma',
    )
    def GET_karma(self):
        """Return a breakdown of subreddit karma."""
        karmas = c.oauth_user.all_karmas(include_old=False)
        resp = KarmaListJsonTemplate().render(karmas)
        return self.api_wrapper(resp.finalize())

    PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True)

    @require_oauth2_scope("account")
    @validate(
        VUser(),
        validated_prefs=PREFS_JSON_VALIDATOR,
    )
    @api_doc(api_section.account,
             json_model=PREFS_JSON_VALIDATOR,
             uri='/api/v1/me/prefs')
    def PATCH_prefs(self, validated_prefs):
        user_prefs = c.user.preferences()
        for short_name, new_value in validated_prefs.iteritems():
            pref_name = "pref_" + short_name
            user_prefs[pref_name] = new_value
        vprefs.filter_prefs(user_prefs, c.user)
        vprefs.set_prefs(c.user, user_prefs)
        c.user._commit()
        return self.api_wrapper(PrefsJsonTemplate().data(c.user))

    FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({
        "name":
        VAccountByName("name"),
        "note":
        VLength("note", 300),
    })
    FRIEND_JSON_VALIDATOR = VValidatedJSON("json",
                                           spec=FRIEND_JSON_SPEC,
                                           body=True)

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend=VAccountByName('username'),
        notes_json=FRIEND_JSON_VALIDATOR,
    )
    @api_doc(api_section.users,
             json_model=FRIEND_JSON_VALIDATOR,
             uri='/api/v1/me/friends/{username}')
    def PUT_friends(self, friend, notes_json):
        """Create or update a "friend" relationship.

        This operation is idempotent. It can be used to add a new
        friend, or update an existing friend (e.g., add/change the
        note on that friend)

        """
        err = None
        if 'name' in notes_json and notes_json['name'] != friend:
            # The 'name' in the JSON is optional, but if present, must
            # match the username from the URL
            err = errors.RedditError('BAD_USERNAME', fields='name')
        if 'note' in notes_json and not c.user.gold:
            err = errors.RedditError('GOLD_REQUIRED', fields='note')
        if err:
            self.on_validation_error(err)

        # See if the target is already an existing friend.
        # If not, create the friend relationship.
        friend_rel = Account.get_friend(c.user, friend)
        rel_exists = bool(friend_rel)
        if not friend_rel:
            friend_rel = c.user.add_friend(friend)
            response.status = 201

        if 'note' in notes_json:
            note = notes_json['note'] or ''
            if not rel_exists:
                # If this is a newly created friend relationship,
                # the cache needs to be updated before a note can
                # be applied
                c.user.friend_rels_cache(_update=True)
            c.user.add_friend_note(friend, note)
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('mysubreddits')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def GET_friends(self, friend_rel):
        """Get information about a specific 'friend', such as notes."""
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def DELETE_friends(self, friend_rel):
        """Stop being friends with a user."""
        c.user.remove_friend(friend_rel._thing2)
        if c.user.gold:
            c.user.friend_rels_cache(_update=True)
        response.status = 204
Пример #18
0
class LiveUpdateController(RedditController):
    def __before__(self, event):
        RedditController.__before__(self)

        if event:
            try:
                c.liveupdate_event = LiveUpdateEvent._byID(event)
            except tdb_cassandra.NotFound:
                pass

        if not c.liveupdate_event:
            self.abort404()

        if c.user_is_loggedin:
            c.liveupdate_permissions = \
                    c.liveupdate_event.get_permissions(c.user)

            # revoke some permissions from everyone after closing
            if c.liveupdate_event.state != "live":
                c.liveupdate_permissions = (c.liveupdate_permissions
                    .without("update")
                    .without("close")
                )

            if c.user_is_admin:
                c.liveupdate_permissions = ContributorPermissionSet.SUPERUSER
        else:
            c.liveupdate_permissions = ContributorPermissionSet.NONE

        if c.liveupdate_event.banned and not c.liveupdate_permissions:
            error_page = RedditError(
                title=_("this thread has been banned"),
                message="",
                image="subreddit-banned.png",
            )
            request.environ["usable_error_content"] = error_page.render()
            self.abort403()

        if (c.liveupdate_event.nsfw and
                not c.over18 and
                request.host != g.media_domain and  # embeds are special
                c.render_style == "html"):
            return self.intermediate_redirect("/over18", sr_path=False)

    @require_oauth2_scope("read")
    @validate(
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateID("after"),
        before=VLiveUpdateID("before"),
        count=VCount("count"),
        is_embed=VBoolean("is_embed", docs={"is_embed": "(internal use only)"}),
        style_sr=VSRByName("stylesr"),
    )
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}",
        supports_rss=True,
        notes=[paginated_listing.doc_note],
    )
    def GET_listing(self, num, after, before, count, is_embed, style_sr):
        """Get a list of updates posted in this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """

        # preemptively record activity for clients that don't send pixel pings.
        # this won't capture their continued visit, but will at least show a
        # correct activity count for short lived connections.
        record_activity(c.liveupdate_event._id)

        reverse = False
        if before:
            reverse = True
            after = before

        query = LiveUpdateStream.query([c.liveupdate_event._id],
                                       count=num, reverse=reverse)
        if after:
            query.column_start = after
        builder = LiveUpdateBuilder(query=query, skip=True,
                                    reverse=reverse, num=num,
                                    count=count)
        listing = pages.LiveUpdateListing(builder)
        wrapped_listing = listing.listing()

        if c.user_is_loggedin:
            report_type = LiveUpdateReportsByAccount.get_report(
                c.user, c.liveupdate_event)
        else:
            report_type = None

        content = pages.LiveUpdateEventApp(
            event=c.liveupdate_event,
            listing=wrapped_listing,
            show_sidebar=not is_embed,
            report_type=report_type,
        )

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + "/about.json",
            Wrapped(c.liveupdate_event),
        )

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + ".json",
            wrapped_listing,
        )

        if not is_embed:
            return pages.LiveUpdateEventAppPage(
                content=content,
                page_classes=['liveupdate-app'],
            ).render()
        else:
            # ensure we're off the cookie domain before allowing embedding
            if request.host != g.media_domain:
                abort(404)
            c.allow_framing = True

            # interstitial redirects and nsfw settings are funky on the media
            # domain. just disable nsfw embeds.
            if c.liveupdate_event.nsfw:
                embed_page = pages.LiveUpdateEventEmbed(
                    content=pages.LiveUpdateNSFWEmbed(),
                )
                request.environ["usable_error_content"] = embed_page.render()
                abort(403)

            embed_page = pages.LiveUpdateEventEmbed(
                content=content,
                page_classes=['liveupdate-app'],
            )

            if style_sr and getattr(style_sr, "type", "private") != "private":
                c.can_apply_styles = True
                c.allow_styles = True
                embed_page.subreddit_stylesheet_url = \
                    Reddit.get_subreddit_stylesheet_url(style_sr)

            return embed_page.render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/updates/{update_id}",
    )
    def GET_focus(self, target):
        """Get details about a specific update in a live thread."""
        try:
            target = uuid.UUID(target)
        except (TypeError, ValueError):
            self.abort404()

        try:
            update = LiveUpdateStream.get_update(c.liveupdate_event, target)
        except tdb_cassandra.NotFound:
            self.abort404()

        if update.deleted:
            self.abort404()

        query = FocusQuery([update])

        builder = LiveUpdateBuilder(
            query=query, skip=True, reverse=True, num=1, count=0)
        listing = pages.LiveUpdateListing(builder)
        wrapped_listing = listing.listing()

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + ".json",
            wrapped_listing,
        )

        content = pages.LiveUpdateFocusApp(
            event=c.liveupdate_event,
            listing=wrapped_listing,
        )

        return pages.LiveUpdateEventFocusPage(
            content=content,
            focused_update=update,
            page_classes=["liveupdate-focus"],
        ).render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/about",
    )
    def GET_about(self):
        """Get some basic information about the live thread.

        See also: [/api/live/*thread*/edit](#POST_api_live_{thread}_edit).

        """
        if not is_api():
            self.abort404()
        content = Wrapped(c.liveupdate_event)
        return pages.LiveUpdateEventPage(content=content).render()

    @require_oauth2_scope("read")
    @base_listing
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/discussions",
        supports_rss=True,
    )
    def GET_discussions(self, num, after, reverse, count):
        """Get a list of reddit submissions linking to this thread."""
        builder = url_links_builder(
            url="/live/" + c.liveupdate_event._id,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
        )
        listing = LinkListing(builder).listing()
        return pages.LiveUpdateEventPage(
            content=listing,
        ).render()

    def GET_edit(self):
        if not (c.liveupdate_permissions.allow("settings") or
                c.liveupdate_permissions.allow("close")):
            abort(403)

        return pages.LiveUpdateEventPage(
            content=pages.LiveUpdateEventConfiguration(),
        ).render()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("settings"),
        VModhash(),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_edit(self, form, jquery, title, description, resources, nsfw):
        """Configure the thread.

        Requires the `settings` permission for this thread.

        See also: [/live/*thread*/about.json](#GET_live_{thread}_about.json).

        """
        if not is_event_configuration_valid(form):
            return

        changes = {}
        if title != c.liveupdate_event.title:
            changes["title"] = title
        if description != c.liveupdate_event.description:
            changes["description"] = description
            changes["description_html"] = safemarkdown(description, nofollow=True) or ""
        if resources != c.liveupdate_event.resources:
            changes["resources"] = resources
            changes["resources_html"] = safemarkdown(resources, nofollow=True) or ""
        if nsfw != c.liveupdate_event.nsfw:
            changes["nsfw"] = nsfw

        if changes:
            _broadcast(type="settings", payload=changes)

        c.liveupdate_event.title = title
        c.liveupdate_event.description = description
        c.liveupdate_event.resources = resources
        c.liveupdate_event.nsfw = nsfw
        c.liveupdate_event._commit()

        amqp.add_item("liveupdate_event_edited", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "editor_fullname": c.user._fullname,
        }))

        form.set_html(".status", _("saved"))
        form.refresh()

    # TODO: pass listing params on
    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/contributors",
    )
    def GET_contributors(self):
        """Get a list of users that contribute to this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor), and
        [/api/live/*thread*/rm_contributor]
        (#POST_api_live_{thread}_rm_contributor).

        """
        editable = c.liveupdate_permissions.allow("manage")

        content = [pages.LinkBackToLiveUpdate()]

        contributors = c.liveupdate_event.contributors
        invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event)

        contributor_builder = LiveUpdateContributorBuilder(
            c.liveupdate_event, contributors, editable)
        contributor_listing = pages.LiveUpdateContributorListing(
            c.liveupdate_event,
            contributor_builder,
            has_invite=c.user_is_loggedin and c.user._id in invites,
            is_contributor=c.user_is_loggedin and c.user._id in contributors,
        ).listing()
        content.append(contributor_listing)

        if editable:
            invite_builder = LiveUpdateInvitedContributorBuilder(
                c.liveupdate_event, invites, editable)
            invite_listing = pages.LiveUpdateInvitedContributorListing(
                c.liveupdate_event,
                invite_builder,
                editable=editable,
            ).listing()
            content.append(invite_listing)

        return pages.LiveUpdateEventPage(
            content=PaneStack(content),
        ).render()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VExistingUname("name"),
        type_and_perms=VLiveUpdatePermissions("type", "permissions"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_invite_contributor(self, form, jquery, user, type_and_perms):
        """Invite another user to contribute to the thread.

        Requires the `manage` permission for this thread.  If the recipient
        accepts the invite, they will be granted the permissions specified.

        See also: [/api/live/*thread*/accept_contributor_invite]
        (#POST_api_live_{thread}_accept_contributor_invite), and
        [/api/live/*thread*/rm_contributor_invite]
        (#POST_api_live_{thread}_rm_contributor_invite).

        """
        if form.has_errors("name", errors.USER_DOESNT_EXIST,
                                   errors.NO_USER):
            return
        if form.has_errors("type", errors.INVALID_PERMISSION_TYPE):
            return
        if form.has_errors("permissions", errors.INVALID_PERMISSIONS):
            return

        type, permissions = type_and_perms

        invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event)
        if user._id in invites or user._id in c.liveupdate_event.contributors:
            c.errors.add(errors.LIVEUPDATE_ALREADY_CONTRIBUTOR, field="name")
            form.has_errors("name", errors.LIVEUPDATE_ALREADY_CONTRIBUTOR)
            return

        if len(invites) >= g.liveupdate_invite_quota:
            c.errors.add(errors.LIVEUPDATE_TOO_MANY_INVITES, field="name")
            form.has_errors("name", errors.LIVEUPDATE_TOO_MANY_INVITES)
            return

        LiveUpdateContributorInvitesByEvent.create(
            c.liveupdate_event, user, permissions)
        queries.add_contributor(c.liveupdate_event, user)

        # TODO: make this i18n-friendly when we have such a system for PMs
        send_system_message(
            user,
            subject="invitation to contribute to " + c.liveupdate_event.title,
            body=INVITE_MESSAGE % {
                "title": c.liveupdate_event.title,
                "url": "/live/" + c.liveupdate_event._id,
            },
        )

        amqp.add_item("new_liveupdate_contributor", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "inviter_fullname": c.user._fullname,
            "invitee_fullname": user._fullname,
        }))

        # add the user to the table
        contributor = LiveUpdateContributor(user, permissions)
        user_row = pages.InvitedLiveUpdateContributorTableItem(
            contributor, c.liveupdate_event, editable=True)
        jquery(".liveupdate_contributor_invite-table").show(
            ).find("table").insert_table_rows(user_row)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VUser(),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_leave_contributor(self, form, jquery):
        """Abdicate contributorship of the thread.

        See also: [/api/live/*thread*/accept_contributor_invite]
        (#POST_api_live_{thread}_accept_contributor_invite), and
        [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        c.liveupdate_event.remove_contributor(c.user)
        queries.remove_contributor(c.liveupdate_event, c.user)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VByName("id", thing_cls=Account),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_rm_contributor_invite(self, form, jquery, user):
        """Revoke an outstanding contributor invite.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        LiveUpdateContributorInvitesByEvent.remove(
            c.liveupdate_event, user)
        queries.remove_contributor(c.liveupdate_event, user)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VUser(),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_accept_contributor_invite(self, form, jquery):
        """Accept a pending invitation to contribute to the thread.

        See also: [/api/live/*thread*/leave_contributor]
        (#POST_api_live_{thread}_leave_contributor).

        """
        try:
            permissions = LiveUpdateContributorInvitesByEvent.get(
                c.liveupdate_event, c.user)
        except InviteNotFoundError:
            c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND)
            form.set_error(errors.LIVEUPDATE_NO_INVITE_FOUND, None)
            return

        LiveUpdateContributorInvitesByEvent.remove(
            c.liveupdate_event, c.user)

        c.liveupdate_event.add_contributor(c.user, permissions)
        jquery.refresh()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VExistingUname("name"),
        type_and_perms=VLiveUpdatePermissions("type", "permissions"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_set_contributor_permissions(self, form, jquery, user, type_and_perms):
        """Change a contributor or contributor invite's permissions.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor) and
        [/api/live/*thread*/rm_contributor]
        (#POST_api_live_{thread}_rm_contributor).

        """
        if form.has_errors("name", errors.USER_DOESNT_EXIST,
                                   errors.NO_USER):
            return
        if form.has_errors("type", errors.INVALID_PERMISSION_TYPE):
            return
        if form.has_errors("permissions", errors.INVALID_PERMISSIONS):
            return

        type, permissions = type_and_perms
        if type == "liveupdate_contributor":
            if user._id not in c.liveupdate_event.contributors:
                c.errors.add(errors.LIVEUPDATE_NOT_CONTRIBUTOR, field="user")
                form.has_errors("user", errors.LIVEUPDATE_NOT_CONTRIBUTOR)
                return

            c.liveupdate_event.update_contributor_permissions(user, permissions)
        elif type == "liveupdate_contributor_invite":
            try:
                LiveUpdateContributorInvitesByEvent.get(
                    c.liveupdate_event, user)
            except InviteNotFoundError:
                c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND, field="user")
                form.has_errors("user", errors.LIVEUPDATE_NO_INVITE_FOUND)
                return
            else:
                LiveUpdateContributorInvitesByEvent.update_invite_permissions(
                    c.liveupdate_event, user, permissions)

        row = form.closest("tr")
        editor = row.find(".permissions").data("PermissionEditor")
        editor.onCommit(permissions.dumps())

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VByName("id", thing_cls=Account),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_rm_contributor(self, form, jquery, user):
        """Revoke another user's contributorship.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        c.liveupdate_event.remove_contributor(user)
        queries.remove_contributor(c.liveupdate_event, user)

    @require_oauth2_scope("submit")
    @validatedForm(
        VLiveUpdateContributorWithPermission("update"),
        VModhash(),
        text=VMarkdownLength("body", max_length=4096),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_update(self, form, jquery, text):
        """Post an update to the thread.

        Requires the `update` permission for this thread.

        See also: [/api/live/*thread*/strike_update]
        (#POST_api_live_{thread}_strike_update), and
        [/api/live/*thread*/delete_update]
        (#POST_api_live_{thread}_delete_update).

        """
        if form.has_errors("body", errors.NO_TEXT,
                                   errors.TOO_LONG):
            return

        # create and store the new update
        update = LiveUpdate(data={
            "author_id": c.user._id,
            "body": text,
            "_spam": c.user._spam,
        })

        hooks.get_hook("liveupdate.update").call(update=update)

        LiveUpdateStream.add_update(c.liveupdate_event, update)

        # tell the world about our new update
        builder = LiveUpdateBuilder(None)
        wrapped = builder.wrap_items([update])[0]
        rendered = wrapped.render(style="api")
        _broadcast(type="update", payload=rendered)

        amqp.add_item("new_liveupdate_update", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "author_fullname": c.user._fullname,
            "liveupdate_id": str(update._id),
            "body": text,
        }))

        liveupdate_events.update_event(update, context=c, request=request)

        # reset the submission form
        t = form.find("textarea")
        t.attr('rows', 3).html("").val("")

    @require_oauth2_scope("edit")
    @validatedForm(
        VModhash(),
        update=VLiveUpdate("id"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_delete_update(self, form, jquery, update):
        """Delete an update from the thread.

        Requires that specified update must have been authored by the user or
        that you have the `edit` permission for this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if form.has_errors("id", errors.NO_THING_ID):
            return

        if not (c.liveupdate_permissions.allow("edit") or
                (c.user_is_loggedin and update.author_id == c.user._id)):
            abort(403)

        update.deleted = True
        LiveUpdateStream.add_update(c.liveupdate_event, update)
        liveupdate_events.update_event(update, context=c, request=request)

        _broadcast(type="delete", payload=update._fullname)

    @require_oauth2_scope("edit")
    @validatedForm(
        VModhash(),
        update=VLiveUpdate("id"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_strike_update(self, form, jquery, update):
        """Strike (mark incorrect and cross out) the content of an update.

        Requires that specified update must have been authored by the user or
        that you have the `edit` permission for this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if form.has_errors("id", errors.NO_THING_ID):
            return

        if not (c.liveupdate_permissions.allow("edit") or
                (c.user_is_loggedin and update.author_id == c.user._id)):
            abort(403)

        update.stricken = True
        LiveUpdateStream.add_update(c.liveupdate_event, update)

        liveupdate_events.update_event(
            update, stricken=True, context=c, request=request
        )
        _broadcast(type="strike", payload=update._fullname)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("close"),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_close_thread(self, form, jquery):
        """Permanently close the thread, disallowing future updates.

        Requires the `close` permission for this thread.

        """
        close_event(c.liveupdate_event)
        liveupdate_events.close_event(context=c, request=request)

        form.refresh()

    @require_oauth2_scope("report")
    @validatedForm(
        VUser(),
        VModhash(),
        report_type=VOneOf("type", pages.REPORT_TYPES),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_report(self, form, jquery, report_type):
        """Report the thread for violating the rules of reddit."""
        if form.has_errors("type", errors.INVALID_OPTION):
            return

        if c.user._spam or c.user.ignorereports:
            return

        already_reported = LiveUpdateReportsByAccount.get_report(
            c.user, c.liveupdate_event)
        if already_reported:
            self.abort403()

        LiveUpdateReportsByAccount.create(
            c.user, c.liveupdate_event, type=report_type)
        queries.report_event(c.liveupdate_event)

        liveupdate_events.report_event(
            report_type, context=c, request=request
        )

        amqp.add_item("new_liveupdate_report", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "reporter_fullname": c.user._fullname,
            "reason": report_type,
        }))

        try:
            default_subreddit = Subreddit._by_name(g.default_sr)
        except NotFound:
            pass
        else:
            not_yet_reported = g.ratelimitcache.add(
                "rl:lu_reported_" + str(c.liveupdate_event._id), 1, time=3600)
            if not_yet_reported:
                send_system_message(
                    default_subreddit,
                    subject="live thread reported",
                    body=REPORTED_MESSAGE % {
                        "title": c.liveupdate_event.title,
                        "url": "/live/" + c.liveupdate_event._id,
                        "reason": pages.REPORT_TYPES[report_type],
                    },
                )

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_approve(self, form, jquery):
        c.liveupdate_event.banned = False
        c.liveupdate_event._commit()

        queries.unreport_event(c.liveupdate_event)
        liveupdate_events.ban_event(context=c, request=request)

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_ban(self, form, jquery):
        c.liveupdate_event.banned = True
        c.liveupdate_event.banned_by = c.user.name
        c.liveupdate_event._commit()

        queries.unreport_event(c.liveupdate_event)
        liveupdate_events.ban_event(context=c, request=request)
Пример #19
0
class StripeController(GoldPaymentController):
    name = 'stripe'
    webhook_secret = g.STRIPE_WEBHOOK_SECRET
    event_type_mappings = {
        'charge.succeeded': 'succeeded',
        'charge.failed': 'failed',
        'charge.refunded': 'refunded',
        'charge.dispute.created': 'noop',
        'charge.dispute.updated': 'noop',
        'charge.dispute.closed': 'noop',
        'customer.created': 'noop',
        'customer.card.created': 'noop',
        'customer.card.deleted': 'noop',
        'transfer.created': 'noop',
        'transfer.paid': 'noop',
        'balance.available': 'noop',
        'invoice.created': 'noop',
        'invoice.updated': 'noop',
        'invoice.payment_succeeded': 'noop',
        'invoice.payment_failed': 'failed_subscription',
        'invoiceitem.deleted': 'noop',
        'customer.subscription.created': 'noop',
        'customer.deleted': 'noop',
        'customer.updated': 'noop',
        'customer.subscription.deleted': 'noop',
        'customer.subscription.trial_will_end': 'noop',
        'customer.subscription.updated': 'noop',
        'dummy': 'noop',
    }

    @classmethod
    def process_response(cls):
        event_dict = json.loads(request.body)
        event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY)
        status = event.type

        if status == 'invoice.created':
            # sent 1 hr before a subscription is charged or immediately for
            # a new subscription
            invoice = event.data.object
            customer_id = invoice.customer
            account = account_from_stripe_customer_id(customer_id)
            # if the charge hasn't been attempted (meaning this is 1 hr before
            # the charge) check that the account can receive the gold
            if (not invoice.attempted
                    and (not account or (account and account._banned))):
                # there's no associated account - delete the subscription
                # to cancel the charge
                g.log.error('no account for stripe invoice: %s', invoice)
                try:
                    customer = stripe.Customer.retrieve(customer_id)
                    customer.delete()
                except stripe.InvalidRequestError:
                    pass
        elif status == 'invoice.payment_failed':
            invoice = event.data.object
            customer_id = invoice.customer
            buyer = account_from_stripe_customer_id(customer_id)
            webhook = Webhook(subscr_id=customer_id, buyer=buyer)
            return status, webhook

        event_type = cls.event_type_mappings.get(status)
        if not event_type:
            raise ValueError('Stripe: unrecognized status %s' % status)
        elif event_type == 'noop':
            return status, None

        charge = event.data.object
        description = charge.description
        invoice_id = charge.invoice
        transaction_id = 'S%s' % charge.id
        pennies = charge.amount
        months, days = months_and_days_from_pennies(pennies)

        if status == 'charge.failed' and invoice_id:
            # we'll get an additional failure notification event of
            # "invoice.payment_failed", don't double notify
            return 'dummy', None
        elif status == 'charge.failed' and not description:
            # create_customer can POST successfully but fail to create a
            # customer because the card is declined. This will trigger a
            # 'charge.failed' notification but without description so we can't
            # do anything with it
            return 'dummy', None
        elif invoice_id:
            # subscription charge - special handling
            customer_id = charge.customer
            buyer = account_from_stripe_customer_id(customer_id)
            if not buyer:
                charge_date = datetime.fromtimestamp(charge.created, tz=g.tz)

                # don't raise exception if charge date is within the past hour
                # db replication lag may cause the account lookup to fail
                if charge_date < timeago('1 hour'):
                    raise ValueError('no buyer for charge: %s' % charge.id)
                else:
                    abort(404, "not found")
            webhook = Webhook(transaction_id=transaction_id,
                              subscr_id=customer_id,
                              pennies=pennies,
                              months=months,
                              goldtype='autorenew',
                              buyer=buyer)
            return status, webhook
        else:
            try:
                passthrough, buyer_name = description.split('-', 1)
            except (AttributeError, ValueError):
                g.log.error('stripe_error on charge: %s', charge)
                raise

            webhook = Webhook(passthrough=passthrough,
                              transaction_id=transaction_id,
                              pennies=pennies,
                              months=months)
            return status, webhook

    @classmethod
    @handle_stripe_error
    def create_customer(cls, form, token):
        description = c.user.name
        customer = stripe.Customer.create(card=token, description=description)

        if (customer['active_card']['address_line1_check'] == 'fail'
                or customer['active_card']['address_zip_check'] == 'fail'):
            form.set_html('.status', _('error: address verification failed'))
            form.find('.stripe-submit').removeAttr('disabled').end()
            return None
        elif customer['active_card']['cvc_check'] == 'fail':
            form.set_html('.status', _('error: cvc check failed'))
            form.find('.stripe-submit').removeAttr('disabled').end()
            return None
        else:
            return customer

    @classmethod
    @handle_stripe_error
    def charge_customer(cls, form, customer, pennies, passthrough):
        charge = stripe.Charge.create(amount=pennies,
                                      currency="usd",
                                      customer=customer['id'],
                                      description='%s-%s' %
                                      (passthrough, c.user.name))
        return charge

    @classmethod
    @handle_stripe_error
    def set_creditcard(cls, form, user, token):
        if not user.has_stripe_subscription:
            return

        customer = stripe.Customer.retrieve(user.gold_subscr_id)
        customer.card = token
        customer.save()
        return customer

    @classmethod
    @handle_stripe_error
    def set_subscription(cls, form, customer, plan_id):
        subscription = customer.update_subscription(plan=plan_id)
        return subscription

    @classmethod
    @handle_stripe_error
    def cancel_subscription(cls, form, user):
        if not user.has_stripe_subscription:
            return

        customer = stripe.Customer.retrieve(user.gold_subscr_id)
        customer.delete()

        user.gold_subscr_id = None
        user._commit()
        subject = _('your gold subscription has been cancelled')
        message = _('if you have any questions please email %(email)s')
        message %= {'email': g.goldthanks_email}
        send_system_message(user, subject, message)
        return customer

    @validatedForm(VUser(),
                   token=nop('stripeToken'),
                   passthrough=VPrintable("passthrough", max_length=50),
                   pennies=VInt('pennies'),
                   months=VInt("months"),
                   period=VOneOf("period", ("monthly", "yearly")))
    def POST_goldcharge(self, form, jquery, token, passthrough, pennies,
                        months, period):
        """
        Submit charge to stripe.

        Called by GoldPayment form. This submits the charge to stripe, and gold
        will be applied once we receive a webhook from stripe.

        """

        try:
            payment_blob = validate_blob(passthrough)
        except GoldException as e:
            # This should never happen. All fields in the payment_blob
            # are validated on creation
            form.set_html('.status',
                          _('something bad happened, try again later'))
            g.log.debug('POST_goldcharge: %s' % e.message)
            return

        if period:
            plan_id = (g.STRIPE_MONTHLY_GOLD_PLAN
                       if period == 'monthly' else g.STRIPE_YEARLY_GOLD_PLAN)
            if c.user.has_gold_subscription:
                form.set_html(
                    '.status',
                    _('your account already has a gold subscription'))
                return
        else:
            plan_id = None
            penny_months, days = months_and_days_from_pennies(pennies)
            if not months or months != penny_months:
                form.set_html('.status', _('stop trying to trick the form'))
                return

        customer = self.create_customer(form, token)
        if not customer:
            return

        if period:
            subscription = self.set_subscription(form, customer, plan_id)
            if not subscription:
                return

            c.user.gold_subscr_id = customer.id
            c.user._commit()

            status = _('subscription created')
            subject = _('reddit gold subscription')
            body = _('Your subscription is being processed and reddit gold '
                     'will be delivered shortly.')
        else:
            charge = self.charge_customer(form, customer, pennies, passthrough)
            if not charge:
                return

            status = _('payment submitted')
            subject = _('reddit gold payment')
            body = _('Your payment is being processed and reddit gold '
                     'will be delivered shortly.')

        form.set_html('.status', status)
        body = append_random_bottlecap_phrase(body)
        send_system_message(c.user, subject, body, distinguished='gold-auto')

    @validatedForm(VUser(), VModhash(), token=nop('stripeToken'))
    def POST_modify_subscription(self, form, jquery, token):
        customer = self.set_creditcard(form, c.user, token)
        if not customer:
            return

        form.set_html('.status', _('your payment details have been updated'))

    @validatedForm(VUser(), VModhash(), user=VByName('user'))
    def POST_cancel_subscription(self, form, jquery, user):
        if user != c.user and not c.user_is_admin:
            self.abort403()
        customer = self.cancel_subscription(form, user)
        if not customer:
            return

        form.set_html(".status", _("your subscription has been cancelled"))
Пример #20
0
class SteamController(RedditController):
    @staticmethod
    def make_post_login_url():
        return add_sr("/f2p/steam/postlogin")

    @validate(VUser(), error=nop("error"))
    def GET_start(self, error):
        f2p_status = getattr(c.user, "f2p", None)
        error = bool(error)

        if f2p_status == "participated":
            return SteamPage(content=SteamStart(error=error)).render()
        elif f2p_status == "claiming":
            return SteamPage(content=SteamInProgress()).render()
        elif f2p_status == "claimed":
            return SteamPage(content=SteamStop()).render()
        return SteamPage(content=SteamSorry()).render()

    @validate(VUser(), VModhash())
    def POST_auth(self):
        if getattr(c.user, "f2p", None) != "participated":
            abort(403)

        session = {}
        consumer = openid.consumer.consumer.Consumer(session, store=None)
        auth_request = consumer.begin(STEAM_AUTH_URL)
        post_login_url = self.make_post_login_url()
        url = auth_request.redirectURL(realm=g.origin,
                                       return_to=post_login_url)
        g.f2pcache.set("steam_session_%d" % c.user._id, session)
        g.log.debug("started steam auth for %s", c.user.name)
        return redirect_to(url)

    @validate(VUser())
    def GET_postlogin(self):
        if getattr(c.user, "f2p", None) != "participated":
            return redirect_to("/f2p/steam")

        session = g.f2pcache.get("steam_session_%d" % c.user._id)
        if not session:
            return redirect_to("/f2p/steam?error=no_session")

        consumer = openid.consumer.consumer.Consumer(session, store=None)
        auth_response = consumer.complete(request.params, request.url)

        if auth_response.status == openid.consumer.consumer.CANCEL:
            return redirect_to("/f2p/steam?error=cancel")

        if auth_response.status != openid.consumer.consumer.SUCCESS:
            return redirect_to("/f2p/steam?error=not_success")

        steamid_match = STEAMID_EXTRACTOR.search(auth_response.identity_url)
        if not steamid_match:
            return redirect_to("/f2p/steam?error=id_mismatch")

        steamid = steamid_match.group(1)
        g.log.debug("successful steam auth for %r", steamid)

        with g.make_lock("f2p", "steam_claim_%d" % c.user._id):
            c.user._sync_latest()
            if c.user.f2p != "participated":
                return redirect_to("/f2p/steam")

            c.user.f2p = "claiming"
            c.user._commit()

        message = json.dumps({
            "user-id": c.user._id,
            "steam-id": steamid,
        })
        amqp.add_item(QNAME, message)

        return redirect_to("/f2p/steam")
Пример #21
0
class LiveUpdateEventsController(RedditController):
    def GET_home(self):
        return pages.LiveUpdateMetaPage(
            title=_("reddit live"),
            content=pages.LiveUpdateHome(),
            page_classes=["liveupdate-home"],
        ).render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/api/live/happening_now",
    )
    def GET_happening_now(self):
        """ Get some basic information about the currently featured live thread.

            Returns an empty 204 response for api requests if no thread is currently featured.

            See also: [/api/live/*thread*/about](#GET_api_live_{thread}_about).
        """

        if not is_api():
            self.abort404()

        featured_event = get_featured_event()
        if not featured_event:
            response.status_code = 204
            return

        c.liveupdate_event = featured_event
        content = Wrapped(featured_event)
        return pages.LiveUpdateEventPage(content).render()

    @validate(
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateEvent("after"),
        before=VLiveUpdateEvent("before"),
        count=VCount("count"),
    )
    def GET_listing(self, filter, num, after, before, count):
        reverse = False
        if before:
            after = before
            reverse = True

        builder_cls = LiveUpdateEventBuilder
        wrapper = Wrapped
        listing_cls = Listing
        require_employee = True  # for grepping: this is used like VEmployee

        if filter == "open":
            title = _("live threads")
            query = queries.get_live_events("new", "all")
        elif filter == "closed":
            title = _("closed threads")
            query = queries.get_complete_events("new", "all")
        elif filter == "active":
            title = _("most active threads")
            query = queries.get_active_events()
        elif filter == "reported":
            if not c.user_is_admin:
                self.abort403()

            title = _("reported threads")
            query = queries.get_reported_events()
            builder_cls = LiveUpdateReportedEventBuilder
            wrapper = pages.LiveUpdateReportedEventRow
            listing_cls = pages.LiveUpdateReportedEventListing
        elif filter == "happening_now":
            featured_events = get_all_featured_events()

            title = _("featured threads")
            query = sorted(set(featured_events.values()))
            builder_cls = featured_event_builder_factory(featured_events)
            wrapper = pages.LiveUpdateFeaturedEvent
            require_employee = False
        elif filter == "mine":
            if not c.user_is_loggedin:
                self.abort404()

            title = _("my live threads")
            query = queries.get_contributor_events(c.user)
            require_employee = False
        else:
            self.abort404()

        if require_employee and not c.user.employee:
            self.abort403()

        builder = builder_cls(
            query,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
            wrap=wrapper,
            skip=True,
        )

        listing = listing_cls(builder)

        return pages.LiveUpdateMetaPage(
            title=title,
            content=listing.listing(),
        ).render()

    @validate(
        VUser(),
    )
    def GET_create(self):
        return pages.LiveUpdateMetaPage(
            title=_("create live thread"),
            content=pages.LiveUpdateCreate(),
        ).render()

    @require_oauth2_scope("submit")
    @validatedForm(
        VUser(),
        VModhash(),
        VRatelimit(rate_user=True, prefix="liveupdate_create_"),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
        uri="/api/live/create",
    )
    def POST_create(self, form, jquery, title, description, resources, nsfw):
        """Create a new live thread.

        Once created, the initial settings can be modified with
        [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates
        can be posted with
        [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if not is_event_configuration_valid(form):
            return

        # for simplicity, set the live-thread creation threshold at the
        # subreddit creation threshold
        if not c.user_is_admin and not c.user.can_create_subreddit:
            form.set_error(errors.CANT_CREATE_SR, "")
            c.errors.add(errors.CANT_CREATE_SR, field="")
            return

        if form.has_errors("ratelimit", errors.RATELIMIT):
            return

        VRatelimit.ratelimit(
            rate_user=True, prefix="liveupdate_create_", seconds=60)

        event = LiveUpdateEvent.new(
            id=None,
            title=title,
            description=description,
            resources=resources,
            banned=c.user._spam,
            nsfw=nsfw,
        )
        event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER)
        queries.create_event(event)

        amqp.add_item("new_liveupdate_event", json.dumps({
            "event_fullname": event._fullname,
            "creator_fullname": c.user._fullname,
        }))

        form.redirect("/live/" + event._id)
        form._send_data(id=event._id)
        liveupdate_events.create_event(event, context=c, request=request)
Пример #22
0
class RobinController(RedditController):
    def pre(self):
        RedditController.pre(self)
        if not feature.is_enabled("robin"):
            self.abort404()

    @validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_join(self):
        room = RobinRoom.get_room_for_user(c.user)
        if room:
            return self.redirect("/robin")

        return RobinPage(
            title="robin",
            content=RobinJoin(
                robin_heavy_load=g.live_config.get('robin_heavy_load')),
        ).render()

    @validate(
        VAdmin(), )
    def GET_all(self):
        return RobinPage(
            title="robin",
            content=RobinAll(),
        ).render()

    @validate(
        VAdmin(), )
    def GET_admin(self):
        return RobinPage(
            title="robin",
            content=RobinAdmin(),
        ).render()

    @validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_chat(self):
        room = RobinRoom.get_room_for_user(c.user)
        if not room:
            return self.redirect("/robin/join")

        return self._get_chat_page(room)

    @validate(
        VAdmin(),
        room=VRobinRoom("room_id", allow_admin=True),
    )
    def GET_force_room(self, room):
        """Allow admins to view a specific room"""
        return self._get_chat_page(room)

    @validate(
        VAdmin(),
        user=VAccountByName("user"),
    )
    def GET_user_room(self, user):
        """Redirect admins to a user's room"""
        room = RobinRoom.get_room_for_user(user)
        if not room:
            self.abort404()

        self.redirect("/robin/" + room.id)

    def _get_chat_page(self, room):
        path = posixpath.join("/robin", room.id, c.user._id36)
        websocket_url = websockets.make_url(path, max_age=3600)

        all_user_ids = room.get_all_participants()
        all_present_ids = room.get_present_participants()
        all_votes = room.get_all_votes()

        users = Account._byID(all_user_ids, data=True, stale=True)
        user_list = []

        for user in users.itervalues():
            if user._id in all_votes:
                vote = all_votes.get(user._id)
            else:
                vote = None

            user_list.append({
                "name": user.name,
                "present": user._id in all_present_ids,
                "vote": vote,
            })

        return RobinChatPage(
            title="chat in %s" % room.name,
            content=RobinChat(room=room),
            extra_js_config={
                "robin_room_is_continued": room.is_continued,
                "robin_room_name": room.name,
                "robin_room_id": room.id,
                "robin_websocket_url": websocket_url,
                "robin_user_list": user_list,
                "robin_room_date": js_timestamp(room.date),
                "robin_room_reap_time": js_timestamp(get_reap_time(room)),
            },
        ).render()

    def _has_exceeded_ratelimit(self, form, room):
        # grab the ratelimit (as average events per second) for the room's
        # current level, using the highest level configured that's not bigger
        # than the room.  e.g. if ratelimits are defined for levels 1, 2, and 4
        # and the room is level 3, this will give us the ratelimit specified
        # for 2.
        desired_avg_per_sec = 1
        by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {})
        for level, avg_per_sec in sorted(by_level.items(),
                                         key=lambda (x, y): int(x)):
            if int(level) > room.level:
                break
            desired_avg_per_sec = avg_per_sec

        # now figure out how many events per window that means
        window_size = g.live_config.get("robin_ratelimit_window", 10)
        allowed_events_per_window = int(desired_avg_per_sec * window_size)

        try:
            # now figure out how much they've actually used
            ratelimit_key = "robin/{}".format(c.user._id36)
            time_slice = ratelimit.get_timeslice(window_size)
            usage = ratelimit.get_usage(ratelimit_key, time_slice)

            # ratelimit them if too much
            if usage >= allowed_events_per_window:
                g.stats.simple_event("robin.ratelimit.exceeded")

                period_end = datetime.datetime.utcfromtimestamp(time_slice.end)
                period_end_utc = period_end.replace(tzinfo=pytz.UTC)
                until_reset = utils.timeuntil(period_end_utc)
                c.errors.add(errors.RATELIMIT, {"time": until_reset},
                             field="ratelimit",
                             code=429)
                form.has_errors("ratelimit", errors.RATELIMIT)

                return True

            # or record the usage and move on
            ratelimit.record_usage(ratelimit_key, time_slice)
        except ratelimit.RatelimitError as exc:
            g.log.warning("ratelimit error: %s", exc)
        return False

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
        room=VRobinRoom("room_id"),
        message=VLength("message", max_length=140),  # TODO: do we want md?
    )
    def POST_message(self, form, jquery, room, message):
        if self._has_exceeded_ratelimit(form, room):
            return

        if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG):
            return

        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="chat",
            payload={
                "from": c.user.name,
                "body": message,
            },
        )

        events.message(
            room=room,
            message=message,
            sent_dt=datetime.datetime.utcnow(),
            context=c,
            request=request,
        )

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
        room=VRobinRoom("room_id"),
        vote=VOneOf("vote", VALID_VOTES),
    )
    def POST_vote(self, form, jquery, room, vote):
        if self._has_exceeded_ratelimit(form, room):
            return

        if not vote:
            # TODO: error return?
            return

        g.stats.simple_event('robin.vote.%s' % vote)

        room.set_vote(c.user, vote)
        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="vote",
            payload={
                "from": c.user.name,
                "vote": vote,
            },
        )

        events.vote(
            room=room,
            vote=vote,
            sent_dt=datetime.datetime.utcnow(),
            context=c,
            request=request,
        )

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
    )
    def POST_join_room(self, form, jquery):
        if g.live_config.get('robin_heavy_load'):
            request.environ["usable_error_content"] = (
                "Robin is currently experience high load.")
            abort(503)

        room = RobinRoom.get_room_for_user(c.user)
        if room:
            # user is already in a room, they should get redirected by the
            # frontend after polling /api/room_assignment.json
            return

        add_to_waitinglist(c.user)

    @validatedForm(
        VUser(),
        VModhash(),
    )
    def POST_leave_room(self, form, jquery):
        room = RobinRoom.get_room_for_user(c.user)
        if not room:
            return
        room.remove_participants([c.user])
        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="users_abandoned",
            payload={
                "users": [c.user.name],
            },
        )

    @json_validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_room_assignment(self, responder):
        room = RobinRoom.get_room_for_user(c.user)
        if room:
            return {"roomId": room.id}

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_admin_prompt(self, form, jquery):
        prompt_for_voting()

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_admin_reap(self, form, jquery):
        reap_ripe_rooms()

    @validatedForm(
        VAdmin(),
        VModhash(),
        message=VLength("message", max_length=140),
    )
    def POST_admin_broadcast(self, form, jquery, message):
        if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG):
            return

        websockets.send_broadcast(
            namespace="/robin",
            type="system_broadcast",
            payload={
                "body": message,
            },
        )
Пример #23
0
class OAuth2FrontendController(RedditController):
    def check_for_bearer_token(self):
        pass

    def pre(self):
        RedditController.pre(self)
        require_https()

    def _check_redirect_uri(self, client, redirect_uri):
        if not redirect_uri or not client or redirect_uri != client.redirect_uri:
            abort(ForbiddenError(errors.OAUTH2_INVALID_REDIRECT_URI))

    def _error_response(self, state, redirect_uri, as_fragment=False):
        """Return an error redirect, but only if client_id and redirect_uri are valid."""

        resp = {"state": state}

        if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors:
            resp["error"] = "unauthorized_client"
        elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
            resp["error"] = "access_denied"
        elif (errors.BAD_HASH, None) in c.errors:
            resp["error"] = "access_denied"
        elif (errors.INVALID_OPTION, "response_type") in c.errors:
            resp["error"] = "unsupported_response_type"
        elif (errors.OAUTH2_INVALID_SCOPE, "scope") in c.errors:
            resp["error"] = "invalid_scope"
        else:
            resp["error"] = "invalid_request"

        final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment)
        return self.redirect(final_redirect, code=302)

    @validate(VUser(),
              response_type=VOneOf("response_type", ("code", "token")),
              client=VOAuth2ClientID(),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI),
              scope=VOAuth2Scope(),
              state=VRequired("state", errors.NO_TEXT),
              duration=VOneOf("duration", ("temporary", "permanent"),
                              default="temporary"))
    def GET_authorize(self, response_type, client, redirect_uri, scope, state,
                      duration):
        """
        First step in [OAuth 2.0](http://oauth.net/2/) authentication.
        End users will be prompted for their credentials (username/password)
        and asked if they wish to authorize the application identified by
        the **client_id** parameter with the permissions specified by the
        **scope** parameter.  They are then redirected to the endpoint on
        the client application's side specified by **redirect_uri**.

        If the user granted permission to the application, the response will
        contain a **code** parameter with a temporary authorization code
        which can be exchanged for an access token at
        [/api/v1/access_token](#api_method_access_token).

        **redirect_uri** must match the URI configured for the client in the
        [app preferences](/prefs/apps).  If **client_id** or **redirect_uri**
        is not valid, or if the call does not take place over SSL, a 403
        error will be returned.  For all other errors, a redirect to
        **redirect_uri** will be returned, with a **error** parameter
        indicating why the request failed.
        """

        # Check redirect URI first; it will ensure client exists
        self._check_redirect_uri(client, redirect_uri)

        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id"))
        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            c.errors.add((errors.INVALID_OPTION, "duration"))

        if not c.errors:
            return OAuth2AuthorizationPage(client, redirect_uri, scope, state,
                                           duration, response_type).render()
        else:
            return self._error_response(state,
                                        redirect_uri,
                                        as_fragment=(response_type == "token"))

    @validate(VUser(),
              VModhash(fatal=False),
              client=VOAuth2ClientID(),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI),
              scope=VOAuth2Scope(),
              state=VRequired("state", errors.NO_TEXT),
              duration=VOneOf("duration", ("temporary", "permanent"),
                              default="temporary"),
              authorize=VRequired("authorize", errors.OAUTH2_ACCESS_DENIED),
              response_type=VOneOf("response_type", ("code", "token"),
                                   default="code"))
    def POST_authorize(self, authorize, client, redirect_uri, scope, state,
                       duration, response_type):
        """Endpoint for OAuth2 authorization."""

        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id"))
        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            c.errors.add((errors.INVALID_OPTION, "duration"))
        self._check_redirect_uri(client, redirect_uri)

        if c.errors:
            return self._error_response(state,
                                        redirect_uri,
                                        as_fragment=(response_type == "token"))

        if response_type == "code":
            code = OAuth2AuthorizationCode._new(client._id, redirect_uri,
                                                c.user._id36, scope,
                                                duration == "permanent")
            resp = {"code": code._id, "state": state}
            final_redirect = _update_redirect_uri(redirect_uri, resp)
        elif response_type == "token":
            token = OAuth2AccessToken._new(client._id, c.user._id36, scope)
            token_data = OAuth2AccessController._make_token_dict(token)
            token_data["state"] = state
            final_redirect = _update_redirect_uri(redirect_uri,
                                                  token_data,
                                                  as_fragment=True)

        return self.redirect(final_redirect, code=302)
Пример #24
0
class IpnController(RedditController):
    # Used when buying gold with creddits
    @validatedForm(VUser(),
                   months = VInt("months"),
                   passthrough = VPrintable("passthrough", max_length=50))
    def POST_spendcreddits(self, form, jquery, months, passthrough):
        if months is None or months < 1:
            form.set_html(".status", _("nice try."))
            return

        days = months * 31

        if not passthrough:
            raise ValueError("/spendcreddits got no passthrough?")

        blob_key, payment_blob = get_blob(passthrough)
        if payment_blob["goldtype"] != "gift":
            raise ValueError("/spendcreddits payment_blob %s has goldtype %s" %
                             (passthrough, payment_blob["goldtype"]))

        signed = payment_blob["signed"]
        giftmessage = _force_unicode(payment_blob["giftmessage"])
        recipient_name = payment_blob["recipient"]

        if payment_blob["account_id"] != c.user._id:
            fmt = ("/spendcreddits payment_blob %s has userid %d " +
                   "but c.user._id is %d")
            raise ValueError(fmt % passthrough,
                             payment_blob["account_id"],
                             c.user._id)

        try:
            recipient = Account._by_name(recipient_name)
        except NotFound:
            raise ValueError("Invalid username %s in spendcreddits, buyer = %s"
                             % (recipient_name, c.user.name))

        if recipient._deleted:
            form.set_html(".status", _("that user has deleted their account"))
            return

        if not c.user_is_admin:
            if months > c.user.gold_creddits:
                raise ValueError("%s is trying to sneak around the creddit check"
                                 % c.user.name)

            c.user.gold_creddits -= months
            c.user.gold_creddit_escrow += months
            c.user._commit()

        comment_id = payment_blob.get("comment")
        comment = send_gift(c.user, recipient, months, days, signed,
                            giftmessage, comment_id)

        if not c.user_is_admin:
            c.user.gold_creddit_escrow -= months
            c.user._commit()

        payment_blob["status"] = "processed"
        g.hardcache.set(blob_key, payment_blob, 86400 * 30)

        form.set_html(".status", _("the gold has been delivered!"))
        form.find("button").hide()

        if comment:
            gilding_message = make_comment_gold_message(comment,
                                                        user_gilded=True)
            jquery.gild_comment(comment_id, gilding_message, comment.gildings)

    @textresponse(full_sn = VLength('serial-number', 100))
    def POST_gcheckout(self, full_sn):
        if full_sn:
            short_sn = full_sn.split('-')[0]
            g.log.error( "GOOGLE CHECKOUT: %s" % short_sn)
            trans = _google_ordernum_request(short_sn)

            # get the financial details
            auth = trans.find("authorization-amount-notification")

            custom = None
            cart = trans.find("shopping-cart")
            if cart:
                private_item_data = cart.find("merchant-private-item-data")
                if private_item_data:
                    custom = str(private_item_data.contents[0])

            if not auth:
                # see if the payment was declinded
                status = trans.findAll('financial-order-state')
                if 'PAYMENT_DECLINED' in [x.contents[0] for x in status]:
                    g.log.error("google declined transaction found: '%s'" %
                                short_sn)
                elif 'REVIEWING' not in [x.contents[0] for x in status]:
                    g.log.error(("google transaction not found: " +
                                 "'%s', status: %s")
                                % (short_sn, [x.contents[0] for x in status]))
                else:
                    g.log.error(("google transaction status: " +
                                 "'%s', status: %s")
                                % (short_sn, [x.contents[0] for x in status]))
                    if custom:
                        payment_blob = validate_blob(custom)
                        buyer = payment_blob['buyer']
                        subject = _('gold order')
                        msg = _('your order has been received and gold will'
                                ' be delivered shortly. please bear with us'
                                ' as google wallet payments can take up to an'
                                ' hour to complete')
                        try:
                            send_system_message(buyer, subject, msg)
                        except MessageError:
                            g.log.error('gcheckout send_system_message failed')
            elif auth.find("financial-order-state"
                           ).contents[0] == "CHARGEABLE":
                email = str(auth.find("email").contents[0])
                payer_id = str(auth.find('buyer-id').contents[0])
                if custom:
                    days = None
                    try:
                        pennies = int(float(trans.find("order-total"
                                                      ).contents[0])*100)
                        months, days = months_and_days_from_pennies(pennies)
                        if not months:
                            raise ValueError("Bad pennies for %s" % short_sn)
                        charged = trans.find("charge-amount-notification")
                        if not charged:
                            _google_charge_and_ship(short_sn)

                        parameters = request.POST.copy()
                        self.finish(parameters, "g%s" % short_sn,
                                    email, payer_id, None,
                                    custom, pennies, months, days)
                    except ValueError, e:
                        g.log.error(e)
                else:
                    raise ValueError("Got no custom blob for %s" % short_sn)

            return (('<notification-acknowledgment ' +
                     'xmlns="http://checkout.google.com/schema/2" ' +
                     'serial-number="%s" />') % full_sn)
        else: