common.py 45.1 KB
Newer Older
1
2
3
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

Agate's avatar
Agate committed
4
from collections import OrderedDict
5
6
import logging.config
import sys
7

8
from urllib.parse import urlsplit
9

10
import environ
11
12
from celery.schedules import crontab

13
logger = logging.getLogger("funkwhale_api.config")
14
ROOT_DIR = environ.Path(__file__) - 3  # (/a/b/myfile.py - 3 = /)
Eliot Berriot's avatar
Eliot Berriot committed
15
APPS_DIR = ROOT_DIR.path("funkwhale_api")
16
17

env = environ.Env()
Agate's avatar
Agate committed
18
ENV = env
19
LOGLEVEL = env("LOGLEVEL", default="info").upper()
Agate's avatar
Agate committed
20
21
22
"""
Default logging level for the Funkwhale processes"""  # pylint: disable=W0105

23
24
25
26
27
28
29
30
31
32
33
34
35
LOGGING_CONFIG = None
logging.config.dictConfig(
    {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "console": {"format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s"}
        },
        "handlers": {
            "console": {"class": "logging.StreamHandler", "formatter": "console"},
        },
        "loggers": {
            "funkwhale_api": {
Eliot Berriot's avatar
Eliot Berriot committed
36
                "level": LOGLEVEL,
37
38
39
40
                "handlers": ["console"],
                # required to avoid double logging with root logger
                "propagate": False,
            },
Agate's avatar
Agate committed
41
42
43
44
45
46
            "plugins": {
                "level": LOGLEVEL,
                "handlers": ["console"],
                # required to avoid double logging with root logger
                "propagate": False,
            },
47
48
49
50
51
            "": {"level": "WARNING", "handlers": ["console"]},
        },
    }
)

Agate's avatar
Agate committed
52
53
54
55
ENV_FILE = env_file = env("ENV_FILE", default=None)
"""
Path to a .env file to load
"""
56
if env_file:
57
    logger.info("Loading specified env file at %s", env_file)
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    # we have an explicitely specified env file
    # so we try to load and it fail loudly if it does not exist
    env.read_env(env_file)
else:
    # we try to load from .env and config/.env
    # but do not crash if those files don't exist
    paths = [
        # /srv/funwhale/api/.env
        ROOT_DIR,
        # /srv/funwhale/config/.env
        ((ROOT_DIR - 1) + "config"),
    ]
    for path in paths:
        try:
            env_path = path.file(".env")
        except FileNotFoundError:
            logger.debug("No env file found at %s/.env", path)
            continue
        env.read_env(env_path)
        logger.info("Loaded env file at %s/.env", path)
        break
79

80
81
82
FUNKWHALE_PLUGINS_PATH = env(
    "FUNKWHALE_PLUGINS_PATH", default="/srv/funkwhale/plugins/"
)
Agate's avatar
Agate committed
83
"""
jovuit's avatar
jovuit committed
84
85
Path to a directory containing Funkwhale plugins.
These will be imported at runtime.
Agate's avatar
Agate committed
86
"""
87
sys.path.append(FUNKWHALE_PLUGINS_PATH)
Agate's avatar
Agate committed
88
89
CORE_PLUGINS = [
    "funkwhale_api.contrib.scrobbler",
Philipp Wolfer's avatar
Philipp Wolfer committed
90
    "funkwhale_api.contrib.listenbrainz",
91
    "funkwhale_api.contrib.maloja",
Agate's avatar
Agate committed
92
]
93

Agate's avatar
Agate committed
94
LOAD_CORE_PLUGINS = env.bool("FUNKWHALE_LOAD_CORE_PLUGINS", default=True)
Agate's avatar
Agate committed
95
96
97
98
PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
"""
List of Funkwhale plugins to load.
"""
Agate's avatar
Agate committed
99
100
101
102
103
104
if LOAD_CORE_PLUGINS:
    PLUGINS = CORE_PLUGINS + PLUGINS

# Remove duplicates
PLUGINS = list(OrderedDict.fromkeys(PLUGINS))

Agate's avatar
Agate committed
105
106
107
108
109
110
111
112
113
114
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)
115
FUNKWHALE_HOSTNAME = None
Eliot Berriot's avatar
Eliot Berriot committed
116
117
FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
118
119
if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
    # We're in traefik case, in development
Eliot Berriot's avatar
Eliot Berriot committed
120
121
122
123
    FUNKWHALE_HOSTNAME = "{}.{}".format(
        FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX
    )
    FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
124
125
else:
    try:
Eliot Berriot's avatar
Eliot Berriot committed
126
        FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
Agate's avatar
Agate committed
127
        """
jovuit's avatar
jovuit committed
128
        Hostname of your Funkwhale pod, e.g. ``mypod.audio``
Agate's avatar
Agate committed
129
130
        """

Eliot Berriot's avatar
Eliot Berriot committed
131
        FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
Agate's avatar
Agate committed
132
        """
jovuit's avatar
jovuit committed
133
134
        Protocol end users will use to access your pod, either
        ``http`` or ``https``.
Agate's avatar
Agate committed
135
        """
136
    except Exception:
Eliot Berriot's avatar
Eliot Berriot committed
137
        FUNKWHALE_URL = env("FUNKWHALE_URL")
138
139
140
141
        _parsed = urlsplit(FUNKWHALE_URL)
        FUNKWHALE_HOSTNAME = _parsed.netloc
        FUNKWHALE_PROTOCOL = _parsed.scheme

142
143
FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower()
FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower()
Eliot Berriot's avatar
Eliot Berriot committed
144
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
145
146
147
FUNKWHALE_SPA_HTML_ROOT = env(
    "FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL + "/front/"
)
Agate's avatar
Agate committed
148
"""
jovuit's avatar
jovuit committed
149
150
151
URL or path to the Web Application files.
Funkwhale needs access to it so that it can inject <meta> tags relevant
to the given page (e.g page title, cover, etc.).
Agate's avatar
Agate committed
152

jovuit's avatar
jovuit committed
153
154
If a URL is specified, the index.html file will be fetched through HTTP.
If a path is provided,
Agate's avatar
Agate committed
155
156
it will be accessed from disk.

jovuit's avatar
jovuit committed
157
158
Use something like ``/srv/funkwhale/front/dist/`` if the web processes shows
request errors related to this.
Agate's avatar
Agate committed
159
160
"""

161
162
163
164
FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int(
    "FUNKWHALE_SPA_HTML_CACHE_DURATION", default=60 * 15
)
FUNKWHALE_EMBED_URL = env(
165
    "FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/front/embed.html"
166
)
167
168
169
170
171
172
173
FUNKWHALE_SPA_REWRITE_MANIFEST = env.bool(
    "FUNKWHALE_SPA_REWRITE_MANIFEST", default=True
)
FUNKWHALE_SPA_REWRITE_MANIFEST_URL = env.bool(
    "FUNKWHALE_SPA_REWRITE_MANIFEST_URL", default=None
)

174
APP_NAME = "Funkwhale"
175

176
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower()
Eliot Berriot's avatar
Eliot Berriot committed
177
178
179
FEDERATION_SERVICE_ACTOR_USERNAME = env(
    "FEDERATION_SERVICE_ACTOR_USERNAME", default="service"
)
180
181
# How many pages to fetch when crawling outboxes and third-party collections
FEDERATION_COLLECTION_MAX_PAGES = env.int("FEDERATION_COLLECTION_MAX_PAGES", default=5)
Agate's avatar
Agate committed
182
"""
jovuit's avatar
jovuit committed
183
184
Number of existing pages of content to fetch when discovering/refreshing an
actor or channel.
Agate's avatar
Agate committed
185
186
187
188

More pages means more content will be loaded, but will require more resources.
"""

189
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[]) + [FUNKWHALE_HOSTNAME]
Agate's avatar
Agate committed
190
191
192
"""
List of allowed hostnames for which the Funkwhale server will answer.
"""
193

194
195
196
# APP CONFIGURATION
# ------------------------------------------------------------------------------
DJANGO_APPS = (
Eliot Berriot's avatar
Eliot Berriot committed
197
    "channels",
198
    # Default Django apps:
Eliot Berriot's avatar
Eliot Berriot committed
199
200
201
202
203
204
205
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.sites",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.postgres",
206
207
208
    # Useful template tags:
    # 'django.contrib.humanize',
    # Admin
Eliot Berriot's avatar
Eliot Berriot committed
209
    "django.contrib.admin",
210
211
212
)
THIRD_PARTY_APPS = (
    # 'crispy_forms',  # Form layouts
Eliot Berriot's avatar
Eliot Berriot committed
213
214
215
216
    "allauth",  # registration
    "allauth.account",  # registration
    "allauth.socialaccount",  # registration
    "corsheaders",
217
    "oauth2_provider",
Eliot Berriot's avatar
Eliot Berriot committed
218
219
220
221
222
223
224
    "rest_framework",
    "rest_framework.authtoken",
    "rest_auth",
    "rest_auth.registration",
    "dynamic_preferences",
    "django_filters",
    "django_cleanup",
Eliot Berriot's avatar
Eliot Berriot committed
225
    "versatileimagefield",
226
227
)

Eliot Berriot's avatar
Eliot Berriot committed
228

229
230
# Apps specific for this project go here.
LOCAL_APPS = (
231
    "funkwhale_api.common.apps.CommonConfig",
Eliot Berriot's avatar
Eliot Berriot committed
232
233
    "funkwhale_api.activity.apps.ActivityConfig",
    "funkwhale_api.users",  # custom users app
234
    "funkwhale_api.users.oauth",
235
    # Your stuff: custom apps go here
Eliot Berriot's avatar
Eliot Berriot committed
236
    "funkwhale_api.instance",
237
    "funkwhale_api.audio",
Eliot Berriot's avatar
Eliot Berriot committed
238
239
240
241
    "funkwhale_api.music",
    "funkwhale_api.requests",
    "funkwhale_api.favorites",
    "funkwhale_api.federation",
242
    "funkwhale_api.moderation.apps.ModerationConfig",
Eliot Berriot's avatar
Eliot Berriot committed
243
244
245
246
    "funkwhale_api.radios",
    "funkwhale_api.history",
    "funkwhale_api.playlists",
    "funkwhale_api.subsonic",
Eliot Berriot's avatar
Eliot Berriot committed
247
    "funkwhale_api.tags",
248
249
250
)

# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
Eliot Berriot's avatar
Eliot Berriot committed
251

252
ADDITIONAL_APPS = env.list("ADDITIONAL_APPS", default=[])
Agate's avatar
Agate committed
253
254
255
"""
List of Django apps to load in addition to Funkwhale plugins and apps.
"""
256
257
258
259
INSTALLED_APPS = (
    DJANGO_APPS
    + THIRD_PARTY_APPS
    + LOCAL_APPS
260
    + tuple(ADDITIONAL_APPS)
Agate's avatar
Agate committed
261
    + tuple(plugins.trigger_filter(plugins.PLUGINS_APPS, [], enabled=True))
262
)
263
264
265

# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
266
ADDITIONAL_MIDDLEWARES_BEFORE = env.list("ADDITIONAL_MIDDLEWARES_BEFORE", default=[])
Agate's avatar
Agate committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
MIDDLEWARE = (
    tuple(plugins.trigger_filter(plugins.MIDDLEWARES_BEFORE, [], enabled=True))
    + tuple(ADDITIONAL_MIDDLEWARES_BEFORE)
    + (
        "django.middleware.security.SecurityMiddleware",
        "django.middleware.clickjacking.XFrameOptionsMiddleware",
        "corsheaders.middleware.CorsMiddleware",
        # needs to be before SPA middleware
        "django.contrib.sessions.middleware.SessionMiddleware",
        "django.middleware.common.CommonMiddleware",
        "django.middleware.csrf.CsrfViewMiddleware",
        # /end
        "funkwhale_api.common.middleware.SPAFallbackMiddleware",
        "django.contrib.auth.middleware.AuthenticationMiddleware",
        "django.contrib.messages.middleware.MessageMiddleware",
        "funkwhale_api.users.middleware.RecordActivityMiddleware",
        "funkwhale_api.common.middleware.ThrottleStatusMiddleware",
    )
    + tuple(plugins.trigger_filter(plugins.MIDDLEWARES_AFTER, [], enabled=True))
286
287
288
289
290
)

# DEBUG
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
Agate's avatar
Agate committed
291
292
DJANGO_DEBUG = DEBUG = env.bool("DJANGO_DEBUG", False)
"""
jovuit's avatar
jovuit committed
293
294
295
Whether to enable debugging info and pages.
Never enable this on a production server, as it can leak very sensitive
information.
Agate's avatar
Agate committed
296
"""
297
298
# FIXTURE CONFIGURATION
# ------------------------------------------------------------------------------
jovuit's avatar
jovuit committed
299
300
# See:
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
Eliot Berriot's avatar
Eliot Berriot committed
301
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
302
303
304

# EMAIL CONFIGURATION
# ------------------------------------------------------------------------------
305
306
307
308

# EMAIL
# ------------------------------------------------------------------------------
DEFAULT_FROM_EMAIL = env(
Eliot Berriot's avatar
Eliot Berriot committed
309
310
    "DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME)
)
Agate's avatar
Agate committed
311
"""
312
Name and e-mail address used to send system e-mails.
Agate's avatar
Agate committed
313
314
315
316
317
318
319

Default: ``Funkwhale <noreply@yourdomain>``

.. note::

    Both the forms ``Funkwhale <noreply@yourdomain>`` and
    ``noreply@yourdomain`` work.
320

Agate's avatar
Agate committed
321
"""
Eliot Berriot's avatar
Eliot Berriot committed
322
EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
Agate's avatar
Agate committed
323
"""
324
Subject prefix for system e-mails.
Agate's avatar
Agate committed
325
"""
Eliot Berriot's avatar
Eliot Berriot committed
326
SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
327
328


Eliot Berriot's avatar
Eliot Berriot committed
329
EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://")
Agate's avatar
Agate committed
330
"""
331
SMTP configuration for sending e-mails. Possible values:
Agate's avatar
Agate committed
332

333
334
- ``EMAIL_CONFIG=consolemail://``: output e-mails to console (the default)
- ``EMAIL_CONFIG=dummymail://``: disable e-mail sending completely
Agate's avatar
Agate committed
335
336

On a production instance, you'll usually want to use an external SMTP server:
337

Agate's avatar
Agate committed
338
339
340
341
342
343
344
345
346
- ``EMAIL_CONFIG=smtp://user@:password@youremail.host:25``
- ``EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465``
- ``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:
jovuit's avatar
jovuit committed
347
348
    ``python3 -c 'import urllib.parse; print(urllib.parse.quote_plus
    ("noreply@youremail.host"))'``
Agate's avatar
Agate committed
349
350
351
    (returns ``noreply%40youremail.host``)

"""
352
vars().update(EMAIL_CONFIG)
353
354
355
356

# DATABASE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
Agate's avatar
Agate committed
357
358
359
360
361
362
363
364
DATABASE_URL = env.db("DATABASE_URL")
"""
URL to connect to the PostgreSQL database. Examples:

- ``postgresql://funkwhale@:5432/funkwhale``
- ``postgresql://<user>:<password>@<host>:<port>/<database>``
- ``postgresql://funkwhale:passw0rd@localhost:5432/funkwhale_database``
"""
365
366
DATABASES = {
    # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
Agate's avatar
Agate committed
367
    "default": DATABASE_URL
368
}
Eliot Berriot's avatar
Eliot Berriot committed
369
DATABASES["default"]["ATOMIC_REQUESTS"] = True
Agate's avatar
Agate committed
370
371
372
373
374
375
DB_CONN_MAX_AGE = DATABASES["default"]["CONN_MAX_AGE"] = env(
    "DB_CONN_MAX_AGE", default=60 * 5
)
"""
Max time, in seconds, before database connections are closed.
"""
376
377
378
379
MIGRATION_MODULES = {
    # see https://github.com/jazzband/django-oauth-toolkit/issues/634
    # swappable models are badly designed in oauth2_provider
    # ignore migrations and provide our own models.
380
381
    "oauth2_provider": None,
    "sites": "funkwhale_api.contrib.sites.migrations",
382
383
}

384
385
386
387
388
389
# GENERAL CONFIGURATION
# ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
Eliot Berriot's avatar
Eliot Berriot committed
390
TIME_ZONE = "UTC"
391
392

# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
Eliot Berriot's avatar
Eliot Berriot committed
393
LANGUAGE_CODE = "en-us"
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411

# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N = True

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True

# TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
    {
jovuit's avatar
jovuit committed
412
413
        # See:
        # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
Eliot Berriot's avatar
Eliot Berriot committed
414
        "BACKEND": "django.template.backends.django.DjangoTemplates",
jovuit's avatar
jovuit committed
415
416
        # See:
        # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
Eliot Berriot's avatar
Eliot Berriot committed
417
418
        "DIRS": [str(APPS_DIR.path("templates"))],
        "OPTIONS": {
jovuit's avatar
jovuit committed
419
420
            # See:
            # https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
Eliot Berriot's avatar
Eliot Berriot committed
421
            "debug": DEBUG,
jovuit's avatar
jovuit committed
422
423
            # See:
            # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
424
            # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
Eliot Berriot's avatar
Eliot Berriot committed
425
426
427
            "loaders": [
                "django.template.loaders.filesystem.Loader",
                "django.template.loaders.app_directories.Loader",
428
            ],
jovuit's avatar
jovuit committed
429
430
            # See:
            # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
Eliot Berriot's avatar
Eliot Berriot committed
431
432
433
434
435
436
437
438
439
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.template.context_processors.i18n",
                "django.template.context_processors.media",
                "django.template.context_processors.static",
                "django.template.context_processors.tz",
                "django.contrib.messages.context_processors.messages",
440
441
442
                # Your stuff: custom template context processors go here
            ],
        },
Eliot Berriot's avatar
Eliot Berriot committed
443
    }
444
445
]

jovuit's avatar
jovuit committed
446
447
# See:
# http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
Eliot Berriot's avatar
Eliot Berriot committed
448
CRISPY_TEMPLATE_PACK = "bootstrap3"
449
450
451
452

# STATIC FILE CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
Eliot Berriot's avatar
Eliot Berriot committed
453
STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
Agate's avatar
Agate committed
454
455
456
"""
Path were static files should be collected.
"""
457
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
458
STATIC_URL = env("STATIC_URL", default=FUNKWHALE_URL + "/staticfiles/")
Eliot Berriot's avatar
Eliot Berriot committed
459
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage"
460

461
PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True)
Agate's avatar
Agate committed
462
"""
jovuit's avatar
jovuit committed
463
464
465
Wether to proxy audio files through your reverse proxy.
It's recommended to keep this on, as a way to enforce access control, however,
if you're using S3 storage with :attr:`AWS_QUERYSTRING_AUTH`,
Agate's avatar
Agate committed
466
467
it's safe to disable it.
"""
468
AWS_DEFAULT_ACL = None
469
AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA)
Agate's avatar
Agate committed
470
471
472
473
474
475
"""
Whether to include signatures in S3 urls, as a way to enforce access-control.

Defaults to the inverse of :attr:`PROXY_MEDIA`.
"""

476
477
478
AWS_S3_MAX_MEMORY_SIZE = env.int(
    "AWS_S3_MAX_MEMORY_SIZE", default=1000 * 1000 * 1000 * 20
)
Agate's avatar
Agate committed
479

480
AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600)
Agate's avatar
Agate committed
481
"""
jovuit's avatar
jovuit committed
482
483
Expiration delay, in seconds, of signatures generated when
:attr:`AWS_QUERYSTRING_AUTH` is enabled.
Agate's avatar
Agate committed
484
485
"""

486
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)
Agate's avatar
Agate committed
487
488
489
"""
Access-key ID for your S3 storage.
"""
490
491
492
493

if AWS_ACCESS_KEY_ID:
    AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
Agate's avatar
Agate committed
494
495
496
    """
    Secret access key for your S3 storage.
    """
497
    AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
Agate's avatar
Agate committed
498
499
500
    """
    Bucket name of your S3 storage.
    """
501
    AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", default=None)
Agate's avatar
Agate committed
502
503
504
    """
    Custom domain to use for your S3 storage.
    """
505
    AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None)
Agate's avatar
Agate committed
506
    """
jovuit's avatar
jovuit committed
507
508
    If you use a S3-compatible storage such as minio,
    set the following variable to the full URL to the storage server. Example:
Agate's avatar
Agate committed
509
510
511
512

    - ``https://minio.mydomain.com``
    - ``https://s3.wasabisys.com``
    """
513
    AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None)
jovuit's avatar
jovuit committed
514
515
516
517
    """If you are using Amazon S3 to serve media directly,
    you will need to specify your region name in order to access files.

    Example:
Agate's avatar
Agate committed
518
519
520
521

    - ``eu-west-2``
    """

522
    AWS_S3_SIGNATURE_VERSION = "s3v4"
523
    AWS_LOCATION = env("AWS_LOCATION", default="")
Agate's avatar
Agate committed
524
    """
jovuit's avatar
jovuit committed
525
526
527
    An optional bucket subdirectory were you want to store the files.
    This is especially useful if you plan to use share the bucket with other
    services.
Agate's avatar
Agate committed
528
    """
529
    DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIS3Boto3Storage"
530

Agate's avatar
Agate committed
531

jovuit's avatar
jovuit committed
532
533
# See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
Eliot Berriot's avatar
Eliot Berriot committed
534
STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
535

jovuit's avatar
jovuit committed
536
537
# See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
538
STATICFILES_FINDERS = (
Eliot Berriot's avatar
Eliot Berriot committed
539
540
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
541
542
543
544
545
)

# MEDIA CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
Eliot Berriot's avatar
Eliot Berriot committed
546
MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media")))
Agate's avatar
Agate committed
547
"""
jovuit's avatar
jovuit committed
548
549
Path where media files (such as album covers or audio tracks) are stored
on your system. Ensure this directory actually exists.
Agate's avatar
Agate committed
550
"""
551
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
552
MEDIA_URL = env("MEDIA_URL", default=FUNKWHALE_URL + "/media/")
Agate's avatar
Agate committed
553
554
"""
URL where media files are served. The default value should work fine on most
jovuit's avatar
jovuit committed
555
556
configurations, but could can tweak this if you are hosting media
files on a separate domain, or if you host Funkwhale on a non-standard port.
Agate's avatar
Agate committed
557
"""
558
FILE_UPLOAD_PERMISSIONS = 0o644
Eliot Berriot's avatar
Eliot Berriot committed
559
560
561
562

ATTACHMENTS_UNATTACHED_PRUNE_DELAY = env.int(
    "ATTACHMENTS_UNATTACHED_PRUNE_DELAY", default=3600 * 24
)
Agate's avatar
Agate committed
563
"""
jovuit's avatar
jovuit committed
564
565
Delay in seconds before uploaded but unattached attachements are pruned
from the system.
Agate's avatar
Agate committed
566
"""
Eliot Berriot's avatar
Eliot Berriot committed
567

568
569
# URL Configuration
# ------------------------------------------------------------------------------
Eliot Berriot's avatar
Eliot Berriot committed
570
ROOT_URLCONF = "config.urls"
571
SPA_URLCONF = "config.spa_urls"
Eliot Berriot's avatar
Eliot Berriot committed
572
ASGI_APPLICATION = "config.routing.application"
573

574
# This ensures that Django will be able to detect a secure connection
Eliot Berriot's avatar
Eliot Berriot committed
575
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
576
577
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
578

579
580
581
# AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTHENTICATION_BACKENDS = (
582
    "funkwhale_api.users.auth_backends.ModelBackend",
583
    "funkwhale_api.users.auth_backends.AllAuthBackend",
584
)
585
SESSION_COOKIE_HTTPONLY = False
Agate's avatar
Agate committed
586
587
SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", default=3600 * 25 * 60)

588
# Some really nice defaults
Eliot Berriot's avatar
Eliot Berriot committed
589
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
590
ACCOUNT_EMAIL_REQUIRED = True
591
592
593
ACCOUNT_EMAIL_VERIFICATION_ENFORCE = env.bool(
    "ACCOUNT_EMAIL_VERIFICATION_ENFORCE", default=False
)
Agate's avatar
Agate committed
594
"""
595
596
597
598
599
Determine wether users need to verify their e-mail address before using the service. Enabling this can be useful
to reduce spam or bots accounts, however, you'll need to configure a mail server so that your users can receive the
verification e-mails, using :attr:`EMAIL_CONFIG`.

Note that regardless of the setting value, superusers created through the command line will never require verification.
Agate's avatar
Agate committed
600

jovuit's avatar
jovuit committed
601
602
Note that regardless of the setting value, superusers created through the
command line will never require verification.
Agate's avatar
Agate committed
603
"""
604
605
606
ACCOUNT_EMAIL_VERIFICATION = (
    "mandatory" if ACCOUNT_EMAIL_VERIFICATION_ENFORCE else "optional"
)
607
ACCOUNT_USERNAME_VALIDATORS = "funkwhale_api.users.serializers.username_validators"
608
609
610

# Custom user app defaults
# Select the correct user model
Eliot Berriot's avatar
Eliot Berriot committed
611
612
613
AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "users:redirect"
LOGIN_URL = "account_login"
614

615
616
617
618
619
620
621
622
623
# OAuth configuration
from funkwhale_api.users.oauth import scopes  # noqa

OAUTH2_PROVIDER = {
    "SCOPES": {s.id: s.label for s in scopes.SCOPES_BY_ID.values()},
    "ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https", "urn"],
    # we keep expired tokens for 15 days, for tracability
    "REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 15,
    "AUTHORIZATION_CODE_EXPIRE_SECONDS": 5 * 60,
624
625
626
    "ACCESS_TOKEN_EXPIRE_SECONDS": env.int(
        "ACCESS_TOKEN_EXPIRE_SECONDS", default=60 * 60 * 10
    ),
Eliot Berriot's avatar
Eliot Berriot committed
627
    "OAUTH2_SERVER_CLASS": "funkwhale_api.users.oauth.server.OAuth2Server",
628
629
630
631
632
}
OAUTH2_PROVIDER_APPLICATION_MODEL = "users.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken"
OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken"
Georg Krause's avatar
Georg Krause committed
633
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "users.IdToken"
634

635
636
SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3

637
638
639
# LDAP AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False)
Agate's avatar
Agate committed
640
"""
jovuit's avatar
jovuit committed
641
642
643
Wether to enable LDAP authentication.

See :doc:`/installation/ldap` for more information.
Agate's avatar
Agate committed
644
645
"""

646
647
if AUTH_LDAP_ENABLED:

jovuit's avatar
jovuit committed
648
649
    # Import the LDAP modules here.
    # This way, we don't need the dependency unless someone
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
    # actually enables the LDAP support
    import ldap
    from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion, GroupOfNamesType

    # Add LDAP to the authentication backends
    AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",)

    # Basic configuration
    AUTH_LDAP_SERVER_URI = env("LDAP_SERVER_URI")
    AUTH_LDAP_BIND_DN = env("LDAP_BIND_DN", default="")
    AUTH_LDAP_BIND_PASSWORD = env("LDAP_BIND_PASSWORD", default="")
    AUTH_LDAP_SEARCH_FILTER = env("LDAP_SEARCH_FILTER", default="(uid={0})").format(
        "%(user)s"
    )
    AUTH_LDAP_START_TLS = env.bool("LDAP_START_TLS", default=False)
665
666
667
    AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = env(
        "AUTH_LDAP_BIND_AS_AUTHENTICATING_USER", default=False
    )
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711

    DEFAULT_USER_ATTR_MAP = [
        "first_name:givenName",
        "last_name:sn",
        "username:cn",
        "email:mail",
    ]
    LDAP_USER_ATTR_MAP = env.list("LDAP_USER_ATTR_MAP", default=DEFAULT_USER_ATTR_MAP)
    AUTH_LDAP_USER_ATTR_MAP = {}
    for m in LDAP_USER_ATTR_MAP:
        funkwhale_field, ldap_field = m.split(":")
        AUTH_LDAP_USER_ATTR_MAP[funkwhale_field.strip()] = ldap_field.strip()

    # Determine root DN supporting multiple root DNs
    AUTH_LDAP_ROOT_DN = env("LDAP_ROOT_DN")
    AUTH_LDAP_ROOT_DN_LIST = []
    for ROOT_DN in AUTH_LDAP_ROOT_DN.split():
        AUTH_LDAP_ROOT_DN_LIST.append(
            LDAPSearch(ROOT_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
        )
    # Search for the user in all the root DNs
    AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_ROOT_DN_LIST)

    # Search for group types
    LDAP_GROUP_DN = env("LDAP_GROUP_DN", default="")
    if LDAP_GROUP_DN:
        AUTH_LDAP_GROUP_DN = LDAP_GROUP_DN
        # Get filter
        AUTH_LDAP_GROUP_FILTER = env("LDAP_GROUP_FILER", default="")
        # Search for the group in the specified DN
        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
            AUTH_LDAP_GROUP_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_FILTER
        )
        AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()

        # Configure basic group support
        LDAP_REQUIRE_GROUP = env("LDAP_REQUIRE_GROUP", default="")
        if LDAP_REQUIRE_GROUP:
            AUTH_LDAP_REQUIRE_GROUP = LDAP_REQUIRE_GROUP
        LDAP_DENY_GROUP = env("LDAP_DENY_GROUP", default="")
        if LDAP_DENY_GROUP:
            AUTH_LDAP_DENY_GROUP = LDAP_DENY_GROUP


712
# SLUGLIFIER
Eliot Berriot's avatar
Eliot Berriot committed
713
AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
714

715
CACHE_DEFAULT = "redis://127.0.0.1:6379/0"
Agate's avatar
Agate committed
716
717
718
719
CACHE_URL = env.cache_url("CACHE_URL", default=CACHE_DEFAULT)
"""
URL to your redis server. Examples:

jovuit's avatar
jovuit committed
720
721
722
723
724
- ``redis://<host>:<port>/<database>``
- ``redis://127.0.0.1:6379/0``
- ``redis://:password@localhost:6379/0``
for password auth (the extra semicolon is important)
- ``redis:///run/redis/redis.sock?db=0`` over unix sockets
Agate's avatar
Agate committed
725
726
727

.. note::

jovuit's avatar
jovuit committed
728
729
    If you want to use Redis over unix sockets, you'll also need to update
    :attr:`CELERY_BROKER_URL`
Agate's avatar
Agate committed
730
731

"""
732
CACHES = {
Agate's avatar
Agate committed
733
    "default": CACHE_URL,
734
735
736
737
738
    "local": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "local-cache",
    },
}
739
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
Eliot Berriot's avatar
Eliot Berriot committed
740

Eliot Berriot's avatar
Eliot Berriot committed
741
742
743
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
744
        "CONFIG": {"hosts": [CACHES["default"]["LOCATION"]]},
Eliot Berriot's avatar
Eliot Berriot committed
745
    }
Eliot Berriot's avatar
Eliot Berriot committed
746
747
}

748
CACHES["default"]["OPTIONS"] = {
749
    "CLIENT_CLASS": "funkwhale_api.common.cache.RedisClient",
750
    "IGNORE_EXCEPTIONS": True,  # mimics memcache behavior.
Eliot Berriot's avatar
Eliot Berriot committed
751
    # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
752
}
Eliot Berriot's avatar
Eliot Berriot committed
753
CACHEOPS_DURATION = env("CACHEOPS_DURATION", default=0)
754
755
756
757
CACHEOPS_ENABLED = bool(CACHEOPS_DURATION)

if CACHEOPS_ENABLED:
    INSTALLED_APPS += ("cacheops",)
Agate's avatar
Agate committed
758
    CACHEOPS_REDIS = env("CACHE_URL", default=CACHE_DEFAULT)
759
760
761
762
763
764
765
    CACHEOPS_PREFIX = lambda _: "cacheops"  # noqa
    CACHEOPS_DEFAULTS = {"timeout": CACHEOPS_DURATION}
    CACHEOPS = {
        "music.album": {"ops": "count"},
        "music.artist": {"ops": "count"},
        "music.track": {"ops": "count"},
    }
766

767
# CELERY
Eliot Berriot's avatar
Eliot Berriot committed
768
INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
769
CELERY_BROKER_URL = env(
Eliot Berriot's avatar
Eliot Berriot committed
770
771
    "CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT)
)
Agate's avatar
Agate committed
772
"""
jovuit's avatar
jovuit committed
773
774
URL to celery's task broker. Defaults to :attr:`CACHE_URL`,
so you shouldn't have to tweak this, unless you want
Agate's avatar
Agate committed
775
776
777
778
to use a different one, or use Redis sockets to connect.

Exemple:

jovuit's avatar
jovuit committed
779
780
- ``redis://127.0.0.1:6379/0``
- ``redis+socket:///run/redis/redis.sock?virtual_host=0``
Agate's avatar
Agate committed
781
"""
782
# END CELERY
783
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
Eliot Berriot's avatar
Eliot Berriot committed
784

785
# Your common stuff: Below this line define 3rd party library settings
786
787
CELERY_TASK_DEFAULT_RATE_LIMIT = 1
CELERY_TASK_TIME_LIMIT = 300
788
CELERY_BEAT_SCHEDULE = {
789
790
791
792
793
    "audio.fetch_rss_feeds": {
        "task": "audio.fetch_rss_feeds",
        "schedule": crontab(minute="0", hour="*"),
        "options": {"expires": 60 * 60},
    },
Eliot Berriot's avatar
Eliot Berriot committed
794
795
796
797
798
    "common.prune_unattached_attachments": {
        "task": "common.prune_unattached_attachments",
        "schedule": crontab(minute="0", hour="*"),
        "options": {"expires": 60 * 60},
    },
Eliot Berriot's avatar
Eliot Berriot committed
799
    "federation.clean_music_cache": {
800
        "task": "federation.clean_music_cache",
801
        "schedule": crontab(minute="0", hour="*/2"),
Eliot Berriot's avatar
Eliot Berriot committed
802
        "options": {"expires": 60 * 2},
803
804
805
    },
    "music.clean_transcoding_cache": {
        "task": "music.clean_transcoding_cache",
806
        "schedule": crontab(minute="0", hour="*"),
807
808
        "options": {"expires": 60 * 2},
    },
809
810
811
812
813
    "oauth.clear_expired_tokens": {
        "task": "oauth.clear_expired_tokens",
        "schedule": crontab(minute="0", hour="0"),
        "options": {"expires": 60 * 60 * 24},
    },
814
815
    "federation.refresh_nodeinfo_known_nodes": {
        "task": "federation.refresh_nodeinfo_known_nodes",
816
817
818
819
820
821
        "schedule": crontab(
            **env.dict(
                "SCHEDULE_FEDERATION_REFRESH_NODEINFO_KNOWN_NODES",
                default={"minute": "0", "hour": "*"},
            )
        ),
822
823
        "options": {"expires": 60 * 60},
    },
824
825
}

826
827
828
829
830
831
832
833
834
835
836
837
838
839
if env.bool("ADD_ALBUM_TAGS_FROM_TRACKS", default=True):
    CELERY_BEAT_SCHEDULE["music.albums_set_tags_from_tracks"] = {
        "task": "music.albums_set_tags_from_tracks",
        "schedule": crontab(minute="0", hour="4", day_of_week="4"),
        "options": {"expires": 60 * 60 * 2},
    }

if env.bool("ADD_ARTIST_TAGS_FROM_TRACKS", default=True):
    CELERY_BEAT_SCHEDULE["music.artists_set_tags_from_tracks"] = {
        "task": "music.artists_set_tags_from_tracks",
        "schedule": crontab(minute="0", hour="4", day_of_week="4"),
        "options": {"expires": 60 * 60 * 2},
    }

840
841
NODEINFO_REFRESH_DELAY = env.int("NODEINFO_REFRESH_DELAY", default=3600 * 24)

842
843
844
845
846
847
848

def get_user_secret_key(user):
    from django.conf import settings

    return settings.SECRET_KEY + str(user.secret_key)


849
OLD_PASSWORD_FIELD_ENABLED = True
850
851
852
853
854
855
856
857
858
859
860
AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
        "OPTIONS": {"min_length": env.int("PASSWORD_MIN_LENGTH", default=8)},
    },
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
Agate's avatar
Agate committed
861
862
DISABLE_PASSWORD_VALIDATORS = env.bool("DISABLE_PASSWORD_VALIDATORS", default=False)
"""
jovuit's avatar
jovuit committed
863
864
Wether to disable password validators (length, common words,
similarity with username…) used during regitration.
Agate's avatar
Agate committed
865
866
"""
if DISABLE_PASSWORD_VALIDATORS:
867
    AUTH_PASSWORD_VALIDATORS = []
Eliot Berriot's avatar
Eliot Berriot committed
868
ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter"
869
870
871
872
873
874
CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_WHITELIST = (
#     'localhost',
#     'funkwhale.localhost',
# )
CORS_ALLOW_CREDENTIALS = True
875

876
REST_FRAMEWORK = {
Eliot Berriot's avatar
Eliot Berriot committed
877
878
879
880
881
882
883
    "DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination",
    "PAGE_SIZE": 25,
    "DEFAULT_PARSER_CLASSES": (
        "rest_framework.parsers.JSONParser",
        "rest_framework.parsers.FormParser",
        "rest_framework.parsers.MultiPartParser",
        "funkwhale_api.federation.parsers.ActivityParser",
884
    ),
Eliot Berriot's avatar
Eliot Berriot committed
885
    "DEFAULT_AUTHENTICATION_CLASSES": (
886
        "funkwhale_api.common.authentication.OAuth2Authentication",
887
        "funkwhale_api.common.authentication.ApplicationTokenAuthentication",
Eliot Berriot's avatar
Eliot Berriot committed
888
        "rest_framework.authentication.BasicAuthentication",
889
        "rest_framework.authentication.SessionAuthentication",
890
    ),
891
892
893
    "DEFAULT_PERMISSION_CLASSES": (
        "funkwhale_api.users.oauth.permissions.ScopePermission",
    ),
Eliot Berriot's avatar
Eliot Berriot committed
894
895
896
    "DEFAULT_FILTER_BACKENDS": (
        "rest_framework.filters.OrderingFilter",
        "django_filters.rest_framework.DjangoFilterBackend",
897
    ),
Eliot Berriot's avatar
Eliot Berriot committed
898
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
899
    "NUM_PROXIES": env.int("NUM_PROXIES", default=1),
900
}
901
THROTTLING_ENABLED = env.bool("THROTTLING_ENABLED", default=True)
Agate's avatar
Agate committed
902
"""
jovuit's avatar
jovuit committed
903
904
Wether to enable throttling (also known as rate-limiting).
Leaving this enabled is recommended
Agate's avatar
Agate committed
905
906
907
especially on public pods, to improve the quality of service.
"""

908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
if THROTTLING_ENABLED:
    REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = env.list(
        "THROTTLE_CLASSES",
        default=["funkwhale_api.common.throttling.FunkwhaleThrottle"],
    )

THROTTLING_SCOPES = {
    "*": {"anonymous": "anonymous-wildcard", "authenticated": "authenticated-wildcard"},
    "create": {
        "authenticated": "authenticated-create",
        "anonymous": "anonymous-create",
    },
    "list": {"authenticated": "authenticated-list", "anonymous": "anonymous-list"},
    "retrieve": {
        "authenticated": "authenticated-retrieve",
        "anonymous": "anonymous-retrieve",
    },
    "destroy": {
        "authenticated": "authenticated-destroy",
        "anonymous": "anonymous-destroy",
    },
    "update": {
        "authenticated": "authenticated-update",
        "anonymous": "anonymous-update",
    },
    "partial_update": {
        "authenticated": "authenticated-update",
        "anonymous": "anonymous-update",
    },
}

THROTTLING_USER_RATES = env.dict("THROTTLING_RATES", default={})

THROTTLING_RATES = {
    "anonymous-wildcard": {
        "rate": THROTTLING_USER_RATES.get("anonymous-wildcard", "1000/h"),
        "description": "Anonymous requests not covered by other limits",
    },
    "authenticated-wildcard": {
        "rate": THROTTLING_USER_RATES.get("authenticated-wildcard", "2000/h"),
        "description": "Authenticated requests not covered by other limits",
    },
    "authenticated-create": {
        "rate": THROTTLING_USER_RATES.get("authenticated-create", "1000/hour"),
        "description": "Authenticated POST requests",
    },
    "anonymous-create": {
        "rate": THROTTLING_USER_RATES.get("anonymous-create", "1000/day"),
        "description": "Anonymous POST requests",
    },
    "authenticated-list": {
        "rate": THROTTLING_USER_RATES.get("authenticated-list", "10000/hour"),
        "description": "Authenticated GET requests on resource lists",
    },
    "anonymous-list": {
        "rate": THROTTLING_USER_RATES.get("anonymous-list", "10000/day"),
        "description": "Anonymous GET requests on resource lists",
    },
    "authenticated-retrieve": {
        "rate": THROTTLING_USER_RATES.get("authenticated-retrieve", "10000/hour"),
        "description": "Authenticated GET requests on resource detail",
    },
    "anonymous-retrieve": {
        "rate": THROTTLING_USER_RATES.get("anonymous-retrieve", "10000/day"),
        "description": "Anonymous GET requests on resource detail",
    },
    "authenticated-destroy": {
        "rate": THROTTLING_USER_RATES.get("authenticated-destroy", "500/hour"),
        "description": "Authenticated DELETE requests on resource detail",
    },
    "anonymous-destroy": {
        "rate": THROTTLING_USER_RATES.get("anonymous-destroy", "1000/day"),
        "description": "Anonymous DELETE requests on resource detail",
    },
    "authenticated-update": {
        "rate": THROTTLING_USER_RATES.get("authenticated-update", "1000/hour"),
        "description": "Authenticated PATCH and PUT requests on resource detail",
    },
    "anonymous-update": {
        "rate": THROTTLING_USER_RATES.get("anonymous-update", "1000/day"),
        "description": "Anonymous PATCH and PUT requests on resource detail",
    },
990
    "subsonic": {
991
        "rate": THROTTLING_USER_RATES.get("subsonic", "2000/hour"),
992
993
        "description": "All subsonic API requests",
    },
994
995
996
997
998
999
1000
    # potentially spammy / dangerous endpoints
    "authenticated-reports": {
        "rate": THROTTLING_USER_RATES.get("authenticated-reports", "100/day"),
        "description": "Authenticated report submission",
    },
    "anonymous-reports": {
        "rate": THROTTLING_USER_RATES.get("anonymous-reports", "10/day"),