diff --git a/CHANGED b/CHANGED index 50f7f54c3..d77f188f7 100644 --- a/CHANGED +++ b/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 50_SSChatBot.pm: module to integrate Synology Chat Server + (SSCam is also updated due to compatibility) - feature: 14_FLAMINGO.pm: fix some perlcritic 5 warnings - feature: 98_DOIFtools: get subsInPackageDOIF for MODEL Perl (Forum #63938) - bugfix: 49_SSCam(STRM): avoid possible warnings during shutdown/restart diff --git a/FHEM/49_SSCam.pm b/FHEM/49_SSCam.pm index d453ba89d..0dfe82564 100644 --- a/FHEM/49_SSCam.pm +++ b/FHEM/49_SSCam.pm @@ -136,9 +136,9 @@ BEGIN { TelegramBot_AttrNum TelegramBot_Callback TelegramBot_BinaryFileRead - SSChatBot_formString - SSChatBot_addQueue - SSChatBot_getapisites + FHEM::SSChatBot::formString + FHEM::SSChatBot::addQueue + FHEM::SSChatBot::getApiSites ) ); @@ -159,6 +159,7 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "9.7.0" => "17.08.2020 compatibility to SSChatBot version 1.10.0 ", "9.6.1" => "13.08.2020 avoid warnings during FHEM shutdown/restart ", "9.6.0" => "12.08.2020 new attribute ptzNoCapPrePat ", "9.5.3" => "27.07.2020 fix warning: Use of uninitialized value in subroutine dereference at ... ", @@ -684,7 +685,7 @@ sub Define { return "Error: Perl module ".$SScamMMDBI." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SScamMMDBI); - my @a = split("[ \t][ \t]*", $def); + my @a = split m{\s+}x, $def; if(int(@a) < 4) { return "You need to specify more parameters.\n". "Format: define SSCAM [Port]"; @@ -9720,7 +9721,7 @@ sub sendChat { my $name = $hash->{NAME}; my $type = AttrVal($name,"cacheType","internal"); my $mtype = ""; - my ($ret,$cache); + my ($params,$ret,$cache); Log3($name, 4, "$name - ####################################################"); Log3($name, 4, "$name - ### start send Snap or Video by SSChatBot "); @@ -9835,8 +9836,19 @@ sub sendChat { # Eintrag zur SendQueue hinzufügen # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) $fileUrl = $rootUrl."/".$mtype."/".$fname; - $subject = SSChatBot_formString ($subject, "text"); - $ret = SSChatBot_addQueue ($chatbot, "sendItem", "chatbot", $uid, $subject, $fileUrl, "", ""); + $subject = FHEM::SSChatBot::formString ($subject, "text"); + + $params = { + name => $chatbot, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $subject, + fileUrl => $fileUrl, + channel => "", + attachment => "" + }; + $ret = FHEM::SSChatBot::addQueue ($params); if($ret) { readingsSingleUpdate($hash, "sendChatState", $ret, 1); @@ -9895,8 +9907,19 @@ sub sendChat { # Eintrag zur SendQueue hinzufügen # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) $fileUrl = $rootUrl."/".$mtype."/".$fname; - $subject = SSChatBot_formString ($subject, "text"); - $ret = SSChatBot_addQueue ($chatbot, "sendItem", "chatbot", $uid, $subject, $fileUrl, "", ""); + $subject = FHEM::SSChatBot::formString ($subject, "text"); + + $params = { + name => $chatbot, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $subject, + fileUrl => $fileUrl, + channel => "", + attachment => "" + }; + $ret = FHEM::SSChatBot::addQueue ($params); if($ret) { readingsSingleUpdate($hash, "sendChatState", $ret, 1); @@ -9913,7 +9936,7 @@ sub sendChat { Log3($name, 1, "$name - Send Counter transaction \"$tac\": ".$data{SSCam}{$name}{SENDCOUNT}{$tac}) if(AttrVal($name,"debugactivetoken",0)); } - SSChatBot_getapisites($chatbot); # Übertragung Sendqueue starten + FHEM::SSChatBot::getApiSites ($chatbot); # Übertragung Sendqueue starten # use strict "refs"; undef %chatparams; diff --git a/FHEM/50_SSChatBot.pm b/FHEM/50_SSChatBot.pm new file mode 100644 index 000000000..1e2b5bbba --- /dev/null +++ b/FHEM/50_SSChatBot.pm @@ -0,0 +1,2972 @@ +######################################################################################################################## +# $Id$ +######################################################################################################################### +# 50_SSChatBot.pm +# +# (c) 2019-2020 by Heiko Maaz +# e-mail: Heiko dot Maaz at t-online dot de +# +# This Module can be used to operate as Bot for Synology Chat. +# It's based on and uses Synology Chat Webhook. +# +# This script is part of fhem. +# +# Fhem is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Fhem is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with fhem. If not, see . +# +######################################################################################################################### +# +# Definition: define SSChatBot [ServerPort] [Protocol] +# +# Example of defining a Bot: define SynChatBot SSChatBot 192.168.2.20 [5000] [HTTP(S)] +# + +package FHEM::SSChatBot; ## no critic 'package' + +use strict; +use warnings; +use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt +use Data::Dumper; # Perl Core module +use MIME::Base64; +use Time::HiRes qw(gettimeofday); +use HttpUtils; +use Encode; +eval "use JSON;1;" or my $SSChatBotMM = "JSON"; ## no critic 'eval' # Debian: apt-get install libjson-perl +eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' +eval "use Net::Domain qw(hostname hostfqdn hostdomain domainname);1" or my $SSChatBotNDom = "Net::Domain"; ## no critic 'eval' +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw( + AnalyzePerlCommand + AnalyzeCommandChain + asyncOutput + addToDevAttrList + AttrVal + attr + CancelDelayedShutdown + CommandSet + CommandAttr + CommandDefine + CommandGet + data + defs + devspec2array + FmtDateTime + getKeyValue + HttpUtils_NonblockingGet + init_done + InternalTimer + IsDisabled + IsDevice + Log + Log3 + modules + parseParams + plotAsPng + readingFnAttributes + ReadingsVal + RemoveInternalTimer + readingsBeginUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsEndUpdate + setKeyValue + urlDecode + FW_wname + ) + ); + + # Export to main context with different name + # my $pkg = caller(0); + # my $main = $pkg; + # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/gx; + # foreach (@_) { + # *{ $main . $_ } = *{ $pkg . '::' . $_ }; + # } + GP_Export( + qw( + Initialize + ) + ); +} + +# Versions History intern +my %vNotesIntern = ( + "1.10.0" => "17.08.2020 switch to packages, finalise for repo checkin ", + "1.9.0" => "30.07.2020 restartSendqueue option 'force' added ", + "1.8.0" => "27.05.2020 send SVG Plots with options like svg=',,' possible ", + "1.7.0" => "26.05.2020 send SVG Plots possible ", + "1.6.1" => "22.05.2020 changes according to PBP ", + "1.6.0" => "22.05.2020 replace \" H\" with \"%20H\" in attachments due to problem in HttpUtils ", + "1.5.0" => "15.03.2020 slash commands set in interactive answer field 'value' will be executed ", + "1.4.0" => "15.03.2020 rename '1_sendItem' to 'asyncSendItem' because of Aesthetics ", + "1.3.1" => "14.03.2020 new reading recActionsValue which extract the value from actions, review logs of botCGI ", + "1.3.0" => "13.03.2020 rename 'sendItem' to '1_sendItem', allow attachments ", + "1.2.2" => "07.02.2020 add new permanent error 410 'message too long' ", + "1.2.1" => "27.01.2020 replace \" H\" with \"%20H\" in payload due to problem in HttpUtils ", + "1.2.0" => "04.01.2020 check that Botname with type SSChatBot does exist and write Log if not ", + "1.1.0" => "27.12.2019 both POST- and GET-method are now valid in CGI ", + "1.0.1" => "11.12.2019 check OPIDX in parse sendItem, change error code list, complete forbidSend with error text ", + "1.0.0" => "29.11.2019 initial " +); + +# Versions History extern +my %vNotesExtern = ( + "1.7.0" => "26.05.2020 Now it is possible to send SVG plots very easily with the command asyncSendItem ", + "1.4.0" => "15.03.2020 Command '1_sendItem' renamed to 'asyncSendItem' because of Aesthetics ", + "1.3.0" => "13.03.2020 The set command 'sendItem' was renamed to '1_sendItem' to avoid changing the botToken by chance. ". + "Also attachments are allowed now in the '1_sendItem' command. ", + "1.0.1" => "11.12.2019 check OPIDX in parse sendItem, change error code list, complete forbidSend with error text ", + "1.0.0" => "08.12.2019 initial " +); + +# Hint hash EN +my %vHintsExt_en = ( + +); + +# Hint hash DE +my %vHintsExt_de = ( + +); + +my %errList = ( + 100 => "Unknown error", + 101 => "Payload is empty", + 102 => "API does not exist - may be the Synology Chat Server package is stopped", + 117 => "illegal file name or path", + 120 => "payload has wrong format", + 404 => "bot is not legal - may be the bot is not active or the botToken is wrong", + 407 => "record not valid", + 409 => "exceed max file size", + 410 => "message too long", + 800 => "malformed or unsupported URL", + 805 => "empty API data received - may be the Synology Chat Server package is stopped", + 806 => "couldn't get Synology Chat API informations", + 810 => "The botToken couldn't be retrieved", + 900 => "malformed JSON string received from Synology Chat Server", +); + +my %hset = ( # Hash für Set-Funktion + botToken => { fn => "_setbotToken" }, + listSendqueue => { fn => "_setlistSendqueue" }, + purgeSendqueue => { fn => "_setpurgeSendqueue" }, + asyncSendItem => { fn => "_setasyncSendItem" }, + restartSendqueue => { fn => "_setrestartSendqueue" }, +); + +my %hget = ( # Hash für Get-Funktion + storedToken => { fn => "_getstoredToken" }, + chatUserlist => { fn => "_getchatUserlist" }, + chatChannellist => { fn => "_getchatChannellist" }, + versionNotes => { fn => "_getversionNotes" }, +); + +my %hrecbot = ( # Hash für botCGI receice Slash-commands (/set, /get, /code) + set => { fn => "__botCGIrecSet" }, + get => { fn => "__botCGIrecGet" }, + cod => { fn => "__botCGIrecCod" }, +); + +################################################################ +sub Initialize { + my ($hash) = @_; + $hash->{DefFn} = \&Define; + $hash->{UndefFn} = \&Undef; + $hash->{DeleteFn} = \&Delete; + $hash->{SetFn} = \&Set; + $hash->{GetFn} = \&Get; + $hash->{AttrFn} = \&Attr; + $hash->{DelayedShutdownFn} = \&delayedShutdown; + $hash->{FW_deviceOverview} = 1; + + $hash->{AttrList} = "disable:1,0 ". + "defaultPeer:--wait#for#userlist-- ". + "allowedUserForSet:--wait#for#userlist-- ". + "allowedUserForGet:--wait#for#userlist-- ". + "allowedUserForCode:--wait#for#userlist-- ". + "allowedUserForOwn:--wait#for#userlist-- ". + "ownCommand1 ". + "showTokenInLog:1,0 ". + "httptimeout ". + $readingFnAttributes; + + FHEM::Meta::InitMod( __FILE__, $hash ) if(!$modMetaAbsent); # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) + +return; +} + +################################################################ +# define SynChatBot SSChatBot 192.168.2.10 [5000] [HTTP(S)] +# ($hash) [1] [2] [3] [4] +# +################################################################ +sub Define { + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + + return "Error: Perl module ".$SSChatBotMM." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SSChatBotMM); + return "Error: Perl module ".$SSChatBotNDom." is missing." if($SSChatBotNDom); + + my @a = split m{\s+}x, $def; + + if(int(@a) < 2) { + return "You need to specify more parameters.\n". "Format: define SSChatBot [Port] [HTTP(S)]"; + } + + my $inaddr = $a[2]; + my $inport = $a[3] ? $a[3] : 5000; + my $inprot = $a[4] ? lc($a[4]) : "http"; + + $hash->{INADDR} = $inaddr; + $hash->{INPORT} = $inport; + $hash->{MODEL} = "ChatBot"; + $hash->{INPROT} = $inprot; + $hash->{RESEND} = "next planned SendQueue start: immediately by next entry"; + $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden + $hash->{HELPER}{USERFETCHED} = 0; # Chat User sind noch nicht abgerufen + + CommandAttr(undef,"$name room Chat"); + + # benötigte API's in $hash einfügen + $hash->{HELPER}{APIINFO} = "SYNO.API.Info"; # Info-Seite für alle API's, einzige statische Seite ! + $hash->{HELPER}{CHATEXTERNAL} = "SYNO.Chat.External"; + + # Versionsinformationen setzen + setVersionInfo($hash); + + # Token lesen + getToken($hash,1,"botToken"); + + # Index der Sendequeue initialisieren + $data{SSChatBot}{$name}{sendqueue}{index} = 0; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "QueueLenth", 0); # Länge Sendqueue initialisieren + readingsBulkUpdate ($hash, "state", "Initialized"); # Init state + readingsEndUpdate ($hash,1); + + # initiale Routinen nach Start ausführen , verzögerter zufälliger Start + initOnBoot($hash); + +return; +} + +################################################################ +# Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete +# gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, +# der ebenfalls alle Geräte löscht und danach das +# Konfigurationsfile neu einliest. +# Funktion: typische Aufräumarbeiten wie das +# saubere Schließen von Verbindungen oder das Entfernen von +# internen Timern, sofern diese im Modul zum Pollen verwendet +# wurden. +################################################################ +sub Undef { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + delete $data{SSChatBot}{$name}; + + removeExtension ($hash->{HELPER}{INFIX}); + RemoveInternalTimer ($hash); + +return; +} + +####################################################################################################### +# Mit der X_DelayedShutdown Funktion kann eine Definition das Stoppen von FHEM verzögern um asynchron +# hinter sich aufzuräumen. +# Je nach Rückgabewert $delay_needed wird der Stopp von FHEM verzögert (0|1). +# Sobald alle nötigen Maßnahmen erledigt sind, muss der Abschluss mit CancelDelayedShutdown($name) an +# FHEM zurückgemeldet werden. +####################################################################################################### +sub delayedShutdown { + my $hash = shift; + my $name = $hash->{NAME}; + +return 0; +} + +################################################################# +# Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion +# X_Undef aufgerufen um offene Verbindungen zu schließen, +# anschließend wird die Funktion X_Delete aufgerufen. +# Funktion: Aufräumen von dauerhaften Daten, welche durch das +# Modul evtl. für dieses Gerät spezifisch erstellt worden sind. +# Es geht hier also eher darum, alle Spuren sowohl im laufenden +# FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen +# Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. +################################################################# +sub Delete { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + my $index = $hash->{TYPE}."_".$hash->{NAME}."_botToken"; + + # gespeicherte Credentials löschen + setKeyValue($index, undef); + +return; +} + +################################################################ +sub Attr { + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + my ($do,$val); + + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + + if ($aName eq "disable") { + if($cmd eq "set") { + $do = $aVal?1:0; + } + $do = 0 if($cmd eq "del"); + + $val = ($do == 1 ? "disabled" : "initialized"); + + if ($do == 1) { + RemoveInternalTimer($hash); + } else { + InternalTimer(gettimeofday()+2, "FHEM::SSChatBot::initOnBoot", $hash, 0) if($init_done); + } + + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "state", $val); + readingsEndUpdate ($hash,1); + } + + if ($cmd eq "set") { + if ($aName =~ m/httptimeout/x) { + unless ($aVal =~ /^\d+$/x) { return "The Value for $aName is not valid. Use only figures 1-9 !";} + } + + if ($aName =~ m/ownCommand([1-9][0-9]*)$/x) { + my $num = $1; + return qq{The value of $aName must start with a slash like "/Weather ".} unless ($aVal =~ /^\//x); + addToDevAttrList($name, "ownCommand".($num+1)); # add neue ownCommand dynamisch + } + } + +return; +} + +################################################################ +# Set und Subroutinen +################################################################ +sub Set { + my ($hash, @a) = @_; + return qq{"set X" needs at least an argument} if ( @a < 2 ); + my @items = @a; + my $name = shift @a; + my $opt = shift @a; + my $prop = shift @a; + + my $setlist; + + return if(IsDisabled($name)); + + my $idxlist = join(",", sortVersion("asc",keys %{$data{SSChatBot}{$name}{sendqueue}{entries}})); + + if(!$hash->{TOKEN}) { + # initiale setlist für neue Devices + $setlist = "Unknown argument $opt, choose one of ". + "botToken " + ; + } else { + $setlist = "Unknown argument $opt, choose one of ". + "botToken ". + "listSendqueue:noArg ". + ($idxlist?"purgeSendqueue:-all-,-permError-,$idxlist ":"purgeSendqueue:-all-,-permError- "). + "restartSendqueue ". + "asyncSendItem:textField-long " + ; + } + + my $params = { + hash => $hash, + name => $name, + opt => $opt, + prop => $prop, + aref => \@items, + }; + + no strict "refs"; ## no critic 'NoStrict' + if($hset{$opt}) { + my $ret = ""; + $ret = &{$hset{$opt}{fn}} ($params) if(defined &{$hset{$opt}{fn}}); + return $ret; + } + use strict "refs"; + +return $setlist; +} + +################################################################ +# Setter botToken +################################################################ +sub _setbotToken { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $opt = $paref->{opt}; + my $prop = $paref->{prop}; + + return qq{The command "$opt" needs an argument.} if (!$prop); + my ($success) = setToken($hash, $prop, "botToken"); + + if($success) { + CommandGet(undef, "$name chatUserlist"); # Chatuser Liste abrufen + return qq{botToken saved successfully}; + } else { + return qq{Error while saving botToken - see logfile for details}; + } + +return; +} + +################################################################ +# Setter listSendqueue +################################################################ +sub _setlistSendqueue { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + + my $sub = sub { + my $idx = shift; + my $ret; + for my $key (reverse sort keys %{$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}}) { + $ret .= ", " if($ret); + $ret .= $key."=>".$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{$key}; + } + return $ret; + }; + + if (!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + return qq{SendQueue is empty.}; + } + + my $sq; + for my $idx (sort{$a<=>$b} keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + $sq .= $idx." => ".$sub->($idx)."\n"; + } + +return $sq; +} + +################################################################ +# Setter purgeSendqueue +################################################################ +sub _setpurgeSendqueue { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop}; + + if($prop eq "-all-") { + delete $hash->{OPIDX}; + delete $data{SSChatBot}{$name}{sendqueue}{entries}; + $data{SSChatBot}{$name}{sendqueue}{index} = 0; + return "All entries of SendQueue are deleted"; + + } elsif($prop eq "-permError-") { + for my $idx (keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + delete $data{SSChatBot}{$name}{sendqueue}{entries}{$idx} + if($data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend}); + } + return qq{All entries with state "permanent send error" are deleted}; + + } else { + delete $data{SSChatBot}{$name}{sendqueue}{entries}{$prop}; + return qq{SendQueue entry with index "$prop" deleted}; + } + +return; +} + +###################################################################################################### +# Setter asyncSendItem +# +# einfachster Sendetext users="user1" +# text="First line of message to post.\nAlso you can have a second line of message." users="user1" +# text="" users="user1" +# text="Check this!! for details!" users="user1,user2" +# text="a fun image" fileUrl="http://imgur.com/xxxxx" users="user1,user2" +# text="aktuelles SVG-Plot" svg=",," users="user1,user2" +# +###################################################################################################### +sub _setasyncSendItem { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $aref = $paref->{aref}; + + delete $hash->{HELPER}{RESENDFORCE}; # Option 'force' löschen (könnte durch restartSendqueue gesetzt sein) + return if(!$hash->{HELPER}{USERFETCHED}); + + my ($text,$users,$svg); + + my ($fileUrl,$attachment) = ("",""); + my $cmd = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @$aref; ## no critic 'Map blocks' + my ($arr,$h) = parseParams($cmd); + + if($h) { + $text = $h->{text} if(defined $h->{text}); + $users = $h->{users} if(defined $h->{users}); + $fileUrl = $h->{fileUrl} if(defined $h->{fileUrl}); # ein File soll über einen Link hochgeladen und versendet werden + $svg = $h->{svg} if(defined $h->{svg}); # ein SVG-Plot soll versendet werden + $attachment = formString($h->{attachments}, "attachement") if(defined $h->{attachments}); + } + + if($arr) { + my @t = @{$arr}; + shift @t; shift @t; + $text = join(" ", @t) if(!$text); + } + + if($svg) { # Versenden eines Plotfiles + my ($err, $file) = plotToFile ($name, $svg); + return if($err); + + my $FW = $hash->{FW}; + my $csrf = $defs{$FW}{CSRFTOKEN} // ""; + $fileUrl = (split("sschat", $hash->{OUTDEF}))[0]; + $fileUrl .= "sschat/www/images/$file?&fwcsrf=$csrf"; + + $fileUrl = formString($fileUrl, "text"); + $text = $svg if(!$text); # Name des SVG-Plots + Optionen als Standardtext + } + + return qq{Your sendstring is incorrect. It must contain at least text with the "text=" tag like text="..."\nor only some text like "this is a test" without the "text=" tag.} if(!$text); + + $text = formString($text, "text"); + + $users = AttrVal($name,"defaultPeer", "") if(!$users); + return "You haven't defined any receptor for send the message to. ". + "You have to use the \"users\" tag or define default receptors with attribute \"defaultPeer\"." if(!$users); + + # User aufsplitten und zu jedem die ID ermitteln + my @ua = split(/,/x, $users); + for my $user (@ua) { + next if(!$user); + my $uid = $hash->{HELPER}{USERS}{$user}{id}; + return qq{The receptor "$user" seems to be unknown because its ID coulnd't be found.} if(!$uid); + + # Eintrag zur SendQueue hinzufügen + # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $text, + fileUrl => $fileUrl, + channel => "", + attachment => $attachment + }; + addQueue ($params); + } + + getApiSites($name); + +return; +} + +################################################################ +# Setter restartSendqueue +################################################################ +sub _setrestartSendqueue { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop}; + + if($prop && $prop eq "force") { + $hash->{HELPER}{RESENDFORCE} = 1; + } else { + delete $hash->{HELPER}{RESENDFORCE}; + } + + my $ret = getApiSites($name); + +return $ret if($ret); +return qq{The SendQueue has been restarted.}; +} + +################################################################ +# Get +################################################################ +sub Get { + my ($hash, @a) = @_; + return "\"get X\" needs at least an argument" if ( @a < 2 ); + my $name = shift @a; + my $opt = shift @a; + my $arg = shift @a; + + my $getlist; + + if(!$hash->{TOKEN}) { + return; + + } else { + $getlist = "Unknown argument $opt, choose one of ". + "storedToken:noArg ". + "chatUserlist:noArg ". + "chatChannellist:noArg ". + "versionNotes " + ; + } + + return if(IsDisabled($name)); + + my $pars = { + hash => $hash, + name => $name, + opt => $opt, + arg => $arg, + }; + + no strict "refs"; ## no critic 'NoStrict' + if($hget{$opt}) { + my $ret = ""; + $ret = &{$hget{$opt}{fn}} ($pars) if(defined &{$hget{$opt}{fn}}); + return $ret; + } + use strict "refs"; + +return $getlist; # not generate trigger out of command +} + +################################################################ +# Getter storedToken +################################################################ +sub _getstoredToken { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + if (!$hash->{TOKEN}) {return qq{Token of $name is not set - make sure you've set it with "set $name botToken "};} + + my ($success, $token) = getToken($hash,0,"botToken"); # Token abrufen + unless ($success) {return qq{Token couldn't be retrieved successfully - see logfile}}; + + return qq{Stored Token to act as Synology Chat Bot:\n}. + qq{=========================================\n}. + qq{$token \n} + ; +} + +################################################################ +# Getter chatUserlist +################################################################ +sub _getchatUserlist { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + # übergebenen CL-Hash (FHEMWEB) in Helper eintragen + delClhash ($name); + getClhash ($hash,1); + + # Eintrag zur SendQueue hinzufügen + my $params = { + name => $name, + opmode => "chatUserlist", + method => "user_list", + userid => "", + text => "", + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + getApiSites ($name); + +return; +} + +################################################################ +# Getter chatChannellist +################################################################ +sub _getchatChannellist { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + # übergebenen CL-Hash (FHEMWEB) in Helper eintragen + delClhash ($name); + getClhash ($hash,1); + + # Eintrag zur SendQueue hinzufügen + my $params = { + name => $name, + opmode => "chatChannellist", + method => "channel_list", + userid => "", + text => "", + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + getApiSites ($name); + +return; +} + +################################################################ +# Getter versionNotes +################################################################ +sub _getversionNotes { ## no critic "not used" + my $paref = shift; + my $arg = $paref->{arg}; + + my $header = "Module release information
"; + my $header1 = "Helpful hints
"; + my $ret = ""; + my %hs; + + # Ausgabetabelle erstellen + my ($val0,$val1); + my $i = 0; + + $ret = ""; + + # Hints + if(!$arg || $arg =~ /hints/x || $arg =~ /[\d]+/x) { + $ret .= sprintf("
$header1
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + if($arg && $arg =~ /[\d]+/x) { + my @hints = split(",",$arg); + for my $hint (@hints) { + if(AttrVal("global","language","EN") eq "DE") { + $hs{$hint} = $vHintsExt_de{$hint}; + } else { + $hs{$hint} = $vHintsExt_en{$hint}; + } + } + } else { + if(AttrVal("global","language","EN") eq "DE") { + %hs = %vHintsExt_de; + } else { + %hs = %vHintsExt_en; + } + } + $i = 0; + for my $key (sortVersion("desc",keys %hs)) { + $val0 = $hs{$key}; + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0
"; + $ret .= "
"; + } + + # Notes + if(!$arg || $arg =~ /rel/x) { + $ret .= sprintf("
$header
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $i = 0; + for my $key (sortVersion("desc",keys %vNotesExtern)) { + ($val0,$val1) = split(/\s/x, $vNotesExtern{$key},2); + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0 $val1
"; + $ret .= "
"; + } + + $ret .= ""; + +return $ret; +} + +###################################################################################### +# initiale Startroutinen nach Restart FHEM +###################################################################################### +sub initOnBoot { + my $hash = shift; + my $name = $hash->{NAME}; + my ($ret,$csrf,$fuuid); + + RemoveInternalTimer($hash, "FHEM::SSChatBot::initOnBoot"); + + if ($init_done) { + # check ob FHEMWEB Instanz für SSChatBot angelegt ist -> sonst anlegen + my @FWports; + my $FWname = "sschat"; # der Pfad nach http://hostname:port/ der neuen FHEMWEB Instanz -> http://hostname:port/sschat + my $FW = "WEBSSChatBot"; # Name der FHEMWEB Instanz für SSChatBot + + for my $dev ( devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ) { + $hash->{FW} = $dev if ( AttrVal( $dev, "webname", "fhem" ) eq $FWname ); + push @FWports, $defs{$dev}{PORT}; + } + + if (!defined($hash->{FW})) { # FHEMWEB für SSChatBot ist noch nicht angelegt + my $room = AttrVal($name, "room", "Chat"); + my $port = 8082; + + while (grep {/^$port$/x} @FWports) { # den ersten freien FHEMWEB-Port ab 8082 finden + $port++; + } + + if (!defined($defs{$FW})) { # wenn Device "WEBSSChat" wirklich nicht existiert + Log3($name, 3, "$name - Creating new FHEMWEB instance \"$FW\" with webname \"$FWname\"... "); + $ret = CommandDefine(undef, "$FW FHEMWEB $port global"); + } + + if(!$ret) { + Log3($name, 3, "$name - FHEMWEB instance \"$FW\" with webname \"$FWname\" created"); + $hash->{FW} = $FW; + + $fuuid = $defs{$FW}{FUUID}; + $csrf = (split("-", $fuuid, 2))[0]; + + CommandAttr(undef, "$FW closeConn 1"); + CommandAttr(undef, "$FW webname $FWname"); + CommandAttr(undef, "$FW room $room"); + CommandAttr(undef, "$FW csrfToken $csrf"); + CommandAttr(undef, "$FW comment WEB Instance for SSChatBot devices.\nIt catches outgoing messages from Synology Chat server.\nDon't edit this device manually (except such attributes like \"room\", \"icon\") !"); + CommandAttr(undef, "$FW stylesheetPrefix default"); + + } else { + Log3($name, 2, "$name - ERROR while creating FHEMWEB instance ".$hash->{FW}." with webname \"$FWname\" !"); + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "state", "ERROR in initialization - see logfile"); + readingsEndUpdate ($hash,1); + } + } + + if(!$ret) { + CommandGet(undef, "$name chatUserlist"); # Chatuser Liste initial abrufen + + my $host = hostname(); # eigener Host + my $fqdn = hostfqdn(); # MYFQDN eigener Host + chop($fqdn) if($fqdn =~ /\.$/x); # eventuellen "." nach dem FQDN entfernen + my $FWchatport = $defs{$FW}{PORT}; + my $FWprot = AttrVal($FW, "HTTPS", 0); + $FWname = AttrVal($FW, "webname", 0); + + CommandAttr(undef, "$FW csrfToken none") if(!AttrVal($FW, "csrfToken", "")); + + $csrf = $defs{$FW}{CSRFTOKEN} // ""; + $hash->{OUTDEF} = ($FWprot ? "https" : "http")."://".($fqdn // $host).":".$FWchatport."/".$FWname."/outchat?botname=".$name."&fwcsrf=".$csrf; + + addExtension($name, "FHEM::SSChatBot::botCGI", "outchat"); + $hash->{HELPER}{INFIX} = "outchat"; + } + + } else { + InternalTimer(gettimeofday()+3, "FHEM::SSChatBot::initOnBoot", $hash, 0); + } + +return; +} + +###################################################################################### +# Eintrag zur SendQueue hinzufügen +# +# ($name,$opmode,$method,$userid,$text,$fileUrl,$channel,$attachment) +###################################################################################### +sub addQueue { + my $paref = shift; + my $name = $paref->{name} // do {my $err = qq{internal ERROR -> name is empty}; Log 1, "SSChatBot - $err"; return}; + my $hash = $defs{$name}; + my $opmode = $paref->{opmode} // do {my $err = qq{internal ERROR -> opmode is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $method = $paref->{method} // do {my $err = qq{internal ERROR -> method is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $userid = $paref->{userid} // do {my $err = qq{internal ERROR -> userid is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $text = $paref->{text}; + my $fileUrl = $paref->{fileUrl}; + my $channel = $paref->{channel}; + my $attachment = $paref->{attachment}; + + if(!$text && $opmode !~ /chatUserlist|chatChannellist/x) { + my $err = qq{can't add message to queue: "text" is empty}; + Log3($name, 2, "$name - ERROR - $err"); + + setErrorState ($hash, $err); + + return; + } + + $data{SSChatBot}{$name}{sendqueue}{index}++; + my $index = $data{SSChatBot}{$name}{sendqueue}{index}; + + Log3($name, 5, "$name - Add Item to queue - Idx: $index, Opmode: $opmode, Text: $text, fileUrl: $fileUrl, attachment: $attachment, userid: $userid"); + + my $pars = {'opmode' => $opmode, + 'method' => $method, + 'userid' => $userid, + 'channel' => $channel, + 'text' => $text, + 'attachment' => $attachment, + 'fileUrl' => $fileUrl, + 'retryCount' => 0 + }; + + $data{SSChatBot}{$name}{sendqueue}{entries}{$index} = $pars; + + updQLength ($hash); # updaten Länge der Sendequeue + +return; +} + + +############################################################################################# +# Erfolg einer Rückkehrroutine checken und ggf. Send-Retry ausführen +# bzw. den SendQueue-Eintrag bei Erfolg löschen +# $name = Name des Chatbot-Devices +# $retry = 0 -> Opmode erfolgreich (DS löschen), +# 1 -> Opmode nicht erfolgreich (Abarbeitung nach ckeck errorcode +# eventuell verzögert wiederholen) +############################################################################################# +sub checkRetry { + my ($name,$retry) = @_; + my $hash = $defs{$name}; + my $idx = $hash->{OPIDX}; + my $forbidSend = ""; + + if(!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + Log3($name, 4, "$name - SendQueue is empty. Nothing to do ..."); + updQLength ($hash); + return; + } + + if(!$retry) { # Befehl erfolgreich, Senden nur neu starten wenn weitere Einträge in SendQueue + delete $hash->{OPIDX}; + delete $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}; + Log3($name, 4, "$name - Opmode \"$hash->{OPMODE}\" finished successfully, Sendqueue index \"$idx\" deleted."); + updQLength ($hash); + return getApiSites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer) + + } else { # Befehl nicht erfolgreich, (verzögertes) Senden einplanen + $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{retryCount}++; + my $rc = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{retryCount}; + + my $errorcode = ReadingsVal($name, "Errorcode", 0); + if($errorcode =~ /100|101|117|120|407|409|410|800|900/x) { # bei diesen Errorcodes den Queueeintrag nicht wiederholen, da dauerhafter Fehler ! + $forbidSend = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend} = $forbidSend; + + Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. It seems to be a permanent error. Exclude it from new send attempt !"); + + delete $hash->{OPIDX}; + delete $hash->{OPMODE}; + + updQLength ($hash); # updaten Länge der Sendequeue + + return getApiSites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer); + } + + if(!$forbidSend) { + my $rs = 0; + $rs = $rc <= 1 ? 5 + : $rc < 3 ? 20 + : $rc < 5 ? 60 + : $rc < 7 ? 1800 + : $rc < 30 ? 3600 + : 86400 + ; + + Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. Restart SendQueue in $rs seconds (retryCount $rc)."); + + my $rst = gettimeofday()+$rs; # resend Timer + updQLength ($hash,$rst); # updaten Länge der Sendequeue mit resend Timer + + RemoveInternalTimer($hash, "FHEM::SSChatBot::getApiSites"); + InternalTimer($rst, "FHEM::SSChatBot::getApiSites", "$name", 0); + } + } + +return +} + +sub getApiSites { + my ($name) = @_; + my $hash = $defs{$name}; + my $inaddr = $hash->{INADDR}; + my $inport = $hash->{INPORT}; + my $inprot = $hash->{INPROT}; + my $apiinfo = $hash->{HELPER}{APIINFO}; # Info-Seite für alle API's, einzige statische Seite ! + my $chatexternal = $hash->{HELPER}{CHATEXTERNAL}; + + my ($url,$param,$idxset,$ret); + + # API-Pfade und MaxVersions ermitteln + Log3($name, 4, "$name - ####################################################"); + Log3($name, 4, "$name - ### start Chat operation Send "); + Log3($name, 4, "$name - ####################################################"); + Log3($name, 4, "$name - Send Queue force option is set, send also messages marked as 'forbidSend'") if($hash->{HELPER}{RESENDFORCE}); + + if(!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + $ret = "Sendqueue is empty. Nothing to do ..."; + Log3($name, 4, "$name - $ret"); + return $ret; + } + + # den nächsten Eintrag aus "SendQueue" selektieren und ausführen wenn nicht forbidSend gesetzt ist + for my $idx (sort{$a<=>$b} keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + if (!$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend} || $hash->{HELPER}{RESENDFORCE}) { + $hash->{OPIDX} = $idx; + $hash->{OPMODE} = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{opmode}; + $idxset = 1; + last; + } + } + + if(!$idxset) { + $ret = "Only entries with \"forbidSend\" are in Sendqueue. Escaping ..."; + Log3($name, 4, "$name - $ret"); + return $ret; + } + + if ($hash->{HELPER}{APIPARSET}) { # API-Hashwerte sind bereits gesetzt -> Abruf überspringen + Log3($name, 4, "$name - API hashvalues already set - ignore get apisites"); + return chatOp($name); + } + + my $httptimeout = AttrVal($name,"httptimeout",20); + Log3($name, 5, "$name - HTTP-Call will be done with httptimeout: $httptimeout s"); + + # URL zur Abfrage der Eigenschaften der API's + $url = "$inprot://$inaddr:$inport/webapi/query.cgi?api=$apiinfo&method=Query&version=1&query=$chatexternal"; + + Log3($name, 4, "$name - Call-Out: $url"); + + $param = { + url => $url, + timeout => $httptimeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&getApiSites_parse + }; + + HttpUtils_NonblockingGet ($param); + +return; +} + +#################################################################################### +# Auswertung Abruf apisites +#################################################################################### +sub getApiSites_parse { + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $inaddr = $hash->{INADDR}; + my $inport = $hash->{INPORT}; + my $chatexternal = $hash->{HELPER}{CHATEXTERNAL}; + + my ($error,$errorcode,$success); + + if ($err ne "") { + # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - ERROR message: $err"); + + setErrorState ($hash, $err); + checkRetry ($name,1); + + return; + + } elsif ($myjson ne "") { + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash, $success) = evalJSON($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + checkRetry ($name,1); + return; + } + + my $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + my $logstr; + + # Pfad und Maxversion von "SYNO.Chat.External" ermitteln + my $chatexternalpath = $data->{'data'}->{$chatexternal}->{'path'}; + $chatexternalpath =~ tr/_//d if (defined($chatexternalpath)); + my $chatexternalmaxver = $data->{'data'}->{$chatexternal}->{'maxVersion'}; + + $logstr = defined($chatexternalpath) ? "Path of $chatexternal selected: $chatexternalpath" : "Path of $chatexternal undefined - Synology Chat Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($chatexternalmaxver) ? "MaxVersion of $chatexternal selected: $chatexternalmaxver" : "MaxVersion of $chatexternal undefined - Synology Chat Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + # ermittelte Werte in $hash einfügen + if(defined($chatexternalpath) && defined($chatexternalmaxver)) { + $hash->{HELPER}{CHATEXTERNALPATH} = $chatexternalpath; + $hash->{HELPER}{CHATEXTERNALMAXVER} = $chatexternalmaxver; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash,"Errorcode","none"); + readingsBulkUpdateIfChanged ($hash,"Error", "none"); + readingsEndUpdate ($hash,1); + + # Webhook Hash values sind gesetzt + $hash->{HELPER}{APIPARSET} = 1; + + } else { + $errorcode = "805"; + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setErrorState ($hash, $error, $errorcode); + checkRetry ($name,1); + return; + } + + } else { + $errorcode = "806"; + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setErrorState ($hash, $error, $errorcode); + Log3($name, 2, "$name - ERROR - the API-Query couldn't be executed successfully"); + + checkRetry ($name,1); + return; + } + } + +return chatOp ($name); +} + +############################################################################################# +# Ausführung Operation +############################################################################################# +sub chatOp { + my ($name) = @_; + my $hash = $defs{$name}; + my $inprot = $hash->{INPROT}; + my $inaddr = $hash->{INADDR}; + my $inport = $hash->{INPORT}; + my $chatexternal = $hash->{HELPER}{CHATEXTERNAL}; + my $chatexternalpath = $hash->{HELPER}{CHATEXTERNALPATH}; + my $chatexternalmaxver = $hash->{HELPER}{CHATEXTERNALMAXVER}; + my ($url,$httptimeout,$param,$error,$errorcode); + + # Token abrufen + my ($success, $token) = getToken($hash,0,"botToken"); + unless ($success) { + $errorcode = "810"; + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setErrorState ($hash, $error, $errorcode); + Log3($name, 2, "$name - ERROR - $error"); + + checkRetry ($name,1); + return; + } + + my $idx = $hash->{OPIDX}; + my $opmode = $hash->{OPMODE}; + my $method = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{method}; + my $userid = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{userid}; + my $channel = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{channel}; + my $text = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{text}; + my $attachment = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{attachment}; + my $fileUrl = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{fileUrl}; + + Log3($name, 4, "$name - start SendQueue entry index \"$idx\" ($hash->{OPMODE}) for operation."); + + $httptimeout = AttrVal($name, "httptimeout", 20); + + Log3($name, 5, "$name - HTTP-Call will be done with httptimeout: $httptimeout s"); + + if ($opmode =~ /^chatUserlist$|^chatChannellist$/x) { + $url = "$inprot://$inaddr:$inport/webapi/$chatexternalpath?api=$chatexternal&version=$chatexternalmaxver&method=$method&token=\"$token\""; + } + + if ($opmode eq "sendItem") { + # Form: payload={"text": "a fun image", "file_url": "http://imgur.com/xxxxx" "user_ids": [5]} + # payload={"text": "First line of message to post in the channel" "user_ids": [5]} + # payload={"text": "Check this!! for details!" "user_ids": [5]} + + $url = "$inprot://$inaddr:$inport/webapi/$chatexternalpath?api=$chatexternal&version=$chatexternalmaxver&method=$method&token=\"$token\""; + $url .= "&payload={"; + $url .= "\"text\": \"$text\"," if($text); + $url .= "\"file_url\": \"$fileUrl\"," if($fileUrl); + $url .= "\"attachments\": $attachment," if($attachment); + $url .= "\"user_ids\": [$userid]" if($userid); + $url .= "}"; + } + + my $part = $url; + if(AttrVal($name, "showTokenInLog", "0") == 1) { + Log3($name, 4, "$name - Call-Out: $url"); + + } else { + $part =~ s/$token//x; + Log3($name, 4, "$name - Call-Out: $part"); + } + + $param = { + url => $url, + timeout => $httptimeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&chatOp_parse + }; + + HttpUtils_NonblockingGet ($param); + +return; +} + +############################################################################################# +# Callback from chatOp +############################################################################################# +sub chatOp_parse { ## no critic 'complexity' + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $inprot = $hash->{INPROT}; + my $inaddr = $hash->{INADDR}; + my $inport = $hash->{INPORT}; + my $opmode = $hash->{OPMODE}; + my ($data,$success,$error,$errorcode,$cherror); + + my $lang = AttrVal("global","language","EN"); + + if ($err ne "") { + # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - ERROR message: $err"); + + $errorcode = "none"; + $errorcode = "800" if($err =~ /:\smalformed\sor\sunsupported\sURL$/xs); + + setErrorState ($hash, $err, $errorcode); + checkRetry ($name,1); + return; + + } elsif ($myjson ne "") { + # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash,$success) = evalJSON ($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + checkRetry ($name,1); + return; + } + + $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + + if ($opmode eq "chatUserlist") { + my %users = (); + my ($un,$ui,$st,$nn,$em,$uids); + my $i = 0; + + my $out = ""; + $out .= "Synology Chat Server visible Users

"; + $out .= ""; + $out .= ""; + $out .= ""; + + while ($data->{'data'}->{'users'}->[$i]) { + my $deleted = jBoolMap($data->{'data'}->{'users'}->[$i]->{'deleted'}); + my $isdis = jBoolMap($data->{'data'}->{'users'}->[$i]->{'is_disabled'}); + if($deleted ne "true" && $isdis ne "true") { + $un = $data->{'data'}->{'users'}->[$i]->{'username'}; + $ui = $data->{'data'}->{'users'}->[$i]->{'user_id'}; + $st = $data->{'data'}->{'users'}->[$i]->{'status'}; + $nn = $data->{'data'}->{'users'}->[$i]->{'nickname'}; + $em = $data->{'data'}->{'users'}->[$i]->{'user_props'}->{'email'}; + $users{$un}{id} = $ui; + $users{$un}{status} = $st; + $users{$un}{nickname} = $nn; + $users{$un}{email} = $em; + $uids .= "," if($uids); + $uids .= $un; + $out .= ""; + } + $i++; + } + + $hash->{HELPER}{USERS} = \%users if(%users); + $hash->{HELPER}{USERFETCHED} = 1; + + my @newa; + my $list = $modules{$hash->{TYPE}}{AttrList}; + my @deva = split(" ", $list); + + for my $da (@deva) { + push @newa, $da if($da !~ /defaultPeer:|allowedUserFor(?:Set|Get|Code|Own):/x); + } + + push @newa, ($uids?"defaultPeer:multiple-strict,$uids ":"defaultPeer:--no#userlist#selectable--"); + push @newa, ($uids?"allowedUserForSet:multiple-strict,$uids ":"allowedUserForSet:--no#userlist#selectable--"); + push @newa, ($uids?"allowedUserForGet:multiple-strict,$uids ":"allowedUserForGet:--no#userlist#selectable--"); + push @newa, ($uids?"allowedUserForCode:multiple-strict,$uids ":"allowedUserForCode:--no#userlist#selectable--"); + push @newa, ($uids?"allowedUserForOwn:multiple-strict,$uids ":"allowedUserForOwn:--no#userlist#selectable--"); + + $hash->{".AttrList"} = join(" ", @newa); # Device spezifische AttrList, überschreibt Modul AttrList ! + + $out .= "
Username ID state Nickname Email
$un $ui $st $nn $em
"; + $out .= ""; + + # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst + # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) + asyncOutput($hash->{HELPER}{CL}{1},"$out"); + InternalTimer(gettimeofday()+10.0, "FHEM::SSChatBot::delClhash", $name, 0); + + } elsif ($opmode eq "chatChannellist") { + my %channels = (); + my ($ci,$cr,$mb,$ty,$cids); + my $i = 0; + + my $out = ""; + $out .= "Synology Chat Server visible Channels

"; + $out .= ""; + $out .= ""; + $out .= ""; + + while ($data->{'data'}->{'channels'}->[$i]) { + my $cn = jBoolMap($data->{'data'}->{'channels'}->[$i]->{'name'}); + if($cn) { + $ci = $data->{'data'}->{'channels'}->[$i]->{'channel_id'}; + $cr = $data->{'data'}->{'channels'}->[$i]->{'creator_id'}; + $mb = $data->{'data'}->{'channels'}->[$i]->{'members'}; + $ty = $data->{'data'}->{'channels'}->[$i]->{'type'}; + $channels{$cn}{id} = $ci; + $channels{$cn}{creator} = $cr; + $channels{$cn}{members} = $mb; + $channels{$cn}{type} = $ty; + $cids .= "," if($cids); + $cids .= $cn; + $out .= ""; + } + $i++; + } + $hash->{HELPER}{CHANNELS} = \%channels if(%channels); + + $out .= "
Channelname ID Creator Members Type
$cn $ci $cr $mb $ty
"; + $out .= ""; + + # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst + # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) + asyncOutput ($hash->{HELPER}{CL}{1},"$out"); + InternalTimer(gettimeofday()+5.0, "FHEM::SSChatBot::delClhash", $name, 0); + + } elsif ($opmode eq "sendItem" && $hash->{OPIDX}) { + my $postid = ""; + my $idx = $hash->{OPIDX}; + my $uid = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{userid}; + if($data->{data}{succ}{user_id_post_map}{$uid}) { + $postid = $data->{data}{succ}{user_id_post_map}{$uid}; + } + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "sendPostId", $postid); + readingsBulkUpdate ($hash, "sendUserId", $uid ); + readingsEndUpdate ($hash,1); + } + + checkRetry ($name,0); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none" ); + readingsBulkUpdateIfChanged ($hash, "Error", "none" ); + readingsBulkUpdate ($hash, "state", "active"); + readingsEndUpdate ($hash,1); + + } else { + # die API-Operation war fehlerhaft + # Errorcode aus JSON ermitteln + $errorcode = $data->{'error'}->{'code'}; + $cherror = $data->{'error'}->{'errors'}; # vom Chat gelieferter Fehler + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + if ($error =~ /not\sfound/x) { + $error .= " New error: ".($cherror // ""); + } + + setErrorState ($hash, $error, $errorcode); + Log3($name, 2, "$name - ERROR - Operation $opmode was not successful. Errorcode: $errorcode - $error"); + + checkRetry ($name,1); + } + + undef $data; + undef $myjson; + } + +return; +} + +############################################################################### +# Test ob JSON-String empfangen wurde +############################################################################### +sub evalJSON { + my ($hash,$myjson) = @_; + my $OpMode = $hash->{OPMODE}; + my $name = $hash->{NAME}; + my $success = 1; + my ($error,$errorcode); + + eval {decode_json($myjson)} or do { + $success = 0; + $errorcode = "900"; + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setErrorState ($hash, $error, $errorcode); + }; + +return($hash,$success,$myjson); +} + +############################################################################### +# JSON Boolean Test und Mapping +############################################################################### +sub jBoolMap { + my $bool = shift; + + if(JSON::is_bool($bool)) { + $bool = $bool?"true":"false"; + } + +return $bool; +} + + +############################################################################## +# Auflösung Errorcodes SVS API +# Übernahmewerte sind $hash, $errorcode +############################################################################## +sub expError { + my ($hash,$errorcode) = @_; + my $device = $hash->{NAME}; + my $error; + + unless (exists($errList{"$errorcode"})) { + $error = "Value of errorcode \"$errorcode\" not found."; + return ($error); + } + + # Fehlertext aus Hash-Tabelle %errorlist ermitteln + $error = $errList{"$errorcode"}; + +return $error; +} + +################################################################ +# sortiert eine Liste von Versionsnummern x.x.x +# Schwartzian Transform and the GRT transform +# Übergabe: "asc | desc", +################################################################ +sub sortVersion { + my ($sseq,@versions) = @_; + + my @sorted = map {$_->[0]} + sort {$a->[1] cmp $b->[1]} + map {[$_, pack "C*", split /\./x]} @versions; + + @sorted = map {join ".", unpack "C*", $_} + sort + map {pack "C*", split /\./x} @versions; + + if($sseq eq "desc") { + @sorted = reverse @sorted; + } + +return @sorted; +} + +###################################################################################### +# botToken speichern +###################################################################################### +sub setToken { + my ($hash, $token, $ao) = @_; + my $name = $hash->{NAME}; + my ($success, $credstr, $index, $retcode); + my (@key,$len,$i); + + $credstr = encode_base64($token); + + # Beginn Scramble-Routine + @key = qw(1 3 4 5 6 3 2 1 9); + $len = scalar @key; + $i = 0; + $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) + $key[$i]) % 256) } split //x, $credstr; ## no critic 'Map blocks' + # End Scramble-Routine + + $index = $hash->{TYPE}."_".$hash->{NAME}."_".$ao; + $retcode = setKeyValue($index, $credstr); + + if ($retcode) { + Log3($name, 2, "$name - Error while saving Token - $retcode"); + $success = 0; + } else { + ($success, $token) = getToken($hash,1,$ao); # Credentials nach Speicherung lesen und in RAM laden ($boot=1) + } + +return $success; +} + +###################################################################################### +# botToken lesen +###################################################################################### +sub getToken { + my ($hash,$boot, $ao) = @_; + my $name = $hash->{NAME}; + my ($success, $token, $index, $retcode, $credstr); + my (@key,$len,$i); + + if ($boot) { + # mit $boot=1 botToken von Platte lesen und als scrambled-String in RAM legen + $index = $hash->{TYPE}."_".$hash->{NAME}."_".$ao; + ($retcode, $credstr) = getKeyValue($index); + + if ($retcode) { + Log3($name, 2, "$name - Unable to read botToken from file: $retcode"); + $success = 0; + } + + if ($credstr) { + # beim Boot scrambled botToken in den RAM laden + $hash->{HELPER}{TOKEN} = $credstr; + + # "TOKEN" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung + $hash->{TOKEN} = "Set"; + $success = 1; + } + + } else { + # boot = 0 -> botToken aus RAM lesen, decoden und zurückgeben + $credstr = $hash->{HELPER}{TOKEN}; + + if($credstr) { + # Beginn Descramble-Routine + @key = qw(1 3 4 5 6 3 2 1 9); + $len = scalar @key; + $i = 0; + $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) - $key[$i] + 256) % 256) } split //x, $credstr; ## no critic 'Map blocks' + # Ende Descramble-Routine + + $token = decode_base64($credstr); + + my $logtok = AttrVal($name, "showTokenInLog", "0") == 1 ? $token : "********"; + + Log3($name, 4, "$name - botToken read from RAM: $logtok"); + + } else { + Log3($name, 2, "$name - botToken not set in RAM !"); + } + + $success = (defined($token)) ? 1 : 0; + } + +return ($success, $token); +} + +############################################################################################# +# FHEMWEB Extension hinzufügen +############################################################################################# +sub addExtension { + my ($name, $func, $link) = @_; + + my $url = "/$link"; + $data{FWEXT}{$url}{deviceName} = $name; + $data{FWEXT}{$url}{FUNC} = $func; + $data{FWEXT}{$url}{LINK} = $link; + + Log3($name, 3, "$name - SSChatBot \"$name\" for URL $url registered"); + +return; +} + +############################################################################################# +# FHEMWEB Extension löschen +############################################################################################# +sub removeExtension { + my ($link) = @_; + + my $url = "/$link"; + my $name = $data{FWEXT}{$url}{deviceName}; + + my @chatdvs = devspec2array("TYPE=SSChatBot"); + for my $cd (@chatdvs) { # /outchat erst deregistrieren wenn keine SSChat-Devices mehr vorhanden sind außer $name + if($defs{$cd} && $cd ne $name) { + Log3($name, 2, "$name - Skip unregistering SSChatBot for URL $url"); + return; + } + } + + Log3($name, 2, "$name - Unregistering SSChatBot for URL $url..."); + delete $data{FWEXT}{$url}; + +return; +} + +############################################################################################# +# Leerzeichen am Anfang / Ende eines strings entfernen +############################################################################################# +sub trim { + my $str = shift; + $str =~ s/^\s+|\s+$//xg; + +return ($str); +} + +############################################################################################# +# Länge Senedequeue updaten +############################################################################################# +sub updQLength { + my ($hash,$rst) = @_; + my $name = $hash->{NAME}; + + my $ql = keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "QueueLenth", $ql); # Länge Sendqueue updaten + readingsEndUpdate ($hash,1); + + my $head = "next planned SendQueue start:"; + if($rst) { # resend Timer gesetzt + $hash->{RESEND} = $head." ".FmtDateTime($rst); + } else { + $hash->{RESEND} = $head." immediately by next entry"; + } + +return; +} + +############################################################################################# +# Text für den Versand an Synology Chat formatieren +# und nicht erlaubte Zeichen entfernen +# +# $txt : der zu formatierende String +# $func : ein Name zur Identifizierung der aufrufenden Funktion +############################################################################################# +sub formString { + my $txt = shift; + my $func = shift; + my (%replacements,$pat); + + if($func ne "attachement") { + %replacements = ( + '"' => "´", # doppelte Hochkomma sind im Text nicht erlaubt + " H" => "%20H", # Bug in HttpUtils(?) wenn vor großem H ein Zeichen + Leerzeichen vorangeht + "#" => "%23", # Hashtags sind im Text nicht erlaubt und wird encodiert + "&" => "%26", # & ist im Text nicht erlaubt und wird encodiert + "%" => "%25", # % ist nicht erlaubt und wird encodiert + "+" => "%2B", + ); + + } else { + %replacements = ( + " H" => "%20H" # Bug in HttpUtils(?) wenn vor großem H ein Zeichen + Leerzeichen vorangeht + ); + } + + $txt =~ s/\n/ESC_newline_ESC/xg; + my @acr = split (/\s+/x, $txt); + + $txt = ""; + for my $line (@acr) { # Einzeiligkeit für Versand herstellen + $txt .= " " if($txt); + $line =~ s/ESC_newline_ESC/\\n/xg; + $txt .= $line; + } + + $pat = join '|', map { quotemeta; } keys(%replacements); + + $txt =~ s/($pat)/$replacements{$1}/xg; + +return ($txt); +} + +#################################################################################### +# zentrale Funktion Error State in Readings setzen +# $error = Fehler als Text +# $errc = Fehlercode gemäß %errList +#################################################################################### +sub setErrorState { + my $hash = shift; + my $error = shift; + my $errc = shift; + + my $errcode = $errc // "none"; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $error); + readingsBulkUpdateIfChanged ($hash, "Errorcode", $errcode); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + +return; +} + +############################################################################################# +# Clienthash übernehmen oder zusammenstellen +# Identifikation ob über FHEMWEB ausgelöst oder nicht -> erstellen $hash->CL +############################################################################################# +sub getClhash { + my ($hash,$nobgd)= @_; + my $name = $hash->{NAME}; + my $ret; + + if($nobgd) { + # nur übergebenen CL-Hash speichern, + # keine Hintergrundverarbeitung bzw. synthetische Erstellung CL-Hash + $hash->{HELPER}{CL}{1} = $hash->{CL}; + return; + } + + if (!defined($hash->{CL})) { + # Clienthash wurde nicht übergeben und wird erstellt (FHEMWEB Instanzen mit canAsyncOutput=1 analysiert) + my @webdvs = devspec2array("TYPE=FHEMWEB:FILTER=canAsyncOutput=1:FILTER=STATE=Connected"); + my $i = 1; + for my $outdev (@webdvs) { + next if(!$defs{$outdev}); + $hash->{HELPER}{CL}{$i}->{NAME} = $defs{$outdev}{NAME}; + $hash->{HELPER}{CL}{$i}->{NR} = $defs{$outdev}{NR}; + $hash->{HELPER}{CL}{$i}->{COMP} = 1; + $i++; + } + } else { + # übergebenen CL-Hash in Helper eintragen + $hash->{HELPER}{CL}{1} = $hash->{CL}; + } + + # Clienthash auflösen zur Fehlersuche (aufrufende FHEMWEB Instanz + if (defined($hash->{HELPER}{CL}{1})) { + for (my $k=1; (defined($hash->{HELPER}{CL}{$k})); $k++ ) { + Log3($name, 4, "$name - Clienthash number: $k"); + while (my ($key,$val) = each(%{$hash->{HELPER}{CL}{$k}})) { + $val = $val?$val:" "; + Log3($name, 4, "$name - Clienthash: $key -> $val"); + } + } + } else { + Log3($name, 2, "$name - Clienthash was neither delivered nor created !"); + $ret = "Clienthash was neither delivered nor created. Can't use asynchronous output for function."; + } + +return ($ret); +} + +############################################################################################# +# Clienthash löschen +############################################################################################# +sub delClhash { + my $name = shift; + my $hash = $defs{$name}; + + delete($hash->{HELPER}{CL}); + +return; +} + +#################################################################################### +# Ausgabe der SVG-Funktion "plotAsPng" in eine Datei schreiben +# Die Datei wird im Verzeichnis "/opt/fhem/www/images" erstellt +# +#################################################################################### +sub plotToFile { + my $name = shift; + my $svg = shift; + my $hash = $defs{$name}; + my $file = $name."_SendPlot.png"; + my $path = $attr{global}{modpath}."/www/images"; + my $err = ""; + + my @options = split ",", $svg; + my $svgdev = $options[0]; + my $zoom = $options[1]; + my $offset = $options[2]; + + if(!$defs{$svgdev}) { + $err = qq{SVG device "$svgdev" doesn't exist}; + Log3($name, 1, "$name - ERROR - $err !"); + + setErrorState ($hash, $err); + return $err; + } + + open (my $FILE, ">", "$path/$file") or do { + $err = qq{>PlotToFile< can't open $path/$file for write access}; + Log3($name, 1, "$name - ERROR - $err !"); + setErrorState ($hash, $err); + return $err; + }; + binmode $FILE; + print $FILE plotAsPng(@options); + close $FILE; + +return ($err, $file); +} + +############################################################################################# +# Versionierungen des Moduls setzen +# Die Verwendung von Meta.pm und Packages wird berücksichtigt +############################################################################################# +sub setVersionInfo { + my $hash = shift; + my $name = $hash->{NAME}; + + my $v = (sortVersion("desc",keys %vNotesIntern))[0]; + my $type = $hash->{TYPE}; + $hash->{HELPER}{PACKAGE} = __PACKAGE__; + $hash->{HELPER}{VERSION} = $v; + + if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) { + # META-Daten sind vorhanden + $modules{$type}{META}{version} = "v".$v; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SSChatBot}{META}} + if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id$ im Kopf komplett! vorhanden ) + $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/gx; + } else { + $modules{$type}{META}{x_version} = $v; + } + return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id$ im Kopf komplett! vorhanden ) + if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) { + # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen + # mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden + use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); ## no critic 'VERSION' + } + } else { + # herkömmliche Modulstruktur + $hash->{VERSION} = $v; + } + +return; +} + +############################################################################################# +# Common Gateway Interface +############################################################################################# +sub botCGI { + my $request = shift; + + if(!$init_done) { + return ( "text/plain; charset=utf-8", "FHEM server is booting up" ); + } + + if ($request =~ /^\/outchat(\?|&)/x) { # POST- oder GET-Methode empfangen + # data received + return _botCGIdata ($request); + + } else { + # no data received + return ("text/plain; charset=utf-8", "Missing data"); + } +} + +############################################################################################# +# Common Gateway data receive +# parsen von outgoing Messages Chat -> FHEM +############################################################################################# +sub _botCGIdata { ## no critic 'complexity' + my $request = shift; + + my ($text,$timestamp,$channelid,$channelname,$userid,$username,$postid,$triggerword) = ("","","","","","","",""); + my ($command,$cr,$au,$arg,$callbackid,$actions,$actval,$avToExec) = ("","","","","","","",""); + my $state = "active"; + my $do = 0; + my $ret = "success"; + my $success; + my @aul; + + my $args = (split(/outchat\?/x, $request))[1]; # GET-Methode empfangen + + if(!$args) { # POST-Methode empfangen wenn keine GET_Methode ? + $args = (split(/outchat&/x, $request))[1]; + if(!$args) { + Log 1, "TYPE SSChatBot - ERROR - no expected data received"; + return ("text/plain; charset=utf-8", "no expected data received"); + } + } + + $args =~ s/&/" /gx; + $args =~ s/=/="/gx; + $args .= "\""; + + $args = urlDecode($args); + my($a,$h) = parseParams($args); + + if (!defined($h->{botname})) { + Log 1, "TYPE SSChatBot - ERROR - no Botname received"; + return ("text/plain; charset=utf-8", "no FHEM SSChatBot name in message"); + } + + # check ob angegebenes SSChatBot Device definiert, wenn ja Kontext auf botname setzen + my $name = $h->{botname}; # das SSChatBot Device + if(!IsDevice($name, 'SSChatBot')) { + Log 1, qq{ERROR - No SSChatBot device "$name" of Type "SSChatBot" exists}; + return ( "text/plain; charset=utf-8", "No SSChatBot device for webhook \"/outchat\" exists" ); + } + + my $hash = $defs{$name}; # hash des SSChatBot Devices + Log3($name, 4, "$name - ####################################################"); + Log3($name, 4, "$name - ### start Chat operation Receive "); + Log3($name, 4, "$name - ####################################################"); + Log3($name, 5, "$name - raw data received (urlDecoded):\n".Dumper($args)); + + # eine Antwort auf ein interaktives Objekt + if (defined($h->{payload})) { + # ein Benutzer hat ein interaktives Objekt ausgelöst (Button). Die Datenfelder sind nachfolgend beschrieben: + # "actions": Array des Aktionsobjekts, das sich auf die vom Benutzer ausgelöste Aktion bezieht + # "callback_id": Zeichenkette, die sich auf die Callback_id des Anhangs bezieht, in dem sich die vom Benutzer ausgelöste Aktion befindet + # "post_id" + # "token" + # "user": { "user_id","username" } + my $pldata = $h->{payload}; + (undef, $success) = evalJSON($hash,$pldata); + + if (!$success) { + Log3($name, 1, "$name - ERROR - invalid JSON data received:\n".Dumper $pldata); + return ("text/plain; charset=utf-8", "invalid JSON data received"); + } + + my $data = decode_json ($pldata); + Log3($name, 5, "$name - interactive object data (JSON decoded):\n". Dumper $data); + + $h->{token} = $data->{token}; + $h->{post_id} = $data->{post_id}; + $h->{user_id} = $data->{user}{user_id}; + $h->{username} = $data->{user}{username}; + $h->{callback_id} = $data->{callback_id}; + $h->{actions} = "type: ".$data->{actions}[0]{type}.", ". + "name: ".$data->{actions}[0]{name}.", ". + "value: ".$data->{actions}[0]{value}.", ". + "text: ".$data->{actions}[0]{text}.", ". + "style: ".$data->{actions}[0]{style}; + } + + if (!defined($h->{token})) { + Log3($name, 5, "$name - received insufficient data:\n".Dumper($args)); + return ("text/plain; charset=utf-8", "Insufficient data"); + } + + # CSRF Token check + my $FWdev = $hash->{FW}; # das FHEMWEB Device für SSChatBot Device -> ist das empfangene Device + my $FWhash = $defs{$FWdev}; + my $want = $FWhash->{CSRFTOKEN}; + $want = $want?$want:"none"; + my $supplied = $h->{fwcsrf}; + + if($want eq "none" || $want ne $supplied) { # $FW_wname enthält ebenfalls das aufgerufenen FHEMWEB-Device + Log3 ($FW_wname, 2, "$FW_wname - WARNING - FHEMWEB CSRF error for client \"$FWdev\": ". + "received $supplied token is not $want. ". + "For details see the csrfToken FHEMWEB attribute. ". + "The csrfToken must be identical to the token in OUTDEF of $name device."); + return ("text/plain; charset=utf-8", "400 Bad Request"); + } + + # Timestamp dekodieren + if ($h->{timestamp}) { + $h->{timestamp} = FmtDateTime(($h->{timestamp})/1000); + } + + Log3($name, 4, "$name - received data decoded:\n".Dumper($h)); + + $hash->{OPMODE} = "receiveData"; + + # ausgehende Datenfelder (Chat -> FHEM), die das Chat senden kann + # =============================================================== + # token: bot token + # channel_id + # channel_name + # user_id + # username + # post_id + # timestamp + # text + # trigger_word: which trigger word is matched + # + + $channelid = $h->{channel_id} if($h->{channel_id}); + $channelname = $h->{channel_name} if($h->{channel_name}); + $userid = $h->{user_id} if($h->{user_id}); + $username = $h->{username} if($h->{username}); + $postid = $h->{post_id} if($h->{post_id}); + $callbackid = $h->{callback_id} if($h->{callback_id}); + $timestamp = $h->{timestamp} if($h->{timestamp}); + + # interaktive Schaltflächen (Aktionen) auswerten + if ($h->{actions}) { + $actions = $h->{actions}; + ($actval) = $actions =~ m/^type:\s+button.*?value:\s+(.*?),\s+text:/x; + + if($actval =~ /^\//x) { + Log3($name, 4, "$name - slash command \"$actval\" got from interactive data and execute it with priority"); + $avToExec = $actval; + } + } + + if ($h->{text} || $avToExec) { + $text = $h->{text}; + $text = $avToExec if($avToExec); # Vorrang für empfangene interaktive Data (Schaltflächenwerte) die Slash-Befehle enthalten + if($text =~ /^\/(set.*?|get.*?|code.*?)\s+(.*)$/ix) { # vordefinierte Befehle in FHEM ausführen + my $p1 = substr lc $1, 0, 3; + my $p2 = $2; + + my $pars = { + name => $name, + username => $username, + state => $state, + p2 => $p2, + }; + + if($hrecbot{$p1} && defined &{$hrecbot{$p1}{fn}}) { + $do = 1; + no strict "refs"; ## no critic 'NoStrict' + ($command, $cr, $state) = &{$hrecbot{$p1}{fn}} ($pars); + use strict "refs"; + } + + $cr = $cr ne q{} ? $cr : qq{command '$command' executed}; + Log3($name, 4, "$name - FHEM command return: ".$cr); + + $cr = formString($cr, "command"); + + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $userid, + text => $cr, + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + } + + my $ua = $attr{$name}{userattr}; # Liste aller ownCommandxx zusammenstellen + $ua = "" if(!$ua); + my %hc = map { ($_ => 1) } grep { "$_" =~ m/ownCommand(\d+)/x } split(" ","ownCommand1 $ua"); + + for my $ca (sort keys %hc) { + my $uc = AttrVal($name, $ca, ""); + next if (!$uc); + ($uc,$arg) = split(/\s+/x, $uc, 2); + + if($uc && $text =~ /^$uc\s*?$/x) { # User eigener Slash-Befehl, z.B.: /Wetter + $command = $arg; + $do = 1; + $au = AttrVal($name,"allowedUserForOwn", "all"); # Berechtgung des Chat-Users checken + @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$arg); + $cr = AnalyzeCommandChain(undef, $arg); # FHEM Befehlsketten ausführen + + } else { + $cr = qq{User "$username" is not allowed execute "$arg" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$arg" command. Execution denied !}); + } + + $cr = $cr ne q{} ? $cr : qq{command '$arg' executed}; + Log3($name, 4, "$name - FHEM command return: ".$cr); + + $cr = formString($cr, "command"); + + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $userid, + text => $cr, + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + } + } + + # Wenn Kommando ausgeführt wurde Ergebnisse aus Queue übertragen + if($do) { + RemoveInternalTimer ($hash, "FHEM::SSChatBot::getApiSites"); + InternalTimer (gettimeofday()+1, "FHEM::SSChatBot::getApiSites", "$name", 0); + } + } + + if ($h->{trigger_word}) { + $triggerword = urlDecode($h->{trigger_word}); + Log3($name, 4, "$name - trigger_word received: ".$triggerword); + } + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "recActions", $actions ); + readingsBulkUpdate ($hash, "recCallbackId", $callbackid ); + readingsBulkUpdate ($hash, "recActionsValue", $actval ); + readingsBulkUpdate ($hash, "recChannelId", $channelid ); + readingsBulkUpdate ($hash, "recChannelname", $channelname ); + readingsBulkUpdate ($hash, "recUserId", $userid ); + readingsBulkUpdate ($hash, "recUsername", $username ); + readingsBulkUpdate ($hash, "recPostId", $postid ); + readingsBulkUpdate ($hash, "recTimestamp", $timestamp ); + readingsBulkUpdate ($hash, "recText", $text ); + readingsBulkUpdate ($hash, "recTriggerword", $triggerword ); + readingsBulkUpdate ($hash, "recCommand", $command ); + readingsBulkUpdate ($hash, "sendCommandReturn", $cr ); + readingsBulkUpdate ($hash, "Errorcode", "none" ); + readingsBulkUpdate ($hash, "Error", "none" ); + readingsBulkUpdate ($hash, "state", $state ); + readingsEndUpdate ($hash,1); + + return ("text/plain; charset=utf-8", $ret); +} + +################################################################ +# botCGI /set +# set-Befehl in FHEM ausführen +################################################################ +sub __botCGIrecSet { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = "set ".$p2; + my $au = AttrVal($name,"allowedUserForSet", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$command); + $cr = CommandSet(undef, $p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} + +################################################################ +# botCGI /get +# get-Befehl in FHEM ausführen +################################################################ +sub __botCGIrecGet { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = "get ".$p2; + my $au = AttrVal($name,"allowedUserForGet", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$command); + $cr = CommandGet(undef, $p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} + +################################################################ +# botCGI /code +# Perl Code in FHEM ausführen +################################################################ +sub __botCGIrecCod { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = $p2; + my $au = AttrVal($name,"allowedUserForCode", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + my $code = $p2; + if($p2 =~ m/^\s*(\{.*\})\s*$/xs) { + $p2 = $1; + } else { + $p2 = ''; + } + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$p2); + $cr = AnalyzePerlCommand(undef, $p2) if($p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} + +1; + +=pod +=item summary module to integrate Synology Chat into FHEM +=item summary_DE Modul zur Integration von Synology Chat in FHEM +=begin html + + +

SSChatBot

+
    + This module is used to integrate Synology Chat Server with FHEM. This makes it possible, + Exchange messages between FHEM and Synology Chat Server.
    + A detailed description of the module is available in the + Wiki available.
    +

    + + + Definition +

    +
      + + The definition is made with:

      +
        + define <Name> SSChatBot <IP> [Port] [Protokoll] +
      +
      + + The Port and Protocol entries are optional. +

      + +
        +
      • IP: IP address or name of Synology DiskStation. If the name is used, set the dnsServer global attribute.
      • +
      • Port: Port of Synology DiskStation (default 5000)
      • +
      • Protocol: Protocol for messages towards chat server, http or https (default http)
      +
      + + During the definition, an extra FHEMWEB device for receiving messages is created in addition to the SSChaBot device + in the "Chat" room. The port of the FHEMWEB device is automatically determined with start port 8082. If this port is occupied, + the ports are checked in ascending order for possible assignment by an FHEMWEB device and the next + free port is assigned to the new Device.
      + + The chat integration distinguishes between "Incoming messages" (FHEM -> Chat) and "Outgoing messages + (Chat -> FHEM).
      + +
    +

    + + + Configuration +

    +
      + + For the activation of incoming messages (FHEM -> Chat) a bot token is required. This token can be activated via the user-defined + Embedding functions in the Synology Chat application can be created or modified from within the Synology Chat application. + (see also the wiki section )
      + + The token is inserted into the newly defined SSChatBot device with the command: +

      +
        + set <Name> botToken U6FOMH9IgT22ECJceaIW0fNwEi7VfqWQFPMgJQUJ6vpaGoWZ1SJkOGP7zlVIscCp +
      +
      + Use of course the real token created by the chat application. +

      + + For activation of outgoing messages (Chat -> FHEM) the field Outgoing URL must be filled in the chat application. + To do so, click the Profile Photo icon in the upper right corner of the called Synology Chat Application and select + "Integration." Then select the bot created in the first step in the "Bots" menu. + + The value of the internal OUTDEF of the created SSChatBot device is now copied into the field Outgoing URL.
      + For example, the string could look like this:

      + +
        + http://myserver.mydom:8086/sschat/outchat?botname=SynChatBot&fwcsrf=5de17731 +
      +
      + + (see also the wiki section )
      + +

      + General information on sending messages
      + Messages that FHEM sends to the chat server (incoming messages) are first placed in a queue in FHEM. + The send process is started immediately. If the transmission was successful, the message is deleted from the queue. + Otherwise, it remains in the queue and the send process will, in a time interval, restarted.
      + With the Set command restartSendqueue the processing of the queue can be started manually + (for example, after Synology maintenance). + +

      + + Allgemeine Information zum Nachrichtempfang
      + Um Befehle vom Chat Server an FHEM zu senden, werden Slash-Befehle (/) verwendet. Sie sind vor der Verwendung im Synology + Chat und ggf. zusätzlich im SSChatBot Device (User spezifische Befehle) zu konfigurieren.

      + + The following command forms are supported:

      +
        +
      • /set
      • +
      • /get
      • +
      • /code
      • +
      • /<User specific command> (see attribute ownCommandx)
      • +
      +
      + + Further detailed information on configuring message reception is available in the corresponding + wiki section. +
    +

    + + + Set +

    +
      + +
        + +
      • asyncSendItem <Item>
        + + Sends a message to one or more chat recipients.
        + For more information about the available options for asyncSendItem, especially the use of interactive + objects (buttons), please consult this + wiki section. +

        + +
          + Beispiele:
          + set <Name> asyncSendItem First message line to post.\n You can also have a second message line. [users="<User>"]
          + set <Name> asyncSendItem text="First message line to post.\n You can also have a second message line. [users="<User>"]
          + set <Name> asyncSendItem text="https://www.synology.com" [users="<User>"]
          + set <Name> asyncSendItem text="Check this! <https://www.synology.com|Click here> for details!" [users="<User1>,<User2>"]
          + set <name> asyncSendItem text="a funny picture" fileUrl="http://imgur.com/xxxxx" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="current plot file" svg="<SVG-Device>[,<Zoom>][,<Offset>]" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="<Message text>" attachments="[{ + "callback_id": "<Text for Reading recCallbackId>", "text": "<Heading of the button>", + "actions":[{"type": "button", "name": "<text>", "value": "<value>", "text": "<text>", "style": "<color>"}] }]"
        + +

      • +
      + +
        + +
      • botToken <Token>
        + + Saves the token for access to the chat as a bot. + +

      • +
      + +
        + +
      • listSendqueue
        + + Shows the messages still to be transmitted to the chat.
        + All messages to be sent are first stored in a queue and transmitted asynchronously to the chat server. + +

      • +
      + +
        + +
      • purgeSendqueue <-all- | -permError- | index>
        + + Deletes entries from the send queue.

        + +
          +
        • -all- : Deletes all entries of the send queue.
        • +
        • -permError- : Deletes all entries of the send queue with "permanent Error" status.
        • +
        • index : Deletes selected entry with "index".
          + The entries in the send queue can be viewed beforehand with "set listSendqueue" to find the desired index.
        • +
        + +

      • +
      + +
        + +
      • restartSendqueue [force]
        + + Restarts the processing of the send queue manually.
        + Any entries in the send queue marked forbidSend are not sent again.
        + If the call is made with the option force, entries marked forbidSend are also taken into account. + +

      • +
      + +
    + + + Get +

    +
      + +
        + +
      • chatChannellist
        + + Creates a list of channels visible to the bot. + +

      • +
      + +
        + +
      • chatUserlist
        + + Creates a list of users visible to the bot.
        + If no users are listed, the users on Synology must have permission to use the chat application + can be assigned. + +

      • +
      + +
        + +
      • storedToken
        + + Displays the stored token. + +

      • +
      + +
        + +
      • versionNotes
        + + Lists significant changes in the version history of the module. + +

      • +
      + +
    + + + Attributes +

    +
      + +
        + +
      • allowedUserForCode
        + + Names the chat users who are allowed to trigger Perl code in FHEM when the slash command /code is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForGet
        + + Names the chat users who may trigger Get commands in FHEM when the slash command /get is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForOwn
        + + Names the chat users who are allowed to trigger the commands defined in the attribute "ownCommand" in FHEM.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForSet
        + + Names the chat users who are allowed to trigger set commands in FHEM when the slash command /set is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • defaultPeer
        + + One or more (default) recipients for messages. Can be specified with the users= tag in the command asyncSendItem + can be overridden. + +

      • +
      + +
        + +
      • httptimeout <seconds>
        + + Sets the connection timeout to the chat server.
        + (default 20 seconds) + +

      • +
      + +
        + +
      • ownCommandx <Slash command> <Command>
        + + Defines a <Slash command> <Command> pair. The slash command and the command are separated by a + Separate spaces.
        + The command is executed when the SSChatBot receives the slash command. + The command can be an FHEM command or Perl code. Perl code must be enclosed in { }.

        + +
          + Examples:
          + attr <Name> ownCommand1 /Wozi_Temp {ReadingsVal("eg.wz.wallthermostat","measured-temp",0)}
          + attr <Name> ownCommand2 /Wetter get MyWetter wind_speed
          +
        + +

      • +
      + +
        + +
      • showTokenInLog
        + + If set, the transmitted bot token is displayed in the log with verbose 4/5.
        + (default: 0) + +

      • +
      + +
    + +
+ +=end html +=begin html_DE + + +

SSChatBot

+
    + Mit diesem Modul erfolgt die Integration des Synology Chat Servers in FHEM. Dadurch ist es möglich, + Nachrichten zwischen FHEM und Synology Chat Server auszutauschen.
    + Eine ausführliche Beschreibung des Moduls ist im + Wiki vorhanden.
    +

    + + + Definition +

    +
      + + Die Definition erfolgt mit:

      +
        + define <Name> SSChatBot <IP> [Port] [Protokoll] +
      +
      + + Die Angaben Port und Protokoll sind optional. +

      +
        +
      • IP: IP-Adresse oder Name der Synology Diskstation. Wird der Name benutzt, ist das globale Attribut dnsServer zu setzen.
      • +
      • Port: Port der Synology Diskstation (default 5000)
      • +
      • Protokoll: Protokoll für Messages Richtung Chat-Server, http oder https (default http)
      • +
      +
      + + Bei der Definition wird neben dem SSChaBot Device ebenfalls ein extra FHEMWEB Device zum Nachrichtenempfang automatisiert + im Raum "Chat" angelegt. Der Port des FHEMWEB Devices wird automatisch ermittelt mit Startport 8082. Ist dieser Port belegt, + werden die Ports in aufsteigender Reihenfolge auf eine eventuelle Belegung durch ein FHEMWEB Device geprüft und der nächste + freie Port wird dem neuen Device zugewiesen.
      + + Die Chatintegration unterscheidet zwischen "Eingehende Nachrichten" (FHEM -> Chat) und "Ausgehende Nachrichten" + (Chat -> FHEM).
      + +
    +

    + + + Konfiguration +

    +
      + + Für die Aktivierung eingehender Nachrichten (FHEM -> Chat) wird ein Bot-Token benötigt. Dieser Token wird über die benutzerdefinierte + Einbindungsfunktionen in der Synology Chat-Applikation erstellt bzw. kann darüber auch verändert werden. + (siehe dazu auch den Wiki-Abschnitt )
      + + Der Token wird in das neu definierten SSChatBot-Device eingefügt mit dem Befehl: +

      +
        + set <Name> botToken U6FOMH9IgT22ECJceaIW0fNwEi7VfqWQFPMgJQUJ6vpaGoWZ1SJkOGP7zlVIscCp +
      +
      + Es ist natürlich der reale, durch die Chat-Applikation erstellte Token einzusetzen. +

      + + Zur Aktivierung ausgehende Nachrichten (Chat -> FHEM) muß in der Chat-Applikation das Feld Ausgehende URL gefüllt werden. + Klicken Sie dazu auf das Symbol Profilfoto oben rechts in der aufgerufenen Synology Chat-Applikation und wählen Sie + "Einbindung". Wählen sie dann im Menü "Bots" den im ersten Schritt erstellten Bot aus. + + In das Feld Ausgehende URL wird nun der Wert des Internals OUTDEF des erstellten SSChatBot Devices hineinkopiert.
      + Zum Beispiel könnte der String so aussehen:

      + +
        + http://myserver.mydom:8086/sschat/outchat?botname=SynChatBot&fwcsrf=5de17731 +
      +
      + + (siehe dazu auch den Wiki-Abschnitt )
      + +

      + Allgemeine Information zum Nachrichtenversand
      + Nachrichten, die FHEM an den Chat Server sendet (eingehende Nachrichten), werden in FHEM zunächst in eine Queue gestellt. + Der Sendeprozess wird sofort gestartet. War die Übermittlung erfolgreich, wird die Nachricht aus der Queue gelöscht. + Anderenfalls verbleibt sie in der Queue und der Sendeprozess wird, in einem von der Anzahl der Fehlversuche abhängigen + Zeitintervall, erneut gestartet.
      + Mit dem Set-Befehl restartSendqueue kann die Abarbeitung der Queue manuell angestartet werden + (zum Beispiel nach einer Synology Wartung). + +

      + + Allgemeine Information zum Nachrichtempfang
      + Um Befehle vom Chat Server an FHEM zu senden, werden Slash-Befehle (/) verwendet. Sie sind vor der Verwendung im Synology + Chat und ggf. zusätzlich im SSChatBot Device (User spezifische Befehle) zu konfigurieren.

      + + Folgende Befehlsformen werden unterstützt:

      +
        +
      • /set
      • +
      • /get
      • +
      • /code
      • +
      • /<User spezifischer Befehl> (siehe Attribut ownCommandx)
      • +
      +
      + + Weitere ausfühliche Informationen zur Konfiguration des Nachrichtenempfangs sind im entsprechenden + Wiki-Abschnitt enthalten. + +
    +

    + + + Set +

    +
      + +
        + +
      • asyncSendItem <Item>
        + + Sendet eine Nachricht an einen oder mehrere Chatempfänger.
        + Für weitere Informationen zu den verfügbaren Optionen für asyncSendItem, insbesondere zur Benutzung von interaktiven + Objekten (Schaltflächen), konsultieren sie bitte diesen + Wiki-Abschnitt. +

        + +
          + Beispiele:
          + set <Name> asyncSendItem Erste zu postende Nachrichtenzeile.\n Sie können auch eine zweite Nachrichtenzeile haben. [users="<User>"]
          + set <Name> asyncSendItem text="Erste zu postende Nachrichtenzeile.\n Sie können auch eine zweite Nachrichtenzeile haben." [users="<User>"]
          + set <Name> asyncSendItem text="https://www.synology.com" [users="<User>"]
          + set <Name> asyncSendItem text="Überprüfen Sie dies!! <https://www.synology.com|Click hier> für Einzelheiten!" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="ein lustiges Bild" fileUrl="http://imgur.com/xxxxx" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="aktuelles Plotfile" svg="<SVG-Device>[,<Zoom>][,<Offset>]" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="<Mitteilungstext>" attachments="[{ + "callback_id": "<Text für Reading recCallbackId>", "text": "<Überschrift des Buttons>", + "actions":[{"type": "button", "name": "<Text>", "value": "<Wert>", "text": "<Text>", "style": "<Farbe>"}] }]"
          +
        + +

      • +
      + +
        + +
      • botToken <Token>
        + + Seichert den Token für den Zugriff auf den Chat als Bot. + +

      • +
      + +
        + +
      • listSendqueue
        + + Zeigt die noch an den Chat zu übertragenden Nachrichten.
        + Alle zu sendenden Nachrichten werden zunächst in einer Queue gespeichert und asynchron zum Chatserver übertragen. + +

      • +
      + +
        + +
      • purgeSendqueue <-all- | -permError- | index>
        + + Löscht Einträge aus der Sendequeue.

        + +
          +
        • -all- : Löscht alle Einträge der Sendqueue.
        • +
        • -permError- : Löscht alle Einträge der Sendqueue mit "permanent Error" Status.
        • +
        • index : Löscht ausgewählten Eintrag mit "index".
          + Die Einträge in der Sendqueue kann man sich vorher mit "set listSendqueue" ansehen um den gewünschten Index zu finden.
        • +
        + +

      • +
      + +
        + +
      • restartSendqueue [force]
        + + Startet die Abarbeitung der Sendequeue manuell neu.
        + Eventuell in der Sendequeue vorhandene Einträge mit der Kennzeichnung forbidSend werden nicht erneut versendet.
        + Erfolgt der Aufruf mit der Option force, werden auch Einträge mit der Kennzeichnung forbidSend berücksichtigt. + +

      • +
      + +
    + + + Get +

    +
      + +
        + +
      • chatChannellist
        + + Erstellt eine Liste der für den Bot sichtbaren Channels. + +

      • +
      + +
        + +
      • chatUserlist
        + + Erstellt eine Liste der für den Bot sichtbaren Usern.
        + Sollten keine User gelistet werden, muss den Usern auf der Synology die Berechtigung für die Chat-Anwendung + zugewiesen werden. + +

      • +
      + +
        + +
      • storedToken
        + + Zeigt den gespeicherten Token an. + +

      • +
      + +
        + +
      • versionNotes
        + + Listet wesentliche Änderungen in der Versionshistorie des Moduls auf. + +

      • +
      + +
    + + + Attribute +

    +
      + +
        + +
      • allowedUserForCode
        + + Benennt die Chat-User, die Perl-Code in FHEM auslösen dürfen wenn der Slash-Befehl /code empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForGet
        + + Benennt die Chat-User, die Get-Kommandos in FHEM auslösen dürfen wenn der Slash-Befehl /get empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForOwn
        + + Benennt die Chat-User, die die im Attribut "ownCommand" definierte Kommandos in FHEM auslösen dürfen.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForSet
        + + Benennt die Chat-User, die Set-Kommandos in FHEM auslösen dürfen wenn der Slash-Befehl /set empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • defaultPeer
        + + Ein oder mehrere (default) Empfänger für Nachrichten. Kann mit dem users= Tag im Befehl asyncSendItem + übersteuert werden. + +

      • +
      + +
        + +
      • httptimeout <Sekunden>
        + + Stellt den Verbindungstimeout zum Chatserver ein.
        + (default 20 Sekunden) + +

      • +
      + +
        + +
      • ownCommandx <Slash-Befehl> <Kommando>
        + + Definiert ein <Slash-Befehl> <Kommando> Paar. Der Slash-Befehl und das Kommando sind durch ein + Leerzeichen zu trennen.
        + Das Kommando wird ausgeführt wenn der SSChatBot den Slash-Befehl empfängt. + Das Kommando kann ein FHEM Befehl oder Perl-Code sein. Perl-Code ist in { } einzuschließen.

        + +
          + Beispiele:
          + attr <Name> ownCommand1 /Wozi_Temp {ReadingsVal("eg.wz.wandthermostat","measured-temp",0)}
          + attr <Name> ownCommand2 /Wetter get MyWetter wind_speed
          +
        + +

      • +
      + +
        + +
      • showTokenInLog
        + + Wenn gesetzt, wird im Log mit verbose 4/5 der übermittelte Bot-Token angezeigt.
        + (default: 0) + +

      • +
      + +
    + +
+ +=end html_DE + +=for :application/json;q=META.json 50_SSChatBot.pm +{ + "abstract": "Integration of Synology Chat Server into FHEM.", + "x_lang": { + "de": { + "abstract": "Integration des Synology Chat Servers in FHEM." + } + }, + "keywords": [ + "synology", + "synologychat", + "chatbot", + "chat", + "messenger" + ], + "version": "v1.1.1", + "release_status": "stable", + "author": [ + "Heiko Maaz " + ], + "x_fhem_maintainer": [ + "DS_Starter" + ], + "x_fhem_maintainer_github": [ + "nasseeder1" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.014, + "JSON": 0, + "Data::Dumper": 0, + "MIME::Base64": 0, + "Time::HiRes": 0, + "HttpUtils": 0, + "Encode": 0, + "Net::Domain": 0 + }, + "recommends": { + "FHEM::Meta": 0 + }, + "suggests": { + } + } + }, + "resources": { + "x_wiki": { + "web": "https://wiki.fhem.de/wiki/SSChatBot - Integration des Synology Chat Servers", + "title": "SSChatBot - Integration des Synology Chat Servers" + }, + "repository": { + "x_dev": { + "type": "svn", + "url": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter", + "web": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/50_SSChatBot.pm", + "x_branch": "dev", + "x_filepath": "fhem/contrib/", + "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/50_SSChatBot.pm" + } + } + } +} +=end :application/json;q=META.json + +=cut diff --git a/MAINTAINER.txt b/MAINTAINER.txt index ab0bda677..a810b16ac 100644 --- a/MAINTAINER.txt +++ b/MAINTAINER.txt @@ -264,6 +264,7 @@ FHEM/49_SSCamSTRM.pm DS_Starter Sonstiges FHEM/49_TBot_List.pm viegener Unterstützende Dienste FHEM/50_HP1000.pm loredo Unterstützende Dienste/Wettermodule FHEM/50_MOBILEALERTSGW.pm MarkusF Sonstige Systeme +FHEM/50_SSChatBot.pm DS_Starter Sonstiges FHEM/50_TelegramBot.pm viegener Unterstützende Dienste FHEM/50_WS300.pm Dirk SlowRF FHEM/51_I2C_BH1750.pm arnoaugustin Einplatinencomputer (bitte auch PM) diff --git a/contrib/DS_Starter/49_SSCam.pm b/contrib/DS_Starter/49_SSCam.pm index 5775e5d63..ac629b707 100644 --- a/contrib/DS_Starter/49_SSCam.pm +++ b/contrib/DS_Starter/49_SSCam.pm @@ -1,5 +1,5 @@ ######################################################################################################################## -# $Id: 49_SSCam.pm 22592 2020-08-12 21:28:56Z DS_Starter $ +# $Id: 49_SSCam.pm 22600 2020-08-14 19:22:36Z DS_Starter $ ######################################################################################################################### # 49_SSCam.pm # @@ -136,9 +136,9 @@ BEGIN { TelegramBot_AttrNum TelegramBot_Callback TelegramBot_BinaryFileRead - SSChatBot_formString - SSChatBot_addQueue - SSChatBot_getapisites + FHEM::SSChatBot::formString + FHEM::SSChatBot::addQueue + FHEM::SSChatBot::getApiSites ) ); @@ -159,6 +159,7 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "9.7.0" => "17.08.2020 compatibility to SSChatBot version 1.10.0 ", "9.6.1" => "13.08.2020 avoid warnings during FHEM shutdown/restart ", "9.6.0" => "12.08.2020 new attribute ptzNoCapPrePat ", "9.5.3" => "27.07.2020 fix warning: Use of uninitialized value in subroutine dereference at ... ", @@ -684,7 +685,7 @@ sub Define { return "Error: Perl module ".$SScamMMDBI." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SScamMMDBI); - my @a = split("[ \t][ \t]*", $def); + my @a = split m{\s+}x, $def; if(int(@a) < 4) { return "You need to specify more parameters.\n". "Format: define SSCAM [Port]"; @@ -9720,7 +9721,7 @@ sub sendChat { my $name = $hash->{NAME}; my $type = AttrVal($name,"cacheType","internal"); my $mtype = ""; - my ($ret,$cache); + my ($params,$ret,$cache); Log3($name, 4, "$name - ####################################################"); Log3($name, 4, "$name - ### start send Snap or Video by SSChatBot "); @@ -9835,8 +9836,19 @@ sub sendChat { # Eintrag zur SendQueue hinzufügen # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) $fileUrl = $rootUrl."/".$mtype."/".$fname; - $subject = SSChatBot_formString ($subject, "text"); - $ret = SSChatBot_addQueue ($chatbot, "sendItem", "chatbot", $uid, $subject, $fileUrl, "", ""); + $subject = FHEM::SSChatBot::formString ($subject, "text"); + + $params = { + name => $chatbot, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $subject, + fileUrl => $fileUrl, + channel => "", + attachment => "" + }; + $ret = FHEM::SSChatBot::addQueue ($params); if($ret) { readingsSingleUpdate($hash, "sendChatState", $ret, 1); @@ -9895,8 +9907,19 @@ sub sendChat { # Eintrag zur SendQueue hinzufügen # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) $fileUrl = $rootUrl."/".$mtype."/".$fname; - $subject = SSChatBot_formString ($subject, "text"); - $ret = SSChatBot_addQueue ($chatbot, "sendItem", "chatbot", $uid, $subject, $fileUrl, "", ""); + $subject = FHEM::SSChatBot::formString ($subject, "text"); + + $params = { + name => $chatbot, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $subject, + fileUrl => $fileUrl, + channel => "", + attachment => "" + }; + $ret = FHEM::SSChatBot::addQueue ($params); if($ret) { readingsSingleUpdate($hash, "sendChatState", $ret, 1); @@ -9913,7 +9936,7 @@ sub sendChat { Log3($name, 1, "$name - Send Counter transaction \"$tac\": ".$data{SSCam}{$name}{SENDCOUNT}{$tac}) if(AttrVal($name,"debugactivetoken",0)); } - SSChatBot_getapisites($chatbot); # Übertragung Sendqueue starten + FHEM::SSChatBot::getApiSites ($chatbot); # Übertragung Sendqueue starten # use strict "refs"; undef %chatparams; @@ -11441,12 +11464,12 @@ sub setVersionInfo { if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) { # META-Daten sind vorhanden $modules{$type}{META}{version} = "v".$v; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}} - if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id: 49_SSCam.pm 22592 2020-08-12 21:28:56Z DS_Starter $ im Kopf komplett! vorhanden ) + if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id: 49_SSCam.pm 22600 2020-08-14 19:22:36Z DS_Starter $ im Kopf komplett! vorhanden ) $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/gx; } else { $modules{$type}{META}{x_version} = $v; } - return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 49_SSCam.pm 22592 2020-08-12 21:28:56Z DS_Starter $ im Kopf komplett! vorhanden ) + return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 49_SSCam.pm 22600 2020-08-14 19:22:36Z DS_Starter $ im Kopf komplett! vorhanden ) if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) { # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen # mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden diff --git a/contrib/DS_Starter/50_SSChatBot.pm b/contrib/DS_Starter/50_SSChatBot.pm index 4c97f0b06..3e3b22e1f 100644 --- a/contrib/DS_Starter/50_SSChatBot.pm +++ b/contrib/DS_Starter/50_SSChatBot.pm @@ -31,24 +31,82 @@ # Example of defining a Bot: define SynChatBot SSChatBot 192.168.2.20 [5000] [HTTP(S)] # -package main; +package FHEM::SSChatBot; ## no critic 'package' use strict; use warnings; -eval "use JSON;1;" or my $SSChatBotMM = "JSON"; ## no critic 'eval' # Debian: apt-get install libjson-perl +use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use Data::Dumper; # Perl Core module use MIME::Base64; -use Time::HiRes; +use Time::HiRes qw(gettimeofday); use HttpUtils; -use Encode; -no if $] >= 5.017011, warnings => 'experimental::smartmatch'; -eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' +use Encode; +eval "use JSON;1;" or my $SSChatBotMM = "JSON"; ## no critic 'eval' # Debian: apt-get install libjson-perl +eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' eval "use Net::Domain qw(hostname hostfqdn hostdomain domainname);1" or my $SSChatBotNDom = "Net::Domain"; ## no critic 'eval' - -# no if $] >= 5.017011, warnings => 'experimental'; +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw( + AnalyzePerlCommand + AnalyzeCommandChain + asyncOutput + addToDevAttrList + AttrVal + attr + CancelDelayedShutdown + CommandSet + CommandAttr + CommandDefine + CommandGet + data + defs + devspec2array + FmtDateTime + getKeyValue + HttpUtils_NonblockingGet + init_done + InternalTimer + IsDisabled + IsDevice + Log + Log3 + modules + parseParams + plotAsPng + readingFnAttributes + ReadingsVal + RemoveInternalTimer + readingsBeginUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsEndUpdate + setKeyValue + urlDecode + FW_wname + ) + ); + + # Export to main context with different name + # my $pkg = caller(0); + # my $main = $pkg; + # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/gx; + # foreach (@_) { + # *{ $main . $_ } = *{ $pkg . '::' . $_ }; + # } + GP_Export( + qw( + Initialize + ) + ); +} # Versions History intern -my %SSChatBot_vNotesIntern = ( +my %vNotesIntern = ( + "1.10.0" => "17.08.2020 switch to packages, finalise for repo checkin ", "1.9.0" => "30.07.2020 restartSendqueue option 'force' added ", "1.8.0" => "27.05.2020 send SVG Plots with options like svg=',,' possible ", "1.7.0" => "26.05.2020 send SVG Plots possible ", @@ -56,7 +114,7 @@ my %SSChatBot_vNotesIntern = ( "1.6.0" => "22.05.2020 replace \" H\" with \"%20H\" in attachments due to problem in HttpUtils ", "1.5.0" => "15.03.2020 slash commands set in interactive answer field 'value' will be executed ", "1.4.0" => "15.03.2020 rename '1_sendItem' to 'asyncSendItem' because of Aesthetics ", - "1.3.1" => "14.03.2020 new reading recActionsValue which extract the value from actions, review logs of SSChatBot_CGI ", + "1.3.1" => "14.03.2020 new reading recActionsValue which extract the value from actions, review logs of botCGI ", "1.3.0" => "13.03.2020 rename 'sendItem' to '1_sendItem', allow attachments ", "1.2.2" => "07.02.2020 add new permanent error 410 'message too long' ", "1.2.1" => "27.01.2020 replace \" H\" with \"%20H\" in payload due to problem in HttpUtils ", @@ -67,7 +125,8 @@ my %SSChatBot_vNotesIntern = ( ); # Versions History extern -my %SSChatBot_vNotesExtern = ( +my %vNotesExtern = ( + "1.7.0" => "26.05.2020 Now it is possible to send SVG plots very easily with the command asyncSendItem ", "1.4.0" => "15.03.2020 Command '1_sendItem' renamed to 'asyncSendItem' because of Aesthetics ", "1.3.0" => "13.03.2020 The set command 'sendItem' was renamed to '1_sendItem' to avoid changing the botToken by chance. ". "Also attachments are allowed now in the '1_sendItem' command. ", @@ -75,7 +134,17 @@ my %SSChatBot_vNotesExtern = ( "1.0.0" => "08.12.2019 initial " ); -my %SSChatBot_errlist = ( +# Hint hash EN +my %vHintsExt_en = ( + +); + +# Hint hash DE +my %vHintsExt_de = ( + +); + +my %errList = ( 100 => "Unknown error", 101 => "Payload is empty", 102 => "API does not exist - may be the Synology Chat Server package is stopped", @@ -92,20 +161,37 @@ my %SSChatBot_errlist = ( 900 => "malformed JSON string received from Synology Chat Server", ); -# Standardvariablen und Forward-Deklaration -use vars qw(%SSChatBot_vHintsExt_en); -use vars qw(%SSChatBot_vHintsExt_de); +my %hset = ( # Hash für Set-Funktion + botToken => { fn => "_setbotToken" }, + listSendqueue => { fn => "_setlistSendqueue" }, + purgeSendqueue => { fn => "_setpurgeSendqueue" }, + asyncSendItem => { fn => "_setasyncSendItem" }, + restartSendqueue => { fn => "_setrestartSendqueue" }, +); + +my %hget = ( # Hash für Get-Funktion + storedToken => { fn => "_getstoredToken" }, + chatUserlist => { fn => "_getchatUserlist" }, + chatChannellist => { fn => "_getchatChannellist" }, + versionNotes => { fn => "_getversionNotes" }, +); + +my %hrecbot = ( # Hash für botCGI receice Slash-commands (/set, /get, /code) + set => { fn => "__botCGIrecSet" }, + get => { fn => "__botCGIrecGet" }, + cod => { fn => "__botCGIrecCod" }, +); ################################################################ -sub SSChatBot_Initialize { +sub Initialize { my ($hash) = @_; - $hash->{DefFn} = "SSChatBot_Define"; - $hash->{UndefFn} = "SSChatBot_Undef"; - $hash->{DeleteFn} = "SSChatBot_Delete"; - $hash->{SetFn} = "SSChatBot_Set"; - $hash->{GetFn} = "SSChatBot_Get"; - $hash->{AttrFn} = "SSChatBot_Attr"; - $hash->{DelayedShutdownFn} = "SSChatBot_DelayedShutdown"; + $hash->{DefFn} = \&Define; + $hash->{UndefFn} = \&Undef; + $hash->{DeleteFn} = \&Delete; + $hash->{SetFn} = \&Set; + $hash->{GetFn} = \&Get; + $hash->{AttrFn} = \&Attr; + $hash->{DelayedShutdownFn} = \&delayedShutdown; $hash->{FW_deviceOverview} = 1; $hash->{AttrList} = "disable:1,0 ". @@ -129,14 +215,14 @@ return; # ($hash) [1] [2] [3] [4] # ################################################################ -sub SSChatBot_Define { +sub Define { my ($hash, $def) = @_; my $name = $hash->{NAME}; return "Error: Perl module ".$SSChatBotMM." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SSChatBotMM); return "Error: Perl module ".$SSChatBotNDom." is missing." if($SSChatBotNDom); - my @a = split("[ \t][ \t]*", $def); + my @a = split m{\s+}x, $def; if(int(@a) < 2) { return "You need to specify more parameters.\n". "Format: define SSChatBot [Port] [HTTP(S)]"; @@ -161,10 +247,10 @@ sub SSChatBot_Define { $hash->{HELPER}{CHATEXTERNAL} = "SYNO.Chat.External"; # Versionsinformationen setzen - SSChatBot_setVersionInfo($hash); + setVersionInfo($hash); # Token lesen - SSChatBot_getToken($hash,1,"botToken"); + getToken($hash,1,"botToken"); # Index der Sendequeue initialisieren $data{SSChatBot}{$name}{sendqueue}{index} = 0; @@ -175,7 +261,7 @@ sub SSChatBot_Define { readingsEndUpdate ($hash,1); # initiale Routinen nach Start ausführen , verzögerter zufälliger Start - SSChatBot_initonboot($hash); + initOnBoot($hash); return; } @@ -190,13 +276,14 @@ return; # internen Timern, sofern diese im Modul zum Pollen verwendet # wurden. ################################################################ -sub SSChatBot_Undef { +sub Undef { my ($hash, $arg) = @_; my $name = $hash->{NAME}; delete $data{SSChatBot}{$name}; - SSChatBot_removeExtension($hash->{HELPER}{INFIX}); - RemoveInternalTimer($hash); + + removeExtension ($hash->{HELPER}{INFIX}); + RemoveInternalTimer ($hash); return; } @@ -208,9 +295,9 @@ return; # Sobald alle nötigen Maßnahmen erledigt sind, muss der Abschluss mit CancelDelayedShutdown($name) an # FHEM zurückgemeldet werden. ####################################################################################################### -sub SSChatBot_DelayedShutdown { - my ($hash) = @_; - my $name = $hash->{NAME}; +sub delayedShutdown { + my $hash = shift; + my $name = $hash->{NAME}; return 0; } @@ -225,7 +312,7 @@ return 0; # FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen # Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. ################################################################# -sub SSChatBot_Delete { +sub Delete { my ($hash, $arg) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_botToken"; @@ -237,7 +324,7 @@ return; } ################################################################ -sub SSChatBot_Attr { +sub Attr { my ($cmd,$name,$aName,$aVal) = @_; my $hash = $defs{$name}; my ($do,$val); @@ -257,7 +344,7 @@ sub SSChatBot_Attr { if ($do == 1) { RemoveInternalTimer($hash); } else { - InternalTimer(gettimeofday()+2, "SSChatBot_initonboot", $hash, 0) if($init_done); + InternalTimer(gettimeofday()+2, "FHEM::SSChatBot::initOnBoot", $hash, 0) if($init_done); } readingsBeginUpdate($hash); @@ -270,9 +357,9 @@ sub SSChatBot_Attr { unless ($aVal =~ /^\d+$/x) { return "The Value for $aName is not valid. Use only figures 1-9 !";} } - if ($aName =~ m/ownCommand([1-9][0-9]*)$/) { + if ($aName =~ m/ownCommand([1-9][0-9]*)$/x) { my $num = $1; - return qq{The value of $aName must start with a slash like "/Weather ".} unless ($aVal =~ /^\/.*$/); + return qq{The value of $aName must start with a slash like "/Weather ".} unless ($aVal =~ /^\//x); addToDevAttrList($name, "ownCommand".($num+1)); # add neue ownCommand dynamisch } } @@ -281,21 +368,21 @@ return; } ################################################################ -sub SSChatBot_Set { ## no critic 'complexity' +# Set und Subroutinen +################################################################ +sub Set { my ($hash, @a) = @_; return qq{"set X" needs at least an argument} if ( @a < 2 ); my @items = @a; my $name = shift @a; my $opt = shift @a; my $prop = shift @a; - my $prop1 = shift @a; - my $prop2 = shift @a; - my $prop3 = shift @a; - my ($success,$setlist); + + my $setlist; return if(IsDisabled($name)); - my $idxlist = join(",", SSChatBot_sortVersion("asc",keys %{$data{SSChatBot}{$name}{sendqueue}{entries}})); + my $idxlist = join(",", sortVersion("asc",keys %{$data{SSChatBot}{$name}{sendqueue}{entries}})); if(!$hash->{TOKEN}) { # initiale setlist für neue Devices @@ -311,147 +398,227 @@ sub SSChatBot_Set { ## no critic 'complexity' "asyncSendItem:textField-long " ; } - - if ($opt eq "botToken") { - return "The command \"$opt\" needs an argument." if (!$prop); - ($success) = SSChatBot_setToken($hash,$prop,"botToken"); - - if($success) { - CommandGet(undef, "$name chatUserlist"); # Chatuser Liste abrufen - return qq{botToken saved successfully}; - } else { - return qq{Error while saving botToken - see logfile for details}; - } - - } elsif ($opt eq "listSendqueue") { - my $sub = sub ($) { - my $idx = shift; - my $ret; - foreach my $key (reverse sort keys %{$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}}) { - $ret .= ", " if($ret); - $ret .= $key."=>".$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{$key}; - } - return $ret; - }; - - if (!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { - return qq{SendQueue is empty.}; - } - my $sq; - foreach my $idx (sort{$a<=>$b} keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { - $sq .= $idx." => ".$sub->($idx)."\n"; - } - return $sq; - } elsif ($opt eq "purgeSendqueue") { - if($prop eq "-all-") { - delete $hash->{OPIDX}; - delete $data{SSChatBot}{$name}{sendqueue}{entries}; - $data{SSChatBot}{$name}{sendqueue}{index} = 0; - return "All entries of SendQueue are deleted"; - - } elsif($prop eq "-permError-") { - foreach my $idx (keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { - delete $data{SSChatBot}{$name}{sendqueue}{entries}{$idx} - if($data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend}); - } - return qq{All entries with state "permanent send error" are deleted}; - - } else { - delete $data{SSChatBot}{$name}{sendqueue}{entries}{$prop}; - return qq{SendQueue entry with index "$prop" deleted}; - } + my $params = { + hash => $hash, + name => $name, + opt => $opt, + prop => $prop, + aref => \@items, + }; - } elsif ($opt eq "asyncSendItem") { - # einfachster Sendetext users="user1" - # text="First line of message to post.\nAlso you can have a second line of message." users="user1" - # text="" users="user1" - # text="Check this!! for details!" users="user1,user2" - # text="a fun image" fileUrl="http://imgur.com/xxxxx" users="user1,user2" - # text="aktuelles SVG-Plot" svg=",," users="user1,user2" - delete $hash->{HELPER}{RESENDFORCE}; # Option 'force' löschen (könnte durch restartSendqueue gesetzt sein) - return if(!$hash->{HELPER}{USERFETCHED}); - my ($text,$users,$svg); - my ($fileUrl,$attachment) = ("",""); - my $cmd = join(" ", map { my $p = $_; $p =~ s/\s//g; $p; } @items); - my ($arr,$h) = parseParams($cmd); - - if($h) { - $text = $h->{text} if(defined $h->{text}); - $users = $h->{users} if(defined $h->{users}); - $fileUrl = $h->{fileUrl} if(defined $h->{fileUrl}); # ein File soll über einen Link hochgeladen und versendet werden - $svg = $h->{svg} if(defined $h->{svg}); # ein SVG-Plot soll versendet werden - $attachment = SSChatBot_formString($h->{attachments}, "attachement") if(defined $h->{attachments}); - } - - if($arr) { - my @t = @{$arr}; - shift @t; shift @t; - $text = join(" ", @t) if(!$text); - } - - if($svg) { # Versenden eines Plotfiles - my ($err, $file) = SSChatBot_PlotToFile ($name, $svg); - return if($err); - - my $FW = $hash->{FW}; - my $csrf = $defs{$FW}{CSRFTOKEN} // ""; - $fileUrl = (split("sschat", $hash->{OUTDEF}))[0]; - $fileUrl .= "sschat/www/images/$file?&fwcsrf=$csrf"; - - $fileUrl = SSChatBot_formString($fileUrl, "text"); - $text = $svg if(!$text); # Name des SVG-Plots + Optionen als Standardtext - } - - return qq{Your sendstring is incorrect. It must contain at least text with the "text=" tag like text="..."\nor only some text like "this is a test" without the "text=" tag.} if(!$text); - - $text = SSChatBot_formString($text, "text"); - - $users = AttrVal($name,"defaultPeer", "") if(!$users); - return "You haven't defined any receptor for send the message to. ". - "You have to use the \"users\" tag or define default receptors with attribute \"defaultPeer\"." if(!$users); - - # User aufsplitten und zu jedem die ID ermitteln - my @ua = split(/,/, $users); - foreach (@ua) { - next if(!$_); - my $uid = $hash->{HELPER}{USERS}{$_}{id}; - return qq{The receptor "$_" seems to be unknown because its ID coulnd't be found.} if(!$uid); - - # Eintrag zur SendQueue hinzufügen - # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) - SSChatBot_addQueue($name, "sendItem", "chatbot", $uid, $text, $fileUrl, "", $attachment); - } - - SSChatBot_getapisites($name); - - } elsif ($opt eq "restartSendqueue") { - if($prop && $prop eq "force") { - $hash->{HELPER}{RESENDFORCE} = 1; - } else { - delete $hash->{HELPER}{RESENDFORCE}; - } - my $ret = SSChatBot_getapisites($name); - return $ret if($ret); - return qq{The SendQueue has been restarted.}; - - } else { - return "$setlist"; + no strict "refs"; ## no critic 'NoStrict' + if($hset{$opt}) { + my $ret = ""; + $ret = &{$hset{$opt}{fn}} ($params) if(defined &{$hset{$opt}{fn}}); + return $ret; } + use strict "refs"; +return $setlist; +} + +################################################################ +# Setter botToken +################################################################ +sub _setbotToken { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $opt = $paref->{opt}; + my $prop = $paref->{prop}; + + return qq{The command "$opt" needs an argument.} if (!$prop); + my ($success) = setToken($hash, $prop, "botToken"); + + if($success) { + CommandGet(undef, "$name chatUserlist"); # Chatuser Liste abrufen + return qq{botToken saved successfully}; + } else { + return qq{Error while saving botToken - see logfile for details}; + } + return; } ################################################################ -sub SSChatBot_Get { ## no critic 'complexity' +# Setter listSendqueue +################################################################ +sub _setlistSendqueue { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + + my $sub = sub { + my $idx = shift; + my $ret; + for my $key (reverse sort keys %{$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}}) { + $ret .= ", " if($ret); + $ret .= $key."=>".$data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{$key}; + } + return $ret; + }; + + if (!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + return qq{SendQueue is empty.}; + } + + my $sq; + for my $idx (sort{$a<=>$b} keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + $sq .= $idx." => ".$sub->($idx)."\n"; + } + +return $sq; +} + +################################################################ +# Setter purgeSendqueue +################################################################ +sub _setpurgeSendqueue { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop}; + + if($prop eq "-all-") { + delete $hash->{OPIDX}; + delete $data{SSChatBot}{$name}{sendqueue}{entries}; + $data{SSChatBot}{$name}{sendqueue}{index} = 0; + return "All entries of SendQueue are deleted"; + + } elsif($prop eq "-permError-") { + for my $idx (keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { + delete $data{SSChatBot}{$name}{sendqueue}{entries}{$idx} + if($data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend}); + } + return qq{All entries with state "permanent send error" are deleted}; + + } else { + delete $data{SSChatBot}{$name}{sendqueue}{entries}{$prop}; + return qq{SendQueue entry with index "$prop" deleted}; + } + +return; +} + +###################################################################################################### +# Setter asyncSendItem +# +# einfachster Sendetext users="user1" +# text="First line of message to post.\nAlso you can have a second line of message." users="user1" +# text="" users="user1" +# text="Check this!! for details!" users="user1,user2" +# text="a fun image" fileUrl="http://imgur.com/xxxxx" users="user1,user2" +# text="aktuelles SVG-Plot" svg=",," users="user1,user2" +# +###################################################################################################### +sub _setasyncSendItem { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $aref = $paref->{aref}; + + delete $hash->{HELPER}{RESENDFORCE}; # Option 'force' löschen (könnte durch restartSendqueue gesetzt sein) + return if(!$hash->{HELPER}{USERFETCHED}); + + my ($text,$users,$svg); + + my ($fileUrl,$attachment) = ("",""); + my $cmd = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @$aref; ## no critic 'Map blocks' + my ($arr,$h) = parseParams($cmd); + + if($h) { + $text = $h->{text} if(defined $h->{text}); + $users = $h->{users} if(defined $h->{users}); + $fileUrl = $h->{fileUrl} if(defined $h->{fileUrl}); # ein File soll über einen Link hochgeladen und versendet werden + $svg = $h->{svg} if(defined $h->{svg}); # ein SVG-Plot soll versendet werden + $attachment = formString($h->{attachments}, "attachement") if(defined $h->{attachments}); + } + + if($arr) { + my @t = @{$arr}; + shift @t; shift @t; + $text = join(" ", @t) if(!$text); + } + + if($svg) { # Versenden eines Plotfiles + my ($err, $file) = plotToFile ($name, $svg); + return if($err); + + my $FW = $hash->{FW}; + my $csrf = $defs{$FW}{CSRFTOKEN} // ""; + $fileUrl = (split("sschat", $hash->{OUTDEF}))[0]; + $fileUrl .= "sschat/www/images/$file?&fwcsrf=$csrf"; + + $fileUrl = formString($fileUrl, "text"); + $text = $svg if(!$text); # Name des SVG-Plots + Optionen als Standardtext + } + + return qq{Your sendstring is incorrect. It must contain at least text with the "text=" tag like text="..."\nor only some text like "this is a test" without the "text=" tag.} if(!$text); + + $text = formString($text, "text"); + + $users = AttrVal($name,"defaultPeer", "") if(!$users); + return "You haven't defined any receptor for send the message to. ". + "You have to use the \"users\" tag or define default receptors with attribute \"defaultPeer\"." if(!$users); + + # User aufsplitten und zu jedem die ID ermitteln + my @ua = split(/,/x, $users); + for my $user (@ua) { + next if(!$user); + my $uid = $hash->{HELPER}{USERS}{$user}{id}; + return qq{The receptor "$user" seems to be unknown because its ID coulnd't be found.} if(!$uid); + + # Eintrag zur SendQueue hinzufügen + # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $uid, + text => $text, + fileUrl => $fileUrl, + channel => "", + attachment => $attachment + }; + addQueue ($params); + } + + getApiSites($name); + +return; +} + +################################################################ +# Setter restartSendqueue +################################################################ +sub _setrestartSendqueue { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $prop = $paref->{prop}; + + if($prop && $prop eq "force") { + $hash->{HELPER}{RESENDFORCE} = 1; + } else { + delete $hash->{HELPER}{RESENDFORCE}; + } + + my $ret = getApiSites($name); + +return $ret if($ret); +return qq{The SendQueue has been restarted.}; +} + +################################################################ +# Get +################################################################ +sub Get { my ($hash, @a) = @_; return "\"get X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; my $arg = shift @a; - my $arg1 = shift @a; - my $arg2 = shift @a; - my $ret = ""; + my $getlist; if(!$hash->{TOKEN}) { @@ -466,154 +633,218 @@ sub SSChatBot_Get { ## no critic 'complexity' ; } - return if(IsDisabled($name)); - - if ($opt eq "storedToken") { - if (!$hash->{TOKEN}) {return qq{Token of $name is not set - make sure you've set it with "set $name botToken "};} - # Token abrufen - my ($success, $token) = SSChatBot_getToken($hash,0,"botToken"); - unless ($success) {return qq{Token couldn't be retrieved successfully - see logfile}}; - - return qq{Stored Token to act as Synology Chat Bot:\n}. - qq{=========================================\n}. - qq{$token \n} - ; - - } elsif ($opt eq "chatUserlist") { - # übergebenen CL-Hash (FHEMWEB) in Helper eintragen - SSChatBot_delclhash ($name); - SSChatBot_getclhash($hash,1); - - # Eintrag zur SendQueue hinzufügen - # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) - SSChatBot_addQueue($name, "chatUserlist", "user_list", "", "", "", "", ""); - - SSChatBot_getapisites($name); - - } elsif ($opt eq "chatChannellist") { - # übergebenen CL-Hash (FHEMWEB) in Helper eintragen - SSChatBot_delclhash ($name); - SSChatBot_getclhash($hash,1); - - # Eintrag zur SendQueue hinzufügen - # Werte: (name,opmode,method,userid,text,fileUrl,channel,attachment) - SSChatBot_addQueue($name, "chatChannellist", "channel_list", "", "", "", "", ""); - - SSChatBot_getapisites($name); - - } elsif ($opt =~ /versionNotes/x) { - my $header = "Module release information
"; - my $header1 = "Helpful hints
"; - my %hs; - - # Ausgabetabelle erstellen - my ($ret,$val0,$val1); - my $i = 0; - - $ret = ""; - - # Hints - if(!$arg || $arg =~ /hints/x || $arg =~ /[\d]+/x) { - $ret .= sprintf("
$header1
"); - $ret .= ""; - $ret .= ""; - $ret .= ""; - if($arg && $arg =~ /[\d]+/x) { - my @hints = split(",",$arg); - foreach (@hints) { - if(AttrVal("global","language","EN") eq "DE") { - $hs{$_} = $SSChatBot_vHintsExt_de{$_}; - } else { - $hs{$_} = $SSChatBot_vHintsExt_en{$_}; - } - } - } else { - if(AttrVal("global","language","EN") eq "DE") { - %hs = %SSChatBot_vHintsExt_de; - } else { - %hs = %SSChatBot_vHintsExt_en; - } - } - $i = 0; - foreach my $key (SSChatBot_sortVersion("desc",keys %hs)) { - $val0 = $hs{$key}; - $ret .= sprintf("" ); - $ret .= ""; - $i++; - if ($i & 1) { - # $i ist ungerade - $ret .= ""; - } else { - $ret .= ""; - } - } - $ret .= ""; - $ret .= ""; - $ret .= "
$key $val0
"; - $ret .= "
"; - } - - # Notes - if(!$arg || $arg =~ /rel/x) { - $ret .= sprintf("
$header
"); - $ret .= ""; - $ret .= ""; - $ret .= ""; - $i = 0; - foreach my $key (SSChatBot_sortVersion("desc",keys %SSChatBot_vNotesExtern)) { - ($val0,$val1) = split(/\s/,$SSChatBot_vNotesExtern{$key},2); - $ret .= sprintf("" ); - $ret .= ""; - $i++; - if ($i & 1) { - # $i ist ungerade - $ret .= ""; - } else { - $ret .= ""; - } - } - $ret .= ""; - $ret .= ""; - $ret .= "
$key $val0 $val1
"; - $ret .= "
"; - } - - $ret .= ""; - - return $ret; - - } else { - return "$getlist"; - } + return if(IsDisabled($name)); -return $ret; # not generate trigger out of command + my $pars = { + hash => $hash, + name => $name, + opt => $opt, + arg => $arg, + }; + + no strict "refs"; ## no critic 'NoStrict' + if($hget{$opt}) { + my $ret = ""; + $ret = &{$hget{$opt}{fn}} ($pars) if(defined &{$hget{$opt}{fn}}); + return $ret; + } + use strict "refs"; + +return $getlist; # not generate trigger out of command +} + +################################################################ +# Getter storedToken +################################################################ +sub _getstoredToken { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + if (!$hash->{TOKEN}) {return qq{Token of $name is not set - make sure you've set it with "set $name botToken "};} + + my ($success, $token) = getToken($hash,0,"botToken"); # Token abrufen + unless ($success) {return qq{Token couldn't be retrieved successfully - see logfile}}; + + return qq{Stored Token to act as Synology Chat Bot:\n}. + qq{=========================================\n}. + qq{$token \n} + ; +} + +################################################################ +# Getter chatUserlist +################################################################ +sub _getchatUserlist { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + # übergebenen CL-Hash (FHEMWEB) in Helper eintragen + delClhash ($name); + getClhash ($hash,1); + + # Eintrag zur SendQueue hinzufügen + my $params = { + name => $name, + opmode => "chatUserlist", + method => "user_list", + userid => "", + text => "", + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + getApiSites ($name); + +return; +} + +################################################################ +# Getter chatChannellist +################################################################ +sub _getchatChannellist { ## no critic "not used" + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + + # übergebenen CL-Hash (FHEMWEB) in Helper eintragen + delClhash ($name); + getClhash ($hash,1); + + # Eintrag zur SendQueue hinzufügen + my $params = { + name => $name, + opmode => "chatChannellist", + method => "channel_list", + userid => "", + text => "", + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + getApiSites ($name); + +return; +} + +################################################################ +# Getter versionNotes +################################################################ +sub _getversionNotes { ## no critic "not used" + my $paref = shift; + my $arg = $paref->{arg}; + + my $header = "Module release information
"; + my $header1 = "Helpful hints
"; + my $ret = ""; + my %hs; + + # Ausgabetabelle erstellen + my ($val0,$val1); + my $i = 0; + + $ret = ""; + + # Hints + if(!$arg || $arg =~ /hints/x || $arg =~ /[\d]+/x) { + $ret .= sprintf("
$header1
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + if($arg && $arg =~ /[\d]+/x) { + my @hints = split(",",$arg); + for my $hint (@hints) { + if(AttrVal("global","language","EN") eq "DE") { + $hs{$hint} = $vHintsExt_de{$hint}; + } else { + $hs{$hint} = $vHintsExt_en{$hint}; + } + } + } else { + if(AttrVal("global","language","EN") eq "DE") { + %hs = %vHintsExt_de; + } else { + %hs = %vHintsExt_en; + } + } + $i = 0; + for my $key (sortVersion("desc",keys %hs)) { + $val0 = $hs{$key}; + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0
"; + $ret .= "
"; + } + + # Notes + if(!$arg || $arg =~ /rel/x) { + $ret .= sprintf("
$header
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $i = 0; + for my $key (sortVersion("desc",keys %vNotesExtern)) { + ($val0,$val1) = split(/\s/x, $vNotesExtern{$key},2); + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0 $val1
"; + $ret .= "
"; + } + + $ret .= ""; + +return $ret; } ###################################################################################### # initiale Startroutinen nach Restart FHEM ###################################################################################### -sub SSChatBot_initonboot { - my ($hash) = @_; - my $name = $hash->{NAME}; +sub initOnBoot { + my $hash = shift; + my $name = $hash->{NAME}; my ($ret,$csrf,$fuuid); - RemoveInternalTimer($hash, "SSChatBot_initonboot"); + RemoveInternalTimer($hash, "FHEM::SSChatBot::initOnBoot"); if ($init_done) { # check ob FHEMWEB Instanz für SSChatBot angelegt ist -> sonst anlegen my @FWports; my $FWname = "sschat"; # der Pfad nach http://hostname:port/ der neuen FHEMWEB Instanz -> http://hostname:port/sschat my $FW = "WEBSSChatBot"; # Name der FHEMWEB Instanz für SSChatBot - foreach ( devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ) { - $hash->{FW} = $_ if ( AttrVal( $_, "webname", "fhem" ) eq $FWname ); - push @FWports, $defs{$_}{PORT}; + + for my $dev ( devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') ) { + $hash->{FW} = $dev if ( AttrVal( $dev, "webname", "fhem" ) eq $FWname ); + push @FWports, $defs{$dev}{PORT}; } if (!defined($hash->{FW})) { # FHEMWEB für SSChatBot ist noch nicht angelegt my $room = AttrVal($name, "room", "Chat"); my $port = 8082; - while (grep {/^$port$/} @FWports) { # den ersten freien FHEMWEB-Port ab 8082 finden + while (grep {/^$port$/x} @FWports) { # den ersten freien FHEMWEB-Port ab 8082 finden $port++; } @@ -649,21 +880,22 @@ sub SSChatBot_initonboot { my $host = hostname(); # eigener Host my $fqdn = hostfqdn(); # MYFQDN eigener Host - chop($fqdn) if($fqdn =~ /\.$/); # eventuellen "." nach dem FQDN entfernen + chop($fqdn) if($fqdn =~ /\.$/x); # eventuellen "." nach dem FQDN entfernen my $FWchatport = $defs{$FW}{PORT}; my $FWprot = AttrVal($FW, "HTTPS", 0); $FWname = AttrVal($FW, "webname", 0); + CommandAttr(undef, "$FW csrfToken none") if(!AttrVal($FW, "csrfToken", "")); + $csrf = $defs{$FW}{CSRFTOKEN} // ""; - $hash->{OUTDEF} = ($FWprot ? "https" : "http")."://".($fqdn // $host).":".$FWchatport."/".$FWname."/outchat?botname=".$name."&fwcsrf=".$csrf; - SSChatBot_addExtension($name, "SSChatBot_CGI", "outchat"); + addExtension($name, "FHEM::SSChatBot::botCGI", "outchat"); $hash->{HELPER}{INFIX} = "outchat"; } } else { - InternalTimer(gettimeofday()+3, "SSChatBot_initonboot", $hash, 0); + InternalTimer(gettimeofday()+3, "FHEM::SSChatBot::initOnBoot", $hash, 0); } return; @@ -674,15 +906,23 @@ return; # # ($name,$opmode,$method,$userid,$text,$fileUrl,$channel,$attachment) ###################################################################################### -sub SSChatBot_addQueue ($$$$$$$$) { - my ($name,$opmode,$method,$userid,$text,$fileUrl,$channel,$attachment) = @_; - my $hash = $defs{$name}; +sub addQueue { + my $paref = shift; + my $name = $paref->{name} // do {my $err = qq{internal ERROR -> name is empty}; Log 1, "SSChatBot - $err"; return}; + my $hash = $defs{$name}; + my $opmode = $paref->{opmode} // do {my $err = qq{internal ERROR -> opmode is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $method = $paref->{method} // do {my $err = qq{internal ERROR -> method is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $userid = $paref->{userid} // do {my $err = qq{internal ERROR -> userid is empty}; Log3($name, 1, "$name - $err"); setErrorState ($hash, $err); return}; + my $text = $paref->{text}; + my $fileUrl = $paref->{fileUrl}; + my $channel = $paref->{channel}; + my $attachment = $paref->{attachment}; - if(!$text && $opmode !~ /chatUserlist|chatChannellist/) { + if(!$text && $opmode !~ /chatUserlist|chatChannellist/x) { my $err = qq{can't add message to queue: "text" is empty}; Log3($name, 2, "$name - ERROR - $err"); - SSChatBot_setErrorState ($hash, $err); + setErrorState ($hash, $err); return; } @@ -704,7 +944,7 @@ sub SSChatBot_addQueue ($$$$$$$$) { $data{SSChatBot}{$name}{sendqueue}{entries}{$index} = $pars; - SSChatBot_updQLength ($hash); # updaten Länge der Sendequeue + updQLength ($hash); # updaten Länge der Sendequeue return; } @@ -718,7 +958,7 @@ return; # 1 -> Opmode nicht erfolgreich (Abarbeitung nach ckeck errorcode # eventuell verzögert wiederholen) ############################################################################################# -sub SSChatBot_checkretry { +sub checkRetry { my ($name,$retry) = @_; my $hash = $defs{$name}; my $idx = $hash->{OPIDX}; @@ -726,7 +966,7 @@ sub SSChatBot_checkretry { if(!keys %{$data{SSChatBot}{$name}{sendqueue}{entries}}) { Log3($name, 4, "$name - SendQueue is empty. Nothing to do ..."); - SSChatBot_updQLength ($hash); + updQLength ($hash); return; } @@ -734,8 +974,8 @@ sub SSChatBot_checkretry { delete $hash->{OPIDX}; delete $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}; Log3($name, 4, "$name - Opmode \"$hash->{OPMODE}\" finished successfully, Sendqueue index \"$idx\" deleted."); - SSChatBot_updQLength ($hash); - return SSChatBot_getapisites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer) + updQLength ($hash); + return getApiSites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer) } else { # Befehl nicht erfolgreich, (verzögertes) Senden einplanen $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{retryCount}++; @@ -743,7 +983,7 @@ sub SSChatBot_checkretry { my $errorcode = ReadingsVal($name, "Errorcode", 0); if($errorcode =~ /100|101|117|120|407|409|410|800|900/x) { # bei diesen Errorcodes den Queueeintrag nicht wiederholen, da dauerhafter Fehler ! - $forbidSend = SSChatBot_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $forbidSend = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{forbidSend} = $forbidSend; Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. It seems to be a permanent error. Exclude it from new send attempt !"); @@ -751,41 +991,35 @@ sub SSChatBot_checkretry { delete $hash->{OPIDX}; delete $hash->{OPMODE}; - SSChatBot_updQLength ($hash); # updaten Länge der Sendequeue + updQLength ($hash); # updaten Länge der Sendequeue - return SSChatBot_getapisites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer); + return getApiSites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer); } if(!$forbidSend) { my $rs = 0; - if($rc <= 1) { - $rs = 5; - } elsif ($rc < 3) { - $rs = 20; - } elsif ($rc < 5) { - $rs = 60; - } elsif ($rc < 7) { - $rs = 1800; - } elsif ($rc < 30) { - $rs = 3600; - } else { - $rs = 86400; - } + $rs = $rc <= 1 ? 5 + : $rc < 3 ? 20 + : $rc < 5 ? 60 + : $rc < 7 ? 1800 + : $rc < 30 ? 3600 + : 86400 + ; Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. Restart SendQueue in $rs seconds (retryCount $rc)."); my $rst = gettimeofday()+$rs; # resend Timer - SSChatBot_updQLength ($hash,$rst); # updaten Länge der Sendequeue mit resend Timer + updQLength ($hash,$rst); # updaten Länge der Sendequeue mit resend Timer - RemoveInternalTimer($hash, "SSChatBot_getapisites"); - InternalTimer($rst, "SSChatBot_getapisites", "$name", 0); + RemoveInternalTimer($hash, "FHEM::SSChatBot::getApiSites"); + InternalTimer($rst, "FHEM::SSChatBot::getApiSites", "$name", 0); } } return } -sub SSChatBot_getapisites ($) { +sub getApiSites { my ($name) = @_; my $hash = $defs{$name}; my $inaddr = $hash->{INADDR}; @@ -793,6 +1027,7 @@ sub SSChatBot_getapisites ($) { my $inprot = $hash->{INPROT}; my $apiinfo = $hash->{HELPER}{APIINFO}; # Info-Seite für alle API's, einzige statische Seite ! my $chatexternal = $hash->{HELPER}{CHATEXTERNAL}; + my ($url,$param,$idxset,$ret); # API-Pfade und MaxVersions ermitteln @@ -825,7 +1060,7 @@ sub SSChatBot_getapisites ($) { if ($hash->{HELPER}{APIPARSET}) { # API-Hashwerte sind bereits gesetzt -> Abruf überspringen Log3($name, 4, "$name - API hashvalues already set - ignore get apisites"); - return SSChatBot_chatop($name); + return chatOp($name); } my $httptimeout = AttrVal($name,"httptimeout",20); @@ -842,8 +1077,9 @@ sub SSChatBot_getapisites ($) { hash => $hash, method => "GET", header => "Accept: application/json", - callback => \&SSChatBot_getapisites_parse + callback => \&getApiSites_parse }; + HttpUtils_NonblockingGet ($param); return; @@ -852,7 +1088,7 @@ return; #################################################################################### # Auswertung Abruf apisites #################################################################################### -sub SSChatBot_getapisites_parse { +sub getApiSites_parse { my ($param, $err, $myjson) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; @@ -860,23 +1096,23 @@ sub SSChatBot_getapisites_parse { my $inport = $hash->{INPORT}; my $chatexternal = $hash->{HELPER}{CHATEXTERNAL}; - my ($error,$errorcode,$success,$chatexternalmaxver,$chatexternalpath); + my ($error,$errorcode,$success); if ($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist Log3($name, 2, "$name - ERROR message: $err"); - SSChatBot_setErrorState ($hash, $err); - SSChatBot_checkretry ($name,1); + setErrorState ($hash, $err); + checkRetry ($name,1); return; } elsif ($myjson ne "") { # Evaluiere ob Daten im JSON-Format empfangen wurden - ($hash, $success) = SSChatBot_evaljson($hash,$myjson); + ($hash, $success) = evalJSON($hash,$myjson); unless ($success) { Log3($name, 4, "$name - Data returned: ".$myjson); - SSChatBot_checkretry($name,1); + checkRetry ($name,1); return; } @@ -915,32 +1151,32 @@ sub SSChatBot_getapisites_parse { } else { $errorcode = "805"; - $error = SSChatBot_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln - SSChatBot_setErrorState ($hash, $error, $errorcode); - SSChatBot_checkretry ($name,1); + setErrorState ($hash, $error, $errorcode); + checkRetry ($name,1); return; } } else { $errorcode = "806"; - $error = SSChatBot_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln - SSChatBot_setErrorState ($hash, $error, $errorcode); + setErrorState ($hash, $error, $errorcode); Log3($name, 2, "$name - ERROR - the API-Query couldn't be executed successfully"); - SSChatBot_checkretry($name,1); + checkRetry ($name,1); return; } } -return SSChatBot_chatop($name); +return chatOp ($name); } ############################################################################################# # Ausführung Operation ############################################################################################# -sub SSChatBot_chatop { +sub chatOp { my ($name) = @_; my $hash = $defs{$name}; my $inprot = $hash->{INPROT}; @@ -952,15 +1188,15 @@ sub SSChatBot_chatop { my ($url,$httptimeout,$param,$error,$errorcode); # Token abrufen - my ($success, $token) = SSChatBot_getToken($hash,0,"botToken"); + my ($success, $token) = getToken($hash,0,"botToken"); unless ($success) { $errorcode = "810"; - $error = SSChatBot_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln - SSChatBot_setErrorState ($hash, $error, $errorcode); + setErrorState ($hash, $error, $errorcode); Log3($name, 2, "$name - ERROR - $error"); - SSChatBot_checkretry($name,1); + checkRetry ($name,1); return; } @@ -972,6 +1208,7 @@ sub SSChatBot_chatop { my $text = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{text}; my $attachment = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{attachment}; my $fileUrl = $data{SSChatBot}{$name}{sendqueue}{entries}{$idx}{fileUrl}; + Log3($name, 4, "$name - start SendQueue entry index \"$idx\" ($hash->{OPMODE}) for operation."); $httptimeout = AttrVal($name, "httptimeout", 20); @@ -999,6 +1236,7 @@ sub SSChatBot_chatop { my $part = $url; if(AttrVal($name, "showTokenInLog", "0") == 1) { Log3($name, 4, "$name - Call-Out: $url"); + } else { $part =~ s/$token//x; Log3($name, 4, "$name - Call-Out: $part"); @@ -1010,7 +1248,7 @@ sub SSChatBot_chatop { hash => $hash, method => "GET", header => "Accept: application/json", - callback => \&SSChatBot_chatop_parse + callback => \&chatOp_parse }; HttpUtils_NonblockingGet ($param); @@ -1019,9 +1257,9 @@ return; } ############################################################################################# -# Callback from SSChatBot_chatop +# Callback from chatOp ############################################################################################# -sub SSChatBot_chatop_parse { ## no critic 'complexity' +sub chatOp_parse { ## no critic 'complexity' my ($param, $err, $myjson) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; @@ -1040,17 +1278,17 @@ sub SSChatBot_chatop_parse { ## no critic $errorcode = "none"; $errorcode = "800" if($err =~ /:\smalformed\sor\sunsupported\sURL$/xs); - SSChatBot_setErrorState ($hash, $err, $errorcode); - SSChatBot_checkretry ($name,1); + setErrorState ($hash, $err, $errorcode); + checkRetry ($name,1); return; } elsif ($myjson ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) # Evaluiere ob Daten im JSON-Format empfangen wurden - ($hash,$success) = SSChatBot_evaljson($hash,$myjson); + ($hash,$success) = evalJSON ($hash,$myjson); unless ($success) { Log3($name, 4, "$name - Data returned: ".$myjson); - SSChatBot_checkretry($name,1); + checkRetry ($name,1); return; } @@ -1066,23 +1304,23 @@ sub SSChatBot_chatop_parse { ## no critic if ($opmode eq "chatUserlist") { my %users = (); my ($un,$ui,$st,$nn,$em,$uids); - my $i = 0; + my $i = 0; - my $out = ""; - $out .= "Synology Chat Server visible Users

"; - $out .= ""; - $out .= ""; - $out .= ""; + my $out = ""; + $out .= "Synology Chat Server visible Users

"; + $out .= "
Username ID state Nickname Email
"; + $out .= ""; + $out .= ""; while ($data->{'data'}->{'users'}->[$i]) { - my $deleted = SSChatBot_jboolmap($data->{'data'}->{'users'}->[$i]->{'deleted'}); - my $isdis = SSChatBot_jboolmap($data->{'data'}->{'users'}->[$i]->{'is_disabled'}); + my $deleted = jBoolMap($data->{'data'}->{'users'}->[$i]->{'deleted'}); + my $isdis = jBoolMap($data->{'data'}->{'users'}->[$i]->{'is_disabled'}); if($deleted ne "true" && $isdis ne "true") { - $un = $data->{'data'}->{'users'}->[$i]->{'username'}; - $ui = $data->{'data'}->{'users'}->[$i]->{'user_id'}; - $st = $data->{'data'}->{'users'}->[$i]->{'status'}; - $nn = $data->{'data'}->{'users'}->[$i]->{'nickname'}; - $em = $data->{'data'}->{'users'}->[$i]->{'user_props'}->{'email'}; + $un = $data->{'data'}->{'users'}->[$i]->{'username'}; + $ui = $data->{'data'}->{'users'}->[$i]->{'user_id'}; + $st = $data->{'data'}->{'users'}->[$i]->{'status'}; + $nn = $data->{'data'}->{'users'}->[$i]->{'nickname'}; + $em = $data->{'data'}->{'users'}->[$i]->{'user_props'}->{'email'}; $users{$un}{id} = $ui; $users{$un}{status} = $st; $users{$un}{nickname} = $nn; @@ -1093,15 +1331,18 @@ sub SSChatBot_chatop_parse { ## no critic } $i++; } + $hash->{HELPER}{USERS} = \%users if(%users); $hash->{HELPER}{USERFETCHED} = 1; my @newa; my $list = $modules{$hash->{TYPE}}{AttrList}; my @deva = split(" ", $list); - foreach (@deva) { - push @newa, $_ if($_ !~ /defaultPeer:|allowedUserFor(Set|Get|Code|Own):/); + + for my $da (@deva) { + push @newa, $da if($da !~ /defaultPeer:|allowedUserFor(?:Set|Get|Code|Own):/x); } + push @newa, ($uids?"defaultPeer:multiple-strict,$uids ":"defaultPeer:--no#userlist#selectable--"); push @newa, ($uids?"allowedUserForSet:multiple-strict,$uids ":"allowedUserForSet:--no#userlist#selectable--"); push @newa, ($uids?"allowedUserForGet:multiple-strict,$uids ":"allowedUserForGet:--no#userlist#selectable--"); @@ -1116,11 +1357,11 @@ sub SSChatBot_chatop_parse { ## no critic # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) asyncOutput($hash->{HELPER}{CL}{1},"$out"); - InternalTimer(gettimeofday()+10.0, "SSChatBot_delclhash", $name, 0); + InternalTimer(gettimeofday()+10.0, "FHEM::SSChatBot::delClhash", $name, 0); } elsif ($opmode eq "chatChannellist") { my %channels = (); - my ($cn,$ci,$cr,$mb,$ty,$cids); + my ($ci,$cr,$mb,$ty,$cids); my $i = 0; my $out = ""; @@ -1130,19 +1371,19 @@ sub SSChatBot_chatop_parse { ## no critic $out .= ""; while ($data->{'data'}->{'channels'}->[$i]) { - my $cn = SSChatBot_jboolmap($data->{'data'}->{'channels'}->[$i]->{'name'}); + my $cn = jBoolMap($data->{'data'}->{'channels'}->[$i]->{'name'}); if($cn) { - $ci = $data->{'data'}->{'channels'}->[$i]->{'channel_id'}; - $cr = $data->{'data'}->{'channels'}->[$i]->{'creator_id'}; - $mb = $data->{'data'}->{'channels'}->[$i]->{'members'}; - $ty = $data->{'data'}->{'channels'}->[$i]->{'type'}; - $channels{$cn}{id} = $ci; - $channels{$cn}{creator} = $cr; - $channels{$cn}{members} = $mb; - $channels{$cn}{type} = $ty; - $cids .= "," if($cids); - $cids .= $cn; - $out .= ""; + $ci = $data->{'data'}->{'channels'}->[$i]->{'channel_id'}; + $cr = $data->{'data'}->{'channels'}->[$i]->{'creator_id'}; + $mb = $data->{'data'}->{'channels'}->[$i]->{'members'}; + $ty = $data->{'data'}->{'channels'}->[$i]->{'type'}; + $channels{$cn}{id} = $ci; + $channels{$cn}{creator} = $cr; + $channels{$cn}{members} = $mb; + $channels{$cn}{type} = $ty; + $cids .= "," if($cids); + $cids .= $cn; + $out .= ""; } $i++; } @@ -1153,8 +1394,8 @@ sub SSChatBot_chatop_parse { ## no critic # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) - asyncOutput($hash->{HELPER}{CL}{1},"$out"); - InternalTimer(gettimeofday()+5.0, "SSChatBot_delclhash", $name, 0); + asyncOutput ($hash->{HELPER}{CL}{1},"$out"); + InternalTimer(gettimeofday()+5.0, "FHEM::SSChatBot::delClhash", $name, 0); } elsif ($opmode eq "sendItem" && $hash->{OPIDX}) { my $postid = ""; @@ -1166,15 +1407,15 @@ sub SSChatBot_chatop_parse { ## no critic readingsBeginUpdate ($hash); readingsBulkUpdate ($hash, "sendPostId", $postid); - readingsBulkUpdate ($hash, "sendUserId", $uid); + readingsBulkUpdate ($hash, "sendUserId", $uid ); readingsEndUpdate ($hash,1); } - SSChatBot_checkretry($name,0); + checkRetry ($name,0); readingsBeginUpdate ($hash); - readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); - readingsBulkUpdateIfChanged ($hash, "Error", "none"); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none" ); + readingsBulkUpdateIfChanged ($hash, "Error", "none" ); readingsBulkUpdate ($hash, "state", "active"); readingsEndUpdate ($hash,1); @@ -1183,15 +1424,15 @@ sub SSChatBot_chatop_parse { ## no critic # Errorcode aus JSON ermitteln $errorcode = $data->{'error'}->{'code'}; $cherror = $data->{'error'}->{'errors'}; # vom Chat gelieferter Fehler - $error = SSChatBot_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln - if ($error =~ /not found/) { + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + if ($error =~ /not\sfound/x) { $error .= " New error: ".($cherror // ""); } - SSChatBot_setErrorState ($hash, $error, $errorcode); + setErrorState ($hash, $error, $errorcode); Log3($name, 2, "$name - ERROR - Operation $opmode was not successful. Errorcode: $errorcode - $error"); - SSChatBot_checkretry($name,1); + checkRetry ($name,1); } undef $data; @@ -1204,7 +1445,7 @@ return; ############################################################################### # Test ob JSON-String empfangen wurde ############################################################################### -sub SSChatBot_evaljson { +sub evalJSON { my ($hash,$myjson) = @_; my $OpMode = $hash->{OPMODE}; my $name = $hash->{NAME}; @@ -1212,14 +1453,11 @@ sub SSChatBot_evaljson { my ($error,$errorcode); eval {decode_json($myjson)} or do { - $success = 0; - - $errorcode = "900"; - - # Fehlertext zum Errorcode ermitteln - $error = SSChatBot_experror($hash,$errorcode); + $success = 0; + $errorcode = "900"; + $error = expError($hash,$errorcode); # Fehlertext zum Errorcode ermitteln - SSChatBot_setErrorState ($hash, $error, $errorcode); + setErrorState ($hash, $error, $errorcode); }; return($hash,$success,$myjson); @@ -1228,8 +1466,8 @@ return($hash,$success,$myjson); ############################################################################### # JSON Boolean Test und Mapping ############################################################################### -sub SSChatBot_jboolmap { - my ($bool) = @_; +sub jBoolMap { + my $bool = shift; if(JSON::is_bool($bool)) { $bool = $bool?"true":"false"; @@ -1243,20 +1481,20 @@ return $bool; # Auflösung Errorcodes SVS API # Übernahmewerte sind $hash, $errorcode ############################################################################## -sub SSChatBot_experror { +sub expError { my ($hash,$errorcode) = @_; my $device = $hash->{NAME}; my $error; - unless (exists($SSChatBot_errlist{"$errorcode"})) { + unless (exists($errList{"$errorcode"})) { $error = "Value of errorcode \"$errorcode\" not found."; return ($error); } # Fehlertext aus Hash-Tabelle %errorlist ermitteln - $error = $SSChatBot_errlist{"$errorcode"}; + $error = $errList{"$errorcode"}; -return ($error); +return $error; } ################################################################ @@ -1264,16 +1502,16 @@ return ($error); # Schwartzian Transform and the GRT transform # Übergabe: "asc | desc", ################################################################ -sub SSChatBot_sortVersion { +sub sortVersion { my ($sseq,@versions) = @_; my @sorted = map {$_->[0]} sort {$a->[1] cmp $b->[1]} - map {[$_, pack "C*", split /\./]} @versions; + map {[$_, pack "C*", split /\./x]} @versions; @sorted = map {join ".", unpack "C*", $_} sort - map {pack "C*", split /\./} @versions; + map {pack "C*", split /\./x} @versions; if($sseq eq "desc") { @sorted = reverse @sorted; @@ -1285,7 +1523,7 @@ return @sorted; ###################################################################################### # botToken speichern ###################################################################################### -sub SSChatBot_setToken { +sub setToken { my ($hash, $token, $ao) = @_; my $name = $hash->{NAME}; my ($success, $credstr, $index, $retcode); @@ -1297,9 +1535,7 @@ sub SSChatBot_setToken { @key = qw(1 3 4 5 6 3 2 1 9); $len = scalar @key; $i = 0; - $credstr = join "", - map { $i = ($i + 1) % $len; - chr((ord($_) + $key[$i]) % 256) } split //, $credstr; + $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) + $key[$i]) % 256) } split //x, $credstr; ## no critic 'Map blocks' # End Scramble-Routine $index = $hash->{TYPE}."_".$hash->{NAME}."_".$ao; @@ -1309,16 +1545,16 @@ sub SSChatBot_setToken { Log3($name, 2, "$name - Error while saving Token - $retcode"); $success = 0; } else { - ($success, $token) = SSChatBot_getToken($hash,1,$ao); # Credentials nach Speicherung lesen und in RAM laden ($boot=1) + ($success, $token) = getToken($hash,1,$ao); # Credentials nach Speicherung lesen und in RAM laden ($boot=1) } -return ($success); +return $success; } ###################################################################################### # botToken lesen ###################################################################################### -sub SSChatBot_getToken { +sub getToken { my ($hash,$boot, $ao) = @_; my $name = $hash->{NAME}; my ($success, $token, $index, $retcode, $credstr); @@ -1352,10 +1588,7 @@ sub SSChatBot_getToken { @key = qw(1 3 4 5 6 3 2 1 9); $len = scalar @key; $i = 0; - $credstr = join "", - map { $i = ($i + 1) % $len; - chr((ord($_) - $key[$i] + 256) % 256) } - split //, $credstr; + $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) - $key[$i] + 256) % 256) } split //x, $credstr; ## no critic 'Map blocks' # Ende Descramble-Routine $token = decode_base64($credstr); @@ -1377,7 +1610,7 @@ return ($success, $token); ############################################################################################# # FHEMWEB Extension hinzufügen ############################################################################################# -sub SSChatBot_addExtension { +sub addExtension { my ($name, $func, $link) = @_; my $url = "/$link"; @@ -1393,15 +1626,15 @@ return; ############################################################################################# # FHEMWEB Extension löschen ############################################################################################# -sub SSChatBot_removeExtension { +sub removeExtension { my ($link) = @_; my $url = "/$link"; my $name = $data{FWEXT}{$url}{deviceName}; my @chatdvs = devspec2array("TYPE=SSChatBot"); - foreach (@chatdvs) { # /outchat erst deregistrieren wenn keine SSChat-Devices mehr vorhanden sind außer $name - if($defs{$_} && $_ ne $name) { + for my $cd (@chatdvs) { # /outchat erst deregistrieren wenn keine SSChat-Devices mehr vorhanden sind außer $name + if($defs{$cd} && $cd ne $name) { Log3($name, 2, "$name - Skip unregistering SSChatBot for URL $url"); return; } @@ -1416,9 +1649,9 @@ return; ############################################################################################# # Leerzeichen am Anfang / Ende eines strings entfernen ############################################################################################# -sub SSChatBot_trim { +sub trim { my $str = shift; - $str =~ s/^\s+|\s+$//g; + $str =~ s/^\s+|\s+$//xg; return ($str); } @@ -1426,7 +1659,7 @@ return ($str); ############################################################################################# # Länge Senedequeue updaten ############################################################################################# -sub SSChatBot_updQLength { +sub updQLength { my ($hash,$rst) = @_; my $name = $hash->{NAME}; @@ -1453,7 +1686,7 @@ return; # $txt : der zu formatierende String # $func : ein Name zur Identifizierung der aufrufenden Funktion ############################################################################################# -sub SSChatBot_formString { +sub formString { my $txt = shift; my $func = shift; my (%replacements,$pat); @@ -1474,19 +1707,19 @@ sub SSChatBot_formString { ); } - $txt =~ s/\n/ESC_newline_ESC/g; - my @acr = split (/\s+/, $txt); + $txt =~ s/\n/ESC_newline_ESC/xg; + my @acr = split (/\s+/x, $txt); $txt = ""; - foreach (@acr) { # Einzeiligkeit für Versand herstellen + for my $line (@acr) { # Einzeiligkeit für Versand herstellen $txt .= " " if($txt); - $_ =~ s/ESC_newline_ESC/\\n/g; - $txt .= $_; + $line =~ s/ESC_newline_ESC/\\n/xg; + $txt .= $line; } $pat = join '|', map { quotemeta; } keys(%replacements); - $txt =~ s/($pat)/$replacements{$1}/g; + $txt =~ s/($pat)/$replacements{$1}/xg; return ($txt); } @@ -1494,9 +1727,9 @@ return ($txt); #################################################################################### # zentrale Funktion Error State in Readings setzen # $error = Fehler als Text -# $errc = Fehlercode gemäß %SSChatBot_errlist +# $errc = Fehlercode gemäß %errList #################################################################################### -sub SSChatBot_setErrorState { +sub setErrorState { my $hash = shift; my $error = shift; my $errc = shift; @@ -1516,7 +1749,7 @@ return; # Clienthash übernehmen oder zusammenstellen # Identifikation ob über FHEMWEB ausgelöst oder nicht -> erstellen $hash->CL ############################################################################################# -sub SSChatBot_getclhash { +sub getClhash { my ($hash,$nobgd)= @_; my $name = $hash->{NAME}; my $ret; @@ -1530,11 +1763,9 @@ sub SSChatBot_getclhash { if (!defined($hash->{CL})) { # Clienthash wurde nicht übergeben und wird erstellt (FHEMWEB Instanzen mit canAsyncOutput=1 analysiert) - my $outdev; my @webdvs = devspec2array("TYPE=FHEMWEB:FILTER=canAsyncOutput=1:FILTER=STATE=Connected"); my $i = 1; - foreach (@webdvs) { - $outdev = $_; + for my $outdev (@webdvs) { next if(!$defs{$outdev}); $hash->{HELPER}{CL}{$i}->{NAME} = $defs{$outdev}{NAME}; $hash->{HELPER}{CL}{$i}->{NR} = $defs{$outdev}{NR}; @@ -1566,7 +1797,7 @@ return ($ret); ############################################################################################# # Clienthash löschen ############################################################################################# -sub SSChatBot_delclhash { +sub delClhash { my $name = shift; my $hash = $defs{$name}; @@ -1580,9 +1811,9 @@ return; # Die Datei wird im Verzeichnis "/opt/fhem/www/images" erstellt # #################################################################################### -sub SSChatBot_PlotToFile { +sub plotToFile { my $name = shift; - my $svg = shift; + my $svg = shift; my $hash = $defs{$name}; my $file = $name."_SendPlot.png"; my $path = $attr{global}{modpath}."/www/images"; @@ -1594,19 +1825,19 @@ sub SSChatBot_PlotToFile { my $offset = $options[2]; if(!$defs{$svgdev}) { - my $err = qq{SVG device "$svgdev" doesn't exist}; + $err = qq{SVG device "$svgdev" doesn't exist}; Log3($name, 1, "$name - ERROR - $err !"); - SSChatBot_setErrorState ($hash, $err); + setErrorState ($hash, $err); return $err; } - - open (my $FILE, ">", "$path/$file") or do { - my $err = qq{>PlotToFile< can't open $path/$file for write access}; + + open (my $FILE, ">", "$path/$file") or do { + $err = qq{>PlotToFile< can't open $path/$file for write access}; Log3($name, 1, "$name - ERROR - $err !"); - SSChatBot_setErrorState ($hash, $err); + setErrorState ($hash, $err); return $err; - }; + }; binmode $FILE; print $FILE plotAsPng(@options); close $FILE; @@ -1618,11 +1849,11 @@ return ($err, $file); # Versionierungen des Moduls setzen # Die Verwendung von Meta.pm und Packages wird berücksichtigt ############################################################################################# -sub SSChatBot_setVersionInfo { - my ($hash) = @_; - my $name = $hash->{NAME}; +sub setVersionInfo { + my $hash = shift; + my $name = $hash->{NAME}; - my $v = (SSChatBot_sortVersion("desc",keys %SSChatBot_vNotesIntern))[0]; + my $v = (sortVersion("desc",keys %vNotesIntern))[0]; my $type = $hash->{TYPE}; $hash->{HELPER}{PACKAGE} = __PACKAGE__; $hash->{HELPER}{VERSION} = $v; @@ -1650,294 +1881,373 @@ return; } ############################################################################################# -# Common Gateway Interface -# parsen von outgoing Messages Chat -> FHEM +# Common Gateway Interface ############################################################################################# -sub SSChatBot_CGI { ## no critic 'complexity' - my ($request) = @_; - my ($hash,$name,$link,$args); - my ($text,$timestamp,$channelid,$channelname,$userid,$username,$postid,$triggerword) = ("","","","","","","",""); - my ($command,$cr,$au,$arg,$callbackid,$actions,$actval,$avToExec) = ("","","","","","","",""); - my $success; - my @aul; - my $state = "active"; - my $do = 0; - my $ret = "success"; +sub botCGI { + my $request = shift; - return ( "text/plain; charset=utf-8", "Booting up" ) unless ($init_done); - - # data received - if ($request =~ /^\/outchat(\?|&).*/) { # POST- oder GET-Methode empfangen - $args = (split(/outchat\?/, $request))[1]; # GET-Methode empfangen - if(!$args) { # POST-Methode empfangen wenn keine GET_Methode ? - $args = (split(/outchat&/, $request))[1]; - if(!$args) { - Log 1, "TYPE SSChatBot - ERROR - no expected data received"; - return ("text/plain; charset=utf-8", "no expected data received"); - } - } - $args =~ s/&/" /g; - $args =~ s/=/="/g; - $args .= "\""; + if(!$init_done) { + return ( "text/plain; charset=utf-8", "FHEM server is booting up" ); + } - $args = urlDecode($args); - my($a,$h) = parseParams($args); - - if (!defined($h->{botname})) { - Log 1, "TYPE SSChatBot - ERROR - no Botname received"; - return ("text/plain; charset=utf-8", "no FHEM SSChatBot name in message"); - } - - # check ob angegebenes SSChatBot Device definiert, wenn ja Kontext auf botname setzen - $name = $h->{botname}; # das SSChatBot Device - unless (IsDevice($name, 'SSChatBot')) { - Log 1, "ERROR - No SSChatBot device \"$name\" of Type \"SSChatBot\" exists"; - return ( "text/plain; charset=utf-8", "No SSChatBot device for webhook \"/outchat\" exists" ); - } - - $hash = $defs{$name}; # hash des SSChatBot Devices - Log3($name, 4, "$name - ####################################################"); - Log3($name, 4, "$name - ### start Chat operation Receive "); - Log3($name, 4, "$name - ####################################################"); - Log3($name, 5, "$name - raw data received (urlDecoded):\n".Dumper($args)); - - # eine Antwort auf ein interaktives Objekt - if (defined($h->{payload})) { - # ein Benutzer hat ein interaktives Objekt ausgelöst (Button). Die Datenfelder sind nachfolgend beschrieben: - # "actions": Array des Aktionsobjekts, das sich auf die vom Benutzer ausgelöste Aktion bezieht - # "callback_id": Zeichenkette, die sich auf die Callback_id des Anhangs bezieht, in dem sich die vom Benutzer ausgelöste Aktion befindet - # "post_id" - # "token" - # "user": { "user_id","username" } - my $pldata = $h->{payload}; - (undef, $success) = SSChatBot_evaljson($hash,$pldata); - unless ($success) { - Log3($name, 1, "$name - ERROR - invalid JSON data received:\n".Dumper($pldata)); - return ("text/plain; charset=utf-8", "invalid JSON data received"); - } - my $data = decode_json($pldata); - Log3($name, 5, "$name - interactive object data (JSON decoded):\n". Dumper $data); - - $h->{token} = $data->{token}; - $h->{post_id} = $data->{post_id}; - $h->{user_id} = $data->{user}{user_id}; - $h->{username} = $data->{user}{username}; - $h->{callback_id} = $data->{callback_id}; - $h->{actions} = "type: ".$data->{actions}[0]{type}.", ". - "name: ".$data->{actions}[0]{name}.", ". - "value: ".$data->{actions}[0]{value}.", ". - "text: ".$data->{actions}[0]{text}.", ". - "style: ".$data->{actions}[0]{style}; - } - - if (!defined($h->{token})) { - Log3($name, 5, "$name - received insufficient data:\n".Dumper($args)); - return ("text/plain; charset=utf-8", "Insufficient data"); - } - - # CSRF Token check - my $FWdev = $hash->{FW}; # das FHEMWEB Device für SSChatBot Device -> ist das empfangene Device - my $FWhash = $defs{$FWdev}; - my $want = $FWhash->{CSRFTOKEN}; - $want = $want?$want:"none"; - my $supplied = $h->{fwcsrf}; - if($want eq "none" || $want ne $supplied) { # $FW_wname enthält ebenfalls das aufgerufenen FHEMWEB-Device - Log3 ($FW_wname, 2, "$FW_wname - WARNING - FHEMWEB CSRF error for client \"$FWdev\": ". - "received $supplied token is not $want. ". - "For details see the csrfToken FHEMWEB attribute. ". - "The csrfToken must be identical to the token in OUTDEF of $name device."); - return ("text/plain; charset=utf-8", "400 Bad Request"); - } - - # Timestamp dekodieren - if ($h->{timestamp}) { - $h->{timestamp} = FmtDateTime(($h->{timestamp})/1000); - } - - Log3($name, 4, "$name - received data decoded:\n".Dumper($h)); - - $hash->{OPMODE} = "receiveData"; - - # ausgehende Datenfelder (Chat -> FHEM), die das Chat senden kann - # =============================================================== - # token: bot token - # channel_id - # channel_name - # user_id - # username - # post_id - # timestamp - # text - # trigger_word: which trigger word is matched - # - - $channelid = $h->{channel_id} if($h->{channel_id}); - $channelname = $h->{channel_name} if($h->{channel_name}); - $userid = $h->{user_id} if($h->{user_id}); - $username = $h->{username} if($h->{username}); - $postid = $h->{post_id} if($h->{post_id}); - $callbackid = $h->{callback_id} if($h->{callback_id}); - $timestamp = $h->{timestamp} if($h->{timestamp}); - - # interaktive Schaltflächen (Aktionen) auswerten - if ($h->{actions}) { - $actions = $h->{actions}; - $actions =~ m/^type: button.*value: (.*), text:.*$/; - $actval = $1; - if($actval =~ /^\/.*$/) { - Log3($name, 4, "$name - slash command \"$actval\" got from interactive data and execute it with priority"); - $avToExec = $actval; - } - } - - if ($h->{text} || $avToExec) { - $text = $h->{text}; - $text = $avToExec if($avToExec); # Vorrang für empfangene interaktive Data (Schaltflächenwerte) die Slash-Befehle enthalten - if($text =~ /^\/([Ss]et.*?|[Gg]et.*?|[Cc]ode.*?)\s+(.*)$/) { # vordefinierte Befehle in FHEM ausführen - my $p1 = $1; - my $p2 = $2; - - if($p1 =~ /set.*/i) { - $command = "set ".$p2; - $do = 1; - $au = AttrVal($name,"allowedUserForSet", "all"); - @aul = split(",",$au); - if($au eq "all" || $username ~~ @aul) { - Log3($name, 4, "$name - Synology Chat user \"$username\" execute FHEM command: ".$command); - $cr = CommandSet(undef, $p2); # set-Befehl in FHEM ausführen - } else { - $cr = "User \"$username\" is not allowed execute \"$command\" command"; - $state = "command execution denied"; - Log3($name, 2, "$name - WARNING - Chat user \"$username\" is not authorized for \"$command\" command. Execution denied !"); - } - - } elsif ($p1 =~ /get.*/i) { - $command = "get ".$p2; - $do = 1; - $au = AttrVal($name,"allowedUserForGet", "all"); - @aul = split(",",$au); - if($au eq "all" || $username ~~ @aul) { - Log3($name, 4, "$name - Synology Chat user \"$username\" execute FHEM command: ".$command); - $cr = CommandGet(undef, $p2); # get-Befehl in FHEM ausführen - } else { - $cr = "User \"$username\" is not allowed execute \"$command\" command"; - $state = "command execution denied"; - Log3($name, 2, "$name - WARNING - Chat user \"$username\" is not authorized for \"$command\" command. Execution denied !"); - } - - } elsif ($p1 =~ /code.*/i) { - $command = $p2; - $do = 1; - $au = AttrVal($name,"allowedUserForCode", "all"); - @aul = split(",",$au); - if($au eq "all" || $username ~~ @aul) { - my $code = $p2; - if($p2 =~ m/^\s*(\{.*\})\s*$/s) { - $p2 = $1; - } else { - $p2 = ''; - } - Log3($name, 4, "$name - Synology Chat user \"$username\" execute FHEM command: ".$p2); - $cr = AnalyzePerlCommand(undef, $p2) if($p2); # Perl Code in FHEM ausführen - } else { - $cr = "User \"$username\" is not allowed execute \"$command\" command"; - $state = "command execution denied"; - Log3($name, 2, "$name - WARNING - Chat user \"$username\" is not authorized for \"$command\" command. Execution denied !"); - } - } - - $cr = $cr ne ""?$cr:"command '$command' executed"; - Log3($name, 4, "$name - FHEM command return: ".$cr); - - $cr = SSChatBot_formString($cr, "command"); - - SSChatBot_addQueue($name, "sendItem", "chatbot", $userid, $cr, "", "", ""); - } - - my $ua = $attr{$name}{userattr}; # Liste aller ownCommand.. zusammenstellen - $ua = "" if(!$ua); - my %hc = map { ($_ => 1) } grep { "$_" =~ m/ownCommand(\d+)/ } split(" ","ownCommand1 $ua"); - - foreach my $ca (sort keys %hc) { - my $uc = AttrVal($name, $ca, ""); - next if (!$uc); - ($uc,$arg) = split(/\s+/, $uc, 2); - - if($uc && $text =~ /^$uc\s?$/) { # User eigener Slash-Befehl, z.B.: /Wetter - $command = $arg; - $do = 1; - $au = AttrVal($name,"allowedUserForOwn", "all"); # Berechtgung des Chat-Users checken - @aul = split(",",$au); - if($au eq "all" || $username ~~ @aul) { - Log3($name, 4, "$name - Synology Chat user \"$username\" execute FHEM command: ".$arg); - $cr = AnalyzeCommandChain(undef, $arg); # FHEM Befehlsketten ausführen - } else { - $cr = "User \"$username\" is not allowed execute \"$arg\" command"; - $state = "command execution denied"; - Log3($name, 2, "$name - WARNING - Chat user \"$username\" is not authorized for \"$arg\" command. Execution denied !"); - } - - $cr = $cr ne ""?$cr:"command '$arg' executed"; - Log3($name, 4, "$name - FHEM command return: ".$cr); - - $cr = SSChatBot_formString($cr, "command"); - - SSChatBot_addQueue($name, "sendItem", "chatbot", $userid, $cr, "", "", ""); - } - } - - # Wenn Kommando ausgeführt wurde Ergebnisse aus Queue übertragen - if($do) { - RemoveInternalTimer($hash, "SSChatBot_getapisites"); - InternalTimer(gettimeofday()+1, "SSChatBot_getapisites", "$name", 0); - } - } - - if ($h->{trigger_word}) { - $triggerword = urlDecode($h->{trigger_word}); - Log3($name, 4, "$name - trigger_word received: ".$triggerword); - } - - readingsBeginUpdate ($hash); - readingsBulkUpdate ($hash, "recActions", $actions); - readingsBulkUpdate ($hash, "recCallbackId", $callbackid); - readingsBulkUpdate ($hash, "recActionsValue", $actval); - readingsBulkUpdate ($hash, "recChannelId", $channelid); - readingsBulkUpdate ($hash, "recChannelname", $channelname); - readingsBulkUpdate ($hash, "recUserId", $userid); - readingsBulkUpdate ($hash, "recUsername", $username); - readingsBulkUpdate ($hash, "recPostId", $postid); - readingsBulkUpdate ($hash, "recTimestamp", $timestamp); - readingsBulkUpdate ($hash, "recText", $text); - readingsBulkUpdate ($hash, "recTriggerword", $triggerword); - readingsBulkUpdate ($hash, "recCommand", $command); - readingsBulkUpdate ($hash, "sendCommandReturn", $cr); - readingsBulkUpdate ($hash, "Errorcode", "none"); - readingsBulkUpdate ($hash, "Error", "none"); - readingsBulkUpdate ($hash, "state", $state); - readingsEndUpdate ($hash,1); - - return ("text/plain; charset=utf-8", $ret); - + if ($request =~ /^\/outchat(\?|&)/x) { # POST- oder GET-Methode empfangen + # data received + return _botCGIdata ($request); + } else { # no data received return ("text/plain; charset=utf-8", "Missing data"); } - } ############################################################################################# -# Hint Hash EN +# Common Gateway data receive +# parsen von outgoing Messages Chat -> FHEM ############################################################################################# -%SSChatBot_vHintsExt_en = ( -); +sub _botCGIdata { ## no critic 'complexity' + my $request = shift; + + my ($text,$timestamp,$channelid,$channelname,$userid,$username,$postid,$triggerword) = ("","","","","","","",""); + my ($command,$cr,$au,$arg,$callbackid,$actions,$actval,$avToExec) = ("","","","","","","",""); + my $state = "active"; + my $do = 0; + my $ret = "success"; + my $success; + my @aul; -############################################################################################# -# Hint Hash DE -############################################################################################# -%SSChatBot_vHintsExt_de = ( + my $args = (split(/outchat\?/x, $request))[1]; # GET-Methode empfangen + + if(!$args) { # POST-Methode empfangen wenn keine GET_Methode ? + $args = (split(/outchat&/x, $request))[1]; + if(!$args) { + Log 1, "TYPE SSChatBot - ERROR - no expected data received"; + return ("text/plain; charset=utf-8", "no expected data received"); + } + } + + $args =~ s/&/" /gx; + $args =~ s/=/="/gx; + $args .= "\""; + + $args = urlDecode($args); + my($a,$h) = parseParams($args); + + if (!defined($h->{botname})) { + Log 1, "TYPE SSChatBot - ERROR - no Botname received"; + return ("text/plain; charset=utf-8", "no FHEM SSChatBot name in message"); + } + + # check ob angegebenes SSChatBot Device definiert, wenn ja Kontext auf botname setzen + my $name = $h->{botname}; # das SSChatBot Device + if(!IsDevice($name, 'SSChatBot')) { + Log 1, qq{ERROR - No SSChatBot device "$name" of Type "SSChatBot" exists}; + return ( "text/plain; charset=utf-8", "No SSChatBot device for webhook \"/outchat\" exists" ); + } + + my $hash = $defs{$name}; # hash des SSChatBot Devices + Log3($name, 4, "$name - ####################################################"); + Log3($name, 4, "$name - ### start Chat operation Receive "); + Log3($name, 4, "$name - ####################################################"); + Log3($name, 5, "$name - raw data received (urlDecoded):\n".Dumper($args)); + + # eine Antwort auf ein interaktives Objekt + if (defined($h->{payload})) { + # ein Benutzer hat ein interaktives Objekt ausgelöst (Button). Die Datenfelder sind nachfolgend beschrieben: + # "actions": Array des Aktionsobjekts, das sich auf die vom Benutzer ausgelöste Aktion bezieht + # "callback_id": Zeichenkette, die sich auf die Callback_id des Anhangs bezieht, in dem sich die vom Benutzer ausgelöste Aktion befindet + # "post_id" + # "token" + # "user": { "user_id","username" } + my $pldata = $h->{payload}; + (undef, $success) = evalJSON($hash,$pldata); + + if (!$success) { + Log3($name, 1, "$name - ERROR - invalid JSON data received:\n".Dumper $pldata); + return ("text/plain; charset=utf-8", "invalid JSON data received"); + } + + my $data = decode_json ($pldata); + Log3($name, 5, "$name - interactive object data (JSON decoded):\n". Dumper $data); + + $h->{token} = $data->{token}; + $h->{post_id} = $data->{post_id}; + $h->{user_id} = $data->{user}{user_id}; + $h->{username} = $data->{user}{username}; + $h->{callback_id} = $data->{callback_id}; + $h->{actions} = "type: ".$data->{actions}[0]{type}.", ". + "name: ".$data->{actions}[0]{name}.", ". + "value: ".$data->{actions}[0]{value}.", ". + "text: ".$data->{actions}[0]{text}.", ". + "style: ".$data->{actions}[0]{style}; + } + + if (!defined($h->{token})) { + Log3($name, 5, "$name - received insufficient data:\n".Dumper($args)); + return ("text/plain; charset=utf-8", "Insufficient data"); + } + + # CSRF Token check + my $FWdev = $hash->{FW}; # das FHEMWEB Device für SSChatBot Device -> ist das empfangene Device + my $FWhash = $defs{$FWdev}; + my $want = $FWhash->{CSRFTOKEN}; + $want = $want?$want:"none"; + my $supplied = $h->{fwcsrf}; + + if($want eq "none" || $want ne $supplied) { # $FW_wname enthält ebenfalls das aufgerufenen FHEMWEB-Device + Log3 ($FW_wname, 2, "$FW_wname - WARNING - FHEMWEB CSRF error for client \"$FWdev\": ". + "received $supplied token is not $want. ". + "For details see the csrfToken FHEMWEB attribute. ". + "The csrfToken must be identical to the token in OUTDEF of $name device."); + return ("text/plain; charset=utf-8", "400 Bad Request"); + } + + # Timestamp dekodieren + if ($h->{timestamp}) { + $h->{timestamp} = FmtDateTime(($h->{timestamp})/1000); + } + + Log3($name, 4, "$name - received data decoded:\n".Dumper($h)); + + $hash->{OPMODE} = "receiveData"; + + # ausgehende Datenfelder (Chat -> FHEM), die das Chat senden kann + # =============================================================== + # token: bot token + # channel_id + # channel_name + # user_id + # username + # post_id + # timestamp + # text + # trigger_word: which trigger word is matched + # -); + $channelid = $h->{channel_id} if($h->{channel_id}); + $channelname = $h->{channel_name} if($h->{channel_name}); + $userid = $h->{user_id} if($h->{user_id}); + $username = $h->{username} if($h->{username}); + $postid = $h->{post_id} if($h->{post_id}); + $callbackid = $h->{callback_id} if($h->{callback_id}); + $timestamp = $h->{timestamp} if($h->{timestamp}); + + # interaktive Schaltflächen (Aktionen) auswerten + if ($h->{actions}) { + $actions = $h->{actions}; + ($actval) = $actions =~ m/^type:\s+button.*?value:\s+(.*?),\s+text:/x; + + if($actval =~ /^\//x) { + Log3($name, 4, "$name - slash command \"$actval\" got from interactive data and execute it with priority"); + $avToExec = $actval; + } + } + + if ($h->{text} || $avToExec) { + $text = $h->{text}; + $text = $avToExec if($avToExec); # Vorrang für empfangene interaktive Data (Schaltflächenwerte) die Slash-Befehle enthalten + if($text =~ /^\/(set.*?|get.*?|code.*?)\s+(.*)$/ix) { # vordefinierte Befehle in FHEM ausführen + my $p1 = substr lc $1, 0, 3; + my $p2 = $2; + + my $pars = { + name => $name, + username => $username, + state => $state, + p2 => $p2, + }; + + if($hrecbot{$p1} && defined &{$hrecbot{$p1}{fn}}) { + $do = 1; + no strict "refs"; ## no critic 'NoStrict' + ($command, $cr, $state) = &{$hrecbot{$p1}{fn}} ($pars); + use strict "refs"; + } + + $cr = $cr ne q{} ? $cr : qq{command '$command' executed}; + Log3($name, 4, "$name - FHEM command return: ".$cr); + + $cr = formString($cr, "command"); + + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $userid, + text => $cr, + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + } + + my $ua = $attr{$name}{userattr}; # Liste aller ownCommandxx zusammenstellen + $ua = "" if(!$ua); + my %hc = map { ($_ => 1) } grep { "$_" =~ m/ownCommand(\d+)/x } split(" ","ownCommand1 $ua"); + + for my $ca (sort keys %hc) { + my $uc = AttrVal($name, $ca, ""); + next if (!$uc); + ($uc,$arg) = split(/\s+/x, $uc, 2); + + if($uc && $text =~ /^$uc\s*?$/x) { # User eigener Slash-Befehl, z.B.: /Wetter + $command = $arg; + $do = 1; + $au = AttrVal($name,"allowedUserForOwn", "all"); # Berechtgung des Chat-Users checken + @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$arg); + $cr = AnalyzeCommandChain(undef, $arg); # FHEM Befehlsketten ausführen + + } else { + $cr = qq{User "$username" is not allowed execute "$arg" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$arg" command. Execution denied !}); + } + + $cr = $cr ne q{} ? $cr : qq{command '$arg' executed}; + Log3($name, 4, "$name - FHEM command return: ".$cr); + + $cr = formString($cr, "command"); + + my $params = { + name => $name, + opmode => "sendItem", + method => "chatbot", + userid => $userid, + text => $cr, + fileUrl => "", + channel => "", + attachment => "" + }; + addQueue ($params); + } + } + + # Wenn Kommando ausgeführt wurde Ergebnisse aus Queue übertragen + if($do) { + RemoveInternalTimer ($hash, "FHEM::SSChatBot::getApiSites"); + InternalTimer (gettimeofday()+1, "FHEM::SSChatBot::getApiSites", "$name", 0); + } + } + + if ($h->{trigger_word}) { + $triggerword = urlDecode($h->{trigger_word}); + Log3($name, 4, "$name - trigger_word received: ".$triggerword); + } + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "recActions", $actions ); + readingsBulkUpdate ($hash, "recCallbackId", $callbackid ); + readingsBulkUpdate ($hash, "recActionsValue", $actval ); + readingsBulkUpdate ($hash, "recChannelId", $channelid ); + readingsBulkUpdate ($hash, "recChannelname", $channelname ); + readingsBulkUpdate ($hash, "recUserId", $userid ); + readingsBulkUpdate ($hash, "recUsername", $username ); + readingsBulkUpdate ($hash, "recPostId", $postid ); + readingsBulkUpdate ($hash, "recTimestamp", $timestamp ); + readingsBulkUpdate ($hash, "recText", $text ); + readingsBulkUpdate ($hash, "recTriggerword", $triggerword ); + readingsBulkUpdate ($hash, "recCommand", $command ); + readingsBulkUpdate ($hash, "sendCommandReturn", $cr ); + readingsBulkUpdate ($hash, "Errorcode", "none" ); + readingsBulkUpdate ($hash, "Error", "none" ); + readingsBulkUpdate ($hash, "state", $state ); + readingsEndUpdate ($hash,1); + + return ("text/plain; charset=utf-8", $ret); +} + +################################################################ +# botCGI /set +# set-Befehl in FHEM ausführen +################################################################ +sub __botCGIrecSet { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = "set ".$p2; + my $au = AttrVal($name,"allowedUserForSet", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$command); + $cr = CommandSet(undef, $p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} + +################################################################ +# botCGI /get +# get-Befehl in FHEM ausführen +################################################################ +sub __botCGIrecGet { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = "get ".$p2; + my $au = AttrVal($name,"allowedUserForGet", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$command); + $cr = CommandGet(undef, $p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} + +################################################################ +# botCGI /code +# Perl Code in FHEM ausführen +################################################################ +sub __botCGIrecCod { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $username = $paref->{username}; + my $state = $paref->{state}; + my $p2 = $paref->{p2}; + + my $cr = ""; + my $command = $p2; + my $au = AttrVal($name,"allowedUserForCode", "all"); + my @aul = split(",",$au); + + if($au eq "all" || $username ~~ @aul) { + my $code = $p2; + if($p2 =~ m/^\s*(\{.*\})\s*$/xs) { + $p2 = $1; + } else { + $p2 = ''; + } + Log3($name, 4, qq{$name - Synology Chat user "$username" execute FHEM command: }.$p2); + $cr = AnalyzePerlCommand(undef, $p2) if($p2); + } else { + $cr = qq{User "$username" is not allowed execute "$command" command}; + $state = qq{command execution denied}; + Log3($name, 2, qq{$name - WARNING - Chat user "$username" is not authorized for "$command" command. Execution denied !}); + } + +return ($command, $cr, $state); +} 1; @@ -1949,21 +2259,647 @@ sub SSChatBot_CGI { ## no critic

SSChatBot

    + This module is used to integrate Synology Chat Server with FHEM. This makes it possible, + Exchange messages between FHEM and Synology Chat Server.
    + A detailed description of the module is available in the + Wiki available.
    +

    -The guide for this module is currently only available in the german Wiki. + + Definition +

    +
      + + The definition is made with:

      +
        + define <Name> SSChatBot <IP> [Port] [Protokoll] +
      +
      + + The Port and Protocol entries are optional. +

      + +
        +
      • IP: IP address or name of Synology DiskStation. If the name is used, set the dnsServer global attribute.
      • +
      • Port: Port of Synology DiskStation (default 5000)
      • +
      • Protocol: Protocol for messages towards chat server, http or https (default http)
      +
      + + During the definition, an extra FHEMWEB device for receiving messages is created in addition to the SSChaBot device + in the "Chat" room. The port of the FHEMWEB device is automatically determined with start port 8082. If this port is occupied, + the ports are checked in ascending order for possible assignment by an FHEMWEB device and the next + free port is assigned to the new Device.
      + + The chat integration distinguishes between "Incoming messages" (FHEM -> Chat) and "Outgoing messages + (Chat -> FHEM).
      + +
    +

    + + + Configuration +

    +
      + + For the activation of incoming messages (FHEM -> Chat) a bot token is required. This token can be activated via the user-defined + Embedding functions in the Synology Chat application can be created or modified from within the Synology Chat application. + (see also the wiki section )
      + + The token is inserted into the newly defined SSChatBot device with the command: +

      +
        + set <Name> botToken U6FOMH9IgT22ECJceaIW0fNwEi7VfqWQFPMgJQUJ6vpaGoWZ1SJkOGP7zlVIscCp +
      +
      + Use of course the real token created by the chat application. +

      + + For activation of outgoing messages (Chat -> FHEM) the field Outgoing URL must be filled in the chat application. + To do so, click the Profile Photo icon in the upper right corner of the called Synology Chat Application and select + "Integration." Then select the bot created in the first step in the "Bots" menu. + + The value of the internal OUTDEF of the created SSChatBot device is now copied into the field Outgoing URL.
      + For example, the string could look like this:

      + +
        + http://myserver.mydom:8086/sschat/outchat?botname=SynChatBot&fwcsrf=5de17731 +
      +
      + + (see also the wiki section )
      + +

      + General information on sending messages
      + Messages that FHEM sends to the chat server (incoming messages) are first placed in a queue in FHEM. + The send process is started immediately. If the transmission was successful, the message is deleted from the queue. + Otherwise, it remains in the queue and the send process will, in a time interval, restarted.
      + With the Set command restartSendqueue the processing of the queue can be started manually + (for example, after Synology maintenance). + +

      + + Allgemeine Information zum Nachrichtempfang
      + Um Befehle vom Chat Server an FHEM zu senden, werden Slash-Befehle (/) verwendet. Sie sind vor der Verwendung im Synology + Chat und ggf. zusätzlich im SSChatBot Device (User spezifische Befehle) zu konfigurieren.

      + + The following command forms are supported:

      +
        +
      • /set
      • +
      • /get
      • +
      • /code
      • +
      • /<User specific command> (see attribute ownCommandx)
      • +
      +
      + + Further detailed information on configuring message reception is available in the corresponding + wiki section. +
    +

    + + + Set +

    +
      + +
        + +
      • asyncSendItem <Item>
        + + Sends a message to one or more chat recipients.
        + For more information about the available options for asyncSendItem, especially the use of interactive + objects (buttons), please consult this + wiki section. +

        + +
          + Beispiele:
          + set <Name> asyncSendItem First message line to post.\n You can also have a second message line. [users="<User>"]
          + set <Name> asyncSendItem text="First message line to post.\n You can also have a second message line. [users="<User>"]
          + set <Name> asyncSendItem text="https://www.synology.com" [users="<User>"]
          + set <Name> asyncSendItem text="Check this! <https://www.synology.com|Click here> for details!" [users="<User1>,<User2>"]
          + set <name> asyncSendItem text="a funny picture" fileUrl="http://imgur.com/xxxxx" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="current plot file" svg="<SVG-Device>[,<Zoom>][,<Offset>]" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="<Message text>" attachments="[{ + "callback_id": "<Text for Reading recCallbackId>", "text": "<Heading of the button>", + "actions":[{"type": "button", "name": "<text>", "value": "<value>", "text": "<text>", "style": "<color>"}] }]"
        + +

      • +
      + +
        + +
      • botToken <Token>
        + + Saves the token for access to the chat as a bot. + +

      • +
      + +
        + +
      • listSendqueue
        + + Shows the messages still to be transmitted to the chat.
        + All messages to be sent are first stored in a queue and transmitted asynchronously to the chat server. + +

      • +
      + +
        + +
      • purgeSendqueue <-all- | -permError- | index>
        + + Deletes entries from the send queue.

        + +
          +
        • -all- : Deletes all entries of the send queue.
        • +
        • -permError- : Deletes all entries of the send queue with "permanent Error" status.
        • +
        • index : Deletes selected entry with "index".
          + The entries in the send queue can be viewed beforehand with "set listSendqueue" to find the desired index.
        • +
        + +

      • +
      + +
        + +
      • restartSendqueue [force]
        + + Restarts the processing of the send queue manually.
        + Any entries in the send queue marked forbidSend are not sent again.
        + If the call is made with the option force, entries marked forbidSend are also taken into account. + +

      • +
      + +
    + + + Get +

    +
      + +
        + +
      • chatChannellist
        + + Creates a list of channels visible to the bot. + +

      • +
      + +
        + +
      • chatUserlist
        + + Creates a list of users visible to the bot.
        + If no users are listed, the users on Synology must have permission to use the chat application + can be assigned. + +

      • +
      + +
        + +
      • storedToken
        + + Displays the stored token. + +

      • +
      + +
        + +
      • versionNotes
        + + Lists significant changes in the version history of the module. + +

      • +
      + +
    + + + Attributes +

    +
      + +
        + +
      • allowedUserForCode
        + + Names the chat users who are allowed to trigger Perl code in FHEM when the slash command /code is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForGet
        + + Names the chat users who may trigger Get commands in FHEM when the slash command /get is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForOwn
        + + Names the chat users who are allowed to trigger the commands defined in the attribute "ownCommand" in FHEM.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • allowedUserForSet
        + + Names the chat users who are allowed to trigger set commands in FHEM when the slash command /set is received.
        + (default: all users allowed) + +

      • +
      + +
        + +
      • defaultPeer
        + + One or more (default) recipients for messages. Can be specified with the users= tag in the command asyncSendItem + can be overridden. + +

      • +
      + +
        + +
      • httptimeout <seconds>
        + + Sets the connection timeout to the chat server.
        + (default 20 seconds) + +

      • +
      + +
        + +
      • ownCommandx <Slash command> <Command>
        + + Defines a <Slash command> <Command> pair. The slash command and the command are separated by a + Separate spaces.
        + The command is executed when the SSChatBot receives the slash command. + The command can be an FHEM command or Perl code. Perl code must be enclosed in { }.

        + +
          + Examples:
          + attr <Name> ownCommand1 /Wozi_Temp {ReadingsVal("eg.wz.wallthermostat","measured-temp",0)}
          + attr <Name> ownCommand2 /Wetter get MyWetter wind_speed
          +
        + +

      • +
      + +
        + +
      • showTokenInLog
        + + If set, the transmitted bot token is displayed in the log with verbose 4/5.
        + (default: 0) + +

      • +
      + +
- =end html =begin html_DE

SSChatBot

    + Mit diesem Modul erfolgt die Integration des Synology Chat Servers in FHEM. Dadurch ist es möglich, + Nachrichten zwischen FHEM und Synology Chat Server auszutauschen.
    + Eine ausführliche Beschreibung des Moduls ist im + Wiki vorhanden.
    +

    -Die Beschreibung des Moduls ist momentan nur im Wiki vorhanden. + + Definition +

    +
      + + Die Definition erfolgt mit:

      +
        + define <Name> SSChatBot <IP> [Port] [Protokoll] +
      +
      + + Die Angaben Port und Protokoll sind optional. +

      +
        +
      • IP: IP-Adresse oder Name der Synology Diskstation. Wird der Name benutzt, ist das globale Attribut dnsServer zu setzen.
      • +
      • Port: Port der Synology Diskstation (default 5000)
      • +
      • Protokoll: Protokoll für Messages Richtung Chat-Server, http oder https (default http)
      • +
      +
      + + Bei der Definition wird neben dem SSChaBot Device ebenfalls ein extra FHEMWEB Device zum Nachrichtenempfang automatisiert + im Raum "Chat" angelegt. Der Port des FHEMWEB Devices wird automatisch ermittelt mit Startport 8082. Ist dieser Port belegt, + werden die Ports in aufsteigender Reihenfolge auf eine eventuelle Belegung durch ein FHEMWEB Device geprüft und der nächste + freie Port wird dem neuen Device zugewiesen.
      + + Die Chatintegration unterscheidet zwischen "Eingehende Nachrichten" (FHEM -> Chat) und "Ausgehende Nachrichten" + (Chat -> FHEM).
      + +
    +

    + + + Konfiguration +

    +
      + + Für die Aktivierung eingehender Nachrichten (FHEM -> Chat) wird ein Bot-Token benötigt. Dieser Token wird über die benutzerdefinierte + Einbindungsfunktionen in der Synology Chat-Applikation erstellt bzw. kann darüber auch verändert werden. + (siehe dazu auch den Wiki-Abschnitt )
      + + Der Token wird in das neu definierten SSChatBot-Device eingefügt mit dem Befehl: +

      +
        + set <Name> botToken U6FOMH9IgT22ECJceaIW0fNwEi7VfqWQFPMgJQUJ6vpaGoWZ1SJkOGP7zlVIscCp +
      +
      + Es ist natürlich der reale, durch die Chat-Applikation erstellte Token einzusetzen. +

      + + Zur Aktivierung ausgehende Nachrichten (Chat -> FHEM) muß in der Chat-Applikation das Feld Ausgehende URL gefüllt werden. + Klicken Sie dazu auf das Symbol Profilfoto oben rechts in der aufgerufenen Synology Chat-Applikation und wählen Sie + "Einbindung". Wählen sie dann im Menü "Bots" den im ersten Schritt erstellten Bot aus. + + In das Feld Ausgehende URL wird nun der Wert des Internals OUTDEF des erstellten SSChatBot Devices hineinkopiert.
      + Zum Beispiel könnte der String so aussehen:

      + +
        + http://myserver.mydom:8086/sschat/outchat?botname=SynChatBot&fwcsrf=5de17731 +
      +
      + + (siehe dazu auch den Wiki-Abschnitt )
      + +

      + Allgemeine Information zum Nachrichtenversand
      + Nachrichten, die FHEM an den Chat Server sendet (eingehende Nachrichten), werden in FHEM zunächst in eine Queue gestellt. + Der Sendeprozess wird sofort gestartet. War die Übermittlung erfolgreich, wird die Nachricht aus der Queue gelöscht. + Anderenfalls verbleibt sie in der Queue und der Sendeprozess wird, in einem von der Anzahl der Fehlversuche abhängigen + Zeitintervall, erneut gestartet.
      + Mit dem Set-Befehl restartSendqueue kann die Abarbeitung der Queue manuell angestartet werden + (zum Beispiel nach einer Synology Wartung). + +

      + + Allgemeine Information zum Nachrichtempfang
      + Um Befehle vom Chat Server an FHEM zu senden, werden Slash-Befehle (/) verwendet. Sie sind vor der Verwendung im Synology + Chat und ggf. zusätzlich im SSChatBot Device (User spezifische Befehle) zu konfigurieren.

      + + Folgende Befehlsformen werden unterstützt:

      +
        +
      • /set
      • +
      • /get
      • +
      • /code
      • +
      • /<User spezifischer Befehl> (siehe Attribut ownCommandx)
      • +
      +
      + + Weitere ausfühliche Informationen zur Konfiguration des Nachrichtenempfangs sind im entsprechenden + Wiki-Abschnitt enthalten. + +
    +

    + + + Set +

    +
      + +
        + +
      • asyncSendItem <Item>
        + + Sendet eine Nachricht an einen oder mehrere Chatempfänger.
        + Für weitere Informationen zu den verfügbaren Optionen für asyncSendItem, insbesondere zur Benutzung von interaktiven + Objekten (Schaltflächen), konsultieren sie bitte diesen + Wiki-Abschnitt. +

        + +
          + Beispiele:
          + set <Name> asyncSendItem Erste zu postende Nachrichtenzeile.\n Sie können auch eine zweite Nachrichtenzeile haben. [users="<User>"]
          + set <Name> asyncSendItem text="Erste zu postende Nachrichtenzeile.\n Sie können auch eine zweite Nachrichtenzeile haben." [users="<User>"]
          + set <Name> asyncSendItem text="https://www.synology.com" [users="<User>"]
          + set <Name> asyncSendItem text="Überprüfen Sie dies!! <https://www.synology.com|Click hier> für Einzelheiten!" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="ein lustiges Bild" fileUrl="http://imgur.com/xxxxx" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="aktuelles Plotfile" svg="<SVG-Device>[,<Zoom>][,<Offset>]" [users="<User1>,<User2>"]
          + set <Name> asyncSendItem text="<Mitteilungstext>" attachments="[{ + "callback_id": "<Text für Reading recCallbackId>", "text": "<Überschrift des Buttons>", + "actions":[{"type": "button", "name": "<Text>", "value": "<Wert>", "text": "<Text>", "style": "<Farbe>"}] }]"
          +
        + +

      • +
      + +
        + +
      • botToken <Token>
        + + Seichert den Token für den Zugriff auf den Chat als Bot. + +

      • +
      + +
        + +
      • listSendqueue
        + + Zeigt die noch an den Chat zu übertragenden Nachrichten.
        + Alle zu sendenden Nachrichten werden zunächst in einer Queue gespeichert und asynchron zum Chatserver übertragen. + +

      • +
      + +
        + +
      • purgeSendqueue <-all- | -permError- | index>
        + + Löscht Einträge aus der Sendequeue.

        + +
          +
        • -all- : Löscht alle Einträge der Sendqueue.
        • +
        • -permError- : Löscht alle Einträge der Sendqueue mit "permanent Error" Status.
        • +
        • index : Löscht ausgewählten Eintrag mit "index".
          + Die Einträge in der Sendqueue kann man sich vorher mit "set listSendqueue" ansehen um den gewünschten Index zu finden.
        • +
        + +

      • +
      + +
        + +
      • restartSendqueue [force]
        + + Startet die Abarbeitung der Sendequeue manuell neu.
        + Eventuell in der Sendequeue vorhandene Einträge mit der Kennzeichnung forbidSend werden nicht erneut versendet.
        + Erfolgt der Aufruf mit der Option force, werden auch Einträge mit der Kennzeichnung forbidSend berücksichtigt. + +

      • +
      + +
    + + + Get +

    +
      + +
        + +
      • chatChannellist
        + + Erstellt eine Liste der für den Bot sichtbaren Channels. + +

      • +
      + +
        + +
      • chatUserlist
        + + Erstellt eine Liste der für den Bot sichtbaren Usern.
        + Sollten keine User gelistet werden, muss den Usern auf der Synology die Berechtigung für die Chat-Anwendung + zugewiesen werden. + +

      • +
      + +
        + +
      • storedToken
        + + Zeigt den gespeicherten Token an. + +

      • +
      + +
        + +
      • versionNotes
        + + Listet wesentliche Änderungen in der Versionshistorie des Moduls auf. + +

      • +
      + +
    + + + Attribute +

    +
      + +
        + +
      • allowedUserForCode
        + + Benennt die Chat-User, die Perl-Code in FHEM auslösen dürfen wenn der Slash-Befehl /code empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForGet
        + + Benennt die Chat-User, die Get-Kommandos in FHEM auslösen dürfen wenn der Slash-Befehl /get empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForOwn
        + + Benennt die Chat-User, die die im Attribut "ownCommand" definierte Kommandos in FHEM auslösen dürfen.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • allowedUserForSet
        + + Benennt die Chat-User, die Set-Kommandos in FHEM auslösen dürfen wenn der Slash-Befehl /set empfangen wurde.
        + (default: alle User erlaubt) + +

      • +
      + +
        + +
      • defaultPeer
        + + Ein oder mehrere (default) Empfänger für Nachrichten. Kann mit dem users= Tag im Befehl asyncSendItem + übersteuert werden. + +

      • +
      + +
        + +
      • httptimeout <Sekunden>
        + + Stellt den Verbindungstimeout zum Chatserver ein.
        + (default 20 Sekunden) + +

      • +
      + +
        + +
      • ownCommandx <Slash-Befehl> <Kommando>
        + + Definiert ein <Slash-Befehl> <Kommando> Paar. Der Slash-Befehl und das Kommando sind durch ein + Leerzeichen zu trennen.
        + Das Kommando wird ausgeführt wenn der SSChatBot den Slash-Befehl empfängt. + Das Kommando kann ein FHEM Befehl oder Perl-Code sein. Perl-Code ist in { } einzuschließen.

        + +
          + Beispiele:
          + attr <Name> ownCommand1 /Wozi_Temp {ReadingsVal("eg.wz.wandthermostat","measured-temp",0)}
          + attr <Name> ownCommand2 /Wetter get MyWetter wind_speed
          +
        + +

      • +
      + +
        + +
      • showTokenInLog
        + + Wenn gesetzt, wird im Log mit verbose 4/5 der übermittelte Bot-Token angezeigt.
        + (default: 0) + +

      • +
      +
    +
=end html_DE
Username ID state Nickname Email
$cn $ci $cr $mb $ty
$cn $ci $cr $mb $ty