Moar plugins polishing and sugar
This commit is contained in:
parent
4c4ab5919a
commit
8c587d07c4
|
@ -14,9 +14,25 @@ class ConfigError(ValueError):
|
||||||
class Plugin(AppConfig):
|
class Plugin(AppConfig):
|
||||||
conf = {}
|
conf = {}
|
||||||
path = "noop"
|
path = "noop"
|
||||||
|
conf_serializer = None
|
||||||
|
|
||||||
def get_conf(self):
|
def get_conf(self):
|
||||||
return {"enabled": self.plugin_settings.enabled}
|
return self.instance.conf
|
||||||
|
|
||||||
|
def set_conf(self, data):
|
||||||
|
if self.conf_serializer:
|
||||||
|
s = self.conf_serializer(data=data)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
data = s.validated_data
|
||||||
|
instance = self.instance()
|
||||||
|
instance.conf = data
|
||||||
|
instance.save(update_fields=["conf"])
|
||||||
|
|
||||||
|
def instance(self):
|
||||||
|
"""Return the DB object that match the plugin"""
|
||||||
|
from funkwhale_api.common import models
|
||||||
|
|
||||||
|
return models.PodPlugin.objects.get_or_create(code=self.name)[0]
|
||||||
|
|
||||||
def plugin_settings(self):
|
def plugin_settings(self):
|
||||||
"""
|
"""
|
||||||
|
@ -94,7 +110,13 @@ plugins_manager.add_hookspecs(HookSpec())
|
||||||
|
|
||||||
|
|
||||||
def register(plugin_class):
|
def register(plugin_class):
|
||||||
return plugins_manager.register(plugin_class("noop", "noop"))
|
return plugins_manager.register(plugin_class(plugin_class.name, "noop"))
|
||||||
|
|
||||||
|
|
||||||
|
def save(plugin_class):
|
||||||
|
from funkwhale_api.common.models import PodPlugin
|
||||||
|
|
||||||
|
return PodPlugin.objects.get_or_create(code=plugin_class.name)[0]
|
||||||
|
|
||||||
|
|
||||||
def trigger_hook(name, *args, **kwargs):
|
def trigger_hook(name, *args, **kwargs):
|
||||||
|
@ -104,6 +126,13 @@ def trigger_hook(name, *args, **kwargs):
|
||||||
|
|
||||||
@register
|
@register
|
||||||
class DefaultPlugin(Plugin):
|
class DefaultPlugin(Plugin):
|
||||||
|
name = "default"
|
||||||
|
verbose_name = "Default plugin"
|
||||||
|
|
||||||
@plugin_hook
|
@plugin_hook
|
||||||
def database_engine(self):
|
def database_engine(self):
|
||||||
return "django.db.backends.postgresql"
|
return "django.db.backends.postgresql"
|
||||||
|
|
||||||
|
@plugin_hook
|
||||||
|
def urls(self):
|
||||||
|
return []
|
||||||
|
|
|
@ -24,7 +24,11 @@ class Plugins(persisting_theory.Registry):
|
||||||
look_into = "entrypoint"
|
look_into = "entrypoint"
|
||||||
|
|
||||||
|
|
||||||
PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
|
PLUGINS = [
|
||||||
|
"funkwhale_plugin_{}".format(p)
|
||||||
|
for p in env.list("FUNKWHALE_PLUGINS", default=[])
|
||||||
|
if p
|
||||||
|
]
|
||||||
"""
|
"""
|
||||||
List of Funkwhale plugins to load.
|
List of Funkwhale plugins to load.
|
||||||
"""
|
"""
|
||||||
|
@ -33,8 +37,6 @@ from config import plugins # noqa
|
||||||
plugins_registry = Plugins()
|
plugins_registry = Plugins()
|
||||||
plugins_registry.autodiscover(PLUGINS)
|
plugins_registry.autodiscover(PLUGINS)
|
||||||
|
|
||||||
# plugins.plugins_manager.register(Plugin("noop", "noop"))
|
|
||||||
|
|
||||||
LOGLEVEL = env("LOGLEVEL", default="info").upper()
|
LOGLEVEL = env("LOGLEVEL", default="info").upper()
|
||||||
"""
|
"""
|
||||||
Default logging level for the Funkwhale processes""" # pylint: disable=W0105
|
Default logging level for the Funkwhale processes""" # pylint: disable=W0105
|
||||||
|
@ -272,7 +274,7 @@ List of Django apps to load in addition to Funkwhale plugins and apps.
|
||||||
PLUGINS_APPS = tuple()
|
PLUGINS_APPS = tuple()
|
||||||
|
|
||||||
for p in plugins.trigger_hook("register_apps"):
|
for p in plugins.trigger_hook("register_apps"):
|
||||||
PLUGINS_APPS += (p,)
|
PLUGINS_APPS += tuple(p)
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
DJANGO_APPS
|
DJANGO_APPS
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
@ -8,12 +11,27 @@ from django.conf import settings
|
||||||
from . import base
|
from . import base
|
||||||
|
|
||||||
|
|
||||||
|
PIP = os.path.join(sys.prefix, "bin", "pip")
|
||||||
|
|
||||||
|
|
||||||
@base.cli.group()
|
@base.cli.group()
|
||||||
def plugins():
|
def plugins():
|
||||||
"""Install, configure and remove plugins"""
|
"""Install, configure and remove plugins"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_plugins():
|
||||||
|
plugins = [
|
||||||
|
f.path
|
||||||
|
for f in os.scandir(settings.FUNKWHALE_PLUGINS_PATH)
|
||||||
|
if "/funkwhale_plugin_" in f.path
|
||||||
|
]
|
||||||
|
plugins = [
|
||||||
|
p.split("-")[0].split("/")[-1].replace("funkwhale_plugin_", "") for p in plugins
|
||||||
|
]
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
@plugins.command("install")
|
@plugins.command("install")
|
||||||
@click.argument("name_or_url", nargs=-1)
|
@click.argument("name_or_url", nargs=-1)
|
||||||
@click.option("--builtins", is_flag=True)
|
@click.option("--builtins", is_flag=True)
|
||||||
|
@ -21,17 +39,56 @@ def plugins():
|
||||||
def install(name_or_url, builtins, pip_args):
|
def install(name_or_url, builtins, pip_args):
|
||||||
"""
|
"""
|
||||||
Installed the specified plug using their name.
|
Installed the specified plug using their name.
|
||||||
|
|
||||||
If --builtins is provided, it will also install
|
|
||||||
plugins present at FUNKWHALE_PLUGINS_PATH
|
|
||||||
"""
|
"""
|
||||||
pip_args = pip_args or ""
|
pip_args = pip_args or ""
|
||||||
target_path = settings.FUNKWHALE_PLUGINS_PATH
|
all_plugins = []
|
||||||
builtins_path = os.path.join(settings.APPS_DIR, "plugins")
|
for p in name_or_url:
|
||||||
builtins_plugins = [f.path for f in os.scandir(builtins_path) if f.is_dir()]
|
builtin_path = os.path.join(
|
||||||
command = "pip install {} --target={} {}".format(
|
settings.APPS_DIR, "plugins", "funkwhale_plugin_{}".format(p)
|
||||||
pip_args, target_path, " ".join(builtins_plugins)
|
)
|
||||||
|
if os.path.exists(builtin_path):
|
||||||
|
all_plugins.append(builtin_path)
|
||||||
|
else:
|
||||||
|
all_plugins.append(p)
|
||||||
|
install_plugins(pip_args, all_plugins)
|
||||||
|
click.echo(
|
||||||
|
"Installation completed, ensure FUNKWHALE_PLUGINS={} is present in your .env file".format(
|
||||||
|
",".join(get_all_plugins())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_plugins(pip_args, all_plugins):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
command = "{} install {} --target {} --build={} {}".format(
|
||||||
|
PIP,
|
||||||
|
pip_args,
|
||||||
|
settings.FUNKWHALE_PLUGINS_PATH,
|
||||||
|
tmpdirname,
|
||||||
|
" ".join(all_plugins),
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
command, shell=True, check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@plugins.command("uninstall")
|
||||||
|
@click.argument("name", nargs=-1)
|
||||||
|
def uninstall(name):
|
||||||
|
"""
|
||||||
|
Remove plugins
|
||||||
|
"""
|
||||||
|
to_remove = ["funkwhale_plugin_{}".format(n) for n in name]
|
||||||
|
command = "{} uninstall -y {}".format(PIP, " ".join(to_remove))
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
command, shell=True, check=True,
|
command, shell=True, check=True,
|
||||||
)
|
)
|
||||||
|
for f in os.scandir(settings.FUNKWHALE_PLUGINS_PATH):
|
||||||
|
for n in name:
|
||||||
|
if "/funkwhale_plugin_{}".format(n) in f.path:
|
||||||
|
shutil.rmtree(f.path)
|
||||||
|
click.echo(
|
||||||
|
"Removal completed, set FUNKWHALE_PLUGINS={} in your .env file".format(
|
||||||
|
",".join(get_all_plugins())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 3.0.6 on 2020-06-17 19:02
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0007_auto_20200116_1610'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PodPlugin',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('conf', django.contrib.postgres.fields.jsonb.JSONField(default=None, null=True, blank=True)),
|
||||||
|
('code', models.CharField(max_length=100, unique=True)),
|
||||||
|
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='attachment',
|
||||||
|
name='url',
|
||||||
|
field=models.URLField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,6 +18,7 @@ from django.urls import reverse
|
||||||
from versatileimagefield.fields import VersatileImageField
|
from versatileimagefield.fields import VersatileImageField
|
||||||
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
|
||||||
|
|
||||||
|
from config import plugins
|
||||||
from funkwhale_api.federation import utils as federation_utils
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -363,3 +364,17 @@ def remove_attached_content(sender, instance, **kwargs):
|
||||||
getattr(instance, field).delete()
|
getattr(instance, field).delete()
|
||||||
except Content.DoesNotExist:
|
except Content.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PodPlugin(models.Model):
|
||||||
|
conf = JSONField(default=None, null=True, blank=True)
|
||||||
|
code = models.CharField(max_length=100, unique=True)
|
||||||
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self):
|
||||||
|
"""Links to the Plugin instance in entryposint.py"""
|
||||||
|
candidates = plugins.plugins_manager.get_plugins()
|
||||||
|
for p in candidates:
|
||||||
|
if p.name == self.code:
|
||||||
|
return p
|
||||||
|
|
|
@ -339,3 +339,21 @@ class NullToEmptDict(object):
|
||||||
if not v:
|
if not v:
|
||||||
return v
|
return v
|
||||||
return super().to_representation(v)
|
return super().to_representation(v)
|
||||||
|
|
||||||
|
|
||||||
|
class PodPluginSerializer(serializers.Serializer):
|
||||||
|
code = serializers.CharField(read_only=True)
|
||||||
|
enabled = serializers.BooleanField()
|
||||||
|
conf = serializers.JSONField()
|
||||||
|
label = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
"code",
|
||||||
|
"label",
|
||||||
|
"enabled",
|
||||||
|
"conf",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_label(self, o):
|
||||||
|
return o.plugin.verbose_name
|
||||||
|
|
|
@ -5,7 +5,8 @@ from config import plugins
|
||||||
|
|
||||||
@plugins.register
|
@plugins.register
|
||||||
class Plugin(plugins.Plugin):
|
class Plugin(plugins.Plugin):
|
||||||
name = "prometheus_exporter"
|
name = "funkwhale_plugin_prometheus"
|
||||||
|
verbose_name = "Prometheus metrics exporter"
|
||||||
|
|
||||||
@plugins.plugin_hook
|
@plugins.plugin_hook
|
||||||
def database_engine(self):
|
def database_engine(self):
|
||||||
|
@ -13,7 +14,7 @@ class Plugin(plugins.Plugin):
|
||||||
|
|
||||||
@plugins.plugin_hook
|
@plugins.plugin_hook
|
||||||
def register_apps(self):
|
def register_apps(self):
|
||||||
return "django_prometheus"
|
return ["django_prometheus"]
|
||||||
|
|
||||||
@plugins.plugin_hook
|
@plugins.plugin_hook
|
||||||
def middlewares_before(self):
|
def middlewares_before(self):
|
|
@ -1,6 +1,6 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = funkwhale-prometheus
|
name = funkwhale-plugin-prometheus
|
||||||
description = "A prometheus metric exporter for your Funkwhale pod"
|
description = "A prometheus metrics exporter for your Funkwhale pod"
|
||||||
version = 0.1.dev0
|
version = 0.1.dev0
|
||||||
author = Agate Blue
|
author = Agate Blue
|
||||||
author_email = me@agate.blue
|
author_email = me@agate.blue
|
|
@ -0,0 +1,34 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from config import plugins
|
||||||
|
from funkwhale_api.common import models
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_validate_set_conf():
|
||||||
|
class S(serializers.Serializer):
|
||||||
|
test = serializers.CharField()
|
||||||
|
foo = serializers.BooleanField()
|
||||||
|
|
||||||
|
class P(plugins.Plugin):
|
||||||
|
conf_serializer = S
|
||||||
|
|
||||||
|
p = P("noop", "noop")
|
||||||
|
with pytest.raises(serializers.ValidationError):
|
||||||
|
assert p.set_conf({"test": "hello", "foo": "bar"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_validate_set_conf_persists():
|
||||||
|
class S(serializers.Serializer):
|
||||||
|
test = serializers.CharField()
|
||||||
|
foo = serializers.BooleanField()
|
||||||
|
|
||||||
|
class P(plugins.Plugin):
|
||||||
|
name = "test_plugin"
|
||||||
|
conf_serializer = S
|
||||||
|
|
||||||
|
p = P("noop", "noop")
|
||||||
|
p.set_conf({"test": "hello", "foo": False})
|
||||||
|
assert p.instance() == models.PodPlugin.objects.latest("id")
|
||||||
|
assert p.instance().conf == {"test": "hello", "foo": False}
|
|
@ -6,6 +6,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
|
from config import plugins
|
||||||
from funkwhale_api.common import serializers
|
from funkwhale_api.common import serializers
|
||||||
from funkwhale_api.common import utils
|
from funkwhale_api.common import utils
|
||||||
from funkwhale_api.users import models
|
from funkwhale_api.users import models
|
||||||
|
@ -267,3 +268,21 @@ def test_content_serializer(factories):
|
||||||
serializer = serializers.ContentSerializer(content)
|
serializer = serializers.ContentSerializer(content)
|
||||||
|
|
||||||
assert serializer.data == expected
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_serializer():
|
||||||
|
class TestPlugin(plugins.Plugin):
|
||||||
|
name = "test_plugin"
|
||||||
|
verbose_name = "A test plugin"
|
||||||
|
|
||||||
|
plugins.register(TestPlugin)
|
||||||
|
instance = plugins.save(TestPlugin)
|
||||||
|
assert isinstance(instance.plugin, TestPlugin)
|
||||||
|
expected = {
|
||||||
|
"code": "test_plugin",
|
||||||
|
"label": "A test plugin",
|
||||||
|
"enabled": True,
|
||||||
|
"conf": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert serializers.PodPluginSerializer(instance).data == expected
|
||||||
|
|
|
@ -24,6 +24,7 @@ from aioresponses import aioresponses
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
from rest_framework.test import APIClient, APIRequestFactory
|
from rest_framework.test import APIClient, APIRequestFactory
|
||||||
|
|
||||||
|
from config import plugins
|
||||||
from funkwhale_api.activity import record
|
from funkwhale_api.activity import record
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
from funkwhale_api.moderation import mrf
|
from funkwhale_api.moderation import mrf
|
||||||
|
@ -429,3 +430,13 @@ def clear_license_cache(db):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def faker():
|
def faker():
|
||||||
return factory.Faker._get_faker()
|
return factory.Faker._get_faker()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def plugins_manager():
|
||||||
|
return plugins.PluginManager("tests")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hook(plugins_manager):
|
||||||
|
return plugins.HookimplMarker("tests")
|
||||||
|
|
|
@ -13,5 +13,6 @@ Reference
|
||||||
architecture
|
architecture
|
||||||
../api
|
../api
|
||||||
./authentication
|
./authentication
|
||||||
|
./plugins
|
||||||
../federation/index
|
../federation/index
|
||||||
subsonic
|
subsonic
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Funkwhale Plugins
|
||||||
|
=================
|
||||||
|
|
||||||
|
With version 1.0, Funkwhale makes it possible for third party to write plugins
|
||||||
|
and distribute them.
|
||||||
|
|
||||||
|
Funkwhale plugins are regular django apps, that can register models, API
|
||||||
|
endpoints, and react to specific events (e.g a son was listened, a federation message was delivered, etc.)
|
Loading…
Reference in New Issue