diff --git a/actions/infinoted b/actions/infinoted
new file mode 100755
index 000000000..9976f0789
--- /dev/null
+++ b/actions/infinoted
@@ -0,0 +1,179 @@
+#!/usr/bin/python3
+# -*- mode: python -*-
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Configuration helper for infinoted.
+"""
+
+import argparse
+import grp
+import os
+import pwd
+import shutil
+import subprocess
+
+from plinth import action_utils
+
+
+DATA_DIR = '/var/lib/infinoted'
+KEY_DIR = '/etc/infinoted'
+
+CONF_PATH = '/etc/xdg/infinoted.conf'
+CONF = '''
+[infinoted]
+
+# Possible values : no-tls, allow-tls, require-tls
+security-policy=require-tls
+
+# Absolute path of the certificate file.
+certificate-file=/etc/infinoted/infinoted-cert.pem
+
+# Absolute path of the private key file.
+key-file=/etc/infinoted/infinoted-key.pem
+
+# Setting this to 0 disables autosave.
+autosave-interval=60
+
+# Specify a path to use a root certificate instead of a certificate-key pair.
+#certificate-chain=
+
+#password=
+
+# If you want to regularly synchronize the saved documents.
+#sync-directory
+
+#sync-interval=
+'''
+
+DEFAULT_PATH = '/etc/default/infinoted'
+DEFAULT = '''
+# defaults file for infinoted
+
+# Should infinoted be started by the init script? (true/false)
+INFINOTED_ENABLED=true
+
+# The configuration file to be used.
+INFINOTED_CONFIG=/etc/xdg/infinoted.conf
+
+# The session autosave file to be updated periodically and loaded
+# upon startup.
+INFINOTED_SESSION_FILE=/var/lib/infinoted
+'''
+
+SYSTEMD_SERVICE_PATH = '/etc/systemd/system/infinoted.service'
+SYSTEMD_SERVICE = '''
+#
+# This file is managed and overwritten by Plinth. If you wish to edit
+# it, disable infinoted in Plinth, remove this file and manage it manually.
+#
+
+[Unit]
+Description=collaborative text editor service
+Documentation=man:infinoted(1)
+After=network.target
+
+[Service]
+User=infinoted
+EnvironmentFile=-/etc/default/infinoted
+ExecStart=/usr/bin/infinoted ${OPTIONS}
+
+[Install]
+WantedBy=multi-user.target
+'''
+
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary."""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ subparsers.add_parser('setup', help='Configure infinoted after install')
+
+ return parser.parse_args()
+
+
+def subcommand_setup(_):
+ """Configure infinoted after install."""
+
+ if not os.path.isfile(CONF_PATH):
+ with open(CONF_PATH, 'w') as file_handle:
+ file_handle.write(CONF)
+
+ if not os.path.isfile(DEFAULT_PATH):
+ with open(DEFAULT_PATH, 'w') as file_handle:
+ file_handle.write(DEFAULT)
+
+ if not os.path.isfile(SYSTEMD_SERVICE_PATH):
+ with open(SYSTEMD_SERVICE_PATH, 'w') as file_handle:
+ file_handle.write(SYSTEMD_SERVICE)
+ subprocess.check_call(['systemctl', 'daemon-reload'])
+
+ # Create infinoted group if needed.
+ try:
+ grp.getgrnam('infinoted')
+ except KeyError:
+ subprocess.run(['addgroup', '--system', 'infinoted'], check=True)
+
+ # Create infinoted user is needed.
+ try:
+ pwd.getpwnam('infinoted')
+ except KeyError:
+ subprocess.run(['adduser', '--system', '--ingroup', 'infinoted',
+ '--home', DATA_DIR,
+ '--gecos', '"Infinoted collaborative editing server"',
+ 'infinoted'], check=True)
+
+ if not os.path.exists(DATA_DIR):
+ os.makedirs(DATA_DIR, mode=0o750)
+ shutil.chown(DATA_DIR, user='infinoted', group='infinoted')
+
+ if not os.path.exists(KEY_DIR):
+ os.makedirs(KEY_DIR, mode=0o750)
+ shutil.chown(KEY_DIR, user='infinoted', group='infinoted')
+
+ if not os.path.exists(KEY_DIR + '/infinoted-cert.pem'):
+ old_umask = os.umask(0o027)
+ try:
+ # infinoted doesn't have a "create key and exit" mode. Run as
+ # daemon so we can stop after.
+ subprocess.run(['infinoted', '--create-key',
+ '--create-certificate', '--daemonize'], check=True)
+ subprocess.run(['infinoted', '--kill-daemon'], check=True)
+ finally:
+ os.umask(old_umask)
+
+ shutil.chown(KEY_DIR + '/infinoted-cert.pem',
+ user='infinoted', group='infinoted')
+ shutil.chown(KEY_DIR + '/infinoted-key.pem',
+ user='infinoted', group='infinoted')
+
+ action_utils.service_enable('infinoted')
+
+
+def main():
+ """Parse arguments and perform all duties."""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/data/etc/plinth/modules-enabled/infinoted b/data/etc/plinth/modules-enabled/infinoted
new file mode 100644
index 000000000..0f25f2eb0
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/infinoted
@@ -0,0 +1 @@
+plinth.modules.infinoted
diff --git a/data/usr/lib/firewalld/services/infinoted-plinth.xml b/data/usr/lib/firewalld/services/infinoted-plinth.xml
new file mode 100644
index 000000000..0a9ed778c
--- /dev/null
+++ b/data/usr/lib/firewalld/services/infinoted-plinth.xml
@@ -0,0 +1,6 @@
+
+
+ infinoted
+ infinoted is a dedicated server which allows clients to edit plain text documents and source files collaboratively over a network.
+
+
diff --git a/plinth/modules/infinoted/__init__.py b/plinth/modules/infinoted/__init__.py
new file mode 100644
index 000000000..f46de06b2
--- /dev/null
+++ b/plinth/modules/infinoted/__init__.py
@@ -0,0 +1,107 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Plinth module for infinoted.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import actions
+from plinth import action_utils
+from plinth import cfg
+from plinth import frontpage
+from plinth import service as service_module
+from plinth.utils import format_lazy
+from plinth.views import ServiceView
+
+version = 1
+
+depends = ['apps']
+
+service = None
+
+managed_services = ['infinoted']
+
+managed_packages = ['infinoted']
+
+title = _('Gobby Server (infinoted)')
+
+description = [
+ _('infinoted is a server for Gobby, a collaborative text editor.'),
+
+ format_lazy(
+ _('To use it, download Gobby '
+ 'and install it. Then start Gobby and select "Connect to Server" '
+ 'and enter your {box_name}\'s domain name.'),
+ box_name=_(cfg.box_name)),
+]
+
+
+def init():
+ """Initialize the infinoted module."""
+ menu = cfg.main_menu.get('apps:index')
+ menu.add_urlname(title, 'glyphicon-pencil', 'infinoted:index')
+
+ global service
+ service = service_module.Service(
+ managed_services[0], title, ports=['infinoted-plinth'],
+ is_external=True, enable=enable, disable=disable)
+
+ if service.is_enabled():
+ add_shortcut()
+
+
+class InfinotedServiceView(ServiceView):
+ service_id = managed_services[0]
+ diagnostics_module_name = "infinoted"
+ description = description
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.install(managed_packages)
+ helper.call('post', actions.superuser_run, 'infinoted', ['setup'])
+ helper.call('post', service.notify_enabled, None, True)
+ helper.call('post', add_shortcut)
+
+
+def add_shortcut():
+ frontpage.add_shortcut('infinoted', title, None, 'glyphicon-pencil',
+ description)
+
+
+def enable():
+ """Enable the module."""
+ actions.superuser_run('service', ['enable', managed_services[0]])
+ add_shortcut()
+
+
+def disable():
+ """Disable the module."""
+ actions.superuser_run('service', ['disable', managed_services[0]])
+ frontpage.remove_shortcut('infinoted')
+
+
+def diagnose():
+ """Run diagnostics and return the results."""
+ results = []
+
+ results.append(action_utils.diagnose_port_listening(6523, 'tcp4'))
+ results.append(action_utils.diagnose_port_listening(6523, 'tcp6'))
+
+ return results
diff --git a/plinth/modules/infinoted/tests/__init__.py b/plinth/modules/infinoted/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/infinoted/urls.py b/plinth/modules/infinoted/urls.py
new file mode 100644
index 000000000..ac903e564
--- /dev/null
+++ b/plinth/modules/infinoted/urls.py
@@ -0,0 +1,29 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+URLs for the infinoted module.
+"""
+
+from django.conf.urls import url
+
+from plinth.modules.infinoted import InfinotedServiceView
+
+
+urlpatterns = [
+ url(r'^apps/infinoted/$', InfinotedServiceView.as_view(), name='index'),
+]