Rewrite plugins docs

environments/review-docs-feder-8q8gxy/deployments/14273
Ciarán Ainsworth 2022-10-20 22:04:38 +02:00
rodzic 5d4c10165f
commit a16343a531
8 zmienionych plików z 268 dodań i 274 usunięć

Wyświetl plik

@ -0,0 +1 @@
Rewrote the plugins documentation (#1910)

Wyświetl plik

@ -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"

Wyświetl plik

@ -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
```

Wyświetl plik

@ -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

Wyświetl plik

@ -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 <https://docs.funkwhale.audio/swagger/>`_.
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 <https://tools.ietf.org/html/rfc6749#section-4.1>`_.
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 <https://tools.ietf.org/html/rfc6749#section-6>`_.
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:<base_scope>`: get read-only access to the resource
- `write:<base_scope>`: 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).
* - ``<read/write>:profile``
- Access to profile data (e-mail address, username, etc.)
* - ``<read/write>:libraries``
- Access to library data (uploads, libraries, tracks, albums, artists…)
* - ``<read/write>:favorites``
- Access to favorites
* - ``<read/write>:listenings``
- Access to history
* - ``<read/write>:follows``
- Access to followers
* - ``<read/write>:playlists``
- Access to playlists
* - ``<read/write>:radios``
- Access to radios
* - ``<read/write>:filters``
- Access to content filters
* - ``<read/write>:notifications``
- Access to notifications
* - ``<read/write>:edits``
- Access to metadata edits

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -76,6 +76,7 @@ developer_documentation/setup/index
developer_documentation/contribute/index
developer_documentation/workflows/index
developer_documentation/api/index
developer_documentation/plugins/index
```