diff --git a/FHEM/98_Matrix.pm b/FHEM/98_Matrix.pm index a5ecf61..71306ab 100644 --- a/FHEM/98_Matrix.pm +++ b/FHEM/98_Matrix.pm @@ -25,465 +25,43 @@ ########################################################################## # $Id: 98_Matrix.pm 28158 2022-11-02 19:56:00Z Man-fred $ -package main; +package FHEM::Matrix; use strict; use warnings; use HttpUtils; -use vars qw(%data); +use FHEM::Meta; +use GPUtils qw(GP_Export); -my $Module_Version = '0.0.7'; +require FHEM::Device::Matrix; -my $AttrList = "MatrixRoom MatrixSender MatrixQuestion_0 MatrixQuestion_1 " . $readingFnAttributes; +#-- Run before package compilation +BEGIN { -sub Matrix_PerformHttpRequest($$$) -{ - my ($hash, $def, $value) = @_; - my $now = gettimeofday(); - my $name = $hash->{NAME}; - my $param = { - timeout => 10, - hash => $hash, # Muss gesetzt werden, damit die Callback funktion wieder $hash hat - def => $def, # sichern für eventuelle Wiederholung - value => $value, # sichern für eventuelle Wiederholung - method => "POST", # standard, sonst überschreiben - header => "User-Agent: HttpUtils/2.2.3\r\nAccept: application/json", # Den Header gemäß abzufragender Daten setzen - callback => \&Matrix_ParseHttpResponse # Diese Funktion soll das Ergebnis dieser HTTP Anfrage bearbeiten - }; - $data{MATRIX}{"$name"}{"busy"} = $data{MATRIX}{"$name"}{"busy"} + 1; # queue is busy until response is received - $data{MATRIX}{"$name"}{'LASTSEND'} = $now; # remember when last sent - if ($def eq "sync" && $data{MATRIX}{"$name"}{"next_refresh"} < $now){ - $def = "refresh"; - $data{MATRIX}{"$name"}{"next_refresh"} = $now + 300; - } - - my $device_id = ReadingsVal($name, 'device_id', undef) ? ', "device_id":"'.ReadingsVal($name, 'device_id', undef).'"' : ""; - if ($def eq "register"){ - $param->{'url'} = $hash->{server}."/_matrix/client/v3/register"; - $param->{'data'} = '{"type":"m.login.password", "identifier":{ "type":"m.id.user", "user":"'.$hash->{user}.'" }, "password":"'.$hash->{password}.'"}'; - } - if ($def eq"reg2"){ - $param->{'url'} = $hash->{server}."/_matrix/client/v3/register"; - $param->{'data'} = '{"username":"'.$hash->{user}.'", "password":"'.$hash->{password}.'", "auth": {"session":"'.$data{MATRIX}{"$name"}{"session"}.'","type":"m.login.dummy"}}'; - } - if ($def eq "login"){ - $param->{'url'} = $hash->{server}."/_matrix/client/v3/login"; - $param->{'data'} = '{"type":"m.login.password", "refresh_token": true, "identifier":{ "type":"m.id.user", "user":"'.$hash->{user}.'" }, "password":"'.$hash->{password}.'"' - .$device_id.'}'; - } - if ($def eq "refresh"){ - $param->{'url'} = $hash->{server}.'/_matrix/client/v1/refresh'; - $param->{'data'} = '{"refresh_token": "'.$data{MATRIX}{"$name"}{"refresh_token"}.'"}'; - } - if ($def eq "wellknown"){ - $param->{'url'} = $hash->{server}."/.well-known/matrix/client"; - } - if ($def eq "msg"){ - $param->{'url'} = $hash->{server}.'/_matrix/client/r0/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.room.message?access_token='.$data{MATRIX}{"$name"}{"access_token"}; - $param->{'data'} = '{"msgtype":"m.text", "body":"'.$value.'"}'; - } - if ($def eq "question.start"){ - $value = AttrVal($name, "MatrixQuestion_$value",$value); # if ($value =~ /[0-9]/); - my @question = split(':',$value); - my $size = @question; - $value =~ tr/:/
/; - # min. question and one answer - if (int(@question) >= 2){ - $param->{'url'} = $hash->{server}.'/_matrix/client/v3/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.poll.start?access_token='.$data{MATRIX}{"$name"}{"access_token"}; - $param->{'data'} = '{"org.matrix.msc3381.poll.start": {"max_selections": 1,'. - '"question": {"org.matrix.msc1767.text": "'.$question[0].'"},'. - '"kind": "org.matrix.msc3381.poll.undisclosed",'. - '"answers": [{"id": "'.$question[1].'", "org.matrix.msc1767.text": "'.$question[1].'"},{"id":"'.$question[2].'","org.matrix.msc1767.text": "'.$question[2].'"}],'. - '"org.matrix.msc1767.text": "'.$value.'"}}'; - } else { - Log3 $name, 3, "question.start: $value $size $question[0]"; - return; - } - } - if ($def eq "question.end"){ - $value = ReadingsVal($name, "question_id", "") if (!$value); - $param->{'url'} = $hash->{server}.'/_matrix/client/v3/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.poll.end?access_token='.$data{MATRIX}{"$name"}{"access_token"}; - # ""'.ReadingsVal($name, 'questionEventId', '!!').' - $param->{'data'} = '{"m.relates_to": {"rel_type": "m.reference","event_id": "'.$value.'"},"org.matrix.msc3381.poll.end": {},'. - '"org.matrix.msc1767.text": "Antort '.ReadingsVal($name, "answer", "").' erhalten von '.ReadingsVal($name, "sender", "").'"}'; - } - if ($def eq "sync"){ - my $since = ReadingsVal($name, "since", undef) ? '&since='.ReadingsVal($name, "since", undef) : ""; - my $full_state = ReadingsVal($name, "poll.fullstate",undef); - if ($full_state){ - $full_state = "&full_state=true"; - readingsSingleUpdate($hash, "poll.fullstate", 0, 1); - } else { - $full_state = ""; - } - $param->{'url'} = $hash->{server}.'/_matrix/client/r0/sync?access_token='.$data{MATRIX}{"$name"}{"access_token"}.$since.$full_state.'&timeout=50000&filter='.ReadingsVal($name, 'filter_id',0); - $param->{'method'} = 'GET'; - $param->{'timeout'} = 60; - } - if ($def eq "filter"){ - if ($value){ # get - $param->{'url'} = $hash->{server}.'/_matrix/client/v3/user/'.ReadingsVal($name, "user_id",0).'/filter/'.$value.'?access_token='.$data{MATRIX}{"$name"}{"access_token"}; - $param->{'method'} = 'GET'; - } else { - $param->{'url'} = $hash->{server}.'/_matrix/client/v3/user/'.ReadingsVal($name, "user_id",0).'/filter?access_token='.$data{MATRIX}{"$name"}{"access_token"}; - $param->{'data'} = '{'; - $param->{'data'} .= '"event_fields": ["type","content","sender"],'; - $param->{'data'} .= '"event_format": "client", '; - $param->{'data'} .= '"presence": { "senders": [ "@xx:example.com"]}'; # no presence - #$param->{'data'} .= '"room": { "ephemeral": {"rooms": ["'.AttrVal($name, 'MatrixRoom', '!!').'"],"types": ["m.receipt"]}, "state": {"types": ["m.room.*"]},"timeline": {"types": ["m.room.message"] } }'; - $param->{'data'} .= '}'; - } - } - - my $test = "$param->{url}, " - . ( $param->{data} ? "\r\ndata: $param->{data}, " : "" ) - . ( $param->{header} ? "\r\nheader: $param->{header}" : "" ); - readingsSingleUpdate($hash, "fullRequest", $test, 1); # Readings erzeugen - $test = "$name: Matrix sends with timeout $param->{timeout} to ".$test; - Log3 $name, 3, $test; - - HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. - return undef; -} - -sub Matrix_ParseHttpResponse($) -{ - my ($param, $err, $data) = @_; - my $hash = $param->{hash}; - my $def = $param->{def}; - my $value = $param->{value}; - my $name = $hash->{NAME}; - my $now = gettimeofday(); - my $nextRequest = ""; - - readingsBeginUpdate($hash); - ###readingsBulkUpdate($hash, "httpHeader", $param->{httpheader}); - readingsBulkUpdate($hash, "httpStatus", $param->{code}); - $hash->{STATE} = $def.' - '.$param->{code}; - if($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist - Log3 $name, 3, "error while requesting ".$param->{url}." - $err"; # Eintrag fürs Log - readingsBulkUpdate($hash, "fullResponse", "ERROR ".$err); # Readings erzeugen - $data{MATRIX}{"$name"}{"FAILS"} = 3; - } - elsif($data ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) - Log3 $name, 3, $def." returned: $data"; # Eintrag fürs Log - my $decoded = eval { decode_json($data) }; - Log3 $name, 2, "$name: json error: $@ in data" if( $@ ); - if ($param->{code} == 200){ - $data{MATRIX}{"$name"}{"FAILS"} = 0; - } else { - $data{MATRIX}{"$name"}{"FAILS"}++; - } - - # default next request - $nextRequest = "sync" ; - # An dieser Stelle die Antwort parsen / verarbeiten mit $data - - # "errcode":"M_UNKNOWN_TOKEN: login or refresh - readingsBulkUpdate($hash, "fullResponse", $data); - my $errcode = $decoded->{'errcode'} ? $decoded->{'errcode'} : ""; - if ($errcode eq "M_UNKNOWN_TOKEN"){ - $data{MATRIX}{"$name"}{"repeat"} = $param if ($def ne "sync"); - if ($decoded->{'error'} eq "Access token has expired"){ - if ($decoded->{'soft_logout'} eq "true"){ - $nextRequest = 'refresh'; - }else{ - $nextRequest = 'login'; - } - } elsif ($decoded->{'error'} eq "refresh token does not exist"){ - $nextRequest = 'login'; - } - } - - if ($def eq "register"){ - $data{MATRIX}{"$name"}{"session"} = $decoded->{'session'}; - $nextRequest = "reg2"; - } - if ($param->{code} == 200 && ($def eq "reg2" || $def eq "login" || $def eq "refresh")){ - readingsBulkUpdate($hash, "user_id", $decoded->{'user_id'}) if ($decoded->{'user_id'}); - readingsBulkUpdate($hash, "home_server", $decoded->{'home_server'}) if ($decoded->{'home_server'}); - readingsBulkUpdate($hash, "device_id", $decoded->{'device_id'}) if ($decoded->{'device_id'}); - readingsBulkUpdate($hash, "last_register", $param->{code}) if $def eq "reg2"; - readingsBulkUpdate($hash, "last_login", $param->{code}) if $def eq "login"; - readingsBulkUpdate($hash, "last_refresh", $param->{code}) if $def eq "refresh"; - - $data{MATRIX}{"$name"}{"expires"} = $decoded->{'expires_in_ms'} if ($decoded->{'expires_in_ms'}); - $data{MATRIX}{"$name"}{"refresh_token"} = $decoded->{'refresh_token'} if ($decoded->{'refresh_token'}); - $data{MATRIX}{"$name"}{"access_token"} = $decoded->{'access_token'} if ($decoded->{'access_token'}); - $data{MATRIX}{"$name"}{"next_refresh"} = $now + $data{MATRIX}{"$name"}{"expires"}/1000 - 60; # refresh one minute before end - } - if ($def eq "wellknown"){ - # https://spec.matrix.org/unstable/client-server-api/ - } - if ($param->{code} == 200 && $def eq "sync"){ - readingsBulkUpdate($hash, "since", $decoded->{'next_batch'}) if ($decoded->{'next_batch'}); - # roomlist - my $list = $decoded->{'rooms'}->{'join'}; - #my @roomlist = (); - my $pos = 0; - foreach my $id ( keys $list->%* ) { - if (ref $list->{$id} eq ref {}) { - my $member = ""; - #my $room = $list->{$id}; - $pos = $pos + 1; - # MatrixRoom ? - readingsBulkUpdate($hash, "room$pos.id", $id); - #foreach my $id ( $decoded->{'rooms'}->{'join'}->{AttrVal($name, 'MatrixRoom', '!!')}->{'timeline'}->{'events'}->@* ) { - foreach my $ev ( $list->{$id}->{'state'}->{'events'}->@* ) { - readingsBulkUpdate($hash, "room$pos.topic", $ev->{'content'}->{'topic'}) if ($ev->{'type'} eq 'm.room.topic'); - readingsBulkUpdate($hash, "room$pos.name", $ev->{'content'}->{'name'}) if ($ev->{'type'} eq 'm.room.name'); - $member .= "$ev->{'sender'} " if ($ev->{'type'} eq 'm.room.member'); - } - readingsBulkUpdate($hash, "room$pos.member", $member); - foreach my $tl ( $list->{$id}->{'timeline'}->{'events'}->@* ) { - readingsBulkUpdate($hash, "room$pos.topic", $tl->{'content'}->{'topic'}) if ($tl->{'type'} eq 'm.room.topic'); - readingsBulkUpdate($hash, "room$pos.name", $tl->{'content'}->{'name'}) if ($tl->{'type'} eq 'm.room.name'); - if ($tl->{'type'} eq 'm.room.message' && $tl->{'content'}->{'msgtype'} eq 'm.text'){ - my $sender = $tl->{'sender'}; - if (AttrVal($name, 'MatrixSender', '') =~ $sender){ - readingsBulkUpdate($hash, "message", $tl->{'content'}->{'body'}); - readingsBulkUpdate($hash, "sender", $sender); - # command - - } - #else { - # readingsBulkUpdate($hash, "message", 'ignoriert, nicht '.AttrVal($name, 'MatrixSender', '')); - # readingsBulkUpdate($hash, "sender", $sender); - #} - } elsif ($tl->{'type'} eq "org.matrix.msc3381.poll.response"){ - my $sender = $tl->{'sender'}; - if (AttrVal($name, 'MatrixSender', '') =~ $sender){ - readingsBulkUpdate($hash, "answer", $tl->{'content'}->{'org.matrix.msc3381.poll.response'}->{'answers'}[0]); - readingsBulkUpdate($hash, "sender", $sender); - # poll.end and - $nextRequest = "question.end" ; - # command - - } - } - } - #push(@roomlist,"$id: "; - } - } - } - if ($def eq "filter"){ - readingsBulkUpdate($hash, "filter_id", $decoded->{'filter_id'}) if ($decoded->{'filter_id'}); - } - if ($def eq "msg" ){ - readingsBulkUpdate($hash, "event_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); - #m.relates_to - } - if ($def eq "question.start"){ - readingsBulkUpdate($hash, "question_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); - #m.relates_to - } - if ($def eq "question.end"){ - readingsBulkUpdate($hash, "event_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); - readingsBulkUpdate($hash, "question_id", "") if ($decoded->{'event_id'}); - #m.relates_to - } - } - readingsEndUpdate($hash, 1); - $data{MATRIX}{"$name"}{"busy"} = $data{MATRIX}{"$name"}{"busy"} - 1; # queue is busy until response is received - $data{MATRIX}{"$name"}{"sync"} = 0 if ($def eq "sync"); # possible next sync - $nextRequest = "" if ($nextRequest eq "sync" && $data{MATRIX}{"$name"}{"sync"} == 1); # only one sync at a time! - - #if ($def eq "sync" && $nextRequest eq "sync" && ReadingsVal($name,'poll',0) == 1 && $data{MATRIX}{"$name"}{"FAILS"} < 3){ - # Matrix_PerformHttpRequest($hash, $nextRequest, ''); - #} els - if ($nextRequest ne "" && ReadingsVal($name,'poll',0) == 1 && $data{MATRIX}{"$name"}{"FAILS"} < 3) { - if ($nextRequest eq "sync" && $data{MATRIX}{"$name"}{"repeat"}){ - $def = $data{MATRIX}{"$name"}{"repeat"}->{"def"}; - $value = $data{MATRIX}{"$name"}{"repeat"}->{"value"}; - $data{MATRIX}{"$name"}{"repeat"} = undef; - Matrix_PerformHttpRequest($hash, $def, $value); - } else { - Matrix_PerformHttpRequest($hash, $nextRequest, ''); - } - } - # Damit ist die Abfrage zuende. + #-- Export to main context with different name + GP_Export( + qw( + Initialize + ) + ); } sub Matrix_Initialize { my ($hash) = @_; - $hash->{DefFn} = \&Matrix_Define; - $hash->{UndefFn} = \&Matrix_Undef; - $hash->{SetFn} = \&Matrix_Set; - $hash->{GetFn} = \&Matrix_Get; - $hash->{AttrFn} = \&Matrix_Attr; - $hash->{ReadFn} = \&Matrix_Read; - $hash->{RenameFn} = \&Matrix_Rename; - $hash->{NotifyFn} = \&Matrix_Notify; + $hash->{DefFn} = \&FHEM::Device::Matrix::Define; + $hash->{UndefFn} = \&FHEM::Device::Matrix::Undef; + $hash->{SetFn} = \&FHEM::Device::Matrix::Set; + $hash->{GetFn} = \&FHEM::Device::Matrix::Get; + $hash->{AttrFn} = \&FHEM::Device::Matrix::Attr; + $hash->{ReadFn} = \&FHEM::Device::Matrix::Read; + $hash->{RenameFn} = \&FHEM::Device::Matrix::Rename; + $hash->{NotifyFn} = \&FHEM::Device::Matrix::Notify; - $hash->{AttrList} = $AttrList." MatrixMessage"; -} + $hash->{AttrList} = "MatrixRoom MatrixSender MatrixQuestion_0 MatrixQuestion_1 MatrixMessage " . $::readingFnAttributes; -sub Matrix_Define { - my ($hash, $def) = @_; - my @param = split('[ \t]+', $def); - - if(int(@param) < 4) { - return "too few parameters: define Matrix "; - } - - $hash->{name} = $param[0]; - $hash->{server} = $param[2]; - $hash->{user} = $param[3]; - $hash->{password} = $param[4]; - - my $name = $param[0]; - #$data{MATRIX}{"$name"}{"FAILS"} = 0; - #$data{MATRIX}{"$name"}{"busy"} = 0; # queue is busy until response is received - #$data{MATRIX}{"$name"}{'LASTSEND'} = 0; # remember when last sent - #$data{MATRIX}{"$name"}{"expires"} = 0; - #$data{MATRIX}{"$name"}{"refresh_token"} = ""; - #$data{MATRIX}{"$name"}{"access_token"} = ""; - #$data{MATRIX}{"$name"}{"session"} = ""; # used for register - #$hash->{STATE} = "paused"; - $hash->{NOTIFYDEV} = "global"; - Matrix_Startproc($hash) if($init_done); - return ; -} + $hash->{parseParams} = 1; -sub Matrix_Undef { - my ($hash, $arg) = @_; - my $name = $hash->{NAME}; - # undef $data - $data{MATRIX}{"$name"} = undef; - return ; -} - -sub Matrix_Startproc { - my ($hash) = @_; - my $name = $hash->{NAME}; - Log3 $name, 1, "$name: Matrix_Startproc V".$hash->{ModuleVersion}." -> V".$Module_Version; - # Update necessary? - $hash->{ModuleVersion} = $Module_Version; -} - -########################## -sub Matrix_Notify($$) -{ - my ($hash, $dev) = @_; - my $name = $hash->{NAME}; - my $devName = $dev->{NAME}; - return "" if(IsDisabled($name)); - Log3 $name, 1, "$name : X_Notify $devName"; - my $events = deviceEvents($dev,1); - return if( !$events ); - - if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) - { - Matrix_Startproc($hash); - } - - foreach my $event (@{$events}) { - $event = "" if(!defined($event)); - ### Writing log entry - Log3 $name, 1, "$name : X_Notify $devName - $event"; - # Examples: - # $event = "readingname: value" - # or - # $event = "INITIALIZED" (for $devName equal "global") - # - # processing $event with further code - } - return undef; -} - -############################################################################################# -# called when the device gets renamed, copy from telegramBot -# in this case we then also need to rename the key in the token store and ensure it is recoded with new name -sub Matrix_Rename($$) { - my ($new,$old) = @_; - $data{MATRIX}{"$new"} = $data{MATRIX}{"$old"}; - $data{MATRIX}{"$old"} = undef; - - my $nhash = $defs{$new}; - - #my $token = Matrix_readToken( $nhash, $old ); - #Matrix_storeToken( $nhash, $token ); - - # remove old token with old name - my $index_old = "Matrix_" . $old . "_token"; - #setKeyValue($index_old, undef); -} - -sub Matrix_Get { - my ( $hash, $name, $opt, @args ) = @_; - - return "\"get $name\" needs at least one argument" unless(defined($opt)); - - my $value = shift @args; - - if ($opt eq "wellknown") { - return Matrix_PerformHttpRequest($hash, $opt, ''); - } - elsif ($opt eq "sync") { - $data{MATRIX}{"$name"}{"FAILS"} = 0; - return Matrix_PerformHttpRequest($hash, $opt, ''); - } - elsif ($opt eq "filter") { - return "\"get Matrix $opt\" needs at least two arguments" if (int(@args) < 1); - return Matrix_PerformHttpRequest($hash, $opt, $value); - } - return "Unknown argument $opt, choose one of filter sync wellknown"; -} - -sub Matrix_Set { - my ($hash, @param) = @_; - - #return '"set Matrix needs at least two arguments' if (int(@param) < 3); - - my $name = shift @param; - my $opt = shift @param; - my $value = join("", @param); - - if ($opt eq "msg") { - return Matrix_PerformHttpRequest($hash, $opt, $value); - } - elsif ($opt eq "poll" || $opt eq "poll.fullstate") { - readingsSingleUpdate($hash, $opt, $value, 1); # Readings erzeugen - } - elsif ($opt eq "filter") { - return Matrix_PerformHttpRequest($hash, $opt, ''); - } - elsif ($opt eq "question.start") { - return Matrix_PerformHttpRequest($hash, $opt, $value); - } - elsif ($opt eq "question.end") { - return Matrix_PerformHttpRequest($hash, $opt, $value); - } - elsif ($opt eq "register") { - return Matrix_PerformHttpRequest($hash, $opt, ''); # 2 steps (ToDo: 3 steps empty -> dummy -> registration_token o.a.) - } - elsif ($opt eq "login") { - return Matrix_PerformHttpRequest($hash, $opt, ''); - } - elsif ($opt eq "refresh") { - return Matrix_PerformHttpRequest($hash, $opt, ''); - } - else { - return "Unknown argument $opt, choose one of filter:noArg question.start question.end poll:0,1 poll.fullstate:0,1 msg register login:noArg refresh:noArg"; - } - - #return "$opt set to $value. Try to get it."; -} - - -sub Matrix_Attr { - my ($cmd,$name,$attr_name,$attr_value) = @_; - if($cmd eq "set") { - if($attr_name eq "xxMatrixRoom") { - $attr_value =~ tr/: /~:/; - addToDevAttrList("mt", "MatrixMessage:".$attr_value); - } elsif($attr_name eq "xxMatrixMessage") { - @_[3] =~ tr/~/:/; - } else { - return ; - } - } - return ; + return FHEM::Meta::InitMod( __FILE__, $hash ); } 1; diff --git a/FHEM/lib/Device/Matrix.pm b/FHEM/lib/Device/Matrix.pm new file mode 100644 index 0000000..ffe56b4 --- /dev/null +++ b/FHEM/lib/Device/Matrix.pm @@ -0,0 +1,437 @@ +my $Module_Version = '0.0.7'; + +sub Matrix_PerformHttpRequest($$$) +{ + my ($hash, $def, $value) = @_; + my $now = gettimeofday(); + my $name = $hash->{NAME}; + my $param = { + timeout => 10, + hash => $hash, # Muss gesetzt werden, damit die Callback funktion wieder $hash hat + def => $def, # sichern für eventuelle Wiederholung + value => $value, # sichern für eventuelle Wiederholung + method => "POST", # standard, sonst überschreiben + header => "User-Agent: HttpUtils/2.2.3\r\nAccept: application/json", # Den Header gemäß abzufragender Daten setzen + callback => \&Matrix_ParseHttpResponse # Diese Funktion soll das Ergebnis dieser HTTP Anfrage bearbeiten + }; + $data{MATRIX}{"$name"}{"busy"} = $data{MATRIX}{"$name"}{"busy"} + 1; # queue is busy until response is received + $data{MATRIX}{"$name"}{'LASTSEND'} = $now; # remember when last sent + if ($def eq "sync" && $data{MATRIX}{"$name"}{"next_refresh"} < $now){ + $def = "refresh"; + $data{MATRIX}{"$name"}{"next_refresh"} = $now + 300; + } + + my $device_id = ReadingsVal($name, 'device_id', undef) ? ', "device_id":"'.ReadingsVal($name, 'device_id', undef).'"' : ""; + if ($def eq "register"){ + $param->{'url'} = $hash->{server}."/_matrix/client/v3/register"; + $param->{'data'} = '{"type":"m.login.password", "identifier":{ "type":"m.id.user", "user":"'.$hash->{user}.'" }, "password":"'.$hash->{password}.'"}'; + } + if ($def eq"reg2"){ + $param->{'url'} = $hash->{server}."/_matrix/client/v3/register"; + $param->{'data'} = '{"username":"'.$hash->{user}.'", "password":"'.$hash->{password}.'", "auth": {"session":"'.$data{MATRIX}{"$name"}{"session"}.'","type":"m.login.dummy"}}'; + } + if ($def eq "login"){ + $param->{'url'} = $hash->{server}."/_matrix/client/v3/login"; + $param->{'data'} = '{"type":"m.login.password", "refresh_token": true, "identifier":{ "type":"m.id.user", "user":"'.$hash->{user}.'" }, "password":"'.$hash->{password}.'"' + .$device_id.'}'; + } + if ($def eq "refresh"){ + $param->{'url'} = $hash->{server}.'/_matrix/client/v1/refresh'; + $param->{'data'} = '{"refresh_token": "'.$data{MATRIX}{"$name"}{"refresh_token"}.'"}'; + } + if ($def eq "wellknown"){ + $param->{'url'} = $hash->{server}."/.well-known/matrix/client"; + } + if ($def eq "msg"){ + $param->{'url'} = $hash->{server}.'/_matrix/client/r0/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.room.message?access_token='.$data{MATRIX}{"$name"}{"access_token"}; + $param->{'data'} = '{"msgtype":"m.text", "body":"'.$value.'"}'; + } + if ($def eq "question.start"){ + $value = AttrVal($name, "MatrixQuestion_$value",$value); # if ($value =~ /[0-9]/); + my @question = split(':',$value); + my $size = @question; + $value =~ tr/:/
/; + # min. question and one answer + if (int(@question) >= 2){ + $param->{'url'} = $hash->{server}.'/_matrix/client/v3/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.poll.start?access_token='.$data{MATRIX}{"$name"}{"access_token"}; + $param->{'data'} = '{"org.matrix.msc3381.poll.start": {"max_selections": 1,'. + '"question": {"org.matrix.msc1767.text": "'.$question[0].'"},'. + '"kind": "org.matrix.msc3381.poll.undisclosed",'. + '"answers": [{"id": "'.$question[1].'", "org.matrix.msc1767.text": "'.$question[1].'"},{"id":"'.$question[2].'","org.matrix.msc1767.text": "'.$question[2].'"}],'. + '"org.matrix.msc1767.text": "'.$value.'"}}'; + } else { + Log3 $name, 3, "question.start: $value $size $question[0]"; + return; + } + } + if ($def eq "question.end"){ + $value = ReadingsVal($name, "question_id", "") if (!$value); + $param->{'url'} = $hash->{server}.'/_matrix/client/v3/rooms/'.AttrVal($name, 'MatrixMessage', '!!').'/send/m.poll.end?access_token='.$data{MATRIX}{"$name"}{"access_token"}; + # ""'.ReadingsVal($name, 'questionEventId', '!!').' + $param->{'data'} = '{"m.relates_to": {"rel_type": "m.reference","event_id": "'.$value.'"},"org.matrix.msc3381.poll.end": {},'. + '"org.matrix.msc1767.text": "Antort '.ReadingsVal($name, "answer", "").' erhalten von '.ReadingsVal($name, "sender", "").'"}'; + } + if ($def eq "sync"){ + my $since = ReadingsVal($name, "since", undef) ? '&since='.ReadingsVal($name, "since", undef) : ""; + my $full_state = ReadingsVal($name, "poll.fullstate",undef); + if ($full_state){ + $full_state = "&full_state=true"; + readingsSingleUpdate($hash, "poll.fullstate", 0, 1); + } else { + $full_state = ""; + } + $param->{'url'} = $hash->{server}.'/_matrix/client/r0/sync?access_token='.$data{MATRIX}{"$name"}{"access_token"}.$since.$full_state.'&timeout=50000&filter='.ReadingsVal($name, 'filter_id',0); + $param->{'method'} = 'GET'; + $param->{'timeout'} = 60; + } + if ($def eq "filter"){ + if ($value){ # get + $param->{'url'} = $hash->{server}.'/_matrix/client/v3/user/'.ReadingsVal($name, "user_id",0).'/filter/'.$value.'?access_token='.$data{MATRIX}{"$name"}{"access_token"}; + $param->{'method'} = 'GET'; + } else { + $param->{'url'} = $hash->{server}.'/_matrix/client/v3/user/'.ReadingsVal($name, "user_id",0).'/filter?access_token='.$data{MATRIX}{"$name"}{"access_token"}; + $param->{'data'} = '{'; + $param->{'data'} .= '"event_fields": ["type","content","sender"],'; + $param->{'data'} .= '"event_format": "client", '; + $param->{'data'} .= '"presence": { "senders": [ "@xx:example.com"]}'; # no presence + #$param->{'data'} .= '"room": { "ephemeral": {"rooms": ["'.AttrVal($name, 'MatrixRoom', '!!').'"],"types": ["m.receipt"]}, "state": {"types": ["m.room.*"]},"timeline": {"types": ["m.room.message"] } }'; + $param->{'data'} .= '}'; + } + } + + my $test = "$param->{url}, " + . ( $param->{data} ? "\r\ndata: $param->{data}, " : "" ) + . ( $param->{header} ? "\r\nheader: $param->{header}" : "" ); + readingsSingleUpdate($hash, "fullRequest", $test, 1); # Readings erzeugen + $test = "$name: Matrix sends with timeout $param->{timeout} to ".$test; + Log3 $name, 3, $test; + + HttpUtils_NonblockingGet($param); # Starten der HTTP Abfrage. Es gibt keinen Return-Code. + return undef; +} + +sub Matrix_ParseHttpResponse($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $def = $param->{def}; + my $value = $param->{value}; + my $name = $hash->{NAME}; + my $now = gettimeofday(); + my $nextRequest = ""; + + readingsBeginUpdate($hash); + ###readingsBulkUpdate($hash, "httpHeader", $param->{httpheader}); + readingsBulkUpdate($hash, "httpStatus", $param->{code}); + $hash->{STATE} = $def.' - '.$param->{code}; + if($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3 $name, 3, "error while requesting ".$param->{url}." - $err"; # Eintrag fürs Log + readingsBulkUpdate($hash, "fullResponse", "ERROR ".$err); # Readings erzeugen + $data{MATRIX}{"$name"}{"FAILS"} = 3; + } + elsif($data ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) + Log3 $name, 3, $def." returned: $data"; # Eintrag fürs Log + my $decoded = eval { decode_json($data) }; + Log3 $name, 2, "$name: json error: $@ in data" if( $@ ); + if ($param->{code} == 200){ + $data{MATRIX}{"$name"}{"FAILS"} = 0; + } else { + $data{MATRIX}{"$name"}{"FAILS"}++; + } + + # default next request + $nextRequest = "sync" ; + # An dieser Stelle die Antwort parsen / verarbeiten mit $data + + # "errcode":"M_UNKNOWN_TOKEN: login or refresh + readingsBulkUpdate($hash, "fullResponse", $data); + my $errcode = $decoded->{'errcode'} ? $decoded->{'errcode'} : ""; + if ($errcode eq "M_UNKNOWN_TOKEN"){ + $data{MATRIX}{"$name"}{"repeat"} = $param if ($def ne "sync"); + if ($decoded->{'error'} eq "Access token has expired"){ + if ($decoded->{'soft_logout'} eq "true"){ + $nextRequest = 'refresh'; + }else{ + $nextRequest = 'login'; + } + } elsif ($decoded->{'error'} eq "refresh token does not exist"){ + $nextRequest = 'login'; + } + } + + if ($def eq "register"){ + $data{MATRIX}{"$name"}{"session"} = $decoded->{'session'}; + $nextRequest = "reg2"; + } + if ($param->{code} == 200 && ($def eq "reg2" || $def eq "login" || $def eq "refresh")){ + readingsBulkUpdate($hash, "user_id", $decoded->{'user_id'}) if ($decoded->{'user_id'}); + readingsBulkUpdate($hash, "home_server", $decoded->{'home_server'}) if ($decoded->{'home_server'}); + readingsBulkUpdate($hash, "device_id", $decoded->{'device_id'}) if ($decoded->{'device_id'}); + readingsBulkUpdate($hash, "last_register", $param->{code}) if $def eq "reg2"; + readingsBulkUpdate($hash, "last_login", $param->{code}) if $def eq "login"; + readingsBulkUpdate($hash, "last_refresh", $param->{code}) if $def eq "refresh"; + + $data{MATRIX}{"$name"}{"expires"} = $decoded->{'expires_in_ms'} if ($decoded->{'expires_in_ms'}); + $data{MATRIX}{"$name"}{"refresh_token"} = $decoded->{'refresh_token'} if ($decoded->{'refresh_token'}); + $data{MATRIX}{"$name"}{"access_token"} = $decoded->{'access_token'} if ($decoded->{'access_token'}); + $data{MATRIX}{"$name"}{"next_refresh"} = $now + $data{MATRIX}{"$name"}{"expires"}/1000 - 60; # refresh one minute before end + } + if ($def eq "wellknown"){ + # https://spec.matrix.org/unstable/client-server-api/ + } + if ($param->{code} == 200 && $def eq "sync"){ + readingsBulkUpdate($hash, "since", $decoded->{'next_batch'}) if ($decoded->{'next_batch'}); + # roomlist + my $list = $decoded->{'rooms'}->{'join'}; + #my @roomlist = (); + my $pos = 0; + foreach my $id ( keys $list->%* ) { + if (ref $list->{$id} eq ref {}) { + my $member = ""; + #my $room = $list->{$id}; + $pos = $pos + 1; + # MatrixRoom ? + readingsBulkUpdate($hash, "room$pos.id", $id); + #foreach my $id ( $decoded->{'rooms'}->{'join'}->{AttrVal($name, 'MatrixRoom', '!!')}->{'timeline'}->{'events'}->@* ) { + foreach my $ev ( $list->{$id}->{'state'}->{'events'}->@* ) { + readingsBulkUpdate($hash, "room$pos.topic", $ev->{'content'}->{'topic'}) if ($ev->{'type'} eq 'm.room.topic'); + readingsBulkUpdate($hash, "room$pos.name", $ev->{'content'}->{'name'}) if ($ev->{'type'} eq 'm.room.name'); + $member .= "$ev->{'sender'} " if ($ev->{'type'} eq 'm.room.member'); + } + readingsBulkUpdate($hash, "room$pos.member", $member); + foreach my $tl ( $list->{$id}->{'timeline'}->{'events'}->@* ) { + readingsBulkUpdate($hash, "room$pos.topic", $tl->{'content'}->{'topic'}) if ($tl->{'type'} eq 'm.room.topic'); + readingsBulkUpdate($hash, "room$pos.name", $tl->{'content'}->{'name'}) if ($tl->{'type'} eq 'm.room.name'); + if ($tl->{'type'} eq 'm.room.message' && $tl->{'content'}->{'msgtype'} eq 'm.text'){ + my $sender = $tl->{'sender'}; + if (AttrVal($name, 'MatrixSender', '') =~ $sender){ + readingsBulkUpdate($hash, "message", $tl->{'content'}->{'body'}); + readingsBulkUpdate($hash, "sender", $sender); + # command + + } + #else { + # readingsBulkUpdate($hash, "message", 'ignoriert, nicht '.AttrVal($name, 'MatrixSender', '')); + # readingsBulkUpdate($hash, "sender", $sender); + #} + } elsif ($tl->{'type'} eq "org.matrix.msc3381.poll.response"){ + my $sender = $tl->{'sender'}; + if (AttrVal($name, 'MatrixSender', '') =~ $sender){ + readingsBulkUpdate($hash, "answer", $tl->{'content'}->{'org.matrix.msc3381.poll.response'}->{'answers'}[0]); + readingsBulkUpdate($hash, "sender", $sender); + # poll.end and + $nextRequest = "question.end" ; + # command + + } + } + } + #push(@roomlist,"$id: "; + } + } + } + if ($def eq "filter"){ + readingsBulkUpdate($hash, "filter_id", $decoded->{'filter_id'}) if ($decoded->{'filter_id'}); + } + if ($def eq "msg" ){ + readingsBulkUpdate($hash, "event_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); + #m.relates_to + } + if ($def eq "question.start"){ + readingsBulkUpdate($hash, "question_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); + #m.relates_to + } + if ($def eq "question.end"){ + readingsBulkUpdate($hash, "event_id", $decoded->{'event_id'}) if ($decoded->{'event_id'}); + readingsBulkUpdate($hash, "question_id", "") if ($decoded->{'event_id'}); + #m.relates_to + } + } + readingsEndUpdate($hash, 1); + $data{MATRIX}{"$name"}{"busy"} = $data{MATRIX}{"$name"}{"busy"} - 1; # queue is busy until response is received + $data{MATRIX}{"$name"}{"sync"} = 0 if ($def eq "sync"); # possible next sync + $nextRequest = "" if ($nextRequest eq "sync" && $data{MATRIX}{"$name"}{"sync"} == 1); # only one sync at a time! + + #if ($def eq "sync" && $nextRequest eq "sync" && ReadingsVal($name,'poll',0) == 1 && $data{MATRIX}{"$name"}{"FAILS"} < 3){ + # Matrix_PerformHttpRequest($hash, $nextRequest, ''); + #} els + if ($nextRequest ne "" && ReadingsVal($name,'poll',0) == 1 && $data{MATRIX}{"$name"}{"FAILS"} < 3) { + if ($nextRequest eq "sync" && $data{MATRIX}{"$name"}{"repeat"}){ + $def = $data{MATRIX}{"$name"}{"repeat"}->{"def"}; + $value = $data{MATRIX}{"$name"}{"repeat"}->{"value"}; + $data{MATRIX}{"$name"}{"repeat"} = undef; + Matrix_PerformHttpRequest($hash, $def, $value); + } else { + Matrix_PerformHttpRequest($hash, $nextRequest, ''); + } + } + # Damit ist die Abfrage zuende. +} + +sub Matrix_Define { + my ($hash, $def) = @_; + my @param = split('[ \t]+', $def); + + if(int(@param) < 4) { + return "too few parameters: define Matrix "; + } + + $hash->{name} = $param[0]; + $hash->{server} = $param[2]; + $hash->{user} = $param[3]; + $hash->{password} = $param[4]; + + my $name = $param[0]; + #$data{MATRIX}{"$name"}{"FAILS"} = 0; + #$data{MATRIX}{"$name"}{"busy"} = 0; # queue is busy until response is received + #$data{MATRIX}{"$name"}{'LASTSEND'} = 0; # remember when last sent + #$data{MATRIX}{"$name"}{"expires"} = 0; + #$data{MATRIX}{"$name"}{"refresh_token"} = ""; + #$data{MATRIX}{"$name"}{"access_token"} = ""; + #$data{MATRIX}{"$name"}{"session"} = ""; # used for register + #$hash->{STATE} = "paused"; + $hash->{NOTIFYDEV} = "global"; + Matrix_Startproc($hash) if($init_done); + return ; +} + +sub Matrix_Undef { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + # undef $data + $data{MATRIX}{"$name"} = undef; + return ; +} + +sub Matrix_Startproc { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 $name, 1, "$name: Matrix_Startproc V".$hash->{ModuleVersion}." -> V".$Module_Version; + # Update necessary? + $hash->{ModuleVersion} = $Module_Version; +} + +########################## +sub Matrix_Notify($$) +{ + my ($hash, $dev) = @_; + my $name = $hash->{NAME}; + my $devName = $dev->{NAME}; + return "" if(IsDisabled($name)); + Log3 $name, 1, "$name : X_Notify $devName"; + my $events = deviceEvents($dev,1); + return if( !$events ); + + if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) + { + Matrix_Startproc($hash); + } + + foreach my $event (@{$events}) { + $event = "" if(!defined($event)); + ### Writing log entry + Log3 $name, 1, "$name : X_Notify $devName - $event"; + # Examples: + # $event = "readingname: value" + # or + # $event = "INITIALIZED" (for $devName equal "global") + # + # processing $event with further code + } + return undef; +} + +############################################################################################# +# called when the device gets renamed, copy from telegramBot +# in this case we then also need to rename the key in the token store and ensure it is recoded with new name +sub Matrix_Rename($$) { + my ($new,$old) = @_; + $data{MATRIX}{"$new"} = $data{MATRIX}{"$old"}; + $data{MATRIX}{"$old"} = undef; + + my $nhash = $defs{$new}; + + #my $token = Matrix_readToken( $nhash, $old ); + #Matrix_storeToken( $nhash, $token ); + + # remove old token with old name + my $index_old = "Matrix_" . $old . "_token"; + #setKeyValue($index_old, undef); +} + +sub Matrix_Get { + my ( $hash, $name, $opt, @args ) = @_; + + return "\"get $name\" needs at least one argument" unless(defined($opt)); + + my $value = shift @args; + + if ($opt eq "wellknown") { + return Matrix_PerformHttpRequest($hash, $opt, ''); + } + elsif ($opt eq "sync") { + $data{MATRIX}{"$name"}{"FAILS"} = 0; + return Matrix_PerformHttpRequest($hash, $opt, ''); + } + elsif ($opt eq "filter") { + return "\"get Matrix $opt\" needs at least two arguments" if (int(@args) < 1); + return Matrix_PerformHttpRequest($hash, $opt, $value); + } + return "Unknown argument $opt, choose one of filter sync wellknown"; +} + +sub Matrix_Set { + my ($hash, @param) = @_; + + #return '"set Matrix needs at least two arguments' if (int(@param) < 3); + + my $name = shift @param; + my $opt = shift @param; + my $value = join("", @param); + + if ($opt eq "msg") { + return Matrix_PerformHttpRequest($hash, $opt, $value); + } + elsif ($opt eq "poll" || $opt eq "poll.fullstate") { + readingsSingleUpdate($hash, $opt, $value, 1); # Readings erzeugen + } + elsif ($opt eq "filter") { + return Matrix_PerformHttpRequest($hash, $opt, ''); + } + elsif ($opt eq "question.start") { + return Matrix_PerformHttpRequest($hash, $opt, $value); + } + elsif ($opt eq "question.end") { + return Matrix_PerformHttpRequest($hash, $opt, $value); + } + elsif ($opt eq "register") { + return Matrix_PerformHttpRequest($hash, $opt, ''); # 2 steps (ToDo: 3 steps empty -> dummy -> registration_token o.a.) + } + elsif ($opt eq "login") { + return Matrix_PerformHttpRequest($hash, $opt, ''); + } + elsif ($opt eq "refresh") { + return Matrix_PerformHttpRequest($hash, $opt, ''); + } + else { + return "Unknown argument $opt, choose one of filter:noArg question.start question.end poll:0,1 poll.fullstate:0,1 msg register login:noArg refresh:noArg"; + } + + #return "$opt set to $value. Try to get it."; +} + + +sub Matrix_Attr { + my ($cmd,$name,$attr_name,$attr_value) = @_; + if($cmd eq "set") { + if($attr_name eq "xxMatrixRoom") { + $attr_value =~ tr/: /~:/; + addToDevAttrList("mt", "MatrixMessage:".$attr_value); + } elsif($attr_name eq "xxMatrixMessage") { + @_[3] =~ tr/~/:/; + } else { + return ; + } + } + return ; +} diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 0000000..7c0320b --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,39 @@ +parseParams#!/usr/bin/perl -w + +use File::Basename; +use POSIX qw(strftime); +use strict; + +my @filenames = ( 'FHEM/98_Matrix.pm', + 'lib/FHEM/Devices/Matrix.pm' + ); + +my $controlsfile = 'controls_Matrix.txt'; + +open(FH, ">$controlsfile") || return("Can't open $controlsfile: $!"); + +for my $filename (@filenames) { + my @statOutput = stat($filename); + + if (scalar @statOutput != 13) { + printf 'error: stat has unexpected return value for ' . $filename . "\n"; + next; + } + + my $mtime = $statOutput[9]; + my $date = POSIX::strftime("%Y-%m-%d", localtime($mtime)); + my $time = POSIX::strftime("%H:%M:%S", localtime($mtime)); + my $filetime = $date."_".$time; + + my $filesize = $statOutput[7]; + + printf FH 'UPD ' . $filetime . ' ' . $filesize . ' ' .$filename . "\n"; +} + +close(FH); + +system("git add $controlsfile"); + +print 'Create controls File succesfully' . "\n"; + +exit 0;