diff --git a/changes/changelog.d/1910.doc b/changes/changelog.d/1910.doc new file mode 100644 index 000000000..a1f331caa --- /dev/null +++ b/changes/changelog.d/1910.doc @@ -0,0 +1 @@ +Rewrote the plugins documentation (#1910) diff --git a/docs/developer_documentation/plugins/create.md b/docs/developer_documentation/plugins/create.md new file mode 100644 index 000000000..f65cb2632 --- /dev/null +++ b/docs/developer_documentation/plugins/create.md @@ -0,0 +1,200 @@ +# Write a plugin + +You can write plugins to extend the features of your Funkwhale pod. Follow the instructions in this guide to get started with your first plugin. + +```{contents} +:local: +:depth: 2 +``` + +## Before you begin + +Before you start writing your plugin, you need to understand the following core concepts: + +```{contents} +:local: +:depth: 1 +``` + +We'll explain each of these concepts in the next few sections + +### Scopes + +Plugins fall into two different **scopes**: + +1. User-level plugins that are configured by end-users for their own use +2. Pod-level plugins that are configured by pod admins and are not connected to a particular user + +User-level plugins can also be used to import files from a third-party service, such as cloud storage or FTP. + +### Hooks + +**Hooks** are entrypoints that allow your plugin to listen to changes. You can create hooks to react to different events that occur in the Funkwhale application. + +An example of this can be seen in our Scrobbler plugin. We register a `LISTENING_CREATED` hook to notify any registered callback function when a listening is recorded. When a user listens to a track, the `notfy_lastfm` function fires. + +```{code-block} python +from config import plugins +from .funkwhale_startup import PLUGIN + +@plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN) +def notify_lastfm(listening, conf, **kwargs): + # do something +``` + +#### Available hooks + +```{eval-rst} +.. autodata:: config.plugins.LISTENING_CREATED +``` + +### Filters + +**Filters** are entrypoints that allow you to modify or add information. When you use the `register_filter` decorator, your function should return a value to be used by the server. + +In this example, the `PLUGINS_DEPENDENCIES` filter is used to install additional dependencies required by your plugin. The `dependencies` function returns the additional dependency `django_prometheus` to request the dependency be installed by the server. + +```{code-block} python +# funkwhale_startup.py +# ... +from config import plugins + +@plugins.register_filter(plugins.PLUGINS_DEPENDENCIES, PLUGIN) +def dependencies(dependencies, **kwargs): + return dependencies + ["django_prometheus"] + +``` + +#### Available filters + +```{eval-rst} +.. autodata:: config.plugins.PLUGINS_DEPENDENCIES +.. autodata:: config.plugins.PLUGINS_APPS +.. autodata:: config.plugins.MIDDLEWARES_BEFORE +.. autodata:: config.plugins.MIDDLEWARES_AFTER +.. autodata:: config.plugins.URLS +``` + +## Write your plugin + +Once you know what type of plugin you want to write and what entrypoint you want to use, you can start writing your plugin. + +Plugins are made up of the following 3 files: + +- `__init__.py` - indicates that the directory is a Python package +- `funkwhale_startup.py` - the file that loads during Funkwhale initialization +- `funkwhale_ready.py` - the file that loads when Funkwhale is configured and ready + +### Declare your plugin + +You need to declare your plugin and its configuration options so that Funkwhale knows how to load the plugin. To do this, you must declare a new `plugins` instance in your `funkwhale_startup.py` file. + +Your `plugins` should include the following information: + +```{list-table} +:header-rows: 1 + + * - Parameter + - Data type + - Description + * - `name` + - String + - The name of your plugin, used in the `.env` file + * - `label` + - String + - The readable label that appears in the Funkwhale frontend + * - `description` + - String + - A meaningful description of your plugin and what it does + * - `version` + - String + - The version number of your plugin + * - `user` + - Boolean + - Whether the plugin is a **user-level** plugin or a **pod-level** plugin. See [scopes](#scopes) for more information + * - `conf` + - Array of Objects + - A list of configuration options + +``` + +In this example, we declare a new **user-level** plugin called "My Plugin". The user can configure a `greeting` in the plugin configuration. + +```{code-block} python +# funkwhale_startup.py +from config import plugins + +PLUGIN = plugins.get_plugin_config( + name="myplugin", + label="My Plugin", + description="An example plugin that greets you", + version="0.1", + user=True, + conf=[ + # This configuration option is editable by each user + {"name": "greeting", "type": "text", "label": "Greeting", "default": "Hello"}, + ], +) +``` + +### Write your plugin logic + +Once you've declared your plugin, you can write the plugin code in your `funkwhale_ready.py` file. + +```{note} +You must import your plugin declaration from your `funkwhale_startup.py` file. +``` + +In this example, we create a simple API endpoint that returns a greeting to the user. To do this: + +1. We create a new APIView class that accepts a `GET` request +2. We read the greeting value from the plugin `conf` +3. We return the greeting value with the user's username +4. We register this view at the endpoint `/greeting` + +```{code-block} python +# funkwhale_ready.py +from django.urls import path +from rest_framework import response +from rest_framework import views + +from config import plugins + +# Import the plugin declaration from funkwhale_startup +from .funkwhale_startup import PLUGIN + +# Create a new APIView class +class GreetingView(views.APIView): + permission_classes = [] + # Register a GET response + def get(self, request, *args, **kwargs): + # Check the conf value of the plugin for the user + conf = plugins.get_conf(PLUGIN["name"], request.user) + if not conf["enabled"]: + # Return an error code if the user hasn't enabled the plugin + return response.Response(status=405) + # Set the greeting value to the user's configured greeting + greeting = conf["conf"]["greeting"] + data = { + # Append the user's username to the greeting + "greeting": "{} {}!".format(greeting, request.user.username) + } + # Return the greeting + return response.Response(data) + +# Register the new APIView at the /greeting endpoint +@plugins.register_filter(plugins.URLS, PLUGIN) +def register_view(urls, **kwargs): + return urls + [ + path('greeting', GreetingView.as_view()) + ] +``` + +### Result + +Here is an example of how the above plugin works: + +1. User "Harry" enables the plugin +2. "Harry" changes the greeting to "You're a wizard" +3. "Harry" visits the `/greeting` endpoint in their browser +4. The browser returns the message "You're a wizard Harry" diff --git a/docs/developer_documentation/plugins/index.md b/docs/developer_documentation/plugins/index.md new file mode 100644 index 000000000..6bce1a96c --- /dev/null +++ b/docs/developer_documentation/plugins/index.md @@ -0,0 +1,16 @@ +# Funkwhale plugins + +Plugins can be used to extend Funkwhale's featureset without needing to touch the underlying code. Plugins can extend existing features, add support for third-party services, or introduce cosmetic changes to the Funkwhale webapp. + +Plugins have been supported since Funkwhale 1.0. Some core plugins, such as the standard Scrobbler plugin, are maintained by the Funkwhale team. + +```{toctree} +--- +caption: Resources +maxdepth: 1 +--- + +create +install + +``` diff --git a/docs/developer_documentation/plugins/install.md b/docs/developer_documentation/plugins/install.md new file mode 100644 index 000000000..4f50c50ca --- /dev/null +++ b/docs/developer_documentation/plugins/install.md @@ -0,0 +1,50 @@ +# Install a plugin + +Once you have [created your plugin](create.md), you can install it on your Funkwhale pod. + +## Install a local plugin + +To install a plugin located on your server: + +1. Add the plugin directory to the `FUNKWHALE_PLUGINS_PATH` variable in your `.env` file +2. Add the plugin name to the `FUNKWHALE_PLUGINS` variable in your `.env` file + + ```{code-block} text + FUNKWHALE_PLUGINS=myplugin,anotherplugin + ``` + +3. Restart Funkwhale to pick up the changes + +## Install a third-party plugin + +You can install third-party plugins using the `manage.py` script. To do this: + +1. Add the plugin name to the `FUNKWHALE_PLUGINS` variable in your `.env` file + + ```{code-block} text + FUNKWHALE_PLUGINS=myplugin,anotherplugin + ``` + +2. Call the `manage.py` script with the location of the plugin archive + + :::: {tab-set} + + :::{tab-item} Debian + + ```{code-block} shell + python manage.py fw plugins install https://plugin_url.zip + ``` + + ::: + + :::{tab-item} Docker + + ```{code-block} shell + docker-compose run --rm api python manage.py fw plugins install https://plugin_url.zip + ``` + + ::: + + :::: + +3. Restart Funkwhale to pick up the changes diff --git a/docs/developers/authentication.rst b/docs/developers/authentication.rst deleted file mode 100644 index 6c8f7d6de..000000000 --- a/docs/developers/authentication.rst +++ /dev/null @@ -1,95 +0,0 @@ -API Authentication -================== - -Each Funkwhale API endpoint supports access from: - -- Anonymous users (if the endpoint is configured to do so, for exemple via the ``API Authentication Required`` setting) -- Logged-in users -- Third-party apps (via OAuth2) - -To seamlessly support this range of access modes, we internally use oauth scopes -to describes what permissions are required to perform any given operation. - -OAuth ------ - -Create an app -::::::::::::: - -To connect to Funkwhale API via OAuth, you need to create an application. There are -two ways to do that: - -1. By visiting ``/settings/applications/new`` when logged in on your Funkwhale instance. -2. By sending a ``POST`` request to ``/api/v1/oauth/apps/``, as described in `our API documentation `_. - -Both method will give you a client ID and secret. - -Getting an access token -::::::::::::::::::::::: - -Once you have a client ID and secret, you can request access tokens -using the `authorization code grant flow `_. - -We support the ``urn:ietf:wg:oauth:2.0:oob`` redirect URI for non-web applications, as well -as traditionnal redirection-based flow. - -Our authorization endpoint is located at ``/authorize``, and our token endpoint at ``/api/v1/oauth/token/``. - -Refreshing tokens -::::::::::::::::: - -When your access token is expired, you can `request a new one as described in the OAuth specification `_. - -Security considerations -::::::::::::::::::::::: - -- Grant codes are valid for a 5 minutes after authorization request is approved by the end user. -- Access codes are valid for 10 hours. When expired, you will need to request a new one using your refresh token. -- We return a new refresh token everytime an access token is requested, and invalidate the old one. Ensure you store the new refresh token in your app. - - -Scopes -:::::: - -Scopes are defined in :file:`funkwhale_api/users/oauth/scopes.py:BASE_SCOPES`, and generally are mapped to a business-logic resources (follows, favorites, etc.). All those base scopes come in two flawours: - -- `read:`: get read-only access to the resource -- `write:`: get write-only access to the ressource - -For example, ``playlists`` is a base scope, and ``write:playlists`` is the actual scope needed to perform write -operations on playlists (via a ``POST``, ``PATCH``, ``PUT`` or ``DELETE``. ``read:playlists`` is used -to perform read operations on playlists such as fetching a given playlist via ``GET``. - -Having the generic ``read`` or ``write`` scope give you the corresponding access on *all* resources. - -This is the list of OAuth scopes that third-party applications can request: - -.. list-table:: Oauth scopes - :header-rows: 1 - - * - Scope - - Description - * - ``read`` - - Read-only access to all data (equivalent to all ``read:*`` scopes). - * - ``write`` - - Read-only access to all data (equivalent to all ``write:*`` scopes). - * - ``:profile`` - - Access to profile data (e-mail address, username, etc.) - * - ``:libraries`` - - Access to library data (uploads, libraries, tracks, albums, artists…) - * - ``:favorites`` - - Access to favorites - * - ``:listenings`` - - Access to history - * - ``:follows`` - - Access to followers - * - ``:playlists`` - - Access to playlists - * - ``:radios`` - - Access to radios - * - ``:filters`` - - Access to content filters - * - ``:notifications`` - - Access to notifications - * - ``:edits`` - - Access to metadata edits diff --git a/docs/developers/index.rst b/docs/developers/index.rst deleted file mode 100644 index e8201d501..000000000 --- a/docs/developers/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Developer documentation -========================= - -This documentation is targeted primarily at developers who want to understand -how Funkwhale works and how to build apps that integrate with Funkwhale's ecosystem. - -.. toctree:: - :maxdepth: 2 - - ../api - ./authentication - ./plugins - ../federation/index - subsonic diff --git a/docs/developers/plugins.rst b/docs/developers/plugins.rst deleted file mode 100644 index abfd58dc1..000000000 --- a/docs/developers/plugins.rst +++ /dev/null @@ -1,165 +0,0 @@ -Funkwhale plugins -================= - -Starting with Funkwhale 1.0, it is now possible to implement new features -via plugins. - -Some plugins are maintained by the Funkwhale team (e.g. this is the case of the ``scrobbler`` plugin), -or by third-parties. - -Installing a plugin -------------------- - -To install a plugin, ensure its directory is present in the ``FUNKWHALE_PLUGINS_PATH`` directory. - -Then, add its name to the ``FUNKWHALE_PLUGINS`` environment variable, like this:: - - FUNKWHALE_PLUGINS=myplugin,anotherplugin - -We provide a command to make it easy to install third-party plugins:: - - python manage.py fw plugins install https://pluginurl.zip - -.. note:: - - If you use the command, you will still need to append the plugin name to ``FUNKWHALE_PLUGINS`` - - -Types of plugins ----------------- - -There are two types of plugins: - -1. Plugins that are accessible to end-users, a.k.a. user-level plugins. This is the case of our Scrobbler plugin -2. Pod-level plugins that are configured by pod admins and are not tied to a particular user - -Additionally, user-level plugins can be regular plugins or source plugins. A source plugin provides -a way to import files from a third-party service, e.g via webdav, FTP or something similar. - -Hooks and filters ------------------ - -Funkwhale includes two kind of entrypoints for plugins to use: hooks and filters. B - -Hooks should be used when you want to react to some change. For instance, the ``LISTENING_CREATED`` hook -notify each registered callback that a listening was created. Our ``scrobbler`` plugin has a callback -registered to this hook, so that it can notify Last.fm properly: - -.. code-block:: python - - from config import plugins - from .funkwhale_startup import PLUGIN - - @plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN) - def notify_lastfm(listening, conf, **kwargs): - # do something - -Filters work slightly differently, and expect callbacks to return a value that will be used by Funkwhale. - -For instance, the ``PLUGINS_DEPENDENCIES`` filter can be used as a way to install additional dependencies needed by your plugin: - - -.. code-block:: python - - # funkwhale_startup.py - # ... - from config import plugins - - @plugins.register_filter(plugins.PLUGINS_DEPENDENCIES, PLUGIN) - def dependencies(dependencies, **kwargs): - return dependencies + ["django_prometheus"] - -To sum it up, hooks are used when you need to react to something, and filters when you need to alter something. - -Writing a plugin ----------------- - -Regardless of the type of plugin you want to write, lots of concepts are similar. - -First, a plugin need three files: - -- a ``__init__.py`` file, since it's a Python package -- a ``funkwhale_startup.py`` file, that is loaded during Funkwhale initialization -- a ``funkwhale_ready.py`` file, that is loaded when Funkwhale is configured and ready - -So your plugin directory should look like this:: - - myplugin - ├── funkwhale_ready.py - ├── funkwhale_startup.py - └── __init__.py - -Now, let's write our plugin! - -``funkwhale_startup.py`` is where you declare your plugin and it's configuration options: - -.. code-block:: python - - # funkwhale_startup.py - from config import plugins - - PLUGIN = plugins.get_plugin_config( - name="myplugin", - label="My Plugin", - description="An example plugin that greets you", - version="0.1", - # here, we write a user-level plugin - user=True, - conf=[ - # this configuration options are editable by each user - {"name": "greeting", "type": "text", "label": "Greeting", "default": "Hello"}, - ], - ) - -Now that our plugin is declared and configured, let's implement actual functionality in ``funkwhale_ready.py``: - -.. code-block:: python - - # funkwhale_ready.py - from django.urls import path - from rest_framework import response - from rest_framework import views - - from config import plugins - - from .funkwhale_startup import PLUGIN - - # Our greeting view, where the magic happens - class GreetingView(views.APIView): - permission_classes = [] - def get(self, request, *args, **kwargs): - # retrieve plugin configuration for the current user - conf = plugins.get_conf(PLUGIN["name"], request.user) - if not conf["enabled"]: - # plugin is disabled for this user - return response.Response(status=405) - greeting = conf["conf"]["greeting"] - data = { - "greeting": "{} {}!".format(greeting, request.user.username) - } - return response.Response(data) - - # Ensure our view is known by Django and available at /greeting - @plugins.register_filter(plugins.URLS, PLUGIN) - def register_view(urls, **kwargs): - return urls + [ - path('greeting', GreetingView.as_view()) - ] - -And that's pretty much it. Now, login, visit https://yourpod.domain/settings/plugins, set a value in the ``greeting`` field and enable the plugin. - -After that, you should be greeted properly if you go to https://yourpod.domain/greeting. - -Hooks reference ---------------- - -.. autodata:: config.plugins.LISTENING_CREATED - -Filters reference ------------------ - -.. autodata:: config.plugins.PLUGINS_DEPENDENCIES -.. autodata:: config.plugins.PLUGINS_APPS -.. autodata:: config.plugins.MIDDLEWARES_BEFORE -.. autodata:: config.plugins.MIDDLEWARES_AFTER -.. autodata:: config.plugins.URLS diff --git a/docs/index.md b/docs/index.md index 68e44872e..aab103d87 100644 --- a/docs/index.md +++ b/docs/index.md @@ -76,6 +76,7 @@ developer_documentation/setup/index developer_documentation/contribute/index developer_documentation/workflows/index developer_documentation/api/index +developer_documentation/plugins/index ```