Browse Source

Aggiunta deduzione lingua dell'istanza dai toot.

RedGlow 4 years ago
parent
commit
aa143ed793
4 changed files with 251 additions and 53 deletions
  1. 6 0
      .gitignore
  2. 23 0
      .vscode/launch.json
  3. 217 53
      web/admin/crawler/crawler.php
  4. 5 0
      web/composer.json

+ 6 - 0
.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

+ 23 - 0
.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"
+        }
+    ]
+}

+ 217 - 53
web/admin/crawler/crawler.php

@@ -16,6 +16,9 @@
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+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 ($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);
+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);
 	}
-	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) {
+/** <LANGUAGE MANAGEMENT> */
+/**
+ * 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;
+}
+/** </LANGUAGE MANAGEMENT> */
+
+/**
+ * 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).'\'')
+	$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);
-			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);
+			$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 «<a href="editinst.php?id='.$instrow['ID'].'">'.$instrow['URI'].'</a>» è 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);

+ 5 - 0
web/composer.json

@@ -0,0 +1,5 @@
+{
+    "require": {
+        "patrickschur/language-detection": "^3.4"
+    }
+}