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
Select Git revision

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
Select Git revision
Show changes
Showing
with 858 additions and 377 deletions
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 -*-
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import datetime
import logging.config import logging.config
import os
import sys import sys
import warnings
from urllib.parse import urlsplit from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
import environ import environ
from celery.schedules import crontab from celery.schedules import crontab
from funkwhale_api import __version__
logger = logging.getLogger("funkwhale_api.config") logger = logging.getLogger("funkwhale_api.config")
ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /)
APPS_DIR = ROOT_DIR.path("funkwhale_api") APPS_DIR = ROOT_DIR.path("funkwhale_api")
env = environ.Env() env = environ.Env()
ENV = env
# If DEBUG is `true`, we automatically set the loglevel to "DEBUG"
# If DEBUG is `false`, we try to read the level from LOGLEVEL environment and default to "INFO"
LOGLEVEL = (
"DEBUG" if env.bool("DEBUG", False) else env("LOGLEVEL", default="info").upper()
)
"""
Default logging level for the Funkwhale processes.
.. note::
The `DEBUG` variable overrides the `LOGLEVEL` if it is set to `TRUE`.
The `LOGLEVEL` value only applies if `DEBUG` is `false` or not present.
Available levels:
- ``debug``
- ``info``
- ``warning``
- ``error``
- ``critical``
LOGLEVEL = env("LOGLEVEL", default="info").upper()
""" """
Default logging level for the Funkwhale processes""" # pylint: disable=W0105
IS_DOCKER_SETUP = env.bool("IS_DOCKER_SETUP", False)
if env("FUNKWHALE_SENTRY_DSN", default=None) is not None:
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from funkwhale_api import __version__ as version
sentry_sdk.init(
dsn=env("FUNKWHALE_SENTRY_DSN"),
integrations=[DjangoIntegration(), CeleryIntegration()],
traces_sample_rate=env("FUNKWHALE_SENTRY_SR", default=0.25),
send_default_pii=False,
environment="api",
debug=env.bool("DEBUG", False),
release=version,
)
sentry_sdk.set_tag("instance", env("FUNKWHALE_HOSTNAME"))
LOGGING_CONFIG = None LOGGING_CONFIG = None
logging.config.dictConfig( logging.config.dictConfig(
...@@ -33,11 +67,6 @@ logging.config.dictConfig( ...@@ -33,11 +67,6 @@ logging.config.dictConfig(
}, },
"handlers": { "handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "console"}, "console": {"class": "logging.StreamHandler", "formatter": "console"},
# # Add Handler for Sentry for `warning` and above
# 'sentry': {
# 'level': 'WARNING',
# 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
# },
}, },
"loggers": { "loggers": {
"funkwhale_api": { "funkwhale_api": {
...@@ -46,6 +75,12 @@ logging.config.dictConfig( ...@@ -46,6 +75,12 @@ logging.config.dictConfig(
# required to avoid double logging with root logger # required to avoid double logging with root logger
"propagate": False, "propagate": False,
}, },
"plugins": {
"level": LOGLEVEL,
"handlers": ["console"],
# required to avoid double logging with root logger
"propagate": False,
},
"": {"level": "WARNING", "handlers": ["console"]}, "": {"level": "WARNING", "handlers": ["console"]},
}, },
} }
...@@ -57,7 +92,7 @@ Path to a .env file to load ...@@ -57,7 +92,7 @@ Path to a .env file to load
""" """
if env_file: if env_file:
logger.info("Loading specified env file at %s", env_file) logger.info("Loading specified env file at %s", env_file)
# we have an explicitely specified env file # we have an explicitly specified env file
# so we try to load and it fail loudly if it does not exist # so we try to load and it fail loudly if it does not exist
env.read_env(env_file) env.read_env(env_file)
else: else:
...@@ -79,14 +114,42 @@ else: ...@@ -79,14 +114,42 @@ else:
logger.info("Loaded env file at %s/.env", path) logger.info("Loaded env file at %s/.env", path)
break break
FUNKWHALE_PLUGINS = env("FUNKWHALE_PLUGINS", default="")
FUNKWHALE_PLUGINS_PATH = env( FUNKWHALE_PLUGINS_PATH = env(
"FUNKWHALE_PLUGINS_PATH", default="/srv/funkwhale/plugins/" "FUNKWHALE_PLUGINS_PATH", default="/srv/funkwhale/plugins/"
) )
""" """
Path to a directory containing Funkwhale plugins. These will be imported at runtime. Path to a directory containing Funkwhale plugins.
These are imported at runtime.
""" """
sys.path.append(FUNKWHALE_PLUGINS_PATH) sys.path.append(FUNKWHALE_PLUGINS_PATH)
CORE_PLUGINS = [
"funkwhale_api.contrib.scrobbler",
"funkwhale_api.contrib.listenbrainz",
"funkwhale_api.contrib.maloja",
]
LOAD_CORE_PLUGINS = env.bool("FUNKWHALE_LOAD_CORE_PLUGINS", default=True)
PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
"""
List of Funkwhale plugins to load.
"""
if LOAD_CORE_PLUGINS:
PLUGINS = CORE_PLUGINS + PLUGINS
# Remove duplicates
PLUGINS = list(OrderedDict.fromkeys(PLUGINS))
if PLUGINS:
logger.info("Running with the following plugins enabled: %s", ", ".join(PLUGINS))
else:
logger.info("Running with no plugins")
from .. import plugins # noqa
plugins.startup.autodiscover([p + ".funkwhale_startup" for p in PLUGINS])
DEPENDENCIES = plugins.trigger_filter(plugins.PLUGINS_DEPENDENCIES, [], enabled=True)
plugins.install_dependencies(DEPENDENCIES)
FUNKWHALE_HOSTNAME = None FUNKWHALE_HOSTNAME = None
FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None) FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None) FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
...@@ -100,12 +163,13 @@ else: ...@@ -100,12 +163,13 @@ else:
try: try:
FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME") FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
""" """
Hostname of your Funkwhale pod, e.g ``mypod.audio`` Hostname of your Funkwhale pod, e.g. ``mypod.audio``.
""" """
FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https") FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
""" """
Protocol end users will use to access your pod, either ``http`` or ``https``. Protocol end users will use to access your pod, either
``http`` or ``https``.
""" """
except Exception: except Exception:
FUNKWHALE_URL = env("FUNKWHALE_URL") FUNKWHALE_URL = env("FUNKWHALE_URL")
...@@ -115,26 +179,25 @@ else: ...@@ -115,26 +179,25 @@ else:
FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower() FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower()
FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower() FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower()
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME) FUNKWHALE_URL = f"{FUNKWHALE_PROTOCOL}://{FUNKWHALE_HOSTNAME}"
FUNKWHALE_SPA_HTML_ROOT = env( FUNKWHALE_SPA_HTML_ROOT = env("FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL)
"FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL + "/front/"
)
""" """
URL or path to the Web Application files. Funkwhale needs access to it so that URL or path to the Web Application files.
it can inject <meta> tags relevant to the given page (e.g page title, cover, etc.). Funkwhale needs access to it so that it can inject <meta> tags relevant
to the given page (e.g page title, cover, etc.).
If a URL is specified, the index.html file will be fetched through HTTP. If a path is provided, If a URL is specified, the index.html file will be fetched through HTTP.
If a path is provided,
it will be accessed from disk. it will be accessed from disk.
Use something like ``/srv/funkwhale/front/dist/`` if the web processes shows request errors related to this. Use something like ``/srv/funkwhale/front/dist/`` if the web processes shows
request errors related to this.
""" """
FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int( FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int(
"FUNKWHALE_SPA_HTML_CACHE_DURATION", default=60 * 15 "FUNKWHALE_SPA_HTML_CACHE_DURATION", default=60 * 15
) )
FUNKWHALE_EMBED_URL = env( FUNKWHALE_EMBED_URL = env("FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/embed.html")
"FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/front/embed.html"
)
FUNKWHALE_SPA_REWRITE_MANIFEST = env.bool( FUNKWHALE_SPA_REWRITE_MANIFEST = env.bool(
"FUNKWHALE_SPA_REWRITE_MANIFEST", default=True "FUNKWHALE_SPA_REWRITE_MANIFEST", default=True
) )
...@@ -144,29 +207,15 @@ FUNKWHALE_SPA_REWRITE_MANIFEST_URL = env.bool( ...@@ -144,29 +207,15 @@ FUNKWHALE_SPA_REWRITE_MANIFEST_URL = env.bool(
APP_NAME = "Funkwhale" APP_NAME = "Funkwhale"
# XXX: for backward compat with django 2.2, remove this when django 2.2 support is dropped
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = env.bool(
"DJANGO_ALLOW_ASYNC_UNSAFE", default="true"
)
# XXX: deprecated, see #186
FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True)
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower() FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower()
# XXX: deprecated, see #186
FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50)
# XXX: deprecated, see #186
FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
"FEDERATION_MUSIC_NEEDS_APPROVAL", default=True
)
# XXX: deprecated, see #186
FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12)
FEDERATION_SERVICE_ACTOR_USERNAME = env( FEDERATION_SERVICE_ACTOR_USERNAME = env(
"FEDERATION_SERVICE_ACTOR_USERNAME", default="service" "FEDERATION_SERVICE_ACTOR_USERNAME", default="service"
) )
# How many pages to fetch when crawling outboxes and third-party collections # How many pages to fetch when crawling outboxes and third-party collections
FEDERATION_COLLECTION_MAX_PAGES = env.int("FEDERATION_COLLECTION_MAX_PAGES", default=5) FEDERATION_COLLECTION_MAX_PAGES = env.int("FEDERATION_COLLECTION_MAX_PAGES", default=5)
""" """
Number of existing pages of content to fetch when discovering/refreshing an actor or channel. Number of existing pages of content to fetch when discovering/refreshing an
actor or channel.
More pages means more content will be loaded, but will require more resources. More pages means more content will be loaded, but will require more resources.
""" """
...@@ -176,10 +225,21 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[]) + [FUNKWHALE_HOSTNA ...@@ -176,10 +225,21 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[]) + [FUNKWHALE_HOSTNA
List of allowed hostnames for which the Funkwhale server will answer. List of allowed hostnames for which the Funkwhale server will answer.
""" """
CSRF_TRUSTED_ORIGINS = [
urlparse("//" + o, FUNKWHALE_PROTOCOL).geturl() for o in ALLOWED_HOSTS
]
"""
List of origins that are trusted for unsafe requests
We simply consider all allowed hosts to be trusted origins
See DJANGO_ALLOWED_HOSTS in .env.example for details
See https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
"""
# APP CONFIGURATION # APP CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_APPS = ( DJANGO_APPS = (
"channels", "channels",
"daphne",
# Default Django apps: # Default Django apps:
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
...@@ -202,8 +262,8 @@ THIRD_PARTY_APPS = ( ...@@ -202,8 +262,8 @@ THIRD_PARTY_APPS = (
"oauth2_provider", "oauth2_provider",
"rest_framework", "rest_framework",
"rest_framework.authtoken", "rest_framework.authtoken",
"rest_auth", "dj_rest_auth",
"rest_auth.registration", "dj_rest_auth.registration",
"dynamic_preferences", "dynamic_preferences",
"django_filters", "django_filters",
"django_cleanup", "django_cleanup",
...@@ -211,19 +271,6 @@ THIRD_PARTY_APPS = ( ...@@ -211,19 +271,6 @@ THIRD_PARTY_APPS = (
) )
# Sentry
RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False)
RAVEN_DSN = env("RAVEN_DSN", default="")
if RAVEN_ENABLED:
RAVEN_CONFIG = {
"dsn": RAVEN_DSN,
# If you are using git, you can also automatically configure the
# release based on the git info.
"release": __version__,
}
THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",)
# Apps specific for this project go here. # Apps specific for this project go here.
LOCAL_APPS = ( LOCAL_APPS = (
"funkwhale_api.common.apps.CommonConfig", "funkwhale_api.common.apps.CommonConfig",
...@@ -233,6 +280,7 @@ LOCAL_APPS = ( ...@@ -233,6 +280,7 @@ LOCAL_APPS = (
# Your stuff: custom apps go here # Your stuff: custom apps go here
"funkwhale_api.instance", "funkwhale_api.instance",
"funkwhale_api.audio", "funkwhale_api.audio",
"funkwhale_api.contrib.listenbrainz",
"funkwhale_api.music", "funkwhale_api.music",
"funkwhale_api.requests", "funkwhale_api.requests",
"funkwhale_api.favorites", "funkwhale_api.favorites",
...@@ -243,60 +291,61 @@ LOCAL_APPS = ( ...@@ -243,60 +291,61 @@ LOCAL_APPS = (
"funkwhale_api.playlists", "funkwhale_api.playlists",
"funkwhale_api.subsonic", "funkwhale_api.subsonic",
"funkwhale_api.tags", "funkwhale_api.tags",
"funkwhale_api.typesense",
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
"""
List of Funkwhale plugins to load.
"""
if PLUGINS:
logger.info("Running with the following plugins enabled: %s", ", ".join(PLUGINS))
else:
logger.info("Running with no plugins")
ADDITIONAL_APPS = env.list("ADDITIONAL_APPS", default=[]) ADDITIONAL_APPS = env.list("ADDITIONAL_APPS", default=[])
""" """
List of Django apps to load in addition to Funkwhale plugins and apps. List of Django apps to load in addition to Funkwhale plugins and apps.
""" """
INSTALLED_APPS = ( INSTALLED_APPS = (
DJANGO_APPS LOCAL_APPS
+ DJANGO_APPS
+ THIRD_PARTY_APPS + THIRD_PARTY_APPS
+ LOCAL_APPS
+ tuple(["{}.apps.Plugin".format(p) for p in PLUGINS])
+ tuple(ADDITIONAL_APPS) + tuple(ADDITIONAL_APPS)
+ tuple(plugins.trigger_filter(plugins.PLUGINS_APPS, [], enabled=True))
) )
# MIDDLEWARE CONFIGURATION # MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ADDITIONAL_MIDDLEWARES_BEFORE = env.list("ADDITIONAL_MIDDLEWARES_BEFORE", default=[]) ADDITIONAL_MIDDLEWARES_BEFORE = env.list("ADDITIONAL_MIDDLEWARES_BEFORE", default=[])
MIDDLEWARE = tuple(ADDITIONAL_MIDDLEWARES_BEFORE) + ( MIDDLEWARE = (
tuple(plugins.trigger_filter(plugins.MIDDLEWARES_BEFORE, [], enabled=True))
+ tuple(ADDITIONAL_MIDDLEWARES_BEFORE)
+ (
"allauth.account.middleware.AccountMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware", "corsheaders.middleware.CorsMiddleware",
"funkwhale_api.common.middleware.SPAFallbackMiddleware", # needs to be before SPA middleware
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
# /end
"funkwhale_api.common.middleware.SPAFallbackMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"funkwhale_api.users.middleware.RecordActivityMiddleware", "funkwhale_api.users.middleware.RecordActivityMiddleware",
"funkwhale_api.common.middleware.ThrottleStatusMiddleware", "funkwhale_api.common.middleware.ThrottleStatusMiddleware",
) )
+ tuple(plugins.trigger_filter(plugins.MIDDLEWARES_AFTER, [], enabled=True))
)
# DEBUG # DEBUG
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DJANGO_DEBUG = DEBUG = env.bool("DJANGO_DEBUG", False) DJANGO_DEBUG = DEBUG = env.bool("DJANGO_DEBUG", False)
""" """
Whether to enable debugging info and pages. Never enable this on a production server, Whether to enable debugging info and pages.
as it can leak very sensitive information. Never enable this on a production server, as it can leak very sensitive
information.
""" """
# FIXTURE CONFIGURATION # FIXTURE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# EMAIL CONFIGURATION # EMAIL CONFIGURATION
...@@ -305,17 +354,16 @@ FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) ...@@ -305,17 +354,16 @@ FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DEFAULT_FROM_EMAIL = env( DEFAULT_FROM_EMAIL = env(
"DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME) "DEFAULT_FROM_EMAIL", default=f"Funkwhale <noreply@{FUNKWHALE_HOSTNAME}>"
) )
""" """
Name and email address used to send system emails. The name and email address used to send system emails.
Defaults to ``Funkwhale <noreply@yourdomain>``.
Default: ``Funkwhale <noreply@yourdomain>``
.. note:: Available formats:
Both the forms ``Funkwhale <noreply@yourdomain>`` and - ``Name <email address>``
``noreply@yourdomain`` work. - ``<Email address>``
""" """
EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ") EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
...@@ -334,17 +382,9 @@ SMTP configuration for sending emails. Possible values: ...@@ -334,17 +382,9 @@ SMTP configuration for sending emails. Possible values:
On a production instance, you'll usually want to use an external SMTP server: On a production instance, you'll usually want to use an external SMTP server:
- ``EMAIL_CONFIG=smtp://user@:password@youremail.host:25`` - ``EMAIL_CONFIG=smtp://user:password@youremail.host:25``
- ``EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465`` - ``EMAIL_CONFIG=smtp+ssl://user:password@youremail.host:465``
- ``EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587`` - ``EMAIL_CONFIG=smtp+tls://user:password@youremail.host:587``
.. note::
If ``user`` or ``password`` contain special characters (eg.
``noreply@youremail.host`` as ``user``), be sure to urlencode them, using
for example the command:
``python3 -c 'import urllib.parse; print(urllib.parse.quote_plus("noreply@youremail.host"))'``
(returns ``noreply%40youremail.host``)
""" """
vars().update(EMAIL_CONFIG) vars().update(EMAIL_CONFIG)
...@@ -352,14 +392,73 @@ vars().update(EMAIL_CONFIG) ...@@ -352,14 +392,73 @@ vars().update(EMAIL_CONFIG)
# DATABASE CONFIGURATION # DATABASE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASE_URL = env.db("DATABASE_URL")
# The `_database_url_docker` variable will only by used as default for DATABASE_URL
# in the context of a docker deployment.
_database_url_docker = None
if IS_DOCKER_SETUP and env.str("DATABASE_URL", None) is None:
warnings.warn(
DeprecationWarning(
"the automatically generated 'DATABASE_URL' configuration in the docker "
"setup is deprecated, please configure either the 'DATABASE_URL' "
"environment variable or the 'DATABASE_HOST', 'DATABASE_USER' and "
"'DATABASE_PASSWORD' environment variables instead"
)
)
_DOCKER_DATABASE_HOST = "postgres"
_DOCKER_DATABASE_PORT = 5432
_DOCKER_DATABASE_USER = env.str("POSTGRES_ENV_POSTGRES_USER", "postgres")
_DOCKER_DATABASE_PASSWORD = env.str("POSTGRES_ENV_POSTGRES_PASSWORD", "")
_DOCKER_DATABASE_NAME = _DOCKER_DATABASE_USER
_database_url_docker = (
f"postgres:"
f"//{_DOCKER_DATABASE_USER}:{_DOCKER_DATABASE_PASSWORD}"
f"@{_DOCKER_DATABASE_HOST}:{_DOCKER_DATABASE_PORT}"
f"/{_DOCKER_DATABASE_NAME}"
)
DATABASE_HOST = env.str("DATABASE_HOST", "localhost")
"""
The hostname of the PostgreSQL server. Defaults to ``localhost``.
"""
DATABASE_PORT = env.int("DATABASE_PORT", 5432)
"""
The port of the PostgreSQL server. Defaults to ``5432``.
"""
DATABASE_USER = env.str("DATABASE_USER", "funkwhale")
""" """
URL to connect to the PostgreSQL database. Examples: The name of the PostgreSQL user. Defaults to ``funkwhale``.
"""
DATABASE_PASSWORD = env.str("DATABASE_PASSWORD", "funkwhale")
"""
The password of the PostgreSQL user. Defaults to ``funkwhale``.
"""
DATABASE_NAME = env.str("DATABASE_NAME", "funkwhale")
"""
The name of the PostgreSQL database. Defaults to ``funkwhale``.
"""
DATABASE_URL = env.db(
"DATABASE_URL",
_database_url_docker # This is only set in the context of a docker deployment.
or (
f"postgres:"
f"//{DATABASE_USER}:{DATABASE_PASSWORD}"
f"@{DATABASE_HOST}:{DATABASE_PORT}"
f"/{DATABASE_NAME}"
),
)
"""
The URL used to connect to the PostgreSQL database. Defaults to an auto generated url
build using the `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`
and `DATABASE_NAME` variables.
Examples:
- ``postgresql://funkwhale@:5432/funkwhale`` - ``postgresql://funkwhale@:5432/funkwhale``
- ``postgresql://<user>:<password>@<host>:<port>/<database>`` - ``postgresql://<user>:<password>@<host>:<port>/<database>``
- ``postgresql://funkwhale:passw0rd@localhost:5432/funkwhale_database`` - ``postgresql://funkwhale:passw0rd@localhost:5432/funkwhale_database``
""" """
DATABASES = { DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
"default": DATABASE_URL "default": DATABASE_URL
...@@ -369,7 +468,7 @@ DB_CONN_MAX_AGE = DATABASES["default"]["CONN_MAX_AGE"] = env( ...@@ -369,7 +468,7 @@ DB_CONN_MAX_AGE = DATABASES["default"]["CONN_MAX_AGE"] = env(
"DB_CONN_MAX_AGE", default=60 * 5 "DB_CONN_MAX_AGE", default=60 * 5
) )
""" """
Max time, in seconds, before database connections are closed. The maximum time in seconds before database connections close.
""" """
MIGRATION_MODULES = { MIGRATION_MODULES = {
# see https://github.com/jazzband/django-oauth-toolkit/issues/634 # see https://github.com/jazzband/django-oauth-toolkit/issues/634
...@@ -379,6 +478,9 @@ MIGRATION_MODULES = { ...@@ -379,6 +478,9 @@ MIGRATION_MODULES = {
"sites": "funkwhale_api.contrib.sites.migrations", "sites": "funkwhale_api.contrib.sites.migrations",
} }
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# see https://docs.djangoproject.com/en/4.0/releases/3.2/
# GENERAL CONFIGURATION # GENERAL CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here: # Local time zone for this installation. Choices can be found here:
...@@ -407,20 +509,25 @@ USE_TZ = True ...@@ -407,20 +509,25 @@ USE_TZ = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#templates # See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [ TEMPLATES = [
{ {
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
"DIRS": [str(APPS_DIR.path("templates"))], "DIRS": [str(APPS_DIR.path("templates"))],
"OPTIONS": { "OPTIONS": {
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
"debug": DEBUG, "debug": DEBUG,
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
"loaders": [ "loaders": [
"django.template.loaders.filesystem.Loader", "django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader", "django.template.loaders.app_directories.Loader",
], ],
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
...@@ -436,7 +543,8 @@ TEMPLATES = [ ...@@ -436,7 +543,8 @@ TEMPLATES = [
} }
] ]
# See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs # See:
# http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = "bootstrap3" CRISPY_TEMPLATE_PACK = "bootstrap3"
# STATIC FILE CONFIGURATION # STATIC FILE CONFIGURATION
...@@ -444,24 +552,47 @@ CRISPY_TEMPLATE_PACK = "bootstrap3" ...@@ -444,24 +552,47 @@ CRISPY_TEMPLATE_PACK = "bootstrap3"
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles"))) STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
""" """
Path were static files should be collected. The path where static files are collected.
""" """
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = env("STATIC_URL", default=FUNKWHALE_URL + "/staticfiles/") STATIC_URL = env("STATIC_URL", default=FUNKWHALE_URL + "/staticfiles/")
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage" STORAGES = {
"default": {
"BACKEND": "funkwhale_api.common.storage.ASCIIFileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True) PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True)
""" """
Wether to proxy audio files through your reverse proxy. It's recommended to keep this on, Whether to proxy audio files through your reverse proxy.
as a way to enforce access control, however, if you're using S3 storage with :attr:`AWS_QUERYSTRING_AUTH`, We recommend you leave this enabled to enforce access control.
it's safe to disable it.
If you're using S3 storage with :attr:`AWS_QUERYSTRING_AUTH`
enabled, it's safe to disable this setting.
"""
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default=None)
"""
The default ACL to use when uploading files to an S3-compatible object storage
bucket.
ACLs and bucket policies are distinct concepts, and some storage
providers (ie Linode, Scaleway) will always apply the most restrictive between
a bucket's ACL and policy, meaning a default private ACL will supersede
a relaxed bucket policy.
If present, the value should be a valid canned ACL.
See `<https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl>`_
""" """
AWS_DEFAULT_ACL = None
AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA) AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA)
""" """
Whether to include signatures in S3 urls, as a way to enforce access-control. Whether to include signatures in S3 URLs. Signatures
are used to enforce access control.
Defaults to the inverse of :attr:`PROXY_MEDIA`. Defaults to the opposite of :attr:`PROXY_MEDIA`.
""" """
AWS_S3_MAX_MEMORY_SIZE = env.int( AWS_S3_MAX_MEMORY_SIZE = env.int(
...@@ -470,7 +601,8 @@ AWS_S3_MAX_MEMORY_SIZE = env.int( ...@@ -470,7 +601,8 @@ AWS_S3_MAX_MEMORY_SIZE = env.int(
AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600) AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600)
""" """
Expiration delay, in seconds, of signatures generated when :attr:`AWS_QUERYSTRING_AUTH` is enabled. The time in seconds before AWS signatures expire.
Only takes effect you enable :attr:`AWS_QUERYSTRING_AUTH`
""" """
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None) AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)
...@@ -486,23 +618,41 @@ if AWS_ACCESS_KEY_ID: ...@@ -486,23 +618,41 @@ if AWS_ACCESS_KEY_ID:
""" """
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
""" """
Bucket name of your S3 storage. Your S3 bucket name.
""" """
AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", default=None) AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", default=None)
""" """
Custom domain to use for your S3 storage. Custom domain for serving your S3 files.
Useful if your provider offers a CDN-like service for your bucket.
.. important::
The URL must not contain a scheme (:attr:`AWS_S3_URL_PROTOCOL` is
automatically prepended) nor a trailing slash.
"""
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", default="https:")
"""
Protocol to use when constructing the custom domain (see :attr:`AWS_S3_CUSTOM_DOMAIN`)
.. important::
It must end with a `:`, remove `//`.
""" """
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None) AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None)
""" """
If you use a S3-compatible storage such as minio, set the following variable to If you use a S3-compatible storage such as minio,
the full URL to the storage server. Example: set the following variable to the full URL to the storage server.
Examples:
- ``https://minio.mydomain.com`` - ``https://minio.mydomain.com``
- ``https://s3.wasabisys.com`` - ``https://s3.wasabisys.com``
""" """
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None) AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None)
"""If you are using Amazon S3 to serve media directly, you will need to specify your region """
name in order to access files. Example: If you're using Amazon S3 to serve media without a proxy,
you need to specify your region name to access files.
Example:
- ``eu-west-2`` - ``eu-west-2``
""" """
...@@ -510,16 +660,18 @@ if AWS_ACCESS_KEY_ID: ...@@ -510,16 +660,18 @@ if AWS_ACCESS_KEY_ID:
AWS_S3_SIGNATURE_VERSION = "s3v4" AWS_S3_SIGNATURE_VERSION = "s3v4"
AWS_LOCATION = env("AWS_LOCATION", default="") AWS_LOCATION = env("AWS_LOCATION", default="")
""" """
An optional bucket subdirectory were you want to store the files. This is especially useful A directory in your S3 bucket where you store files.
if you plan to use share the bucket with other services Use this if you plan to share the bucket between services.
""" """
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIS3Boto3Storage" STORAGES["default"]["BACKEND"] = "funkwhale_api.common.storage.ASCIIS3Boto3Storage"
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS # See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (str(APPS_DIR.path("static")),) STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders # See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
...@@ -530,15 +682,14 @@ STATICFILES_FINDERS = ( ...@@ -530,15 +682,14 @@ STATICFILES_FINDERS = (
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media"))) MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media")))
""" """
Where media files (such as album covers or audio tracks) should be stored The path where you store media files (such as album covers or audio tracks)
on your system? (Ensure this directory actually exists) on your system. Make sure this directory actually exists.
""" """
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = env("MEDIA_URL", default=FUNKWHALE_URL + "/media/") MEDIA_URL = env("MEDIA_URL", default=FUNKWHALE_URL + "/media/")
""" """
URL where media files are served. The default value should work fine on most The URL from which your pod serves media files. Change this if you're hosting media
configurations, but could can tweak this if you are hosting media files on a separate files on a separate domain, or if you host Funkwhale on a non-standard port.
domain, or if you host Funkwhale on a non-standard port.
""" """
FILE_UPLOAD_PERMISSIONS = 0o644 FILE_UPLOAD_PERMISSIONS = 0o644
...@@ -546,13 +697,14 @@ ATTACHMENTS_UNATTACHED_PRUNE_DELAY = env.int( ...@@ -546,13 +697,14 @@ ATTACHMENTS_UNATTACHED_PRUNE_DELAY = env.int(
"ATTACHMENTS_UNATTACHED_PRUNE_DELAY", default=3600 * 24 "ATTACHMENTS_UNATTACHED_PRUNE_DELAY", default=3600 * 24
) )
""" """
Delay in seconds before uploaded but unattached attachements are pruned from the system. The delay in seconds before Funkwhale prunes uploaded but detached attachments
from the system.
""" """
# URL Configuration # URL Configuration
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ROOT_URLCONF = "config.urls" ROOT_URLCONF = "config.urls"
SPA_URLCONF = "config.spa_urls" SPA_URLCONF = "config.urls.spa"
ASGI_APPLICATION = "config.routing.application" ASGI_APPLICATION = "config.routing.application"
# This ensures that Django will be able to detect a secure connection # This ensures that Django will be able to detect a secure connection
...@@ -567,6 +719,8 @@ AUTHENTICATION_BACKENDS = ( ...@@ -567,6 +719,8 @@ AUTHENTICATION_BACKENDS = (
"funkwhale_api.users.auth_backends.AllAuthBackend", "funkwhale_api.users.auth_backends.AllAuthBackend",
) )
SESSION_COOKIE_HTTPONLY = False SESSION_COOKIE_HTTPONLY = False
SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", default=3600 * 25 * 60)
# Some really nice defaults # Some really nice defaults
ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
...@@ -574,12 +728,12 @@ ACCOUNT_EMAIL_VERIFICATION_ENFORCE = env.bool( ...@@ -574,12 +728,12 @@ ACCOUNT_EMAIL_VERIFICATION_ENFORCE = env.bool(
"ACCOUNT_EMAIL_VERIFICATION_ENFORCE", default=False "ACCOUNT_EMAIL_VERIFICATION_ENFORCE", default=False
) )
""" """
Determine wether users need to verify their email address before using the service. Enabling this can be useful Set whether users need to verify their email address before using your pod. Enabling this setting
to reduce spam or bots accounts, however, you'll need to configure a mail server so that your users can receive the is useful for reducing spam and bot accounts. To use this setting you need to configure a mail server
verification emails, using :attr:`EMAIL_CONFIG`. to send verification emails. See :attr:`EMAIL_CONFIG`.
Note that regardless of the setting value, superusers created through the command line will never require verification.
.. note::
Superusers created through the command line never need to verify their email address.
""" """
ACCOUNT_EMAIL_VERIFICATION = ( ACCOUNT_EMAIL_VERIFICATION = (
"mandatory" if ACCOUNT_EMAIL_VERIFICATION_ENFORCE else "optional" "mandatory" if ACCOUNT_EMAIL_VERIFICATION_ENFORCE else "optional"
...@@ -601,13 +755,17 @@ OAUTH2_PROVIDER = { ...@@ -601,13 +755,17 @@ OAUTH2_PROVIDER = {
# we keep expired tokens for 15 days, for tracability # we keep expired tokens for 15 days, for tracability
"REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 15, "REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 15,
"AUTHORIZATION_CODE_EXPIRE_SECONDS": 5 * 60, "AUTHORIZATION_CODE_EXPIRE_SECONDS": 5 * 60,
"ACCESS_TOKEN_EXPIRE_SECONDS": 60 * 60 * 10, "ACCESS_TOKEN_EXPIRE_SECONDS": env.int(
"ACCESS_TOKEN_EXPIRE_SECONDS", default=60 * 60 * 10
),
"OAUTH2_SERVER_CLASS": "funkwhale_api.users.oauth.server.OAuth2Server", "OAUTH2_SERVER_CLASS": "funkwhale_api.users.oauth.server.OAuth2Server",
"PKCE_REQUIRED": False,
} }
OAUTH2_PROVIDER_APPLICATION_MODEL = "users.Application" OAUTH2_PROVIDER_APPLICATION_MODEL = "users.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken" OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken"
OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant" OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken" OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "users.IdToken"
SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3 SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3
...@@ -615,15 +773,17 @@ SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3 ...@@ -615,15 +773,17 @@ SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False) AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False)
""" """
Wether to enable LDAP authentication. See :doc:`/installation/ldap` for more information. Whether to enable LDAP authentication.
See :doc:`/administrator_documentation/configuration_docs/ldap` for more information.
""" """
if AUTH_LDAP_ENABLED: if AUTH_LDAP_ENABLED:
# Import the LDAP modules here.
# Import the LDAP modules here; this way, we don't need the dependency unless someone # This way, we don't need the dependency unless someone
# actually enables the LDAP support # actually enables the LDAP support
import ldap import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion, GroupOfNamesType from django_auth_ldap.config import GroupOfNamesType, LDAPSearch, LDAPSearchUnion
# Add LDAP to the authentication backends # Add LDAP to the authentication backends
AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",) AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",)
...@@ -686,29 +846,43 @@ if AUTH_LDAP_ENABLED: ...@@ -686,29 +846,43 @@ if AUTH_LDAP_ENABLED:
# SLUGLIFIER # SLUGLIFIER
AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify" AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
CACHE_DEFAULT = "redis://127.0.0.1:6379/0" CACHE_URL_DEFAULT = "redis://127.0.0.1:6379/0"
CACHE_URL = env.cache_url("CACHE_URL", default=CACHE_DEFAULT) if IS_DOCKER_SETUP:
CACHE_URL_DEFAULT = "redis://redis:6379/0"
CACHE_URL = env.str("CACHE_URL", default=CACHE_URL_DEFAULT)
""" """
URL to your redis server. Examples: The URL of your redis server. For example:
- ``redis://<host>:<port>/<database>``
- ``redis://127.0.0.1:6379/0``
- ``redis://:password@localhost:6379/0``
- `redis://<host>:<port>/<database>` If you're using password auth (the extra slash is important)
- `redis://127.0.0.1:6379/0` - ``redis:///run/redis/redis.sock?db=0`` over unix sockets
- `redis://:password@localhost:6379/0` for password auth (the extra semicolon is important)
- `redis:///run/redis/redis.sock?db=0` over unix sockets
.. note:: .. note::
If you want to use Redis over unix sockets, you'll also need to update :attr:`CELERY_BROKER_URL` If you want to use Redis over unix sockets, you also need to update
:attr:`CELERY_BROKER_URL`, because the scheme differs from the one used by
:attr:`CACHE_URL`.
""" """
CACHES = { CACHES = {
"default": CACHE_URL, "default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": CACHE_URL,
"OPTIONS": {
"CLIENT_CLASS": "funkwhale_api.common.cache.RedisClient",
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
},
},
"local": { "local": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "local-cache", "LOCATION": "local-cache",
}, },
} }
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
...@@ -717,17 +891,12 @@ CHANNEL_LAYERS = { ...@@ -717,17 +891,12 @@ CHANNEL_LAYERS = {
} }
} }
CACHES["default"]["OPTIONS"] = {
"CLIENT_CLASS": "funkwhale_api.common.cache.RedisClient",
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
}
CACHEOPS_DURATION = env("CACHEOPS_DURATION", default=0) CACHEOPS_DURATION = env("CACHEOPS_DURATION", default=0)
CACHEOPS_ENABLED = bool(CACHEOPS_DURATION) CACHEOPS_ENABLED = bool(CACHEOPS_DURATION)
if CACHEOPS_ENABLED: if CACHEOPS_ENABLED:
INSTALLED_APPS += ("cacheops",) INSTALLED_APPS += ("cacheops",)
CACHEOPS_REDIS = env("CACHE_URL", default=CACHE_DEFAULT) CACHEOPS_REDIS = CACHE_URL
CACHEOPS_PREFIX = lambda _: "cacheops" # noqa CACHEOPS_PREFIX = lambda _: "cacheops" # noqa
CACHEOPS_DEFAULTS = {"timeout": CACHEOPS_DURATION} CACHEOPS_DEFAULTS = {"timeout": CACHEOPS_DURATION}
CACHEOPS = { CACHEOPS = {
...@@ -738,24 +907,24 @@ if CACHEOPS_ENABLED: ...@@ -738,24 +907,24 @@ if CACHEOPS_ENABLED:
# CELERY # CELERY
INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",) INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
CELERY_BROKER_URL = env( CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default=CACHE_URL)
"CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT)
)
""" """
URL to celery's task broker. Defaults to :attr:`CACHE_URL`, so you shouldn't have to tweak this, unless you want The celery task broker URL. Defaults to :attr:`CACHE_URL`.
to use a different one, or use Redis sockets to connect. You don't need to tweak this unless you want
to use a different server or use Redis sockets to connect.
Exemple: Example:
- ``unix://127.0.0.1:6379/0``
- ``redis+socket:///run/redis/redis.sock?virtual_host=0``
- `redis://127.0.0.1:6379/0`
- `redis+socket:///run/redis/redis.sock?virtual_host=0`
""" """
# END CELERY # END CELERY
# Location of root django.contrib.admin URL, use {% url 'admin:index' %} # Location of root django.contrib.admin URL, use {% url 'admin:index' %}
# Your common stuff: Below this line define 3rd party library settings # Your common stuff: Below this line define 3rd party library settings
CELERY_TASK_DEFAULT_RATE_LIMIT = 1 CELERY_TASK_DEFAULT_RATE_LIMIT = 1
CELERY_TASK_TIME_LIMIT = 300 CELERY_TASK_TIME_LIMIT = env.int("CELERY_TASK_TIME_LIMIT", default=300)
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"audio.fetch_rss_feeds": { "audio.fetch_rss_feeds": {
"task": "audio.fetch_rss_feeds", "task": "audio.fetch_rss_feeds",
...@@ -792,6 +961,43 @@ CELERY_BEAT_SCHEDULE = { ...@@ -792,6 +961,43 @@ CELERY_BEAT_SCHEDULE = {
), ),
"options": {"expires": 60 * 60}, "options": {"expires": 60 * 60},
}, },
"music.library.schedule_remote_scan": {
"task": "music.library.schedule_scan",
"schedule": crontab(day_of_week="1", minute="0", hour="2"),
"options": {"expires": 60 * 60 * 24},
},
"federation.check_all_remote_instance_availability": {
"task": "federation.check_all_remote_instance_availability",
"schedule": crontab(
**env.dict(
"SCHEDULE_FEDERATION_CHECK_INTANCES_AVAILABILITY",
default={"minute": "0", "hour": "*"},
)
),
"options": {"expires": 60 * 60},
},
"listenbrainz.trigger_listening_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_listening_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
"listenbrainz.trigger_favorite_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_favorite_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
"tags.update_musicbrainz_genre": {
"task": "tags.update_musicbrainz_genre",
"schedule": crontab(day_of_month="2", minute="30", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
}
if env.str("TYPESENSE_API_KEY", default=None):
CELERY_BEAT_SCHEDULE["typesense.build_canonical_index"] = {
"task": "typesense.build_canonical_index",
"schedule": crontab(day_of_week="*/2", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
} }
if env.bool("ADD_ALBUM_TAGS_FROM_TRACKS", default=True): if env.bool("ADD_ALBUM_TAGS_FROM_TRACKS", default=True):
...@@ -817,13 +1023,6 @@ def get_user_secret_key(user): ...@@ -817,13 +1023,6 @@ def get_user_secret_key(user):
return settings.SECRET_KEY + str(user.secret_key) return settings.SECRET_KEY + str(user.secret_key)
JWT_AUTH = {
"JWT_ALLOW_REFRESH": True,
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
"JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30),
"JWT_AUTH_HEADER_PREFIX": "JWT",
"JWT_GET_USER_SECRET_KEY": get_user_secret_key,
}
OLD_PASSWORD_FIELD_ENABLED = True OLD_PASSWORD_FIELD_ENABLED = True
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
...@@ -838,7 +1037,8 @@ AUTH_PASSWORD_VALIDATORS = [ ...@@ -838,7 +1037,8 @@ AUTH_PASSWORD_VALIDATORS = [
] ]
DISABLE_PASSWORD_VALIDATORS = env.bool("DISABLE_PASSWORD_VALIDATORS", default=False) DISABLE_PASSWORD_VALIDATORS = env.bool("DISABLE_PASSWORD_VALIDATORS", default=False)
""" """
Wether to disable password validators (length, common words, similarity with username…) used during regitration. Whether to disable password validation rules during registration.
Validators include password length, common words, similarity with username.
""" """
if DISABLE_PASSWORD_VALIDATORS: if DISABLE_PASSWORD_VALIDATORS:
AUTH_PASSWORD_VALIDATORS = [] AUTH_PASSWORD_VALIDATORS = []
...@@ -861,9 +1061,7 @@ REST_FRAMEWORK = { ...@@ -861,9 +1061,7 @@ REST_FRAMEWORK = {
), ),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"funkwhale_api.common.authentication.OAuth2Authentication", "funkwhale_api.common.authentication.OAuth2Authentication",
"funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", "funkwhale_api.common.authentication.ApplicationTokenAuthentication",
"funkwhale_api.common.authentication.BearerTokenHeaderAuth",
"funkwhale_api.common.authentication.JSONWebTokenAuthentication",
"rest_framework.authentication.BasicAuthentication", "rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
), ),
...@@ -879,8 +1077,9 @@ REST_FRAMEWORK = { ...@@ -879,8 +1077,9 @@ REST_FRAMEWORK = {
} }
THROTTLING_ENABLED = env.bool("THROTTLING_ENABLED", default=True) THROTTLING_ENABLED = env.bool("THROTTLING_ENABLED", default=True)
""" """
Wether to enable throttling (also known as rate-limiting). Leaving this enabled is recommended Whether to enable throttling (also known as rate-limiting).
especially on public pods, to improve the quality of service. We recommend you leave this enabled to improve the quality
of the service, especially on public pods .
""" """
if THROTTLING_ENABLED: if THROTTLING_ENABLED:
...@@ -998,13 +1197,9 @@ THROTTLING_RATES = { ...@@ -998,13 +1197,9 @@ THROTTLING_RATES = {
"rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"), "rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"),
"description": "OAuth token deletion", "description": "OAuth token deletion",
}, },
"jwt-login": { "login": {
"rate": THROTTLING_USER_RATES.get("jwt-login", "30/hour"), "rate": THROTTLING_USER_RATES.get("login", "30/hour"),
"description": "JWT token creation", "description": "Login",
},
"jwt-refresh": {
"rate": THROTTLING_USER_RATES.get("jwt-refresh", "30/hour"),
"description": "JWT token refresh",
}, },
"signup": { "signup": {
"rate": THROTTLING_USER_RATES.get("signup", "10/day"), "rate": THROTTLING_USER_RATES.get("signup", "10/day"),
...@@ -1033,9 +1228,10 @@ THROTTLING_RATES = { ...@@ -1033,9 +1228,10 @@ THROTTLING_RATES = {
} }
THROTTLING_RATES = THROTTLING_RATES THROTTLING_RATES = THROTTLING_RATES
""" """
Throttling rates for specific endpoints and features of the app. You can tweak this if you are Throttling rates for specific endpoints and app features.
encountering to severe rate limiting issues or, on the contrary, if you want to reduce Tweak this if you're hitting rate limit issues or if you want
the consumption on some endpoints. to reduce the consumption of specific endpoints. Takes
the format ``<endpoint name>=<number>/<interval>``.
Example: Example:
...@@ -1048,45 +1244,49 @@ if BROWSABLE_API_ENABLED: ...@@ -1048,45 +1244,49 @@ if BROWSABLE_API_ENABLED:
"rest_framework.renderers.BrowsableAPIRenderer", "rest_framework.renderers.BrowsableAPIRenderer",
) )
REST_AUTH_SERIALIZERS = { REST_AUTH = {
"PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer" # noqa "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer", # noqa
"PASSWORD_RESET_CONFIRM_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetConfirmSerializer", # noqa
} }
REST_SESSION_LOGIN = False REST_SESSION_LOGIN = False
REST_USE_JWT = True
ATOMIC_REQUESTS = False ATOMIC_REQUESTS = False
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True USE_X_FORWARDED_PORT = True
# Wether we should use Apache, Nginx (or other) headers when serving audio files # Whether we should use Apache, Nginx (or other) headers
# Default to Nginx # when serving audio files. Defaults to Nginx.
REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx") REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx")
""" """
Depending on the reverse proxy used in front of your funkwhale instance, Set your reverse proxy type. This changes the headers the
the API will use different kind of headers to serve audio files API uses to serve audio files. Allowed values:
Allowed values: ``nginx``, ``apache2`` - ``nginx``
- ``apache2``
""" """
assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE" assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE"
PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected") PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected")
""" """
Which path will be used to process the internal redirection to the reverse proxy The path used to process internal redirection
**DO NOT** put a slash at the end. to the reverse proxy.
You shouldn't have to tweak this. .. important::
Don't insert a slash at the end of this path.
""" """
MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300) MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300)
""" """
How long to cache MusicBrainz results, in seconds Length of time in seconds to cache MusicBrainz results.
""" """
MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org") MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org")
""" """
Use this setting to change the musicbrainz hostname, for instance to The hostname of your MusicBrainz instance. Change
use a mirror. The hostname can also contain a port number. this setting if you run your own server or use a mirror.
You can include a port number in the hostname.
Example: Examples:
- ``mymusicbrainz.mirror`` - ``mymusicbrainz.mirror``
- ``localhost:5000`` - ``localhost:5000``
...@@ -1095,22 +1295,17 @@ Example: ...@@ -1095,22 +1295,17 @@ Example:
# Custom Admin URL, use {% url 'admin:index' %} # Custom Admin URL, use {% url 'admin:index' %}
ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/") ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
""" """
Path to the Django admin area. Path to the Django admin dashboard.
Exemples: Examples:
- `^api/admin/` - ``^api/admin/``
- `^api/mycustompath/` - ``^api/mycustompath/``
""" """
CSRF_USE_SESSIONS = True CSRF_USE_SESSIONS = False
SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_ENGINE = "django.contrib.sessions.backends.cache"
# Playlist settings
# XXX: deprecated, see #186
PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250)
ACCOUNT_USERNAME_BLACKLIST = [ ACCOUNT_USERNAME_BLACKLIST = [
"funkwhale", "funkwhale",
"library", "library",
...@@ -1136,69 +1331,70 @@ ACCOUNT_USERNAME_BLACKLIST = [ ...@@ -1136,69 +1331,70 @@ ACCOUNT_USERNAME_BLACKLIST = [
"actor", "actor",
] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[]) ] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])
""" """
List of usernames that will be unavailable during registration. List of usernames that can't be used for registration. Given as a list of strings.
""" """
EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True) EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True)
""" """
Wether to enforce HTTPS certificates verification when doing outgoing HTTP requests (typically with federation). Whether to enforce TLS certificate verification
Disabling this is not recommended. when performing outgoing HTTP requests.
We recommend you leave this setting enabled.
""" """
EXTERNAL_REQUESTS_TIMEOUT = env.int("EXTERNAL_REQUESTS_TIMEOUT", default=10) EXTERNAL_REQUESTS_TIMEOUT = env.int("EXTERNAL_REQUESTS_TIMEOUT", default=10)
""" """
Default timeout for external requests. Default timeout for external requests.
""" """
# XXX: deprecated, see #186
API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True)
MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None) MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None)
""" """
The path on your server where Funkwhale can import files using :ref:`in-place import The path on your server where Funkwhale places
<in-place-import>`. It must be readable by the webserver and Funkwhale files from in-place imports. This path needs to be
api and worker processes. readable by the webserver and ``api`` and ``worker``
processes.
On docker installations, we recommend you use the default of ``/music`` .. important::
for this value. For non-docker installation, you can use any absolute path.
``/srv/funkwhale/data/music`` is a safe choice if you don't know what to use.
.. note:: This path should not include any trailing slash Don’t insert a slash at the end of this path.
.. warning:: On Docker installations, we recommend you use the default ``/music`` path.
On Debian installations you can use any absolute path. Defaults to
``/srv/funkwhale/data/music``.
.. note::
You need to adapt your :ref:`reverse-proxy configuration<reverse-proxy-setup>` to You need to add this path to your reverse proxy configuration.
serve the directory pointed by ``MUSIC_DIRECTORY_PATH`` on Add the directory to your ``/_protected/music`` server block.
``/_protected/music`` URL.
""" """
MUSIC_DIRECTORY_SERVE_PATH = env( MUSIC_DIRECTORY_SERVE_PATH = env(
"MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
) )
""" """
Default: :attr:`MUSIC_DIRECTORY_PATH` On Docker setups the value of :attr:`MUSIC_DIRECTORY_PATH`
may be different from the actual path on your server.
When using Docker, the value of :attr:`MUSIC_DIRECTORY_PATH` in your containers You can specify this path in your :file:`docker-compose.yml` file::
may differ from the real path on your host. Assuming you have the following directive
in your :file:`docker-compose.yml` file::
volumes: volumes:
- /srv/funkwhale/data/music:/music:ro - /srv/funkwhale/data/music:/music:ro
Then, the value of :attr:`MUSIC_DIRECTORY_SERVE_PATH` should be In this case, you need to set :attr:`MUSIC_DIRECTORY_SERVE_PATH`
``/srv/funkwhale/data/music``. This must be readable by the webserver. to ``/srv/funkwhale/data/music``. The webserver needs to be
able to read this directory.
On non-docker setup, you don't need to configure this setting. .. important::
.. note:: This path should not include any trailing slash Don’t insert a slash at the end of this path.
""" """
# When this is set to default=True, we need to reenable migration music/0042 # When this is set to default=True, we need to re-enable migration music/0042
# to ensure data is populated correctly on existing pods # to ensure data is populated correctly on existing pods
MUSIC_USE_DENORMALIZATION = env.bool("MUSIC_USE_DENORMALIZATION", default=False) MUSIC_USE_DENORMALIZATION = env.bool("MUSIC_USE_DENORMALIZATION", default=True)
USERS_INVITATION_EXPIRATION_DAYS = env.int( USERS_INVITATION_EXPIRATION_DAYS = env.int(
"USERS_INVITATION_EXPIRATION_DAYS", default=14 "USERS_INVITATION_EXPIRATION_DAYS", default=14
) )
""" """
Expiration delay in days, for user invitations. The number of days before a user invite expires.
""" """
VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = { VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
...@@ -1210,10 +1406,15 @@ VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = { ...@@ -1210,10 +1406,15 @@ VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
], ],
"attachment_square": [ "attachment_square": [
("original", "url"), ("original", "url"),
("small_square_crop", "crop__50x50"),
("medium_square_crop", "crop__200x200"), ("medium_square_crop", "crop__200x200"),
("large_square_crop", "crop__600x600"),
], ],
} }
VERSATILEIMAGEFIELD_SETTINGS = {"create_images_on_demand": False} VERSATILEIMAGEFIELD_SETTINGS = {
"create_images_on_demand": False,
"jpeg_resize_quality": env.int("THUMBNAIL_JPEG_RESIZE_QUALITY", default=95),
}
RSA_KEY_SIZE = 2048 RSA_KEY_SIZE = 2048
# for performance gain in tests, since we don't need to actually create the # for performance gain in tests, since we don't need to actually create the
# thumbnails # thumbnails
...@@ -1224,24 +1425,27 @@ SUBSONIC_DEFAULT_TRANSCODING_FORMAT = ( ...@@ -1224,24 +1425,27 @@ SUBSONIC_DEFAULT_TRANSCODING_FORMAT = (
env("SUBSONIC_DEFAULT_TRANSCODING_FORMAT", default="mp3") or None env("SUBSONIC_DEFAULT_TRANSCODING_FORMAT", default="mp3") or None
) )
""" """
Default format for transcoding when using Subsonic API. The default format files are transcoded into when using the Subsonic
API.
""" """
# extra tags will be ignored # extra tags will be ignored
TAGS_MAX_BY_OBJ = env.int("TAGS_MAX_BY_OBJ", default=30) TAGS_MAX_BY_OBJ = env.int("TAGS_MAX_BY_OBJ", default=30)
""" """
Maximum number of tags that can be associated with an object. Extra tags will be ignored. Maximum number of tags that can be associated with an object.
Extra tags are ignored.
""" """
FEDERATION_OBJECT_FETCH_DELAY = env.int( FEDERATION_OBJECT_FETCH_DELAY = env.int(
"FEDERATION_OBJECT_FETCH_DELAY", default=60 * 24 * 3 "FEDERATION_OBJECT_FETCH_DELAY", default=60 * 24 * 3
) )
""" """
Number of minutes before a remote object will be automatically refetched when accessed in the UI. The delay in minutes before a remote object is automatically
refetched when accessed in the UI.
""" """
MODERATION_EMAIL_NOTIFICATIONS_ENABLED = env.bool( MODERATION_EMAIL_NOTIFICATIONS_ENABLED = env.bool(
"MODERATION_EMAIL_NOTIFICATIONS_ENABLED", default=True "MODERATION_EMAIL_NOTIFICATIONS_ENABLED", default=True
) )
""" """
Whether to enable email notifications to moderators and pods admins. Whether to enable email notifications to moderators and pod admins.
""" """
FEDERATION_AUTHENTIFY_FETCHES = True FEDERATION_AUTHENTIFY_FETCHES = True
FEDERATION_SYNCHRONOUS_FETCH = env.bool("FEDERATION_SYNCHRONOUS_FETCH", default=True) FEDERATION_SYNCHRONOUS_FETCH = env.bool("FEDERATION_SYNCHRONOUS_FETCH", default=True)
...@@ -1249,31 +1453,31 @@ FEDERATION_DUPLICATE_FETCH_DELAY = env.int( ...@@ -1249,31 +1453,31 @@ FEDERATION_DUPLICATE_FETCH_DELAY = env.int(
"FEDERATION_DUPLICATE_FETCH_DELAY", default=60 * 50 "FEDERATION_DUPLICATE_FETCH_DELAY", default=60 * 50
) )
""" """
Delay, in seconds, between two manual fetch of the same remote object. The delay in seconds between two manual fetches of the same remote object.
""" """
INSTANCE_SUPPORT_MESSAGE_DELAY = env.int("INSTANCE_SUPPORT_MESSAGE_DELAY", default=15) INSTANCE_SUPPORT_MESSAGE_DELAY = env.int("INSTANCE_SUPPORT_MESSAGE_DELAY", default=15)
""" """
Delay in days after signup before we show the "support your pod" message The number of days before your pod shows the "support your pod" message.
The timer starts after the user signs up.
""" """
FUNKWHALE_SUPPORT_MESSAGE_DELAY = env.int("FUNKWHALE_SUPPORT_MESSAGE_DELAY", default=15) FUNKWHALE_SUPPORT_MESSAGE_DELAY = env.int("FUNKWHALE_SUPPORT_MESSAGE_DELAY", default=15)
""" """
Delay in days after signup before we show the "support Funkwhale" message The number of days before your pod shows the "support Funkwhale" message.
The timer starts after the user signs up.
""" """
# XXX Stable release: remove
USE_FULL_TEXT_SEARCH = env.bool("USE_FULL_TEXT_SEARCH", default=True)
MIN_DELAY_BETWEEN_DOWNLOADS_COUNT = env.int( MIN_DELAY_BETWEEN_DOWNLOADS_COUNT = env.int(
"MIN_DELAY_BETWEEN_DOWNLOADS_COUNT", default=60 * 60 * 6 "MIN_DELAY_BETWEEN_DOWNLOADS_COUNT", default=60 * 60 * 6
) )
""" """
Minimum required period, in seconds, for two downloads of the same track by the same IP The required number of seconds between downloads of a track
or user to be recorded in statistics. by the same IP or user to be counted separately in listen statistics.
""" """
MARKDOWN_EXTENSIONS = env.list("MARKDOWN_EXTENSIONS", default=["nl2br", "extra"]) MARKDOWN_EXTENSIONS = env.list("MARKDOWN_EXTENSIONS", default=["nl2br", "extra"])
""" """
List of markdown extensions to enable. A list of markdown extensions to enable.
Cf `<https://python-markdown.github.io/extensions/>`_ See `<https://python-markdown.github.io/extensions/>`_.
""" """
LINKIFIER_SUPPORTED_TLDS = ["audio"] + env.list("LINKINFIER_SUPPORTED_TLDS", default=[]) LINKIFIER_SUPPORTED_TLDS = ["audio"] + env.list("LINKINFIER_SUPPORTED_TLDS", default=[])
""" """
...@@ -1281,23 +1485,28 @@ Additional TLDs to support with our markdown linkifier. ...@@ -1281,23 +1485,28 @@ Additional TLDs to support with our markdown linkifier.
""" """
EXTERNAL_MEDIA_PROXY_ENABLED = env.bool("EXTERNAL_MEDIA_PROXY_ENABLED", default=True) EXTERNAL_MEDIA_PROXY_ENABLED = env.bool("EXTERNAL_MEDIA_PROXY_ENABLED", default=True)
""" """
Wether to proxy attachment files hosted on third party pods and and servers. Keeping Whether to proxy attachment files hosted on third party pods and and servers.
this to true is recommended, to reduce leaking browsing information of your users, and We recommend you leave this set to ``true``. This reduces the risk of leaking
reduce the bandwidth used on remote pods. user browsing information and reduces the bandwidth used on remote pods.
""" """
PODCASTS_THIRD_PARTY_VISIBILITY = env("PODCASTS_THIRD_PARTY_VISIBILITY", default="me") PODCASTS_THIRD_PARTY_VISIBILITY = env("PODCASTS_THIRD_PARTY_VISIBILITY", default="me")
""" """
By default, only people who subscribe to a podcast RSS will have access to their episodes. By default, only people who subscribe to a podcast RSS have access
switch to "instance" or "everyone" to change that. to its episodes. Change to ``instance`` or ``everyone`` to change the
default visibility.
.. note::
Changing it only affect new podcasts. Changing this value only affect new podcasts.
""" """
PODCASTS_RSS_FEED_REFRESH_DELAY = env.int( PODCASTS_RSS_FEED_REFRESH_DELAY = env.int(
"PODCASTS_RSS_FEED_REFRESH_DELAY", default=60 * 60 * 24 "PODCASTS_RSS_FEED_REFRESH_DELAY", default=60 * 60 * 24
) )
""" """
Delay in seconds between to fetch of RSS feeds. Reducing this mean you'll receive new episodes faster, The delay in seconds between two fetch of RSS feeds.
but will require more resources.
A lower rate means new episodes are fetched sooner,
but requires more resources.
""" """
# maximum items loaded through XML feed # maximum items loaded through XML feed
PODCASTS_RSS_FEED_MAX_ITEMS = env.int("PODCASTS_RSS_FEED_MAX_ITEMS", default=250) PODCASTS_RSS_FEED_MAX_ITEMS = env.int("PODCASTS_RSS_FEED_MAX_ITEMS", default=250)
...@@ -1309,8 +1518,35 @@ IGNORE_FORWARDED_HOST_AND_PROTO = env.bool( ...@@ -1309,8 +1518,35 @@ IGNORE_FORWARDED_HOST_AND_PROTO = env.bool(
"IGNORE_FORWARDED_HOST_AND_PROTO", default=True "IGNORE_FORWARDED_HOST_AND_PROTO", default=True
) )
""" """
Use :attr:`FUNKWHALE_HOSTNAME` and :attr:`FUNKWHALE_PROTOCOL ` instead of request header. Use :attr:`FUNKWHALE_HOSTNAME` and :attr:`FUNKWHALE_PROTOCOL`
instead of request header.
""" """
HASHING_ALGORITHM = "sha256" HASHING_ALGORITHM = "sha256"
HASHING_CHUNK_SIZE = 1024 * 100 HASHING_CHUNK_SIZE = 1024 * 100
"""
Typenses Settings
"""
TYPESENSE_API_KEY = env("TYPESENSE_API_KEY", default=None)
""" Typesense API key. This need to be defined in the .env file for Typenses to be activated."""
TYPESENSE_PORT = env("TYPESENSE_PORT", default="8108")
"""Typesense listening port"""
TYPESENSE_PROTOCOL = env("TYPESENSE_PROTOCOL", default="http")
"""Typesense listening protocol"""
TYPESENSE_HOST = env(
"TYPESENSE_HOST",
default="typesense" if IS_DOCKER_SETUP else "localhost",
)
"""
Typesense hostname. Defaults to `localhost` on non-Docker deployments and to `typesense` on
Docker deployments.
"""
TYPESENSE_NUM_TYPO = env("TYPESENSE_NUM_TYPO", default=5)
"""
Max tracks to be downloaded when the THIRD_PARTY_UPLOAD plugin hook is triggered.
Each api request to playlist tracks or radio tracks trigger the hook if tracks upload are missing.
If your instance is big your ip might get rate limited.
"""
THIRD_PARTY_UPLOAD_MAX_UPLOADS = env.int("THIRD_PARTY_UPLOAD_MAX_UPLOADS", default=10)
# -*- coding: utf-8 -*-
""" """
Local settings Local settings
- Run in Debug mode - Run in Debug mode
- Use console backend for emails - Add Django Debug Toolbar when INTERNAL_IPS are given and match the request
- Add Django Debug Toolbar
- Add django-extensions as app - Add django-extensions as app
""" """
from .common import * # noqa from funkwhale_api import __version__ as funkwhale_version
from .common import * # noqa
# DEBUG # DEBUG
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
...@@ -25,11 +24,6 @@ SECRET_KEY = env( ...@@ -25,11 +24,6 @@ SECRET_KEY = env(
"DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc" "DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc"
) )
# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = "localhost"
EMAIL_PORT = 1025
# django-debug-toolbar # django-debug-toolbar
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
...@@ -76,7 +70,9 @@ DEBUG_TOOLBAR_PANELS = [ ...@@ -76,7 +70,9 @@ DEBUG_TOOLBAR_PANELS = [
# django-extensions # django-extensions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# INSTALLED_APPS += ('django_extensions', ) INSTALLED_APPS += ("django_extensions",)
INSTALLED_APPS += ("drf_spectacular",)
# Debug toolbar is slow, we disable it for tests # Debug toolbar is slow, we disable it for tests
DEBUG_TOOLBAR_ENABLED = env.bool("DEBUG_TOOLBAR_ENABLED", default=DEBUG) DEBUG_TOOLBAR_ENABLED = env.bool("DEBUG_TOOLBAR_ENABLED", default=DEBUG)
...@@ -94,8 +90,47 @@ CELERY_TASK_ALWAYS_EAGER = False ...@@ -94,8 +90,47 @@ CELERY_TASK_ALWAYS_EAGER = False
# Your local stuff: Below this line define 3rd party library settings # Your local stuff: Below this line define 3rd party library settings
CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS] REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "funkwhale_api.schema.CustomAutoSchema"
SPECTACULAR_SETTINGS = {
"TITLE": "Funkwhale API",
"DESCRIPTION": open("Readme.md").read(),
"VERSION": funkwhale_version,
"SCHEMA_PATH_PREFIX": "/api/(v[0-9])?",
"OAUTH_FLOWS": ["authorizationCode"],
"AUTHENTICATION_WHITELIST": [
"funkwhale_api.common.authentication.OAuth2Authentication",
"funkwhale_api.common.authentication.ApplicationTokenAuthentication",
],
"SERVERS": [
{"url": "https://demo.funkwhale.audio", "description": "Demo Server"},
{
"url": "https://funkwhale.audio",
"description": "Read server with real content",
},
{
"url": "{protocol}://{domain}",
"description": "Custom server",
"variables": {
"domain": {
"default": "yourdomain",
"description": "Your Funkwhale Domain",
},
"protocol": {"enum": ["http", "https"], "default": "https"},
},
},
],
"OAUTH2_FLOWS": ["authorizationCode"],
"OAUTH2_AUTHORIZATION_URL": "/authorize",
"OAUTH2_TOKEN_URL": "/api/v1/oauth/token/",
"PREPROCESSING_HOOKS": ["config.schema.custom_preprocessing_hook"],
"ENUM_NAME_OVERRIDES": {
"FederationChoiceEnum": "funkwhale_api.federation.models.TYPE_CHOICES",
"ReportTypeEnum": "funkwhale_api.moderation.models.REPORT_TYPES",
"PrivacyLevelEnum": "funkwhale_api.common.fields.PRIVACY_LEVEL_CHOICES",
"LibraryPrivacyLevelEnum": "funkwhale_api.music.models.LIBRARY_PRIVACY_LEVEL_CHOICES",
},
"COMPONENT_SPLIT_REQUEST": True,
}
if env.bool("WEAK_PASSWORDS", default=False): if env.bool("WEAK_PASSWORDS", default=False):
# Faster during tests # Faster during tests
...@@ -104,4 +139,19 @@ if env.bool("WEAK_PASSWORDS", default=False): ...@@ -104,4 +139,19 @@ if env.bool("WEAK_PASSWORDS", default=False):
MIDDLEWARE = ( MIDDLEWARE = (
"funkwhale_api.common.middleware.DevHttpsMiddleware", "funkwhale_api.common.middleware.DevHttpsMiddleware",
"funkwhale_api.common.middleware.ProfilerMiddleware", "funkwhale_api.common.middleware.ProfilerMiddleware",
"funkwhale_api.common.middleware.PymallocMiddleware",
) + MIDDLEWARE ) + MIDDLEWARE
REST_FRAMEWORK.update(
{
"TEST_REQUEST_RENDERER_CLASSES": [
"rest_framework.renderers.MultiPartRenderer",
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.TemplateHTMLRenderer",
"funkwhale_api.playlists.renderers.PlaylistXspfRenderer",
],
}
)
# allows makemigrations and superuser creation
FORCE = env("FORCE", default=True)
# -*- coding: utf-8 -*-
""" """
Production Configurations Production Configurations
- Use djangosecure - Use djangosecure
- Use Amazon's S3 for storing static files and uploaded media - 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 - Use Redis on Heroku
""" """
from __future__ import absolute_import, unicode_literals
from .common import * # noqa from .common import * # noqa
...@@ -43,18 +41,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY") ...@@ -43,18 +41,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY")
# SESSION_COOKIE_HTTPONLY = True # SESSION_COOKIE_HTTPONLY = True
# SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=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"
# TEMPLATE CONFIGURATION # TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: # See:
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.conf.urls import url
from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from funkwhale_api.common import admin from django.urls import include, path, re_path
from django.views import defaults as default_views from django.views import defaults as default_views
from config import plugins
from funkwhale_api.common import admin
plugins_patterns = plugins.trigger_filter(plugins.URLS, [], enabled=True)
api_patterns = [
re_path("v1/", include("config.urls.api")),
re_path("v2/", include("config.urls.api_v2")),
re_path("subsonic/", include("config.urls.subsonic")),
]
urlpatterns = [ urlpatterns = [
# Django Admin, use {% url 'admin:index' %} # Django Admin, use {% url 'admin:index' %}
url(settings.ADMIN_URL, admin.site.urls), re_path(settings.ADMIN_URL, admin.site.urls),
url(r"^api/", include(("config.api_urls", "api"), namespace="api")), re_path(r"^api/", include((api_patterns, "api"), namespace="api")),
url( re_path(
r"^", r"^",
include( include(
("funkwhale_api.federation.urls", "federation"), namespace="federation" ("funkwhale_api.federation.urls", "federation"), namespace="federation"
), ),
), ),
url(r"^api/v1/auth/", include("funkwhale_api.users.rest_auth_urls")), re_path(
url(r"^accounts/", include("allauth.urls")), r"^api/v1/auth/",
# Your stuff: custom urls includes go here include("funkwhale_api.users.rest_auth_urls"),
] ),
re_path(
r"^api/v2/auth/",
include("funkwhale_api.users.rest_auth_urls"),
),
re_path(r"^accounts/", include("allauth.urls")),
] + plugins_patterns
if settings.DEBUG: if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit # This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like. # these url in browser to see how these error pages look like.
urlpatterns += [ urlpatterns += [
url(r"^400/$", default_views.bad_request), re_path(r"^400/$", default_views.bad_request),
url(r"^403/$", default_views.permission_denied), re_path(r"^403/$", default_views.permission_denied),
url(r"^404/$", default_views.page_not_found), re_path(r"^404/$", default_views.page_not_found),
url(r"^500/$", default_views.server_error), re_path(r"^500/$", default_views.server_error),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if "debug_toolbar" in settings.INSTALLED_APPS: if "debug_toolbar" in settings.INSTALLED_APPS:
...@@ -43,5 +55,5 @@ if settings.DEBUG: ...@@ -43,5 +55,5 @@ if settings.DEBUG:
if "silk" in settings.INSTALLED_APPS: if "silk" in settings.INSTALLED_APPS:
urlpatterns = [ urlpatterns = [
url(r"^api/silk/", include("silk.urls", namespace="silk")) re_path(r"^api/silk/", include("silk.urls", namespace="silk"))
] + urlpatterns ] + urlpatterns
from django.conf.urls import include, url from django.conf.urls import include
from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet from django.urls import re_path
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from funkwhale_api.activity import views as activity_views from funkwhale_api.activity import views as activity_views
from funkwhale_api.audio import views as audio_views from funkwhale_api.audio import views as audio_views
from funkwhale_api.common import views as common_views
from funkwhale_api.common import routers as common_routers from funkwhale_api.common import routers as common_routers
from funkwhale_api.common import views as common_views
from funkwhale_api.music import views from funkwhale_api.music import views
from funkwhale_api.playlists import views as playlists_views from funkwhale_api.playlists import views as playlists_views
from funkwhale_api.subsonic.views import SubsonicViewSet
from funkwhale_api.tags import views as tags_views from funkwhale_api.tags import views as tags_views
from funkwhale_api.users import jwt_views
router = common_routers.OptionalSlashRouter() router = common_routers.OptionalSlashRouter()
router.register(r"settings", GlobalPreferencesViewSet, basename="settings")
router.register(r"activity", activity_views.ActivityViewSet, "activity") router.register(r"activity", activity_views.ActivityViewSet, "activity")
router.register(r"tags", tags_views.TagViewSet, "tags") router.register(r"tags", tags_views.TagViewSet, "tags")
router.register(r"plugins", common_views.PluginViewSet, "plugins")
router.register(r"tracks", views.TrackViewSet, "tracks") router.register(r"tracks", views.TrackViewSet, "tracks")
router.register(r"uploads", views.UploadViewSet, "uploads") router.register(r"uploads", views.UploadViewSet, "uploads")
router.register(r"libraries", views.LibraryViewSet, "libraries") router.register(r"libraries", views.LibraryViewSet, "libraries")
router.register(r"listen", views.ListenViewSet, "listen") router.register(r"listen", views.ListenViewSet, "listen")
router.register(r"stream", views.StreamViewSet, "stream")
router.register(r"artists", views.ArtistViewSet, "artists") router.register(r"artists", views.ArtistViewSet, "artists")
router.register(r"channels", audio_views.ChannelViewSet, "channels") router.register(r"channels", audio_views.ChannelViewSet, "channels")
router.register(r"subscriptions", audio_views.SubscriptionsViewSet, "subscriptions") router.register(r"subscriptions", audio_views.SubscriptionsViewSet, "subscriptions")
router.register(r"albums", views.AlbumViewSet, "albums") router.register(r"albums", views.AlbumViewSet, "albums")
router.register(r"licenses", views.LicenseViewSet, "licenses") router.register(r"licenses", views.LicenseViewSet, "licenses")
router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists") router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
router.register(
r"playlist-tracks", playlists_views.PlaylistTrackViewSet, "playlist-tracks"
)
router.register(r"mutations", common_views.MutationViewSet, "mutations") router.register(r"mutations", common_views.MutationViewSet, "mutations")
router.register(r"attachments", common_views.AttachmentViewSet, "attachments") router.register(r"attachments", common_views.AttachmentViewSet, "attachments")
v1_patterns = router.urls v1_patterns = router.urls
subsonic_router = routers.SimpleRouter(trailing_slash=False)
subsonic_router.register(r"subsonic/rest", SubsonicViewSet, basename="subsonic")
v1_patterns += [ v1_patterns += [
url(r"^oembed/$", views.OembedView.as_view(), name="oembed"), re_path(r"^oembed/$", views.OembedView.as_view(), name="oembed"),
url( re_path(
r"^instance/", r"^instance/",
include(("funkwhale_api.instance.urls", "instance"), namespace="instance"), include(("funkwhale_api.instance.urls", "instance"), namespace="instance"),
), ),
url( re_path(
r"^manage/", r"^manage/",
include(("funkwhale_api.manage.urls", "manage"), namespace="manage"), include(("funkwhale_api.manage.urls", "manage"), namespace="manage"),
), ),
url( re_path(
r"^moderation/", r"^moderation/",
include( include(
("funkwhale_api.moderation.urls", "moderation"), namespace="moderation" ("funkwhale_api.moderation.urls", "moderation"), namespace="moderation"
), ),
), ),
url( re_path(
r"^federation/", r"^federation/",
include( include(
("funkwhale_api.federation.api_urls", "federation"), namespace="federation" ("funkwhale_api.federation.api_urls", "federation"), namespace="federation"
), ),
), ),
url( re_path(
r"^providers/", r"^providers/",
include(("funkwhale_api.providers.urls", "providers"), namespace="providers"), include(("funkwhale_api.providers.urls", "providers"), namespace="providers"),
), ),
url( re_path(
r"^favorites/", r"^favorites/",
include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"), include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"),
), ),
url(r"^search$", views.Search.as_view(), name="search"), re_path(r"^search$", views.Search.as_view(), name="search"),
url( re_path(
r"^radios/", r"^radios/",
include(("funkwhale_api.radios.urls", "radios"), namespace="radios"), include(("funkwhale_api.radios.urls", "radios"), namespace="radios"),
), ),
url( re_path(
r"^history/", r"^history/",
include(("funkwhale_api.history.urls", "history"), namespace="history"), include(("funkwhale_api.history.urls", "history"), namespace="history"),
), ),
url( re_path(
r"^users/", r"^",
include(("funkwhale_api.users.api_urls", "users"), namespace="users"), include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
), ),
url( # XXX: remove if Funkwhale 1.1
re_path(
r"^users/",
include(("funkwhale_api.users.api_urls", "users"), namespace="users-nested"),
),
re_path(
r"^oauth/", r"^oauth/",
include(("funkwhale_api.users.oauth.urls", "oauth"), namespace="oauth"), include(("funkwhale_api.users.oauth.urls", "oauth"), namespace="oauth"),
), ),
url(r"^token/?$", jwt_views.obtain_jwt_token, name="token"), re_path(r"^rate-limit/?$", common_views.RateLimitView.as_view(), name="rate-limit"),
url(r"^token/refresh/?$", jwt_views.refresh_jwt_token, name="token_refresh"), re_path(
url(r"^rate-limit/?$", common_views.RateLimitView.as_view(), name="rate-limit"),
url(
r"^text-preview/?$", common_views.TextPreviewView.as_view(), name="text-preview" r"^text-preview/?$", common_views.TextPreviewView.as_view(), name="text-preview"
), ),
] ]
urlpatterns = [ urlpatterns = [re_path("", include((v1_patterns, "v1"), namespace="v1"))]
url(r"^v1/", include((v1_patterns, "v1"), namespace="v1"))
] + format_suffix_patterns(subsonic_router.urls, allowed=["view"])
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"))]
...@@ -4,7 +4,6 @@ from funkwhale_api.audio import spa_views as audio_spa_views ...@@ -4,7 +4,6 @@ from funkwhale_api.audio import spa_views as audio_spa_views
from funkwhale_api.federation import spa_views as federation_spa_views from funkwhale_api.federation import spa_views as federation_spa_views
from funkwhale_api.music import spa_views from funkwhale_api.music import spa_views
urlpatterns = [ urlpatterns = [
urls.re_path( urls.re_path(
r"^library/tracks/(?P<pk>\d+)/?$", spa_views.library_track, name="library_track" r"^library/tracks/(?P<pk>\d+)/?$", spa_views.library_track, name="library_track"
......
from django.conf.urls import include
from django.urls import re_path
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from funkwhale_api.subsonic.views import SubsonicViewSet
subsonic_router = routers.SimpleRouter(trailing_slash=False)
subsonic_router.register(r"rest", SubsonicViewSet, basename="subsonic")
subsonic_patterns = format_suffix_patterns(subsonic_router.urls, allowed=["view"])
urlpatterns = [
re_path("", include((subsonic_patterns, "subsonic"), namespace="subsonic"))
]
# urlpatterns = [
# url(
# r"^subsonic/rest/",
# include((subsonic_router.urls, "subsonic"), namespace="subsonic"),
# )
# ]
#!/bin/sh
set -eux
funkwhale-manage collectstatic --noinput
funkwhale-manage migrate
# shellcheck disable=SC2086
exec gunicorn config.asgi:application \
--workers "${FUNKWHALE_WEB_WORKERS-1}" \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:"${FUNKWHALE_API_PORT}" \
${GUNICORN_ARGS-}
# -*- coding: utf-8 -*- from importlib.metadata import version as get_version
__version__ = "0.21.1"
__version_info__ = tuple( version = get_version("funkwhale_api")
[ __version__ = version
int(num) if num.isdigit() else num
for num in __version__.replace("-", ".", 1).split(".")
]
)
...@@ -7,7 +7,7 @@ class ActivityConfig(AppConfig): ...@@ -7,7 +7,7 @@ class ActivityConfig(AppConfig):
name = "funkwhale_api.activity" name = "funkwhale_api.activity"
def ready(self): def ready(self):
super(ActivityConfig, self).ready() super().ready()
app_names = [app.name for app in apps.app_configs.values()] app_names = [app.name for app in apps.app_configs.values()]
record.registry.autodiscover(app_names) record.registry.autodiscover(app_names)
...@@ -17,7 +17,7 @@ def combined_recent(limit, **kwargs): ...@@ -17,7 +17,7 @@ def combined_recent(limit, **kwargs):
_qs_list = list(querysets.values()) _qs_list = list(querysets.values())
union_qs = _qs_list[0].union(*_qs_list[1:]) union_qs = _qs_list[0].union(*_qs_list[1:])
records = [] records = []
for row in union_qs.order_by("-{}".format(datetime_field))[:limit]: for row in union_qs.order_by(f"-{datetime_field}")[:limit]:
records.append( records.append(
{"type": row["__type"], "when": row[datetime_field], "pk": row["pk"]} {"type": row["__type"], "when": row[datetime_field], "pk": row["pk"]}
) )
...@@ -38,13 +38,27 @@ def combined_recent(limit, **kwargs): ...@@ -38,13 +38,27 @@ def combined_recent(limit, **kwargs):
def get_activity(user, limit=20): def get_activity(user, limit=20):
query = fields.privacy_level_query(user, lookup_field="user__privacy_level") query = fields.privacy_level_query(
user, "actor__user__privacy_level", "actor__user"
)
querysets = [ querysets = [
Listening.objects.filter(query).select_related( Listening.objects.filter(query)
"track", "user", "track__artist", "track__album__artist" .select_related(
"track",
"actor",
)
.prefetch_related(
"track__artist_credit__artist",
"track__album__artist_credit__artist",
), ),
TrackFavorite.objects.filter(query).select_related( TrackFavorite.objects.filter(query)
"track", "user", "track__artist", "track__album__artist" .select_related(
"track",
"actor",
)
.prefetch_related(
"track__artist_credit__artist",
"track__album__artist_credit__artist",
), ),
] ]
records = combined_recent(limit=limit, querysets=querysets) records = combined_recent(limit=limit, querysets=querysets)
......
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
...@@ -8,11 +9,11 @@ from . import serializers, utils ...@@ -8,11 +9,11 @@ from . import serializers, utils
class ActivityViewSet(viewsets.GenericViewSet): class ActivityViewSet(viewsets.GenericViewSet):
serializer_class = serializers.AutoSerializer serializer_class = serializers.AutoSerializer
permission_classes = [ConditionalAuthentication] permission_classes = [ConditionalAuthentication]
queryset = TrackFavorite.objects.none() queryset = TrackFavorite.objects.none()
@extend_schema(operation_id="get_activity")
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
activity = utils.get_activity(user=request.user) activity = utils.get_activity(user=request.user)
serializer = self.serializer_class(activity, many=True) serializer = self.serializer_class(activity, many=True)
......
...@@ -2,7 +2,7 @@ import uuid ...@@ -2,7 +2,7 @@ import uuid
import factory import factory
from funkwhale_api.factories import registry, NoUpdateOnCreate from funkwhale_api.factories import NoUpdateOnCreate, registry
from funkwhale_api.federation import actors from funkwhale_api.federation import actors
from funkwhale_api.federation import factories as federation_factories from funkwhale_api.federation import factories as federation_factories
from funkwhale_api.music import factories as music_factories from funkwhale_api.music import factories as music_factories
...@@ -15,7 +15,7 @@ def set_actor(o): ...@@ -15,7 +15,7 @@ def set_actor(o):
def get_rss_channel_name(): def get_rss_channel_name():
return "rssfeed-{}".format(uuid.uuid4()) return f"rssfeed-{uuid.uuid4()}"
@registry.register @registry.register
......
from django.db.models import Q
import django_filters import django_filters
from django.db.models import Q
from funkwhale_api.common import fields from funkwhale_api.common import fields
from funkwhale_api.common import filters as common_filters from funkwhale_api.common import filters as common_filters
...@@ -22,7 +21,11 @@ TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags) ...@@ -22,7 +21,11 @@ TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
class ChannelFilter(moderation_filters.HiddenContentFilterSet): class ChannelFilter(moderation_filters.HiddenContentFilterSet):
q = fields.SearchFilter( q = fields.SearchFilter(
search_fields=["artist__name", "actor__summary", "actor__preferred_username"] search_fields=[
"artist_credit__artist__name",
"actor__summary",
"actor__preferred_username",
]
) )
tag = TAG_FILTER tag = TAG_FILTER
scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True) scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
...@@ -41,7 +44,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet): ...@@ -41,7 +44,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
class Meta: class Meta:
model = models.Channel model = models.Channel
fields = ["q", "scope", "tag", "subscribed", "ordering", "external"] fields = []
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["CHANNEL"] hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["CHANNEL"]
def filter_subscribed(self, queryset, name, value): def filter_subscribed(self, queryset, name, value):
...@@ -54,7 +57,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet): ...@@ -54,7 +57,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
query = Q(actor__in=emitted_follows.values_list("target", flat=True)) query = Q(actor__in=emitted_follows.values_list("target", flat=True))
if value is True: if value:
return queryset.filter(query) return queryset.filter(query)
else: else:
return queryset.exclude(query) return queryset.exclude(query)
...@@ -64,9 +67,9 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet): ...@@ -64,9 +67,9 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
attributed_to=actors.get_service_actor(), attributed_to=actors.get_service_actor(),
actor__preferred_username__startswith="rssfeed-", actor__preferred_username__startswith="rssfeed-",
) )
if value is True: if value:
queryset = queryset.filter(query) queryset = queryset.filter(query)
if value is False: else:
queryset = queryset.exclude(query) queryset = queryset.exclude(query)
return queryset return queryset
......
# Generated by Django 3.2.13 on 2022-06-27 19:15
import django.core.serializers.json
from django.db import migrations, models
import funkwhale_api.audio.models
class Migration(migrations.Migration):
dependencies = [
('audio', '0003_channel_rss_url'),
]
operations = [
migrations.AlterField(
model_name='channel',
name='metadata',
field=models.JSONField(blank=True, default=funkwhale_api.audio.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
]
import uuid import uuid
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import JSONField
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.db import models
from django.urls import reverse from django.db.models import JSONField
from django.utils import timezone
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
from funkwhale_api.federation import keys from funkwhale_api.federation import keys
from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import models as federation_models
...@@ -81,7 +80,7 @@ class Channel(models.Model): ...@@ -81,7 +80,7 @@ class Channel(models.Model):
return self.actor.fid return self.actor.fid
@property @property
def is_local(self): def is_local(self) -> bool:
return self.actor.is_local return self.actor.is_local
@property @property
...@@ -94,7 +93,7 @@ class Channel(models.Model): ...@@ -94,7 +93,7 @@ class Channel(models.Model):
suffix = self.actor.preferred_username suffix = self.actor.preferred_username
else: else:
suffix = self.actor.full_username suffix = self.actor.full_username
return federation_utils.full_url("/channels/{}".format(suffix)) return federation_utils.full_url(f"/channels/{suffix}")
def get_rss_url(self): def get_rss_url(self):
if not self.artist.is_local or self.is_external_rss: if not self.artist.is_local or self.is_external_rss:
......
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from rest_framework import negotiation from rest_framework import negotiation, renderers
from rest_framework import renderers
from funkwhale_api.subsonic.renderers import dict_to_xml_tree from funkwhale_api.subsonic.renderers import dict_to_xml_tree
...@@ -17,6 +16,7 @@ class PodcastRSSRenderer(renderers.JSONRenderer): ...@@ -17,6 +16,7 @@ class PodcastRSSRenderer(renderers.JSONRenderer):
"version": "2.0", "version": "2.0",
"xmlns:atom": "http://www.w3.org/2005/Atom", "xmlns:atom": "http://www.w3.org/2005/Atom",
"xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd", "xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd",
"xmlns:content": "http://purl.org/rss/1.0/modules/content/",
"xmlns:media": "http://search.yahoo.com/mrss/", "xmlns:media": "http://search.yahoo.com/mrss/",
} }
final.update(data) final.update(data)
...@@ -26,7 +26,6 @@ class PodcastRSSRenderer(renderers.JSONRenderer): ...@@ -26,7 +26,6 @@ class PodcastRSSRenderer(renderers.JSONRenderer):
class PodcastRSSContentNegociation(negotiation.DefaultContentNegotiation): class PodcastRSSContentNegociation(negotiation.DefaultContentNegotiation):
def select_renderer(self, request, renderers, format_suffix=None): def select_renderer(self, request, renderers, format_suffix=None):
return (PodcastRSSRenderer(), PodcastRSSRenderer.media_type) return (PodcastRSSRenderer(), PodcastRSSRenderer.media_type)
......