From 8c2df2b2ca122b2be2e171c9cd1e46bdf4231fc6 Mon Sep 17 00:00:00 2001 From: Mattia Mascarello Date: Sun, 4 Dec 2022 20:10:04 +0100 Subject: [PATCH] First commt --- API.php | 18 +++ LICENSE | 21 +++ README.md | 8 ++ art.tex | 4 + artpage.tex | 14 ++ bot.php | 60 +++++++++ lastUpdate | 1 + model.tex | 77 +++++++++++ process.php | 379 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 582 insertions(+) create mode 100644 API.php create mode 100644 LICENSE create mode 100644 README.md create mode 100644 art.tex create mode 100644 artpage.tex create mode 100644 bot.php create mode 100644 lastUpdate create mode 100644 model.tex create mode 100644 process.php diff --git a/API.php b/API.php new file mode 100644 index 0000000..d5a1629 --- /dev/null +++ b/API.php @@ -0,0 +1,18 @@ + $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); diff --git a/lastUpdate b/lastUpdate new file mode 100644 index 0000000..db3df79 --- /dev/null +++ b/lastUpdate @@ -0,0 +1 @@ +NONE \ No newline at end of file diff --git a/model.tex b/model.tex new file mode 100644 index 0000000..04694fe --- /dev/null +++ b/model.tex @@ -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} diff --git a/process.php b/process.php new file mode 100644 index 0000000..c775430 --- /dev/null +++ b/process.php @@ -0,0 +1,379 @@ + '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("", "\\textit{", $text); + $text = str_replace("", "\\textbb{", $text); + $text = str_replace("", "}", $text); + $text = str_replace("", "}", $text); + $text = str_replace(" , ", ", ", $text); + $text = str_replace("", "\\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 .= ""; + elseif ($node["tag"] == "b" || $node["tag"][0] == "h") $res .= ""; + if (isset($node["children"])) { + foreach ($node["children"] as $cnode) { + $res .= recurseText($cnode); + } + } + if ($node["tag"] == "em") $res .= ""; + elseif ($node["tag"] == "b" || $node["tag"][0] == "h") $res .= ""; + 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)]; +}