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
