Merge remote-tracking branch 'freedombox-team/master'

This commit is contained in:
James Valleroy 2019-04-01 21:23:00 -04:00
commit 5c2a8c0b40
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
15 changed files with 800 additions and 2 deletions

View File

@ -120,6 +120,7 @@ def subcommand_setup(arguments):
webserver.enable('proxy', kind='module')
webserver.enable('proxy_http', kind='module')
webserver.enable('proxy_fcgi', kind='module')
webserver.enable('proxy_html', kind='module')
webserver.enable('rewrite', kind='module')
webserver.enable('macro', kind='module')

113
actions/i2p Executable file
View File

@ -0,0 +1,113 @@
#!/usr/bin/python3
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Wrapper to list and handle system services
"""
import argparse
import os
from plinth import action_utils, cfg
cfg.read()
module_config_path = os.path.join(cfg.config_dir, 'modules-enabled')
I2P_CONF_DIR = '/var/lib/i2p/i2p-config'
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('enable', help='enable i2p service')
subparsers.add_parser('disable', help='disable i2p service')
subparser = subparsers.add_parser(
'add-favorite', help='Add an eepsite to the list of favorites')
subparser.add_argument('--name', help='Name of the entry', required=True)
subparser.add_argument('--url', help='URL of the entry', required=True)
subparsers.required = True
return parser.parse_args()
def subcommand_enable(_):
"""Enable I2P service."""
action_utils.service_enable('i2p')
action_utils.webserver_enable('i2p-freedombox')
def subcommand_disable(_):
"""Disable I2P service."""
action_utils.service_disable('i2p')
action_utils.webserver_disable('i2p-freedombox')
def subcommand_add_favorite(arguments):
"""
Adds a favorite to the router.config
:param arguments:
:type arguments:
"""
router_config_path = os.path.join(I2P_CONF_DIR, 'router.config')
# Read config
with open(router_config_path) as config_file:
config_lines = config_file.readlines()
found_favorites = False
url = arguments.url
new_favorite = '{name},{description},{url},{icon},'.format(
name=arguments.name, description='', url=arguments.url,
icon='/themes/console/images/eepsite.png')
for i in range(len(config_lines)):
line = config_lines[i]
# Find favorites line
if line.startswith('routerconsole.favorites'):
found_favorites = True
if url in line:
print('URL already in favorites')
exit(0)
# Append favorite
config_lines[i] = line.strip() + new_favorite + '\n'
break
if not found_favorites:
config_lines.append('routerconsole.favorites=' + new_favorite + '\n')
# Update config
with open(router_config_path, mode='w') as config_file:
config_file.writelines(config_lines)
print('Added {} to favorites'.format(url))
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()

View File

@ -0,0 +1,34 @@
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
@apps @i2p-app
Feature: I2P Anonymity Network
Manage I2P configuration.
Background:
Given I'm a logged in user
Given the i2p application is installed
Scenario: Enable i2p application
Given the i2p application is disabled
When I enable the i2p application
Then the i2p service should be running
Scenario: Disable i2p application
Given the i2p application is enabled
When I disable the i2p application
Then the i2p service should not be running

View File

@ -20,7 +20,7 @@ FreedomBox app for Apache server.
from plinth import actions
version = 6
version = 7
is_essential = True

View File

@ -26,6 +26,6 @@ from . import diagnostics as views
urlpatterns = [
url(r'^sys/diagnostics/$', views.index, name='index'),
url(r'^sys/diagnostics/(?P<module_name>[a-z\-]+)/$', views.module,
url(r'^sys/diagnostics/(?P<module_name>[1-9a-z\-]+)/$', views.module,
name='module'),
]

View File

@ -0,0 +1,153 @@
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
FreedomBox app to configure I2P.
"""
from django.utils.translation import ugettext_lazy as _
from plinth import action_utils, actions, frontpage
from plinth import service as service_module
from plinth.menu import main_menu
from plinth.modules.users import register_group
from .manifest import backup, clients
version = 1
service_name = 'i2p'
managed_services = [service_name]
managed_packages = ['i2p']
name = _('I2P')
short_description = _('Anonymity Network')
description = [
_('The Invisible Internet Project is an anonymous network layer intended '
'to protect communication from censorship and surveillance. I2P '
'provides anonymity by sending encrypted traffic through a '
'volunteer-run network distributed around the world.'),
_('Find more information about I2P on their project '
'<a href="https://geti2p.net" target="_blank">homepage</a>.'),
_('The first visit to the provided web interface will initiate the '
'configuration process.')
]
clients = clients
group = ('i2p', _('Manage I2P application'))
service = None
manual_page = 'I2P'
additional_favorites = [
('Searx instance', 'http://ransack.i2p'),
('Torrent tracker', 'http://tracker2.postman.i2p'),
('YaCy Legwork', 'http://legwork.i2p'),
('YaCy Seeker', 'http://seeker.i2p'),
]
def init():
"""Intialize the module."""
menu = main_menu.get('apps')
menu.add_urlname(name, 'i2p', 'i2p:index', short_description)
register_group(group)
global service
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
service = service_module.Service(managed_services[0], name, ports=[
'http', 'https'
], is_external=True, is_enabled=is_enabled, enable=enable,
disable=disable,
is_running=is_running)
if is_enabled():
add_shortcut()
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
# Add favorites to the configuration
for fav_name, fav_url in additional_favorites:
helper.call('post', actions.superuser_run, 'i2p', [
'add-favorite',
'--name',
fav_name,
'--url',
fav_url,
])
helper.call('post', enable)
global service
if service is None:
service = service_module.Service(managed_services[0], name, ports=[
'http', 'https'
], is_external=True, is_enabled=is_enabled, enable=enable,
disable=disable,
is_running=is_running)
helper.call('post', service.notify_enabled, None, True)
helper.call('post', add_shortcut)
def add_shortcut():
"""Helper method to add a shortcut to the frontpage."""
frontpage.add_shortcut('i2p', name, short_description=short_description,
url='/i2p/', login_required=True,
allowed_groups=[group[0]])
def is_running():
"""Return whether the service is running."""
return action_utils.service_is_running('i2p')
def is_enabled():
"""Return whether the module is enabled."""
return action_utils.service_is_enabled('i2p') and \
action_utils.webserver_is_enabled('i2p-freedombox')
def enable():
"""Enable the module."""
actions.superuser_run('i2p', ['enable'])
add_shortcut()
def disable():
"""Enable the module."""
actions.superuser_run('i2p', ['disable'])
frontpage.remove_shortcut('i2p')
def diagnose():
"""Run diagnostics and return the results."""
results = []
results.append(action_utils.diagnose_port_listening(7657, 'tcp6'))
results.extend(
action_utils.diagnose_url_on_all('https://{host}/i2p/',
check_certificate=False))
return results

View File

@ -0,0 +1,30 @@
##
## On all sites, provide I2P on a default path: /i2p
##
## Requires the following Apache modules to be enabled:
## mod_headers
## mod_proxy
## mod_proxy_http
## mod_proxy_html
##
<Location /i2p>
# Disable compression
# As soon as it has to be chunked, it doesn't work
RequestHeader unset Accept-Encoding
ProxyPass http://localhost:7657
ProxyPassReverse http://localhost:7657
# Rewrite absolute urls from i2p to pass through apache
ProxyHTMLEnable On
ProxyHTMLURLMap / /i2p/
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "admin" "i2p"
</IfModule>
</Location>
# Catch some other root i2p addresses
# These are most likely generated by javascript
RedirectMatch "^/(i2p[^/]+.*)" "/i2p/$1"

View File

@ -0,0 +1 @@
#plinth.modules.i2p

View File

@ -0,0 +1,56 @@
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
from django.utils.translation import ugettext_lazy as _
from plinth.clients import validate
from plinth.modules.backups.api import validate as validate_backup
_package_id = 'net.geti2p.i2p'
_download_url = 'https://geti2p.net/download'
clients = validate([{
'name':
_('I2P'),
'platforms': [{
'type': 'web',
'url': '/i2p/'
}, {
'type': 'package',
'format': 'deb',
'name': 'i2p',
}, {
'type': 'download',
'os': 'gnu-linux',
'url': _download_url,
}, {
'type': 'download',
'os': 'macos',
'url': _download_url,
}, {
'type': 'download',
'os': 'windows',
'url': _download_url,
}]
}])
backup = validate_backup({
'secrets': {
'directories': ['/var/lib/i2p/i2p-config']
},
'services': ['i2p']
})

View File

@ -0,0 +1,63 @@
{% extends "service-subsubmenu.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block configuration %}
{% block status %}
{% if show_status_block %}
<h3>{% trans "Status" %}</h3>
<p class="running-status-parent">
{% with service_name=service.name %}
{% if service.is_running %}
<span class="running-status active"></span>
{% blocktrans trimmed %}
Service <em>{{ service_name }}</em> is running.
{% endblocktrans %}
{% else %}
<span class="running-status inactive"></span>
{% blocktrans trimmed %}
Service <em>{{ service_name }}</em> is not running.
{% endblocktrans %}
{% endif %}
{% endwith %}
</p>
{% endif %}
{% endblock %}
{% block diagnostics %}
{% if diagnostics_module_name %}
{% include "diagnostics_button.html" with module=diagnostics_module_name enabled=service.is_enabled %}
{% endif %}
{% endblock %}
<h3>{% trans "Configuration" %}</h3>
<form class="form form-configuration" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "service-subsubmenu.html" %}
{% load i18n %}
{% block configuration %}
{% for line in service_description %}
<p>{{ line|safe }}</p>
{% endfor %}
<p>
<a class="btn btn-primary" target="_blank" role="button"
href="{{ service_path }}">
{% trans "Launch" %}
</a>
</p>
{% endblock %}

View File

@ -0,0 +1,29 @@
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
URLs for the I2P module.
"""
from django.conf.urls import url
from plinth.modules.i2p import views
urlpatterns = [
url(r'^apps/i2p/$', views.I2PServiceView.as_view(), name='index'),
url(r'^apps/i2p/tunnels/?$', views.TunnelsView.as_view(), name='tunnels'),
url(r'^apps/i2p/torrents/?$', views.TorrentsView.as_view(), name='torrents'),
]

106
plinth/modules/i2p/views.py Normal file
View File

@ -0,0 +1,106 @@
#
# This file is part of FreedomBox.
#
# 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 <http://www.gnu.org/licenses/>.
#
"""
Views for I2P application.
"""
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views.generic import TemplateView
import plinth.modules.i2p as i2p
from plinth.views import ServiceView
subsubmenu = [{
'url': reverse_lazy('i2p:index'),
'text': ugettext_lazy('Configure')
}, {
'url': reverse_lazy('i2p:tunnels'),
'text': ugettext_lazy('Proxies')
},
{
'url': reverse_lazy('i2p:torrents'),
'text': ugettext_lazy('Anonymous torrents')
}]
class I2PServiceView(ServiceView):
"""Serve configuration page."""
service_id = i2p.service_name
clients = i2p.clients
description = i2p.description
diagnostics_module_name = i2p.service_name
show_status_block = True
template_name = 'i2p.html'
def get_context_data(self, **kwargs):
"""Return the context data for rendering the template view."""
context = super().get_context_data(**kwargs)
context['title'] = i2p.name
context['description'] = i2p.description
context['clients'] = i2p.clients
context['manual_page'] = i2p.manual_page
context['subsubmenu'] = subsubmenu
return context
class ServiceBaseView(TemplateView):
"""View to describe and launch a service."""
service_description = None
service_title = None
service_path = None
def get_context_data(self, **kwargs):
"""Add context data for template."""
context = super().get_context_data(**kwargs)
context['title'] = i2p.name
context['description'] = i2p.description
context['clients'] = i2p.clients
context['manual_page'] = i2p.manual_page
context['subsubmenu'] = subsubmenu
context['service_title'] = self.service_title
context['service_path'] = self.service_path
context['service_description'] = self.service_description
return context
class TunnelsView(ServiceBaseView):
"""View to describe and launch tunnel configuration."""
template_name = 'i2p_service.html'
service_title = _('I2P Proxies and Tunnels')
service_path = '/i2p/i2ptunnel/'
service_description = [
_('I2P lets you browse the Internet and hidden services (eepsites) '
'anonymously. For this, your browser, preferably a Tor Browser, '
'needs to be configured for a proxy.'),
_('By default HTTP, HTTPS and SOCKS5 proxies are available. Additional '
'proxies and tunnels may be configured using the tunnel '
'configuration interface.'),
]
class TorrentsView(ServiceBaseView):
"""View to describe and launch I2P torrents application."""
template_name = 'i2p_service.html'
service_title = _('Anonymous Torrents')
service_path = '/i2p/i2psnark/'
service_description = [
_('I2P provides an application to download files anonymously in a '
'peer-to-peer network. Download files by adding torrents or create a '
'new torrent to share a file.'),
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="82.238564mm"
height="82.233482mm"
viewBox="0 0 82.238564 82.233482"
version="1.1"
id="svg5311"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/bunny/work/freedombox/i2p/logo.png"
inkscape:export-xdpi="158.14484"
inkscape:export-ydpi="158.14484">
<title
id="title4548">I2P</title>
<defs
id="defs5305" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="-333.60853"
inkscape:cy="83.287284"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0.2"
fit-margin-bottom="0"
inkscape:window-width="2165"
inkscape:window-height="1223"
inkscape:window-x="960"
inkscape:window-y="329"
inkscape:window-maximized="0" />
<metadata
id="metadata5308">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>I2P</dc:title>
<cc:license
rdf:resource="https://www.gnu.org/licenses/agpl-3.0.en.html" />
<dc:creator>
<cc:Agent>
<dc:title>Sunil Mohan Adapa &lt;sunil@medhas.org&gt;</dc:title>
</cc:Agent>
</dc:creator>
<dc:date>2019-04-01</dc:date>
<dc:description />
<dc:source>https://commons.wikimedia.org/wiki/File:I2P_logo.svg</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-70.007914,-84.282074)">
<g
id="g5041"
transform="matrix(1.5533984,0,0,1.5533984,-113.33574,24.244873)">
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3798-3"
d="m 125.47315,47.54207 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3800-6"
d="m 125.47315,59.2592 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3802-7"
d="m 125.47315,70.97627 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3804-5"
d="m 125.47315,82.69341 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
</g>
<g
id="g6025"
transform="translate(-8.0673233,-4.3621223e-4)">
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3873-0"
d="m 107.20556,98.097074 a 5.499614,5.4996023 0 1 1 -10.999228,0 5.499614,5.4996023 0 1 1 10.999228,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3875-9"
d="m 107.20556,116.29841 a 5.499614,5.4996023 0 1 1 -10.999228,0 5.499614,5.4996023 0 1 1 10.999228,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3877-3"
d="m 107.20556,134.49965 a 5.499614,5.4996023 0 1 1 -10.999228,0 5.499614,5.4996023 0 1 1 10.999228,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3879-6"
d="m 107.20556,152.70099 a 5.499614,5.4996023 0 1 1 -10.999228,0 5.499614,5.4996023 0 1 1 10.999228,0 z" />
</g>
<g
id="g6019"
transform="translate(8.5383197,0.00534911)">
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:0.85882353;stroke:none;stroke-width:2.26994538"
id="path3921-0"
d="m 125.72828,98.096947 a 5.4996129,5.4996129 0 1 1 -10.99922,0 5.4996129,5.4996129 0 1 1 10.99922,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.26994538"
id="path3923-6"
d="m 125.72828,116.29832 a 5.4996129,5.4996129 0 1 1 -10.99922,0 5.4996129,5.4996129 0 1 1 10.99922,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:1;stroke:none;stroke-width:2.26994538"
id="path3925-2"
d="m 125.72828,134.49959 a 5.4996129,5.4996129 0 1 1 -10.99922,0 5.4996129,5.4996129 0 1 1 10.99922,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.26994538"
id="path3927-6"
d="m 125.72828,152.70098 a 5.4996129,5.4996129 0 1 1 -10.99922,0 5.4996129,5.4996129 0 1 1 10.99922,0 z" />
</g>
<g
id="g5169"
transform="matrix(1.5533984,0,0,1.5533984,-324.6688,22.140315)">
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3953-1"
d="m 306.74654,48.896879 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3955-8"
d="m 306.74654,60.614009 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3957-7"
d="m 306.74654,72.331079 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#16ff0e;fill-opacity:1;stroke:none;stroke-width:1.46127701"
id="path3959-9"
d="m 306.74654,84.048219 a 3.540375,3.540375 0 1 1 -7.08075,0 3.540375,3.540375 0 1 1 7.08075,0 z" />
</g>
<g
id="g6013"
transform="translate(35.695276,-0.1044443)">
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3873-0-7"
d="m 81.007144,98.201083 a 5.499615,5.4996033 0 1 1 -10.99923,0 5.499615,5.4996033 0 1 1 10.99923,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.269943"
id="path3877-3-5"
d="m 81.007144,134.60366 a 5.499615,5.4996033 0 1 1 -10.99923,0 5.499615,5.4996033 0 1 1 10.99923,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.26994538"
id="path3923-6-9"
d="m 81.007137,116.40233 a 5.499613,5.499613 0 1 1 -10.99922,0 5.499613,5.499613 0 1 1 10.99922,0 z" />
<path
inkscape:connector-curvature="0"
style="fill:#ebed02;fill-opacity:1;stroke:none;stroke-width:2.26994538"
id="path3927-6-2"
d="m 81.007137,152.80499 a 5.499613,5.499613 0 1 1 -10.99922,0 5.499613,5.499613 0 1 1 10.99922,0 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB