Create accounts search API for searching local and remote accounts

This commit is contained in:
Bofh 2022-12-01 01:05:56 +01:00
parent 423c5f4567
commit 1aa7682a91
3 changed files with 295 additions and 2 deletions

View File

@ -0,0 +1,142 @@
<?php chdir('../../../../../../') ?>
<?php require 'base.php' ?>
<?php
ini_set('max_execution_time', 120);
if (count($_GET) < 2)
apiresult(['error' => 'you must specify filter key-values for searching'], 400);
$pg = new PgDatabase();
if (!$pg->is_ok())
apiresult(['error' => 'Error on database'], 500);
$filtered_accounts = [];
$filtered_accounts_index = [];
$sql_all = '
SELECT
a.id, a.username, a.domain, a.fields, a.note, a.avatar_file_name,
a.avatar_storage_schema_version
FROM accounts AS a
{userLocalInnerJoin}
WHERE
{userFilter}
{userLocalWhere1}
{userLocalWhere2}
a.suspended_at IS NULL
ORDER BY a.updated_at DESC
';
$user_filter = 'all';
if (isset($_GET['user_filter'])) {
$user_filter = strtolower(trim($_GET['user_filter']));
if (!in_array($user_filter, ['all','local','remote']))
$user_filter = 'all';
}
switch ($user_filter) {
case 'all':
$sql_all = str_replace('{userFilter}', '', $sql_all);
$sql_all = str_replace('{userLocalInnerJoin}', '', $sql_all);
$sql_all = str_replace('{userLocalWhere1}', '', $sql_all);
$sql_all = str_replace('{userLocalWhere2}', '', $sql_all);
break;
case 'local':
$sql_all = str_replace('{userFilter}', 'a.domain IS NULL AND', $sql_all);
$sql_all = str_replace('{userLocalInnerJoin}', 'INNER JOIN users AS u ON u.account_id = a.id', $sql_all);
$sql_all = str_replace('{userLocalWhere1}', 'u.disabled = false AND', $sql_all);
$sql_all = str_replace('{userLocalWhere2}', 'u.approved = true AND', $sql_all);
break;
case 'remote':
$sql_all = str_replace('{userFilter}', 'NOT a.domain IS NULL AND', $sql_all);
$sql_all = str_replace('{userLocalInnerJoin}', '', $sql_all);
$sql_all = str_replace('{userLocalWhere1}', '', $sql_all);
$sql_all = str_replace('{userLocalWhere2}', '', $sql_all);
break;
}
$all_accounts = $pg->fetch_all($sql_all);
if (isset($_GET['profile']) && trim($_GET['profile']) != '')
{
$q = strtolower(trim($_GET['profile']));
if ($q === '<empty>') $q = '';
$q = normalize_for_search($q);
$ai = -1;
foreach ($all_accounts as $account)
{
$ai++;
$a_note = trim($account['note']);
$a_fields = json_decode($account['fields'], true);
$fields = '';
foreach ($a_fields as $field)
$fields .= $field['name'].' '.$field['value'].' ';
$a_note = trim($fields) . $a_note;
if ($q === '') {
if ($a_note === '') {
$filtered_accounts []= $account['id'];
$filtered_accounts_index[$account['id']] = $ai;
}
continue;
}
if ($a_note === '')
continue;
$words = explode(' ', normalize_for_search($a_note));
$matches = true;
foreach (explode(' ', $q) as $qi) {
$found = false;
foreach ($words as $word) {
if (str_starts_with($word, $qi)) {
$found = true;
break;
}
}
if (!$found) {
$matches = false;
break;
}
}
if ($matches) {
$filtered_accounts []= $account['id'];
$filtered_accounts_index[$account['id']] = $ai;
}
}
}
$config = instance_config('mastodon');
$filtered_accounts = array_unique($filtered_accounts);
$result_accounts = [];
foreach ($filtered_accounts as $id) {
$newacc = [];
$account = $all_accounts[$filtered_accounts_index[$id]];
$account['fields'] = json_decode($account['fields'], true);
$account['avatar_storage_schema_version'] = intval($account['avatar_storage_schema_version']);
$newacc['id'] = $id;
$newacc['acct'] = $account['username'].'@'.($account['domain'] === null ? '' : $account['domain']);
$newacc['acct'] = trim($newacc['acct'],'@');
$newacc['note'] = $account['note'];
$newacc['fields'] = $account['fields'];
if ($account['avatar_storage_schema_version'] === 1) {
$c = 0;
$parts = [];
$part = '';
for ($i = 0; $i < strlen($id); $i++) {
$part .= $id[$i];
$c++;
if ($c === 3) {
$c = 0;
$parts []= $part;
$part = '';
}
}
$newacc['avatar'] = $config['instance_url'].'/system';
if ($account['domain'] !== null)
$newacc['avatar'] .= '/cache';
$newacc['avatar'] .= '/accounts/avatars/'.implode('/',$parts).'/original/'.$account['avatar_file_name'];
}
$result_accounts []= $newacc;
}
apiresult($result_accounts);

149
base.php
View File

@ -46,9 +46,12 @@ function instance_config($software, $instance=null) {
if (!in_array($software, $GLOBALS['supported_ap_software']))
return null;
if ($instance === null)
if ($instance === null && isset($_GET['instance']))
$instance = trim($_GET['instance']);
if ($instance === null)
return apiresult(['error' => '<instance> GET argument must be provided'], 400);
if (isset($GLOBALS['_cache'][$software.$instance]))
return $GLOBALS['_cache'][$software.$instance];
@ -126,5 +129,149 @@ function valid_mastodon_account_id($id) {
return preg_match('/^\d+$/', strval($id));
}
function remove_accents($string) {
if ( !preg_match('/[\x80-\xff]/', $string) )
return $string;
$chars = array(
// Decompositions for Latin-1 Supplement
chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
chr(195).chr(191) => 'y',
// Decompositions for Latin Extended-A
chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
chr(197).chr(190) => 'z', chr(197).chr(191) => 's'
);
$string = strtr($string, $chars);
return $string;
}
if (!function_exists('str_starts_with')) {
function str_starts_with($haystack, $needle) {
return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
function normalize_for_search($str) {
if (trim($str) === '') return '';
$str = preg_replace('/\s\s+/', ' ', trim($str));
$words = explode(' ', $str);
$newwords = [];
foreach ($words as $word) {
$word = remove_accents($word);
$word = strtolower($word);
$word = preg_replace('/[^a-z0-9]+/', '', $word);
if (trim($word) === '')
continue;
$word = str_replace('4', 'a', $word);
$word = str_replace('3', 'e', $word);
$word = str_replace('1', 'i', $word);
$word = str_replace('0', 'o', $word);
$word = str_replace('5', 's', $word);
$word = str_replace('8', 'b', $word);
$nword = '';
for ($i = 0; $i < strlen($word); $i++) {
if ($i === 0) {
$nword .= $word[0];
continue;
}
if ($word[$i] !== $word[$i-1]) {
$nword .= $word[$i];
}
}
$newwords [] = $nword;
}
return trim(implode(' ', $newwords));
}
// classes
require 'classes/PgDatabase.php';

View File

@ -26,8 +26,12 @@ class PgDatabase {
return $this->db !== null;
}
public function escape($str) {
return pg_escape_string($this->db, $str);
}
public function query($sql) {
$result = pg_query($sql);
$result = pg_query($this->db, $sql);
if (!$result) apiresult('The postgres query has failed: '.pg_last_error());
return $result;
}