Telegram instant messaging TelegramBot - Empfangen und Senden per FHEM

Begonnen von viegener, 20 Juni 2015, 18:59:41

Vorheriges Thema - Nächstes Thema

viegener

Zitat von: raimundl am 11 Dezember 2016, 17:40:43
Hallo!

Tolle neue Funktion (mit mehreren Keys) - funktioniert!

Habe aber nachstehen Meldung:

PollingLastError
Callback returned no valid JSON: malformed JSON string, neither array, object, number, string or atom, at character offset 0 (before "<html>\r\n<head><tit...") at ./FHEM/50_TelegramBot.pm line 1746

Danke und LG

Die Meldung verheisst erstmal, dass Telegram mal ein HTML (vermutlich server überlastet o.ä.) statt einem wirklichen Resultat zurückgeliefert hat. Die Meldung kommt immer mal wieder vor, wenn die Server mal kurzzeitig nicht erreichbar oder überlastet sind. Das stammt aber aus der Abfragen nach neuen Nachrichten (Polling) und nicht aus dem Senden des keyboards.
Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

igami

Wo wir schon bei neuen Features sind. Direkt SVGs zu senden wäre auch cool, verwende da momentan auch eine eigene sub

my $TBD_TelegramBot = "TelegramBot";
my $TBD_hash = $defs{$TBD_TelegramBot};
my $TBD_me = (split(":", InternalVal($TBD_TelegramBot, "me", "0:$TBD_TelegramBot:0")))[1];

sub TBD_SVG($) {
  my ($SVG) = @_;

  TelegramBot_ExecuteCommand($TBD_hash, TBD_peer(), "{plotAsPng(\"$SVG\")}");

  return;
}

Man könnte das ja als

set aTelegramBotDevice SVG @@someusername <SVG name> [<caption>]

implementieren ;)
Pi3 mit fhem.cfg + DbLog/logProxy
Komm vorbei zum FHEM Treffen im Kreis Gütersloh! Das nächste Mal im April 2020.

MAINTAINER: archetype, LuftdatenInfo, monitoring, msgDialog, Nmap, powerMap
ToDo: AVScene, FluxLED

Muschelpuster

Zitat von: igami am 12 Dezember 2016, 06:13:00
Wo wir schon bei neuen Features sind. Direkt SVGs zu senden wäre auch cool, verwende da momentan auch eine eigene sub
:
:
Man könnte das ja als

set aTelegramBotDevice SVG @@someusername <SVG name> [<caption>]

implementieren ;)
Ja, wäre cool - ich habe die Sub direkt im Befehl drin:{fhem "set telegramBot message \@someusername some text" ;; TelegramBot_ExecuteCommand($defs{"telegramBot"}, someusername, '{plotAsPng("svg_name")}');; return;;})Toll, aber ohne Forenhilfe hätte ich das nicht hin bekommen  ;)

beschränkte Grüße
Niels
fhem @ ZBOX mit 1,6MHz Celeron, 4GB RAM & 120GB SSD mit Debian Bullseye # MiLight # Homematic via CCU3 # W&T WebIO # Rademacher DuoFern # ESPeasy # logdb@mysql # configdb@mysql # Shelly @ MQTT2 # go-eCharger mit PV-Überschussladung via DOIF

Prof. Dr. Peter Henning

Mein Tipp für die Umwandliung von SVG in PNG bei wirklich hoher Qualität: Inkscape mit entsprechenden Kommandozeilenoptionen verwenden.

Einen netten Use-Case für Telegram kann ich in diesem Thread auch noch beisteuern: Verwalten einer digitalen Einkaufsliste mit Telegram, siehe hier: https://wiki.fhem.de/wiki/Modul_PostMe

LG

pah

dergeberl

#1024
Hallo zusammen,

ich habe mir gestern mal das Thema InlineKeyboards angeschaut.
Gleich eins vorweg: ich bin kein Perl Entwickler und mein Code ist demensprechend wahrscheinlich nicht der schönste.  ;D

Mein Ziel war folgendes:
- Versenden von InlineKeyboards mit Callback Events bei dem klick des Buttons
- Auswerten der Callback Events
- Beantworten der Callback Events mit und ohne Benachrichtigung (wird in dem Chat oben angezeigt)

Versenden von Nachrichten mit InlineKeyboards:
Die InlineKeyboards werden beim Nachrichten Versand als JSON mit an die API gesendet. Für jede Zeile gibt es ein eigenes Array. Für jeden Botton wird ein Text und ein Feld Callbackdata benötigt. Dafür habe ich mir folgendes überlegt.
ButtonText und CallbackData werden mit Minus getrennt und Buttons werden mit Doppelpunkten getrennt:
"Button Name-Callback_data:Button Name2- Callback_data2"
Mehrere Zeilen werden mit Kommas getrennt:
"Button Name-Callback_data:Button Name2- Callback_data2,Button Name Zeil2-Callback_data3 "

Hierfür habe ich eine Funktion in der myUtils angelegt:

sub setTelegramInlineKeyboard($$$)
{
my ($to, $msg, $btns) = @_;
my @lines = split(/\,/,$btns);
my $btnjson = '{"inline_keyboard":[';
my $linefirst = 1;
my $data;
my $text;
foreach my $line (@lines){
if($linefirst eq 0){
$btnjson = $btnjson.',[' ;
}else{
$linefirst = 0;
$btnjson = $btnjson.'[' ;
}
my @btn = split(/\:/,$line);
my $btnfirst = 1;
foreach (@btn){
my @btndata = split(/\-/,$_);
$text =  $btndata[0];
if ($btndata[1] ne ''){
$data = $btndata[1];
}else{
$data = $btndata[0];
}
if($btnfirst eq 0){
$btnjson = $btnjson.',{"text":"'.$text.'","callback_data":"'.$data.'"}' ;
}else{
$btnfirst = 0;
$btnjson = $btnjson.'{"text":"'.$text.'","callback_data":"'.$data.'"}' ;
}

}
$btnjson = $btnjson.']' ;
}
$btnjson = $btnjson.']}';

    my $param = {
                    url        => "https://api.telegram.org/bot--APIKEY--/sendMessage",
                    timeout    => 5,
                    method     => "POST",                                                                                 
                    header     => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", 
                };
    $param->{data}{text} = $msg;                                                       
$param->{data}{chat_id} = $to;
$param->{data}{reply_markup} = $btnjson;
    my $return = HttpUtils_BlockingGet($param);                                                                                   
}


Nun wird bei einem Klick auf den Button wird nun ein Callback ausgelost. Dieser wird von dem TelegramBot allerdings nicht ausgewertet (zumindest nicht angezeigt)
Deshalb habe ich in dem Modul (50_TelegramBot.pm) folgendes hinzugefügt.
In der Funktion "TelegramBot_Callback" innerhalb des "foreach my $update ( @$result ) {" habe ich folgednes eingefügt (in meinem Fall war das Zeile 1863):


if ( defined( $update->{callback_query} ) ) {
          $ret = TelegramBot_Parsecallback( $hash, $update->{update_id}, $update->{callback_query} );
}



Desweiteren habe ich eine die Funktion "TelegramBot_Parsecallback" hinzugefügt:


sub TelegramBot_Parsecallback($$$)
{
my ( $hash, $uid, $callback ) = @_;
my $callbackID = $callback->{id};
my $from = $callback->{from};
my $mpeer = $from->{id};
my $data = $callback->{data};
 
    readingsBeginUpdate($hash);
    readingsBulkUpdate($hash, "callbackID", $callbackID);
readingsBulkUpdate($hash, "callbackpeerID", $mpeer);
    readingsBulkUpdate($hash, "callData", $data );       
    readingsEndUpdate($hash, 1);
}

Nun stehen die Infos aus dem aktuellen bzw. letzten Callback im TelegramBot als Reading.

Ein Callback muss beantwortet werden sonst erscheint ein Ewigkeit Ladebalken nach dem Klick auf den Botton. Des Weiteren gibt es hier die Möglichkeit noch eine kleine Popup Nachricht zu senden (z.B. "Licht angeschaltet")
Hier habe ich wieder eine Funktion in myUtils angelegt:

sub answerCallback($$)
{
my ($callbackid, $msg) = @_;

    my $param = {
                    url        => "https://api.telegram.org/bot--APIKEY--/answerCallbackQuery",
                    timeout    => 5,
                    method     => "POST",                                                                                 
                    header     => "agent: TeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json", 
                };
$param->{data}{callback_query_id} = $callbackid;
$param->{data}{text} = $msg;                                                       
    my $return = HttpUtils_BlockingGet($param);                                                                                   
}


Für Die Logik habe ich ein Notify angelegt welches die CallBackDatas auswertet (hier ein kleiner Auszug):
In dem einen if muss die eigene Telegram ID hinterlegt werden um eine Prüfung des Absenders durchzuführen.

Telegram:callbackID:.* { 
my $callbackid = ReadingsVal("Telegram", "callbackID", 0);
my $sender = ReadingsVal("Telegram", "callbackpeerID", 0);
my $calldata = ReadingsVal("Telegram", "callData", "");
my $callantwort;
  if ($sender eq "--meine Telegram ID--"){
if ($calldata =~ /hauptmenue/) {
{setTelegramInlineKeyboard($sender, "Ich kann folgendes für dich tun:", "Heizung-heizung:Licht-licht:Rolladen-rolladen,Müll-muell:Temperatur-temperatur:Fenster-fenster")}
}elsif ($calldata =~ /licht/) {
if($calldata =~ /wohnzimmer_schrank_an/){
fhem("set Wohnzimmer_LED_Schrank on");
$callantwort="Wohnzimmer LED Spot angeschalten";
}elsif($calldata =~ /wohnzimmer_schrank_aus/){
fhem("set Wohnzimmer_LED_Schrank off");
$callantwort="Wohnzimmer LED Spot ausgeschalten";
}elsif($calldata =~ /wohnzimmer_led_an/){
fhem("set Wohnzimmer_LED on");
$callantwort="Wohnzimmer LED Streifen angeschalten";
}elsif($calldata =~ /wohnzimmer_led_aus/){
fhem("set Wohnzimmer_LED off");
$callantwort="Wohnzimmer LED Streifen ausgeschalten";
}else{
{setTelegramInlineKeyboard($sender, "Lichtsteuerung", "Wohnzimmer Schrank aus-licht_wohnzimmer_schrank_aus:Wohnzimmer Schrank an-licht_wohnzimmer_schrank_an,Wohnzimmer LED aus-licht_wohnzimmer_led_aus:Wohnzimmer LED an-licht_wohnzimmer_led_an,Hauptmenü-hauptmenue")}
}
}elsif ($calldata =~ /rolladen/) {
if($calldata =~ /rolladen_schlafzimmer_hoch/){
fhem("set Schlafzimmer_Rolladen on");
$callantwort="Rolladen Schlafzimmer hoch";
}elsif($calldata =~ /rolladen_schlafzimmer_runter/){
fhem("set Schlafzimmer_Rolladen off");
$callantwort="Rolladen Schlafzimmer runter";
}elsif($calldata =~ /rolladen_esszimmer_hoch/){
fhem("set Esszimmer_Rolladen on");
$callantwort="Rolladen Esszimmer hoch";
}elsif($calldata =~ /rolladen_esszimmer_runter/){
fhem("set Esszimmer_Rolladen off");
$callantwort="Rolladen Esszimmer runter";
}elsif($calldata =~ /rolladen_buero_hoch/){
fhem("set Buero_Rolladen on");
$callantwort="Rolladen Büro hoch";
}elsif($calldata =~ /rolladen_buero_runter/){
fhem("set Buero_Rolladen off");
$callantwort="Rolladen Büro runter";
}else{
{setTelegramInlineKeyboard($sender, "Rolladensteuerung", "Schlafzimmer hoch-rolladen_schlafzimmer_hoch:Schlafzimmer runter-rolladen_schlafzimmer_runter,Esszimmer hoch-rolladen_esszimmer_hoch:Esszimmer runter-rolladen_esszimmer_runter,Büro hoch-rolladen_buero_hoch:Büro runter-rolladen_buero_runter,Hauptmenü-hauptmenue")}
}
}

{answerCallback($callbackid, $callantwort)}
}
}


Anbei noch ein paar Bilder.

Ich hoffe dass es einigermaßen verständlich ist und ich jemanden damit helfen konnte.
Bitte verschlagt mich nicht für meinen Chaos Perl Code.  :o 8)

Viele Grüße
Max

Edit: Prüfung des Absenders des Callbacks.

MadMax-FHEM

@dergeberl:

Hi Max,

klasse!

Perl code hab ich mir noch nicht angeschaut...
...aber meiner wäre wohl auch nicht "schöner"... ;-)

Wird Zeit, dass Weihnachten kommt...
...dann habe ich endlich mal Zeit meine Liste mit allem was ich schon immer ausprobieren/umsetzen wolte "abzuarbeiten" ;-)

Danke, Joachim
FHEM PI3B+ Bullseye: HM-CFG-USB, 40x HM, ZWave-USB, 13x ZWave, EnOcean-PI, 15x EnOcean, HUE/deCONZ, CO2, ESP-Multisensor, Shelly, alexa-fhem, ...
FHEM PI2 Buster: HM-CFG-USB, 25x HM, ZWave-USB, 4x ZWave, EnOcean-PI, 3x EnOcean, Shelly, ha-bridge, ...
FHEM PI3 Buster (Test)

viegener

@dergeberl - Reife Leistung - keine Frage! Ich habe bisher immer von den inlinequeries zurückgeschreckt, weil ich das Problem mit der Reaktion vom Telegrambot unschön finde. Es muss halt eine Reaktion erfolgen sonst ist der Telegram client eingefroren.

Wenn es aber Interesse gibt, kann ich Teile Deines Codes übernehmen?
Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

viegener

@dergeberl: Achtung: Du hast ein offenes Scheunentor in Deinen Bot eingebaut - Soweit ich das auf die Schnelle sehen kann findet keinerlei Prüfung statt wer gerade Dein Hausfernsteuert - Das erscheint mir nicht ganz so smart.

Bitte übernehmt den Code so nicht!
Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

dergeberl


@viegner
Vielen Dank für die Blumen.
So wie ich das verstanden habe werden die Callbacks nach einer Weile automatisch gelöscht.
Ich glaube es erscheint lediglich ein Ladebalken (bzw. Ladekreis) in dem Button der geklickt wurde. Muss ich mal testen ob der einfriert.

Klar nimm was du brauchst :)

Also schon einmal sehr interessant wäre die Ausgabe der Callback Infos in den readings.

Grüße
Max

dergeberl

Zitat von: viegener am 14 Dezember 2016, 23:13:38
@dergeberl - Reife Leistung - keine Frage! Ich habe bisher immer von den inlinequeries zurückgeschreckt, weil ich das Problem mit der Reaktion vom Telegrambot unschön finde. Es muss halt eine Reaktion erfolgen sonst ist der Telegram client eingefroren.

Wenn es aber Interesse gibt, kann ich Teile Deines Codes übernehmen?
@viegener
Schande über mein Haupt...
Vielen Dank für Deinen Hinweis.
Ich habe oben noch eine If Abfrage eingefügt welche die Absender ID prüft.

Viele Grüße
Max


Gesendet von meinem VIE-L09 mit Tapatalk


Prof. Dr. Peter Henning

Aus meiner Sicht ist das immer noch sehr unsicher. Insbesondere müsste überprüft werden, ob ein Callback tatsächlich von demjenigen kommt, der vorher ein entsprechendes Keyboard erhalten hat. Da das ggf. mehrere Benutzer des Smarthome sein können, muss der TelegramBot darüber Buch führen.

Zweitens sollte aus Sicherheitsgründen eine Protokollierung erfolgen: Wer hat von außen wann welche Aktionen im Smarthome ausführen lassen.

Drittens ist natürlich für eine Quick-and-dirty Lösung die Angabe eines Keyboards in einem String sehr gut. Für eine generische Lösung allerdings halte ich das auch für ungeeignet. Stattdessen sollten die jeweiligen Keyboards in entsprechenden (kleinen) Konfigurationsdateien liegen und bei Bedarf eingelesen werden. Dadurch kann man sie pflegen, ohne jeweils das myUtils.pm neu einzulesen.

LG

pah

viegener

Zitat von: Prof. Dr. Peter Henning am 15 Dezember 2016, 03:58:28
Aus meiner Sicht ist das immer noch sehr unsicher. Insbesondere müsste überprüft werden, ob ein Callback tatsächlich von demjenigen kommt, der vorher ein entsprechendes Keyboard erhalten hat. Da das ggf. mehrere Benutzer des Smarthome sein können, muss der TelegramBot darüber Buch führen.

Zweitens sollte aus Sicherheitsgründen eine Protokollierung erfolgen: Wer hat von außen wann welche Aktionen im Smarthome ausführen lassen.

Drittens ist natürlich für eine Quick-and-dirty Lösung die Angabe eines Keyboards in einem String sehr gut. Für eine generische Lösung allerdings halte ich das auch für ungeeignet. Stattdessen sollten die jeweiligen Keyboards in entsprechenden (kleinen) Konfigurationsdateien liegen und bei Bedarf eingelesen werden. Dadurch kann man sie pflegen, ohne jeweils das myUtils.pm neu einzulesen.

LG

pah

Eine Protokollierung ist auf jeden Fall sehr wichtig für alle Einfallstore, sollte heute über die Readings-Filelog möglich sein.

Vielleicht hier auch nochmal ein grundsätzlicher "Disclaimer" zur Sicherheit:

Der Code vom TelegramBot ist nicht auf Sicherheit überprüft und ich bin auch kein Sicherheitsexperte. Generell ist die Öffnung eines Zuganges für die Ausführung von Kommandos über den TelegramBot inhärent ein Unsicherheitsfaktor. Die Absicherung sollte auf jeden Fall über die Mechanismen des Moduls beschränkt werden.

Alle Mechanismen setzen aber voraus, dass es in Telegram nicht möglich ist die ID des Absenders nicht gefälscht werden kann!

Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

dergeberl

Zitat von: Prof. Dr. Peter Henning am 15 Dezember 2016, 03:58:28
Aus meiner Sicht ist das immer noch sehr unsicher. Insbesondere müsste überprüft werden, ob ein Callback tatsächlich von demjenigen kommt, der vorher ein entsprechendes Keyboard erhalten hat. Da das ggf. mehrere Benutzer des Smarthome sein können, muss der TelegramBot darüber Buch führen.

So wie ich die Callbacks verstanden (korrigiert mich) können die nur gesendet werden wenn man auch die Buttons erhalten hat. Wie @viegener gehe ich außerdem davon aus das ein Spoofing von BenutzerIDs nicht möglich ist :)
Desweiteren wäre hier auch interessant die Auswertung (für die erlaubten Kontakte) über das Modul zu machen, dass readings nur gesetzt werden wenn der Absender in den Kontakten ist. Hier fehlen mir allerdings Perl Kenntnisse.

Die Idee die Buttons über Config files zu erstellen finde ich nicht schlecht. Wobei die Buttons ja über die Funktion erzeugt werden und somit nicht in der myUtils sondern in dem Notify.

Für mich ist es definitiv weiterhin ein spannendes Thema.

viele Grüße
Max

viegener

Ich habe in github jetzt nochmal eine neue Version hochgeladen, in der ich die inline keyboards (set ... inline)  und auch die Beantwortung (set ... answer) hinzugefügt habe. Vielleicht mag das ja jemand ausprobieren.

2 Hinweise:
- Es findet keine automatische Beantwortung von inline queries statt, der answer befehl muss abgesetzt werden, sonst ist der Client erstal blockiert
- Es werden nur inline query buttons von berechtigten Benutzern (selbe Prüfung wie bei cmdKeyword) entgegengenommen, alle anderen werden auch nicht als reading bereitgestellt

Damit sollte auch der blockierende HTTP-Zugriff ausserhalb von telegramBot unnötig werden
Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

Prof. Dr. Peter Henning

#1034
Ich lade es mal runter, hatte auch schon angefangen, an dem Modul herumzuspielen.

Mit dem Modul von github:

reload 50_TelegramBot.pm
Too many arguments for main::TelegramBot_UpdatePoll at /opt/fhem/FHEM/50_TelegramBot.pm line 696, near ""doOnce" )"
===> FHEM crash.

Manueller Neustart: Scheint zu laufen, auch mit "inline".

Problem noch: Wie bekommt man mehrere Buttons in eine Zeile ? Dazu sollte ab Zeile 1641 eine zweite Schleifenverschachtelung rein, also etwa
  foreach my $aKey ( @$aKeyRow ) {
        foreach my $bKey (split( /,/,$aKey)){
          my ( $keytext, $keydata ) = split( /:/, $bKey, 2);
          $keydata = $keytext if ( ! defined( $keydata ) );
          my %oneKey = ( "text" => $keytext, "callback_data" => $keydata );
          push( @parRow, \%oneKey );
        }
      }


Dann kann man mit set <Device>inline (Einkauf:E,Baumarkt:B) (Test:T) PostIt ein Keyboard erzeugen, das in der ersten Zeile 2 Buttons und in der 2. Zeile einen Button hat

Außerdem ist unklar, was die Kiste jetzt mit dem callbackdata anfangen soll - nur das reading setzen ? Warum nicht ein "eval" über den Text laufen lassen, oder FHEM-Kommandos als Attribute zu setzen ?

Sonstige Dinge:

1. Neues undokumentiertes set _msg - dupliziert set msg ??
2. Tippfehler disbale in der Doku
3. Zeile 537 => Kommentar sollte nicht "location" lauten
4. Zeile 1643 =>  sollte lauten         my %oneKey = ( "text" => $keytext, "callback_data" => $keydata );   (keydata statt keytext)
5. Die beiden Kommandos "reply" und "answer" sind schwer auseinander zu halten, und "inline" ist etwas zu allgemein. Mein Vorschlag: "keyboard" statt "inline", "kbresponse" statt "answer".
6. Warum macht man eigentlich kein automatisches Response aus dem Modul heraus ? Wenn die externe Routine zum Abfangen eines Events nicht funktioniert, hängt der Client. Ich votiere für Asychronität: Automatische Respoinse in demMoment, wo Callbackdata gesetzt wurde bzw. ein FHEM-Kommando abgesandt wurde.

Also am Ende von ParseCallBack, außerhalb des BulkUpdate:

    #-- set result reading and answer call ansynchronously
    if( $data ){
      readingsSingleUpdate($hash, "callbackData", $data,1 );
      TelegramBot_Set($hash,$name,"answer ".$qid);
    }else{
      readingsSingleUpdate($hash, "callbackData", "",1  );       
    }


7. Ich habe noch in Zeile 1104 eingefügt


  $retMsg=""
    if( $retMsg eq "none");


um damit die ausführliche Meldung nach der Ausführung eines abgekürzten Kommandos unterdrücken zu können.


LG

pah