diff --git a/.gitignore b/.gitignore
index b3f9877..eac6c11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,62 +1,64 @@
 # Created by .ignore support plugin (hsz.mobi)
 ### Android template
 # Built application files
 *.apk
 *.ap_
 
 # Files for the ART/Dalvik VM
 *.dex
 
 # Java class files
 *.class
 
 # Generated files
 bin/
 gen/
 out/
 
 # Gradle files
 .gradle/
 build/
 
 # Local configuration file (sdk path, etc)
 local.properties
 
 # Proguard folder generated by Eclipse
 proguard/
 
 # Log Files
 *.log
 
 # Android Studio Navigation editor temp files
 .navigation/
 
 # Android Studio captures folder
 captures/
 
 # IntelliJ
 *.iml
 .idea
 
 # Keystore files
 # Uncomment the following line if you do not want to check your keystore files in.
 #*.jks
 
 # External native build folder generated in Android Studio 2.2 and later
 .externalNativeBuild
 
 # Google Services (e.g. APIs or Firebase)
 google-services.json
 
 # Freeline
 freeline.py
 freeline/
 freeline_project_description.json
 
 # fastlane
 fastlane/report.xml
 fastlane/Preview.html
 fastlane/screenshots
 fastlane/test_output
 fastlane/readme.md
 
+# script stuff
+/cli/config.php
diff --git a/cli/config-example.php b/cli/config-example.php
new file mode 100644
index 0000000..5c1fd3e
--- /dev/null
+++ b/cli/config-example.php
@@ -0,0 +1,29 @@
+<?php
+# Libre BusTO
+# Copyright (C) 2023 Valerio Bozzolan and contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+
+/**
+ * The "config-example.php" file must be copied as "config.php" file
+ */
+
+// pathname to your Arcanist installation
+define( 'ARCANIST_INIT', __DIR__ . '/../../arcanist/support/init/init-script.php' );
+
+// Phabricator Conduit API token
+// https://gitpull.it/conduit/token/
+define( 'CONDUIT_API_TOKEN', 'asd' );
+
+// must end with a slash
+define( 'PHABRICATOR_HOME', 'https://gitpull.it/' );
diff --git a/cli/diff-changelog.php b/cli/diff-changelog.php
new file mode 100755
index 0000000..b51e4fc
--- /dev/null
+++ b/cli/diff-changelog.php
@@ -0,0 +1,166 @@
+#!/usr/bin/php
+<?php
+# Libre BusTO
+# Copyright (C) 2023 Valerio Bozzolan and contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+
+// load configuration
+require 'config.php';
+
+// load Arcanist stuff
+require ARCANIST_INIT;
+
+// repository pathname
+if( !defined( 'REPO_PATH' ) ) {
+	define( 'REPO_PATH', __DIR__ . '/..' );
+}
+
+// load gradle stuff
+$gradle_content = file_get_contents( REPO_PATH . '/build.gradle' );
+preg_match( '/versionCode +([0-9]+)/', $gradle_content, $matches );
+$version_code = $matches[1] ?? null;
+
+// no code no party
+if( !$version_code ) {
+	throw new Exception( "unable to extract version code" );
+}
+
+// no internationalization no party
+$I18N_raw = file_get_contents( __DIR__ . '/i18n.json' );
+if( $I18N_raw === false ) {
+	throw new Exception("cannot load file i18n");
+}
+
+// no JSON no party
+$I18N = json_decode( $I18N_raw, true );
+if( $I18N === null ) {
+	throw new Exception( "cannot JSON-decode file" );
+}
+
+// read first argument
+$diff_id = $argv[1] ?? null;
+
+// no diff ID no party
+if( !$diff_id ) {
+	echo "Please specify Differential ID like D66\n";
+	exit( 1 );
+}
+
+$client = new ConduitClient( PHABRICATOR_HOME );
+$client->setConduitToken( CONDUIT_API_TOKEN );
+
+$tasks = [];
+$tasks_phid = [];
+
+// cache with users by phids
+$USERS_BY_PHID = [];
+
+// https://gitpull.it/conduit/method/edge.search/
+$edge_api_parameters = [
+	'sourcePHIDs' => [ $diff_id ],
+	'types'       => [ 'revision.task' ],
+];
+
+// find Tasks attached to Diff patch
+$edge_result = $client->callMethodSynchronous( 'edge.search', $edge_api_parameters );
+foreach( $edge_result['data'] as $data ) {
+	$tasks_phid[] = $data['destinationPHID'];
+}
+
+// https://gitpull.it/conduit/method/maniphest.search/
+$maniphest_api_parameters = [
+	'constraints' => [
+		'phids' => $tasks_phid,
+	],
+];
+
+// query Tasks info
+$maniphest_result = $client->callMethodSynchronous( 'maniphest.search', $maniphest_api_parameters );
+foreach( $maniphest_result['data'] as $task ) {
+
+	// append in known Tasks
+	$tasks[] = $task;
+
+	// remember User PHIDs since we will need to get their extra info
+	$phid_task_author = $task['fields']['authorPHID'];
+	$phid_task_owner  = $task['fields']['ownerPHID'];
+	$USERS_BY_PHID[ $phid_task_author ] = null;
+	$USERS_BY_PHID[ $phid_task_owner  ] = null;
+}
+
+// get users info from their PHID identifiers
+$users_phid = array_keys( $USERS_BY_PHID );
+$users_api_parameters = [
+	'constraints' => [
+		'phids' => $users_phid,
+	],
+];
+$users_result = $client->callMethodSynchronous( 'user.search', $users_api_parameters );
+foreach( $users_result['data'] as $user_data ) {
+	$phid_user = $user_data['phid'];
+	$USERS_BY_PHID[ $phid_user ] = $user_data;
+}
+
+// for each language
+foreach( $I18N as $lang => $msg ) {
+
+	$changelog_blocks = [];
+
+	// for each Task
+	foreach( $tasks as $task ) {
+		$task_id          = $task['id'];
+		$task_name        = $task['fields']['name'];
+		$task_descr       = $task['fields']['description'];
+		$phid_task_author = $task['fields']['authorPHID'];
+		$phid_task_owner  = $task['fields']['ownerPHID'];
+
+		$author = $USERS_BY_PHID[ $phid_task_author ];
+		$owner  = $USERS_BY_PHID[ $phid_task_owner  ];
+
+		$username_author = $author['fields']['username'];
+		$username_owner  = $owner ['fields']['username'];
+		$realname_author = $author['fields']['realName'] ?? null;
+		$realname_owner  = $owner ['fields']['realName'] ?? null;
+
+		$task_url   = PHABRICATOR_HOME . "T{$task_id}";
+		$url_author = PHABRICATOR_HOME . 'p/' . $username_author;
+		$url_owner  = PHABRICATOR_HOME . 'p/' . $username_owner;
+
+		// just try to show something useful for a F-Droid changelog
+		$changelog_lines = [];
+
+		// Task name and URL
+		$changelog_lines[] = $task_name;
+
+		// reporter by (author)
+		$changelog_lines[] = sprintf( $msg['reportedByName'], $username_author );
+
+		// resolved by (owner)
+		$changelog_lines[] = sprintf( $msg['resolvedByName'], $username_owner );
+
+		$changelog_lines[] = $task_url;
+
+		$changelog_block = implode( "\n", $changelog_lines );
+		$changelog_blocks[] = $changelog_block;
+	}
+
+	// print all changelog blocks
+	$changelog_content = implode( "\n\n", $changelog_blocks );
+
+	// expected changelog file
+	$changelog_path = REPO_PATH . "/metadata/{$lang}/changelogs/{$version_code}.txt";
+
+	// save
+	file_put_contents( $changelog_path, $changelog_content );
+}
diff --git a/cli/i18n.json b/cli/i18n.json
new file mode 100644
index 0000000..23472bc
--- /dev/null
+++ b/cli/i18n.json
@@ -0,0 +1,14 @@
+{
+	"en-US": {
+		"reportedBy": "Reported by:",
+		"reportedByName": "Reported by %s",
+		"resolvedBy": "Resolved by:",
+		"resolvedByName": "Resolved by %s"
+	},
+	"it": {
+		"reportedBy": "Segnalazione di:",
+		"reportedByName": "Segnalato da %s",
+		"resolvedBy": "Risolto da:",
+		"resolvedByName": "Risolto da %s"
+	}
+}
diff --git a/metadata/en-US/changelogs/46.txt b/metadata/en-US/changelogs/46.txt
new file mode 100644
index 0000000..bd60720
--- /dev/null
+++ b/metadata/en-US/changelogs/46.txt
@@ -0,0 +1,14 @@
+Fix crash caused by NullPointerException on org.osmdroid.views.MapView.getMapCenter() (MapFragment.java:290) on version 1.18.3
+Reported by valerio.bozzolan
+Resolved by fabio.mazza
+https://gitpull.it/T1146
+
+Fix crash caused by NullPointerException in MapFragment.java:134 on version 1.18.2
+Reported by valerio.bozzolan
+Resolved by fabio.mazza
+https://gitpull.it/T1142
+
+Feature request - Bus list in maps pop-up
+Reported by canonex
+Resolved by fabio.mazza
+https://gitpull.it/T1140
\ No newline at end of file
diff --git a/metadata/it/changelogs/46.txt b/metadata/it/changelogs/46.txt
new file mode 100644
index 0000000..7ac00c1
--- /dev/null
+++ b/metadata/it/changelogs/46.txt
@@ -0,0 +1,14 @@
+Fix crash caused by NullPointerException on org.osmdroid.views.MapView.getMapCenter() (MapFragment.java:290) on version 1.18.3
+Segnalato da valerio.bozzolan
+Risolto da fabio.mazza
+https://gitpull.it/T1146
+
+Fix crash caused by NullPointerException in MapFragment.java:134 on version 1.18.2
+Segnalato da valerio.bozzolan
+Risolto da fabio.mazza
+https://gitpull.it/T1142
+
+Feature request - Bus list in maps pop-up
+Segnalato da canonex
+Risolto da fabio.mazza
+https://gitpull.it/T1140
\ No newline at end of file