diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index fad674fb8..addb6792d 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -204,6 +204,7 @@ def make_map(global_conf={}, app_conf={}): mc('/post/:action', controller='post', requirements=dict(action="options|over18|unlogged_options|optout|optin|login|reg")) + mc('/api', controller='apihelp', action='help') mc('/api/distinguish/:how', controller='api', action="distinguish") # wherever this is, google has to agree. mc('/api/gcheckout', controller='ipn', action='gcheckout') diff --git a/r2/r2/controllers/__init__.py b/r2/r2/controllers/__init__.py index 912258876..1fa391884 100644 --- a/r2/r2/controllers/__init__.py +++ b/r2/r2/controllers/__init__.py @@ -59,7 +59,7 @@ try: except ImportError: from api import ApiController -from api import ApiminimalController +from api import ApiminimalController, ApihelpController from apiv1 import APIv1Controller as Apiv1Controller from oauth2 import OAuth2FrontendController as Oauth2frontendController from oauth2 import OAuth2AccessController as Oauth2accessController diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index ba1f0e09d..d95ebc0b2 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -34,7 +34,7 @@ from r2.lib.utils import query_string, timefromnow, randstr from r2.lib.utils import timeago, tup, filter_links from r2.lib.pages import EnemyList, FriendList, ContributorList, ModList, \ BannedList, BoringPage, FormPage, CssError, UploadedImage, ClickGadget, \ - UrlParser, WrappedUser + UrlParser, WrappedUser, ApiHelp, BoringPage from r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor, \ FlairSelector from r2.lib.utils.trial_utils import indict, end_trial, trial_info @@ -2494,3 +2494,52 @@ class ApiController(RedditController): self.enable_admin_mode(c.user) form.redirect(dest) + +class ApihelpController(RedditController): + @staticmethod + def docs_from_controller(controller, url_prefix='/api'): + api_methods = defaultdict(dict) + for name, func in controller.__dict__.iteritems(): + i = name.find('_') + if i > 0: + method = name[:i] + action = name[i+1:] + else: + continue + + if func.__doc__ and method in ('GET', 'POST'): + docs = func.__doc__.strip() + if hasattr(func, 'oauth2_perms'): + scopes = func.oauth2_perms.get('allowed_scopes') + if scopes: + docs = '*OAuth2 scope(s): %s*\n\n%s' % ( + ', '.join([ + ('[%s](#oauth2_scope_%s)' % (scope, scope)) + for scope in scopes + ]), + docs, + ) + api_methods['/'.join((url_prefix, action))][method] = docs + + return api_methods + + def GET_help(self): + from r2.controllers.apiv1 import APIv1Controller + from r2.controllers.oauth2 import OAuth2FrontendController, OAuth2AccessController, scope_info + + api_methods = defaultdict(dict) + for controller, url_prefix in ((ApiController, '/api'), + (ApiminimalController, '/api'), + (OAuth2FrontendController, '/api/v1'), + (OAuth2AccessController, '/api/v1'), + (APIv1Controller, '/api/v1')): + for url, methods in self.docs_from_controller(controller, url_prefix).iteritems(): + api_methods[url].update(methods) + + return BoringPage( + _('api documentation'), + content=ApiHelp( + api_methods=api_methods, + oauth2_scopes=scope_info, + ) + ).render() diff --git a/r2/r2/controllers/apiv1.py b/r2/r2/controllers/apiv1.py index 5a6745412..5a61aa5fb 100644 --- a/r2/r2/controllers/apiv1.py +++ b/r2/r2/controllers/apiv1.py @@ -29,5 +29,6 @@ class APIv1Controller(OAuth2ResourceController): @require_oauth2_scope("identity") 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) diff --git a/r2/r2/controllers/oauth2.py b/r2/r2/controllers/oauth2.py index 07d4c0012..2cd183a25 100644 --- a/r2/r2/controllers/oauth2.py +++ b/r2/r2/controllers/oauth2.py @@ -89,6 +89,8 @@ class OAuth2FrontendController(RedditController): scope = VOneOf("scope", scope_info.keys()), state = VRequired("state", errors.NO_TEXT)) def GET_authorize(self, response_type, client, redirect_uri, scope, state): + """Endpoint for OAuth2 authorization.""" + self._check_redirect_uri(client, redirect_uri) resp = {} @@ -107,6 +109,8 @@ class OAuth2FrontendController(RedditController): state = VRequired("state", errors.NO_TEXT), authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED)) def POST_authorize(self, authorize, client, redirect_uri, scope, state): + """Endpoint for OAuth2 authorization.""" + self._check_redirect_uri(client, redirect_uri) resp = {} diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 35dab8eaf..185a9c4e7 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -139,6 +139,9 @@ def validate(*simple_vals, **param_vals): return self.intermediate_redirect('/login') except VerifiedUserRequiredException: return self.intermediate_redirect('/verify') + + newfn.__name__ = fn.__name__ + newfn.__doc__ = fn.__doc__ return newfn return val @@ -183,11 +186,14 @@ def api_validate(response_type=None): except VerifiedUserRequiredException: responder.send_failure(errors.VERIFIED_USER_REQUIRED) return self.api_wrapper(responder.make_response()) + + newfn.__name__ = fn.__name__ + newfn.__doc__ = fn.__doc__ return newfn return val return _api_validate return wrap - + @api_validate("html") def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw): diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 07be0af9c..417bc3b9b 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -3782,3 +3782,9 @@ class UserIPHistory(Templated): def __init__(self): self.ips = ips_by_account_id(c.user._id) super(UserIPHistory, self).__init__() + +class ApiHelp(Templated): + def __init__(self, api_methods, oauth2_scopes, *a, **kw): + self.api_methods = api_methods + self.oauth2_scopes = oauth2_scopes + super(ApiHelp, self).__init__(*a, **kw) diff --git a/r2/r2/templates/apihelp.html b/r2/r2/templates/apihelp.html new file mode 100644 index 000000000..4194c9683 --- /dev/null +++ b/r2/r2/templates/apihelp.html @@ -0,0 +1,78 @@ +<%! + from r2.lib.filters import safemarkdown +%> + +
This is the automatically-generated documentation for the Reddit API.
+It's gathered from the docstrings in the code.
+