diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b74752d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +web/admin/crawler/crawler.log +web/admin/crawler/currinst.job +web/admin/crawler/instances.job +web/admin/crawler/instances.json +vendor +composer.lock \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..80923e5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000, + "runtimeExecutable": "C:\\wamp64\\bin\\php\\php7.3.12\\php.exe" + } + ] +} \ No newline at end of file diff --git a/web/admin/crawler/crawler.php b/web/admin/crawler/crawler.php index 543edda..ee20354 100755 --- a/web/admin/crawler/crawler.php +++ b/web/admin/crawler/crawler.php @@ -16,6 +16,9 @@ along with this program. If not, see . */ +require __DIR__ . "/../../vendor/autoload.php"; +use LanguageDetection\Language; + define('N',"\n"); $link=false; @@ -23,27 +26,29 @@ $logf=false; $jsonf=false; declare(ticks=1); -pcntl_signal(SIGTERM,'signalHandler');// Termination ('kill' was called) -pcntl_signal(SIGHUP,'signalHandler');// Terminal log-out -pcntl_signal(SIGINT,'signalHandler');// Interrupted (Ctrl-C is pressed) -function signalHandler($signal) { - global $link, $logf, $jsonf; - lecho(N.'Sono stato interrotto.'.N); - if ($link) { - lecho('La connessione MySQL è aperta, la chiudo.'.N); - mysqli_close($link); +if(defined("pcntl_signal")) { + pcntl_signal(SIGTERM,'signalHandler');// Termination ('kill' was called) + pcntl_signal(SIGHUP,'signalHandler');// Terminal log-out + pcntl_signal(SIGINT,'signalHandler');// Interrupted (Ctrl-C is pressed) + function signalHandler($signal) { + global $link, $logf, $jsonf; + lecho(N.'Sono stato interrotto.'.N); + if ($link) { + lecho('La connessione MySQL è aperta, la chiudo.'.N); + mysqli_close($link); + } + if ($jsonf) { + lecho('Il file di dump json è aperto, lo chiudo.'.N); + // qui no, altrimenti "riprendi" fa poi casino + // fwrite($jsonf,'"Fine?": true'.N.'}'.N); + fclose($jsonf); + } + if ($logf) { + lecho('Il file di log è aperto, lo chiudo.'.N); + fclose($logf); + } + exit(2); } - if ($jsonf) { - lecho('Il file di dump json è aperto, lo chiudo.'.N); -// qui no, altrimenti "riprendi" fa poi casino -// fwrite($jsonf,'"Fine?": true'.N.'}'.N); - fclose($jsonf); - } - if ($logf) { - lecho('Il file di log è aperto, lo chiudo.'.N); - fclose($logf); - } - exit(2); } $opts=array( @@ -257,7 +262,7 @@ function flushtronc($id) { function truncs($str,$tab,$col,$ctx) { global $tables, $tronconi; - $size=$tables[$tab][$col]; + $size=$tables[strtolower($tab)][$col]; $len=mb_strlen($str,'UTF-8'); if ($len>$size) { $tronconi[]=array('id'=>null,'tab'=>$tab,'col'=>$col,'ctx'=>$ctx,'len'=>$len,'size'=>$size); @@ -269,12 +274,12 @@ function truncs($str,$tab,$col,$ctx) { function truncn($num,$tab,$col,$ctx) { global $tables; if (is_numeric($num)) { - if ($num>$tables[$tab][$col]['max']) { - notify($ctx.': ho dovuto troncare «'.$num.'» al valore massimo «'.$tables[$tab][$col]['max'].'» che può avere nella colonna «'.$col.'» della tabella «'.$tab.'»).',2); - $num=$tables[$tab][$col]['max']; - } elseif ($num<$tables[$tab][$col]['min']) { - notify($ctx.': ho dovuto troncare «'.$num.'» al valore minimo «'.$tables[$tab][$col]['min'].'» che può avere nella colonna «'.$col.'» della tabella «'.$tab.'»).',2); - $num=$tables[$tab][$col]['min']; + if ($num>$tables[strtolower($tab)][$col]['max']) { + notify($ctx.': ho dovuto troncare «'.$num.'» al valore massimo «'.$tables[strtolower($tab)][$col]['max'].'» che può avere nella colonna «'.$col.'» della tabella «'.$tab.'»).',2); + $num=$tables[strtolower($tab)][$col]['max']; + } elseif ($num<$tables[strtolower($tab)][$col]['min']) { + notify($ctx.': ho dovuto troncare «'.$num.'» al valore minimo «'.$tables[strtolower($tab)][$col]['min'].'» che può avere nella colonna «'.$col.'» della tabella «'.$tab.'»).',2); + $num=$tables[strtolower($tab)][$col]['min']; } } else { notify($ctx.': truncn(): mi aspettavo un numero, invece non lo era; ritorno «0».',3); @@ -401,7 +406,7 @@ if (!$riprendi) { function willtrunc($str,$tab,$col) { global $tables; - if (mb_strlen($str,'UTF-8')>$tables[$tab][$col]) + if (mb_strlen($str,'UTF-8')>$tables[strtolower($tab)][$col]) return(true); else return(false); @@ -450,36 +455,195 @@ function subarimp($glue,$key,&$arr) { function notify($msg,$sev) { global $link, $tables; lecho('NOTIFICAZIÒ: '.strip_tags($msg).N); - mysqli_query($link,'INSERT INTO Notifications (ID, Notification, Severity, Microtime, Seen) VALUES (NULL, \''.myesc($link,mb_substr($msg,0,$tables['Notifications']['Notification'],'UTF-8')).'\', '.$sev.', \''.microtime(true).'\', 0)') + mysqli_query($link,'INSERT INTO Notifications (ID, Notification, Severity, Microtime, Seen) VALUES (NULL, \''.myesc($link,mb_substr($msg,0,$tables['notifications']['Notification'],'UTF-8')).'\', '.$sev.', \''.microtime(true).'\', 0)') or mexit(mysqli_error($link).N,3); } -function langs($instid) { +/** */ +/** + * Effettua una chiamata alla API di Mastodon. + * + * @param string $host L'host da chiamare (e.g.: "mastodon.bida.im") + * @param string $path Il path della API (e.g.: "/api/v1/timelines/public?local=true") + * @return mixed L'oggetto ritornato dalla chiamata, già parsato da json_decode, o NULL se la chiamata fallisce + */ +function get_api($host, $path) { + global $context; + try { + $buf = @file_get_contents('https://' . $host . $path, false, $context); + } catch(Exception $e) { + echo "error:"; + echo $e; + return NULL; + } + if ($buf!==false) { + $data = json_decode($buf, true); + return $data; + } else { + return NULL; + } +} + +/** + * Torna un elenco di linguaggi riconosciuti nel toot fornito con relativa probabilità. + * + * @param mixed $toot Il toot da analizzare, come ritornato dalle API + * @return array Mappa tra codice lingua e probabilità che il toot sia in quella lingua. + */ +function get_toot_languages($toot) { + $l = $toot['language']; + $res = []; + if($l !== NULL) { + // la lingua è specificata già nel toot: usa quella + $langs[$l] = 1; + } else { + // la lingua non è specificata: deducila + $text = strip_tags($toot['content']); + $ld = new Language; + $langs = $ld->detect($text)->bestResults()->close(); + } + // raggruppa le lingue derivate, e.g.: "zh" e "zh-CN" + $grouped_langs = array(); + foreach($langs as $key => $value) { + $l = explode("-", $key)[0]; + if(array_key_exists($l, $grouped_langs)) { + $grouped_langs[$l] = max($grouped_langs[$l], $value); + } else { + $grouped_langs[$l] = $value; + } + } + return $grouped_langs; +} + +/** + * Date le probabilità di lingua per ogni toot, calcola la media. + * + * @param array $detected_langs Array di mappe tra lingua e probabilità + * @return array Mappa tra lingua e probabilità + */ +function summary($detected_langs) { + $res = Array(); + foreach($detected_langs as $langs) { + foreach($langs as $l => $weight) { + if(!array_key_exists($l, $res)) { + $res[$l] = 0; + } + $res[$l] += $weight; + } + } + foreach($res as $l => $sumweight) { + $res[$l] = $sumweight / count($detected_langs); + } + return $res; +} + +/** + * Helper function per usort: compara due array usando il primo elemento. + * + * @param array $entry1 Primo array da comparare + * @param array $entry2 Secondo array da comparare + * @return number -1, 0 o 1 a seconda che $entry1[0] sia minore, uguale o superiore a $entry2[0] + */ +function sort_weights($entry1, $entry2) { + $w1 = $entry1[0]; + $w2 = $entry2[0]; + return $w1 < $w2 ? 1 : $w1 == $w2 ? 0 : -1; +} + +/** + * Data una mappa di lingue, ritorna una lista di linguaggi considerati probabili. + * + * @param array $summary Mappa tra lingue e probabilità + * @return string[] Elenco di lingue considerate probabili + */ +function get_languages($summary) { + $lst = []; + foreach($summary as $code => $weight) { + $lst[] = [$weight, $code]; + } + usort($lst, 'sort_weights'); + $languages = []; + $lastweight = 0; + foreach($lst as $entry) { + $l = $entry[1]; + $weight = $entry[0]; + if($weight < $lastweight * 2 / 3) { + break; + } + $languages[] = $l; + $lastweight = $weight; + } + return $languages; +} + +/** + * Ritorna una lista di lingue probabili per la data istanza. + * + * @param string $host Hostname dell'istanza (e.g.: "mastodon.bida.im") + * @return string[] Lista di lingue probabili + */ +function get_instance_langs($host) { + $data = get_api($host, '/api/v1/timelines/public?local=true'); + if($data == NULL) { + return []; + } + $detected_langs = array_map('get_toot_languages', $data); + $summary = summary($detected_langs); + $languages = get_languages($summary); + return $languages; +} +/** */ + +/** + * ucfirst UTF-8 aware function + * + * @param string $string + * @return string + * @see http://ca.php.net/ucfirst + */ +function my_ucfirst($string, $e ='utf-8') { + if (function_exists('mb_strtoupper') && function_exists('mb_substr') && !empty($string)) { + $string = mb_strtolower($string, $e); + $upper = mb_strtoupper($string, $e); + preg_match('#(.)#us', $upper, $matches); + $string = $matches[1] . mb_substr($string, 1, mb_strlen($string, $e), $e); + } else { + $string = ucfirst($string); + } + return $string; +} + +function langs($instid, $uri) { global $info, $instrow, $link; $instlangs=array(); - if (akeavinn('languages',$info)) { - $pos=0; - foreach ($info['languages'] as $lang) { - $res=mysqli_query($link,'SELECT * FROM Languages WHERE Code=\''.myesc($link,$lang).'\'') - or mexit(mysqli_error($link).N,3); - if (mysqli_num_rows($res)<1) { - $NameIt=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'it')),'Languages','NameIT','«'.$instrow['URI'].'»')); - $NameEn=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'en')),'Languages','NameEN','«'.$instrow['URI'].'»')); - $NameFr=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'fr')),'Languages','NameFR','«'.$instrow['URI'].'»')); - $NameEs=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'es')),'Languages','NameES','«'.$instrow['URI'].'»')); - $NameOrig=myesc($link,truncs(ucfirst(locale_get_display_name($lang,$lang)),'Languages','NameOrig','«'.$instrow['URI'].'»')); - mysqli_query($link,'INSERT INTO Languages (ID, Code, NameIT, NameEN, NameFR, NameES, NameOrig) VALUES (NULL, \''.myesc($link,truncs($lang,'Languages','Code','«'.$instrow['URI'].'»')).'\', \''.$NameIt.'\', \''.$NameEn.'\', \''.$NameFr.'\', \''.$NameEs.'\', \''.$NameOrig.'\')') - or mexit(mysqli_error($link).N,3); - $langid=mysqli_insert_id($link); - flushtronc($langid); - } else { - $row=mysqli_fetch_assoc($res); - $langid=$row['ID']; - } - $pos++; - $instlangs[]=array('InstID'=>$instid,'LangID'=>$langid,'Pos'=>$pos,'Code'=>$lang); - } + $languages = get_instance_langs($uri); + if(count($languages) == 0 && akeavinn('languages',$info)) { + $languages = $info['languages']; } + echo "Lingue trovate: " . implode(", ", $languages).N; + $pos=0; + foreach($languages as $lang) { + $res=mysqli_query($link,'SELECT * FROM Languages WHERE Code=\''.myesc($link,$lang).'\'') + or mexit(mysqli_error($link).N,3); + if (mysqli_num_rows($res)<1) { + $NameIt=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'it')),'Languages','NameIT','«'.$instrow['URI'].'»')); + $NameEn=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'en')),'Languages','NameEN','«'.$instrow['URI'].'»')); + $NameFr=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'fr')),'Languages','NameFR','«'.$instrow['URI'].'»')); + $NameEs=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'es')),'Languages','NameES','«'.$instrow['URI'].'»')); + $NameOrig=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,$lang)),'Languages','NameES','«'.$instrow['URI'].'»')); + $q = 'INSERT INTO Languages (ID, Code, NameIT, NameEN, NameFR, NameES, NameOrig) VALUES (NULL, \''.myesc($link,truncs($lang,'Languages','Code','«'.$instrow['URI'].'»')).'\', \''.$NameIt.'\', \''.$NameEn.'\', \''.$NameFr.'\', \''.$NameEs.'\', \''.$NameOrig.'\')'; + mysqli_query($link, $q) + or mexit(mysqli_error($link).N,3); + $langid=mysqli_insert_id($link); + flushtronc($langid); + } else { + $row=mysqli_fetch_assoc($res); + $langid=$row['ID']; + } + $pos++; + $instlangs[]=array('InstID'=>$instid,'LangID'=>$langid,'Pos'=>$pos,'Code'=>$lang); + } + // } return($instlangs); } @@ -597,7 +761,7 @@ while ($i<$cinsts) { $ok=false; lecho('ERRORE :-('.N); // questo è anche il limbo delle istanze che non rispondono, perciò controlliamo se già esistono nel db e, nel caso, aggiorniamo InstChecks - $res=mysqli_query($link,'SELECT * FROM Instances WHERE URI=\''.myesc($link,mb_substr($dom,0,$tables['Instances']['URI'],'UTF-8')).'\'') + $res=mysqli_query($link,'SELECT * FROM Instances WHERE URI=\''.myesc($link,mb_substr($dom,0,$tables['instances']['URI'],'UTF-8')).'\'') or mexit(mysqli_error($link).N,3); if (mysqli_num_rows($res)>0) { lecho('«'.$dom.'» non risponde, ma è presente nel database; aggiorno InstChecks.'.N); @@ -742,7 +906,7 @@ while ($i<$cinsts) { $oldinstlangs=array(); while ($row=mysqli_fetch_assoc($res)) $oldinstlangs[]=$row; - $instlangs=langs($instrow['ID']); + $instlangs=langs($instrow['ID'], $instrow['URI']); if ($instlangs!=$oldinstlangs) { notify('La lista delle lingue utilizzate dichiarate dall’istanza «'.$instrow['URI'].'» è cambiata da «'.subarimp(', ','Code',$oldinstlangs).'» a «'.subarimp(', ','Code',$instlangs).'».',1); mysqli_query($link,'DELETE FROM InstLangs WHERE InstID='.$instrow['ID']) @@ -773,7 +937,7 @@ while ($i<$cinsts) { flushtronc($instid); - $instlangs=langs($instid); + $instlangs=langs($instid, $instrow['URI']); foreach ($instlangs as $row) { mysqli_query($link,'INSERT INTO InstLangs (InstID, LangID, Pos) VALUES ('.$row['InstID'].', '.$row['LangID'].', '.$row['Pos'].')') or mexit(mysqli_error($link).N,3); diff --git a/web/composer.json b/web/composer.json new file mode 100644 index 0000000..70060cd --- /dev/null +++ b/web/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "patrickschur/language-detection": "^3.4" + } +}