From b4bc35ee311f125352491f6cccb99cf47e16baa5 Mon Sep 17 00:00:00 2001 From: SebastianObi Date: Fri, 21 Oct 2022 17:58:11 +0200 Subject: [PATCH] Removed history, due to sensitive data --- CHANGELOG.md | 0 LICENSE | 19 + README.md | 189 + build_git.sh | 132 + docs/screenshots/lxmf_bridge_mqtt_01.png | Bin 0 -> 62459 bytes docs/screenshots/lxmf_chatbot_01.png | Bin 0 -> 65660 bytes docs/screenshots/lxmf_cmd_01.png | Bin 0 -> 56962 bytes .../lxmf_distribution_group_01.png | Bin 0 -> 9419 bytes .../lxmf_distribution_group_02.png | Bin 0 -> 11839 bytes .../lxmf_distribution_group_03.png | Bin 0 -> 10419 bytes .../lxmf_distribution_group_04.png | Bin 0 -> 8948 bytes .../lxmf_distribution_group_05.png | Bin 0 -> 11672 bytes .../lxmf_distribution_group_06.png | Bin 0 -> 9711 bytes .../lxmf_distribution_group_07.png | Bin 0 -> 6067 bytes .../lxmf_distribution_group_08.png | Bin 0 -> 84444 bytes .../lxmf_distribution_group_minimal_01.png | Bin 0 -> 7910 bytes .../lxmf_distribution_group_minimal_02.png | Bin 0 -> 7488 bytes .../lxmf_distribution_group_minimal_03.png | Bin 0 -> 77079 bytes docs/screenshots/lxmf_echo_01.png | Bin 0 -> 48212 bytes docs/screenshots/lxmf_ping_01.png | Bin 0 -> 11394 bytes docs/screenshots/lxmf_terminal_01.png | Bin 0 -> 56878 bytes lxmf_bridge_matrix/CHANGELOG.md | 0 lxmf_bridge_matrix/README.md | 6 + lxmf_bridge_matrix/lxmf_bridge_matrix.py | 0 lxmf_bridge_meshtastic/CHANGELOG.md | 0 lxmf_bridge_meshtastic/README.md | 6 + .../lxmf_bridge_meshtastic.py | 0 lxmf_bridge_mqtt/CHANGELOG.md | 0 lxmf_bridge_mqtt/README.md | 200 + lxmf_bridge_mqtt/lxmf_bridge_mqtt.py | 1407 +++++ lxmf_bridge_telegram/CHANGELOG.md | 0 lxmf_bridge_telegram/README.md | 6 + lxmf_bridge_telegram/lxmf_bridge_telegram.py | 0 lxmf_chatbot/CHANGELOG.md | 0 lxmf_chatbot/README.md | 197 + lxmf_chatbot/lxmf_chatbot.py | 1145 ++++ lxmf_cmd/CHANGELOG.md | 0 lxmf_cmd/README.md | 196 + lxmf_cmd/lxmf_cmd.py | 1169 +++++ lxmf_distribution_group/CHANGELOG.md | 0 lxmf_distribution_group/README.md | 685 +++ .../lxmf_distribution_group.py | 4676 +++++++++++++++++ lxmf_distribution_group_minimal/CHANGELOG.md | 0 lxmf_distribution_group_minimal/README.md | 295 ++ .../lxmf_distribution_group_minimal.py | 1297 +++++ lxmf_echo/CHANGELOG.md | 0 lxmf_echo/README.md | 197 + lxmf_echo/lxmf_echo.py | 1126 ++++ lxmf_ping/CHANGELOG.md | 0 lxmf_ping/README.md | 100 + lxmf_ping/lxmf_ping.py | 782 +++ lxmf_terminal/CHANGELOG.md | 0 lxmf_terminal/README.md | 197 + lxmf_terminal/lxmf_terminal.py | 1337 +++++ 54 files changed, 15364 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100755 build_git.sh create mode 100644 docs/screenshots/lxmf_bridge_mqtt_01.png create mode 100644 docs/screenshots/lxmf_chatbot_01.png create mode 100644 docs/screenshots/lxmf_cmd_01.png create mode 100644 docs/screenshots/lxmf_distribution_group_01.png create mode 100644 docs/screenshots/lxmf_distribution_group_02.png create mode 100644 docs/screenshots/lxmf_distribution_group_03.png create mode 100644 docs/screenshots/lxmf_distribution_group_04.png create mode 100644 docs/screenshots/lxmf_distribution_group_05.png create mode 100644 docs/screenshots/lxmf_distribution_group_06.png create mode 100644 docs/screenshots/lxmf_distribution_group_07.png create mode 100644 docs/screenshots/lxmf_distribution_group_08.png create mode 100644 docs/screenshots/lxmf_distribution_group_minimal_01.png create mode 100644 docs/screenshots/lxmf_distribution_group_minimal_02.png create mode 100644 docs/screenshots/lxmf_distribution_group_minimal_03.png create mode 100644 docs/screenshots/lxmf_echo_01.png create mode 100644 docs/screenshots/lxmf_ping_01.png create mode 100644 docs/screenshots/lxmf_terminal_01.png create mode 100644 lxmf_bridge_matrix/CHANGELOG.md create mode 100644 lxmf_bridge_matrix/README.md create mode 100755 lxmf_bridge_matrix/lxmf_bridge_matrix.py create mode 100644 lxmf_bridge_meshtastic/CHANGELOG.md create mode 100644 lxmf_bridge_meshtastic/README.md create mode 100755 lxmf_bridge_meshtastic/lxmf_bridge_meshtastic.py create mode 100644 lxmf_bridge_mqtt/CHANGELOG.md create mode 100644 lxmf_bridge_mqtt/README.md create mode 100755 lxmf_bridge_mqtt/lxmf_bridge_mqtt.py create mode 100644 lxmf_bridge_telegram/CHANGELOG.md create mode 100644 lxmf_bridge_telegram/README.md create mode 100755 lxmf_bridge_telegram/lxmf_bridge_telegram.py create mode 100644 lxmf_chatbot/CHANGELOG.md create mode 100644 lxmf_chatbot/README.md create mode 100755 lxmf_chatbot/lxmf_chatbot.py create mode 100644 lxmf_cmd/CHANGELOG.md create mode 100644 lxmf_cmd/README.md create mode 100755 lxmf_cmd/lxmf_cmd.py create mode 100644 lxmf_distribution_group/CHANGELOG.md create mode 100644 lxmf_distribution_group/README.md create mode 100755 lxmf_distribution_group/lxmf_distribution_group.py create mode 100644 lxmf_distribution_group_minimal/CHANGELOG.md create mode 100644 lxmf_distribution_group_minimal/README.md create mode 100755 lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py create mode 100644 lxmf_echo/CHANGELOG.md create mode 100644 lxmf_echo/README.md create mode 100755 lxmf_echo/lxmf_echo.py create mode 100644 lxmf_ping/CHANGELOG.md create mode 100644 lxmf_ping/README.md create mode 100755 lxmf_ping/lxmf_ping.py create mode 100644 lxmf_terminal/CHANGELOG.md create mode 100644 lxmf_terminal/README.md create mode 100755 lxmf_terminal/lxmf_terminal.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..161e4d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Sebastian Obele / obele.eu + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd95a79 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# LXMF-Tools +Various small programs and tools which use the message protocol LXMF from https://github.com/markqvist/LXMF + + +## lxmf_bridge_matrix +For more information, see the detailed [README.md](lxmf_bridge_matrix). + + +## lxmf_bridge_meshtastic +For more information, see the detailed [README.md](lxmf_bridge_meshtastic). + + +## lxmf_bridge_mqtt +This program provides an interface between LXMF and MQTT. It serves as a single message endpoint and not to transfer the LXMF/Reticlum traffic 1:1 to MQTT. It serves the purpose of providing an endpoint in the Reticulum network for third party applications that can communicate via MQTT. Through this all LXMF capable applications can communicate with it via messages. This can be used for example to communicate via text messages with a smarthome system (FHEM, openHAB, ioBroker, Node-RED or similar). The transmission format used by MQTT is JSON with freely definable topics. The target system can then respond to these JSON messages. + +For more information, see the detailed [README.md](lxmf_bridge_mqtt). + + +## lxmf_bridge_telegram +For more information, see the detailed [README.md](lxmf_bridge_telegram). + + +## lxmf_chatbot +This program provides a simple chatbot (RiveScript) which can communicate via LXMF. + +For more information, see the detailed [README.md](lxmf_chatbot). + + +## lxmf_cmd +This program executes any text received by message as a system command and returns the output of the command as a message. Only single commands can be executed directly. No interactive terminal is created. + +For more information, see the detailed [README.md](lxmf_cmd). + + +## lxmf_distribution_group +This program provides an email like distribution group. It will distribute incoming LXMF messages to multiple recipients. Since this program acts as a normal LXMF endpoint, all compatible chat applications can be used. In addition to simple messaging, there is a simple command-based user interface. Where all relevant actions for daily administration can be performed. The basic configuration is done in the configuration files. There are various options to adapt the entire behavior of the group to personal needs. This distribution group is much more than a standard email distribution group. It emulates advanced group functions with automatic notifications etc. Different user permissions can be defined. For each user type, the range of functions can be defined individually. The normal users have only small rights. While a moderator or admin can perform everything necessary by simple commands. Once the basic configuration is done, everything else can be done by LXMF messages as commands. + +For more information, see the detailed [README.md](lxmf_distribution_group). + + +## lxmf_distribution_group_minimal +This program is a minimalist version of the normal distribution group. The functionality is reduced to a minimum. Only sender and receiver users can be defined. Messages are then sent to the other users accordingly. There is no user interface or other notifications. Only the messages are distributed 1:1. The administration is done completely by the respective configuration files which are to be edited accordingly. + +For more information, see the detailed [README.md](lxmf_distribution_group_minimal). + + +## lxmf_echo +This program is a simple echo server. All received messages are sent back 1:1 as an answer. This can be used as a simple counterpart to test the chat functionality of applications. + +For more information, see the detailed [README.md](lxmf_echo). + + +## lxmf_ping +This program sends an adjustable number of LXMF messages to a destination. Then a simple statistic is created to check the success or failure of a single message. This tool can be useful to load the LXMF/Reticulum network with a defined load of messages. This can be used to simulate a certain amount of users. + +For more information, see the detailed [README.md](lxmf_ping). + + +## lxmf_terminal +This program provides a complete terminal session on the server. Any commands can be executed on the target device. The communication is done by single LXMF messages. This offers the advantage that simple terminal commands can be used by any LXMF capable application. + +For more information, see the detailed [README.md](lxmf_terminal). + + +## General Information for all tools/programs + + +### Current Status: +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` + +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` + +- Download the [file](lxmf_distribution_group.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group/lxmf_distribution_group.py + ``` + +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_distribution_group/config.cfg.owr + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group + systemctl stop lxmf_distribution_group + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group + systemctl disable lxmf_distribution_group + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_2nd + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +## Support / Donations +You can help support the continued development by donating via one of the following channels: + +- PayPal: https://paypal.me/SebastianObi +- Liberapay: https://liberapay.com/SebastianObi/donate + + +## Support in another way? +You are welcome to participate in the development. Just create a pull request. Or just contact me for further clarifications. + + +## Do you need a special function or customization? +Then feel free to contact me. Customizations or tools developed specifically for you can be realized. + + +## FAQ diff --git a/build_git.sh b/build_git.sh new file mode 100755 index 0000000..0410369 --- /dev/null +++ b/build_git.sh @@ -0,0 +1,132 @@ +#!/bin/bash + + +############################################################################################################## +# Configuration + + +BRANCH="main" +ORIGIN="git@github.com:SebastianObi/LXMF-Tools.git" +FILES_ADD=("*") +FILES_REMOVE=(".git/*") +COMMENT_COMMIT="$(date +%Y-%m-%d_%H:%M:%S)" +COMMENT_CLEAR="Removed history, due to sensitive data" +COMMENT_INIT="Initial commit" + + +############################################################################################################## +# Functions + + +_prompt() { + echo -e "" + echo -e "Select an option:" + options=("Commit/Push" "Clear History" "Init" "Exit") + select opt in "${options[@]}"; do + case $opt in + "Commit/Push"*) + _commit + break;; + "Clear History"*) + _clear + break;; + "Init"*) + _init + break;; + "Exit"*) + echo -e "" + echo -e "Exit" + break;; + *) + echo -e "Invalid choice!" + break;; + esac + done +} + + +_define_files() { + for file in ${FILES_ADD[@]}; do + git add "${file}" + done + + for file in ${FILES_REMOVE[@]}; do + git reset -- "${file}" + done +} + + +_commit() { + _define_files + + git diff --numstat + + echo -e "" + echo -e "Commit/Push to Git" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_COMMIT}" + fi + + git commit -a -m "${VAR}" + git push +} + + +_clear() { + echo -e "" + echo -e "Clear History" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_CLEAR}" + fi + + rm -rf .git + + git init + + _define_files + + git commit -m "${VAR}" + git branch -M "${BRANCH}" + git remote add origin "${ORIGIN}" + git push -f -u origin "${BRANCH}" +} + + +_init() { + echo -e "" + echo -e "Init" + echo -e "Comment:" + + read VAR + if [ -z "${VAR}" ]; then + VAR="${COMMENT_INIT}" + fi + + rm -rf .git + + git init + + _define_files + + git branch -M "${BRANCH}" + git remote add origin "${ORIGIN}" + + git pull origin "${BRANCH}" + + git commit -m "${VAR}" + + git push -f -u origin "${BRANCH}" +} + + +############################################################################################################## +# Setup/Start + + +_prompt \ No newline at end of file diff --git a/docs/screenshots/lxmf_bridge_mqtt_01.png b/docs/screenshots/lxmf_bridge_mqtt_01.png new file mode 100644 index 0000000000000000000000000000000000000000..73d2a5e9055adedc84a0adf1dec932219a0f0001 GIT binary patch literal 62459 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

&IL&UG}HYUw(E!=|lSZw8YmX_1cq`Iqf; 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_cmd_01.png b/docs/screenshots/lxmf_cmd_01.png new file mode 100644 index 0000000000000000000000000000000000000000..56048faea7963997b1e30e22cfe6cb065667415c GIT binary patch literal 56962 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

c;w6OBgarMl3MfqSA?{MZxZZ+aR%2 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

UXn#|aVt_oTb?rs4IRXC^au`T+Tu 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_02.png b/docs/screenshots/lxmf_distribution_group_02.png new file mode 100644 index 0000000000000000000000000000000000000000..99696649eee9b7368747a38c76b463a71cc307fd GIT binary patch literal 11839 zcmbt)2V7Ijws#PvSCJxwCI|{rjq-gz&-mC4MkHEU01X0N^eYwailJuO-)b}9e>KzmzT z{XPId0s;We=~IvrdnVjKb;KVMulri6fTB*|BC&DaQAJk;0QeY3ePnlm$cr+#Yovj6 z5+bqyLQcrqvkzPcpaZY1MLPX#BjH4@&RId^1I{=^{!eK*(g{f@QcQm+GtJ2??A;shstoP>k`NGBbl4q^ci5+W9G9U*uv zQNx*5p)(Cc1;k68&5S7VbBvCYlar7RajXte4q1C94u>NF5yb#$LBL%>fHV{k0R?aq z=SgfP0q$x7yrBTwhXC$}fQTf3^h1DmF~GJM;QbH)&;)Q3Nf2P01n^D**lGgqLIKhM zfHbl4(8&pIQw&ho1l)fJVAcc(2m%C%6+r;=L%{tcfI6{+o?YR7F+hsg2>?hX0R*4` z8zQS1AOHZ^KmqDdfPf}IJqaMC36T1EH7O{7IB&Sl*{V35t$~h{jt<}y04Oeoy0JYX zPMH#{ZRQ04&^4cZN!r}=8~}hDO1ITjjQp)v)1w27GzN-?kzNwXys%WdoA-`7(^S-3 zc!DIRELHgvl$D=rdp}NKApI>&U4pJNt?8iipuMYRFxxM;HdYjta6W zuMK#Gf=()z?=8pw!EY6JA5z^@?%eqwj4@?pP_T)JhkGatd+1J@!oxGQ|t1WiRvxM#g{t*T=t|0So+f#N^h5$;H#UdE6*Av z6?f-ncw_clqN!ISHG%cDH^xrM1hs-m={l9w#2ZHiQKiRCMKcE}D8!~8%8L{%xkaeO zo;PDF%!LIO$#`&0)oEeUYc|}zBtsO|>Gje0HFftN85z<0a*Q27PBZuHO|O6-Vlirc`eYJ-;B#L zE^g)jSRnV0q_HG0kcZ9bC~U~@@Uo?jc+IpZp9TjjOM%QKuTJm3RoKhg#K)BQCp?sY zmw$(!mK(ax=}VWEx)LYXGIMaRrV#9*aAig(a7F&+#whjB&98%yk0UW$v`94!a$a&YPZvIHUD`~fbGKGj43Rf?CuFpovYz%#$nICoUsJ1g!2Y4kh6(CDD9)9 z!{*d}Tl6Gb(OY-b&X-D3?BKV*)h5jAI0r2>;{Ytny6D9x3M$)bfmPQ|88ul0ITuN5 zvGb4y`SE=)MPdE3fXOS8WXS^ChUxI=)7lQ=+OBC$FGqy8xID20IpBO>$@nQ?yzG+U z;YeWc_jNws;FeWG%PQ5sQy6$Fv1gHniFo)9w_Tdu>@Wu*%XG}KWNG89E?yJul$gzs z~Wgj4kV)ooqM9T3LOE%f)Z~cV z+jU1r7@w8KsTd1Guc%<5j*GXi&3qfr&##rZ-DF%c!i<)~Yzm`b?VMpgIS#E&Px zNW?_^^}KD~_LsSD=DcOEh)P%CwdrL#owpe}yN^Ct@>?b}?S4*`z@|IUn2A*v3UwdR z=+X*{`Bp&;WZ6iz`viWwFPz3{N0xpgi14*g_DvtSjw1HtT#l?sY)Z7@rD-RWsA?Ti zCtdY$NUbjE6$;FJ0;Zq*%Wvcfp1QkE{-g7!ziZv;t$b5!$|ER7uLdu^36`q8E@CUk z94oSI;D=*bJfm4)IAI zZbcYix$RugH5Gs^pX0^i7_edVh+bHPqzSB;(!j7Q+$u94#yRWxd>H2pw3+^1v}N-s zbZu#nRz%DfMG{-QeD#YhuG{M3LUvDCfQ7s7)h(5(Q<1#mmVgk)9yu!rfq(NA91` z?`!0a4+Ka<^wtOuSy}tDsF2yV`I)^bwM_jFhMTddWTKD2m!Udn7lZj=>LInEtwJiGxLxDDO#IbwXF6LSV zn3$g`v#f5joVjOthujDWM_X;n zE{3E=EX}}02AAo1bJCoW9DEGHP2oRoqP=_mCO3b>a)@3pznmP`8hh07Ud5ck5&wu= zrs-&bF!a=>ro<+FMQ5-qc6UAl1>S6BEM7KtJ(U_~)q7%Bd5ksKb(orzu8yHEyYZ-M zg*fia`yDz7)&qEy_S53%u6`;H($^j$-zzMd)dQRbou_?Z&xgnyG%c0pN7w8 z;ose`1f3ovc`JyigQ_u6U{^%3kQ})@HEX903$6x=Mp3Y?d|K;2Yk4>N5CV<&eslB;PwPLP6ow!(yQ(?h3(8c9)=V=bb^Zh3bVFJtp+{SyE4_+z6xqmRiR_-SlKPW;4 zEXU0{aI{R!@#URZX%~(!kf$C)dsE(MR~hGBzZFug0 zg3C5JUmy-GjLSckQS!xTUStV7GyCc3FQ<*=Zq8p87@ISHbB`k3uz3*k>^0R(u%({u z;Xpmsd79gS;X`t-3G?SiuLmzzG`$i_VFt$L$=kGFH z*e8|EMR{l&3&2}RPnwM$XC(L|txp6qa*0cxX;PK_2sCC1eNk-N-@9iWNWKoBQqUr(XR)OkgTA3extvC+m>|_6+LJv;`oNMsC84Sb44JWVnb%MM>EjK47y5)&|SKA7F~zU#z5mUB$%xL%clRy*CQ z_@O@N=v>{Q_s)f(MKabRhKrqxT*RMW#6rjF3seeSRwZcZYv~slT|4dgv2ak}dRbw7 zCC7fZ5%PegXniVbsUygM4)!(@@j>A35dtMA)2(xm{;4CJWK^|)$z}y#!Vgs9Eun{B z=E&8eT9P<=!v+1GsThnm4(iM=bkEir(dd$gdv8prRP8R?_W9SD(EP||^;ft-*``zd zoX&{vR8cd+OzY|GRK;LpgcoYrqI5nzRuRhj*iX`Uv?#ogaqWT)eJbUk|Ef4=?s=T@ zjax7AA0T>sEQV&qcV?flNth{EB@UI*nGBOI+imBkD8`$L)2aikx=`~%Y1RXI7E+jU zEAoPsP<%-TH=UZlN_!5SUTK#5Mg%EUc9(48_R+K44nwKJTnrpv=rahKykJe=j?u8u z+7Ey!<{G_)MIXw);VyRMtT>b0!7-rY9PrKqO^-K!E|Hty(C)L`!#K{1MIGCEd%W^8 zQ`R^ClGFH0SQ~U;A)8D6hw~U%@64)>sgGPXF7?SKA$T3nUAWSoSZt~xX-RENo#@s<;sV&j{3rdVw4uxahI@Cm*zSUIk zt7NiG<<I*4?hvHVvN3#P0m3+$l?^D2_|FTxweGW7}( zO13^Wk{yGP))|*FY!a4(&t`m@+$JIuJ)E6EDC9dJ5eZ@}lNr~19T=14M6KCo z^D1jM51foZH7{DCg?W&BuCS<0>4A6~#kW}zhyq2Nu`(%g?=jTi2 z?+*Y2rDl?0G8Bq(NIVJ+rsZ$Y#Y9;xN7>YK=jFE!df{DGd;Mz~(px)V?Z@WN_*v{Z zwisC};`)?_P;xkfs~yRZEbd16$QP3rfH%we0CAvOnFLGfiAQIDnyG=H`2@AON_(!P zrd(%g_{2BC-Gjg5xOKLc-6KTLZLM|x!1)8cVyvd^5)RIu+c-;Yhl-v4eCyjFWIxz} zn|m4l)(;vN|oMvKianXa(t?^c$c?faVPx8y*SoE6a(`mdylX>ykyn`CQf-kNAD=YeFX z6?l#g6qBGs=_+Dsr+3m?#Jal9$AON1D5)#K&3PJZXZOZBd%WxpxrW5%IW^tjw@CBB zyL%qj16fJFEo=kJfG3iK?4t#(rPbY|=NaJ}5>NOcs!=C9y%0hgS-!i++qrq=CqP|1 z+WaJtQf<#Dc$ZfYV@G-rX`R~5DA?uI?aAkk$m=BI3^A`&Xdnq55ggrHE@X)j?s6(2 zEJnIO=wL~GM(YKSx{@jKz**}!`e8?2c8|ao&M#3p&X#mPhRIGy9%c^EZEd&_uCWiY zrog6KmK-s&U7_8cDbMFM;)R6=zlX$3t7hneCdV8)B1s?CQj#b3&1;k~&`lEXn9qCD zl5{ye+gH9ATZ-CO!Z@7Yb2CpL-GbMR?H{1z+V>J+<^c?DD_FE=0x^#Z+gW>k zWohqyZD>)SiSt_7FoNp<72HD+uR@BpPoGTBUzbdj-x54&AV2Z=Yu{m|<3Oj&}0-qd{EzDfgiYte&(W?tY%%Y|XpRihiHIpwyw=-8Q^ zBi`zY6}An0*1_ouzi{cUEX?E{{9qVWL+Rue5y@P!B>m>}JDFV#VTYxT z0f{+c-jFzcn^vxQhzyb{_YLQM5g_HFtIn{t#CazY-j%y1s(hr8S3!|a^k*6`A}vE! z4NGmt-u2tcKTnyv*s{YKm@%3=$6E9Xj8kCFyQIU9j2HIH%~|W@r_I+O>I@HF>^h4r z%m_H?-ep+3L-ZqkF4Pq2;bZPj7|>lxwjcU2hU?aWAm*Btdj$k`g%K~KA1jrgJ+8WP z&@CU&iIrkGwNWn7%rwWcY`vgQ`3yz_R830q&24783%EmOuc<>;v3Dqx!u#p2YKe=zGgoQ1&0Y2Mtp%6Lv)A)|fkQ zuZSD12r+41=*2@O z7U!XOX|synZ*cTG$W%H}yTD6T5Y*&kKyZ^rKfRPEd)nr~E=JeNN}KBYbZuOdyBd|+ zy{-lK-7e?1N3%rIGMF+NR60Vk$Iy`8kkLxxi`j*$9P5>?OynAvn zMjf>fQZnF7ICmrAY&X25yN;ds#`@n?yqe^VVaB-k$0j$xvq9(r<*lFij;1alKOY%j5U>35RGlDJ789^`m-gf;T z1rVqssISLPqpvO^PAeZE#d*B5?c&Cz$|CUQLG`Lt#A!D_kUnQ_Ym_>7wmHbXTBwd|*O5u|031pB zh|?qm-cHn5=+|*P=_tG=F<6#iG>|K;Yn(q(Hepfp`-h0~d`Mobhe$-gQCUppB`R!|zgsf(q@+hNbl6F3Cycq^o;5%NC?{TqeuS5tOdZ zwkx|3tmC7-f!Yc_?l6&|LOiPLn_Hout5)u^9khOFyZJB14Ty0D7|g=6ruA=u12?n# zxujQcCTkwQe=xzCzA5@(yf^Ko3nyQ8dF?uTzEliaZ{ix*91$|Yt=jz_L&#dh%+e0;qa?DkVThM?Yf`(fP2-rExor4h=V0&J#@$rl?u-f%A% zfj)i+D&5(zK)?1lEK>sWuT$jSCn?I<%TrHf+}lvBiG6@0iE&O)ldbHZR5t0fJ^V&i zXS+_j8uh)O;u1x)IVI1o@Hr}?<$WoDGvS&JnI4X4IX2+SzPA&bv$Pam>LeD0yvpi{PSOCzqK4Il5V{(fg$=`@?HED{6r$D$?&RP0wrWDN5ls02Q=+?L6 zGpwN*Uo<%Sz7z$CO#BRCKAXK)PQuw*vTU2HL#|cusdGVp{A@1Sk~VYyJ4BUtk20@ zG>XrSr5c?xX%Lzk^~%aUJkYvv0!s`mV66(w?+Lzwl7X;SqVF|uBWhZ*1;uEmLPxSQ4hlo3 z$cMx@cLvXV|0w&#QYM(Z&lbb+5{BD~Z^Mw9yIX0C>S$Tly$eVG;3S4+t32bk19sPs zkMOOn!5E<>QyG({LEb!f-~8O5W$hs4Xq)q^x|NJqi4OlrSdYh;MWKXIRlEKKj`ZX! zRXlj;x^`QzoTkMdP+@@W$nQql4BrEKEvhx@!ENper+VjirUm>M9NI7?)VDA<2p?qo z`hH-o{+Sc^huDsO{`YBz~$rLg;uqM$-6cKyBNzN)w2>WLC= zPC``6(DT3++^)+DGK>k9GD64+-QNerp&08z)N*{!PclZ38zEe9wI41&8H7xkPEhDd zq3cxuCY8Ro>q;8Zss=#`b3xNvRxu!DK^Z^$qd=(va&9d?ar}mAjN#-KT->;A^C8!) zi^4do+tbNgs}lnF4C&Yr5#KkYsKm4)g#0Hu*6bf)VlZNYV?q{)N*?^q0TpR@a5xA>dDPOfIl`LJY>QV0 z5#;M9mGluDZ^6S}Oy|Z7fPj5I%k^6L;R7eF)`|Sio}%%sx(kI7eC)wL=z&U1;}I|A z&wI*TXdm`spIP5Yxe(9;n~oiZq5x7x@=q##FwhV0`Ml>N*Mh(3GB9hxvo5n)vBsvc zEu5~?>phUoC#UM0SR@HhtJ)7rj;Cy^k6N==NCh)B9NJFt)$62DXa(o)UAZr88Q7p> zYv(QW63jbP-F15>v?@29xF`M@wK=_eB`%_M|8xT?lRbZo`z=rs{<;)G@PqJ3 z{irXvjo^@*^5r_2_yBo=T66WiT@`w?eD~ap;yTIp_mv-!qx1iE!~S#U{}mcUEheI< z3L~c;`967yUlj4@4deR|>_w1}b%`O=)0UqCiD{E4KPjh2s(%3&=$&3s8i?hn%$d7* zYLps%=TCXX{&m-9GLpuQyw+o9&*a1#Top@H-WKW35xp|6Nk7lBRpd(Z?4`TSj9l7crI+E=$`M%`houn_@rvMNIGBQ$)_vl@?+` za5+AewrW(8CyXah^!ivl z1giM1HV(IVA6KS-NUHTU?-dn``%heu&1C1CR1c-3<%#fg%Wn$(kk+*8wDUWPy;pqc z4i#GEfsqcwy*j-d6w6mQDJfmmIp%2-Pq~D8QBEmirI{w)! zLyAh<2mNW~EIrSozTdoxd#G@F^+AqOa?U4(SQ!MNnAwf0y{pNX+q1ZD9yw=KPeMw! zg;S#tZ=l{sp7)T7NxNMqjnNgkf5^<$`+yNGpscGv?@Lltv(LYuT`cyn&1jT$s+`HKtLKxN{PzRR^gcNvGRm^5V1rs zF;5tG$7`}TO!<7O>BO?hcTlHW*AYV*F^W)9Q_5lV)3YeXfN0Rm&u+SWv8&G%%(_Ol zPc~*O*7bItL&qTnSq-_5Zr@+`u@-b$wq#~@yJQNfH&}>hX0V=onl4ngO>1qNT3QeO zIne9kak#l1v|z1Q024|p%4%2rn2A(u#8MX)J(!=7dV|eF*;0S76a^;Ath!Yy!AgBl z)AI#OH-a9}%yO4s>D~uR9YpMZYW6K?TY#7_cMep(b2}2fib0OnOBp-Gs!sc)FTGUS zQX=-EeoJM!Bk>h^}af z`ZZSD!`^SE^w*?@P8d`R!IbCX25}QqJCCn!Zg1FJbMUf6b!c3@^yx?>GM?pw#q+=|E)1=BFH1=fQH$I&_nU-f$+N(r4+dDC7vd8e>IG+DMIZexmP?qnWw!p#P5e zO2u`zLa_8gD(uf`um3MNc>5C^;HH_N)8y)JFqS2~XsVoTTj_ zk`PW(T1Mv~RZi7CGaHOkk;he(rJtvFddf^=mX|-@B-(9+x%c@W=)l1?R2uOU9_UG} z$+RWXNdn=NOH9VEJ_h1D0_ii^ude`=2FvDT3u1XNIJ+xAg0qGPXFOkVP%@ws#l-Vx(DHZmLaf||b2N%I+Fq9TiTA^Yi8XE% zTtO?yKxz=dEmQgrzE(m)+mqRk1>Zrkay{CC_`}V79ZSeoFArCq)zaGu_G>{Q$Gs}S z2H>W!{z;k-p>WSpzSug44v+UPlQZ|o=9MOaw9YmXzGzKMxV)?6&q=9 z(Z@V{i>K!Hx?3^p>Bnqwp4Z2^4cs5f84o zewmoO7_$SCfNxShlf0k_$*=6ccNeLVLT{^(s=?1(O&*0vY8411sgI6A>d{Q#__pbm;kQK!7P{acmcF&tvOkCITo z3mdG0$46Z3b^db;muqciB^ecg8zd|$pqrz|Lb*{qV+&$*v&1YwecGjqv2HWCn1Yw2 z*x7KEs(p0wt1B3s7n`l-)M|-g4CztY1vV&-*vpuM>SO*(Pr3hzaKIf9!K?o9kAxKj zGq=aTdAl|qd}_KMtk?FRUHO0E#)I|t{-x_z6Bys{n^(=dp8eA*&9^-_5?7AT-rc^f Mp{HJ?Y8&?d08Il@;Q#;t literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_03.png b/docs/screenshots/lxmf_distribution_group_03.png new file mode 100644 index 0000000000000000000000000000000000000000..e67ba648f5a8c1a4384428d28d070390b09c0b63 GIT binary patch literal 10419 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 US%wO4R~+D~k;UcGOLrdrAG2*YlK=n! literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_04.png b/docs/screenshots/lxmf_distribution_group_04.png new file mode 100644 index 0000000000000000000000000000000000000000..422348f9814213835fdc6bca8811e4b694bbafd7 GIT binary patch literal 8948 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 SPQR%O(9+PmTXM(lkN*P(ifYdQ literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_06.png b/docs/screenshots/lxmf_distribution_group_06.png new file mode 100644 index 0000000000000000000000000000000000000000..ba032508dba51a5e92179e5f11a3fa821f88de02 GIT binary patch literal 9711 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 Z<>u@4*pvCIq1rRd+QR-M`NV~r{{sCkzs~>w literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_08.png b/docs/screenshots/lxmf_distribution_group_08.png new file mode 100644 index 0000000000000000000000000000000000000000..36d4a91224d13a92d4149961d8bdcc449fde23f7 GIT binary patch literal 84444 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 N1fH&bF6*2UngHj%PZIzD literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_minimal_01.png b/docs/screenshots/lxmf_distribution_group_minimal_01.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6aa93b51e2c62e6e3d0952a0b999bf99b28e15 GIT binary patch literal 7910 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_distribution_group_minimal_02.png b/docs/screenshots/lxmf_distribution_group_minimal_02.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e7a49370d9c39843e91e9572ee98d66bdd2261 GIT binary patch literal 7488 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%0D-l1Q1t;h(TU#n5kVo!hOueuB&`>2BNr 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 U7)CfvLP2g{rIn>>B#Z<9KRjx`Q~&?~ literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_echo_01.png b/docs/screenshots/lxmf_echo_01.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d433725c916a56fa929f4132ec29db0f7dd1f3 GIT binary patch literal 48212 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 M{M51HBNuP|7Z0l^(*OVf literal 0 HcmV?d00001 diff --git a/docs/screenshots/lxmf_terminal_01.png b/docs/screenshots/lxmf_terminal_01.png new file mode 100644 index 0000000000000000000000000000000000000000..2cb3ae8603e50ab6ef28da18ef8e7e494364f917 GIT binary patch literal 56878 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 are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_bridge_mqtt.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_bridge_mqtt.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_bridge_mqtt.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_bridge_mqtt/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_bridge_mqtt.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_bridge_mqtt.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=LXMF Bridge MQTT Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_bridge_mqtt.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_bridge_mqtt + ``` +- Start the service. + ```bash + systemctl start lxmf_bridge_mqtt + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_bridge_mqtt + systemctl stop lxmf_bridge_mqtt + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_bridge_mqtt + systemctl disable lxmf_bridge_mqtt + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_bridge_mqtt.py -p /root/.lxmf_bridge_mqtt_2nd + ./lxmf_bridge_mqtt.py -p /root/.lxmf_bridge_mqtt_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_distribution_group_minimal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] + [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit + --exampledata Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py new file mode 100755 index 0000000..cadfeb3 --- /dev/null +++ b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py @@ -0,0 +1,1407 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import datetime +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + +#### MQTT #### +# Install: pip3 install paho-mqtt +# Source: https://pypi.org/project/paho-mqtt/ +import paho.mqtt.client as mqtt +import paho.mqtt.publish as publish + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Bridge MQTT" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None +MQTT_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_announce_to_mqtt"): + log("LXMF - Routing disabled", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(destination_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(destination_hash)): + message_out = json.dumps({ + "source": RNS.hexrep(destination_hash, False), + "data": app_data.decode("utf-8") + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_announce"], message_out) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("lxmf_to_mqtt"): + log("LXMF - Routing disabled", LOG_DEBUG) + return + + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "lxmf_to_mqtt_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "lxmf_to_mqtt_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "lxmf_to_mqtt_prefix") + content_suffix = config_get(CONFIG, "message", "lxmf_to_mqtt_suffix") + + search = config_get(CONFIG, "message", "lxmf_to_mqtt_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "lxmf_to_mqtt_replace")) + + search = config_get(CONFIG, "message", "lxmf_to_mqtt_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "lxmf_to_mqtt_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if message.signature_validated: + signature_string = "Validated" + signature_valid = 1 + else: + signature_valid = 0 + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + + message_out = json.dumps({ + "source": RNS.hexrep(message.source_hash, False), + "destination": RNS.hexrep(message.destination_hash, False), + "title": message.title.decode('utf-8').strip(), + "content": content, + "fields": str(message.fields), + "date_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), + "timestamp": message.timestamp, + "signature_valid": signature_valid, + "signature_string": signature_string + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_receive"], message_out) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# MQTT Functions + + +#### MQTT - Log #### +def mqtt_log_message(message, message_tag="MQTT - Message log"): + log(message_tag + ":", LOG_DEBUG) + log("- Topic: " + str(message.topic), LOG_DEBUG) + log("- Payload: " + str(message.payload.decode('utf-8')), LOG_DEBUG) + log("- QOS: " + str(message.qos), LOG_DEBUG) + + + + +#### MQTT - State #### +def mqtt_state(): + t = threading.Timer(int(CONFIG["mqtt"]["state_interval"])*60, mqtt_state_now) + t.daemon = True + t.start() + + + + +#### MQTT - State #### +def mqtt_state_now(): + if not CONFIG["router"].getboolean("state_to_mqtt"): + log("MQTT - Routing disabled", LOG_DEBUG) + return + + if CONFIG["main"].getboolean("power"): + power = "on" + else: + power = "off" + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], power) + + interfaces = None + try: + interfaces = RNS_CONNECTION.get_interface_stats() + except Exception: + pass + + message_out = json.dumps({ + "power": power, + "identity": str(LXMF_CONNECTION.identity), + "destination": str(LXMF_CONNECTION.destination), + "hash": RNS.hexrep(LXMF_CONNECTION.destination_hash(), False), + #"interfaces": interfaces, + "file_info": __file__, + "path_info": PATH, + "version_info": VERSION, + "host_info": os.uname(), + }) + + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_state"], message_out) + + + + +#### MQTT - Connected #### +def mqtt_connected_callback(client, userdata, flags, rc): + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_power"]) + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_state"]) + MQTT_CONNECTION.subscribe(CONFIG["mqtt"]["topic_send"]) + + if CONFIG["mqtt"].getboolean("state_startup"): + mqtt_state_now() + + if CONFIG["mqtt"].getboolean("state_periodic"): + mqtt_state() + + log("MQTT - Connected", LOG_DEBUG) + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_power(client, userdata, message): + global CONFIG + + mqtt_log_message(message) + + if message.payload.decode('utf-8') == "on" or message.payload.decode('utf-8') == "1": + CONFIG["main"]["power"] = True + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], "on") + else: + CONFIG["main"]["power"] = False + MQTT_CONNECTION.publish(CONFIG["mqtt"]["topic_rm_power"], "off") + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_state(client, userdata, message): + global CONFIG + + mqtt_log_message(message) + + mqtt_state_now() + + + + +#### MQTT - Message #### +def mqtt_message_received_callback_send(client, userdata, message): + mqtt_log_message(message) + + if not CONFIG["main"].getboolean("power") or not CONFIG["router"].getboolean("mqtt_to_lxmf"): + log("MQTT - Routing disabled", LOG_DEBUG) + return + + message_data = json.loads(message.payload.decode('utf-8')) + + if "destination" not in message_data or "content" not in message_data: + return + + content = message_data["content"].strip() + + length = config_getint(CONFIG, "message", "mqtt_to_lxmf_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "mqtt_to_lxmf_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "mqtt_to_lxmf_prefix") + content_suffix = config_get(CONFIG, "message", "mqtt_to_lxmf_suffix") + + search = config_get(CONFIG, "message", "mqtt_to_lxmf_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "mqtt_to_lxmf_replace")) + + search = config_get(CONFIG, "message", "mqtt_to_lxmf_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "mqtt_to_lxmf_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if "title" not in message_data: + message_data["title"] = "" + + timestamp = None + if "timestamp" in message_data and timestamp is None: + message_data["timestamp"] = message_data["timestamp"].strip() + if message_data["timestamp"] != "": + timestamp = float(message_data["timestamp"]) + + if "date_time" in message_data and timestamp is None: + message_data["date_time"] = message_data["date_time"].strip() + if message_data["date_time"] != "": + timestamp = time.mktime(datetime.datetime.strptime(message_data["date_time"], '%Y-%m-%d %H:%M:%S').timetuple()) + + LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), timestamp) + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + global MQTT_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("MQTT - Connecting ...", LOG_DEBUG) + MQTT_CONNECTION = mqtt.Client(CONFIG["mqtt"]["client_id"], False, userdata=None, transport=CONFIG["mqtt"]["transport"]) + MQTT_CONNECTION.on_connect = mqtt_connected_callback + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_power"], mqtt_message_received_callback_power) + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_state"], mqtt_message_received_callback_state) + MQTT_CONNECTION.message_callback_add(CONFIG["mqtt"]["topic_send"], mqtt_message_received_callback_send) + if CONFIG.has_option("mqtt", "username") and CONFIG.has_option("mqtt", "password"): + if CONFIG["mqtt"]["username"] != "" and CONFIG["mqtt"]["password"] != "": + MQTT_CONNECTION.username_pw_set(CONFIG["mqtt"]["username"], CONFIG["mqtt"]["password"]) + MQTT_CONNECTION.connect(CONFIG["mqtt"]["host"], int(CONFIG["mqtt"]["port"]), 60) + MQTT_CONNECTION.loop_forever() + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = + +# Enable/Disable the message routing. +# This is controllable with a MQTT message. +power = Yes + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = No +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = No +announce_periodic_interval = 360 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### MQTT connection settings #### +[mqtt] + +host = 192.168.10.201 + +port = 1883 + +transport = tcp + +client_id = lxmf_mqtt_bridge + +#username = + +#password = + +# Topic for power switch. +# Direction: From MQTT server. +# Data: on/off +topic_power = message/lxmf/power + +# Topic for power switch state. +# Direction: To MQTT server. +# Data: on/off +topic_rm_power = message/lxmf/rm_power + +# Topic for state request. +# Direction: From MQTT server. +# Data: any +topic_state = message/lxmf/state + +# Topic for state (diagnose data). +# Direction: To MQTT server. +# Data: {"power": "on", "identity": "<36a82374456b9c31a37f>", "destination": "", "hash": "36a82374456b9c31a37f", "interfaces": [{"clients": 0, "name": "Shared Instance[37428]", "rxb": 0, "txb": 0, "status": true}, {"clients": null, "name": "AutoInterface[Default Interface]", "rxb": 1111, "txb": 1176, "status": true}], "file_info": "/root/./lxmf_mqtt_bridge.py", "path_info": "/root/.lxmf_mqtt_bridge", "version_info": "0.0.1", "host_info": ["Linux", "test", "5.13.19-6", "#1 5.13.19-15 (Tue, 29 Mar 2022 15:59:50 +0200)", "x86_64"]} +topic_rm_state = message/lxmf/rm_state + +# Topic for announce. +# Direction: To MQTT server. +# Data: {"source": "36a82374456b9c31a37f", "data": "Sideband"} +topic_announce = message/lxmf/announce + +# Topic for receive messages (LXMF->MQTT). +# Direction: To MQTT server. +# Data: {"source": "36a82374456b9c31a37f", "destination": "36a82374456b9c31a37f", "title": "", "content": "Frage", "fields": "None", "date_time": "2022-05-07 14:19:10", "timestamp": 1651933150.803059, "signature_valid": 1, "signature_string": "Validated"} +topic_receive = message/lxmf/receive + +# Topic for send messages (MQTT->LXMF). +# Direction: From MQTT server. +# Data: {"destination":"36a82374456b9c31a37f", "content":"Antwort"} +# Data: {"destination":"36a82374456b9c31a37f", "content":"Antwort", "title":"Example", "date_time":"2022-10-01 11:45:00", "timestamp":""} +topic_send = message/lxmf/send + +# Send state at startup. +state_startup = Yes + +# Send state periodically. +state_periodic = Yes +state_interval = 30 #Minutes + + + + +#### Message router settings #### +[router] + +# Transmit LXMF messages to MQTT +lxmf_to_mqtt = True + +# Transmit MQTT messages to LXMF +mqtt_to_lxmf = True + +# Transmit LXMF announces to MQTT +lxmf_announce_to_mqtt = False + +# Transmit state to MQTT +state_to_mqtt = True + + + + +#### Message settings #### +[message] + +# Text is added. +lxmf_to_mqtt_prefix = +lxmf_to_mqtt_suffix = + +# Text is replaced. +lxmf_to_mqtt_search = +lxmf_to_mqtt_replace = + +# Text is replaced by regular expression. +lxmf_to_mqtt_regex_search = +lxmf_to_mqtt_regex_replace = + +# Length limitation. +lxmf_to_mqtt_length_min = 0 #0=any length +lxmf_to_mqtt_length_max = 0 #0=any length + + +# Text is added. +mqtt_to_lxmf_prefix = +mqtt_to_lxmf_suffix = + +# Text is replaced. +mqtt_to_lxmf_search = +mqtt_to_lxmf_replace = + +# Text is replaced by regular expression. +mqtt_to_lxmf_regex_search = +mqtt_to_lxmf_regex_replace = + +# Length limitation. +mqtt_to_lxmf_length_min = 0 #0=any length +mqtt_to_lxmf_length_max = 0 #0=any length + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_bridge_telegram/CHANGELOG.md b/lxmf_bridge_telegram/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_bridge_telegram/README.md b/lxmf_bridge_telegram/README.md new file mode 100644 index 0000000..be907b3 --- /dev/null +++ b/lxmf_bridge_telegram/README.md @@ -0,0 +1,6 @@ +# lxmf_bridge_telegram + + +## Overview + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. diff --git a/lxmf_bridge_telegram/lxmf_bridge_telegram.py b/lxmf_bridge_telegram/lxmf_bridge_telegram.py new file mode 100755 index 0000000..e69de29 diff --git a/lxmf_chatbot/CHANGELOG.md b/lxmf_chatbot/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_chatbot/README.md b/lxmf_chatbot/README.md new file mode 100644 index 0000000..a981d64 --- /dev/null +++ b/lxmf_chatbot/README.md @@ -0,0 +1,197 @@ +# lxmf_chatbot +This program provides a simple chatbot (RiveScript) which can communicate via LXMF. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_chatbot.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_chatbot/lxmf_chatbot.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_chatbot.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_chatbot.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_chatbot/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_chatbot.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_chatbot.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_chatbot.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_chatbot.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_chatbot + ``` +- Start the service. + ```bash + systemctl start lxmf_chatbot + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_chatbot + systemctl stop lxmf_chatbot + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_chatbot + systemctl disable lxmf_chatbot + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_chatbot.py -p /root/.lxmf_chatbot_2nd + ./lxmf_chatbot.py -p /root/.lxmf_chatbot_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_chatbot.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Chatbot - Simple chatbot (RiveScript) for LXMF messages + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_chatbot/lxmf_chatbot.py b/lxmf_chatbot/lxmf_chatbot.py new file mode 100755 index 0000000..bdff852 --- /dev/null +++ b/lxmf_chatbot/lxmf_chatbot.py @@ -0,0 +1,1145 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + +#### RiveScript #### +# Install: pip3 install rivescript +# Source: https://github.com/aichaos/rivescript-python +from rivescript import RiveScript + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Chatbot" +DESCRIPTION = "Simple chatbot (RiveScript) for LXMF messages" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None +RIVESCRIPT_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + content = RIVESCRIPT_CONNECTION.reply(RNS.hexrep(message.source_hash, False), content) + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + LXMF_CONNECTION.send(message.source_hash, content, message.title.decode('utf-8').strip()) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + global RIVESCRIPT_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("RiveScript - Connecting ...", LOG_DEBUG) + RIVESCRIPT_CONNECTION = RiveScript(utf8=True) + if not os.path.isdir(PATH + "/rivescript"): + os.makedirs(PATH + "/rivescript") + log("RiveScript - Path was created. You need to put the RiveScript configuration into the path befor you can use it!", LOG_ERROR) + else: + RIVESCRIPT_CONNECTION.load_directory(PATH + "/rivescript") + RIVESCRIPT_CONNECTION.sort_replies() + log("RiveScript - Connected", LOG_DEBUG) + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Echo Test + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = Chatbot + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = No +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = No +announce_periodic_interval = 360 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_cmd/CHANGELOG.md b/lxmf_cmd/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_cmd/README.md b/lxmf_cmd/README.md new file mode 100644 index 0000000..73dc465 --- /dev/null +++ b/lxmf_cmd/README.md @@ -0,0 +1,196 @@ +# lxmf_cmd +This program executes any text received by message as a system command and returns the output of the command as a message. Only single commands can be executed directly. No interactive terminal is created. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_cmd.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_cmd/lxmf_cmd.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_cmd.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_cmd.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_cmd/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_cmd.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_cmd.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_cmd.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_cmd.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_cmd + ``` +- Start the service. + ```bash + systemctl start lxmf_cmd + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_cmd + systemctl stop lxmf_cmd + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_cmd + systemctl disable lxmf_cmd + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_cmd.py -p /root/.lxmf_cmd_2nd + ./lxmf_cmd.py -p /root/.lxmf_cmd_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_cmd.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF CMD - + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_cmd/lxmf_cmd.py b/lxmf_cmd/lxmf_cmd.py new file mode 100755 index 0000000..e4c77f2 --- /dev/null +++ b/lxmf_cmd/lxmf_cmd.py @@ -0,0 +1,1169 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### External process #### +import subprocess +import socket + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF CMD" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + content = cmd(content) + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + content_prefix = replace(content_prefix) + content_suffix = replace(content_suffix) + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + LXMF_CONNECTION.send(message.source_hash, content_prefix + content + content_suffix, "") + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Functions + + +def replace(text): + text = text.replace("!user!", os.getlogin()) + text = text.replace("!hostname!", socket.gethostname()) + text = text.replace("!path!", os.getcwd()) + text = text.replace("!n!", "\n") + return text + + + + +def cmd(cmd): + content = "" + + params = cmd.split(' ') + + if params[0] == "cd": + try: + os.chdir(params[1]) + except: + content = "ERROR: No such directory: '" + params[1] + "'" + else: + try: + #process = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE) + #out, err = process.communicate(cmd.encode('utf-8')) + #content = out.decode('utf-8') + result = subprocess.run(params, capture_output=True, text=True) + content = result.stdout + result.stderr + except: + content = "ERROR: Command '" + cmd + "' not found" + + return content + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = CMD + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = CMD + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = No +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = No +announce_periodic_interval = 360 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = !user!@!hostname!:!path!#!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +#any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_distribution_group/CHANGELOG.md b/lxmf_distribution_group/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_distribution_group/README.md b/lxmf_distribution_group/README.md new file mode 100644 index 0000000..abe8f27 --- /dev/null +++ b/lxmf_distribution_group/README.md @@ -0,0 +1,685 @@ +# lxmf_distribution_group +This program provides an email like distribution group. It will distribute incoming LXMF messages to multiple recipients. Since this program acts as a normal LXMF endpoint, all compatible chat applications can be used. In addition to simple messaging, there is a simple command-based user interface. Where all relevant actions for daily administration can be performed. The basic configuration is done in the configuration files. There are various options to adapt the entire behavior of the group to personal needs. This distribution group is much more than a standard email distribution group. It emulates advanced group functions with automatic notifications etc. Different user permissions can be defined. For each user type, the range of functions can be defined individually. The normal users have only small rights. While a moderator or admin can perform everything necessary by simple commands. Once the basic configuration is done, everything else can be done by LXMF messages as commands. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Server/Node based message routing and processing +- Direct or propagated message delivery (receive/send) +- Simple group functions (As in other messenger apps) +- User authorization and permissions +- Different user types with different permissions +- Automatic or manual group joining +- Text based interface to display advanced functions or infos +- Cluster of several groups (communication between groups with different levels) +- Automatic negotiation of the clusters +- Statistics at cluster, group and user level +- Easy configuration within readable config files +- Various admin commands for the daily tasks to be controlled via LXMF messages +- Group description, rules and pinned messages +- Optional enableable waiting room for new members before joining the group +- Multiple language support (English & German are predifined) + + +## Examples of use + +### Local self-sufficient group +In a small group of people, this group software can be hosted on a centrally located node. This then allows users to communicate with each other via this group. + +### Multiple local self-sufficient group +On the same node/server several groups can be operated independently of each other. How this works is described below in the installation instructions. + +### Networking groups as a cluster +It is possible to connect several locally independent groups to a cluster. This makes it possible to send messages from one group to another. + +### Hierarchical cluster groups over widely spread areas +A group cluster can be built in several levels. A group can be labeled with several names according to the naming of the levels. +This makes it possible, for example to send a messages to several groups at the same time. So you could define the cluster names as follows. `Country/Region/City` +With this it is possible to contact all groups of a certain country or region. + +### General info how the messages are transported +All messages between client<->group-server and group-server<->group-server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + +When a message is sent to a multi-level (hierarchical) cluster. A 1:1 connection is always established from the source to each target group in this cluster level. + +There is no central server for communication between the individual groups. This offers the advantage that all groups work autonomously. A failure of a group only affects this one local group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Development Roadmap +- Planned, but not yet scheduled + - Parameters for backup/restore configuration and data + - Parameters for backup/restore identity + - Cluster bridges/repeater + - Different message priorities + - Fallback solution: Master/Slave + - Centralized user/group authorization + - Internal queue with prioritization + - More intelligent messages sending + - Command to display the send status of the last message + - Automatic send confirmation + - Complete documentation + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_distribution_group.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group/lxmf_distribution_group.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_distribution_group/config.cfg.owr + ``` + ```bash + # This is the user configuration file to override the default configuration file. + # All settings made here have precedence. + # This file can be used to clearly summarize all settings that deviate from the default. + # This also has the advantage that all changed settings can be kept when updating the program. + + + #### Main program settings #### + [main] + + # Default language. + lng = en # en/de + + + #### LXMF connection settings #### + [lxmf] + + # The name will be visible to other peers + # on the network, and included in announces. + # It is also used in the group description/info. + display_name = Distribution Group + + # Propagation node address/hash. + propagation_node = ca2762fe5283873719aececfb9e18835 + + # Try to deliver a message via the LXMF propagation network, + # if a direct delivery to the recipient is not possible. + try_propagation_on_fail = Yes + + + #### Cluster settings #### + [cluster] + + # Enable/Disable this functionality. + enabled = True + + # To use several completely separate clusters/groups, + # an individual name and type can be assigned here. + name = grp + type = cluster + + # Slash-separated list with the names of this cluster. + # No spaces are allowed in the name. + # All send messages that match the name will be received. + # The last name is the main name of this group + # and is used as source for send messages. + display_name = County/Region/City + + + #### Statistic/Counter settings #### + [statistic] + + # Enable/Disable this functionality. + enabled = True + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group + systemctl stop lxmf_distribution_group + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group + systemctl disable lxmf_distribution_group + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_2nd + ./lxmf_distribution_group.py -p /root/.lxmf_distribution_group_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own group LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- If auto add user is active (default) you can simply send a first message via Sideband/NomadNet to this address. After that you are a member of the group and can use the functions. +- Alternatively, the users can also be entered manually in the `data.cfg` file. It is necessary to add an admin user here to use all commands via LXMF messages! +- Now the group can be used. + + +### Startup parameters: +```bash +usage: lxmf_distribution_group.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit + --exampledata Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + +- data.cfg + + This is the data file. It is automatically created and saved/overwritten. + It contains data managed by the software itself. + If manual adjustments are made here, the program must be shut down first! + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +### Cluster: +This example shows the configuration for a cluster with 2 groups. This allows communication between both groups. +It is possible to write directly to each group or to the higher level which then includes both groups. + +- Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + +- Group #2 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 2 + [cluster] + enabled = True + name = test + type = cluster + display_name = Germany/Bayern/München + ``` + +- Group #2 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + + +### 2 independent cluster: +This example shows the configuration for 2 separate clusters. +This makes it possible to operate several clusters in parallel via the same communication network. +It is important to configure the `name` and `type` differently. + +- Cluster #1 - Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test1 + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Cluster #1 - Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + +- Cluster #2 - Group #1 `config.cfg.owr` + ``` + [lxmf] + display_name = Group Test 1 + [cluster] + enabled = True + name = test2 + type = cluster + display_name = Germany/NRW/Düsseldorf + ``` + +- Cluster #2 - Group #1 `data.cfg` + ``` + [main] + enabled_cluster = True + auto_add_cluster = True + ``` + + +### Members/Clusters: +Normally all data here (`data.cfg`) is created automatically by the software. Based on automatic creation of new users/clusters or executed commands for administration. +Here are a few examples of how the content can look. Of course, the file can also be edited manually. This is necessary if an auto add is disabled. +Please do not forget to close the program first! + +- Group #1 `data.cfg` + ``` + [user] + 04652a820cc69d47940ce39050c455a6 = Test User 1 + + [cluster] + d1b551e1b89fff5a4a6f2aaff2464971 = Germany/Bayern/München + ``` + +- Group #2 `data.cfg` + ``` + [user] + 18201a931dd69d47940ce39050c487c9 = Test User 1 + + [cluster] + 801f48d54bc71cb3e0886944832aaf8d = Germany/NRW/Düsseldorf + ``` + + +### Waiting room for new members: +This example shows the configuration for a waiting room for new members. +When an unknown user joins the group by the first message to the group, he is added to the "wait" type. +There he will be in a kind of waiting room where no messages can be written and received. +An admin or moderator can then allow or disallow this user. + +The configuration shows only the minimum necessary part for this functionality. Of course, further rights can be assigned to the users. + +- `config.cfg.owr` + ``` + [rights] + admin = interface,receive_join,allow,deny + mod = interface,receive_join,allow,deny + wait = + + [interface_messages] + auto_add_wait = Welcome to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. + auto_add_wait-de = Willkommen in der Gruppe "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + + allow_user = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name + allow_user-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name + + deny_user = You have been denied to join the group "!display_name!"! + deny_user-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! + + member_join = !source_name! joins the waiting room and must be allowed to join the group. + member_join-de = !source_name! betritt den Warteraum und muss zur Gruppe zugelassen werden. + ``` + +- `data.cfg` + ``` + [main] + auto_add_user = True + auto_add_user_type = wait + allow_user = True + allow_user_type = user + deny_user = True + deny_user_type = block_wait + ``` + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + +An administartor has correspondingly higher permissions and more commands are available. In general, the permissions can be freely defined. All users/admins etc. can also generally have the same permissions. + + +### Activate/deactivate functions: +The following functions can be adjusted accordingly by command. + +`/enable_local ` = Local message routing + +`/enable_cluster ` = Cluster message routing + +`/auto_add_user ` = Add unknown user functionality + +`/auto_add_cluster ` = Add unknown cluster functionality + + +### Change values: +`/description ` = Change description + +`/rules ` = Change rules + + +### Send a manual announce of the group and cluster: +`/announce` + + +### Manage users (display of existing users): +`/show or /list` + +`/show or /list ` + +`/search ` + + +### Manage users (invite): +Additional users can be invited, this can be done with the command `/invite `. +Then the user gets a welcome message and enters the group. + + +### Manage users (allow/deny): +If the waiting room is activated, the users can be administered with the following 2 commands. + +`/allow ` + +`/deny ` + + +### Manage users (add/delete/move): +The following commands can be used to administrate the users. +Only in case of an invite a welcome message will be sent to the user. Users added here will not get a notification and have to start the first conversation with the group themselves. Or get a message sent directly. + +`/add ` + +`/del or /rm ` + +`/del or /rm ` + +`/move ` + + +### Manage users (kick/block/unblock): +The following commands can be used to remove/enable the users. + +`/kick ` + +`/block ` + +`/unblock ` + + +### Save data: +If an automatic save is set in the config nothing has to be done here. If not or additionally the data can be saved with the following command. + +`/save` + + +### Help: +To display the help and all available commands the following commands can be used. `/help` or `/?` + + +### Examples of possible commands: +``` +/help or /? = Shows this help +/leave or /part = Leave group +/name = Show current nickname +/nick = Show current nickname +/name = Change/Define nickname +/nick = Change/Define nickname +/address = Dislay address info +/info = Show group info +/description = Show current description +/rules = Show current rules +/version = Show version info +/groups or /cluster = Show all groups/clusters +/groups = Searches for a group/cluster by name +/members or /names or /who = Show all group members +/admins = Show group admins +/moderators or /mods = Show group moderators +/users = Show group users +/guests = Show group guests +/search = Searches for a user by nickname or address +/whois = Searches for a user by nickname or address +/activitys = Show user activitys +/statistic or /stat = Show group statistic +/status = Show status +/delivery or /message = Show delivery status of last message +/enable_local = Local message routing +/enable_cluster = Cluster message routing +/auto_add_user = Add unknown user functionality +/auto_add_user_type +/auto_add_cluster = Add unknown cluster functionality +/invite_user = Invite functionality +/invite_user_type +/description = Change description +/rules = Change rules +/announce = Send announce +/sync = Synchronize messages with propagation node +/show run = Show current configuration +/show or /list +/show or /list +/add +/del or /rm +/del or /rm +/move +/invite = Invites user to group +/kick = Kicks user out of group +/block = Block user +/ban = Block user +/unblock = Unblock user +/unban = Unblock user +/load or /read = Read the configuration/data +/save or /wr = Saves the current configuration/data +``` + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +### Start/Join the group: +Just send a first message to the group address with Sideband/NomadNet. +However, this is only possible if automatic joining of the group is activated. + + +### Send local group message: +Any normal text without `/` or `@` at the beginning will be interpreted as a normal message and sent to all local members accordingly. There is nothing else to consider here. + + +### Send cluster message: +It is possible to send messages to other groups which are part of the cluster. To do this you must first enter the command `@` followed by the target name of the group and then the normal message text. + +For example `@Berlin Hello this is a test :)`. So this example would send this message to the Berlin group. + +A group in a cluster can be arranged hierarchically in different levels. If the higher level is defined as the target, all groups below it receive this message. + +For example, there are the following 3 groups `Germany/Berlin` and `Germany/Hamburg` and `Germany/Munich`. Accordingly, these can be written to directly or a higher level. + +With the command `@Germany ` all 3 groups are now accessible. With the command `@Munich ` only this one group is accessible. + + +### Pin message (local group): +It is possible to pin local group messages permanently. This will then sent to all members. Additionally, all pinned messages can be displayed later. + +This feature is useful to give new members access to important news from the past. + +`/pin` = Display all pinned messages + +`/pin ` = Pin a new message + +`/unpin <#id>` = Remove a pinned message + + +### Pin message (cluster group): +It is possible to pin cluster group messages permanently. This will then sent to all members. Additionally, all pinned messages can be displayed later. + +This feature is useful to give new members access to important news from the past. + +`@Group /pin ` = Pin a new message + + +### Interface/Commands: +A simple text-message based user interface is integrated. Like you might know it from other chat programs. Every command must start with the delimiter `/`. Then followed by the command and any data. For example `/name My new nick name`. + +If there is no `/` at the beginning this is a normal message and will be sent to the other members. + + +### Help: +To display the help and all available commands the following commands can be used. `/help` or `/?` + + +### Leave the group: +The `/leave` command is used to leave the group. Afterwards, the group can be re-entered (if it is allowed). + + +### Invite users: +If the admin has allowed additional users to be invited, this can be done with the command `/invite `. +Then the user gets a welcome message and enters the group. + + +### Change nickname: +The own nickname is either assigned automatically via received announce (after joining the group) or can be changed via the following command. + +`/name ` For example `/name Max Walker`. + + +### Examples of possible commands: +``` +/help or /? = Shows this help +/leave or /part = Leave group +/name = Show current nickname +/nick = Show current nickname +/name = Change/Define nickname +/nick = Change/Define nickname +/address = Dislay address info +/info = Show group info +/description = Show current description +/rules = Show current rules +/version = Show version info +/groups or /cluster = Show all groups/clusters +/groups = Searches for a group/cluster by name +/members or /names or /who = Show all group members +/admins = Show group admins +/moderators or /mods = Show group moderators +/users = Show group users +/guests = Show group guests +/search = Searches for a user by nickname or address +/whois = Searches for a user by nickname or address +/activitys = Show user activitys +/statistic or /stat = Show group statistic +/delivery or /message = Show delivery status of last message +/invite = Invites user to group +``` + + +## FAQ + +### Why this server based group function and no direct groups in the client software? +At the time of the development of these group functions there is no other possibility to use groups via Sideband/Nomadnet. Therefore this software was developed as a workaround. +This software also offers other functions than a normal group broadcast. + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_distribution_group/lxmf_distribution_group.py b/lxmf_distribution_group/lxmf_distribution_group.py new file mode 100755 index 0000000..ca11350 --- /dev/null +++ b/lxmf_distribution_group/lxmf_distribution_group.py @@ -0,0 +1,4676 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import datetime +import argparse + +#### Config #### +import configparser + +#### Variables #### +from collections import defaultdict + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Search #### +import fnmatch + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Distribution Group" +DESCRIPTION = "Server-Side group functions for LXMF based apps" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +CONFIG = None +STATISTIC = None +RNS_MAIN_CONNECTION = None +LXMF_CONNECTION = None +RNS_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# RNS Class + + +class rns_connection: + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="rns", destination_type="connect", announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, announce_data=""): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.announce_data = announce_data + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("RNS - Storage path was created", LOG_NOTICE) + log("RNS - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("RNS - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("RNS - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("RNS - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("RNS - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("RNS - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("RNS - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("RNS - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("RNS - Could not create and save a new Primary Identity", LOG_ERROR) + log("RNS - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("RNS - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("RNS - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def announce(self, initial=False): + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce(self.announce_data.encode("utf-8")) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + global DATA + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + if CONFIG["main"].getboolean("auto_name_def") or CONFIG["main"].getboolean("auto_name_change"): + source_hash = RNS.hexrep(destination_hash, False) + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + if (val == "" and CONFIG["main"].getboolean("auto_name_def")) or (val != "" and CONFIG["main"].getboolean("auto_name_change")): + value = app_data.decode("utf-8").strip() + if value != DATA[section][key]: + if DATA[section][key] == "": + content_type = "name_def" + content_add = " " + value + else: + content_type = "name_change" + content_add = " " + DATA[section][key] + " -> " + value + + DATA[section][key] = value + + content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) + if content_group != "": + content_group = replace(content_group, source_hash, value, "", lng_key) + content_group = content_group + content_add + for section in sections: + if "receive_auto_"+content_type in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + content = message.content.decode('utf-8') + content = content.strip() + if content == "": + return + + title = message.title.decode('utf-8') + title = title.strip() + + fields = message.fields + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + destination_hash = RNS.hexrep(message.destination_hash, False) + source_hash = RNS.hexrep(message.source_hash, False) + source_name = "" + source_right = "" + + for section in DATA.sections(): + if section.startswith("block"): + if DATA.has_option(section, source_hash): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " blocked", LOG_DEBUG) + source_right = section.replace("block_", "") + source_rights = config_get(CONFIG, "rights", source_right) + source_rights = source_rights.split(",") + if "reply_block" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_block", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + source_name = val + source_right = section + + if fields: + if "c_n" in fields and "c_t" in fields and "m_t" in fields: + if fields["c_n"] == CONFIG["cluster"]["name"] and fields["c_t"] == CONFIG["cluster"]["type"] and source_right == "cluster" and config_getboolean(CONFIG, "cluster", "enabled"): + content_prefix = config_get(CONFIG, "message", "cluster_receive_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "cluster_receive_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + source = source_name.rsplit('/', 1)[-1] + destination = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + + search = config_get(CONFIG, "message", "cluster_receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "cluster_receive_replace")) + + search = config_get(CONFIG, "message", "cluster_receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "cluster_receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", "cluster_in_" + message.desired_method_str) + + if fields["m_t"] == "message": + for section in sections: + if "receive_cluster" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "cluster_send") + elif fields["m_t"] == "pin": + delimiter = CONFIG["interface"]["delimiter_output"] + + value_new = config_get(CONFIG, "interface_menu", "cluster_pin", "", lng_key) + value_new = replace(value_new, source_hash, source_name, source_right, lng_key) + value_new = value_new.replace(delimiter+"value"+delimiter, content) + + key = time.strftime(config_get(CONFIG, "message", "pin_id", "%y%m%d-%H%M%S", lng_key), time.localtime(time.time())) + if DATA.has_option("pin", key): + key = key + "-" + key_int = 0 + while DATA.has_option("pin", key+str(key_int)): + key_int += 1 + key = key+str(key_int) + + DATA["pin"][key] = value_new + + content_group = config_get(CONFIG, "interface_messages", "cluster_pin_add", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value_new) + if content_group != "": + for section in sections: + if "receive_cluster_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "cluster_send") + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + + return + + if source_right == "" and DATA["main"].getboolean("auto_add_user"): + if CONFIG["lxmf"].getboolean("signature_validated_new") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature 'new'", LOG_DEBUG) + return + + key = DATA["main"]["auto_add_user_type"] + if DATA.has_section(key) and key != "main": + DATA[key][source_hash] = "" + DATA.remove_option("main", "unsaved") + content = config_get(CONFIG, "interface_messages", "auto_add_"+key, "", lng_key) + content_group = config_get(CONFIG, "interface_messages", "member_join", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_join" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, title, None, None, "interface_send") + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + content = replace(content, source_hash, source_name, source_right, lng_key) + if content != "": + LXMF_CONNECTION.send(source_hash, content, title, None, None, "interface_send") + return + elif source_right == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist (auto add disabled)", LOG_DEBUG) + return + + + source_rights = config_get(CONFIG, "rights", source_right) + if source_rights == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no right", LOG_DEBUG) + return + source_rights = source_rights.split(",") + + + if CONFIG["lxmf"].getboolean("signature_validated_known") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature 'known'", LOG_DEBUG) + if "reply_signature" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_signature", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + length = config_getint(CONFIG, "message", "receive_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + length = config_getint(CONFIG, "message", "receive_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + + content_prefix = config_get(CONFIG, "message", "receive_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "receive_suffix", "", lng_key) + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + + # Interface + if content.startswith(CONFIG["interface"]["delimiter_input"]): + if not config_getboolean(CONFIG, "interface", "enabled"): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' disabled", LOG_DEBUG) + if "reply_interface_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_interface_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + if "interface" not in source_rights: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' not allowed", LOG_DEBUG) + if "reply_interface_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_interface_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content = interface(content[len(CONFIG["interface"]["delimiter_input"]):], source_hash, source_name, source_right, source_rights, lng_key) + if content == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' not allowed (empty response)", LOG_DEBUG) + return + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("interface"): + statistic("add", "interface_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + LXMF_CONNECTION.send(source_hash, content, "", None, None, "interface_send") + return + + + # Message - Cluster + if content.startswith(CONFIG["cluster"]["delimiter_input"]): + if not config_getboolean(CONFIG, "cluster", "enabled") or not DATA["main"].getboolean("enabled_cluster"): + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'cluster' disabled", LOG_DEBUG) + if "reply_cluster_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_cluster_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + if "send_cluster" not in source_rights: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'cluster' not allowed", LOG_DEBUG) + if "reply_cluster_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_cluster_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + try: + content = content[len(CONFIG["cluster"]["delimiter_input"]):] + destination, content = content.split(" ", 1) + except: + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_format_error", "", lng_key) , "", None, None, "interface_send") + return + + destinations = [] + for (key, val) in DATA.items("cluster"): + if key != destination_hash and destination in val.split("/"): + destinations.append(key) + + if len(destinations) == 0: + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_found_error", "", lng_key) , "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "cluster_send_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "cluster_send_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content_prefix = config_get(CONFIG, "message", "cluster_send_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "cluster_send_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + source = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) + + search = config_get(CONFIG, "message", "cluster_send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "cluster_send_replace")) + + search = config_get(CONFIG, "message", "cluster_send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "cluster_send_regex_replace"), content) + + fields = defaultdict(dict) + fields["c_n"] = CONFIG["cluster"]["name"] + fields["c_t"] = CONFIG["cluster"]["type"] + + delimiter_input = CONFIG["interface"]["delimiter_input"] + if (content.startswith(delimiter_input+"pin ") or content.startswith(delimiter_input+"pins ")) and "cluster_pin_add" in source_rights: + content = content.lstrip(delimiter_input+"pin ") + content = content.lstrip(delimiter_input+"pins ") + fields["m_t"] = "pin" + else: + fields["m_t"] = "message" + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("cluster"): + statistic("add", "cluster_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("add", source_hash) + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + for val in destinations: + LXMF_CONNECTION.send(key, content, title, fields, timestamp, "cluster_out") + + cluster_loop = False + if destination in config_get(CONFIG, "cluster", "display_name", "", lng_key).split("/"): + cluster_loop = True + + for section in sections: + if "receive_cluster_send" in config_get(CONFIG, "rights", section).split(",") or (cluster_loop and "receive_cluster_loop" in config_get(CONFIG, "rights", section).split(",")): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "local_send") + + return + + + # Message - Local + if DATA["main"].getboolean("enabled_local"): + if "send_local" in source_rights: + + length = config_getint(CONFIG, "message", "send_length_min", 0, lng_key) + if length> 0: + if len(content) < length: + if "reply_length_min" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + length = config_getint(CONFIG, "message", "send_length_max", 0, lng_key) + if length > 0: + if len(content) > length: + if "reply_length_max" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + return + + content_prefix = config_get(CONFIG, "message", "send_prefix", "", lng_key) + content_suffix = config_get(CONFIG, "message", "send_suffix", "", lng_key) + + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + if CONFIG["statistic"].getboolean("enabled"): + if CONFIG["statistic"].getboolean("local"): + statistic("add", "local_received_" + message.desired_method_str) + if CONFIG["statistic"].getboolean("user"): + statistic("add", source_hash) + statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + for section in sections: + if "receive_local" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, None, timestamp, "local_send") + return + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'send' not allowed", LOG_DEBUG) + if "reply_local_right" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_local_right", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + else: + if "reply_local_enabled" in source_rights: + content_user = config_get(CONFIG, "interface_messages", "reply_local_enabled", "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(source_hash, content_user, "", None, None, "interface_send") + + + return + + + + +#### LXMF - Notification #### +def lxmf_message_notification_success_callback(message): + if CONFIG["statistic"].getboolean("enabled"): + if message.app_data.startswith("cluster") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + elif message.app_data.startswith("local") and CONFIG["statistic"].getboolean("local"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + elif message.app_data.startswith("interface") and CONFIG["statistic"].getboolean("interface"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_success") + + if CONFIG["statistic"].getboolean("user"): + if message.desired_method_str == "direct": + destination_hash = RNS.hexrep(message.destination_hash, False) + statistic("value_set", destination_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + statistic("value_set", destination_hash, "activity_send", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + return + + + + +#### LXMF - Notification #### +def lxmf_message_notification_failed_callback(message): + if CONFIG["statistic"].getboolean("enabled"): + if message.app_data.startswith("cluster") and CONFIG["statistic"].getboolean("cluster"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + elif message.app_data.startswith("local") and CONFIG["statistic"].getboolean("local"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + elif message.app_data.startswith("interface") and CONFIG["statistic"].getboolean("interface"): + statistic("add", message.app_data + "_" + message.desired_method_str + "_failed") + return + + +############################################################################################################## +# RNS Functions + + +class rns_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("Cluster - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + global DATA + + lng_key = "-" + CONFIG["main"]["lng"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + receive = app_data.decode("utf-8") + if receive != "": + receive = json.loads(receive) + + if "c" in receive and "c_h" in receive and "c_d" in receive and CONFIG["cluster"].getboolean("enabled") and DATA["main"].getboolean("auto_add_cluster"): + if receive["c"] == "1": + if not DATA.has_option("cluster", receive["c_h"]): + content_group = config_get(CONFIG, "interface_messages", "cluster_join", "", lng_key) + content_group = replace(content_group, receive["c_h"], receive["c_d"], "", lng_key) + if content_group != "": + for section in sections: + if "receive_cluster_join" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + DATA["cluster"][receive["c_h"]] = receive["c_d"] + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + + +############################################################################################################## +# Interface + + +#### Interface ##### +def interface(cmd, source_hash, source_name, source_right, source_rights, lng_key): + cmd = cmd.strip() + + content = "" + + delimiter = CONFIG["interface"]["delimiter_output"] + + sections = [] + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + sections.append(key) + + # "/help" command. + if (cmd == "help" or cmd == "?") and "help" in source_rights: + content = config_get(CONFIG, "interface_menu", "help_"+source_right, "", lng_key) + interface_help = "" + interface_help_command = "" + for value in source_rights: + interface_help = interface_help + config_get(CONFIG, "interface_help", value, "", lng_key) + interface_help_command = interface_help_command + config_get(CONFIG, "interface_help_command", value, "", lng_key) + content = content.replace(delimiter+"interface_help"+delimiter, interface_help) + content = content.replace(delimiter+"interface_help_command"+delimiter, interface_help_command) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/leave" command. + elif (cmd == "leave" or cmd == "part") and "leave" in source_rights: + try: + for section in sections: + for (key, val) in DATA.items(section): + if key == source_hash: + DATA.remove_option(section, key) + + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", key) + + content_group = config_get(CONFIG, "interface_messages", "member_leave", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_leave" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "leave_ok", "", lng_key) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "leave_error", "", lng_key) + + + # "/name" command. + elif (cmd == "name" or cmd == "nick") and "name" in source_rights: + content = config_get(CONFIG, "interface_menu", "name", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif (cmd.startswith("name ") or cmd.startswith("nick ") or cmd.startswith("setname ")) and "name" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + for section in sections: + for (key, val) in DATA.items(section): + if key == source_hash: + DATA[section][key] = value + + if source_name == "": + content_type = "name_def" + content_add = " " + value + else: + content_type = "name_change" + content_add = " " + source_name + " -> " + value + + content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) + if content_group != "": + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group + content_add + for section in sections: + if "receive_"+content_type in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "name_ok", "", lng_key) + " " + value + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "name_error", "", lng_key) + + + # "/address" command. + elif cmd == "address" and "address" in source_rights: + content = config_get(CONFIG, "interface_menu", "address_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/info" command. + elif cmd == "info" and "info" in source_rights: + content = config_get(CONFIG, "interface_menu", "info_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/pin" command. + elif (cmd == "pin" or cmd == "pins") and "pin" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "pin_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for (key, val) in DATA.items("pin"): + count += 1 + content = content + "#" + key + "\n" + val + "\n\n" + content = content.replace(delimiter+"count"+delimiter, str(count)) + + elif (cmd.startswith("pin ") or cmd.startswith("pins ")) and "pin_add" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value_new = config_get(CONFIG, "interface_menu", "pin", "", lng_key) + value_new = replace(value_new, source_hash, source_name, source_right, lng_key) + value_new = value_new.replace(delimiter+"value"+delimiter, value) + + key = time.strftime(config_get(CONFIG, "message", "pin_id", "%y%m%d-%H%M%S", lng_key), time.localtime(time.time())) + if DATA.has_option("pin", key): + key = key + "-" + key_int = 0 + while DATA.has_option("pin", key+str(key_int)): + key_int += 1 + key = key+str(key_int) + + DATA["pin"][key] = value_new + + content_group = config_get(CONFIG, "interface_messages", "pin_add", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value_new) + if content_group != "": + for section in sections: + if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "pin_add_ok", "", lng_key) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + elif (cmd.startswith("unpin ") or cmd.startswith("unpins ")) and "pin_remove" in source_rights: + try: + cmd, key = cmd.split(" ", 1) + if key.startswith("#"): + key = key[1:] + if DATA.has_option("pin", key): + value = DATA["pin"][key] + DATA.remove_option("pin", key) + + content_group = config_get(CONFIG, "interface_messages", "pin_remove", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"key"+delimiter, key) + content_group = content_group.replace(delimiter+"value"+delimiter, value) + if content_group != "": + for section in sections: + if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "pin_remove_ok", "", lng_key) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "pin_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/version" command. + elif cmd == "version" and "version" in source_rights: + content = config_get(CONFIG, "interface_menu", "version_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content + NAME + "\n" + DESCRIPTION + "\nV" + VERSION + + + # "/groups" command. + elif (cmd == "groups" or cmd == "group" or cmd == "cluster") and "groups" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "groups_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "groups_member", "", lng_key) + data_dict = defaultdict(dict) + section = "cluster" + for (key, val) in DATA.items(section): + data_dict[val] = key + for key in sorted(data_dict): + count += 1 + content = content + replace(content_member, data_dict[key], key, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + elif (cmd.startswith("groups ") or cmd.startswith("group ") or cmd.startswith("cluster ")) and "groups" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + executed = False + count = 0 + content = config_get(CONFIG, "interface_menu", "groups_search_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "groups_search_member", "", lng_key) + data_dict = defaultdict(dict) + section = "cluster" + for (key, val) in DATA.items(section): + if value in val: + executed = True + data_dict[val] = key + for key in sorted(data_dict): + count += 1 + content = content + replace(content_member, data_dict[key], key, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + if not executed: + content = config_get(CONFIG, "interface_menu", "groups_search_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/members" command. + elif (cmd == "members" or cmd == "member" or cmd == "names" or cmd == "who") and "members" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "members_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "members_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + + # "/search" command. + elif (cmd.startswith("search ") or cmd.startswith("whois ") or cmd.startswith("w ")) and "search" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + executed = False + count = 0 + content = config_get(CONFIG, "interface_menu", "search_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "search_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + if fnmatch.fnmatch(key, value) or fnmatch.fnmatch(val, value): + executed = True + count += 1 + content = content + replace(content_member, key, val, section, lng_key).replace(delimiter+"activity_receive"+delimiter, statistic_value_get(key, "activity_receive")).replace(delimiter+"activity_send"+delimiter, statistic_value_get(key, "activity_send")) + content = content.replace(delimiter+"count"+delimiter, str(count)) + if not executed: + content = config_get(CONFIG, "interface_menu", "search_found_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/activitys" command. + elif (cmd == "activitys" or cmd == "activity") and "activitys" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", "activitys_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", "activitys_member", "", lng_key) + for section in sections: + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key).replace(delimiter+"activity_receive"+delimiter, statistic_value_get(key, "activity_receive")).replace(delimiter+"activity_send"+delimiter, statistic_value_get(key, "activity_send")) + content = content.replace(delimiter+"count"+delimiter, str(count)) + + + # "/statistic" command. + elif (cmd == "statistic" or cmd == "stat" or cmd == "stats" or cmd.startswith("statistic ") or cmd.startswith("stat ") or cmd.startswith("stats ")) and "statistic" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + except: + value = "day" + values = ["day", "last_day", "week", "last_week", "month", "last_month", "year", "last_year", "all", "max"] + if value in values: + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("cluster") and "statistic_cluster" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_cluster", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("cluster_received_direct") + statistic_recalculate("cluster_received_propagated") + statistic_recalculate("cluster_send_direct_success") + statistic_recalculate("cluster_send_propagated_success") + statistic_recalculate("cluster_send_direct_failed") + statistic_recalculate("cluster_send_propagated_failed") + statistic_recalculate("cluster_in_direct") + statistic_recalculate("cluster_in_propagated") + statistic_recalculate("cluster_out_direct_success") + statistic_recalculate("cluster_out_propagated_success") + statistic_recalculate("cluster_out_direct_failed") + statistic_recalculate("cluster_out_propagated_failed") + content = content + "#Received: " + statistic_value_get("cluster_received_direct", value+"_value", "0") + "d/" + statistic_value_get("cluster_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("cluster_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("cluster_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("cluster_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("cluster_send_propagated_failed", value+"_value", "0") + "p\n" + content = content + "#In: " + statistic_value_get("cluster_in_direct", value+"_value", "0") + "d/" + statistic_value_get("cluster_in_propagated", value+"_value", "0") + "p\n" + content = content + "#Out OK: " + statistic_value_get("cluster_out_direct_success", value+"_value", "0") + "d/" + statistic_value_get("cluster_out_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Out Failed: " + statistic_value_get("cluster_out_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("cluster_out_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("cluster_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("cluster_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("cluster_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("cluster_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("cluster_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("cluster_send_propagated_failed") + "\n\n" + content = content + "#In - Direct:\n" + statistic_get("cluster_in_direct") + "\n\n" + content = content + "#In - Propagated:\n" + statistic_get("cluster_in_propagated") + "\n\n" + content = content + "#Out - Direct - Success:\n" + statistic_get("cluster_out_direct_success") + "\n\n" + content = content + "#Out - Propagated - Success:\n" + statistic_get("cluster_out_propagated_success") + "\n\n" + content = content + "#Out - Direct - Failed:\n" + statistic_get("cluster_out_direct_failed") + "\n\n" + content = content + "#Out - Propagated - Failed:\n" + statistic_get("cluster_out_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("local") and "statistic_local" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_local", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("local_received_direct") + statistic_recalculate("local_received_propagated") + statistic_recalculate("local_send_direct_success") + statistic_recalculate("local_send_propagated_success") + statistic_recalculate("local_send_direct_failed") + statistic_recalculate("local_send_propagated_failed") + content = content + "#Received: " + statistic_value_get("local_received_direct", value+"_value", "0") + "d/" + statistic_value_get("local_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("local_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("local_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("local_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("local_send_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("local_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("local_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("local_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("local_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("local_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("local_send_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("interface") and "statistic_interface" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_interface", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights: + statistic_recalculate("interface_received_direct") + statistic_recalculate("interface_received_propagated") + statistic_recalculate("interface_send_direct_success") + statistic_recalculate("interface_send_propagated_success") + statistic_recalculate("interface_send_direct_failed") + statistic_recalculate("interface_send_propagated_failed") + content = content + "#Received: " + statistic_value_get("local_received_direct", value+"_value", "0") + "d/" + statistic_value_get("local_received_propagated", value+"_value", "0") + "p\n" + content = content + "#Send OK: " + statistic_value_get("interface_send_direct_success", value+"_value", "0") + "d/" + statistic_value_get("interface_send_propagated_success", value+"_value", "0") + "p\n" + content = content + "#Send Failed: " + statistic_value_get("interface_send_direct_failed", value+"_value", "0") + "d/" + statistic_value_get("interface_send_propagated_failed", value+"_value", "0") + "p\n\n" + if "statistic_full" in source_rights: + content = content + "#Received - Direct:\n" + statistic_get("interface_received_direct") + "\n\n" + content = content + "#Received - Propagated:\n" + statistic_get("interface_received_propagated") + "\n\n" + content = content + "#Send - Direct - Success:\n" + statistic_get("interface_send_direct_success") + "\n\n" + content = content + "#Send - Propagated - Success:\n" + statistic_get("interface_send_propagated_success") + "\n\n" + content = content + "#Send - Direct - Failed:\n" + statistic_get("interface_send_direct_failed") + "\n\n" + content = content + "#Send - Propagated - Failed:\n" + statistic_get("interface_send_propagated_failed") + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("user") and "statistic_self" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_self", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + if "statistic_min" in source_rights or "statistic_full" in source_rights: + content = content + statistic_get(source_hash) + "\n\n" + + if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("user") and "statistic_user" in source_rights and ("statistic_min" in source_rights or "statistic_full" in source_rights): + content = content + replace(config_get(CONFIG, "interface_menu", "statistic_header_user", "", lng_key), source_hash, source_name, source_right, lng_key).replace(delimiter+"value"+delimiter, value) + for section in STATISTIC.sections(): + if section != "main" and not section.startswith("cluster") and not section.startswith("local") and not section.startswith("interface"): + if "statistic_min" in source_rights: + statistic_recalculate(section) + content = "<" + section + ">: " + statistic_value_get(section, value+"_value") + "\n" + if "statistic_full" in source_rights: + content = "<" + section + ">:\n" + statistic_get(section) + "\n\n" + else: + content = config_get(CONFIG, "interface_menu", "statistic_found_error", "", lng_key) + + + # "/status" command. + elif cmd == "status" and "status" in source_rights: + content = config_get(CONFIG, "interface_menu", "status_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content.replace(delimiter+"enabled_local"+delimiter, DATA["main"]["enabled_local"]) + content = content.replace(delimiter+"enabled_cluster"+delimiter, DATA["main"]["enabled_cluster"]) + + + # "/delivery" command. + #elif cmd == "delivery" and "delivery" in source_rights: + #todo + + + # "/enable_local" command. + elif cmd == "enable_local" and "enable_local" in source_rights: + if DATA["main"].getboolean("enabled_local"): + content = config_get(CONFIG, "interface_menu", "enable_local_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "enable_local_false", "", lng_key) + + elif cmd.startswith("enable_local ") and "enable_local" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["enabled_local"] = "True" + content = config_get(CONFIG, "interface_menu", "enable_local_true", "", lng_key) + DATA["main"]["unsaved_local"] = "True" + else: + DATA["main"]["enabled_local"] = "False" + content = config_get(CONFIG, "interface_menu", "enable_local_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "enable_local_error", "", lng_key) + + + # "/enable_cluster" command. + elif cmd == "enable_cluster" and "enable_cluster" in source_rights: + if DATA["main"].getboolean("enabled_cluster"): + content = config_get(CONFIG, "interface_menu", "enable_cluster_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "enable_cluster_false", "", lng_key) + + elif cmd.startswith("enable_cluster ") and "enable_cluster" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["enabled_cluster"] = "True" + content = config_get(CONFIG, "interface_menu", "enable_cluster_true", "", lng_key) + DATA["main"]["unsaved_cluster"] = "True" + else: + DATA["main"]["enabled_cluster"] = "False" + content = config_get(CONFIG, "interface_menu", "enable_cluster_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "enable_cluster_error", "", lng_key) + + + # "/auto_add_user" command. + elif cmd == "auto_add_user" and "auto_add_user" in source_rights: + if DATA["main"].getboolean("auto_add_user"): + content = config_get(CONFIG, "interface_menu", "auto_add_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "auto_add_user_false", "", lng_key) + + elif cmd.startswith("auto_add_user ") and "auto_add_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["auto_add_user"] = "True" + content = config_get(CONFIG, "interface_menu", "auto_add_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["auto_add_user"] = "False" + content = config_get(CONFIG, "interface_menu", "auto_add_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_user_error", "", lng_key) + + + # "/auto_add_user_type" command. + elif cmd == "auto_add_user_type" and "auto_add_user_type" in source_rights: + content = config_get(DATA, "main", "auto_add_user_type", "", lng_key) + + elif cmd.startswith("auto_add_user_type ") and "auto_add_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["auto_add_user_type"] = value + content = config_get(CONFIG, "interface_menu", "auto_add_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_user_type_error", "", lng_key) + + + # "/auto_add_cluster" command. + elif cmd == "auto_add_cluster" and "auto_add_cluster" in source_rights: + if DATA["main"].getboolean("auto_add_cluster"): + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_false", "", lng_key) + + elif cmd.startswith("auto_add_cluster ") and "auto_add_cluster" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["auto_add_cluster"] = "True" + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["auto_add_cluster"] = "False" + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "auto_add_cluster_error", "", lng_key) + + + # "/invite_user" command. + elif cmd == "invite_user" and "invite_user" in source_rights: + if DATA["main"].getboolean("invite_user"): + content = config_get(CONFIG, "interface_menu", "invite_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "invite_user_false", "", lng_key) + + elif cmd.startswith("invite_user ") and "invite_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["invite_user"] = "True" + content = config_get(CONFIG, "interface_menu", "invite_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["invite_user"] = "False" + content = config_get(CONFIG, "interface_menu", "invite_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "invite_user_error", "", lng_key) + + + # "/invite_user_type" command. + elif cmd == "invite_user_type" and "invite_user_type" in source_rights: + content = config_get(DATA, "main", "invite_user_type", "", lng_key) + + elif cmd.startswith("invite_user_type ") and "invite_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["invite_user_type"] = value + content = config_get(CONFIG, "interface_menu", "invite_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "invite_user_type_error", "", lng_key) + + + # "/allow_user" command. + elif cmd == "allow_user" and "allow_user" in source_rights: + if DATA["main"].getboolean("allow_user"): + content = config_get(CONFIG, "interface_menu", "allow_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_user_false", "", lng_key) + + elif cmd.startswith("allow_user ") and "allow_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["allow_user"] = "True" + content = config_get(CONFIG, "interface_menu", "allow_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["allow_user"] = "False" + content = config_get(CONFIG, "interface_menu", "allow_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "allow_user_error", "", lng_key) + + + # "/allow_user_type" command. + elif cmd == "allow_user_type" and "allow_user_type" in source_rights: + content = config_get(DATA, "main", "allow_user_type", "", lng_key) + + elif cmd.startswith("allow_user_type ") and "allow_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["allow_user_type"] = value + content = config_get(CONFIG, "interface_menu", "allow_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "allow_user_type_error", "", lng_key) + + + # "/deny_user" command. + elif cmd == "deny_user" and "deny_user" in source_rights: + if DATA["main"].getboolean("deny_user"): + content = config_get(CONFIG, "interface_menu", "deny_user_true", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_user_false", "", lng_key) + + elif cmd.startswith("deny_user ") and "deny_user" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + if val_to_bool(value): + DATA["main"]["deny_user"] = "True" + content = config_get(CONFIG, "interface_menu", "deny_user_true", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["deny_user"] = "False" + content = config_get(CONFIG, "interface_menu", "deny_user_false", "", lng_key) + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "deny_user_error", "", lng_key) + + + # "/deny_user_type" command. + elif cmd == "deny_user_type" and "deny_user_type" in source_rights: + content = config_get(DATA, "main", "deny_user_type", "", lng_key) + + elif cmd.startswith("deny_user_type ") and "deny_user_type" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["deny_user_type"] = value + content = config_get(CONFIG, "interface_menu", "deny_user_type", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "deny_user_type_error", "", lng_key) + + + # "/description" command. + elif cmd == "description" and "description" in source_rights: + content = config_get(DATA, "main", "description", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif cmd.startswith("description ") and "description_set" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["description"+lng_key] = value + + content_group = config_get(CONFIG, "interface_messages", "description", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_description" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "description", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "description_error", "", lng_key) + + + # "/rules" command. + elif cmd == "rules" and "rules" in source_rights: + content = config_get(DATA, "main", "rules", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + elif cmd.startswith("rules ") and "rules_set" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + DATA["main"]["rules"+lng_key] = value + + content_group = config_get(CONFIG, "interface_messages", "rules", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + if content_group != "": + for section in sections: + if "receive_rules" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "rules", "", lng_key) + " " + value + DATA["main"]["unsaved"] = "True" + except: + content = config_get(CONFIG, "interface_menu", "rules_error", "", lng_key) + + + # "/readme" command. + elif cmd == "readme" and "readme" in source_rights: + content = config_get(CONFIG, "interface_menu", "readme", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/time" command. + elif cmd == "time" and "time" in source_rights: + content = config_get(CONFIG, "interface_menu", "time", "", lng_key) + content = time.strftime(content, time.localtime(time.time())) + content = replace(content, source_hash, source_name, source_right, lng_key) + + + # "/announce" command. + elif cmd == "announce" and "announce" in source_rights: + content = config_get(CONFIG, "interface_menu", "announce", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + LXMF_CONNECTION.announce_now() + if CONFIG["cluster"].getboolean("enabled"): + RNS_CONNECTION.announce_now() + + + # "/sync" command. + elif cmd == "sync" and "sync" in source_rights: + content = config_get(CONFIG, "interface_menu", "sync", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + LXMF_CONNECTION.sync_now() + + + # "/show run" command. + elif (cmd == "show run" or cmd == "sh run") and "show_run" in source_rights: + content = config_get(CONFIG, "interface_menu", "show_run_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for (key, val) in DATA.items("main"): + content = content + key + " = " + val + "\n" + + + # "/show" command. + elif (cmd.startswith("show") or cmd.startswith("list") or cmd.startswith("sh")) and "show" in source_rights: + try: + cmd, key = cmd.split(" ", 1) + if DATA.has_section(key) and key != "main": + content = config_get(CONFIG, "interface_menu", "show_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content = content + "[" + key + "]\n" + for (section_key, section_val) in DATA.items(key): + content = content + section_key + " = " + section_val + "\n" + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + " " + key + except: + content = config_get(CONFIG, "interface_menu", "show_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + for section in DATA.sections(): + if section in sections or section.replace("block_", "") in sections: + content = content + "[" + section + "]\n" + for (key, val) in DATA.items(section): + content = content + key + " = " + val + "\n" + content = content + "\n" + + + # "/user" command. + elif cmd.startswith("add ") and "add" in source_rights: + try: + cmd, key, value, name = cmd.split(" ", 3) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key, val) in DATA.items(section): + if key == value: + DATA.remove_option(section, key) + DATA[key][value] = name + content = config_get(CONFIG, "interface_menu", "user_add", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/user" command. + elif (cmd.startswith("del ") or cmd.startswith("rm ") or cmd.startswith("delete ")) and "del" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + if DATA.has_option(key, value): + DATA.remove_option(key, value) + content = config_get(CONFIG, "interface_menu", "user_del", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + + else: + content = config_get(CONFIG, "interface_menu", "user_error", "", lng_key) + " " + value + " -> " + key + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key, val) in DATA.items(section): + if key == value: + DATA.remove_option(section, key) + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + content = "OK: Removed user '" + value + "' from all types" + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/user" command. + elif (cmd.startswith("move ") or cmd.startswith("mv ")) and "move" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + for section in DATA.sections(): + if section != "main": + for (key_old, val_old) in DATA.items(section): + if key_old == value: + DATA.remove_option(section, key_old) + DATA[key][value] = val_old + content = config_get(CONFIG, "interface_menu", "user_move", "", lng_key) + " " + value + " -> " + key + DATA["main"]["unsaved"] = "True" + if content == "": + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "user_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/invite" command. + elif cmd.startswith("invite ") and "invite" in source_rights: + if DATA["main"].getboolean("invite_user"): + try: + cmd, value = cmd.split(" ", 1) + key = DATA["main"]["invite_user_type"] + if DATA.has_section(key) and key != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + DATA[key][value] = "" + + content_user = config_get(CONFIG, "interface_messages", "invite_"+key, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_invite", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + if content_group != "": + for section in sections: + if "receive_invite" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "invite_ok", "", lng_key) + " <" + value + ">" + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "invite_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "invite_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/kick" command. + elif cmd.startswith("kick ") and "kick" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_section = section + user_name = val + executed = True + DATA.remove_option(section, key) + if executed: + if CONFIG["statistic"].getboolean("enabled"): + statistic("del", value) + + content_user = config_get(CONFIG, "interface_messages", "kick_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_kick", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_kick" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "kick_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "kick_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "kick_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/block" command. + elif (cmd.startswith("block ") or cmd.startswith("ban ")) and "block" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_section = section + user_name = val + executed = True + if not DATA.has_section("block_"+section): + DATA.add_section("block_"+section) + DATA["block_"+section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "block_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_block", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "block_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "block_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "block_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/unblock" command. + elif (cmd.startswith("unblock ") or cmd.startswith("unban ")) and "unblock" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in DATA.sections(): + if section.startswith("block"): + for (key, val) in DATA.items(section): + if key == value: + user_section = section.replace("block_", "") + user_name = val + executed = True + if not DATA.has_section(user_section): + DATA.add_section(user_section) + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "unblock_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_unblock", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "unblock_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "unblock_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "unblock_format_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/allow" command. + elif cmd.startswith("allow ") and "allow" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + user_section = DATA["main"]["allow_user_type"] + if DATA.has_section(user_section) and user_section != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + section = "wait" + if DATA.has_section(section): + for (key, val) in DATA.items(section): + if key == value: + user_name = val + executed = True + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "allow_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_allow", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "allow_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "allow_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "allow_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/deny" command. + elif cmd.startswith("deny ") and "deny" in source_rights: + try: + cmd, value = cmd.split(" ", 1) + user_section = DATA["main"]["deny_user_type"] + if DATA.has_section(user_section) and user_section != "main": + value = LXMF_CONNECTION.destination_correct(value) + if value != "": + executed = False + for section in sections: + for (key, val) in DATA.items(section): + if key == value: + user_name = val + executed = True + DATA[user_section][key] = val + DATA.remove_option(section, key) + if executed: + content_user = config_get(CONFIG, "interface_messages", "deny_"+user_section, "", lng_key) + content_user = replace(content_user, source_hash, source_name, source_right, lng_key) + if content_user != "": + LXMF_CONNECTION.send(value, content_user, "", None, None, "interface_send") + + content_group = config_get(CONFIG, "interface_messages", "member_deny", "", lng_key) + content_group = replace(content_group, source_hash, source_name, source_right, lng_key) + content_group = content_group.replace(delimiter+"user_address"+delimiter, value) + content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) + if content_group != "": + for section in sections: + if "receive_block" in config_get(CONFIG, "rights", section).split(","): + for (key, val) in DATA.items(section): + LXMF_CONNECTION.send(key, content_group, "", None, None, "interface_send") + + content = config_get(CONFIG, "interface_menu", "deny_ok", "", lng_key) + content = content.replace(delimiter+"user_address"+delimiter, value) + content = content.replace(delimiter+"user_name"+delimiter, user_name) + + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + else: + DATA["main"]["unsaved"] = "True" + else: + content = config_get(CONFIG, "interface_menu", "deny_found_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_format_error", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "deny_type_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + # "/load" command. + elif (cmd == "load" or cmd == "read") and "load" in source_rights: + if data_read(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "load_ok", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "load_error", "", lng_key) + + + # "/save" command. + elif (cmd == "save" or cmd == "wr") and "save" in source_rights: + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "save_ok", "", lng_key) + else: + content = config_get(CONFIG, "interface_menu", "save_error", "", lng_key) + DATA["main"]["unsaved"] = "True" + + if CONFIG["statistic"].getboolean("enabled"): + statistic_save(PATH + "/statistic.cfg") + + + # "/reload" command. + elif cmd == "reload" and "reload" in source_rights: + content = config_get(CONFIG, "interface_menu", "reload_error", "", lng_key) + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + if data_read(PATH + "/data.cfg"): + content = config_get(CONFIG, "interface_menu", "reload_ok", "", lng_key) + else: + DATA["main"]["unsaved"] = "True" + + + # "/reset" command. + elif cmd.startswith("reset statistic ") and "reset" in source_rights: + try: + cmd, key, value = cmd.split(" ", 2) + + if value == "all": + for section in STATISTIC.sections(): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "cluster": + for section in STATISTIC.sections(): + if section.startswith("cluster"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "local": + for section in STATISTIC.sections(): + if section.startswith("local"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "interface": + for section in STATISTIC.sections(): + if section.startswith("interface"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + elif value == "user": + for section in STATISTIC.sections(): + if not section.startswith("cluster") and not section.startswith("local") and not section.startswith("interface"): + statistic_reset(section) + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + content = config_get(CONFIG, "interface_menu", "reset_statistic_ok", "", lng_key) + + else: + content = config_get(CONFIG, "interface_menu", "reset_statistic_error", "", lng_key) + except: + content = config_get(CONFIG, "interface_menu", "cmd_error", "", lng_key) + + + else: + # "/admins" command. + # "/moderators" command. + # "/users" command. + # "/guests" command. + executed = False + for section in sections: + if (cmd == section or cmd == section+"s") and section+"s" in source_rights: + count = 0 + content = config_get(CONFIG, "interface_menu", section+"s_header", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + content_member = config_get(CONFIG, "interface_menu", section+"s_member", "", lng_key) + for (key, val) in DATA.items(section): + count += 1 + content = content + replace(content_member, key, val, section, lng_key) + content = content.replace(delimiter+"count"+delimiter, str(count)) + executed = True + break + + # cmd_unknown + if not executed: + content = config_get(CONFIG, "interface_menu", "cmd_unknown", "", lng_key) + + + # unsaved + if DATA["main"].getboolean("unsaved") and "unsaved" in source_rights: + if CONFIG["main"].getboolean("auto_save_data"): + DATA.remove_option("main", "unsaved") + if data_save(PATH + "/data.cfg"): + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_ok", "", lng_key) + else: + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_error", "", lng_key) + DATA["main"]["unsaved"] = "True" + else: + content = content + "\n" + config_get(CONFIG, "interface_menu", "save_info", "", lng_key) + + + return content + + + + +#### Replace ##### +def replace(text, source_hash, source_name, source_right, lng_key): + delimiter = CONFIG["interface"]["delimiter_output"] + + text = text.replace(delimiter+"source_address"+delimiter, source_hash) + text = text.replace(delimiter+"source_name"+delimiter, source_name) + text = text.replace(delimiter+"source_right"+delimiter, source_right) + + text = text.replace(delimiter+"name"+delimiter, config_get(CONFIG, "main", "name", "", lng_key)) + text = text.replace(delimiter+"display_name"+delimiter, config_get(CONFIG, "lxmf", "display_name", "", lng_key)) + text = text.replace(delimiter+"description"+delimiter, config_get(DATA, "main", "description", "", lng_key)) + text = text.replace(delimiter+"rules"+delimiter, config_get(DATA, "main", "rules", "", lng_key)) + text = text.replace(delimiter+"destination_address"+delimiter, LXMF_CONNECTION.destination_hash_str()) + text = text.replace(delimiter+"propagation_node"+delimiter, config_get(CONFIG, "lxmf", "propagation_node", "", lng_key)) + text = text.replace(delimiter+"cluster_name"+delimiter, config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1]) + + text = text.replace(delimiter+"n"+delimiter, "\n") + + if delimiter+"count_members"+delimiter in text: + count = 0 + for (section) in CONFIG.items("rights"): + if DATA.has_section(section): + for (key, val) in DATA.items(section): + count += 1 + text = text.replace(delimiter+"count_members"+delimiter, str(count)) + + if delimiter+"count_pin"+delimiter in text: + count = 0 + if DATA.has_section("pin"): + for (key, val) in DATA.items("pin"): + count += 1 + text = text.replace(delimiter+"count_pin"+delimiter, str(count)) + + return text + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Data + + +#### Data - Read ##### +def data_read(file=None): + global DATA + + if file is None: + return False + else: + DATA = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + DATA.sections() + if os.path.isfile(file): + try: + DATA.read(file) + except Exception as e: + return False + else: + if not data_default(file=file): + return False + return True + + + + +#### Data - Save ##### +def data_save(file=None): + global DATA + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + DATA.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Data - Save ##### +def data_save_periodic(initial=False): + data_timer = threading.Timer(CONFIG.getint("main", "periodic_save_data_interval")*60, data_save_periodic) + data_timer.daemon = True + data_timer.start() + + if initial: + return + + global DATA + if DATA.has_section("main"): + if DATA["main"].getboolean("unsaved"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + + + + +#### Data - Default ##### +def data_default(file=None): + global DATA + + if file is None: + return False + elif DEFAULT_DATA != "": + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + data_file = open(file, "w") + data_file.write(DEFAULT_DATA) + data_file.close() + if not data_read(file=file): + return False + except: + return False + else: + return False + return True + + +############################################################################################################## +# Statistic/Counter + + +#### Statistic ##### +def statistic(cmd="add", section="global", key="", value=1): + global STATISTIC + + changed = False + + if cmd == "add": + statistic_add(section, value) + changed = True + elif cmd == "del": + statistic_del(section) + changed = True + elif cmd == "reset": + statistic_reset(section) + changed = True + elif cmd == "get": + return statistic_get(section) + elif cmd == "value_set": + statistic_value_set(section, key, value) + changed = True + elif cmd == "value_get": + return statistic_value_get(section, key) + elif cmd == "read": + return statistic_read(PATH + "/statistic.cfg") + elif cmd == "save": + return statistic_save(PATH + "/statistic.cfg") + + if changed: + if CONFIG["main"].getboolean("auto_save_statistic"): + statistic_save(PATH + "/statistic.cfg") + else: + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + + + + +#### Statistic - Add ##### +def statistic_add(section="global", value=1): + global STATISTIC + + if not STATISTIC.has_section(section): + statistic_default(section) + + statistic_recalculate(section) + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + #day + if STATISTIC[section]["day_index"] == str(day): + STATISTIC[section]["day_value"] = str(STATISTIC.getint(section, "day_value")+value) + + #week + if STATISTIC[section]["week_index"] == str(week): + STATISTIC[section]["week_value"] = str(STATISTIC.getint(section, "week_value")+value) + + #month + if STATISTIC[section]["month_index"] == str(month): + STATISTIC[section]["month_value"] = str(STATISTIC.getint(section, "month_value")+value) + + #year + if STATISTIC[section]["year_index"] == str(year): + STATISTIC[section]["year_value"] = str(STATISTIC.getint(section, "year_value")+value) + + #all + STATISTIC[section]["all_value"] = str(STATISTIC.getint(section, "all_value")+value) + + #max + if STATISTIC.getint(section, "day_value") > STATISTIC.getint(section, "max_value"): + STATISTIC[section]["max_value"] = STATISTIC[section]["day_value"] + STATISTIC[section]["max_index"] = time.strftime("%Y-%m-%d", time.localtime(time.time())) + return + + + + +#### Statistic - Recalculate ##### +def statistic_recalculate(section="global"): + global STATISTIC + + if not STATISTIC.has_section(section): + return + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + #day + if STATISTIC[section]["day_index"] != str(day): + if STATISTIC[section]["day_index"] == str(day-1): + STATISTIC[section]["last_day_value"] = STATISTIC[section]["day_value"] + STATISTIC[section]["last_day_index"] = str(day-1) + else: + STATISTIC[section]["last_day_value"] = "0" + STATISTIC[section]["last_day_index"] = str(day-1) + STATISTIC[section]["day_value"] = "0" + STATISTIC[section]["day_index"] = str(day) + + #week + if STATISTIC[section]["week_index"] != str(week): + if STATISTIC[section]["week_index"] == str(week-1): + STATISTIC[section]["last_week_value"] = STATISTIC[section]["week_value"] + STATISTIC[section]["last_week_index"] = str(week-1) + else: + STATISTIC[section]["last_week_value"] = "0" + STATISTIC[section]["last_week_index"] = str(week-1) + STATISTIC[section]["week_value"] = "0" + STATISTIC[section]["week_index"] = str(week) + + #month + if STATISTIC[section]["month_index"] != str(month): + if STATISTIC[section]["month_index"] == str(month-1): + STATISTIC[section]["last_month_value"] = STATISTIC[section]["month_value"] + STATISTIC[section]["last_month_index"] = str(month-1) + else: + STATISTIC[section]["last_month_value"] = "0" + STATISTIC[section]["last_month_index"] = str(month-1) + STATISTIC[section]["month_value"] = "0" + STATISTIC[section]["month_index"] = str(month) + + #year + if STATISTIC[section]["year_index"] != str(year): + if STATISTIC[section]["year_index"] == str(year-1): + STATISTIC[section]["last_year_value"] = STATISTIC[section]["year_value"] + STATISTIC[section]["last_year_index"] = str(year-1) + else: + STATISTIC[section]["last_year_value"] = "0" + STATISTIC[section]["last_year_index"] = str(year-1) + STATISTIC[section]["year_value"] = "0" + STATISTIC[section]["year_index"] = str(year) + + return + + + + +#### Statistic - Del ##### +def statistic_del(section="global"): + global STATISTIC + + if STATISTIC.has_section(section): + STATISTIC.remove_section(section) + + + + +#### Statistic - Reset ##### +def statistic_reset(section="global"): + statistic_del(section) + statistic_add(section, 0) + + + + +#### Statistic - Get ##### +def statistic_get(section="global"): + global STATISTIC + + text = "" + if STATISTIC.has_section(section): + statistic_recalculate(section) + for (key, val) in STATISTIC.items(section): + if key.endswith("_value"): + text = text + key.capitalize() + ": " + val + "\n" + text = text.replace("_value", "") + text = text.replace("_", " ") + text = text.strip() + + return text + + + + +#### Statistic - Value set ##### +def statistic_value_set(section, key, value): + global STATISTIC + + if not STATISTIC.has_section(section): + statistic_default(section) + + STATISTIC[section][key] = value + + + + +#### Statistic - Value get ##### +def statistic_value_get(section, key, default=""): + global STATISTIC + + if STATISTIC.has_section(section): + if STATISTIC.has_option(section, key): + return STATISTIC[section][key] + return default + + + + +#### Statistic - Read ##### +def statistic_read(file=None): + global STATISTIC + + if file is None: + return False + else: + STATISTIC = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + STATISTIC.sections() + if os.path.isfile(file): + try: + STATISTIC.read(file) + except Exception as e: + return False + return True + + + + +#### Statistic - Save ##### +def statistic_save(file=None): + global STATISTIC + + if file is None: + return False + else: + try: + with open(file,"w") as file: + if STATISTIC.has_section("main"): + STATISTIC.remove_section("main") + STATISTIC.write(file) + except Exception as e: + return False + return True + + + + +#### Statistic - Save ##### +def statistic_save_periodic(initial=False): + statistic_timer = threading.Timer(CONFIG.getint("main", "periodic_save_statistic_interval")*60, statistic_save_periodic) + statistic_timer.daemon = True + statistic_timer.start() + + if initial: + return + + global STATISTIC + if STATISTIC.has_section("main"): + if STATISTIC["main"].getboolean("unsaved"): + STATISTIC.remove_section("main") + if not statistic_save(PATH + "/statistic.cfg"): + if not STATISTIC.has_section("main"): + STATISTIC.add_section("main") + STATISTIC["main"]["unsaved"] = "True" + + + + +#### Statistic - Default ##### +def statistic_default(section="global"): + global STATISTIC + + date = datetime.date.today() + day = date.timetuple().tm_yday + month = date.timetuple().tm_mon + year = date.timetuple().tm_year + week = date.isocalendar().week + + STATISTIC.add_section(section) + STATISTIC[section]["day_value"] = "0" + STATISTIC[section]["day_index"] = str(day) + STATISTIC[section]["last_day_value"] = "0" + STATISTIC[section]["last_day_index"] = str(day-1) + STATISTIC[section]["week_value"] = "0" + STATISTIC[section]["week_index"] = str(week) + STATISTIC[section]["last_week_value"] = "0" + STATISTIC[section]["last_week_index"] = str(week-1) + STATISTIC[section]["month_value"] = "0" + STATISTIC[section]["month_index"] = str(month) + STATISTIC[section]["last_month_value"] = "0" + STATISTIC[section]["last_month_index"] = str(month-1) + STATISTIC[section]["year_value"] = "0" + STATISTIC[section]["year_index"] = str(year) + STATISTIC[section]["last_year_value"] = "0" + STATISTIC[section]["last_year_index"] = str(year-1) + STATISTIC[section]["all_value"] = "0" + STATISTIC[section]["max_value"] = "0" + STATISTIC[section]["max_index"] = time.strftime("%Y-%m-%d", time.localtime(time.time())) + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_MAIN_CONNECTION + global LXMF_CONNECTION + global RNS_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if not data_read(PATH + "/data.cfg"): + print("Data - Error reading data file " + PATH + "/data.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + if CONFIG["statistic"].getboolean("enabled"): + statistic_read(PATH + "/statistic.cfg") + + RNS_MAIN_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Data File: " + PATH + "/data.cfg", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + identity_file="identity", + identity=None, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + if CONFIG["statistic"].getboolean("enabled"): + LXMF_CONNECTION.register_message_notification_success_callback(lxmf_message_notification_success_callback) + LXMF_CONNECTION.register_message_notification_failed_callback(lxmf_message_notification_failed_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + if CONFIG["cluster"].getboolean("enabled") or CONFIG["high_availability"].getboolean("enabled"): + announce_data = defaultdict(dict) + + if CONFIG["high_availability"].getboolean("enabled"): + announce_data["ha"] = "1" + announce_data["ha_r"] = CONFIG["high_availability"]["role"] + else: + announce_data["ha"] = "0" + + if CONFIG["cluster"].getboolean("enabled"): + announce_data["c"] = "1" + announce_data["c_h"] = LXMF_CONNECTION.destination_hash_str() + announce_data["c_d"] = CONFIG["cluster"]["display_name"].replace(" ", "") + else: + announce_data["c"] = "0" + + + log("RNS - Connecting ...", LOG_DEBUG) + RNS_CONNECTION = rns_connection( + storage_path=path, + identity_file="identity", + identity=LXMF_CONNECTION.identity, + destination_name=CONFIG["cluster"]["name"], + destination_type=CONFIG["cluster"]["type"], + announce_startup=CONFIG["rns"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["rns"]["announce_startup_delay"], + announce_periodic=CONFIG["rns"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["rns"]["announce_periodic_interval"], + announce_data = json.dumps(announce_data) + ) + RNS_CONNECTION.register_announce_callback(rns_announce_callback) + log("RNS - Connected", LOG_DEBUG) + + if CONFIG["main"].getboolean("periodic_save_data"): + data_save_periodic(True) + + if CONFIG["main"].getboolean("periodic_save_statistic"): + statistic_save_periodic(True) + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampledata", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + if params.exampledata: + print("Data File: " + PATH + "/data.cfg") + print("Content:") + print(DEFAULT_DATA) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. + + +#### Main program settings #### +[main] + +# Default language. +lng = en # en/de + + +#### LXMF connection settings #### +[lxmf] + +# The name will be visible to other peers +# on the network, and included in announces. +# It is also used in the group description/info. +display_name = Distribution Group + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes + + +#### Cluster settings #### +[cluster] + +# Enable/Disable this functionality. +enabled = True + +# To use several completely separate clusters/groups, +# an individual name and type can be assigned here. +name = grp +type = cluster + +# Slash-separated list with the names of this cluster. +# No spaces are allowed in the name. +# All send messages that match the name will be received. +# The last name is the main name of this group +# and is used as source for send messages. +display_name = County/Region/City + + +#### High availability settings #### +[high_availability] + +# Enable/Disable this functionality. +enabled = False + +# Role of this node (master/slave) +role = master + +# Peer address +peer = + + +#### Statistic/Counter settings #### +[statistic] + +# Enable/Disable this functionality. +enabled = True +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +# Enable/Disable this functionality. +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Distribution Group + +# Default language. +# The following languages are available. Other languages can be defined below in the "interface" settings. +# You have to add the language key to the settings to be used. For example "-de". +# en/de +lng = en + +# Auto save changes. +# If there are changes in the data or statistics, they can be saved directly in the files. +# Attention: This can lead to very high write cycles. +# If you want to prevent frequent writing, please set this to 'False' and use the peridodic save function. +auto_save_data = True +auto_save_statistic = False + +# Periodic actions - Save changes periodically. +periodic_save_data = True +periodic_save_data_interval = 30 #Minutes +periodic_save_statistic = True +periodic_save_statistic_interval = 30 #Minutes + +# Auto apply name from announces. +# As an alternative to defining the nickname manually, it can be used automatically from the announce. +# This works only after the user has joined the group and then an announce is received. +auto_name_def = True +auto_name_change = False + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +# It is also used in the group description/info. +display_name = Distribution Group + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = No +signature_validated_new = No +signature_validated_known = No + + + + +#### RNS connection settings #### +[rns] + +# Destination name & type need to fits the RNS protocoll +# to be compatibel with other RNS programs. +destination_name = grp +destination_type = cluster + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + + + + +#### Cluster settings #### +[cluster] + +# Enable/Disable this functionality. +enabled = True + +# To use several completely separate clusters/groups, +# an individual name and type can be assigned here. +name = grp +type = cluster + +# Slash-separated list with the names of this cluster. +# No spaces are allowed in the name. +# All send messages that match the name will be received. +# The last name is the main name of this group +# and is used as source for send messages. +display_name = County/Region/City + +# Define the delimiters for cluster input. +delimiter_input = @ + + + + +#### High availability settings #### +[high_availability] + +# Enable/Disable this functionality. +enabled = False + +# Role of this node (master/slave) +role = master + +# Peer address +peer = + +# Sync +sync_periodic_interval = 30 #Minutes + +# Sync at startup +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Heartbeat +heartbeat_interval = 1 #Minutes +heartbeat_timeout = 15 #Minutes + + + + +#### Message settings #### +[message] + +## Each message received (message and command) ## + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +## Each message send (message) ## + +# Text is added. +send_prefix = !source_name!!n!!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +## Each cluster message received (message and command) ## + +# Text is added. +cluster_receive_prefix = @!cluster_source!-> +cluster_receive_suffix = + +# Text is replaced. +cluster_receive_search = +cluster_receive_replace = + +# Text is replaced by regular expression. +cluster_receive_regex_search = +cluster_receive_regex_replace = + +# Length limitation. +cluster_receive_length_min = 0 #0=any length +cluster_receive_length_max = 0 #0=any length + + +## Each cluster message send (message) ## + +# Text is added. +cluster_send_prefix = @!cluster_destination!!n!!source_name!!n!!n! +cluster_send_suffix = + +# Text is replaced. +cluster_send_search = +cluster_send_replace = + +# Text is replaced by regular expression. +cluster_send_regex_search = +cluster_send_regex_replace = + +# Length limitation. +cluster_send_length_min = 0 #0=any length +cluster_send_length_max = 0 #0=any length + + +## Each pinned message ## + +pin_id = %%y%%m%%d-%%H%%M%%S + + +# Define which message timestamp should be used. +timestamp = client #client/server + + + + +#### Statistic/Counter settings #### +[statistic] + +# Enable/Disable this functionality. +enabled = True + +# Create cluster statistics. +cluster = True + +# Create local/group statistics. +local = True + +# Create interface statistics. +interface = True + +# Create user statistics. +user = True + + + + +#### User rights assignment #### + +# Define the individual rights for the different user types. +# Delimiter for different rights: , +[rights] + +admin = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_cluster_join,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,receive_pin_remove,receive_name_def,receive_name_change,receive_auto_name_def,receive_auto_name_change,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_full,statistic_cluster,statistic_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,invite_user,invite_user_type,allow_user,allow_user_type,deny_user,deny_user_type,description_set,rules_set,announce,sync,show_run,show,add,del,move,invite,kick,block,unblock,allow,deny,load,save,reload,reset,unsaved +mod = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_local,statistic_self,delivery,show,add,del,move,invite,kick,block,unblock,allow,deny +user = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_local,statistic_self,delivery,invite +guest = interface,receive_local,receive_cluster,receive_cluster_loop,leave +wait = + + +# The following rights can be assigned: +# interface = General function of the command interface. +# receive_local = Receive local (own group) messages. +# receive_cluster = Receive cluster (foreign group) messages. +# receive_cluster_pin_add = Receive cluster (foreign group) pinned messages. +# receive_cluster_send = Receive a copy of the message sent to another cluster. +# receive_cluster_loop = Receive message which is sent to a higher hierarchy in the cluster and includes the own cluster. +# receive_cluster_join = Receive an info message when a new cluster joins. +# receive_join = Receive an info message when a new user joins. +# receive_leave = Receive an info message when a user leaves. +# receive_invite = Receive an info message when a user is invited. +# receive_kick = Receive an info message when a user is kicked. +# receive_block = Receive an info message when a user is blocked. +# receive_unblock = Receive an info message when a user is unblocked. +# receive_allow = Receive an info message when a user has been allowed. +# receive_deny = Receive an info message when a user has been denied. +# receive_description = Receive an info message when the group description is changed. +# receive_pin_add = Receive an info message when a message is pinned. +# receive_pin_remove = Receive an info message when a pinned message is removed. +# receive_rules = Receive an info message when the group rules are changed. +# receive_name_def = Receive an info message when a user assigns a name. +# receive_name_change = Receive an info message when a user changes his name. +# receive_auto_name_def = Receive an info message when a user assigns a name. +# receive_auto_name_change = Receive an info message when a user changes his name. +# reply_signature = Receive an error message if the signature is invalid. +# reply_cluster_enabled = Receive an error message when the cluster is disabled. +# reply_cluster_right = Receive an error message when you do not have permission to send in the cluster. +# reply_interface_enabled = Receive an error message when the interface is disabled. +# reply_interface_right = Receive an error message if you do not have permission to use the interface. +# reply_local_enabled = Receive an error message when sending a local message is disabled. +# reply_local_right = Receive an error message when you do not have permission to send locally. +# reply_block = Receive an error message when you are blocked. +# reply_length_min = Receive an error message if the message length is too short. +# reply_length_max = Receive an error message if the message length is too long. +# send_local = Allows you to send loacally in your own group. +# send_cluster = Allows sending to another cluster/group. +# help = Use of the "/help" command allowed. +# leave = Use of the "/leave" command allowed. +# name = Use of the "/name" command allowed. +# address = Use of the "/address" command allowed. +# info = Use of the "/info" command allowed. +# pin = Use of the "/pin" command allowed. +# pin_add = Use of the "/pin" command allowed. +# pin_remove = Use of the "/pin" command allowed. +# cluster_pin_add = Use of the "/pin" command allowed. +# version = Use of the "/version" command allowed. +# groups = Use of the "/groups" command allowed. +# members = Use of the "/members" command allowed. +# admins = Use of the "/admins" command allowed. +# moderators = Use of the "/moderators" command allowed. +# users = Use of the "/users" command allowed. +# guests = Use of the "/guests" command allowed. +# search = Use of the "/search" command allowed. +# activitys = Use of the "/activitys" command allowed. +# statistic = Use of the "/statistic" command allowed. +# statistic_min = Minimal statistics output. +# statistic_full = Full/Maximal statistics output. +# statistic_cluster = Displays the cluster statistics on the statistics text. +# statistic_local = Displays the local statistics on the statistics text. +# statistic_interface = Displays the interface statistics on the statistics text. +# statistic_self = Displays the own statistics on the statistics text. +# statistic_user = Displays the user statistics on the statistics text. +# status = Use of the "/status" command allowed. +# delivery = Use of the "/delivery" command allowed. +# enable_local = Use of the "/enable_local" command allowed. +# enable_cluster = Use of the "/enable_cluster" command allowed. +# auto_add_user = Use of the "/auto_add_user" command allowed. +# auto_add_user_type = Use of the "/auto_add_user_type" command allowed. +# auto_add_cluster = Use of the "/auto_add_cluster" command allowed. +# invite_user = Use of the "/invite_user" command allowed. +# invite_user_type = Use of the "/invite_user_type" command allowed. +# allow_user = Use of the "/allow_user" command allowed. +# allow_user_type = Use of the "/allow_user_type" command allowed. +# deny_user = Use of the "/deny_user" command allowed. +# deny_user_type = Use of the "/deny_user_type" command allowed. +# description = Use of the "/description" command allowed. +# description_set = Use of the "/description" command allowed. +# rules = Use of the "/rules" command allowed. +# rules_set = Use of the "/rules" command allowed. +# readme = Use of the "/readme" command allowed. +# time = Use of the "/time" command allowed. +# announce = Use of the "/announce" command allowed. +# sync = Use of the "/sync" command allowed. +# show_run = Use of the "/show_run" command allowed. +# show = Use of the "/show" command allowed. +# add = Use of the "/add" command allowed. +# del = Use of the "/del" command allowed. +# move = Use of the "/move" command allowed. +# invite = Use of the "/invite" command allowed. +# kick = Use of the "/kick" command allowed. +# block = Use of the "/block" command allowed. +# unblock = Use of the "/unblock" command allowed. +# allow = Use of the "/allow" command allowed. +# deny = Use of the "/deny" command allowed. +# load = Use of the "/load" command allowed. +# save = Use of the "/save" command allowed. +# reload = Use of the "/reload" command allowed. +# reset = Use of the "/reset" command allowed. +# unsaved = Displays the status of the data file when using any action/command. + + + + +#### Interface settings - General #### +[interface] + +# Enable/Disable the whole interface/commands. +enabled = True + +# Define the delimiters for command input/output. +delimiter_input = / +delimiter_output = ! + + + + +#### Interface settings - Messages #### + +# Define messages for user or automatic actions. +# These messages are sent automatically when a corresponding action is triggered. +# If a message is to be deactivated simply comment it out. +[interface_messages] + +# Auto user add. (Single message to the user.) +auto_error = ERROR: Joining the group is not possible. +auto_error-de = FEHLER: Beitritt zur Gruppe ist nicht möglich. + +auto_add_admin = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_admin-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_mod = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_mod-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_user = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!Please assign a nickname with the command /name +auto_add_user-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +auto_add_guest = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them.!n!!n!To leave the group use the following command: /leave +auto_add_guest-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +auto_add_wait = Welcome to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +auto_add_wait-de = Willkommen in der Gruppe "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +# Manual/Admin user add. (Single message to the user.) +add_admin = +add_admin-de = +add_mod = +add_mod-de = +add_user = +add_user-de = +add_guest = +add_guest-de = +add_wait = +add_wait-de = + +# Invite user. (Single message to the user.) +invite_admin = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_admin-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_mod = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_mod-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_user = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +invite_user-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +invite_guest = You have been invited by !source_name! to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!To leave the group use the following command: /leave +invite_guest-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +invite_wait = You have been invited by !source_name! to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +invite_wait-de = Sie wurden von !source_name! in die Gruppe "!display_name!" eingeladen!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +# Kick user. (Single message to the user.) +kick_admin = You have been kicked out of the group! +kick_admin-de = Sie wurden aus der Gruppe geworfen! +kick_mod = You have been kicked out of the group! +kick_mod-de = Sie wurden aus der Gruppe geworfen! +kick_user = You have been kicked out of the group! +kick_user-de = Sie wurden aus der Gruppe geworfen! +kick_guest = You have been kicked out of the group! +kick_guest-de = Sie wurden aus der Gruppe geworfen! +kick_wait = You have been kicked out of the group! +kick_wait-de = Sie wurden aus der Gruppe geworfen! + +# Block user. (Single message to the user.) +block_admin = +block_admin-de = +block_mod = +block_mod-de = +block_user = +block_user-de = +block_guest = +block_guest-de = +block_wait = +block_wait-de = + +# Unblock user. (Single message to the user.) +unblock_admin = +unblock_admin-de = +unblock_mod = +unblock_mod-de = +unblock_user = +unblock_user-de = +unblock_guest = +unblock_guest-de = +unblock_wait = +unblock_wait-de = + +# Allow user. (Single message to the user.) +allow_admin = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_admin-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_mod = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_mod-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_user = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members.!n!!n!For help enter /?!n!!n!To read the group rules use the command /rules!n!!n!Please assign a nickname with the command /name +allow_user-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt.!n!!n!Für Hilfe geben Sie /? ein.!n!!n!Um die Gruppenregeln zu lesen verwenden Sie den Befehl /rules!n!!n!Bitte vergeben Sie einen Nickname mit dem Befehl /name +allow_guest = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages.!n!!n!To leave the group use the following command: /leave +allow_guest-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen.!n!!n!Um die Gruppe zu verlassen verwenden Sie folgenden Befehl: /leave +allow_wait = +allow_wait-de = + +# Deny user. (Single message to the user.) +deny_admin = You have been denied to join the group "!display_name!"! +deny_admin-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_mod = You have been denied to join the group "!display_name!"! +deny_mod-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_user = You have been denied to join the group "!display_name!"! +deny_user-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_guest = You have been denied to join the group "!display_name!"! +deny_guest-de = Ihnen wurde der Beitritt in die Gruppe "!display_name!" abgelehnt! +deny_wait = +deny_wait-de = + +# General user/member messages. (Group message to all group members.) +member_join = !source_name! joins the group. +member_join-de = !source_name! tritt der Gruppe bei. +member_leave = !source_name! leave the group. +member_leave-de = !source_name! verlässt die Gruppe. +member_invite = was invited to the group by !source_name! +member_invite-de = wurde in die Gruppe eingeladen von !source_name! +member_kick = !user_name! was kicked out of the group by !source_name! +member_kick-de = !user_name! wurde aus der Gruppe geworfen von !source_name! +member_block = !user_name! was blocked by !source_name! +member_block-de = !user_name! wurde geblockt von !source_name! +member_unblock = !user_name! was unblocked by !source_name! +member_unblock-de = !user_name! wurde entsperrt von !source_name! +member_allow = !user_name! was allowed by !source_name! +member_allow-de = !user_name! wurde erlaubt von !source_name! +member_deny = !user_name! was denied by !source_name! +member_deny-de = !user_name! wurde abgelehnt von !source_name! +member_name_def = defined the name: +member_name_def-de = hat den Namen definiert: +member_name_change = changed the name: +member_name_change-de = hat den Namen geändert: +description = !source_name! has changed the group description:!n!!n!!description! +description-de = !source_name! hat die Gruppenbeschreibung geändert:!n!!n!!description! +rules = !source_name! has changed the group rules:!n!!n!!rules! +rules-de = !source_name! hat die Gruppenregeln geändert:!n!!n!!rules! +cluster_join = New cluster/group connected: !source_name! +cluster_join-de = Neue Cluster/Gruppe verbunden: !source_name! +pin_add = New pinned message:!n!#!key!!n!!value! +pin_add-de = Neue angeheftete Nachricht:!n!#!key!!n!!value! +pin_remove = Removed pinned message:!n!#!key!!n!!value! +pin_remove-de = Angeheftete Nachricht entfernt:!n!#!key!!n!!value! +cluster_pin_add = New pinned message:!n!#!key!!n!!value! +cluster_pin_add-de = Neue angeheftete Nachricht:!n!#!key!!n!!value! + +# Reply messages. (Single message to the user.) +reply_signature = Info: Signature invalid! +reply_signature-de = Info: Signatur ungültig! +reply_cluster_enabled = Info: Cluster disabled! +reply_cluster_enabled-de = Info: Cluster deaktiviert! +reply_cluster_right = Info: No authorization for cluster messages! +reply_cluster_right-de = Keine Berechtigung für Clusternachrichten! +reply_interface_enabled = Info: Commands disabled! +reply_interface_enabled-de = Info: Befehle deaktiviert! +reply_interface_right = Info: No authorization for commands! +reply_interface_right-de = Info: Keine Berechtigung für Befehle! +reply_local_enabled = Info: Group deactivated! +reply_local_enabled-de = Info: Gruppe deaktiviert! +reply_local_right = Info: No authorization to send messages! +reply_local_right-de = Info: Keine Berechtigung zum senden von Nachrichten! +reply_block = Info: You are blocked! +reply_block-de = Info: Sie sind geblockt! +reply_length_min = Info: Minimum message length not reached! +reply_length_min-de = Info: Minimale Nachrichtenlänge unterschritten! +reply_length_max = Info: Maximum message length exceeded! +reply_length_max-de = Info: Maximale Nachrichtenlänge überschritten! + + + + +#### Interface settings - Menu/command #### + +# Define menu/command texts. +# These texts are used within the menu the user has to start. +[interface_menu] + +# "/help" command. +help_admin = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_admin-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_mod = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_mod-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_user = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Nickname: !source_name!!n!User right/type: !source_right!!n!!n!!interface_help!!n!Commands:!n!!interface_help_command! +help_user-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Nickname: !source_name!!n!Benutzer Recht/Typ: !source_right!!n!!n!!interface_help!!n!Befehle:!n!!interface_help_command! +help_guest = +help_guest-de = + +# "/leave" command. +leave_ok = OK: You leaved the group. +leave_ok-de = OK: Sie haben die Gruppe verlassen. +leave_error = ERROR: While leaving group. +leave_error-de = FEHLER: Beim Verlassen der Gruppe. + +# "/name" command. +name = Current nickname: !source_name! +name-de = Aktueller Nickname: !source_name! +name_ok = OK: Changed name: +name_ok-de = OK: Name geändert: +name_error = ERROR: Changing name: +name_error-de = FEHLER: Name ändern: + +# "/address" command. +address_admin = Group address:!n!!n!!n!Propagation node:!n! +address_admin-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_mod = Group address:!n!!n!!n!Propagation node:!n! +address_mod-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_user = Group address:!n!!n!!n!Propagation node:!n! +address_user-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! +address_guest = Group address:!n!!n!!n!Propagation node:!n! +address_guest-de = Gruppenadresse:!n!!n!!n!Propagation Node:!n! + +# "/info" command. +info_admin = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_admin-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_mod = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_mod-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_user = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_user-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. +info_guest = Group:!n!!display_name!!n!!n!Description:!n!!description!!n!!n!Number of members: !count_members!!n!Pinned messages: !count_pin!!n!Use the command /pin to display them. +info_guest-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n!Anzahl Mitglieder: !count_members!!n!Angepinnte Nachrichten: !count_pin!!n!Verwenden Sie den Befehl /pin um sie anzuzeigen. + +# "/pin" command. +pin_header = Pinned messages (!count!):!n!!n! +pin_header-de = Angeheftete Nachrichten (!count!):!n!!n! +pin = !source_name!!n!!n!!value! +pin-de = !source_name!!n!!n!!value! +pin_add_ok = OK: Message pinned +pin_add_ok-de = OK: Nachricht angeheftet +pin_remove_ok = OK: Message removed +pin_remove_ok-de = OK: Nachricht entfernt +pin_found_error = ERROR: Message ID not found +pin_found_error-de = FEHLER: Nachrichten ID nicht gefunden +cluster_pin = !value! +cluster_pin-de = !value! + +# "/version" command. +version_header = Version info:!n!!n! +version_header-de = Versionsinformationen:!n!!n! + +# "/groups" command. +groups_header = Groups/Cluster (!count!):!n!!n! +groups_header-de = Gruppen/Cluster (!count!):!n!!n! +groups_member = !source_name!!n! +groups_member-de = !source_name!!n! +groups_search_header = Found groups/cluster (!count!):!n!!n! +groups_search_header-de = Gefundene Gruppen/Cluster (!count!):!n!!n! +groups_search_member = !source_name!!n! +groups_search_member-de = !source_name!!n! +groups_search_found_error = ERROR: Group/Cluster not found +groups_search_found_error-de = FEHLER: Gruppe/Cluster nicht gefunden + +# "/members" command. +members_header = Group members (!count!):!n!!n! +members_header-de = Gruppenmitglieder (!count!):!n!!n! +members_member = !source_name!!n!!n!!n! +members_member-de = !source_name!!n!!n!!n! + +# "/admins" command. +admins_header = Group admins (!count!):!n!!n! +admins_header-de = Gruppenadmins (!count!):!n!!n! +admins_member = !source_name!!n!!n!!n! +admins_member-de = !source_name!!n!!n!!n! + +# "/moderators" command. +moderators_header = Group moderators (!count!):!n!!n! +moderators_header-de = Gruppenmoderatoren (!count!):!n!!n! +moderators_member = !source_name!!n!!n!!n! +moderators_member-de = !source_name!!n!!n!!n! + +# "/users" command. +users_header = Group users (!count!):!n!!n! +users_header-de = Gruppenbenutzer (!count!):!n!!n! +users_member = !source_name!!n!!n!!n! +users_member-de = !source_name!!n!!n!!n! + +# "/guests" command. +guests_header = Group guests (!count!):!n!!n! +guests_header-de = Gruppengäste (!count!):!n!!n! +guests_member = !source_name!!n!!n!!n! +guests_member-de = !source_name!!n!!n!!n! + +# "/search" command. +search_header = Found members (!count!):!n!!n! +search_header-de = Gefundene Mitglieder (!count!):!n!!n! +search_member = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +search_member-de = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +search_found_error = ERROR: Nickname or address not found +search_found_error-de = FEHLER: Benutzername oder Adresse nicht gefunden + +# "/activitys" command. +activitys_header = User activitys (!count!):!n!(receive / send)!n!!n! +activitys_header-de = Benutzeraktivitäten (!count!):!n!(empf. / gesendet)!n!!n! +activitys_member = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! +activitys_member-de = !source_name!!n!!n!!activity_receive! / !activity_send!!n!!n! + +# "/statistic" command. +statistic_header_cluster = -- Cluster statistics - !value! --!n! +statistic_header_cluster-de = -- Cluster-Statistik - !value! --!n! +statistic_header_local = -- Group statistics - !value! --!n! +statistic_header_local-de = -- Gruppen-Statistik - !value! --!n! +statistic_header_interface = -- Interface statistics - !value! --!n! +statistic_header_interface-de = -- Interface-Statistik - !value! --!n! +statistic_header_user = -- User statistics - !value! --!n! +statistic_header_user-de = -- Benutzer-Statistik - !value! --!n! +statistic_header_self = -- Own statistics --!n! +statistic_header_self-de = -- Eigene-Statistik --!n! +statistic_found_error = ERROR: Statistic type not found +statistic_found_error-de = FEHLER: Statistik typ nicht vorhanden + +# "/status" command. +status_admin = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_admin-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_mod = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_mod-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_user = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_user-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! +status_guest = Status:!n!!n!Local message routing:!enabled_local!!n!Cluster message routing:!enabled_cluster!!n! +status_guest-de = Status:!n!!n!Lokales Nachrichten Routing:!enabled_local!!n!Cluster Nachrichten Routing:!enabled_cluster!!n! + +# "/delivery" command. +# todo + +# "/enable_local" command. +enable_local_true = OK: Local message routing enabled. +enable_local_true-de = OK: Lokales Nachrichten Routing aktiviert. +enable_local_false = OK: Local message routing disabled. +enable_local_false-de = OK: Lokales Nachrichten Routing deaktiviert. +enable_local_error = ERROR: Local message routing change. +enable_local_error-de = FEHLER: Änderung Lokales Nachrichten Routing. + +# "/enable_cluster" command. +enable_cluster_true = OK: Cluster message routing enabled. +enable_cluster_true-de = OK: Cluster Nachrichten Routing aktiviert. +enable_cluster_false = OK: Cluster message routing disabled. +enable_cluster_false-de = OK: Cluster Nachrichten Routing deaktiviert. +enable_cluster_error = ERROR: Cluster message routing change. +enable_cluster_error-de = FEHLER: Änderung Cluster Nachrichten Routing. + +# "/auto_add_user" command. +auto_add_user_true = OK: Auto add user enabled. +auto_add_user_true-de = OK: Benutzer automatisch hinzufügen aktiviert. +auto_add_user_false = OK: Auto add user disabled. +auto_add_user_false-de = OK: Benutzer automatisch hinzufügen deaktiviert. +auto_add_user_error = ERROR: Auto add user change. +auto_add_user_error-de = FEHLER: Änderung Benutzer automatisch hinzufügen. + +# "/auto_add_user_type" command. +auto_add_user_type = OK: User type changed to: +auto_add_user_type-de = OK: Benutzertyp geändert in: +auto_add_user_type_error = ERROR: User type change. +auto_add_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/auto_add_cluster" command. +auto_add_cluster_true = OK: Auto add cluster enabled. +auto_add_cluster_true-de = OK: Cluster/Gruppen automatisch hinzufügen aktiviert. +auto_add_cluster_false = OK: Auto add cluster disabled. +auto_add_cluster_false-de = OK: Cluster/Gruppen automatisch hinzufügen deaktiviert. +auto_add_cluster_error = ERROR: Auto add cluster change. +auto_add_cluster_error-de = FEHLER: Änderung Cluster/Gruppen automatisch hinzufügen. + +# "/invite_user" command. +invite_user_true = OK: Invite user enabled. +invite_user_true-de = OK: Benutzer einladen aktiviert. +invite_user_false = OK: Invite user disabled. +invite_user_false-de = OK: Benutzer einladen deaktiviert. +invite_user_error = ERROR: Invite user change. +invite_user_error-de = FEHLER: Änderung Benutzer einladen. + +# "/invite_user_type" command. +invite_user_type = OK: User type changed to: +invite_user_type-de = OK: Benutzertyp geändert in: +invite_user_type_error = ERROR: User type change. +invite_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/allow_user" command. +allow_user_true = OK: Allow user enabled. +allow_user_true-de = OK: Benutzer erlauben aktiviert. +allow_user_false = OK: Allow user disabled. +allow_user_false-de = OK: Benutzer erlauben deaktiviert. +allow_user_error = ERROR: Allow user change. +allow_user_error-de = FEHLER: Änderung Benutzer erlauben. + +# "/allow_user_type" command. +allow_user_type = OK: User type changed to: +allow_user_type-de = OK: Benutzertyp geändert in: +allow_user_type_error = ERROR: User type change. +allow_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/deny_user" command. +deny_user_true = OK: Deny user enabled. +deny_user_true-de = OK: Benutzer ablehnen aktiviert. +deny_user_false = OK: Deny user disabled. +deny_user_false-de = OK: Benutzer ablehnen deaktiviert. +deny_user_error = ERROR: Deny user change. +deny_user_error-de = FEHLER: Änderung Benutzer ablehnen. + +# "/deny_user_type" command. +deny_user_type = OK: User type changed to: +deny_user_type-de = OK: Benutzertyp geändert in: +deny_user_type_error = ERROR: User type change. +deny_user_type_error-de = FEHLER: Änderung des Benutzertyps. + +# "/description" command. +description = OK: Description changed to: +description-de = OK: Beschreibung geändert in: +description_error = ERROR: Description change. +description_error-de = FEHLER: Beschreibung ändern. + +# "/rules" command. +rules = OK: Rules changed to: +rules-de = OK: Regeln geändert in: +rules_error = ERROR: Rules change. +rules_error-de = FEHLER: Regeln ändern. + +# "/readme" command. +readme = +readme-de = + +# "/time" command. +time = Current server time: %%Y-%%m-%%d %%H:%%M:%%S +time-de = Aktuelle Server-Zeit: %%Y-%%m-%%d %%H:%%M:%%S + +# "/announce" command. +announce = Announce send. +announce-de = Announce gesendet. + +# "/sync" command. +sync = Synchronize messages with propagation node . +sync-de = Synchronisiere Nachrichten mit Propagation node . + +# "/show run" command. +show_run_header = Current settings/data:!n!!n! +show_run_header-de = Aktuelle Konfiguration/Daten:!n!!n! + +# "/show" command. +show_header = +show_header-de = + +# "/user" command. +user_add = OK: Added user -> group: +user_add-de = OK: Benutzer -> Gruppe hinzugefügt: +user_del = OK: Removed user -> group: +user_del-de = OK: Entfernter Benutzer -> Gruppe: +user_move = OK: Moved user -> group: +user_move-de = OK: Benutzer -> Gruppe verschoben: +user_error = ERROR: Unknown user -> group: +user_error-de = FEHLER: Unbekannter Benutzer -> Gruppe: +user_format_error = ERROR: Wrong user format +user_format_error-de = FEHLER: Falsches Benutzerformat +user_type_error = ERROR: Unknown user type +user_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/invite" command. +invite_ok = OK: Invited user: +invite_ok-de = OK: Benutzer eingeladen: +invite_error = ERROR: Inviting user: +invite_error-de = FEHLER: Benutzer einladen: +invite_format_error = ERROR: Wrong user format +invite_format_error-de = FEHLER: Falsches Benutzerformat +invite_type_error = ERROR: Unknown user type +invite_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/kick" command. +kick_ok = OK: User kicked out: !user_name! +kick_ok-de = OK: Benutzer rausgeworfen: !user_name! +kick_found_error = ERROR: User address not found +kick_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +kick_format_error = ERROR: Wrong user format +kick_format_error-de = FEHLER: Falsches Benutzerformat + +# "/block" command. +block_ok = OK: User blocked: !user_name! +block_ok-de = OK: Benutzer blockiert: !user_name! +block_found_error = ERROR: User address not found +block_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +block_format_error = ERROR: Wrong user format +block_format_error-de = FEHLER: Falsches Benutzerformat + +# "/unblock" command. +unblock_ok = OK: User unblocked: !user_name! +unblock_ok-de = OK: Blockerierung des Benutzers aufgehoben: !user_name! +unblock_found_error = ERROR: User address not found +unblock_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +unblock_format_error = ERROR: Wrong user format +unblock_format_error-de = FEHLER: Falsches Benutzerformat + +# "/allow" command. +allow_ok = OK: User allowed: !user_name! +allow_ok-de = OK: Benutzer erlaubt: !user_name! +allow_found_error = ERROR: User address not found +allow_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +allow_format_error = ERROR: Wrong user format +allow_format_error-de = FEHLER: Falsches Benutzerformat +allow_type_error = ERROR: Unknown user type +allow_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/deny" command. +deny_ok = OK: User denied: !user_name! +deny_ok-de = OK: Benutzer abgelehnt: !user_name! +deny_found_error = ERROR: User address not found +deny_found_error-de = FEHLER: Benutzer Adresse nicht gefunden +deny_format_error = ERROR: Wrong user format +deny_format_error-de = FEHLER: Falsches Benutzerformat +deny_type_error = ERROR: Unknown user type +deny_type_error-de = FEHLER: Unbekannter Benutzertyp + +# "/load" command. +load_ok = OK: Loading configuration/data. +load_ok-de = OK: Konfiguration/Daten werden geladen. +load_error = ERROR: Loading configuration/data. +load_error-de = FEHLER: Konfiguration/Daten werden geladen. + +# "/save" command. +save_ok = OK: Saved configuration/data. +save_ok-de = OK: Konfiguration/Daten gespeichert. +save_error = ERROR: Saving configuration/data. +save_error-de = FEHLER: Speichern der Konfiguration/Daten. +save_info = INFO: Unsaved changes! Please run the command '/save' to save these changes permanently! +save_info-de = INFO: Nicht gespeicherte Änderungen! Bitte führen Sie den Befehl '/save' aus, um diese Änderungen dauerhaft zu speichern! + +# "/reload" command. +reload_ok = OK: Reloaded configuration/data. +reload_ok-de = OK: Konfiguration/Daten neu geladen. +reload_error = ERROR: Reload configuration/data. +reload_error-de = FEHLER: Neu laden der Konfiguration/Daten. + +# "/reset" command. +reset_statistic_ok = OK: Reset statistic. +reset_statistic_ok-de = OK: Statistik zurückgesetzt. +reset_statistic_error = ERROR: Reset statistic. +reset_statistic_error-de = FEHLER: Statistik zurücksetzen. + +# Cluster messages. +cluster_found_error = ERROR: Cluster name not found +cluster_found_error-de = FEHLER: Clustername nicht gefunden +cluster_format_error = ERROR: Wrong cluster format +cluster_format_error-de = FEHLER: Falsches Clusterformat + +# General messages. +cmd_error = ERROR: Processing command. +cmd_error-de = FEHLER: Verarbeitung des Befehls. +cmd_unknown = ERROR: Unknown command. Type /? for help. +cmd_unknown-de = FEHLER: Unbekannter Befehl. Geben Sie /? für Hilfe ein. + + + + +#### Interface settings - Help #### + +# Define help texts. +# These texts are used within the help-menu. +# Only the commands defined in the user rights are displayed. +# If a message is to be deactivated simply comment it out. +[interface_help] +send_local = To send a message simply enter any text.!n!!n! +send_local-de = Um eine Nachricht zu senden, geben Sie einfach einen beliebigen Text ein.!n!!n! +send_cluster = To send a message to another group enter the destination group with the following command followed by the message: @destination message.!n!!n! +send_cluster-de = Um eine Nachricht an eine andere Gruppe zu senden, geben Sie die Zielgruppe mit dem folgenden Befehl gefolgt von der Nachricht ein: @Zielname Nachricht.!n!!n! +interface = If the sent message is displayed as delivered and no error message is received, everything has worked fine.!n!!n! +interface-de = Wenn die gesendete Nachricht als zugestellt angezeigt wird und keine Fehlermeldung eingeht, hat alles fehlerfrei funktioniert.!n!!n! + + +# Define help texts. +# These texts are used within the help-menu. +# Only the commands defined in the user rights are displayed. +# If a message is to be deactivated simply comment it out. +[interface_help_command] + +help = /help or /? = Shows this help!n! +help-de = /help oder /? = Zeigt diese Hilfe an!n! + +leave = /leave or /part = Leave group!n! +leave-de = /leave oder /part = Gruppe verlassen!n! + +name = /name = Show current nickname!n!/nick = Show current nickname!n!/name = Change/Define nickname!n!/nick = Change/Define nickname!n! +name-de = /name = Aktueller Nickname anzeigen!n!/nick = Aktueller Nickname anzeigen!n!/name = Ändern/Definieren des Nickname!n!/nick = Ändern/Definieren des Nickname!n! + +address = /address = Dislay address info!n! +address-de = /address = Adressinfos anzeigen!n! + +info = /info = Show group info!n! +info-de = /info = Gruppeninfos anzeigen!n! + +pin = /pin = Show pinned messages!n! +pin-de = /pin = Angeheftete Nachrichten anzeigen!n! + +pin_add = /pin = Pin a message!n! +pin_add-de = /pin = Nachricht anheften!n! + +pin_remove = /unpin <#id> = Removes a pinned message!n! +pin_remove-de = /unpin <#id> = Angeheftete Nachricht entfernen!n! + +version = /version = Show version info!n! +version-de = /version = Versionsinformationen anzeigen!n! + +groups = /groups or /cluster = Show all groups/clusters!n!/groups = Searches for a group/cluster by name!n! +groups-de = /groups oder /cluster = Alle Gruppen/Cluster anzeigen!n!/groups = Sucht nach einer Gruppe/Cluster nach Namen!n! + +members = /members or /names or /who = Show all group members!n! +members-de = /members oder /names oder /who = Alle Gruppenmitglieder anzeigen!n! + +admins = /admins = Show group admins!n! +admins-de = /admins = Gruppenadmins anzeigen!n! + +moderators = /moderators or /mods = Show group moderators!n! +moderators-de = /moderators oder /mods = Gruppenmoderatoren anzeigen!n! + +users = /users = Show group users!n! +users-de = /users = Gruppenbenutzer anzeigen!n! + +guests = /guests = Show group guests!n! +guests-de = /guests = Gruppengäste anzeigen!n! + +search = /search = Searches for a user by nickname or address!n!/whois = Searches for a user by nickname or address!n! +search-de = /search = Sucht einen Benutzer anhand des Nicknamens oder Adresse!n!/whois = Sucht einen Benutzer anhand des Nicknamens oder Adresse!n! + +activitys = /activitys = Show user activitys!n! +activitys-de = /activitys = Benutzeraktivitäten anzeigen!n! + +statistic = /statistic or /stat = Show group statistic!n!/statistic or /stat = Show group statistic!n! +statistic-de = /statistic oder /stat = Gruppenstatistik anzeigen!n!/statistic oder /stat = Gruppenstatistik anzeigen!n! + +status = /status = Show status!n! +status-de = /status = Status anzeigen!n! + +delivery = /delivery or /message = Show delivery status of last message!n! +delivery-de = /delivery oder /message = Lieferstatus der letzten Nachricht anzeigen!n! + +enable_local = /enable_local = Local message routing!n! +enable_local-de = /enable_local Lokales Nachrichten Routing!n! + +enable_cluster = /enable_cluster = Cluster message routing!n! +enable_cluster-de = /enable_cluster Cluster Nachrichten Routing!n! + +auto_add_user = /auto_add_user = Add unknown user functionality!n! +auto_add_user-de = /auto_add_user = Unbekannten Benutzer hinzufügen Funktionalität!n! + +auto_add_user_type = /auto_add_user_type !n! +auto_add_user_type-de = /auto_add_user_type !n! + +auto_add_cluster = /auto_add_cluster = Add unknown cluster functionality!n! +auto_add_cluster-de = /auto_add_cluster = Unbekannten Cluster/Gruppen hinzufügen Funktionalität!n! + +invite_user = /invite_user = Invite functionality!n! +invite_user-de = /invite_user = Einladung Funktionalität!n! + +invite_user_type = /invite_user_type !n! +invite_user_type-de = /invite_user_type !n! + +allow_user = /allow_user = Allow user functionality!n! +allow_user-de = /allow_user = Benutzer erlauben Funktionalität!n! + +allow_user_type = /allow_user_type !n! +allow_user_type-de = /allow_user_type !n! + +deny_user = /deny_user = Deny user functionality!n! +deny_user-de = /deny_user = Benutzer ablehnen Funktionalität!n! + +deny_user_type = /deny_user_type !n! +deny_user_type-de = /deny_user_type !n! + +description = /description = Show current description!n! +description-de = /description = Aktuelle Beschreibung anzeigen!n! + +description_set = /description = Change description!n! +description_set-de = /description !n! = Beschreibung ändern + +rules = /rules = Show current rules!n! +rules-de = /rules = Aktuelle Regeln anzeigen!n! + +rules_set = /rules = Change rules!n! +rules_set-de = /rules !n! = Regeln ändern + +readme = /readme = Show readme!n! +readme-de = /readme = Liesmich anzeigen!n! + +time = /time = Show date/time!n! +time-de = /time = Datum/Uhrzeit anzeigen!n! + +announce = /announce = Send announce!n! +announce-de = /announce = Announce senden!n! + +sync = /sync = Synchronize messages with propagation node!n! +sync-de = /sync = Nachrichten mit Propagation Node synchronisieren!n! + +show_run = /show run = Show current configuration!n! +show_run-de = /show run = Aktuelle Konfiguration anzeigen!n! + +show = /show or /list!n!/show or /list !n! +show-de = /show oder /list!n!/show oder /list !n! + +add = /add !n! +add-de = /add !n! + +del = /del or /rm !n!/del or /rm !n! +del-de = /del oder /rm !n!/del oder /rm !n! + +move = /move !n! +move-de = /move !n! + +invite = /invite = Invites user to group!n! +invite-de = /invite = Lädt Benutzer zur Gruppe ein!n! + +kick = /kick = Kicks user out of group!n! +kick-de = /kick = Wirft den Benutzer aus der Gruppe!n! + +block = /block = Block user!n!/ban = Block user!n! +block-de = /block = Blockiert Benutzer!n!/ban = Blockiert Benutzer!n! + +unblock = /unblock = Unblock user!n!/unban = Unblock user!n! +unblock-de = /unblock = Blockierung des Benutzers aufheben!n!/unban = Blockierung des Benutzers aufheben!n! + +allow = /allow = Allow user!n! +allow-de = /allow = Benutzer erlauben!n! + +deny = /deny = Deny user!n! +deny-de = /deny = Benutzer ablehnen!n! + +load = /load or /read = Read the configuration/data!n! +load-de = /load oder /read = Lesen der Konfiguration/Daten!n! + +save = /save or /wr = Saves the current configuration/data!n! +save-de = /save oder /wr = Speichert die aktuelle Konfiguration/Daten!n! + +reload = /reload = Reload the current configuration/data!n! +reload-de = /reload = Neu laden der aktuelle Konfiguration/Daten!n! + +reset = /reset statistic = Reset statistic!n! +reset-de = /reset statistic = Statisktik zurücksetzenn!n! +''' + + +#### Default data file #### +DEFAULT_DATA = '''# This is the data file. It is automatically created and saved/overwritten. +# It contains data managed by the software itself. +# If manual adjustments are made here, the program must be shut down first! + + +#### High availability settings #### +[high_availability] +role = master +last_heartbeat = 0000-00-00 00:00:00 + + +#### Main program settings #### +[main] +enabled_local = True +enabled_cluster = True +auto_add_user = True +auto_add_user_type = user +auto_add_cluster = True +invite_user = True +invite_user_type = user +allow_user = True +allow_user_type = user +deny_user = True +deny_user_type = block_wait +description = This group is for a first test of functionality.!n!!n!To receive offline messages please use the following propagation node: +description-de = Diese Gruppe dient einem ersten Test der Funktionalität.!n!!n!Um offline Nachrichten zu empfangen bitte folgender Propagation Node verwenden: +rules = Please follow the general rules of etiquette which should be taken for granted!!n!Prohibited are:!n!Spam, insults, violence, sex, illegal topics +rules-de = Bitte befolgen Sie die allgemeinen benimm-dich-Regeln welche als selbstverständlich gelten sollten!!n!Verboten sind:!n!Spam, Beleidigungen, Gewalt, Sex, illegale Themen + + +#### Admin user #### +[admin] + +#### Mod/Moderator user #### +[mod] + +#### User #### +[user] + +#### Guest user #### +[guest] + +#### Wait user #### +[wait] + +#### Blocked user #### +[block_admin] + +[block_mod] + +[block_user] + +[block_guest] + +[block_wait] + +#### Cluster (Automatically or manual created) #### +[cluster] + +[block_cluster] + +#### Pinned messages (Automatically created) #### +[pin] +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_distribution_group_minimal/CHANGELOG.md b/lxmf_distribution_group_minimal/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_distribution_group_minimal/README.md b/lxmf_distribution_group_minimal/README.md new file mode 100644 index 0000000..9929ebc --- /dev/null +++ b/lxmf_distribution_group_minimal/README.md @@ -0,0 +1,295 @@ +# lxmf_distribution_group_minimal +This program is a minimalist version of the normal distribution group. The functionality is reduced to a minimum. Only sender and receiver users can be defined. Messages are then sent to the other users accordingly. There is no user interface or other notifications. Only the messages are distributed 1:1. The administration is done completely by the respective configuration files which are to be edited accordingly. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) +- Server/Node based message routing and processing +- Direct or propagated message delivery (receive/send) +- Easy distribution of incoming messages to recipients + + +## Examples of use + +### Local self-sufficient group +In a small group of people, this group software can be hosted on a centrally located node. This then allows users to communicate with each other via this group. + +### Multiple local self-sufficient group +On the same node/server several groups can be operated independently of each other. How this works is described below in the installation instructions. + +### General info how the messages are transported +All messages between client<->group-server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_distribution_group_minimal.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_distribution_group_minimal.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_distribution_group_minimal.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_distribution_group_minimal/config.cfg.owr + ``` + ```bash + # This is the user configuration file to override the default configuration file. + # All settings made here have precedence. + # This file can be used to clearly summarize all settings that deviate from the default. + # This also has the advantage that all changed settings can be kept when updating the program. + + #### LXMF connection settings #### + [lxmf] + + # The name will be visible to other peers + # on the network, and included in announces. + # It is also used in the group description/info. + display_name = Distribution Group + + # Propagation node address/hash. + propagation_node = ca2762fe5283873719aececfb9e18835 + + # Try to deliver a message via the LXMF propagation network, + # if a direct delivery to the recipient is not possible. + try_propagation_on_fail = Yes + ``` +- Start it again. Finished! + ```bash + ./lxmf_distribution_group_minimal.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_distribution_group_minimal.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_distribution_group_minimal.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_distribution_group_minimal.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_distribution_group_minimal + ``` +- Start the service. + ```bash + systemctl start lxmf_distribution_group_minimal + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_distribution_group_minimal + systemctl stop lxmf_distribution_group_minimal + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_distribution_group_minimal + systemctl disable lxmf_distribution_group_minimal + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_distribution_group_minimal.py -p /root/.lxmf_distribution_group_minimal_2nd + ./lxmf_distribution_group_minimal.py -p /root/.lxmf_distribution_group_minimal_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own group LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- The users need to be entered manually in the `data.cfg` file. +- Now the group can be used. + + +### Startup parameters: +```bash +usage: lxmf_distribution_group_minimal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] [--exampledata] + +LXMF Distribution Group - Server-Side group functions for LXMF based apps + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit + --exampledata Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + +- data.cfg + + This is the data file. + It contains the user data. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +### Members: +All data here (`data.cfg`) contains the end users. The users must be maintained manually. There is no automatic joining to the group. +Please do not forget to restart the program after a modification! + +- Receive only and send only members `data.cfg` + ``` + [send] + 04652a820cc69d47940ce39050c455a6 = Test user with send only right 1 + + [receive] + d1b551e1b89fff5a4a6f2aaff2464971 = Test user with receive only right 1 + 801f48d54bc71cb3e0886944832aaf8d = Test user with receive only right 2 + + [receive_send] + ``` + +- Receive and send members (Anyone can communicate with anyone)`data.cfg` + ``` + [send] + + [receive] + + [receive_send] + 04652a820cc69d47940ce39050c455a6 = Test user 1 + d1b551e1b89fff5a4a6f2aaff2464971 = Test user 2 + 801f48d54bc71cb3e0886944832aaf8d = Test user 3 + ``` + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + +### Manage users: +All users are maintained directly in the `data.cfg` file. +There is no automatic joining to the group. +Please do not forget to restart the program after a modification! + + ``` + # This is the data file. It is automatically created and saved/overwritten. + # It contains data managed by the software itself. + # If manual adjustments are made here, the program must be shut down first! + + + #### User with send only rights #### + [send] + 04652a820cc69d47940ce39050c455a6 = Test user 1 + + #### User with receive only rights #### + [receive] + d1b551e1b89fff5a4a6f2aaff2464971 = Test user 2 + + #### User with receive and send rights #### + [receive_send] + 801f48d54bc71cb3e0886944832aaf8d = Test user 3 + ``` + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +### Start/Join the group: +The administrator must create new users manually. + + +### Send message: +Any text will be interpreted as a normal message and sent to all members accordingly. There is nothing else to consider here. + + +## FAQ + +### Why this server based group function and no direct groups in the client software? +At the time of the development of these group functions there is no other possibility to use groups via Sideband/Nomadnet. Therefore this software was developed as a workaround. +This software also offers other functions than a normal group broadcast. + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py new file mode 100755 index 0000000..c52dd6e --- /dev/null +++ b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py @@ -0,0 +1,1297 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import datetime +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Distribution Group" +DESCRIPTION = "Server-Side group functions for LXMF based apps" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + content = message.content.decode('utf-8') + content = content.strip() + if content == "": + return + + title = message.title.decode('utf-8') + title = title.strip() + + source_hash = RNS.hexrep(message.source_hash, False) + source_name = "" + source_right = "" + + for section in DATA.sections(): + for (key, val) in DATA.items(section): + if key == source_hash: + source_name = val + source_right = section + + if source_right == "": + for section in DATA.sections(): + if "send" in section: + if DATA.has_option(section, "any") or DATA.has_option(section, "all") or DATA.has_option(section, "anybody"): + source_right = section + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist -> any allowed", LOG_DEBUG) + break + if source_right == "": + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist", LOG_DEBUG) + return + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + if "send" in source_right: + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + content_prefix = content_prefix.replace("!source_address!", source_hash) + content_prefix = content_prefix.replace("!source_name!", source_name) + content_prefix = content_prefix.replace("!name!", config_get(CONFIG, "main", "name")) + content_prefix = content_prefix.replace("!display_name!", config_get(CONFIG, "lxmf", "display_name")) + content_prefix = content_prefix.replace("!n!", "\n") + + content_suffix = content_suffix.replace("!source_address!", source_hash) + content_suffix = content_suffix.replace("!source_name!", source_name) + content_suffix = content_suffix.replace("!name!", config_get(CONFIG, "main", "name")) + content_suffix = content_suffix.replace("!display_name!", config_get(CONFIG, "lxmf", "display_name")) + content_suffix = content_suffix.replace("!n!", "\n") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + title = message.title.decode('utf-8') + + if config_get(CONFIG, "message", "timestamp") == "client": + timestamp = message.timestamp + else: + timestamp = time.time() + + for section in DATA.sections(): + if "receive" in section: + for (key, val) in DATA.items(section): + if key != source_hash: + LXMF_CONNECTION.send(key, content, title, timestamp) + return + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'send' not allowed", LOG_DEBUG) + + return + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Data + + +#### Data - Read ##### +def data_read(file=None): + global DATA + + if file is None: + return False + else: + DATA = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + DATA.sections() + if os.path.isfile(file): + try: + DATA.read(file) + except Exception as e: + return False + else: + if not data_default(file=file): + return False + return True + + + + +#### Data - Save ##### +def data_save(file=None): + global DATA + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + DATA.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Data - Save ##### +def data_save_periodic(initial=False): + data_timer = threading.Timer(CONFIG.getint("main", "periodic_save_data_interval")*60, data_save_periodic) + data_timer.daemon = True + data_timer.start() + + if initial: + return + + global DATA + if DATA.has_section("main"): + if DATA["main"].getboolean("unsaved"): + DATA.remove_option("main", "unsaved") + if not data_save(PATH + "/data.cfg"): + DATA["main"]["unsaved"] = "True" + + + + +#### Data - Default ##### +def data_default(file=None): + global DATA + + if file is None: + return False + elif DEFAULT_DATA != "": + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + data_file = open(file, "w") + data_file.write(DEFAULT_DATA) + data_file.close() + if not data_read(file=file): + return False + except: + return False + else: + return False + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if not data_read(PATH + "/data.cfg"): + print("Data - Error reading data file " + PATH + "/data.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Data File: " + PATH + "/data.cfg", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampledata", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + if params.exampledata: + print("Data File: " + PATH + "/data.cfg") + print("Content:") + print(DEFAULT_DATA) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. + +#### LXMF connection settings #### +[lxmf] + +# The name will be visible to other peers +# on the network, and included in announces. +# It is also used in the group description/info. +display_name = Distribution Group + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Distribution Group + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = Distribution Group + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +propagation_node = ca2762fe5283873719aececfb9e18835 + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = Yes + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 120 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = No + + + + +#### Message settings #### +[message] +## Each message received (message and command) ## + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +## Each message send (message) ## + +# Text is added. +send_prefix = !source_name!!n!!n! +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +# Define which message timestamp should be used. +timestamp = client #client/server +''' + + +#### Default data file #### +DEFAULT_DATA = '''# This is the data file. It is automatically created and saved/overwritten. +# It contains data managed by the software itself. +# If manual adjustments are made here, the program must be shut down first! + + +#### User with send only rights #### +[send] + +#### User with receive only rights #### +[receive] + +#### User with receive and send rights #### +[receive_send] +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_echo/CHANGELOG.md b/lxmf_echo/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_echo/README.md b/lxmf_echo/README.md new file mode 100644 index 0000000..6b9a227 --- /dev/null +++ b/lxmf_echo/README.md @@ -0,0 +1,197 @@ +# lxmf_echo +This program is a simple echo server. All received messages are sent back 1:1 as an answer. This can be used as a simple counterpart to test the chat functionality of applications. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_echo.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_echo/lxmf_echo.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_echo.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_echo.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_echo/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_echo.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_echo.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_echo.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_echo.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_echo + ``` +- Start the service. + ```bash + systemctl start lxmf_echo + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_echo + systemctl stop lxmf_echo + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_echo + systemctl disable lxmf_echo + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_echo.py -p /root/.lxmf_echo_2nd + ./lxmf_echo.py -p /root/.lxmf_echo_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_echo.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Echo - + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_echo/lxmf_echo.py b/lxmf_echo/lxmf_echo.py new file mode 100755 index 0000000..7cadc7f --- /dev/null +++ b/lxmf_echo/lxmf_echo.py @@ -0,0 +1,1126 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Echo" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + content = message.content.decode('utf-8').strip() + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + + content = content_prefix + content + content_suffix + + LXMF_CONNECTION.send(message.source_hash, content, message.title.decode('utf-8').strip()) + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Echo Test + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = Echo Test + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = Yes +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = Yes +announce_periodic_interval = 360 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_ping/CHANGELOG.md b/lxmf_ping/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_ping/README.md b/lxmf_ping/README.md new file mode 100644 index 0000000..26ef7df --- /dev/null +++ b/lxmf_ping/README.md @@ -0,0 +1,100 @@ +# lxmf_ping +This program sends an adjustable number of LXMF messages to a destination. Then a simple statistic is created to check the success or failure of a single message. This tool can be useful to load the LXMF/Reticulum network with a defined load of messages. This can be used to simulate a certain amount of users. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_ping.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_ping/lxmf_ping.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_ping.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_ping.py + ``` + + +### Startup parameters: +```bash +usage: lxmf_ping.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] -d DEST [-t TIME] [-s SIZE] [-c COUNT] [-i INST] + +LXMF Ping - Periodically sends pings/messages and evaluates the status + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -d DEST, --dest DEST Single destination hash or ,-separated list with destination hashs or . for random destination + -t TIME, --time TIME Time between messages in seconds + -s SIZE, --size SIZE Size (lenght) of the message content + -c COUNT, --count COUNT + Maximum message send count (0=no end) + -i INST, --inst INST Parallel instances (different sender addresses) +``` + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_ping/lxmf_ping.py b/lxmf_ping/lxmf_ping.py new file mode 100755 index 0000000..fee26a7 --- /dev/null +++ b/lxmf_ping/lxmf_ping.py @@ -0,0 +1,782 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import argparse + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Other #### +import random +import secrets + +#### Process #### +import signal +import threading + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Ping" +DESCRIPTION = "Periodically sends pings/messages and evaluates the status" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +DATA = None +RNS_CONNECTION = None +LXMF_CONNECTION = None + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Success #### +def lxmf_success(message): + global DATA + key = RNS.hexrep(message.destination_hash, False) + if not key in DATA: + key = "." + if not key in DATA: + return + DATA[key]["count_success"] = DATA[key]["count_success"] + 1 + timestamp = round(float(time.time()) - float(message.timestamp), 4) + if DATA[key]["time_min"] == 0 or DATA[key]["time_min"] > timestamp: + DATA[key]["time_min"] = timestamp + if DATA[key]["time_max"] == 0 or DATA[key]["time_max"] < timestamp: + DATA[key]["time_max"] = timestamp + DATA[key]["time"] = DATA[key]["time"] + timestamp + DATA[key]["time_avg"] = round(DATA[key]["time"]/DATA[key]["count_success"], 4) + count = str(message.content) + if "#" in count: + count = count.split("#", 1)[1] + count = count.split(" ", 1)[0] + else: + count = "" + print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Success") + + + + +#### LXMF - Failed #### +def lxmf_failed(message): + global DATA + key = RNS.hexrep(message.destination_hash, False) + if not key in DATA: + key = "." + if not key in DATA: + return + DATA[key]["count_failed"] = DATA[key]["count_failed"] + 1 + count = str(message.content) + if "#" in count: + count = count.split("#", 1)[1] + count = count.split(" ", 1)[0] + else: + count = "" + print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Failed") + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, dest="", interval=1, size=128, count=0, inst=1): + global DATA + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + print("...............................................................................") + print(" Name: " + NAME + " - " + DESCRIPTION) + print("Program File: " + __file__) + print(" Version: " + VERSION) + print(" Copyright: " + COPYRIGHT) + print("...............................................................................") + + log("LXMF - Connecting ...", LOG_DEBUG) + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection(storage_path=path) + + LXMF_CONNECTION.register_message_notification_success_callback(lxmf_success) + LXMF_CONNECTION.register_message_notification_failed_callback(lxmf_failed) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + DATA = {} + destinations = dest.split(",") + for key in destinations: + DATA[key] = {} + DATA[key]["count"] = 0 + DATA[key]["count_success"] = 0 + DATA[key]["count_failed"] = 0 + DATA[key]["time"] = 0 + DATA[key]["time_min"] = 0 + DATA[key]["time_max"] = 0 + DATA[key]["time_avg"] = 0 + + count_current = 0 + while True: + if count == 0 or count_current < count: + count_current = count_current + 1 + letters = string.ascii_lowercase + content = ''.join(random.choice(letters) for i in range(size)) + for key in DATA: + DATA[key]["count"] = DATA[key]["count"] + 1 + content = "#"+ str(DATA[key]["count"]) + " " + content + content = content[:size] + if key == ".": + LXMF_CONNECTION.send(secrets.token_hex(nbytes=10), content, "") + else: + LXMF_CONNECTION.send(key, content, "") + print("Destination: " + str (key) + " | #: " + str(DATA[key]["count"]) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Sending/Queued") + time.sleep(interval) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + + parser.add_argument("-d", "--dest", action="store", required=True, type=str, default=None, help="Single destination hash or ,-separated list with destination hashs or . for random destination") + parser.add_argument("-t", "--time", action="store", type=float, default=1, help="Time between messages in seconds") + parser.add_argument("-s", "--size", action="store", type=int, default=128, help="Size (lenght) of the message content") + parser.add_argument("-c", "--count", action="store", type=float, default=0, help="Maximum message send count (0=no end)") + parser.add_argument("-i", "--inst", action="store", type=int, default=1, help="Parallel instances (different sender addresses)") + + params = parser.parse_args() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, dest=params.dest, interval=params.time, size=params.size, count=params.count, inst=params.inst) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lxmf_terminal/CHANGELOG.md b/lxmf_terminal/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/lxmf_terminal/README.md b/lxmf_terminal/README.md new file mode 100644 index 0000000..f93e9d8 --- /dev/null +++ b/lxmf_terminal/README.md @@ -0,0 +1,197 @@ +# lxmf_terminal +This program provides a complete terminal session on the server. Any commands can be executed on the target device. The communication is done by single LXMF messages. This offers the advantage that simple terminal commands can be used by any LXMF capable application. + +For more information, see the configuration options (at the end of the program files). Everything else is briefly documented there. After the first start this configuration will be created as default config in the corresponding file. + + +### Features +- Compatible with all LXMF applications (NomadNet, Sideband, ...) + + +## Examples of use + +### + +### General info how the messages are transported +All messages between client<->server are transported as single 1:1 messages in the LXMF/Reticulum network. +Accordingly, encryption takes place between these end points. +If a direct delivery of the message does not work, it is sent to a propagation node. There it is stored temporarily and can be retrieved by the client later. + +As these are normal LXMF messages, any LXMF capable application can be used to communicate with the group. + + +## Current Status +It should currently be considered beta software. All core features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. +The full documentation is not yet available. Due to lack of time I can also not say when this will be further processed. + + +## Screenshots / Usage examples + + + +## Installation manual + +### Install: +- Install all required prerequisites. (Default Reticulum installation. Only necessary if reticulum is not yet installed.) + ```bash + apt update + apt upgrade + + apt install python3-pip + + pip install pip --upgrade + reboot + + pip3 install rns + pip3 install pyserial netifaces + + pip3 install lxmf + ``` +- Change the Reticulum configuration to suit your needs and use-case. + ```bash + nano /.reticulum/config + ``` +- Download the [file](lxmf_terminal.py) from this repository. + ```bash + wget https://raw.githubusercontent.com/SebastianObi/LXMF-Tools/main/lxmf_terminal/lxmf_terminal.py + ``` +- Make it executable with the following command + ```bash + chmod +x lxmf_terminal.py + ``` + +### Start: +- Start it + ```bash + ./lxmf_terminal.py + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. +- Example minimal configuration (override of the default config `config.cfg`). These are the most relevant settings that need to be adjusted. All other settings are in `config.cfg` + ```bash + nano /root/.lxmf_terminal/config.cfg.owr + ``` + ```bash + ``` +- Start it again. Finished! + ```bash + ./lxmf_terminal.py + ``` + + +### Run as a system service/deamon: +- Create a service file. + ```bash + nano /etc/systemd/system/lxmf_terminal.service + ``` +- Copy and edit the following content to your own needs. + ```bash + [Unit] + Description=lxmf_terminal.py Daemon + After=multi-user.target + [Service] + # ExecStartPre=/bin/sleep 10 + Type=simple + Restart=always + RestartSec=3 + User=root + ExecStart=/root/lxmf_terminal.py + [Install] + WantedBy=multi-user.target + ``` +- Enable the service. + ```bash + systemctl enable lxmf_terminal + ``` +- Start the service. + ```bash + systemctl start lxmf_terminal + ``` + + +### Start/Stop service: + ```bash + systemctl start lxmf_terminal + systemctl stop lxmf_terminal + ``` + + +### Enable/Disable service: + ```bash + systemctl enable lxmf_terminal + systemctl disable lxmf_terminal + ``` + + +### Run several instances (To copy the same application): +- Run the program with a different configuration path. + ```bash + ./lxmf_terminal.py -p /root/.lxmf_terminal_2nd + ./lxmf_terminal.py -p /root/.lxmf_terminal_3nd + ``` +- After the first start edit the configuration file to suit your needs and use-case. The file location is displayed. + + +### First usage: +- With a manual start via the console, the own LXMF address is displayed: + ``` + [] ............................................................................... + [] LXMF - Address: <801f48d54bc71cb3e0886944832aaf8d> + [] ...............................................................................` + ``` +- This address is also annouced at startup in the default setting. +- Now the software can be used. + + +### Startup parameters: +```bash +usage: lxmf_terminal.py [-h] [-p PATH] [-pr PATH_RNS] [-pl PATH_LOG] [-l LOGLEVEL] [-s] [--exampleconfig] [--exampleconfigoverride] + +LXMF Terminal - + +optional arguments: + -h, --help show this help message and exit + -p PATH, --path PATH Path to alternative config directory + -pr PATH_RNS, --path_rns PATH_RNS + Path to alternative Reticulum config directory + -pl PATH_LOG, --path_log PATH_LOG + Path to alternative log directory + -l LOGLEVEL, --loglevel LOGLEVEL + -s, --service Running as a service and should log to file + --exampleconfig Print verbose configuration example to stdout and exit + --exampleconfigoverride + Print verbose configuration example to stdout and exit +``` + + +### Config/data files: +- config.cfg + + This is the default config file. + +- config.cfg.owr + + This is the user configuration file to override the default configuration file. + All settings made here have precedence. + This file can be used to clearly summarize all settings that deviate from the default. + This also has the advantage that all changed settings can be kept when updating the program. + + +## Configuration manual (Examples) +The configurations shown here are only a part of the total configuration. +It only serves to show the configuration that is necessary and adapted for the respective function. +All configurations must be made in the file `config.cfg.owr`. +All possible settings can be seen in the default configuration file `config.cfg`. + + +## Admin manual +This guide applies to all admins. Here are briefly explained the administative possibilities. + + +## User manual +This guide applies to users or admins. Here are briefly explained the normal possibilities of the software. + + +## FAQ + +### How do I start with the software? +You should read the `Installation manual` section. There everything is explained briefly. Just work through everything from top to bottom :) \ No newline at end of file diff --git a/lxmf_terminal/lxmf_terminal.py b/lxmf_terminal/lxmf_terminal.py new file mode 100755 index 0000000..2cf4cdd --- /dev/null +++ b/lxmf_terminal/lxmf_terminal.py @@ -0,0 +1,1337 @@ +#!/usr/bin/env python3 +############################################################################################################## +# +# Copyright (c) 2022 Sebastian Obele / obele.eu +# +# 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. +# +# This software uses the following software-parts: +# Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License +# +############################################################################################################## + + +############################################################################################################## +# Include + + +#### System #### +import sys +import os +import time +import argparse + +#### Config #### +import configparser + +#### JSON #### +import json +import pickle + +#### String #### +import string + +#### Regex #### +import re + +#### Process #### +import threading + +#### Terminal #### +import subprocess +import pty +import termios +import select +import struct +import fcntl +import shlex +import signal + +#### Reticulum, LXMF #### +# Install: pip3 install rns lxmf +# Source: https://markqvist.github.io +import RNS +import LXMF +import RNS.vendor.umsgpack as umsgpack + + +############################################################################################################## +# Globals + + +#### Global Variables - Configuration #### +NAME = "LXMF Terminal" +DESCRIPTION = "" +VERSION = "0.0.1 (2022-10-21)" +COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" +PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] +PATH_RNS = None + + + + +#### Global Variables - System (Not changeable) #### +CONFIG = None +RNS_CONNECTION = None +LXMF_CONNECTION = None +SESSION = {} +TERMINAL = None + + +############################################################################################################## +# Terminal Class + + +class terminal_class: + fd = None + pid = None + + + def size(self, rows, cols, xpix=0, ypix=0): + if self.fd: + size = struct.pack("HHHH", rows, cols, xpix, ypix) + fcntl.ioctl(self.fd, termios.TIOCSWINSZ, size) + + + def get(self, timeout=None, read_bytes=None): + if not timeout: + timeout = self.timeout + if not read_bytes: + read_bytes = self.read_bytes + if not self.fd: return ["", 0] + (data_ready, _, _) = select.select([self.fd], [], [], self.timeout) + if not data_ready: return ["", 0] + output = "" + state = 0 + try: + read_bytes = 1024 * read_bytes + output = os.read(self.fd, read_bytes).decode() + except Exception as e: + output = str(e) + state = e.errno + if e.errno == 5: self.stop() + return [output, state] + + + def set(self, cmd): + if not self.fd and not self.restart_session: return False + if not self.fd and self.restart_session: self.start() + if not self.fd: return False + try: + cmd = cmd.strip() + "\n" + os.write(self.fd, cmd.encode()) + except: + return False + return True + + + def start(self): + if not self.pid: + (pid, fd) = pty.fork() + if pid == 0: + cmd = [self.cmd] + shlex.split(self.cmd_args) + if self.path != "": os.chdir(self.path) + subprocess.run(cmd) + else: + self.fd = fd + self.pid = pid + self.size(self.fd, self.rows, self.cols) + + + def stop(self): + if self.fd: + fd = self.fd + self.fd = None + self.pid = None + try: + os.kill(fd, signal.SIGTERM) + except: + return False + return True + + + def __init__(self, path="", cmd="bash", cmd_args="", timeout=0, read_bytes=20, rows=100, cols=200, restart_session=True): + self.path = path + self.cmd = cmd + self.cmd_args = cmd_args + self.timeout = timeout + self.read_bytes = read_bytes + self.rows = rows + self.cols = cols + self.restart_session = restart_session + self.start() + return + + + def __del__(self): + self.stop() + return + + +############################################################################################################## +# LXMF Class + + +class lxmf_connection: + message_received_callback = None + message_notification_callback = None + message_notification_success_callback = None + message_notification_failed_callback = None + + + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + self.storage_path = storage_path + + self.identity_file = identity_file + + self.identity = identity + + self.destination_name = destination_name + self.destination_type = destination_type + self.aspect_filter = self.destination_name + "." + self.destination_type + + self.display_name = display_name + + self.send_delay = int(send_delay) + + if desired_method == "propagated" or desired_method == "PROPAGATED": + self.desired_method_direct = False + else: + self.desired_method_direct = True + self.propagation_node = propagation_node + self.try_propagation_on_fail = try_propagation_on_fail + + self.announce_startup = announce_startup + self.announce_startup_delay = int(announce_startup_delay) + + self.announce_periodic = announce_periodic + self.announce_periodic_interval = int(announce_periodic_interval) + + self.sync_startup = sync_startup + self.sync_startup_delay = int(sync_startup_delay) + self.sync_limit = int(sync_limit) + self.sync_periodic = sync_periodic + self.sync_periodic_interval = int(sync_periodic_interval) + + if not os.path.isdir(self.storage_path): + os.makedirs(self.storage_path) + log("LXMF - Storage path was created", LOG_NOTICE) + log("LXMF - Storage path: " + self.storage_path, LOG_INFO) + + if self.identity: + log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) + else: + if not self.identity_file: + self.identity_file = "identity" + self.identity_path = self.storage_path + "/" + self.identity_file + if os.path.isfile(self.identity_path): + try: + self.identity = RNS.Identity.from_file(self.identity_path) + if self.identity != None: + log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) + else: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + except Exception as e: + log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + else: + try: + log("LXMF - No Primary Identity file found, creating new...") + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + log("LXMF - Created new Primary Identity %s" % (str(self.identity))) + except Exception as e: + log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) + log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) + + self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) + + self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) + + self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) + + if self.display_name == "": + self.display_name = RNS.prettyhexrep(self.destination_hash()) + + self.destination.set_default_app_data(self.display_name.encode("utf-8")) + + self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) + + RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) + + log("LXMF - Identity: " + str(self.identity), LOG_INFO) + log("LXMF - Destination: " + str(self.destination), LOG_INFO) + log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) + + self.destination.set_link_established_callback(self.client_connected) + + self.autoselect_propagation_node() + + if self.announce_startup or self.announce_periodic: + self.announce(True) + + if self.sync_startup or self.sync_periodic: + self.sync(True) + + + def register_announce_callback(self, handler_function): + self.announce_callback = handler_function(self.aspect_filter) + RNS.Transport.register_announce_handler(self.announce_callback) + + + def register_message_received_callback(self, handler_function): + self.message_received_callback = handler_function + + + def register_message_notification_callback(self, handler_function): + self.message_notification_callback = handler_function + + + def register_message_notification_success_callback(self, handler_function): + self.message_notification_success_callback = handler_function + + + def register_message_notification_failed_callback(self, handler_function): + self.message_notification_failed_callback = handler_function + + + def destination_hash(self): + return self.destination.hash + + + def destination_hash_str(self): + return RNS.hexrep(self.destination.hash, False) + + + def destination_check(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return False + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return False + + return True + + + def destination_correct(self, destination): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + return "" + + try: + destination_bytes = bytes.fromhex(destination) + return destination + except Exception as e: + return "" + + return "" + + + def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): + if type(destination) is not bytes: + if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: + destination = destination[1:-1] + + if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Destination length is invalid", LOG_ERROR) + return + + try: + destination = bytes.fromhex(destination) + except Exception as e: + log("LXMF - Destination is invalid", LOG_ERROR) + return + + destination_identity = RNS.Identity.recall(destination) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) + + + def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): + if self.desired_method_direct: + desired_method = LXMF.LXMessage.DIRECT + else: + desired_method = LXMF.LXMessage.PROPAGATED + + message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) + + if fields is not None: + message.fields = fields + + if timestamp is not None: + message.timestamp = timestamp + + message.app_data = app_data + + self.message_method(message) + self.log_message(message, "LXMF - Message send") + + message.register_delivery_callback(self.message_notification) + message.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + message.try_propagation_on_fail = self.try_propagation_on_fail + + try: + self.message_router.handle_outbound(message) + time.sleep(self.send_delay) + except Exception as e: + log("LXMF - Could not send message " + str(message), LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + + def message_notification(self, message): + self.message_method(message) + + if self.message_notification_callback is not None: + self.message_notification_callback(message) + + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self.message_router.handle_outbound(message) + elif message.state == LXMF.LXMessage.FAILED: + self.log_message(message, "LXMF - Delivery receipt (failed)") + if self.message_notification_failed_callback is not None: + self.message_notification_failed_callback(message) + else: + self.log_message(message, "LXMF - Delivery receipt (success)") + if self.message_notification_success_callback is not None: + self.message_notification_success_callback(message) + + + def message_method(self, message): + if message.desired_method == LXMF.LXMessage.DIRECT: + message.desired_method_str = "direct" + elif message.desired_method == LXMF.LXMessage.PROPAGATED: + message.desired_method_str = "propagated" + + + def announce(self, initial=False): + announce_timer = None + + if self.announce_periodic and self.announce_periodic_interval > 0: + announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) + announce_timer.daemon = True + announce_timer.start() + + if initial: + if self.announce_startup: + if self.announce_startup_delay > 0: + if announce_timer is not None: + announce_timer.cancel() + announce_timer = threading.Timer(self.announce_startup_delay, self.announce) + announce_timer.daemon = True + announce_timer.start() + else: + self.announce_now() + return + + self.announce_now() + + + def announce_now(self, app_data=None): + if app_data: + self.destination.announce(app_data.encode("utf-8")) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) + else: + self.destination.announce() + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) + + + def sync(self, initial=False): + sync_timer = None + + if self.sync_periodic and self.sync_periodic_interval > 0: + sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) + sync_timer.daemon = True + sync_timer.start() + + if initial: + if self.sync_startup: + if self.sync_startup_delay > 0: + if sync_timer is not None: + sync_timer.cancel() + sync_timer = threading.Timer(self.sync_startup_delay, self.sync) + sync_timer.daemon = True + sync_timer.start() + else: + self.sync_now(self.sync_limit) + return + + self.sync_now(self.sync_limit) + + + def sync_now(self, limit=None): + if self.message_router.get_outbound_propagation_node() is not None: + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + return True + else: + return False + else: + return False + + + def autoselect_propagation_node(self): + if self.propagation_node is not None: + if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + else: + try: + propagation_hash = bytes.fromhex(self.propagation_node) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return + + node_identity = RNS.Identity.recall(propagation_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) + propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(propagation_hash) + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + + + def client_connected(self, link): + log("LXMF - Client connected " + str(link), LOG_EXTREME) + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(self.resource_concluded) + link.set_packet_callback(self.packet_received) + + + def packet_received(self, lxmf_bytes, packet): + log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) + self.process_lxmf_message_bytes(lxmf_bytes) + + + def resource_concluded(self, resource): + log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) + if resource.status == RNS.Resource.COMPLETE: + lxmf_bytes = resource.data.read() + self.process_lxmf_message_bytes(lxmf_bytes) + else: + log("LXMF - Received resource message is not complete", LOG_EXTREME) + + + def process_lxmf_message_bytes(self, lxmf_bytes): + try: + message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) + except Exception as e: + log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) + log("LXMF - The contained exception was: " + str(e), LOG_ERROR) + return + + message.desired_method = LXMF.LXMessage.DIRECT + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def process_lxmf_message_propagated(self, message): + message.desired_method = LXMF.LXMessage.PROPAGATED + + self.message_method(message) + self.log_message(message, "LXMF - Message received") + + if self.message_received_callback is not None: + log("LXMF - Call to registered message received callback", LOG_DEBUG) + self.message_received_callback(message) + else: + log("LXMF - No message received callback registered", LOG_DEBUG) + + + def log_message(self, message, message_tag="LXMF - Message log"): + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + else: + signature_string = "Signature is invalid, reason undetermined" + title = message.title.decode('utf-8') + content = message.content.decode('utf-8') + fields = message.fields + log(message_tag + ":", LOG_DEBUG) + log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) + log("- Title: " + title, LOG_DEBUG) + log("- Content: " + content, LOG_DEBUG) + log("- Fields: " + str(fields), LOG_DEBUG) + log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) + log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) + log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) + log("- Signature: " + signature_string, LOG_DEBUG) + log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) + if hasattr(message, "desired_method_str"): + log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) + else: + log("- Method: " + str(message.desired_method), LOG_DEBUG) + if hasattr(message, "app_data"): + log("- App Data: " + message.app_data, LOG_DEBUG) + + +############################################################################################################## +# LXMF Functions + + +#### LXMF - Announce #### +class lxmf_announce_callback: + def __init__(self, aspect_filter=None): + self.aspect_filter = aspect_filter + + + @staticmethod + def received_announce(destination_hash, announced_identity, app_data): + if app_data != None: + log("LXMF - Received an announce from " + RNS.prettyhexrep(destination_hash) + ": " + app_data.decode("utf-8"), LOG_INFO) + + + + +#### LXMF - Message #### +def lxmf_message_received_callback(message): + global SESSION + + if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) + return + + if CONFIG.has_option("allowed", "any") or CONFIG.has_option("allowed", "all") or CONFIG.has_option("allowed", "anybody") or CONFIG.has_option("allowed", RNS.hexrep(message.source_hash, False)) or CONFIG.has_option("allowed", RNS.prettyhexrep(message.source_hash)): + + if TERMINAL: + SESSION["source"] = message.source_hash + content = message.content.decode('utf-8') + + length = config_getint(CONFIG, "message", "receive_length_min", 0) + if length> 0: + if len(content) < length: + return + + length = config_getint(CONFIG, "message", "receive_length_max", 0) + if length > 0: + if len(content) > length: + return + + content_prefix = config_get(CONFIG, "message", "receive_prefix") + content_suffix = config_get(CONFIG, "message", "receive_suffix") + + search = config_get(CONFIG, "message", "receive_search") + if search != "": + content = content.replace(search, config_get(CONFIG, "message", "receive_replace")) + + search = config_get(CONFIG, "message", "receive_regex_search") + if search != "": + content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + + content = content_prefix + content + content_suffix + SESSION["cmd"] = content + TERMINAL.set(content) + output() + else: + log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not allowed", LOG_DEBUG) + return + + +############################################################################################################## +# Functions + + +def output(initial=False): + timer = threading.Timer(CONFIG.getint("terminal", "interval"), output) + timer.daemon = True + timer.start() + if initial: return + output_now() + + + + +def output_now(): + global SESSION + global TERMINAL + + if not "source" in SESSION: return True + + if not TERMINAL: return True + output, state = TERMINAL.get() + if output == "": return True + + log("Output - RAW:" + output, LOG_EXTREME) + + if CONFIG["message"].getboolean("replace_cmd"): + output = re.sub(r'\u001b\[\?2004h.*?#\s', '', output) + output = output.replace("\u001b[?2004l", "") + output = re.sub(r'.*@.*#', '', output) + if output.startswith(SESSION["cmd"]): + output = output.replace(SESSION["cmd"], "", 1) + output = re.sub(r'^\s+|\s+$', '', output) + if output.startswith(SESSION["cmd"]): + output = output.replace(SESSION["cmd"], "", 1) + output = re.sub(r'^\s+|\s+$', '', output) + + if CONFIG["message"].getboolean("replace_ansi"): + ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') + output = ansi_escape.sub('', output) + + if CONFIG["message"].getboolean("replace_log"): + output = re.sub(r'[\[\(]\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[\]\)]', '', output) + + if CONFIG["message"].getboolean("replace_whitespace"): + output = re.sub(r'(\r)+(\n)+', '\n', output) + output = re.sub(r'(\n)+((\s+)?\r)+', '\n', output) + output = re.sub(r'((\s+)?\r)+', '\n', output) + output = re.sub(r'^\s+|\s+$', '', output) + output = re.sub(r'^ +| +$', '', output, flags=re.M) + + if CONFIG["message"].getboolean("replace_double_lines"): + output = re.sub(r'^(.*)(\r?\n\1)+$', r'\1', output, flags=re.M) + + if CONFIG["message"].getboolean("replace_special_characters"): + output = output.replace(" :", ":") + output = output.replace("?:", "?") + + if CONFIG["message"].getboolean("replace_unnecessary_characters"): + output = re.sub(r'^[\-\_]+|[\-\_]+$', '', output) + output = re.sub(r'^[^a-zA-Z0-9]$', '', output, flags=re.M) + output = re.sub(r'^#\?$', '', output, flags=re.M) + output = re.sub(r'\?\s?:', '', output) + + if CONFIG["message"].getboolean("replace_whitespace"): + output = re.sub(r'^\s+|\s+$', '', output) + output = re.sub(r'^ +| +$', '', output, flags=re.M) + + if output != "": + length = config_getint(CONFIG, "message", "send_length_min", 0) + if length> 0: + if len(output) < length: + return True + + length = config_getint(CONFIG, "message", "send_length_max", 0) + if length > 0: + if len(output) > length: + return True + + content_prefix = config_get(CONFIG, "message", "send_prefix") + content_suffix = config_get(CONFIG, "message", "send_suffix") + + search = config_get(CONFIG, "message", "send_search") + if search != "": + output = output.replace(search, config_get(CONFIG, "message", "send_replace")) + + search = config_get(CONFIG, "message", "send_regex_search") + if search != "": + output = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), output) + + LXMF_CONNECTION.send(SESSION["source"], content_prefix + output + content_suffix, "") + + return True + + +############################################################################################################## +# Config + + +#### Config - Get ##### +def config_get(config, section, key, default="", lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section][key+lng_key] + elif config.has_option(section, key): + return config[section][key] + return default + + +def config_getint(config, section, key, default=0, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config.getint(section, key+lng_key) + elif config.has_option(section, key): + return config.getint(section, key) + return default + + +def config_getboolean(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return config[section].getboolean(key+lng_key) + elif config.has_option(section, key): + return config[section].getboolean(key) + return default + + +def config_getsection(config, section, default="", lng_key=""): + if not config or section == "": return default + if not config.has_section(section): return default + if config.has_section(section+lng_key): + return key+lng_key + elif config.has_section(section): + return key + return default + + +def config_getoption(config, section, key, default=False, lng_key=""): + if not config or section == "" or key == "": return default + if not config.has_section(section): return default + if config.has_option(section, key+lng_key): + return key+lng_key + elif config.has_option(section, key): + return key + return default + + + + +#### Config - Read ##### +def config_read(file=None, file_override=None): + global CONFIG + + if file is None: + return False + else: + CONFIG = configparser.ConfigParser(allow_no_value=True, inline_comment_prefixes="#") + CONFIG.sections() + if os.path.isfile(file): + try: + if file_override is None: + CONFIG.read(file, encoding='utf-8') + elif os.path.isfile(file_override): + CONFIG.read([file, file_override], encoding='utf-8') + else: + CONFIG.read(file, encoding='utf-8') + except Exception as e: + return False + else: + if not config_default(file=file, file_override=file_override): + return False + return True + + + + +#### Config - Save ##### +def config_save(file=None): + global CONFIG + + if file is None: + return False + else: + if os.path.isfile(file): + try: + with open(file,"w") as file: + CONFIG.write(file) + except Exception as e: + return False + else: + return False + return True + + + + +#### Config - Default ##### +def config_default(file=None, file_override=None): + global CONFIG + + if file is None: + return False + elif DEFAULT_CONFIG != "": + if file_override and DEFAULT_CONFIG_OVERRIDE != "": + if not os.path.isdir(os.path.dirname(file_override)): + try: + os.makedirs(os.path.dirname(file_override)) + except Exception: + return False + if not os.path.exists(file_override): + try: + config_file = open(file_override, "w") + config_file.write(DEFAULT_CONFIG_OVERRIDE) + config_file.close() + except: + return False + + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + config_file = open(file, "w") + config_file.write(DEFAULT_CONFIG) + config_file.close() + if not config_read(file=file, file_override=file_override): + return False + except: + return False + else: + return False + + if not CONFIG.has_section("main"): CONFIG.add_section("main") + CONFIG["main"]["default_config"] = "True" + return True + + +############################################################################################################## +# Value convert + + +def val_to_bool(val): + if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": + return True + elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": + return False + elif val != "": + return True + else: + return False + + +############################################################################################################## +# Log + + +LOG_FORCE = -1 +LOG_CRITICAL = 0 +LOG_ERROR = 1 +LOG_WARNING = 2 +LOG_NOTICE = 3 +LOG_INFO = 4 +LOG_VERBOSE = 5 +LOG_DEBUG = 6 +LOG_EXTREME = 7 + +LOG_LEVEL = LOG_NOTICE +LOG_LEVEL_SERVICE = LOG_NOTICE +LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" +LOG_MAXSIZE = 5*1024*1024 +LOG_PREFIX = "" +LOG_SUFFIX = "" +LOG_FILE = "" + + + + +def log(text, level=3, file=None): + if not LOG_LEVEL: + return + + if LOG_LEVEL >= level: + name = "Unknown" + if (level == LOG_FORCE): + name = "" + if (level == LOG_CRITICAL): + name = "Critical" + if (level == LOG_ERROR): + name = "Error" + if (level == LOG_WARNING): + name = "Warning" + if (level == LOG_NOTICE): + name = "Notice" + if (level == LOG_INFO): + name = "Info" + if (level == LOG_VERBOSE): + name = "Verbose" + if (level == LOG_DEBUG): + name = "Debug" + if (level == LOG_EXTREME): + name = "Extra" + + if not isinstance(text, str): + text = str(text) + + text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX + + if file == None and LOG_FILE != "": + file = LOG_FILE + + if file == None: + print(text) + else: + try: + file_handle = open(file, "a") + file_handle.write(text + "\n") + file_handle.close() + + if os.path.getsize(file) > LOG_MAXSIZE: + file_prev = file + ".1" + if os.path.isfile(file_prev): + os.unlink(file_prev) + os.rename(file, file_prev) + except: + return + + +############################################################################################################## +# System + + +#### Panic ##### +def panic(): + sys.exit(255) + + +#### Exit ##### +def exit(): + sys.exit(0) + + +############################################################################################################## +# Setup/Start + + +#### Setup ##### +def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False): + global PATH + global PATH_RNS + global LOG_LEVEL + global LOG_FILE + global RNS_CONNECTION + global LXMF_CONNECTION + global TERMINAL + + if path is not None: + if path.endswith("/"): + path = path[:-1] + PATH = path + + if path_rns is not None: + if path_rns.endswith("/"): + path_rns = path_rns[:-1] + PATH_RNS = path_rns + + if loglevel is not None: + LOG_LEVEL = loglevel + rns_loglevel = loglevel + else: + rns_loglevel = None + + if service: + LOG_LEVEL = LOG_LEVEL_SERVICE + if path_log is not None: + if path_log.endswith("/"): + path_log = path_log[:-1] + LOG_FILE = path_log + else: + LOG_FILE = PATH + LOG_FILE = LOG_FILE + "/" + NAME + ".log" + rns_loglevel = None + + if not config_read(PATH + "/config.cfg", PATH + "/config.cfg.owr"): + print("Config - Error reading config file " + PATH + "/config.cfg") + panic() + + if CONFIG["main"].getboolean("default_config"): + print("Exit!") + print("First start with the default config!") + print("You should probably edit the config file \"" + PATH + "/config.cfg\" to suit your needs and use-case!") + print("You should make all your changes at the user configuration file \"" + PATH + "/config.cfg.owr\" to override the default configuration file!") + print("Then restart this program again!") + exit() + + if not CONFIG["main"].getboolean("enabled"): + print("Disabled in config file. Exit!") + exit() + + RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) + + log("...............................................................................", LOG_INFO) + log(" Name: " + CONFIG["main"]["name"], LOG_INFO) + log("Program File: " + __file__, LOG_INFO) + log(" Config File: " + PATH + "/config", LOG_INFO) + log(" Version: " + VERSION, LOG_INFO) + log(" Copyright: " + COPYRIGHT, LOG_INFO) + log("...............................................................................", LOG_INFO) + + log("LXMF - Connecting ...", LOG_DEBUG) + + if CONFIG.has_option("lxmf", "propagation_node"): + config_propagation_node = CONFIG["lxmf"]["propagation_node"] + else: + config_propagation_node = None + + if path is None: + path = PATH + + LXMF_CONNECTION = lxmf_connection( + storage_path=path, + destination_name=CONFIG["lxmf"]["destination_name"], + destination_type=CONFIG["lxmf"]["destination_type"], + display_name=CONFIG["lxmf"]["display_name"], + send_delay=CONFIG["lxmf"]["send_delay"], + desired_method=CONFIG["lxmf"]["desired_method"], + propagation_node=config_propagation_node, + try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), + announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), + announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], + announce_periodic=CONFIG["lxmf"].getboolean("announce_periodic"), + announce_periodic_interval=CONFIG["lxmf"]["announce_periodic_interval"], + sync_startup=CONFIG["lxmf"].getboolean("sync_startup"), + sync_startup_delay=CONFIG["lxmf"]["sync_startup_delay"], + sync_limit=CONFIG["lxmf"]["sync_limit"], + sync_periodic=CONFIG["lxmf"].getboolean("sync_periodic"), + sync_periodic_interval=CONFIG["lxmf"]["sync_periodic_interval"]) + + LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) + LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + + log("LXMF - Connected", LOG_DEBUG) + + log("...............................................................................", LOG_FORCE) + log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) + log("...............................................................................", LOG_FORCE) + + log("Terminal - Connecting ...", LOG_DEBUG) + TERMINAL = terminal_class(CONFIG["terminal"]["path"], CONFIG["terminal"]["cmd"], CONFIG["terminal"]["cmd_args"], CONFIG.getint("terminal", "timeout"), CONFIG.getint("terminal", "read_bytes"), CONFIG.getint("terminal", "size_rows"), CONFIG.getint("terminal", "size_cols"), CONFIG["terminal"].getboolean("restart_session")) + output(True) + log("Terminal - Connected", LOG_DEBUG) + while True: + time.sleep(1) + + + + +#### Start #### +def main(): + try: + description = NAME + " - " + DESCRIPTION + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") + parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") + parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") + parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) + parser.add_argument("-s", "--service", action="store_true", default=False, help="Running as a service and should log to file") + parser.add_argument("--exampleconfig", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + parser.add_argument("--exampleconfigoverride", action="store_true", default=False, help="Print verbose configuration example to stdout and exit") + + params = parser.parse_args() + + if params.exampleconfig: + print("Config File: " + PATH + "/config.cfg") + print("Content:") + print(DEFAULT_CONFIG) + exit() + + if params.exampleconfigoverride: + print("Config Override File: " + PATH + "/config.cfg.owr") + print("Content:") + print(DEFAULT_CONFIG_OVERRIDE) + exit() + + setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, service=params.service) + + except KeyboardInterrupt: + print("Terminated by CTRL-C") + exit() + + +############################################################################################################## +# Files + + +#### Default configuration override file #### +DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override the default configuration file. +# All settings made here have precedence. +# This file can be used to clearly summarize all settings that deviate from the default. +# This also has the advantage that all changed settings can be kept when updating the program. +''' + + +#### Default configuration file #### +DEFAULT_CONFIG = '''# This is the default config file. +# You should probably edit it to suit your needs and use-case. + + + + +#### Main program settings #### +[main] + +enabled = True + +# Name of the program. Only for display in the log or program startup. +name = Terminal + + + + +#### LXMF connection settings #### +[lxmf] + +# Destination name & type need to fits the LXMF protocoll +# to be compatibel with other LXMF programs. +destination_name = lxmf +destination_type = delivery + +# The name will be visible to other peers +# on the network, and included in announces. +display_name = CMD + +# Default send method. +desired_method = direct #direct/propagated + +# Propagation node address/hash. +#propagation_node = + +# Try to deliver a message via the LXMF propagation network, +# if a direct delivery to the recipient is not possible. +try_propagation_on_fail = No + +# The peer is announced at startup +# to let other peers reach it immediately. +announce_startup = No +announce_startup_delay = 0 #Seconds + +# The peer is announced periodically +# to let other peers reach it. +announce_periodic = No +announce_periodic_interval = 360 #Minutes + +# Some waiting time after message send +# for LXMF/Reticulum processing. +send_delay = 0 #Seconds + +# Sync LXMF messages at startup. +sync_startup = No +sync_startup_delay = 0 #Seconds + +# Sync LXMF messages periodically. +sync_periodic = No + +# The sync interval in minutes. +sync_periodic_interval = 360 #Minutes + +# Automatic LXMF syncs will only +# download x messages at a time. You can change +# this number, or set the option to 0 to disable +# the limit, and download everything every time. +sync_limit = 8 + +# Allow only messages with valid signature. +signature_validated = Yes + + + + +#### Terminal settings #### +[terminal] + +path = /tmp +cmd = bash +cmd_args = +timeout = 0 +read_bytes = 20 +size_rows = 100 +size_cols = 200 +restart_session = True +interval = 5 #Seconds + + + + +#### Message settings #### +[message] + +# Text is added. +receive_prefix = +receive_suffix = + +# Text is replaced. +receive_search = +receive_replace = + +# Text is replaced by regular expression. +receive_regex_search = +receive_regex_replace = + +# Length limitation. +receive_length_min = 0 #0=any length +receive_length_max = 0 #0=any length + + +# Text is added. +send_prefix = +send_suffix = + +# Text is replaced. +send_search = +send_replace = + +# Text is replaced by regular expression. +send_regex_search = +send_regex_replace = + +# Length limitation. +send_length_min = 0 #0=any length +send_length_max = 0 #0=any length + + +replace_cmd = False +replace_ansi = True +replace_log = True +replace_whitespace = True +replace_double_lines = True +replace_special_characters = True +replace_unnecessary_characters = True + + + + +#### Right settings #### +# Allow only specific source addresses/hashs or any. +[allowed] + +#any +#2858b7a096899116cd529559cc679ffe +''' + + +############################################################################################################## +# Init + + +if __name__ == "__main__": + main() \ No newline at end of file