First commt

This commit is contained in:
Mattia Mascarello 2022-12-04 20:10:04 +01:00 committed by GitHub
commit 8c2df2b2ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 582 additions and 0 deletions

18
API.php Normal file
View File

@ -0,0 +1,18 @@
<?php
function rand_item($arr){
return $arr[array_rand($arr)];
}
function API($method, $data)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.telegram.org/TOKEN/". $method);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$a= json_decode(curl_exec($ch) , true);
return $a;
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 MatMasIt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# LegTeX bot
[![LegTeX bot](https://img.shields.io/static/v1?label=Telegram&message=LegTeX%20Bot%20%28@legtexbot%29&color=blue&logo=telegram)](https://t.me/legtexbot) ![Licenza](https://img.shields.io/github/license/MatMasIt/LegTex) ![PHP](https://img.shields.io/static/v1?label=PHP&message=%3E=7.3&color=blue&logo=php)
Bot telegram per creare atti della camera fac-simile senza la conoscenza di LaTeX
[Guida](https://telegra.ph/Usare-LegTeX-12-04)

4
art.tex Normal file
View File

@ -0,0 +1,4 @@
\begin{center}
\textsc{Art. {{ARTN}}}\\
\end{center}
\hspace{\parindent} {{ARTTEXT}}

14
artpage.tex Normal file
View File

@ -0,0 +1,14 @@
\newpage
\begin{paracol}{2}
\setlength{\columnseprule}{0mm}
\vspace*{2cm}
\begin{center}
\large{\textsc{{{TIPO}}}}\entry{1pt}
\hspace{\fill}\makebox[5mm]{\hrulefill}\hspace{\fill}
\entry{1cm}
\end{center}
{{ARTS}}
\end{paracol}

60
bot.php Normal file
View File

@ -0,0 +1,60 @@
<?php
/*
* Main file, to be run as `php bot.php` indefinitely
*/
require("API.php");
require("process.php");
/**
* Log to console
* @param mixed $message
*
* @return void
*/
function elog(mixed $message): void
{
echo "[" . date("d/m/Y H:i:s") . "]" . (string) $message . "\n";
}
/**
* Send error message to user given error result
* @param mixed $update update object
* @param mixed $text text to send
*
* @return void
*/
function errorOut($update, $text): void
{
elog("erroring out");
API("sendMessage", ["chat_id" => $update["message"]["chat"]["id"], "text" => $text . "\n\n Guida: https://telegra.ph/Usare-LegTeX-12-04", "reply_to_message_id" => $update["message"]["id"], "parse_mode" => "HTML"]);
}
// main loop
do {
$opts = []; // to get updates we have to send the last acknoledged update id + 1, or none
$lastOffset = file_get_contents("lastUpdate");
if ($lastOffset != "NONE") $opts["offset"] = $lastOffset + 1; // confirm all last updates
//elog("Polling updates");
$updates = API("getUpdates", $opts); // obtain updates
if ($updates["ok"]) {
foreach ($updates["result"] as $u) { // iterate over results
if ($lastOffset != "NONE" && $u["update_id"] < $lastOffset) continue; // if an update is already acknoleged, skip
$add = $u["message"]["from"]["username"] ? " (" . $u["message"]["from"]["username"] . ")" : ""; // add username in parentheses, if present to log
elog("New update " . $u["id"] . " from " . $u["message"]["from"]["first_name"] . " " . $u["message"]["from"]["last_name"] . $add); // log update
if ($u["message"]["chat"]["type"] != "private") { // if the chat is not private, skip
elog("Update is not private chat, continuing");
continue;
}
$o = processAndUploadToUrl($u["message"]["text"]); // process text and upload
if (!$o["ok"]) errorOut($u, $o["text"]); // if failed error out
else {
// send document
elog("Sending doc");
API("sendDocument", ["chat_id" => $u["message"]["chat"]["id"], "document" => $o["file"], "parse_mode" => "HTML"]);
}
if ($lastOffset == "NONE" || $lastOffset < $u["update_id"]) $lastOffset = $u["update_id"];
elog("Updating last update offset");
file_put_contents("lastUpdate", $lastOffset);
}
}
//elog("Sleeping");
sleep(10);
} while (true);

1
lastUpdate Normal file
View File

@ -0,0 +1 @@
NONE

77
model.tex Normal file
View File

@ -0,0 +1,77 @@
\documentclass[12pt, a4paper]{article}
\usepackage[italian]{babel}
\usepackage{multicol}
\usepackage{paracol}
\usepackage{setspace}
\usepackage[
margin=2.6cm,
top=3cm,
headsep=21pt,
headheight=41pt,
]{geometry}
\usepackage{fancyhdr}
\author{@SirFabio}
\setlength{\columnseprule}{0.1mm}
\pagestyle{fancy}
\lhead{\emph{Atti Parlamentari}}
\chead{ \thepage \ }
\rhead{\emph{Camera dei Deputati}}
\cfoot{}
\renewcommand{\headrule}{%
\vspace{-4pt}%
\makebox[\headwidth]{\hrulefill}%
\llap{\raisebox{0.5mm}{\makebox[\headwidth]{\hrulefill}}}\\
\makebox[\headwidth]{\footnotesize
{{LEG}} LEGISLATURA -- DISEGNI DI LEGGE E RELAZIONI -- DOCUMENTI}%
\llap{\raisebox{-6pt}{\makebox[\headwidth]{\hrulefill}}}
}
\newcommand\entry[1]{\par\vspace{#1}\ignorespaces}
\begin{document}
\begingroup
\centering
\vspace*{0.5cm}%
\parbox[t]{10.25cm}{\vspace{0pt}%
\MakeUppercase{\Huge Camera Dei Deputati}\par} \parbox[t]{0pt}{\vspace{0pt}%
\clap{\footnotesize N. {{NLAW}}}
\par\clap{\rule[8pt]{4mm}{0.4pt}}%
}
\entry{6pt} \hspace{\fill}\makebox[3cm]{\hrulefill}\hspace{\fill}
\entry{2cm}
\textbf{\Large {{TIPO}}}
\entry{0.9cm}
\scriptsize{\scshape{{{INIZ}}}}\quad
\entry{0.5cm}
\normalsize{\bfseries{{{RELATORI}}}}\quad
\entry{0pt} \hspace{\fill}\makebox[9mm]{\hrulefill}\hspace{\fill}
\entry{1cm}
{{TITOLO}}\quad
\entry{1.5cm} \hspace{\fill}\makebox[22mm]{\hrulefill}\hspace{\fill}
\entry{1.5mm}
\scriptsize{\emph{Presentata {{DATA}}}}\quad
\entry{0.5pt} \hspace{\fill}\makebox[22mm]{\hrulefill}\hspace{\fill}
\vspace{\baselineskip}\par
\endgroup
\begin{multicols}{2}
\textsc{Onorevoli colleghi!} {{INTRO}}
\end{multicols}
{{ARTPAGE}}
\end{document}

379
process.php Normal file
View File

@ -0,0 +1,379 @@
<?php
error_reporting(E_ERROR);
/**
* Get italian date
* @param mixed $UNIX unix timestamp
*
* @return string the italian date
*/
function ITDate($UNIX): string
{
$mesi = array(
1 => 'gennaio', 'Febbraio', 'Marzo', 'Aprile',
'Maggio', 'Giugno', 'Luglio', 'Agosto',
'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
);
$giorni = array(
'Domenica', 'Lunedì', 'Martedì', 'Mercoledì',
'Giovedì', 'Venerdì', 'Sabato'
);
list($sett, $giorno, $mese, $anno) = explode('-', date('w-d-n-Y', $UNIX));
//return $giorni[$sett].' '.$giorno.' '.$mesi[$mese].' '.$anno;
return 'il ' . $giorno . ' ' . $mesi[$mese] . ', anno ' . $anno;
}
/**
* Sanitize latex and enhance
* @param mixed $text the input
*
* @return string the sanitized and enchanced text
*/
function enchanceSanitizeLatex($text): string
{
$text = str_replace("\\", "\\\\", $text);
$text = str_replace("{", "\\{", $text);
$text = str_replace("}", "\\}", $text);
$text = str_replace("<i>", "\\textit{", $text);
$text = str_replace("<b>", "\\textbb{", $text);
$text = str_replace("</i>", "}", $text);
$text = str_replace("</b>", "}", $text);
$text = str_replace(" , ", ", ", $text);
$text = str_replace("<indenta>", "\\hspace{\parindent}", $text);
return $text;
}
/**
* Map "::key:: value" syntax
* @param mixed $textcode text to map
*
* @return array map
*/
function synMapper($textcode): array
{
elog("Mapping document syntax");
$s = explode("::", $textcode);
$map = [];
if (count($s) % 2 == 0) {
elog("Syntax is not valid");
return null;
}
for ($i = 1; $i < count($s); $i += 2) { // skipping first one
$map[$s[$i]] = enchanceSanitizeLatex(trim($s[$i + 1]));
}
elog("Mapping complete");
return $map;
}
/**
* Build tex from textcode
* @param mixed $textcode textcode
*
* @return array result
*/
function build($textcode)
{
$rn = bin2hex(random_bytes(16)); // create job
elog("Job id is " . $rn);
$map = synMapper($textcode);
if (!$map) return ["ok" => false, "text" => "Sintassi errata"];
elog("Loading model");
$model = file_get_contents("model.tex");
if (!$map["leg"]) {
elog("Legislature not specified");
return ["ok" => false, "text" => "La legislatura non è stata specificata, usare il parametro ::leg::\n Per esempio, \n::leg::XV"];
}
// set legislature
$model = str_replace("{{LEG}}", $map["leg"], $model);
$artAllowed = true;
// set headings depending on document type
switch ($map["tipo"]) {
case "ddl":
elog("Ddl document");
$map["tipo"] = "Disegno di Legge";
switch ($map["inziativa"]) {
case "gov":
elog("Presented by government");
$map["iniziativa"] = "PRESENTATO DAL GOVERNO";
break;
case "dep":
elog("Presented by deputies");
return ["ok" => false, "text" => "Un disegno di legge può solo essere creato dal governo, non dai deputati.\n Stai pensando ad una proposta di legge (pdl)?"];
break;
default:
elog("Custom or empty initiative");
if (empty($map["iniziativa"])) $map["iniziativa"] = "PRESENTATO DAL GOVERNO";
break;
}
break;
case "moz":
elog("Motion");
$map["tipo"] = "Mozione";
if (empty($map["iniziativa"])) {
elog("Motions must have initiative specified");
return ["ok" => false, "text" => "Per le mozioni è necessario specificare una iniziativa\n ::iniziativa:: gov\n per il governo o \n ::iniziativa:: dep\n per i deputati\n oppure ::iniziativa:: DI INIZIATIVA TESTO PERSONALIZATO"];
}
switch ($map["iniziativa"]) {
case "gov":
elog("Presented by government");
$map["iniziativa"] = "PRESENTATA DEL GOVERNO";
break;
case "dep":
elog("Presented by deputies");
$map["iniziativa"] = "D'INIZIATIVA DEI DEPUTATI";
break;
default:
elog("Custom initiative");
$map["iniziativa"] = strtoupper(trim($map["iniziativa"]));
break;
}
$artAllowed = false;
if ($artAllowed)
elog("Articles are allowed");
else
elog("Articles not allowed");
break;
default:
case "pdl":
elog("pdl document");
$map["tipo"] = "Proposta di Legge";
if ($map["inziativa"] == "gov") {
elog("Government cannot make pdl");
return ["ok" => false, "text" => "Una proposta di legge può solo essere creato dai deputati, non dal governo.\n Stai pensando ad un disegno di legge (ddl)?"];
}
$map["iniziativa"] = "D'INIZIATIVA DEI DEPUTATI";
break;
}
// check needed fields
if (empty($map["mozione"]) && $map["tipo"] == "Mozione") {
elog("Motion missing");
return ["ok" => false, "text" => "Specificare l'inizio della mozione con \n ::mozione::"];
}
if (empty($map["relatori"])) {
elog("Relators missing");
return ["ok" => false, "text" => "Deve essere specificato almeno un relatore\n ::relatori::"];
}
if (empty($map["titolo"])) {
elog("Title missing");
return ["ok" => false, "text" => "Deve essere specificato un titolo\n ::titolo::"];
}
if (empty($map["data"]) || ($time = strtotime($map["data"])) === false) {
elog("Date invalid or missing");
return ["ok" => false, "text" => "Deve essere specificata una data valida\n ::DATA::"];
}
if (empty($map["intro"])) {
elog("Intro missing");
return ["ok" => false, "text" => "Deve essere specificata una introduzione\n ::intro::"];
}
// create relators list, fixing spacing issues
$relatori = "";
$i = 0;
foreach (explode(",", $map["relatori"]) as $r) {
if ($i != 0) $relatori .= ", ";
$relatori .= trim(strtoupper($r));
$i++;
}
// set all the needed heading data
$model = str_replace("{{TIPO}}", $map["tipo"], $model);
$model = str_replace("{{INIZ}}", $map["iniziativa"], $model);
$model = str_replace("{{RELATORI}}", $relatori, $model);
$model = str_replace("{{TITOLO}}", $map["titolo"], $model);
$model = str_replace("{{DATA}}", ITDate($time), $model);
$model = str_replace("{{NLAW}}", $map["nlegge"] ?: "1", $model);
// add newlines after "." in intro
$map["intro"] = str_replace(".", ".\\newline ", trim($map["intro"]));
$model = str_replace("{{INTRO}}", $map["intro"], $model);
$articleMap = [];
// map articles
elog("Mapping articles");
foreach ($map as $key => $val) {
$key = trim(strtolower($key));
if (str_starts_with($key, "art ")) {
if (!$artAllowed) { // oops, articles not allowed
elog("Articles were allowed ");
return ["ok" => false, "text" => "\"" . $map["tipo"] . "\" non può contenere articoli"];
}
$ae = explode(" ", $key, 2); // the key may be like "art 1"
$articleMap[trim((string)$ae[1])] = trim($val); // set key in map
}
}
$artpage = "";
if (count($articleMap)) {
elog("Sorting articles");
ksort($articleMap);
$atext = "";
elog("Fetching article templates");
// load templates
$artpage = file_get_contents("artpage.tex");
$articleModel = file_get_contents("art.tex");
$artpage = str_replace("{{TIPO}}", $map["tipo"], $artpage);
foreach ($articleMap as $artN => $content) {
// replace article data
elog("Building article " . $artN . "--");
$amodelTemp = str_replace("{{ARTN}}", $artN, $articleModel);
$content = str_replace(".", ".\\newline ", trim($content));
$amodelTemp = str_replace("{{ARTTEXT}}", $content, $amodelTemp);
$atext .= $amodelTemp . "\n\n";
}
$artpage = str_replace("{{ARTS}}", $atext, $artpage);
} else if ($map["tipo"] == "Mozione") {
// special motion heading
$artpage = <<<'EOD'
\begin{multicols}{2}
\vspace*{2cm}
\begin{center}
\large{\textsc{ATTI D'INDIRIZZO}}\entry{1pt}
\hspace{\fill}\makebox[5mm]{\hrulefill}\hspace{\fill}
\entry{1cm}
\end{center}
\begin{center}
\setlength{\columnseprule}{0.1mm}
\textsc{Mozione}\\
\end{center}
\par
EOD;
$artpage = trim($artpage). " ";
$artpage .= $map["mozione"];
$artpage.="\n\\end{multicols}";
}
$model = str_replace("{{ARTPAGE}}", $artpage, $model);
elog("Tex ready!");
return ["ok" => true, "text" => $model, "rng" => "out/" . $rn . ".tex", "titolo" => $map["titolo"]];
}
/** Checks wether
* @param array $arr
*
* @return bool
*/
function isAssoc(array $arr): bool
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
/** Recusrively extract text from telegra.ph tree
* @param mixed $node top node
*
* @return string the text
*/
function recurseText($node): string
{
$res = "";
if (is_array($node) && !isAssoc($node)) {
foreach ($node as $enode) {
$res .= recurseText($enode);
}
return $res;
}
if (is_string($node)) return $node;
if ($node["tag"] == "p") $res .= "\n";
if ($node["tag"] == "em" || $node["tag"] == "i") $res .= "<i>";
elseif ($node["tag"] == "b" || $node["tag"][0] == "h") $res .= "<b>";
if (isset($node["children"])) {
foreach ($node["children"] as $cnode) {
$res .= recurseText($cnode);
}
}
if ($node["tag"] == "em") $res .= "</i>";
elseif ($node["tag"] == "b" || $node["tag"][0] == "h") $res .= "</b>";
return $res;
}
/**
* Get telegraph url
* @param mixed $url telegraph.url
*
* @return array result
*/
function getTelegraphContent($url)
{
$url = str_replace("http://", "https://", $url);
if (!str_starts_with($url, "https://telegra.ph/")) {
elog("Url not in allowed namespace");
return ["ok" => false, "text" => "Inviare un url telegra.ph"];
}
$pageName = explode("/", $url, 4); // [https:]/[]/[telegra.ph]/[...]
$json = file_get_contents("https://api.telegra.ph/getPage/" . $pageName[count($pageName) - 1] . "?return_content=true");
$data = json_decode($json, true);
elog("Started fetching telegraph data");
if ($data == null || !$data["ok"]) {
elog("Data fetching failed");
return ["ok" => false, "text" => "Impossibile recuperare i dati"];
}
elog("Extracting text");
$text = recurseText($data["result"]["content"]);
elog("Text extracted");
return ["ok" => true, "text" => $text];
}
/** Sanitize filename
* @param mixed $titolo filename
*
* @return string safe filename
*/
function sanitizefn($titolo)
{
$titolo = strtolower($titolo);
$final = "";
foreach (str_split($titolo) as $c) {
if (!in_array($c, str_split("abcdefghijklmnopqrstuvwxyz1234567890"))) $final .= "_";
else $final .= $c;
}
return $final;
}
/** Process telegraph url and obtain pdf url (abstracted)
* @param mixed $telegraphUrl telegraph url
*
* @return array result
*/
function processAndUploadToUrl($telegraphUrl)
{
elog("Procesing " . $telegraphUrl);
$out = getTelegraphContent($telegraphUrl);
if (!$out["ok"]) return $out;
elog("Started building");
$q = build($out["text"]);
if (!$q["ok"]) return $q;
elog("Saving tex");
file_put_contents($q["rng"], $q["text"]);
$out = [];
$retval = 0;
elog("Building pdf");
exec("pdflatex -interaction=nonstopmode -output-directory out " . $q["rng"] . "", $out, $retval);
$out = implode("\n", $out);
if ($retval != 0) {
echo $out;
elog("Error in building pdf");
$all = str_replace(".tex", ".*", $q["rng"]);
foreach (glob($all) as $f) unlink($f);
return ["ok" => false, "text" => "PDFLATEX error:\n Tell @matmasak"];
}
$pdffile = str_replace(".tex", ".pdf", $q["rng"]);
$out = [];
$retval = 0;
elog("Uploading pdf");
exec("curl --upload-file " . $pdffile . " https://transfer.sh/" . sanitizefn($q["titolo"]) . ".pdf", $out, $retval);
if ($retval != 0) {
elog("Error in uploading pdf");
$all = str_replace(".tex", ".*", $q["rng"]);
foreach (glob($all) as $f) unlink($f);
return ["ok" => false, "text" => "CURL error:\n" . $out];
}
$all = str_replace(".tex", ".*", $q["rng"]);
foreach (glob($all) as $f) unlink($f);
$out = implode("\n", $out);
elog("File uploaded to " . trim($out));
return ["ok" => true, "file" => trim($out)];
}