Full Code

Transmission app is already included in FreedomBox. Here is the full source for the module for reference.

plinth/modules/transmission/__init__.py

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app to configure Transmission server.
"""

from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _

from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import (Firewall,
                                                FirewallLocalProtection)
from plinth.modules.users import add_user_to_share_group
from plinth.modules.users.components import UsersAndGroups
from plinth.package import Packages
from plinth.utils import format_lazy

from . import manifest, privileged

_description = [
    _('Transmission is a BitTorrent client with a web interface.'),
    _('BitTorrent is a peer-to-peer file sharing protocol. '
      'Note that BitTorrent is not anonymous.'),
    _('Please do not change the default port of the Transmission daemon.'),
    format_lazy(
        _('Compared to <a href="{deluge_url}">'
          'Deluge</a>, Transmission is simpler and lightweight but is less '
          'customizable.'), deluge_url=reverse_lazy('deluge:index')),
    format_lazy(
        _('It can be accessed by <a href="{users_url}">any user</a> on '
          '{box_name} belonging to the bit-torrent group.'),
        box_name=_(cfg.box_name), users_url=reverse_lazy('users:index')),
    format_lazy(
        _('In addition to the web interface, mobile and desktop apps can also '
          'be used to remotely control Transmission on {box_name}. To '
          'configure remote control apps, use the URL '
          '<a href="/transmission-remote/rpc">/transmission-remote/rpc</a>.'),
        box_name=_(cfg.box_name)),
    format_lazy(
        _('<a href="{samba_url}">Samba</a> shares can be set as the '
          'default download directory from the dropdown menu below.'),
        samba_url=reverse_lazy('samba:index')),
    format_lazy(
        _('After a download has completed, you can also access your files '
          'using the <a href="{sharing_url}">Sharing</a> app.'),
        sharing_url=reverse_lazy('sharing:index'))
]

SYSTEM_USER = 'debian-transmission'


class TransmissionApp(app_module.App):
    """FreedomBox app for Transmission."""

    app_id = 'transmission'

    _version = 7

    DAEMON = 'transmission-daemon'

    def __init__(self) -> None:
        """Create components for the app."""
        super().__init__()

        groups = {
            'bit-torrent': _('Download files using BitTorrent applications')
        }

        info = app_module.Info(
            app_id=self.app_id, version=self._version, name=_('Transmission'),
            icon_filename='transmission',
            short_description=_('BitTorrent Web Client'),
            description=_description, manual_page='Transmission',
            clients=manifest.clients,
            donation_url='https://transmissionbt.com/donate/')
        self.add(info)

        menu_item = menu.Menu('menu-transmission', info.name,
                              info.short_description, info.icon_filename,
                              'transmission:index', parent_url_name='apps')
        self.add(menu_item)

        shortcut = frontpage.Shortcut(
            'shortcut-transmission', info.name,
            short_description=info.short_description, icon=info.icon_filename,
            url='/transmission', clients=info.clients, login_required=True,
            allowed_groups=list(groups))
        self.add(shortcut)

        packages = Packages('packages-transmission', ['transmission-daemon'])
        self.add(packages)

        dropin_configs = DropinConfigs('dropin-configs-transmission', [
            '/etc/apache2/conf-available/transmission-plinth.conf',
        ])
        self.add(dropin_configs)

        firewall = Firewall('firewall-transmission', info.name,
                            ports=['http', 'https',
                                   'transmission-client'], is_external=True)
        self.add(firewall)

        firewall_local_protection = FirewallLocalProtection(
            'firewall-local-protection-transmission', ['9091'])
        self.add(firewall_local_protection)

        webserver = Webserver('webserver-transmission', 'transmission-plinth',
                              urls=['https://{host}/transmission'],
                              last_updated_version=6)
        self.add(webserver)

        daemon = Daemon(
            'daemon-transmission', self.DAEMON, listen_ports=[
                (9091, 'tcp4'),
                (51413, 'tcp4'),
                (51413, 'tcp6'),
                (51413, 'udp4'),
            ])
        self.add(daemon)

        users_and_groups = UsersAndGroups('users-and-groups-transmission',
                                          reserved_usernames=[SYSTEM_USER],
                                          groups=groups)
        self.add(users_and_groups)

        backup_restore = BackupRestore('backup-restore-transmission',
                                       **manifest.backup)
        self.add(backup_restore)

    def setup(self, old_version):
        """Install and configure the app."""
        super().setup(old_version)

        if old_version and old_version <= 3 and self.is_enabled():
            self.get_component('firewall-transmission').enable()

        new_configuration = {
            'rpc-whitelist-enabled': False,
            'rpc-authentication-required': False
        }
        privileged.merge_configuration(new_configuration)
        add_user_to_share_group(SYSTEM_USER, TransmissionApp.DAEMON)

        if not old_version:
            self.enable()

plinth/modules/transmission/forms.py

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Transmission.
"""

from django.utils.translation import gettext_lazy as _

from plinth.modules.storage.forms import (DirectorySelectForm,
                                          DirectoryValidator)

from . import SYSTEM_USER


class TransmissionForm(DirectorySelectForm):
    """Transmission configuration form"""

    def __init__(self, *args, **kw):
        validator = DirectoryValidator(username=SYSTEM_USER,
                                       check_creatable=True)
        super().__init__(title=_('Download directory'),
                         default='/var/lib/transmission-daemon/downloads',
                         validator=validator, *args, **kw)

plinth/modules/transmission/manifest.py

# SPDX-License-Identifier: AGPL-3.0-or-later

from django.utils.translation import gettext_lazy as _

from plinth.clients import store_url

clients = [{
    'name': _('Transmission'),
    'platforms': [{
        'type': 'web',
        'url': '/transmission'
    }]
}, {
    'name':
        _('Tremotesf'),
    'platforms': [{
        'type': 'store',
        'os': 'android',
        'store_name': 'f-droid',
        'url': store_url('f-droid', 'org.equeim.tremotesf')
    }, {
        'type': 'store',
        'os': 'android',
        'store_name': 'google-play',
        'url': store_url('google-play', 'org.equeim.tremotesf')
    }]
}]

backup = {
    'data': {
        'directories': ['/var/lib/transmission-daemon/.config']
    },
    'secrets': {
        'files': ['/etc/transmission-daemon/settings.json']
    },
    'services': ['transmission-daemon']
}

plinth/modules/transmission/privileged.py

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for Transmission daemon.
"""

import json
import pathlib

from plinth import action_utils
from plinth.actions import privileged

_transmission_config = pathlib.Path('/etc/transmission-daemon/settings.json')


@privileged
def get_configuration() -> dict[str, str]:
    """Return the current configuration in JSON format."""
    return json.loads(_transmission_config.read_text(encoding='utf-8'))


@privileged
def merge_configuration(configuration: dict[str, str | bool]):
    """Merge given JSON configuration with existing configuration."""
    current_configuration_bytes = _transmission_config.read_bytes()
    current_configuration = json.loads(current_configuration_bytes)

    new_configuration = current_configuration
    new_configuration.update(configuration)
    new_configuration_bytes = json.dumps(new_configuration, indent=4,
                                         sort_keys=True)

    _transmission_config.write_text(new_configuration_bytes, encoding='utf-8')
    action_utils.service_reload('transmission-daemon')

plinth/modules/transmission/urls.py

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
URLs for the Transmission module.
"""

from django.urls import re_path

from .views import TransmissionAppView

urlpatterns = [
    re_path(r'^apps/transmission/$', TransmissionAppView.as_view(),
            name='index'),
]

plinth/modules/transmission/views.py

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Transmission Server.
"""

import logging
import socket

from django.contrib import messages
from django.utils.translation import gettext as _

from plinth import views

from . import privileged
from .forms import TransmissionForm

logger = logging.getLogger(__name__)


class TransmissionAppView(views.AppView):
    """Serve configuration page."""
    form_class = TransmissionForm
    app_id = 'transmission'

    def get_initial(self):
        """Get the current settings from Transmission server."""
        status = super().get_initial()
        configuration = privileged.get_configuration()
        status['storage_path'] = configuration['download-dir']
        status['hostname'] = socket.gethostname()

        return status

    def form_valid(self, form):
        """Apply the changes submitted in the form."""
        old_status = form.initial
        new_status = form.cleaned_data
        if old_status['storage_path'] != new_status['storage_path']:
            new_configuration = {
                'download-dir': new_status['storage_path'],
            }
            privileged.merge_configuration(new_configuration)
            messages.success(self.request, _('Configuration updated'))

        return super().form_valid(form)

plinth/modules/transmission/data/usr/share/freedombox/modules-enabled/transmission

plinth.modules.transmission

plinth/modules/transmission/data/usr/share/freedombox/etc/apache2/conf-available/transmission-plinth.conf

##
## On all sites, provide Transmission on a default path: /transmission
##
## Requires the following Apache modules to be enabled:
##   mod_headers
##   mod_proxy
##   mod_proxy_http
##
<Location /transmission>
    ProxyPass http://localhost:9091/transmission

    # If a client sends 'Authorization' HTTP Header, perform Basic authorization
    # using LDAP, otherwise redirect to FreedomBox single sign-on. It is not
    # mandatory for the server to return HTTP 401 with 'WWW-Authenticate'. See
    # https://datatracker.ietf.org/doc/html/rfc2616#section-14.8
    <If "-n %{HTTP:Authorization}">
        Include includes/freedombox-auth-ldap.conf
        Require ldap-group cn=admin,ou=groups,dc=thisbox
        Require ldap-group cn=bit-torrent,ou=groups,dc=thisbox
    </If>
    <Else>
        Include includes/freedombox-single-sign-on.conf
        <IfModule mod_auth_pubtkt.c>
            TKTAuthToken "admin" "bit-torrent"
        </IfModule>
    </Else>

    ## Send the scheme from user's request to enable Transmission to
    ## redirect URLs, set cookies, set absolute URLs (if any)
    ## properly.
    RequestHeader    set X-Forwarded-Proto 'https' env=HTTPS

    # Make redirects to avoid 409 Conflict errors. See: #2219. Upstream issue:
    # https://github.com/transmission/transmission/pull/857 . Drop this
    # workaround with Transmission >= 4.0.
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{REQUEST_URI} ^/transmission/$
        RewriteRule .* /transmission/web/ [R=302,L]
        RewriteCond %{REQUEST_URI} ^/transmission/web$
        RewriteRule .* /transmission/web/ [R=302,L]
    </IfModule>
</Location>

# LDAP only authentication for Transmission remote UIs.
<Location /transmission-remote>
    ProxyPass http://localhost:9091/transmission
    Include includes/freedombox-auth-ldap.conf
    Require ldap-group cn=admin,ou=groups,dc=thisbox
    Require ldap-group cn=bit-torrent,ou=groups,dc=thisbox
</Location>

plinth/modules/transmission/tests/__init__.py