Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Show changes
Showing
with 2026 additions and 254 deletions
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
TRIGGER_THIRD_PARTY_UPLOAD = "third_party_upload"
"""
Called when a track is being listened
"""
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
"""
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from django.core.asgi import get_asgi_application
from django.urls import re_path
from funkwhale_api.common.auth import TokenAuthMiddleware
from funkwhale_api.instance import consumers
application = ProtocolTypeRouter(
{
# Empty for now (http->django views is added by default)
"websocket": TokenAuthMiddleware(
URLRouter([url("^api/v1/activity$", consumers.InstanceActivityConsumer)])
)
"websocket": AuthMiddlewareStack(
URLRouter(
[
re_path(
"^api/v1/activity$",
consumers.InstanceActivityConsumer.as_asgi(),
)
]
)
),
"http": get_asgi_application(),
}
)
from drf_spectacular.contrib.django_oauth_toolkit import OpenApiAuthenticationExtension
from drf_spectacular.plumbing import build_bearer_security_scheme_object
class CustomOAuthExt(OpenApiAuthenticationExtension):
target_class = "funkwhale_api.common.authentication.OAuth2Authentication"
name = "oauth2"
def get_security_definition(self, auto_schema):
from drf_spectacular.settings import spectacular_settings
from oauth2_provider.scopes import get_scopes_backend
flows = {}
for flow_type in spectacular_settings.OAUTH2_FLOWS:
flows[flow_type] = {}
if flow_type in ("implicit", "authorizationCode"):
flows[flow_type][
"authorizationUrl"
] = spectacular_settings.OAUTH2_AUTHORIZATION_URL
if flow_type in ("password", "clientCredentials", "authorizationCode"):
flows[flow_type]["tokenUrl"] = spectacular_settings.OAUTH2_TOKEN_URL
if spectacular_settings.OAUTH2_REFRESH_URL:
flows[flow_type]["refreshUrl"] = spectacular_settings.OAUTH2_REFRESH_URL
scope_backend = get_scopes_backend()
flows[flow_type]["scopes"] = scope_backend.get_all_scopes()
return {"type": "oauth2", "flows": flows}
class CustomApplicationTokenExt(OpenApiAuthenticationExtension):
target_class = "funkwhale_api.common.authentication.ApplicationTokenAuthentication"
name = "ApplicationToken"
def get_security_definition(self, auto_schema):
return build_bearer_security_scheme_object(
header_name="Authorization",
token_prefix="Bearer",
)
def custom_preprocessing_hook(endpoints):
filtered = []
# your modifications to the list of operations that are exposed in the schema
for path, path_regex, method, callback in endpoints:
if path.startswith("/api/v1/providers"):
continue
if path.startswith("/api/v1/users/users"):
continue
if path.startswith("/api/v1/oauth/authorize"):
continue
if path.startswith("/api/v1") or path.startswith("/api/v2"):
filtered.append((path, path_regex, method, callback))
return filtered
# -*- coding: utf-8 -*-
This diff is collapsed.
This diff is collapsed.
# -*- coding: utf-8 -*-
"""
Production Configurations
- Use djangosecure
- Use Amazon's S3 for storing static files and uploaded media
- Use mailgun to send emails
- Use mailgun to send e-mails
- Use Redis on Heroku
"""
from __future__ import absolute_import, unicode_literals
from .common import * # noqa
......@@ -43,14 +41,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY")
# SESSION_COOKIE_HTTPONLY = True
# SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
# SITE CONFIGURATION
# ------------------------------------------------------------------------------
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# END SITE CONFIGURATION
# Static Assets
# ------------------------
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
......@@ -73,50 +63,4 @@ TEMPLATES[0]["OPTIONS"]["loaders"] = [
# ------------------------------------------------------------------------------
# Heroku URL does not pass the DB number, so we parse it in
# LOGGING CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
}
},
"handlers": {
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django.request": {
"handlers": ["mail_admins"],
"level": "ERROR",
"propagate": True,
},
"django.security.DisallowedHost": {
"level": "ERROR",
"handlers": ["console", "mail_admins"],
"propagate": True,
},
},
}
# Your production stuff: Below this line define 3rd party library settings
from django.conf.urls import include
from django.urls import re_path
from funkwhale_api.common import routers as common_routers
from . import api
router = common_routers.OptionalSlashRouter()
v2_patterns = router.urls
v2_patterns += [
re_path(
r"^instance/",
include(("funkwhale_api.instance.urls_v2", "instance"), namespace="instance"),
),
re_path(
r"^radios/",
include(("funkwhale_api.radios.urls_v2", "radios"), namespace="radios"),
),
]
v2_paths = {
pattern.pattern.regex.pattern
for pattern in v2_patterns
if hasattr(pattern.pattern, "regex")
}
filtered_v1_patterns = [
pattern
for pattern in api.v1_patterns
if pattern.pattern.regex.pattern not in v2_paths
]
v2_patterns += filtered_v1_patterns
urlpatterns = [re_path("", include((v2_patterns, "v2"), namespace="v2"))]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.