Merge branch 'pre-release/1.3.0' into develop
This commit is contained in:
commit
b0d6a0407a
1122 changed files with 76173 additions and 66002 deletions
137
docs/developer/api/authentication.md
Normal file
137
docs/developer/api/authentication.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# API authentication
|
||||
|
||||
Funkwhale uses the OAuth [authorization grant flow](https://tools.ietf.org/html/rfc6749#section-4.1) for external apps. This flow is a secure way to authenticate apps that requires a user's explicit consent to perform actions.
|
||||
|
||||
```{mermaid}
|
||||
%%{init: { 'sequence': {'mirrorActors': false} } }%%
|
||||
sequenceDiagram
|
||||
accTitle: "Funkwhale OAuth token flow"
|
||||
accDescr: "A sequence diagram showing how apps authenticate with Funkwhale"
|
||||
autonumber
|
||||
actor User
|
||||
participant A as Application
|
||||
participant F as Funkwhale web interface
|
||||
participant T as Token endpoint
|
||||
User ->> A: Log in to Funkwhale
|
||||
A ->> F: Direct to login screen
|
||||
F -->> User: Authenticate this app?
|
||||
User ->> F: Confirm
|
||||
F -->> A: Authorization code
|
||||
A ->> T: Authorization code and redirect URI
|
||||
T -->> A: Access token and refresh token
|
||||
loop Refresh
|
||||
A ->> T: Refresh token
|
||||
T -->> A: Access token
|
||||
end
|
||||
```
|
||||
|
||||
```{contents} Steps
|
||||
:local:
|
||||
```
|
||||
|
||||
## 1. Create an application
|
||||
|
||||
To connect to the Funkwhale API using OAuth, you need to create an **application**. This represents the entity credentials are related to.
|
||||
|
||||
When creating an application you need to define the [**scopes**](https://www.rfc-editor.org/rfc/rfc6749#section-3.3) the application has access to. Scopes define what information your application can access. Each scope can be granted with the following rights:
|
||||
|
||||
- `read:<scope>`: grants read-only access to the resource
|
||||
- `write:<scope>`: grants write-only access to the resource
|
||||
|
||||
`read` rights are required to fetch information using a `GET` request. All other actions (`POST`, `PATCH`, `PUT`, and `DELETE`) require `write` privileges. You may give an application **both** `read` and `write` access to any scope.
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Scope
|
||||
- Description
|
||||
* - `read`
|
||||
- Read-only access to all data
|
||||
* - `write`
|
||||
- Read-only access to all data
|
||||
* - `<read/write>:profile`
|
||||
- Access to profile data (email address, username, etc.)
|
||||
* - `<read/write>:libraries`
|
||||
- Access to library data (uploads, libraries, tracks, albums, artists, etc.)
|
||||
* - `<read/write>:favorites`
|
||||
- Access to favorites
|
||||
* - `<read/write>:listenings`
|
||||
- Access to history
|
||||
* - `<read/write>:follows`
|
||||
- Access to followers
|
||||
* - `<read/write>:playlists`
|
||||
- Access to playlists
|
||||
* - `<read/write>:radios`
|
||||
- Access to radios
|
||||
* - `<read/write>:filters`
|
||||
- Access to content filters
|
||||
* - `<read/write>:notifications`
|
||||
- Access to notifications
|
||||
* - `<read/write>:edits`
|
||||
- Access to metadata edits
|
||||
|
||||
```
|
||||
|
||||
Next, you need to define a [**Redirect URI**](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2). This is the location the user is redirected to once they authenticate your app. This can be any URI you want.
|
||||
|
||||
```{note}
|
||||
Funkwhale supports the `urn:ietf:wg:oauth:2.0:oob` redirect URI for non-web applications. If you use this URI, the user is shown a token to copy and paste.
|
||||
```
|
||||
|
||||
Once you've decided on your scopes and your redirect URI, you can create your app using one of the following methods:
|
||||
|
||||
1. Visit `/settings/applications/new` on your Funkwhale pod while logged in
|
||||
2. Send a `POST` request to `/api/v1/oauth/apps`. See our [API documentation](https://docs.funkwhale.audio/swagger/) for more information
|
||||
|
||||
Both methods return a [**client ID**](https://www.rfc-editor.org/rfc/rfc6749#section-2.2) and a [**secret**](https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1).
|
||||
|
||||
## 2. Get an authorization code
|
||||
|
||||
```{important}
|
||||
Authorization codes are only valid for 5 minutes after the user approves the request.
|
||||
```
|
||||
|
||||
You need an [**authorization code**](https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1) to request an access token for your user. This code confirms to the server that a user has authorized access to their account.
|
||||
|
||||
To fetch an authorization code, you need to send the user to their Funkwhale pod to authenticate. This sends an [authorization request](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2) to the server.
|
||||
|
||||
To do this, call the `/authorize` endpoint with the following URL encoded query parameters:
|
||||
|
||||
- `client_id`\* - Your application's client ID
|
||||
- `response_type`\* - Must be set to `code`.
|
||||
- `redirect_uri` - Your redirect URI
|
||||
- `scope` - A list of scopes
|
||||
- `state` - Used to maintain state between the request and the callback to prevent cross-site request forgery. Typically corresponds with a location in the app (e.g. `/library`)
|
||||
|
||||
Here is an example URL: `https://demo.funkwhale.audio/authorize?response_type=code&scope=read%20write&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback&state=/library&client_id=jDOUfhqLlrbuOkToDCanZmBKEiyorMb9ZUgD2tFQ`.
|
||||
|
||||
When the user authorizes your app, the server responds with an authorization code. See [the OAuth spec](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2) for more information about this response.
|
||||
|
||||
## 3. Get an access token
|
||||
|
||||
Once you receive your authorization code, you need to [request an access token](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3). To request an access token, call the `/api/v1/oauth/token` endpoint with the following information:
|
||||
|
||||
- `grant_type`\* - Must be set to `authorization_code`
|
||||
- `code`\* - Your application's authorization code
|
||||
- `redirect_uri`\* - Your redirect URI
|
||||
- `client_id`\* Your application's client ID
|
||||
|
||||
The server responds with an [`access_token`](https://www.rfc-editor.org/rfc/rfc6749#section-1.4) and a [`refresh_token`](https://www.rfc-editor.org/rfc/rfc6749#section-1.5). See [the OAuth spec](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4) for more information about this response.
|
||||
|
||||
You can use this token to authenticate calls from your application to the Funkwhale API by passing it as a request header with the following format: `Authorization: Bearer <token>`.
|
||||
|
||||
## 4. Refresh your access token
|
||||
|
||||
```{important}
|
||||
When you refresh your token the endpoint returns a new `refresh_token`. You must update your refresh token each time you request a new access token.
|
||||
```
|
||||
|
||||
By default, Funkwhale access tokens are valid for **10 hours**. Pod admins can configure this by setting the `ACCESS_TOKEN_EXPIRE_SECONDS` variable in their `.env` file.
|
||||
|
||||
After the access token expires, you must request a new access token by calling the `/api/v1/oauth/token` endpoint with the following information:
|
||||
|
||||
- `grant_type`\* - Must be set to `refresh_token`
|
||||
- `refresh_token`\* - Your current refresh token
|
||||
- `scope` - A list of scopes
|
||||
|
||||
See [the OAuth spec](https://www.rfc-editor.org/rfc/rfc6749#section-6) for more information about this response.
|
||||
18
docs/developer/api/index.md
Normal file
18
docs/developer/api/index.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Funkwhale API
|
||||
|
||||
The Funkwhale API is a [REST API](https://developer.mozilla.org/en-US/docs/Glossary/REST) written in [Python](https://www.python.org/) using the [Django REST framework](https://www.django-rest-framework.org/). It is the central component of the project and houses the application's logic.
|
||||
|
||||
The current API (v1) is **stable**, meaning we are committed to not introducing breaking changes and to maintaining compatibility. We are currently working on Funkwhale API v2, but this is a work in progress and not yet ready for production use.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
caption: Resources
|
||||
maxdepth: 1
|
||||
---
|
||||
|
||||
API explorer<https://docs.funkwhale.audio/swagger/>
|
||||
authentication
|
||||
rate-limit
|
||||
subsonic
|
||||
|
||||
```
|
||||
51
docs/developer/api/rate-limit.md
Normal file
51
docs/developer/api/rate-limit.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Rate limiting
|
||||
|
||||
Funkwhale supports rate-limiting as of version 0.2.0. Pod admins can choose to rate limit specific endpoints to prevent abuse and improve the stability of the service. If the server drops a request due to rate-limiting, it returns a `429` status code.
|
||||
|
||||
By default, rate limits follow these rules:
|
||||
|
||||
1. Anonymous (unauthenticated) requests are subject to lower limits than authenticated requests
|
||||
2. `PUT`, `DELETE`, `PUT`, `POST`, and `PATCH` requests are subject to lower limits than `GET` requests
|
||||
|
||||
You can return a full list of scope with their corresponding rate-limits by making a `GET` request to `/api/v1/rate-limit`.
|
||||
|
||||
## HTTP headers
|
||||
|
||||
Each API call returns HTTP headers to pass the following information:
|
||||
|
||||
- What was the scope of the request
|
||||
- What is the rate-limit associated with the request scope
|
||||
- How many more requests in the scope can be made within the rate-limit timeframe
|
||||
- How much time does the client need to wait to send another request
|
||||
|
||||
Here is a full list of supported headers
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Header
|
||||
- Example value
|
||||
- Description
|
||||
* - `X-RateLimit-Limit`
|
||||
- 50
|
||||
- The number of requests allowed within a given period
|
||||
* - `X-RateLimit-Duration`
|
||||
- 3600
|
||||
- The time window, in seconds, during which the number of requests are measured
|
||||
* - `X-RateLimit-Scope`
|
||||
- `login`
|
||||
- The name of the scope computed for the request
|
||||
* - `X-RateLimit-Remaining`
|
||||
- 42
|
||||
- How many requests can be sent with the same scope before the rate-limit applies
|
||||
* - `Retry-After`
|
||||
- 3543
|
||||
- How many seconds the client must wait before it can retry. Only applies if `X-RateLimit-Remaining` is `0`
|
||||
* - `X-RateLimit-Reset`
|
||||
- 1568126089
|
||||
- A timestamp indicating when the `X-RateLimit-Remaining` value will reset
|
||||
* - `X-RateLimit-ResetSeconds`
|
||||
- 3599
|
||||
- The number of seconds until the `X-RateLimit-Remaining` value resets
|
||||
|
||||
```
|
||||
71
docs/developer/api/subsonic.md
Normal file
71
docs/developer/api/subsonic.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Subsonic API
|
||||
|
||||
Funkwhale supports a subset of the [Subsonic API's](http://www.subsonic.org/pages/api.jsp) endpoints. This enables users to listen to music stored on their Funkwhale pod through a Subsonic-compatible app.
|
||||
|
||||
We aim to support as many endpoints as we can to give Subsonic users the best possible experience. However, some endpoints require a folder-based endpoint. This doesn't match Funkwhale's internal structure, which means emulating them is difficult.
|
||||
|
||||
## Supported endpoints
|
||||
|
||||
```{note}
|
||||
We aim to keep this list up-to-date. If you think something is missing, you can see all supported endpoints in the [API views](https://dev.funkwhale.audio/funkwhale/funkwhale/blob/develop/api/funkwhale_api/subsonic/views.py).
|
||||
```
|
||||
|
||||
Funkwhale supports both XML and JSON formats for the following Subsonic endpoints:
|
||||
|
||||
- [`createPlaylist`](http://www.subsonic.org/pages/api.jsp#createPlaylist)
|
||||
- [`deletePlaylist`](http://www.subsonic.org/pages/api.jsp#deletePlaylist)
|
||||
- [`getAlbum`](http://www.subsonic.org/pages/api.jsp#getAlbum)
|
||||
- [`getAlbumList2`](http://www.subsonic.org/pages/api.jsp#getAlbumList2)
|
||||
- [`getArtist`](http://www.subsonic.org/pages/api.jsp#getArtist)
|
||||
- [`getArtistInfo2`](http://www.subsonic.org/pages/api.jsp#getArtistInfo2)
|
||||
- [`getArtists`](http://www.subsonic.org/pages/api.jsp#getArtists)
|
||||
- [`getAvatar`](http://www.subsonic.org/pages/api.jsp#getAvatar)
|
||||
- [`getCoverArt`](http://www.subsonic.org/pages/api.jsp#getCoverArt)
|
||||
- [`getIndexes`](http://www.subsonic.org/pages/api.jsp#getIndexes)
|
||||
- [`getLicense`](http://www.subsonic.org/pages/api.jsp#getLicense)
|
||||
- [`getMusicFolders`](http://www.subsonic.org/pages/api.jsp#getMusicFolders)
|
||||
- [`getPlaylist`](http://www.subsonic.org/pages/api.jsp#getPlaylist)
|
||||
- [`getPlaylists`](http://www.subsonic.org/pages/api.jsp#getPlaylists)
|
||||
- [`getRandomSongs`](http://www.subsonic.org/pages/api.jsp#getRandomSongs)
|
||||
- [`getSong`](http://www.subsonic.org/pages/api.jsp#getSong)
|
||||
- [`getStarred`](http://www.subsonic.org/pages/api.jsp#getStarred)
|
||||
- [`getStarred2`](http://www.subsonic.org/pages/api.jsp#getStarred2)
|
||||
- [`getUser`](http://www.subsonic.org/pages/api.jsp#getUser)
|
||||
- [`ping`](http://www.subsonic.org/pages/api.jsp#ping)
|
||||
- [`scrobble`](http://www.subsonic.org/pages/api.jsp#scrobble)
|
||||
- [`search3`](http://www.subsonic.org/pages/api.jsp#search3)
|
||||
- [`star`](http://www.subsonic.org/pages/api.jsp#star)
|
||||
- [`stream`](http://www.subsonic.org/pages/api.jsp#stream)
|
||||
- [`unstar`](http://www.subsonic.org/pages/api.jsp#unstar)
|
||||
- [`updatePlaylist`](http://www.subsonic.org/pages/api.jsp#updatePlaylist)
|
||||
|
||||
### Additional properties
|
||||
|
||||
Funkwhale returns some additional properties to Subsonic payloads. You can use these properties to adapt your client behavior if needed:
|
||||
|
||||
```{list-table}
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`
|
||||
- String
|
||||
- The name of the app (`funkwhale`)
|
||||
* - `funkwhaleVersion`
|
||||
- String
|
||||
- The Funkwhale version the pod is running
|
||||
```
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"subsonic-response": {
|
||||
"type": "funkwhale",
|
||||
"funkwhaleVersion": "1.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test a Subsonic app
|
||||
|
||||
We host a demo server at <https://demo.funkwhale.audio> which you can use to test your Subsonic app.
|
||||
|
||||
You can test the Subsonic API by logging in with a Subsonic client or by directly by calling an endpoint. For example, call this URL to test the `ping` endpoint: <https://demo.funkwhale.audio/rest/ping.view?f=json>
|
||||
107
docs/developer/architecture.md
Normal file
107
docs/developer/architecture.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Project architecture
|
||||
|
||||
Funkwhale is made up of several components. Understanding these components and what they do is important when contributing to Funkwhale's codebase. In this article, we'll break down each part of Funkwhale's architecture to help you understand what each component does.
|
||||
|
||||
Below is a diagram of Funkwhale's project setup.
|
||||
|
||||
```{mermaid}
|
||||
flowchart TD
|
||||
accTitle: Funkwhale data flow diagram
|
||||
accDescr: A diagram showing the components of the Funkwhale app and how data flows through each component.
|
||||
subgraph Entrypoints
|
||||
user[User] --> frontend[Funkwhale web app]
|
||||
user --> ffa[Funkwhale for Android]
|
||||
user --> subsonic[Subsonic app]
|
||||
frontend --> proxy[Nginx/Apache reverse proxy]
|
||||
ffa --> proxy
|
||||
subsonic --> proxy
|
||||
end
|
||||
subgraph Funkwhale backend
|
||||
proxy --> api[Django API server]
|
||||
api --> db[PostgreSQL database]
|
||||
api --> redis[Redis cache and message queue]
|
||||
beat[Celery beat task scheduler] --> redis
|
||||
redis <--> celery[Celery worker]
|
||||
celery --> db
|
||||
end
|
||||
```
|
||||
|
||||
Select a link below to see information about each component.
|
||||
|
||||
```{contents}
|
||||
:local:
|
||||
```
|
||||
|
||||
## Entrypoints
|
||||
|
||||
Users can access Funkwhale using a variety of entrypoints. They can make use of a Funkwhale application, a Subsonic-compatible application, or by calling the API directly. Each entrypoint interacts with the Funkwhale backend in the same way.
|
||||
|
||||
### Funkwhale web app
|
||||
|
||||
The Funkwhale web app is a {abbr}`SPA (Single Page Application)` written in [Vue.js](https://vuejs.org) and [Typescript](https://typescriptlang.org). This is the application most people associate with Funkwhale. Server admins usually run an instance of the web app alongside their Funkwhale {term}`pod`, but you can also connect a standalone web app to another pod.
|
||||
|
||||
The Funkwhale web app interacts with the Funkwhale API to fetch and update data. Using a service worker, the web app caches important information for offline use.
|
||||
|
||||
### Funkwhale for Android
|
||||
|
||||
Funkwhale for Android is the Funkwhale collective's official Android app written in [Kotlin](https://kotlinlang.org/). It interacts with the Funkwhale API to fetch and update data and stores information for offline playback.
|
||||
|
||||
### Subsonic app
|
||||
|
||||
Funkwhale supports a limited subset of the [Subsonic API](http://www.subsonic.org/pages/api.jsp) to support existing Subsonic apps. These apps can request data stored on a Funkwhale server by calling these endpoints.
|
||||
|
||||
### Nginx/Apache reverse proxy
|
||||
|
||||
The reverse proxy acts as a layer between a Funkwhale pod and the open internet. It enhances the pod's security and provides additional options to help increase performance.
|
||||
|
||||
When a user tries to communicate with a Funkwhale pod, the reverse proxy:
|
||||
|
||||
1. Handles the HTTP/HTTPS requests and proxies them to the Funkwhale API server
|
||||
2. Serves requested static files, such as audio files and stylesheets
|
||||
|
||||
## Backend
|
||||
|
||||
The Funkwhale backend is made up of a few components which are responsible for:
|
||||
|
||||
1. Communicating with the user's entrypoint and actioning requests
|
||||
2. Maintaining data consistency
|
||||
3. Communicating with other Funkwhale pods (if federation is enabled)
|
||||
|
||||
### Django API server
|
||||
|
||||
The Funkwhale API is a [REST API](https://developer.mozilla.org/en-US/docs/Glossary/REST) written in [Python](https://www.python.org/) using the [Django REST framework](https://www.django-rest-framework.org/). It is the central piece of the project and houses the application's logic.
|
||||
|
||||
The Funkwhale API is responsible for:
|
||||
|
||||
1. Fetching requested data from the cache/database and returning it to the requester in a meaningful way
|
||||
2. Processing incoming data and writing it to the database in a meaningful way
|
||||
3. Delegating long-running tasks to workers to reduce load
|
||||
|
||||
### PostgreSQL database
|
||||
|
||||
Funkwhale uses a [PostgreSQL database](https://www.postgresql.org/) to store data. All information that is served by and sent to the Funkwhale API is stored in this database.
|
||||
|
||||
The Funkwhale database makes heavy use of [indexes](https://www.postgresql.org/docs/current/indexes.html) for enhanced performance.
|
||||
|
||||
### Redis cache and message queue
|
||||
|
||||
Funkwhale uses [Redis](https://redis.io/) to cache information from the database and to store a queue of messages to send. We use this cache to avoid locking database resources and to speed up requests.
|
||||
|
||||
### Celery worker
|
||||
|
||||
Funkwhale has to handle a lot of tasks that take longer than the average HTTP request/response cycle. To ensure these tasks complete and don't impact the API's performance, they are offloaded to a [Celery](https://docs.celeryq.dev/en/stable/userguide/workers.html) task worker. The worker then works through all the tasks in its queue while the API handles real-time responses.
|
||||
|
||||
Some common tasks the Celery worker handles are:
|
||||
|
||||
- Importing uploaded music to the database
|
||||
- Handling [ActivityPub](https://www.w3.org/TR/activitypub/) messages from other {term}`Fediverse` servers
|
||||
- Scanning new content on remote pods
|
||||
|
||||
### Celery beat task scheduler
|
||||
|
||||
In addition to handling tasks from the API, the Celery worker also needs to handle some recurring tasks. To manage these, we implement a [Celery beat](https://docs.celeryq.dev/en/stable/reference/celery.apps.beat.html#celery.apps.beat.Beat) scheduler. The scheduler is responsible for triggering tasks on a schedule and adding messages to the [queue](#redis-cache-and-message-queue) so the worker can work through them.
|
||||
|
||||
Some common recurring tasks are:
|
||||
|
||||
- Clearing the cache
|
||||
- Refreshing content metadata
|
||||
150
docs/developer/contribute/api.md
Normal file
150
docs/developer/contribute/api.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Contribute to the API
|
||||
|
||||
The Funkwhale API is the core of the Funkwhale ecosystem. It powers all actions in the Funkwhale app as well as other apps such as the CLI and mopidy plugin. The API is written in [Django rest framework](https://www.django-rest-framework.org/).
|
||||
|
||||
Before you start work on the API, you should open up a conversation in [the forum](https://forum.funkwhale.audio) to discuss the changes you want to make. All API changes need to be defined and scoped before code changes are made. If you are fixing a bug, you don't need to discuss this in the forum first.
|
||||
|
||||
Each API endpoint is made up of the following:
|
||||
|
||||
- Model – defines the shape of data and how it is stored in the database
|
||||
- View – defines what data is reflected by an endpoint
|
||||
- Serializer – defines how data is serialized and deserialized by the endpoint
|
||||
|
||||
The API directory is structured as follows:
|
||||
|
||||
- `config` – contains the project settings, URL structure, and web server gateway information setup
|
||||
- `settings` – contains all Django settings files
|
||||
- `funkwhale_api` – contains the Funkwhale API logic
|
||||
- `pyproject.toml` – contains the Python requirements
|
||||
- `tests` – contains all tests. This directory matches the structure of the `funkwhale_api` directory
|
||||
|
||||
## Write tests
|
||||
|
||||
You should write tests to ensure that your code does what you expect it to. We use [pytest](https://pytest.org) and [factory-boy](https://factoryboy.readthedocs.io) to power our API testing suite.
|
||||
|
||||
Writing tests is outside the scope of this documentation, but here are some useful links to help you get started:
|
||||
|
||||
- [A quick introduction to writing unit tests with pytest](https://semaphoreci.com/community/tutorials/testing-python-applications-with-pytest)
|
||||
- [A complete guide to Test-Driven Development](https://www.obeythetestinggoat.com/)
|
||||
- [pytest documentation](https://docs.pytest.org/en/latest)
|
||||
- [pytest-mock documentation](https://pypi.org/project/pytest-mock)
|
||||
- [factory-boy documentation](http://factoryboy.readthedocs.io)
|
||||
|
||||
Try to keep your tests small and focused. Each test should test a single function, so if you need to test multiple things you should write multiple tests.
|
||||
|
||||
```{note}
|
||||
Test files must target a module and follow the `funkwhale_api` directory structure. If you write tests for `funkwhale_api/myapp/views.py`, you should put them in `tests/myapp/test_views.py`.
|
||||
```
|
||||
|
||||
We provide utilities and fixtures to make writing tests as easy as possible. You can see the list of available fixtures by running `sudo docker compose -f dev.yml run --rm api pytest --fixtures`.
|
||||
|
||||
### Factories
|
||||
|
||||
Each directory includes a `factories.py` file which contains factories for the models in the directory. You can use these to create arbitrary objects
|
||||
|
||||
```py
|
||||
# funkwhale_api/myapp/users.py
|
||||
|
||||
def downgrade_user(user):
|
||||
"""
|
||||
A simple function that remove superuser status from users
|
||||
and return True if user was actually downgraded
|
||||
"""
|
||||
downgraded = user.is_superuser
|
||||
user.is_superuser = False
|
||||
user.save()
|
||||
return downgraded
|
||||
|
||||
# tests/myapp/test_users.py
|
||||
from funkwhale_api.myapp import users
|
||||
|
||||
def test_downgrade_superuser(factories):
|
||||
user = factories['users.User'](is_superuser=True)
|
||||
downgraded = users.downgrade_user(user)
|
||||
|
||||
assert downgraded is True
|
||||
assert user.is_superuser is False
|
||||
|
||||
def test_downgrade_normal_user_does_nothing(factories):
|
||||
user = factories['users.User'](is_superuser=False)
|
||||
downgraded = something.downgrade_user(user)
|
||||
|
||||
assert downgraded is False
|
||||
assert user.is_superuser is False
|
||||
```
|
||||
|
||||
### Mocking
|
||||
|
||||
Use mocks to fake logic in your tests. This is useful when testing components that depend on one another.
|
||||
|
||||
```py
|
||||
# funkwhale_api/myapp/notifications.py
|
||||
|
||||
def notify(email, message):
|
||||
"""
|
||||
A function that sends an e-mail to the given recipient
|
||||
with the given message
|
||||
"""
|
||||
|
||||
# our e-mail sending logic here
|
||||
# ...
|
||||
|
||||
# funkwhale_api/myapp/users.py
|
||||
from . import notifications
|
||||
|
||||
def downgrade_user(user):
|
||||
"""
|
||||
A simple function that remove superuser status from users
|
||||
and return True if user was actually downgraded
|
||||
"""
|
||||
downgraded = user.is_superuser
|
||||
user.is_superuser = False
|
||||
user.save()
|
||||
if downgraded:
|
||||
notifications.notify(user.email, 'You have been downgraded!')
|
||||
return downgraded
|
||||
|
||||
# tests/myapp/test_users.py
|
||||
def test_downgrade_superuser_sends_email(factories, mocker):
|
||||
"""
|
||||
Your downgrade logic is already tested, however, we want to ensure
|
||||
an e-mail is sent when user is downgraded, but we don't have any e-mail
|
||||
server available in our testing environment. Thus, we need to mock
|
||||
the e-mail sending process.
|
||||
"""
|
||||
mocked_notify = mocker.patch('funkwhale_api.myapp.notifications.notify')
|
||||
user = factories['users.User'](is_superuser=True)
|
||||
users.downgrade_user(user)
|
||||
|
||||
# here, we ensure our notify function was called with proper arguments
|
||||
mocked_notify.assert_called_once_with(user.email, 'You have been downgraded')
|
||||
|
||||
|
||||
def test_downgrade_not_superuser_skips_email(factories, mocker):
|
||||
mocked_notify = mocker.patch('funkwhale_api.myapp.notifications.notify')
|
||||
user = factories['users.User'](is_superuser=False)
|
||||
users.downgrade_user(user)
|
||||
|
||||
# here, we ensure no e-mail was sent
|
||||
mocked_notify.assert_not_called()
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
You can run all tests in the pytest suite with the following command:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm api pytest
|
||||
```
|
||||
|
||||
Run a specific test file by calling pytest against it:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm api pytest tests/music/test_models.py
|
||||
```
|
||||
|
||||
You can check the full list of options by passing the `-h` flag:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm api pytest -h
|
||||
```
|
||||
106
docs/developer/contribute/copy.md
Normal file
106
docs/developer/contribute/copy.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Update UI copy
|
||||
|
||||
```{note}
|
||||
Funkwhale is localized into several languages using [Weblate](https://translate.funkwhale.audio). You must make sure that any frontend strings are properly marked for localization. We use the [vue-i18n package](https://kazupon.github.io/vue-i18n/) to handle translation of frontend files.
|
||||
```
|
||||
|
||||
All UI strings are stored in `front/locales/en.json` file. The file is structured to mimic the format of the repository. Each string should be labeled following the semantic naming for the item it applies to.
|
||||
|
||||
UI strings can be added to both the `<script>` and `<template>` part of a Vue file using following syntax:
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Locale file
|
||||
|
||||
```json
|
||||
{
|
||||
"components": {
|
||||
"About": {
|
||||
"title": "About",
|
||||
"header": {
|
||||
"funkwhale": "A social platform to enjoy and share music"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Script
|
||||
|
||||
```typescript
|
||||
import { useI18n } from "vue-i18n";
|
||||
//...
|
||||
const { t } = useI18n();
|
||||
//...
|
||||
const labels = computed(() => ({
|
||||
title: t("components.About.title"),
|
||||
}));
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Template
|
||||
|
||||
```html
|
||||
<h2>{{ $t('components.About.header.funkwhale') }}</h2>
|
||||
<button>{{ $t('components.About.button.cancel') }}</button>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
Some strings change depending on whether they are plural or not. You can create plural strings using the [vue-i18n pluralization syntax](https://kazupon.github.io/vue-i18n/guide/pluralization.html)
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Locale file
|
||||
|
||||
```json
|
||||
"components": {
|
||||
"audio": {
|
||||
"ChannelCard": {
|
||||
"meta": {
|
||||
"episodes": "No episodes | {episode_count} episode | {episode_count} episodes",
|
||||
"tracks": "No tracks | {tracks_count} track | {tracks_count} tracks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Template
|
||||
|
||||
```html
|
||||
<div class="description">
|
||||
<span
|
||||
v-if="object.artist?.content_category === 'podcast'"
|
||||
class="meta ellipsis"
|
||||
>
|
||||
{{ $t('components.audio.ChannelCard.meta.episodes', {episode_count:
|
||||
object.artist.tracks_count}) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('components.audio.ChannelCard.meta.tracks', {tracks_count:
|
||||
object.artist?.tracks_count}) }}
|
||||
</span>
|
||||
<tags-list
|
||||
label-classes="tiny"
|
||||
:truncate-size="20"
|
||||
:limit="2"
|
||||
:show-more="false"
|
||||
:tags="object.artist?.tags ?? []"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
36
docs/developer/contribute/frontend.md
Normal file
36
docs/developer/contribute/frontend.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Contribute to the frontend
|
||||
|
||||
The Funkwhale frontend is a {abbr}`SPA (Single Page Application)` written in [Typescript](https://typescriptlang.org) and [Vue.js](https://vuejs.org).
|
||||
|
||||
## Styles
|
||||
|
||||
We currently use [Fomantic UI](https://fomantic-ui.com) as our UI framework. We customize this with our own SCSS files located in `front/src/styles/_main.scss`.
|
||||
|
||||
We apply changes to the Fomantic CSS files before we import them:
|
||||
|
||||
1. We replace hardcoded color values with CSS variables to make themin easier. For example: `color: orange` is replaced by `color: var(--vibrant-color)`
|
||||
2. We remove unused values from the CSS files to keep the size down
|
||||
|
||||
These changes are applied when you run `yarn install` through a `postinstall` hook. If you want to modify these changes, check the `front/scripts/fix-fomantic-css.py` script.
|
||||
|
||||
We plan to replace Fomantic with our own UI framework in the near future. Check our [Penpot](https://design.funkwhale.audio) to see what we've got planned.
|
||||
|
||||
## Components
|
||||
|
||||
Our [component library](https://ui.funkwhale.audio) contains reusable Vue components that you can add to the Funkwhale frontend. If you want to add a new component, check out [the repository](https://dev.funkwhale.audio/funkwhale/vui).
|
||||
|
||||
## Testing
|
||||
|
||||
The Funkwhale frontend contains some tests to catch errors before changes go live. The coverage is still fairly low, so we welcome any contributions.
|
||||
|
||||
To run the test suite, run the following command:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm front yarn test:unit
|
||||
```
|
||||
|
||||
To run tests as you make changes, launch the test suite with the `-w` flag:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm front yarn test:unit -w
|
||||
```
|
||||
14
docs/developer/contribute/index.md
Normal file
14
docs/developer/contribute/index.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Contribute to the Funkwhale codebase
|
||||
|
||||
Funkwhale is an open source project, which means we welcome contributions from anyone! If you want to get involved with Funkwhale development, check the guides in this section.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 1
|
||||
---
|
||||
|
||||
frontend
|
||||
copy
|
||||
api
|
||||
|
||||
```
|
||||
677
docs/developer/federation/index.md
Normal file
677
docs/developer/federation/index.md
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
# Funkwhale federation
|
||||
|
||||
```{note}
|
||||
We aim to keep this document up-to-date to reflect the current state of federation. If you notice an issue, please open a thread on [our forum](https://forum.funkwhale.audio/t/documentation).
|
||||
```
|
||||
|
||||
Funkwhale is a federated platform. Funkwhale pods can share information between one another, and can also communicate with other {term}`Fediverse` software. This article outlines which tools we use, our approach to federation, and how we implement standards.
|
||||
|
||||
```{contents}
|
||||
:local:
|
||||
:depth: 2
|
||||
```
|
||||
|
||||
## Technologies and standards
|
||||
|
||||
Funkwhale's federation is built on top of the following technologies:
|
||||
|
||||
- [`ActivityPub`](https://www.w3.org/TR/activitypub/): Our federation protocol
|
||||
- [`HTTP Signatures`](https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-01.html): A library for authenticating messages sent over the federation
|
||||
- [`Webfinger`](https://tools.ietf.org/html/rfc7033): A protocol for discovering resources using readable names
|
||||
- [`ActivityStreams`](https://www.w3.org/TR/activitystreams-core/) and [`ActivityStreams vocabulary`](https://www.w3.org/TR/activitystreams-vocabulary/): Our approach to structuring messages
|
||||
|
||||
We plan for support [`JSON-LD signatures`](https://w3c-dvcg.github.io/ld-signatures/) in the future as an alternative to `HTTP Signatures`.
|
||||
|
||||
## Our philosophy
|
||||
|
||||
Funkwhale aims to align with the above specifications where possible to maintain compatibility with other ActivityPub-aware platforms. This documentation covers where we have departed from the specifications.
|
||||
|
||||
While we will always aim for compatibility with other services where it makes sense, Funkwhale to Funkwhale interaction is our focus. We use these standards to support activities and objects that fit in with our use cases, such as **follows** and **likes**.
|
||||
|
||||
## Internal logic
|
||||
|
||||
This section covers how we handle federation within the Funkwhale ecosystem.
|
||||
|
||||
### Database schema
|
||||
|
||||
We use ActivityPub entities as a guide when creating [our models](https://dev.funkwhale.audio/funkwhale/funkwhale/blob/develop/api/funkwhale_api/federation/models.py) and database schemas. This ensures compatibility with other ActivityPub projects.
|
||||
|
||||
Funkwhale pods store received activities payloads in the database before attempting to process or deliver them. This enables us to debug federation issues, resend messages, or process historical activities that weren't previously supported.
|
||||
|
||||
Funkwhale users are associated to an `Actor`. Remote and local actors are stord in the same database table. Any federated entities, such as uploads, are linked to the `Actor` rather than the user. We don't distinguish between local and remote users on the database level.
|
||||
|
||||
### Activity creation and delivery
|
||||
|
||||
Any action carried out by a local actor should trigger an `Activity`. This is the equivalent to posting an activity to an object. Funkwhale creates an object with the activity payload and stores it in the `Activity` table. Funkwhale triggers 2 types of deliveries:
|
||||
|
||||
1. Local recipients: Funkwhale creates an `InboxItem` linked to the activity for each local recipient. Funkwhale then creates a feed of available inbox items for each local actor. Items in this feed have both a `Read` and `Unread` status to allow users to mark items as handled.
|
||||
2. Remote recipients: Funkwhale collects the inboxes and shared inbox URLs of all remote recipients. Funkwhale then creates a `Delivery` object and linked to the initial activity and the inbox or shared inbox URL. The worker uses this `Delivery` object to post the activity to the correct inbox.
|
||||
|
||||
When a local inbox receives an activity from a remote actor, it ends up in their inbox for them to handle.
|
||||
|
||||
Funkwhale doesn't support all activities. Our routing logic enables the software to handle supported activities and discard unsupported ones. When Funkwhale receives an activity it checks if there is a route to handle it. If there is, Funkwhale calls a dedicated handler.
|
||||
|
||||
For example: if Funkwhale receives an [`activity-create`](#create) activity for an `object-audio` object, Funkwhale calls a handler to:
|
||||
|
||||
- Persist the data in the local `Upload` table
|
||||
- Retrieve data associated with the audio
|
||||
|
||||
You can find the code for our routing logic here:
|
||||
|
||||
- [Routing logic for activities](https://dev.funkwhale.audio/funkwhale/funkwhale/blob/develop/api/funkwhale_api/federation/routes.py)
|
||||
- [Delivery logic for activities](https://dev.funkwhale.audio/funkwhale/funkwhale/blob/develop/api/funkwhale_api/federation/tasks.py)
|
||||
|
||||
## Service actor
|
||||
|
||||
Funkwhale uses a dedicated service actor to send messages or authenticate fetches. This actor isn't associated to a user.
|
||||
|
||||
You can query a pod's nodeinfo endpoint to return the ID of the service actor in the `metadata > actorId` field. See the [API explorer](https://docs.funkwhale.audio/swagger/) for more information about this endpoint.
|
||||
|
||||
Funkwhale considers a pod's service actor to be an authoritative source for activities associated with **all** objects on its pod's domain. If the service actor sends an activity linked to an object on its domain, remote pods will recognize its authority.
|
||||
|
||||
## Supported activities
|
||||
|
||||
### Follow
|
||||
|
||||
A **follow** enables actors to access and retrieve content from other actors as soon as it updates.
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- [Library objects](#library)
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
When Funkwhale receives a follow on a [library object](#library), it performs one of the following actions depending on the library's visibility:
|
||||
|
||||
- Automatically accept: If the library is public, Funkwhale automatically accepts the follow activity. Funkwhale sends a notification to the owner of the library and an [`Accept`](#accept) activity to the actor who sent the follow
|
||||
- Accept request: If the library isn't public, Funkwhale sends a notification to the library owner. If the owner approves the request, Funkwhale sends an [`Accept`](#accept) activity to the actor who sent the follow
|
||||
|
||||
Funkwhale uses the library follow status to grant access to the actor who sent the follow request. If the library isn't public and the owner doesn't send an approval, the requesting actor can't access the library's content.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale ensures the activity is being sent to the library's owner before handling it.
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, **Alice** sends a follow activity for a [library object](#library) owned by **Bob**.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"type": "Follow",
|
||||
"id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded",
|
||||
"actor": "https://music.rocks/federation/actors/Alice",
|
||||
"to": ["https://awesome.music/federation/actors/Bob"],
|
||||
"object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6"
|
||||
}
|
||||
```
|
||||
|
||||
### Accept
|
||||
|
||||
The `Accept` activity sends a positive response, such as confirming a [`Follow` activity](#follow).
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- `Activity` objects
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
When Funkwhale receives an `Accept` activity related to a [`Follow`](#follow) activity, it marks the `Follow` as accepted in the database. If the `Follow` activity relates to a [`Library` object](#library), the requester receives future activities associated with the library. This includes [`Create`](#create), [`Audio`](#audio), and [`Delete`](#delete) activities. They can also browse and download the library's audio files. See the section on [Audio fetching on restricted libraries](#audio-fetching-on-restricted-libraries) for more details.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale ensures the activity is sent by the library's owner before handling it.
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, **Bob** accepts a follow request from **Alice**.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"type": "Accept",
|
||||
"id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded/accept",
|
||||
"to": ["https://music.rocks/federation/actors/Alice"],
|
||||
"actor": "https://awesome.music/federation/actors/Bob",
|
||||
"object": {
|
||||
"id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded",
|
||||
"type": "Follow",
|
||||
"actor": "https://music.rocks/federation/actors/Alice",
|
||||
"object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Undo
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- [`Follow` objects](#follow)
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
When Funkwhale receives an `Undo` activity, it deletes the corresponding `Follow` from the database.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale ensures the request actor is the same actor who sent the `Follow` activity before handling it.
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, **Alice** notifies **Bob** that she's undoing her follow.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"type": "Undo",
|
||||
"id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded/accept",
|
||||
"to": ["https://awesome.music/federation/actors/Bob"],
|
||||
"actor": "https://music.rocks/federation/actors/Alice",
|
||||
"object": {
|
||||
"id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded",
|
||||
"type": "Follow",
|
||||
"actor": "https://music.rocks/federation/actors/Alice",
|
||||
"object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Create
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- [`Audio` objects](#audio)
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
```{note}
|
||||
See [the `Audio` object reference](#audio) for details on the object's structure.
|
||||
```
|
||||
|
||||
When Funkwhale receives a `Create` activity with an [`Audio` object](#audio), it persists a local upload in the database. It then associates the upload to related library and track information. If no track matches the audio metadata, Funkwhale creates on using the `metadata` attribute in the object.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale ensures the activity actor and library owner are the same before handling the activity. If the associated library has no local followers, Funkwhale discards the activity.
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, **Bob** creates new content in his library and sends a message to its followers.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"to": [
|
||||
"https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers"
|
||||
],
|
||||
"type": "Create",
|
||||
"actor": "https://awesome.music/federation/actors/Bob",
|
||||
"object": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- [`Library` objects](#library)
|
||||
- [`Track` objects](#track)
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
```{note}
|
||||
See [the `Track` object reference](#track) and [`Library` object reference](#library) for details on the object's structure.
|
||||
```
|
||||
|
||||
When Funkwhale receives an update associated with a [`Library`](#library) or [`Track`](#track) object, it attempts to update the corresponding object in its database.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale performs different checks depending on the target of the update:
|
||||
|
||||
- For [`Library`](#library) objects, Funkwhale ensures the actor sending the message is the library owner
|
||||
- For [`Track`](#track) objects, Funkwhale ensures the actor sending the message **either**:
|
||||
- Matches the [`attributedTo`](#attributedto) property on the local copy of the object
|
||||
- Is the [service actor](#service-actor)
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, **Bob** updates his library and sends a message to its followers.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"to": [
|
||||
"https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers"
|
||||
],
|
||||
"type": "Update",
|
||||
"actor": "https://awesome.music/federation/actors/Bob",
|
||||
"object": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
::::{dropdown} Supported on
|
||||
|
||||
- [`Audio` objects](#audio)
|
||||
- [`Library` objects](#library)
|
||||
|
||||
::::
|
||||
|
||||
#### Internal logic
|
||||
|
||||
When Funkwhale receives a `Delete` activity, it deletes the associated object from the database.
|
||||
|
||||
#### Checks
|
||||
|
||||
Funkwhale ensures the actor initiating the activity is the owner of the associated object before handling it.
|
||||
|
||||
#### Example
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Library
|
||||
|
||||
In this example, **Bob** deletes a library and notifies its followers.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"type": "Delete",
|
||||
"to": [
|
||||
"https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers"
|
||||
],
|
||||
"actor": "https://awesome.music/federation/actors/Bob",
|
||||
"object": {
|
||||
"type": "Library",
|
||||
"id": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Audio
|
||||
|
||||
In this example, **Bob** deletes three audio objects in a library and notifies the library's followers.
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
],
|
||||
"type": "Delete",
|
||||
"to": [
|
||||
"https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers"
|
||||
],
|
||||
"actor": "https://awesome.music/federation/actors/Bob",
|
||||
"object": {
|
||||
"type": "Audio",
|
||||
"id": [
|
||||
"https://awesome.music/federation/music/uploads/19420073-3572-48a9-8c6c-b385ee1b7905",
|
||||
"https://awesome.music/federation/music/uploads/11d99680-23c6-4f72-997a-073b980ab204",
|
||||
"https://awesome.music/federation/music/uploads/1efadc1c-a704-4b8a-a71a-b288b1d1f423"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
## Supported objects
|
||||
|
||||
### Artist
|
||||
|
||||
An `Artist` is a custom object used to store musical artist and podcast creator information.
|
||||
|
||||
#### Properties
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`*
|
||||
- String
|
||||
- The object type (`Artist`)
|
||||
* - `id`*
|
||||
- String (URI)
|
||||
- A URI that identifies the artist over federation
|
||||
* - `name`*
|
||||
- String
|
||||
- The artist's name
|
||||
* - `published`*
|
||||
- Datetime
|
||||
- The date on which the artist was published over the federation
|
||||
* - `musicbrainzId`
|
||||
- String (UUID)
|
||||
- The Musicbrainz artist ID
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "https://awesome.music/federation/music/artists/73c32807-a199-4682-8068-e967f734a320",
|
||||
"name": "Metallica",
|
||||
"published": "2018-04-08T12:19:05.920415+00:00",
|
||||
"musicbrainzId": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
|
||||
}
|
||||
```
|
||||
|
||||
### Album
|
||||
|
||||
An `Album` is a custom object used to store album and podcast series information.
|
||||
|
||||
#### Properties
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`*
|
||||
- String
|
||||
- The object type (`Album`)
|
||||
* - `id`*
|
||||
- String (URI)
|
||||
- A URI that identifies the album over federation
|
||||
* - `name`*
|
||||
- String
|
||||
- The album's title
|
||||
* - `artists`
|
||||
- Array of strings
|
||||
- A list of [`Artist` objects](#artist) associated with the albums
|
||||
* - `published`*
|
||||
- Datetime
|
||||
- The date on which the artist was published over the federation
|
||||
* - `released`
|
||||
- Datetime
|
||||
- The date on which the album was released
|
||||
* - `musicbrainzId`
|
||||
- String (UUID)
|
||||
- The Musicbrainz release ID
|
||||
* - `cover`
|
||||
- [`Link` object](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link)
|
||||
- A `Link` object representing the album cover
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"type": "Album",
|
||||
"id": "https://awesome.music/federation/music/albums/69d488b5-fdf6-4803-b47c-9bb7098ea57e",
|
||||
"name": "Ride the Lightning",
|
||||
"released": "1984-01-01",
|
||||
"published": "2018-10-02T19:49:17.412546+00:00",
|
||||
"musicbrainzId": "589ff96d-0be8-3f82-bdd2-299592e51b40",
|
||||
"cover": {
|
||||
"href": "https://awesome.music/media/albums/covers/2018/10/02/b69d398b5-fdf6-4803-b47c-9bb7098ea57e.jpg",
|
||||
"type": "Link",
|
||||
"mediaType": "image/jpeg"
|
||||
},
|
||||
"artists": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Track
|
||||
|
||||
A `Track` is a custom object used to store track information.
|
||||
|
||||
#### Properties
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`*
|
||||
- String
|
||||
- The object type (`Track`)
|
||||
* - `id`*
|
||||
- String (URI)
|
||||
- A URI that identifies the track over federation
|
||||
* - `name`*
|
||||
- String
|
||||
- The track title
|
||||
* - `position`*
|
||||
- Integer
|
||||
- The position of the track in the album
|
||||
* - `published`*
|
||||
- Datetime
|
||||
- The date on which the track was published over the federation
|
||||
* - `musicbrainzId`
|
||||
- String (UUID)
|
||||
- The Musicbrainz recording ID
|
||||
* - `album`
|
||||
- [`Album` object](#album)
|
||||
- The album that contains the track
|
||||
* - `artists`
|
||||
- Array of [`Artist` objects](#artist)
|
||||
- A list of artists associated to the track. This can differ from the album artists
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"type": "Track",
|
||||
"id": "https://awesome.music/federation/music/tracks/82ece296-6397-4e26-be90-bac5f9990240",
|
||||
"name": "For Whom the Bell Tolls",
|
||||
"position": 3,
|
||||
"published": "2018-10-02T19:49:35.822537+00:00",
|
||||
"musicbrainzId": "771ab043-8821-44f9-b8e0-2733c3126c6d",
|
||||
"artists": [
|
||||
{}
|
||||
],
|
||||
"album": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Library
|
||||
|
||||
```{note}
|
||||
Crawling library pages requires authentication and an approved follow unless the library is public.
|
||||
```
|
||||
|
||||
A `Library` is a custom object used to store music collection information. It inherits its behavior and properties from ActivityPub's [`Actor`](https://www.w3.org/TR/activitypub/#actors) and [`Collection`](https://www.w3.org/TR/activitypub/#collections) objects.
|
||||
|
||||
#### Properties
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`*
|
||||
- String
|
||||
- The object type (`Library`)
|
||||
* - `id`*
|
||||
- String (URI)
|
||||
- A URI that identifies the library over federation
|
||||
* - `name`*
|
||||
- String
|
||||
- The library's name
|
||||
* - `followers`*
|
||||
- String (URI)
|
||||
- The ID of the library's followers collection
|
||||
* - `totalItems`*
|
||||
- Integer
|
||||
- The number of [`Audio` objects](#audio) in the library
|
||||
* - `first`*
|
||||
- String (URI)
|
||||
- The URL of the library's first page
|
||||
* - `last`*
|
||||
- String (URI)
|
||||
- The URL of the library's last page
|
||||
* - `summary`
|
||||
- String
|
||||
- The library's description
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"type": "Library",
|
||||
"id": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6",
|
||||
"attributedTo": "https://awesome.music/federation/actors/Alice",
|
||||
"name": "My awesome library",
|
||||
"followers": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers",
|
||||
"summary": "This library is for restricted use only",
|
||||
"totalItems": 4234,
|
||||
"first": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6?page=1",
|
||||
"last": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6?page=56",
|
||||
}
|
||||
```
|
||||
|
||||
### Audio
|
||||
|
||||
```{note}
|
||||
Accessing audio files requires authentication and an approved follow for the containing library unless the library is public.
|
||||
```
|
||||
|
||||
An `Audio` object is a custom object used to store upload information. It extends the [ActivityStreams Audio object](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio) with custom attributes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Property
|
||||
- Data type
|
||||
- Description
|
||||
* - `type`*
|
||||
- String
|
||||
- The object type (`Audio`)
|
||||
* - `id`*
|
||||
- String (URI)
|
||||
- A URI that identifies the audio over federation
|
||||
* - `name`*
|
||||
- String
|
||||
- A readable title for the order. Funkwhale concatenates the track name, album title, and artist name
|
||||
* - `size`*
|
||||
- Integer
|
||||
- The size of the audio in bytes
|
||||
* - `bitrate`*
|
||||
- Integer
|
||||
- The bitrate of the audio in bytes/s
|
||||
* - `duration`*
|
||||
- Integer
|
||||
- The duration of the audio in seconds
|
||||
* - `library`*
|
||||
- String (URI)
|
||||
- The ID of the audio's containing [`Library` object](#library)
|
||||
* - `published`*
|
||||
- Datetime
|
||||
- The date on which the audio was published over the federation
|
||||
* - `updated`*
|
||||
- Datetime
|
||||
- The date on which the audio was last updated over the federation
|
||||
* - `url`*
|
||||
- [`Link` object](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link)
|
||||
- A `Link` object object containing the download location of the audio file
|
||||
* - `track`
|
||||
- [`Track` object](#track)
|
||||
- The track associated with the audio file
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```{code-block} json
|
||||
{
|
||||
"type": "Audio",
|
||||
"id": "https://awesome.music/federation/music/uploads/88f0bc20-d7fd-461d-a641-dd9ac485e096",
|
||||
"name": "For Whom the Bell Tolls - Ride the Lightning - Metallica",
|
||||
"size": 8656581,
|
||||
"bitrate": 320000,
|
||||
"duration": 213,
|
||||
"library": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6",
|
||||
"updated": "2018-10-02T19:49:35.646372+00:00",
|
||||
"published": "2018-10-02T19:49:35.646359+00:00",
|
||||
"track": {},
|
||||
"url": {
|
||||
"href": "https://awesome.music/api/v1/listen/82ece296-6397-4e26-be90-bac5f9990240/?upload=88f0bc20-d7fd-461d-a641-dd9ac485e096",
|
||||
"type": "Link",
|
||||
"mediaType": "audio/mpeg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Audio fetching on restricted libraries
|
||||
|
||||
[`Library` objects](#library) and [`Audio` objects] are subject to the following access rules:
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Library
|
||||
|
||||
- Public libraries can be accessed by actors without restriction
|
||||
- Restricted libraries can only be accessed if the HTTP request is signed by an actor who has an associated **approved** [`Follow` activity](#follow)
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Audio
|
||||
|
||||
- Audio items in public libraries can be accessed by actors without restriction
|
||||
- Audio items in restricted libraries can only be accessed if the HTTP request is signed by an actor who has an associated **approved** [`Follow` activity](#follow)
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
## Custom properties
|
||||
|
||||
### attributedTo
|
||||
|
||||
Funkwhale uses the `attributedTo` property to denote the actor responsible for an object. If an object has an `attributedTo` attributed, the associated actor can perform activities to it, including [`Update`](#update) and [`Delete`](#delete).
|
||||
|
||||
Funkwhale also attributes all objects on a domain with the domain's [Service actor](#service-actor)
|
||||
15
docs/developer/index.md
Normal file
15
docs/developer/index.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Get started
|
||||
|
||||
Funkwhale welcomes contributions from all developers. If this is your first time contributing to an open source project, don't be afraid to get stuck in! The Funkwhale community will guide you through the process and help you grow your confidence.
|
||||
|
||||
## Set up your development environment
|
||||
|
||||
Before you begin, you need to set up a development environment. Follow the [guides in the setup section](setup/index.md) to set up an environment that's right for you.
|
||||
|
||||
## Read up on our processes
|
||||
|
||||
The Funkwhale project follows a few processes to make managing contributions easier. If you're not sure how to get started, check out the [guides in the workflows section](workflows/index.md) to get a better understanding of what you need to do.
|
||||
|
||||
## Contribute to the codebase
|
||||
|
||||
Ready to get stuck in? Take a look at the [contribution guides](contribute/index.md) and start making your changes!
|
||||
200
docs/developer/plugins/create.md
Normal file
200
docs/developer/plugins/create.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Write a plugin
|
||||
|
||||
You can write plugins to extend the features of your Funkwhale pod. Follow the instructions in this guide to get started with your first plugin.
|
||||
|
||||
```{contents}
|
||||
:local:
|
||||
:depth: 2
|
||||
```
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you start writing your plugin, you need to understand the following core concepts:
|
||||
|
||||
```{contents}
|
||||
:local:
|
||||
:depth: 1
|
||||
```
|
||||
|
||||
We'll explain each of these concepts in the next few sections
|
||||
|
||||
### Scopes
|
||||
|
||||
Plugins fall into two different **scopes**:
|
||||
|
||||
1. User-level plugins that are configured by end-users for their own use
|
||||
2. Pod-level plugins that are configured by pod admins and are not connected to a particular user
|
||||
|
||||
User-level plugins can also be used to import files from a third-party service, such as cloud storage or FTP.
|
||||
|
||||
### Hooks
|
||||
|
||||
**Hooks** are entrypoints that allow your plugin to listen to changes. You can create hooks to react to different events that occur in the Funkwhale application.
|
||||
|
||||
An example of this can be seen in our Scrobbler plugin. We register a `LISTENING_CREATED` hook to notify any registered callback function when a listening is recorded. When a user listens to a track, the `notfy_lastfm` function fires.
|
||||
|
||||
```{code-block} python
|
||||
from config import plugins
|
||||
from .funkwhale_startup import PLUGIN
|
||||
|
||||
@plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN)
|
||||
def notify_lastfm(listening, conf, **kwargs):
|
||||
# do something
|
||||
```
|
||||
|
||||
#### Available hooks
|
||||
|
||||
```{eval-rst}
|
||||
.. autodata:: config.plugins.LISTENING_CREATED
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
**Filters** are entrypoints that allow you to modify or add information. When you use the `register_filter` decorator, your function should return a value to be used by the server.
|
||||
|
||||
In this example, the `PLUGINS_DEPENDENCIES` filter is used to install additional dependencies required by your plugin. The `dependencies` function returns the additional dependency `django_prometheus` to request the dependency be installed by the server.
|
||||
|
||||
```{code-block} python
|
||||
# funkwhale_startup.py
|
||||
# ...
|
||||
from config import plugins
|
||||
|
||||
@plugins.register_filter(plugins.PLUGINS_DEPENDENCIES, PLUGIN)
|
||||
def dependencies(dependencies, **kwargs):
|
||||
return dependencies + ["django_prometheus"]
|
||||
|
||||
```
|
||||
|
||||
#### Available filters
|
||||
|
||||
```{eval-rst}
|
||||
.. autodata:: config.plugins.PLUGINS_DEPENDENCIES
|
||||
.. autodata:: config.plugins.PLUGINS_APPS
|
||||
.. autodata:: config.plugins.MIDDLEWARES_BEFORE
|
||||
.. autodata:: config.plugins.MIDDLEWARES_AFTER
|
||||
.. autodata:: config.plugins.URLS
|
||||
```
|
||||
|
||||
## Write your plugin
|
||||
|
||||
Once you know what type of plugin you want to write and what entrypoint you want to use, you can start writing your plugin.
|
||||
|
||||
Plugins are made up of the following 3 files:
|
||||
|
||||
- `__init__.py` - indicates that the directory is a Python package
|
||||
- `funkwhale_startup.py` - the file that loads during Funkwhale initialization
|
||||
- `funkwhale_ready.py` - the file that loads when Funkwhale is configured and ready
|
||||
|
||||
### Declare your plugin
|
||||
|
||||
You need to declare your plugin and its configuration options so that Funkwhale knows how to load the plugin. To do this, you must declare a new `plugins` instance in your `funkwhale_startup.py` file.
|
||||
|
||||
Your `plugins` should include the following information:
|
||||
|
||||
```{list-table}
|
||||
:header-rows: 1
|
||||
|
||||
* - Parameter
|
||||
- Data type
|
||||
- Description
|
||||
* - `name`
|
||||
- String
|
||||
- The name of your plugin, used in the `.env` file
|
||||
* - `label`
|
||||
- String
|
||||
- The readable label that appears in the Funkwhale frontend
|
||||
* - `description`
|
||||
- String
|
||||
- A meaningful description of your plugin and what it does
|
||||
* - `version`
|
||||
- String
|
||||
- The version number of your plugin
|
||||
* - `user`
|
||||
- Boolean
|
||||
- Whether the plugin is a **user-level** plugin or a **pod-level** plugin. See [scopes](#scopes) for more information
|
||||
* - `conf`
|
||||
- Array of Objects
|
||||
- A list of configuration options
|
||||
|
||||
```
|
||||
|
||||
In this example, we declare a new **user-level** plugin called "My Plugin". The user can configure a `greeting` in the plugin configuration.
|
||||
|
||||
```{code-block} python
|
||||
# funkwhale_startup.py
|
||||
from config import plugins
|
||||
|
||||
PLUGIN = plugins.get_plugin_config(
|
||||
name="myplugin",
|
||||
label="My Plugin",
|
||||
description="An example plugin that greets you",
|
||||
version="0.1",
|
||||
user=True,
|
||||
conf=[
|
||||
# This configuration option is editable by each user
|
||||
{"name": "greeting", "type": "text", "label": "Greeting", "default": "Hello"},
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Write your plugin logic
|
||||
|
||||
Once you've declared your plugin, you can write the plugin code in your `funkwhale_ready.py` file.
|
||||
|
||||
```{note}
|
||||
You must import your plugin declaration from your `funkwhale_startup.py` file.
|
||||
```
|
||||
|
||||
In this example, we create a simple API endpoint that returns a greeting to the user. To do this:
|
||||
|
||||
1. We create a new APIView class that accepts a `GET` request
|
||||
2. We read the greeting value from the plugin `conf`
|
||||
3. We return the greeting value with the user's username
|
||||
4. We register this view at the endpoint `/greeting`
|
||||
|
||||
```{code-block} python
|
||||
# funkwhale_ready.py
|
||||
from django.urls import path
|
||||
from rest_framework import response
|
||||
from rest_framework import views
|
||||
|
||||
from config import plugins
|
||||
|
||||
# Import the plugin declaration from funkwhale_startup
|
||||
from .funkwhale_startup import PLUGIN
|
||||
|
||||
# Create a new APIView class
|
||||
class GreetingView(views.APIView):
|
||||
permission_classes = []
|
||||
# Register a GET response
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Check the conf value of the plugin for the user
|
||||
conf = plugins.get_conf(PLUGIN["name"], request.user)
|
||||
if not conf["enabled"]:
|
||||
# Return an error code if the user hasn't enabled the plugin
|
||||
return response.Response(status=405)
|
||||
# Set the greeting value to the user's configured greeting
|
||||
greeting = conf["conf"]["greeting"]
|
||||
data = {
|
||||
# Append the user's username to the greeting
|
||||
"greeting": "{} {}!".format(greeting, request.user.username)
|
||||
}
|
||||
# Return the greeting
|
||||
return response.Response(data)
|
||||
|
||||
# Register the new APIView at the /greeting endpoint
|
||||
@plugins.register_filter(plugins.URLS, PLUGIN)
|
||||
def register_view(urls, **kwargs):
|
||||
return urls + [
|
||||
path('greeting', GreetingView.as_view())
|
||||
]
|
||||
```
|
||||
|
||||
### Result
|
||||
|
||||
Here is an example of how the above plugin works:
|
||||
|
||||
1. User "Harry" enables the plugin
|
||||
2. "Harry" changes the greeting to "You're a wizard"
|
||||
3. "Harry" visits the `/greeting` endpoint in their browser
|
||||
4. The browser returns the message "You're a wizard Harry"
|
||||
16
docs/developer/plugins/index.md
Normal file
16
docs/developer/plugins/index.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Funkwhale plugins
|
||||
|
||||
Plugins can be used to extend Funkwhale's featureset without needing to touch the underlying code. Plugins can extend existing features, add support for third-party services, or introduce cosmetic changes to the Funkwhale webapp.
|
||||
|
||||
Plugins have been supported since Funkwhale 1.0. Some core plugins, such as the standard Scrobbler plugin, are maintained by the Funkwhale team.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
caption: Resources
|
||||
maxdepth: 1
|
||||
---
|
||||
|
||||
create
|
||||
install
|
||||
|
||||
```
|
||||
50
docs/developer/plugins/install.md
Normal file
50
docs/developer/plugins/install.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Install a plugin
|
||||
|
||||
Once you have [created your plugin](create.md), you can install it on your Funkwhale pod.
|
||||
|
||||
## Install a local plugin
|
||||
|
||||
To install a plugin located on your server:
|
||||
|
||||
1. Add the plugin directory to the `FUNKWHALE_PLUGINS_PATH` variable in your `.env` file
|
||||
2. Add the plugin name to the `FUNKWHALE_PLUGINS` variable in your `.env` file
|
||||
|
||||
```{code-block} text
|
||||
FUNKWHALE_PLUGINS=myplugin,anotherplugin
|
||||
```
|
||||
|
||||
3. Restart Funkwhale to pick up the changes
|
||||
|
||||
## Install a third-party plugin
|
||||
|
||||
You can install third-party plugins using the `funkwhale-manage` command line interface. To do this:
|
||||
|
||||
1. Add the plugin name to the `FUNKWHALE_PLUGINS` variable in your `.env` file
|
||||
|
||||
```{code-block} text
|
||||
FUNKWHALE_PLUGINS=myplugin,anotherplugin
|
||||
```
|
||||
|
||||
2. Call the `funkwhale-manage` command line interface with the location of the plugin archive
|
||||
|
||||
:::: {tab-set}
|
||||
|
||||
:::{tab-item} Debian
|
||||
|
||||
```{code-block} shell
|
||||
venv/bin/funkwhale-manage fw plugins install https://plugin_url.zip
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Docker
|
||||
|
||||
```{code-block} shell
|
||||
sudo docker compose run --rm api funkwhale-manage fw plugins install https://plugin_url.zip
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
3. Restart Funkwhale to pick up the changes
|
||||
202
docs/developer/setup/docker.md
Normal file
202
docs/developer/setup/docker.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Develop using Docker
|
||||
|
||||
Funkwhale can be run in Docker containers for local development. You can work on any part of the Funkwhale codebase and run the container setup to test your changes. To work with Docker:
|
||||
|
||||
1. [Install Docker](https://docs.docker.com/install)
|
||||
2. [Install docker compose](https://docs.docker.com/compose/install)
|
||||
3. Clone the Funkwhale repository to your system. The `develop` branch is checked out by default
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} SSH
|
||||
|
||||
```sh
|
||||
git clone git@dev.funkwhale.audio/funkwhale/funkwhale.git
|
||||
cd funkwhale
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} HTTPS
|
||||
|
||||
```sh
|
||||
git clone https://dev.funkwhale.audio/funkwhale/funkwhale.git
|
||||
cd funkwhale
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
## Set up your Docker environment
|
||||
|
||||
````{note}
|
||||
|
||||
Funkwhale provides a `dev.yml` file that contains the required docker compose setup. You need to pass the `-f dev.yml` flag you run docker compose commands to ensure it uses this file. If you don't want to add this each time, you can export it as a `COMPOSE_FILE` variable:
|
||||
|
||||
```sh
|
||||
export COMPOSE_FILE=dev.yml
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
To set up your Docker environment:
|
||||
|
||||
1. Create a `.env` file to enable customization of your setup.
|
||||
|
||||
```sh
|
||||
touch .env
|
||||
```
|
||||
|
||||
2. Add the following variables to load images and enable access to Django admin pages:
|
||||
|
||||
```text
|
||||
MEDIA_URL=http://localhost:8000/media/
|
||||
STATIC_URL=http://localhost:8000/staticfiles/
|
||||
```
|
||||
|
||||
3. Create a network for federation support
|
||||
|
||||
```sh
|
||||
sudo docker network create federation
|
||||
```
|
||||
|
||||
Once you've set everything up, you need to build the containers. Run this command any time there are upstream changes or dependency changes to ensure you're up-to-date.
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml build
|
||||
```
|
||||
|
||||
## Set up the database
|
||||
|
||||
Funkwhale relies on a postgresql database to store information. To set this up, you need to run the `funkwhale-manage migrate` command:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm api funkwhale-manage migrate
|
||||
```
|
||||
|
||||
This command creates all the required tables. You need to run this whenever there are changes to the API schema. You can run this at any time without causing issues.
|
||||
|
||||
## Set up local data
|
||||
|
||||
You need to create some local data to mimic a production environment.
|
||||
|
||||
1. Create a superuser so you can log in to your local app:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm api funkwhale-manage createsuperuser
|
||||
```
|
||||
|
||||
2. Add some fake data to populate the database. The following command creates 25 artists with random albums, tracks, and metadata.
|
||||
|
||||
```sh
|
||||
artists=25 # Adds 25 fake artists
|
||||
command="from funkwhale_api.music import fake_data; fake_data.create_data($artists)"
|
||||
echo $command | sudo docker compose -f dev.yml run --rm -T api funkwhale-manage shell -i python
|
||||
```
|
||||
|
||||
## Manage services
|
||||
|
||||
Once you have set up your containers, bring them up to start working on them.
|
||||
|
||||
1. Compile the translations:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml run --rm front yarn run i18n-compile
|
||||
```
|
||||
|
||||
2. Launch all services:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml up front api nginx celeryworker
|
||||
```
|
||||
|
||||
This gives you access to the following:
|
||||
|
||||
- The Funkwhale webapp on `http://localhost:8000`
|
||||
- The Funkwhale API on `http://localhost:8000/api/v1`
|
||||
- The Django admin interface on `http://localhost:8000/api/admin`
|
||||
|
||||
Once you're done with the containers, you can stop them all:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml stop
|
||||
```
|
||||
|
||||
If you want to destroy your containers, run the following:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f dev.yml down -v
|
||||
```
|
||||
|
||||
## Set up federation support
|
||||
|
||||
Working on federation features requires some additional setup. You need to do the following:
|
||||
|
||||
1. Update your DNS resolver to resolve all your .dev hostnames locally
|
||||
2. Set up a reverse proxy (such as traefik) to catch .dev requests with a TLS certificate
|
||||
3. Set up two or more local instances
|
||||
|
||||
To resolve hostnames locally, run the following:
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} dnsmasq
|
||||
|
||||
```sh
|
||||
echo "address=/test/172.17.0.1" | sudo tee /etc/dnsmasq.d/test.conf
|
||||
sudo systemctl restart dnsmasq
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} NetworkManager
|
||||
|
||||
```sh
|
||||
echo "address=/test/172.17.0.1" | sudo tee /etc/NetworkManager/dnsmasq.d/test.conf
|
||||
sudo systemctl restart NetworkManager
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
To add a wildcard certificate, copy the test certificate from the `docker/ssl` folder. This certificate is a wildcard for `*.funkwhale.test`
|
||||
|
||||
```sh
|
||||
sudo cp docker/ssl/test.crt /usr/local/share/ca-certificates/
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
To run a reverse proxy for your app:
|
||||
|
||||
1. Add the following configuration to your `.env` file:
|
||||
|
||||
```text
|
||||
# Remove any port binding so you can specify this per-instance
|
||||
VUE_PORT_BINDING=
|
||||
# Disable certificate validation
|
||||
EXTERNAL_REQUESTS_VERIFY_SSL=false
|
||||
# Ensure all links use https
|
||||
FUNKWHALE_PROTOCOL=https
|
||||
# Disable host ports binding for the nginx container so that traefik handles everything
|
||||
NGINX_PORTS_MAPPING=80
|
||||
```
|
||||
|
||||
2. Launch traefik using the bundled configuration:
|
||||
|
||||
```sh
|
||||
sudo docker compose -f docker/traefik.yml up -d
|
||||
```
|
||||
|
||||
3. Set up as many different projects as you need. Make sure the `COMPOSE_PROJECT_NAME` and `VUE_PORT` variables are unique per instance
|
||||
|
||||
```sh
|
||||
export COMPOSE_PROJECT_NAME=node2
|
||||
export VUE_PORT=1234 # this has to be unique for each instance
|
||||
sudo docker compose -f dev.yml run --rm api funkwhale-manage migrate
|
||||
sudo docker compose -f dev.yml run --rm api funkwhale-manage createsuperuser
|
||||
sudo docker compose -f dev.yml up nginx api front nginx api celeryworker
|
||||
```
|
||||
|
||||
You can access your project at `https://{COMPOSE_PROJECT_NAME}.funkwhale.test`.
|
||||
BIN
docs/developer/setup/gitpod-select-gitpod-in-gitlab.png
Normal file
BIN
docs/developer/setup/gitpod-select-gitpod-in-gitlab.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
73
docs/developer/setup/gitpod.md
Normal file
73
docs/developer/setup/gitpod.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Develop using Gitpod
|
||||
|
||||
```{note}
|
||||
You need a GitHub or GitLab.com account to log in to Gitpod.
|
||||
```
|
||||
|
||||
Funkwhale has a Gitpod instance that gives you all the tools you need to work on Funkwhale's code. You can work on the code in-browser using a hosted VS Code install or open VS Code on your desktop over SSH.
|
||||
|
||||
You can open Gitpod directly by clicking the link below. This checks out the `develop` branch for you to work on directly.
|
||||
|
||||
[](https://gitpod.io/#https://dev.funkwhale.audio/funkwhale/funkwhale)
|
||||
|
||||
If you want to work on a particular branch, commit, or merge request, you can do this straight from the GitLab interface. Select the arrow icon on the {guilabel}`Web IDE` button and select {guilabel}`Gitpod` to open Gitpod with the currently selected branch checked out.
|
||||
|
||||

|
||||
|
||||
When you start Gitpod, it creates the following using the selected branch:
|
||||
|
||||
- A Funkwhale API instance
|
||||
- A Funkwhale frontend instance
|
||||
|
||||
You can access the web app at `http://localhost:8000`. Log in with the following credentials:
|
||||
|
||||
- Username – `gitpod`
|
||||
- Password – `gitpod`
|
||||
|
||||
## Work on the frontend
|
||||
|
||||
By default, Gitpod spins up an entire Funkwhale stack. If you want to work only on the frontend:
|
||||
|
||||
1. Select `File` > `Open Folder`
|
||||
2. Select `/workspace/funkwhale/front`
|
||||
|
||||
Gitpod starts a new Vite server on port 4000. This creates a frontend that isn't connected to any instance.
|
||||
|
||||
## GitLab Workflow extension
|
||||
|
||||
Gitpod offers a GitLab workflow extension to help manage GitLab issues, merge requests, and pipelines. If you want to use it:
|
||||
|
||||
1. Navigate to the personal access token section of your [GitLab profile settings](https://dev.funkwhale.audio/-/profile/personal_access_tokens)
|
||||
2. Create a personal access token with `api` and `read_user` scopes
|
||||
3. Paste your token into your [Gitpod variables](https://gitpod.io/variables)
|
||||
|
||||
Use the following settings to automatically sign in to the extension with Gitpod. The `funkwhale/*` scope ensures you can use the settings for all Funkwhale-hosted projects.
|
||||
|
||||
```{list-table} Environment variables
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Value
|
||||
- Scope
|
||||
* - `GITLAB_WORKFLOW_INSTANCE_URL`
|
||||
- `https://dev.funkwhale.audio`
|
||||
- `funkwhale/*`
|
||||
* - `GITLAB_WORKFLOW_TOKEN`
|
||||
- Your token
|
||||
- `funkwhale/*`
|
||||
```
|
||||
|
||||
## Configure custom instance URL
|
||||
|
||||
You can configure Gitpod to use your Funkwhale pod as the default server. This means you can test frontend changes on your pod without selecting it each time. To do this, add the following to your [Gitpod variables](https://gitpod.io/variables):
|
||||
|
||||
```{list-table} Environment variables
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Value
|
||||
- Scope
|
||||
* - `VUE_APP_INSTANCE_URL`
|
||||
- `https://funkwhale.example.com`
|
||||
- `funkwhale/funkwhale`
|
||||
```
|
||||
15
docs/developer/setup/index.md
Normal file
15
docs/developer/setup/index.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Set up your development environment
|
||||
|
||||
Follow the instructions in these guides to set up your development environment.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
caption: Choose your setup
|
||||
maxdepth: 1
|
||||
---
|
||||
|
||||
gitpod
|
||||
docker
|
||||
vite
|
||||
|
||||
```
|
||||
48
docs/developer/setup/vite.md
Normal file
48
docs/developer/setup/vite.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Develop using Vite
|
||||
|
||||
If you want to make changes to the frontend, you can use Vite to run a development server. This allows you to run a Funkwhale web app and see changes in real time
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} SSH
|
||||
|
||||
```sh
|
||||
git clone git@dev.funkwhale.audio/funkwhale/funkwhale.git
|
||||
cd funkwhale/front
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} HTTPS
|
||||
|
||||
```sh
|
||||
git clone https://dev.funkwhale.audio/funkwhale/funkwhale.git
|
||||
cd funkwhale/front
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
2. Install [Node.js](https://nodejs.org/en/download/package-manager/) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
|
||||
3. Install all dependencies:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
4. Compile the translations:
|
||||
|
||||
```sh
|
||||
yarn i18n-compile
|
||||
```
|
||||
|
||||
5. Launch the development server:
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
You can access the Funkwhale web app at `http://localhost:8000/front`. Connect this app to your pod by selecting {guilabel}`Switch instance` in the sidebar.
|
||||
33
docs/developer/workflows/changelog.md
Normal file
33
docs/developer/workflows/changelog.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Changelog fragments
|
||||
|
||||
We try to add changelog fragments when we make changes so that we can show users what we've done. These fragments are small text files that contain a summary of changes. When we make a release, we compile these into a full changelog using [towncrier](https://pypi.org/project/towncrier/).
|
||||
|
||||
Each changelog fragment should contain a short and meaningful summary of changes and include the issue number (where applicable). For example:
|
||||
|
||||
```text
|
||||
Fixed broken audio player on Chrome 42 for ogg files (#567)
|
||||
```
|
||||
|
||||
If there's no issue, insert the merge request identifier instead:
|
||||
|
||||
```text
|
||||
Fixed a typo in landing page copy (!342)
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
Changelog fragments use the following naming convention: `changes/changelog.d/<name>.category>`. The `<name>` can be anything that describes your work, or the issue ID. The category can be one of the following:
|
||||
|
||||
- `feature` – a new feature
|
||||
- `enhancement` – an extension of an existing feature
|
||||
- `bugfix` – a bugfix or patch
|
||||
- `refactoring` – refactored code
|
||||
- `doc` – new documentation
|
||||
- `i18n` – internationalization-related work
|
||||
- `misc` – any work that doesn't fit into the above categories
|
||||
|
||||
You can create these files manually or use the following command to create a fragment:
|
||||
|
||||
```sh
|
||||
towncrier new --edit $issue.$category
|
||||
```
|
||||
53
docs/developer/workflows/git.md
Normal file
53
docs/developer/workflows/git.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Git workflow
|
||||
|
||||
Funkwhale uses GitLab's merge requests to manage changes. The workflow looks like this:
|
||||
|
||||
1. Assign the issue you are working on to yourself, or create one if it doesn't exist
|
||||
2. Create a fork of the project
|
||||
3. Check out the `develop` branch. If you're making a minor change (such as fixing a typo) you can check out the `stable` branch
|
||||
4. Create a new branch based on the checked out branch. Make sure to give your branch a meaningful name and include the issue number if required
|
||||
5. Work on your changes locally. Try to keep each commit small to make reviews easier
|
||||
6. Add a changelog fragment summarizing your changes
|
||||
7. Lint the codebase using the following command:
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} API code
|
||||
|
||||
```sh
|
||||
black --check --diff . # Run the black linter in the project root to highlight any new issues
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Frontend code
|
||||
|
||||
```sh
|
||||
cd front
|
||||
yarn run eslint # Run eslint in the front directory
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
8. Push your branch
|
||||
9. Create a merge request in the GitLab frontend
|
||||
10. We'll review your request and feed back
|
||||
|
||||
```{mermaid}
|
||||
%%{init: { 'gitGraph': {'mainBranchName': 'stable'} } }%%
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
commit
|
||||
commit
|
||||
branch feature
|
||||
commit
|
||||
commit
|
||||
checkout develop
|
||||
merge feature
|
||||
commit
|
||||
checkout stable
|
||||
merge develop
|
||||
```
|
||||
16
docs/developer/workflows/index.md
Normal file
16
docs/developer/workflows/index.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Development workflows
|
||||
|
||||
Funkwhale follows workflows for each area of development and release management. You can find a breakdown of these in this section.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
caption: Workflows
|
||||
maxdepth: 1
|
||||
---
|
||||
|
||||
git
|
||||
pre-commit
|
||||
changelog
|
||||
release
|
||||
|
||||
```
|
||||
20
docs/developer/workflows/pre-commit.md
Normal file
20
docs/developer/workflows/pre-commit.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Pre-commit
|
||||
|
||||
Funkwhale uses [pre-commit](https://pre-commit.com/) to ensure that the files you commit are properly formatted, follow best practice, and don't contain syntax or spelling errors.
|
||||
|
||||
You can install and setup pre-commit using the [quick-start guide on the pre-commit documentation](https://pre-commit.com/#quick-start). Make sure to install pre-commit and setup the git pre-commit hook so pre-commit runs before you commit any changes to the repository.
|
||||
|
||||
The workflow looks like this:
|
||||
|
||||
1. Install `pre-commit`.
|
||||
2. After cloning the repository, setup the pre-commit git hooks:
|
||||
|
||||
```sh
|
||||
git clone git@dev.funkwhale.audio:funkwhale/funkwhale.git
|
||||
cd funkwhale
|
||||
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
3. Make your changes and commit them.
|
||||
4. If `pre-commit` fails to validate your changes, the commit process stops. Fix any reported errors and try again.
|
||||
109
docs/developer/workflows/release.md
Normal file
109
docs/developer/workflows/release.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Make a release
|
||||
|
||||
Once we're ready to release a new version of the software, we can use the following process:
|
||||
|
||||
1. Export the new release version
|
||||
|
||||
```sh
|
||||
export NEXT_RELEASE=1.3.0
|
||||
```
|
||||
|
||||
2. Export the previous release version
|
||||
|
||||
```sh
|
||||
export PREVIOUS_RELEASE=1.2.9
|
||||
```
|
||||
|
||||
3. Pull the latest version of the `develop` branch. Use `stable` if you're releasing a bugfix.
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Bugfix release
|
||||
:sync: bugfix
|
||||
|
||||
```sh
|
||||
git checkout stable
|
||||
git pull
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Feature release
|
||||
:sync: feature
|
||||
|
||||
```sh
|
||||
git checkout develop
|
||||
git pull
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
4. Compile the changelog
|
||||
|
||||
```sh
|
||||
towncrier build --version $NEXT_RELEASE
|
||||
```
|
||||
|
||||
5. Check the output and fix typos and mistakes
|
||||
6. Add a list of contributors
|
||||
|
||||
```sh
|
||||
python3 scripts/get-contributions-stats.py $NEXT_RELEASE # Output a list of contributors
|
||||
git log $PREVIOUS_RELEASE.. --format="- %aN" --reverse | sort | uniq # Get a list of all commit authors
|
||||
nano CHANGELOG.md # Add these lists to the CHANGELOG.md
|
||||
```
|
||||
|
||||
7. Update the next release version
|
||||
|
||||
```sh
|
||||
cd api
|
||||
poetry version "$NEXT_RELEASE"
|
||||
cd ..
|
||||
```
|
||||
|
||||
8. Commit all changes
|
||||
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "Version bump and changelog for $NEXT_RELEASE"
|
||||
```
|
||||
|
||||
9. Create a tag
|
||||
|
||||
```sh
|
||||
git tag $NEXT_RELEASE
|
||||
```
|
||||
|
||||
10. Publish the new tag to GitLab
|
||||
|
||||
```sh
|
||||
git push --tags && git push
|
||||
```
|
||||
|
||||
11. Merge your changes into the alternate branch
|
||||
|
||||
::::{tab-set}
|
||||
|
||||
:::{tab-item} Bugfix release
|
||||
:sync: bugfix
|
||||
|
||||
```sh
|
||||
git checkout develop && git merge stable && git push
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{tab-item} Feature release
|
||||
:sync: feature
|
||||
|
||||
```sh
|
||||
git checkout stable && git merge develop && git push
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
Don't forget to create a blog post to announce the new release!
|
||||
Loading…
Add table
Add a link
Reference in a new issue