351 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
| import copy
 | |
| import logging
 | |
| import os
 | |
| import subprocess
 | |
| import sys
 | |
| 
 | |
| import persisting_theory
 | |
| from django.core.cache import cache
 | |
| from django.db.models import Q
 | |
| from rest_framework import serializers
 | |
| 
 | |
| logger = logging.getLogger("plugins")
 | |
| 
 | |
| 
 | |
| class Startup(persisting_theory.Registry):
 | |
|     look_into = "persisting_theory"
 | |
| 
 | |
| 
 | |
| class Ready(persisting_theory.Registry):
 | |
|     look_into = "persisting_theory"
 | |
| 
 | |
| 
 | |
| startup = Startup()
 | |
| ready = Ready()
 | |
| 
 | |
| _plugins = {}
 | |
| _filters = {}
 | |
| _hooks = {}
 | |
| 
 | |
| 
 | |
| class PluginCache:
 | |
|     def __init__(self, prefix):
 | |
|         self.prefix = prefix
 | |
| 
 | |
|     def get(self, key, default=None):
 | |
|         key = ":".join([self.prefix, key])
 | |
|         return cache.get(key, default)
 | |
| 
 | |
|     def set(self, key, value, duration=None):
 | |
|         key = ":".join([self.prefix, key])
 | |
|         return cache.set(key, value, duration)
 | |
| 
 | |
| 
 | |
| def get_plugin_config(
 | |
|     name,
 | |
|     user=False,
 | |
|     source=False,
 | |
|     registry=_plugins,
 | |
|     conf={},
 | |
|     settings={},
 | |
|     description=None,
 | |
|     version=None,
 | |
|     label=None,
 | |
|     homepage=None,
 | |
| ):
 | |
|     conf = {
 | |
|         "name": name,
 | |
|         "label": label or name,
 | |
|         "logger": logger,
 | |
|         # conf is for dynamic settings
 | |
|         "conf": conf,
 | |
|         # settings is for settings hardcoded in .env
 | |
|         "settings": settings,
 | |
|         "user": True if source else user,
 | |
|         # source plugins are plugins that provide audio content
 | |
|         "source": source,
 | |
|         "description": description,
 | |
|         "version": version,
 | |
|         "cache": PluginCache(name),
 | |
|         "homepage": homepage,
 | |
|     }
 | |
|     registry[name] = conf
 | |
|     return conf
 | |
| 
 | |
| 
 | |
| def load_settings(name, settings):
 | |
|     from django.conf import settings as django_settings
 | |
| 
 | |
|     mapping = {
 | |
|         "boolean": django_settings.ENV.bool,
 | |
|         "text": django_settings.ENV,
 | |
|     }
 | |
|     values = {}
 | |
|     prefix = f"FUNKWHALE_PLUGIN_{name.upper()}"
 | |
|     for s in settings:
 | |
|         key = "_".join([prefix, s["name"].upper()])
 | |
|         value = mapping[s["type"]](key, default=s.get("default", None))
 | |
|         values[s["name"]] = value
 | |
| 
 | |
|     logger.debug("Plugin %s running with settings %s", name, values)
 | |
|     return values
 | |
| 
 | |
| 
 | |
| def get_session():
 | |
|     from funkwhale_api.common import session
 | |
| 
 | |
|     return session.get_session()
 | |
| 
 | |
| 
 | |
| def register_filter(name, plugin_config, registry=_filters):
 | |
|     def decorator(func):
 | |
|         handlers = registry.setdefault(name, [])
 | |
| 
 | |
|         def inner(*args, **kwargs):
 | |
|             plugin_config["logger"].debug("Calling filter for %s", name)
 | |
|             rval = func(*args, **kwargs)
 | |
|             return rval
 | |
| 
 | |
|         handlers.append((plugin_config["name"], inner))
 | |
|         return inner
 | |
| 
 | |
|     return decorator
 | |
| 
 | |
| 
 | |
| def register_hook(name, plugin_config, registry=_hooks):
 | |
|     def decorator(func):
 | |
|         handlers = registry.setdefault(name, [])
 | |
| 
 | |
|         def inner(*args, **kwargs):
 | |
|             plugin_config["logger"].debug("Calling hook for %s", name)
 | |
|             func(*args, **kwargs)
 | |
| 
 | |
|         handlers.append((plugin_config["name"], inner))
 | |
|         return inner
 | |
| 
 | |
|     return decorator
 | |
| 
 | |
| 
 | |
| class Skip(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| def trigger_filter(name, value, enabled=False, **kwargs):
 | |
|     """
 | |
|     Call filters registered for "name" with the given
 | |
|     args and kwargs.
 | |
| 
 | |
|     Return the value (that could be modified by handlers)
 | |
|     """
 | |
|     logger.debug("Calling handlers for filter %s", name)
 | |
|     registry = kwargs.pop("registry", _filters)
 | |
|     confs = kwargs.pop("confs", {})
 | |
|     for plugin_name, handler in registry.get(name, []):
 | |
|         if not enabled and confs.get(plugin_name, {}).get("enabled") is False:
 | |
|             continue
 | |
|         try:
 | |
|             value = handler(value, conf=confs.get(plugin_name, {}), **kwargs)
 | |
|         except Skip:
 | |
|             pass
 | |
|         except Exception as e:
 | |
|             logger.warn("Plugin %s errored during filter %s: %s", plugin_name, name, e)
 | |
|     return value
 | |
| 
 | |
| 
 | |
| def trigger_hook(name, enabled=False, **kwargs):
 | |
|     """
 | |
|     Call hooks registered for "name" with the given
 | |
|     args and kwargs.
 | |
| 
 | |
|     Returns nothing
 | |
|     """
 | |
|     logger.debug("Calling handlers for hook %s", name)
 | |
|     registry = kwargs.pop("registry", _hooks)
 | |
|     confs = kwargs.pop("confs", {})
 | |
|     for plugin_name, handler in registry.get(name, []):
 | |
|         if not enabled and confs.get(plugin_name, {}).get("enabled") is False:
 | |
|             continue
 | |
|         try:
 | |
|             handler(conf=confs.get(plugin_name, {}).get("conf"), **kwargs)
 | |
|         except Skip:
 | |
|             pass
 | |
|         except Exception as e:
 | |
|             logger.warn("Plugin %s errored during hook %s: %s", plugin_name, name, e)
 | |
| 
 | |
| 
 | |
| def set_conf(name, conf, user=None, registry=_plugins):
 | |
|     from funkwhale_api.common import models
 | |
| 
 | |
|     if not registry[name]["conf"] and not registry[name]["source"]:
 | |
|         return
 | |
|     conf_serializer = get_serializer_from_conf_template(
 | |
|         registry[name]["conf"],
 | |
|         user=user,
 | |
|         source=registry[name]["source"],
 | |
|     )(data=conf)
 | |
|     conf_serializer.is_valid(raise_exception=True)
 | |
|     if "library" in conf_serializer.validated_data:
 | |
|         conf_serializer.validated_data["library"] = str(
 | |
|             conf_serializer.validated_data["library"]
 | |
|         )
 | |
|     conf, _ = models.PluginConfiguration.objects.update_or_create(
 | |
|         user=user, code=name, defaults={"conf": conf_serializer.validated_data}
 | |
|     )
 | |
| 
 | |
| 
 | |
| def get_confs(user=None):
 | |
|     from funkwhale_api.common import models
 | |
| 
 | |
|     qs = models.PluginConfiguration.objects.filter(code__in=list(_plugins.keys()))
 | |
|     if user:
 | |
|         qs = qs.filter(Q(user=None) | Q(user=user))
 | |
|     else:
 | |
|         qs = qs.filter(user=None)
 | |
|     confs = {
 | |
|         v["code"]: {"conf": v["conf"], "enabled": v["enabled"]}
 | |
|         for v in qs.values("code", "conf", "enabled")
 | |
|     }
 | |
|     for p, v in _plugins.items():
 | |
|         if p not in confs:
 | |
|             confs[p] = {"conf": None, "enabled": False}
 | |
|     return confs
 | |
| 
 | |
| 
 | |
| def get_conf(plugin, user=None):
 | |
|     return get_confs(user=user)[plugin]
 | |
| 
 | |
| 
 | |
| def enable_conf(code, value, user):
 | |
|     from funkwhale_api.common import models
 | |
| 
 | |
|     models.PluginConfiguration.objects.update_or_create(
 | |
|         code=code, user=user, defaults={"enabled": value}
 | |
|     )
 | |
| 
 | |
| 
 | |
| class LibraryField(serializers.UUIDField):
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         self.actor = kwargs.pop("actor")
 | |
|         super().__init__(*args, **kwargs)
 | |
| 
 | |
|     def to_internal_value(self, v):
 | |
|         v = super().to_internal_value(v)
 | |
|         if not self.actor.libraries.filter(uuid=v).first():
 | |
|             raise serializers.ValidationError("Invalid library id")
 | |
|         return v
 | |
| 
 | |
| 
 | |
| def get_serializer_from_conf_template(conf, source=False, user=None):
 | |
|     conf = copy.deepcopy(conf)
 | |
|     validators = {f["name"]: f.pop("validator") for f in conf if "validator" in f}
 | |
|     mapping = {
 | |
|         "url": serializers.URLField,
 | |
|         "boolean": serializers.BooleanField,
 | |
|         "text": serializers.CharField,
 | |
|         "long_text": serializers.CharField,
 | |
|         "password": serializers.CharField,
 | |
|         "number": serializers.IntegerField,
 | |
|     }
 | |
| 
 | |
|     for attr in ["label", "help"]:
 | |
|         for c in conf:
 | |
|             c.pop(attr, None)
 | |
| 
 | |
|     class Serializer(serializers.Serializer):
 | |
|         def __init__(self, *args, **kwargs):
 | |
|             super().__init__(*args, **kwargs)
 | |
|             for field_conf in conf:
 | |
|                 field_kwargs = copy.copy(field_conf)
 | |
|                 name = field_kwargs.pop("name")
 | |
|                 self.fields[name] = mapping[field_kwargs.pop("type")](**field_kwargs)
 | |
|             if source:
 | |
|                 self.fields["library"] = LibraryField(actor=user.actor)
 | |
| 
 | |
|     for vname, v in validators.items():
 | |
|         setattr(Serializer, f"validate_{vname}", v)
 | |
|     return Serializer
 | |
| 
 | |
| 
 | |
| def serialize_plugin(plugin_conf, confs):
 | |
|     return {
 | |
|         "name": plugin_conf["name"],
 | |
|         "label": plugin_conf["label"],
 | |
|         "description": plugin_conf.get("description") or None,
 | |
|         "user": plugin_conf.get("user", False),
 | |
|         "source": plugin_conf.get("source", False),
 | |
|         "conf": plugin_conf.get("conf", None),
 | |
|         "values": confs.get(plugin_conf["name"], {"conf"}).get("conf"),
 | |
|         "enabled": plugin_conf["name"] in confs
 | |
|         and confs[plugin_conf["name"]]["enabled"],
 | |
|         "homepage": plugin_conf["homepage"],
 | |
|     }
 | |
| 
 | |
| 
 | |
| def install_dependencies(deps):
 | |
|     if not deps:
 | |
|         return
 | |
|     logger.info("Installing plugins dependencies %s", deps)
 | |
|     pip_path = os.path.join(os.path.dirname(sys.executable), "pip")
 | |
|     subprocess.check_call([pip_path, "install"] + deps)
 | |
| 
 | |
| 
 | |
| def background_task(name):
 | |
|     from funkwhale_api.taskapp import celery
 | |
| 
 | |
|     def decorator(func):
 | |
|         return celery.app.task(func, name=name)
 | |
| 
 | |
|     return decorator
 | |
| 
 | |
| 
 | |
| # HOOKS
 | |
| LISTENING_CREATED = "listening_created"
 | |
| """
 | |
| Called when a track is being listened
 | |
| """
 | |
| LISTENING_SYNC = "listening_sync"
 | |
| """
 | |
| Called by the task manager to trigger listening sync
 | |
| """
 | |
| FAVORITE_CREATED = "favorite_created"
 | |
| """
 | |
| Called when a track is being favorited
 | |
| """
 | |
| FAVORITE_DELETED = "favorite_deleted"
 | |
| """
 | |
| Called when a favorited track is being unfavorited
 | |
| """
 | |
| FAVORITE_SYNC = "favorite_sync"
 | |
| """
 | |
| Called by the task manager to trigger favorite sync
 | |
| """
 | |
| 
 | |
| SCAN = "scan"
 | |
| """
 | |
| 
 | |
| """
 | |
| # FILTERS
 | |
| PLUGINS_DEPENDENCIES = "plugins_dependencies"
 | |
| """
 | |
| Called with an empty list, use this filter to append pip dependencies
 | |
| to the list for installation.
 | |
| """
 | |
| PLUGINS_APPS = "plugins_apps"
 | |
| """
 | |
| Called with an empty list, use this filter to append apps to INSTALLED_APPS
 | |
| """
 | |
| MIDDLEWARES_BEFORE = "middlewares_before"
 | |
| """
 | |
| Called with an empty list, use this filter to prepend middlewares
 | |
| to MIDDLEWARE
 | |
| """
 | |
| MIDDLEWARES_AFTER = "middlewares_after"
 | |
| """
 | |
| Called with an empty list, use this filter to append middlewares
 | |
| to MIDDLEWARE
 | |
| """
 | |
| URLS = "urls"
 | |
| """
 | |
| Called with an empty list, use this filter to register new urls and views
 | |
| """
 |