New command to merge duplicated contacts

This commit is contained in:
Michael 2022-06-01 22:11:03 +00:00
parent aae735413e
commit e46354a522
5 changed files with 235 additions and 5 deletions

View file

@ -0,0 +1,165 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Console;
use Friendica\Core\L10n;
use Friendica\Database\Database;
/**
* tool to find and merge duplicated contact entries.
*/
class MergeContacts extends \Asika\SimpleConsole\Console
{
protected $helpOptions = ['h', 'help', '?'];
/**
* @var $dba Database
*/
private $dba;
/**
* @var L10n
*/
private $l10n;
protected function getHelp()
{
$help = <<<HELP
console mergecontacts - Merge duplicated contact entries
Synopsis
bin/console mergecontacts
Description
bin/console mergecontacts
Remove duplicated contact entries
Options
-h|--help|-? Show help information
-e|--execute Execute the dropping
HELP;
return $help;
}
public function __construct(Database $dba, L10n $l10n, array $argv = null)
{
parent::__construct($argv);
$this->dba = $dba;
$this->l10n = $l10n;
}
protected function doExecute()
{
$duplicates = $this->dba->p("SELECT COUNT(*) AS `total`, `uri-id`, MAX(`url`) AS `url` FROM `contact` WHERE `uid` = 0 GROUP BY `uri-id` HAVING total > 1");
while ($duplicate = $this->dba->fetch($duplicates)) {
$this->out($this->l10n->t('%d %s, %d duplicates.', $duplicate['uri-id'], $duplicate['url'], $duplicate['total']));
if ($this->getOption(['e', 'execute'], false)) {
$this->mergeContacts($duplicate['uri-id']);
}
}
return 0;
}
private function mergeContacts(int $uriid)
{
$first = $this->dba->selectFirst('contact', ['id', 'nurl', 'url'], ["`uri-id` = ? AND `nurl` != ? AND `url` != ?", $uriid, '', ''], ['order' => ['id']]);
if (empty($first)) {
$this->err($this->l10n->t('No valid first countact found for uri-id %d.', $uriid));
return;
}
$this->out($first['url']);
$duplicates = $this->dba->select('contact', ['id', 'nurl', 'url'], ['uri-id' => $uriid]);
while ($duplicate = $this->dba->fetch($duplicates)) {
if ($first['id'] == $duplicate['id']) {
continue;
}
if ($first['url'] != $duplicate['url']) {
$this->err($this->l10n->t('Wrong duplicate found for uri-id %d in %d (url: %s != %s).', $uriid, $duplicate['id'], $first['url'], $duplicate['url']));
continue;
}
if ($first['nurl'] != $duplicate['nurl']) {
$this->err($this->l10n->t('Wrong duplicate found for uri-id %d in %d (nurl: %s != %s).', $uriid, $duplicate['id'], $first['nurl'], $duplicate['nurl']));
continue;
}
$this->out($duplicate['id'] . "\t" . $duplicate['url']);
$this->mergeContactInTables($duplicate['id'], $first['id']);
}
}
private function mergeContactInTables(int $from, int $to)
{
$this->out($from . "\t=> " . $to);
foreach (['post', 'post-thread', 'post-thread-user', 'post-user'] as $table) {
foreach (['author-id', 'causer-id', 'owner-id'] as $field) {
$this->updateTable($table, $field, $from, $to, false);
}
}
$this->updateTable('contact-relation', 'cid', $from, $to, true);
$this->updateTable('contact-relation', 'relation-cid', $from, $to, true);
$this->updateTable('event', 'cid', $from, $to, false);
$this->updateTable('fsuggest', 'cid', $from, $to, false);
$this->updateTable('group', 'cid', $from, $to, false);
$this->updateTable('group_member', 'contact-id', $from, $to, true);
$this->updateTable('intro', 'contact-id', $from, $to, false);
$this->updateTable('intro', 'suggest-cid', $from, $to, false);
$this->updateTable('mail', 'author-id', $from, $to, false);
$this->updateTable('mail', 'contact-id', $from, $to, false);
$this->updateTable('notification', 'actor-id', $from, $to, false);
$this->updateTable('photo', 'contact-id', $from, $to, false);
$this->updateTable('post-tag', 'cid', $from, $to, true);
$this->updateTable('post-user', 'contact-id', $from, $to, false);
$this->updateTable('post-thread-user', 'contact-id', $from, $to, false);
$this->updateTable('user-contact', 'cid', $from, $to, true);
if (!$this->dba->delete('contact', ['id' => $from])) {
$this->err($this->l10n->t('Deletion of id %d failed', $from));
} else {
$this->out($this->l10n->t('Deletion of id %d was successful', $from));
}
}
private function updateTable(string $table, string $field, int $from, int $to, bool $in_unique_key)
{
$this->out($this->l10n->t('Updating "%s" in "%s" from %d to %d', $field, $table, $from, $to), false);
if ($this->dba->exists($table, [$field => $from])) {
$this->out($this->l10n->t(' - found'), false);
if ($in_unique_key) {
$params = ['ignore' => true];
} else {
$params = [];
}
if (!$this->dba->update($table, [$field => $to], [$field => $from], [], $params)) {
$this->out($this->l10n->t(' - failed'), false);
} else {
$this->out($this->l10n->t(' - success'), false);
}
if ($in_unique_key && $this->dba->exists($table, [$field => $from])) {
$this->dba->delete($table, [$field => $from]);
$this->out($this->l10n->t(' - deleted'), false);
}
}
$this->out($this->l10n->t(' - done'));
}
}

View file

@ -60,6 +60,7 @@ Commands:
lock Edit site locks lock Edit site locks
maintenance Set maintenance mode for this node maintenance Set maintenance mode for this node
movetoavatarcache Move cached avatars to the file based avatar cache movetoavatarcache Move cached avatars to the file based avatar cache
mergecontacts Merge duplicated contact entries
user User management user User management
php2po Generate a messages.po file from a strings.php file php2po Generate a messages.po file from a strings.php file
po2php Generate a strings.php file from a messages.po file po2php Generate a strings.php file from a messages.po file
@ -93,6 +94,7 @@ HELP;
'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class, 'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class,
'lock' => Friendica\Console\Lock::class, 'lock' => Friendica\Console\Lock::class,
'maintenance' => Friendica\Console\Maintenance::class, 'maintenance' => Friendica\Console\Maintenance::class,
'mergecontacts' => Friendica\Console\MergeContacts::class,
'movetoavatarcache' => Friendica\Console\MoveToAvatarCache::class, 'movetoavatarcache' => Friendica\Console\MoveToAvatarCache::class,
'php2po' => Friendica\Console\PhpToPo::class, 'php2po' => Friendica\Console\PhpToPo::class,
'postupdate' => Friendica\Console\PostUpdate::class, 'postupdate' => Friendica\Console\PostUpdate::class,

View file

@ -422,13 +422,14 @@ class DBA
* @param array $fields contains the fields that are updated * @param array $fields contains the fields that are updated
* @param array $condition condition array with the key values * @param array $condition condition array with the key values
* @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields) * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields)
* @param array $params Parameters: "ignore" If set to "true" then the update is done with the ignore parameter
* *
* @return boolean was the update successfull? * @return boolean was the update successfull?
* @throws \Exception * @throws \Exception
*/ */
public static function update($table, $fields, $condition, $old_fields = []) public static function update($table, $fields, $condition, $old_fields = [], $params = [])
{ {
return DI::dba()->update($table, $fields, $condition, $old_fields); return DI::dba()->update($table, $fields, $condition, $old_fields, $params);
} }
/** /**

View file

@ -1284,11 +1284,12 @@ class Database
* @param array $fields contains the fields that are updated * @param array $fields contains the fields that are updated
* @param array $condition condition array with the key values * @param array $condition condition array with the key values
* @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields) * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields)
* @param array $params Parameters: "ignore" If set to "true" then the update is done with the ignore parameter
* *
* @return boolean was the update successfull? * @return boolean was the update successfull?
* @throws \Exception * @throws \Exception
*/ */
public function update($table, $fields, $condition, $old_fields = []) public function update($table, $fields, $condition, $old_fields = [], $params = [])
{ {
if (empty($table) || empty($fields) || empty($condition)) { if (empty($table) || empty($fields) || empty($condition)) {
$this->logger->info('Table, fields and condition have to be set'); $this->logger->info('Table, fields and condition have to be set');
@ -1325,7 +1326,13 @@ class Database
$condition_string = DBA::buildCondition($condition); $condition_string = DBA::buildCondition($condition);
$sql = "UPDATE " . $table_string . " SET " if (!empty($params['ignore'])) {
$ignore = 'IGNORE ';
} else {
$ignore = '';
}
$sql = "UPDATE " . $ignore . $table_string . " SET "
. implode(" = ?, ", array_map([DBA::class, 'quoteIdentifier'], array_keys($fields))) . " = ?" . implode(" = ?, ", array_map([DBA::class, 'quoteIdentifier'], array_keys($fields))) . " = ?"
. $condition_string; . $condition_string;

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2022.05-rc\n" "Project-Id-Version: 2022.05-rc\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-05-31 13:27+0000\n" "POT-Creation-Date: 2022-06-01 22:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -1641,6 +1641,61 @@ msgstr ""
msgid "The contact has been blocked from the node" msgid "The contact has been blocked from the node"
msgstr "" msgstr ""
#: src/Console/MergeContacts.php:74
#, php-format
msgid "%d %s, %d duplicates."
msgstr ""
#: src/Console/MergeContacts.php:86
#, php-format
msgid "No valid first countact found for uri-id %d."
msgstr ""
#: src/Console/MergeContacts.php:97
#, php-format
msgid "Wrong duplicate found for uri-id %d in %d (url: %s != %s)."
msgstr ""
#: src/Console/MergeContacts.php:101
#, php-format
msgid "Wrong duplicate found for uri-id %d in %d (nurl: %s != %s)."
msgstr ""
#: src/Console/MergeContacts.php:137
#, php-format
msgid "Deletion of id %d failed"
msgstr ""
#: src/Console/MergeContacts.php:139
#, php-format
msgid "Deletion of id %d was successful"
msgstr ""
#: src/Console/MergeContacts.php:145
#, php-format
msgid "Updating \"%s\" in \"%s\" from %d to %d"
msgstr ""
#: src/Console/MergeContacts.php:147
msgid " - found"
msgstr ""
#: src/Console/MergeContacts.php:154
msgid " - failed"
msgstr ""
#: src/Console/MergeContacts.php:156
msgid " - success"
msgstr ""
#: src/Console/MergeContacts.php:160
msgid " - deleted"
msgstr ""
#: src/Console/MergeContacts.php:163
msgid " - done"
msgstr ""
#: src/Console/MoveToAvatarCache.php:91 #: src/Console/MoveToAvatarCache.php:91
msgid "The avatar cache needs to be enabled to use this command." msgid "The avatar cache needs to be enabled to use this command."
msgstr "" msgstr ""