# $Id$ package main; use strict; use warnings; use Socket; use IO::Handle; sub yowsup_Initialize($) { my ($hash) = @_; $hash->{ReadFn} = "yowsup_Read"; $hash->{DefFn} = "yowsup_Define"; $hash->{NotifyFn} = "yowsup_Notify"; $hash->{UndefFn} = "yowsup_Undefine"; $hash->{ShutdownFn} = "yowsup_Shutdown"; $hash->{SetFn} = "yowsup_Set"; #$hash->{GetFn} = "yowsup_Get"; $hash->{AttrFn} = "yowsup_Attr"; $hash->{AttrList} = "disable:1 "; $hash->{AttrList} .= "cmd home nickname ". $readingFnAttributes; } ##################################### sub yowsup_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "Usage: define yowsup" if(@a < 2); my $name = $a[0]; my $number = $a[2]; if( !defined($number) ) { my $d = $modules{yowsup}{defptr}{yowsup}; return "yowsup MASTER already defined as $d->{NAME}." if( defined($d) && $d->{NAME} ne $name ); $modules{yowsup}{defptr}{yowsup} = $hash; addToDevAttrList( $name, "acceptFrom" ); } else { return "no yowsup MASTER defined." if( !defined($modules{yowsup}{defptr}{yowsup}) ); my $d = $modules{yowsup}{defptr}{$number}; return "yowsup $number already defined as $d->{NAME}." if( defined($d) && $d->{NAME} ne $name ); $modules{yowsup}{defptr}{$number} = $hash; addToDevAttrList( $name, "commandPrefix" ); addToDevAttrList( $name, "allowedCommands" ); addToDevAttrList( $name, "acceptFrom" ) if( $number =~ m/\./ ); $hash->{NUMBER} = $number; } $hash->{NAME} = $name; $hash->{NOTIFYDEV} = "global"; if( $init_done ) { yowsup_Disconnect($hash); yowsup_Connect($hash); } elsif( $hash->{STATE} ne "???" ) { $hash->{STATE} = "Initialized"; } return undef; } sub yowsup_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); yowsup_Disconnect($hash); yowsup_Connect($hash); } sub yowsup_reConnect($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "$name: reConnect"; yowsup_Disconnect($hash); yowsup_Connect($hash); } sub yowsup_Connect($) { my ($hash) = @_; my $name = $hash->{NAME}; return undef if( $hash->{NUMBER} ); return undef if( AttrVal($name, "disable", 0 ) == 1 ); $hash->{PARTIAL} = ""; my ($yowsup_child, $parent); if( socketpair($yowsup_child, $parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) ) { $yowsup_child->autoflush(1); $parent->autoflush(1); my $pid = fhemFork(); if(!defined($pid)) { close $parent; close $yowsup_child; my $msg = "$name: Cannot fork: $!"; Log 1, $msg; return $msg; } if( $pid ) { close $parent; $hash->{STATE} = "Connected"; $hash->{CONNECTS}++; $hash->{FH} = $yowsup_child; $hash->{FD} = fileno($yowsup_child); $hash->{PID} = $pid; $hash->{WAITING_FOR_LOGIN} = 1; $selectlist{$name} = $hash; } else { close $yowsup_child; close STDIN; close STDOUT; my $fn = $parent->fileno(); open(STDIN, "<&$fn") or die "can't redirect STDIN $!"; open(STDOUT, ">&$fn") or die "can't redirect STDOUT $!"; #select STDIN; $| = 1; #select STDOUT; $| = 1; #STDIN->autoflush(1); STDOUT->autoflush(1); close $parent; $ENV{PYTHONUNBUFFERED} = 1; if( my $home = AttrVal($name, "home", undef ) ) { $home = $ENV{'PWD'} if( $home eq 'PWD' ); $ENV{'HOME'} = $home; Log3 $name, 2, "$name: setting \$HOME to $home"; } my $cmd = AttrVal($name, "cmd", "/opt/local/bin/yowsup-cli demos -c /root/config.yowsup --yowsup" ); Log3 $name, 2, "$name: starting yoswup-cli: $cmd"; exec split( ' ', $cmd ) or Log3 $name, 1, "exec failed"; Log3 $name, 1, "set the cmd attribut to: /yowsup-cli demos -c /config.yowsup --yowsup"; POSIX::_exit(0);; } } else { #$hash->{STATE} = "Connected"; Log3 $name, 3, "$name: socketpair failed"; InternalTimer(gettimeofday()+20, "yowsup_Connect", $hash, 0); } } sub yowsup_Disconnect($) { my ($hash) = @_; my $name = $hash->{NAME}; return undef if( $hash->{NUMBER} ); RemoveInternalTimer($hash); return if( !$hash->{FD} ); if( $hash->{PID} ) { yowsup_Write($hash, '/disconnect' ); kill( 9, $hash->{PID} ); waitpid($hash->{PID}, 0); delete $hash->{PID}; } close($hash->{FH}) if($hash->{FH}); delete($hash->{FH}); delete($hash->{FD}); delete($selectlist{$name}); $hash->{STATE} = "Disconnected"; Log3 $name, 3, "$name: Disconnected"; $hash->{LAST_DISCONNECT} = FmtDateTime( gettimeofday() ); } sub yowsup_Undefine($$) { my ($hash, $arg) = @_; yowsup_Disconnect($hash); if( $hash->{NUMBER} ) { delete $modules{yowsup}{defptr}{$hash->{NUMBER}}; } else { delete $modules{yowsup}{defptr}{yowsup}; } return undef; } sub yowsup_Shutdown($) { my ($hash) = @_; yowsup_Disconnect($hash); return undef; } sub yowsup_Set($$@) { my ($hash, $name, $cmd, @args) = @_; my $list = ""; if( $hash->{NUMBER} ) { my $phash = $modules{yowsup}{defptr}{yowsup}; $list .= "image send" if( $phash->{PID} ); if( $cmd eq 'image' ) { return "MASTER not connected" if( !$phash->{PID} ); readingsSingleUpdate( $hash, 'sent', 'image: '. join( ' ', @args ), 1 ); my $number = $hash->{NUMBER}; $number =~ s/\./-/; my $image = shift(@args); return yowsup_Write( $phash, "/image send $number $image '". join( ' ', @args ) ."'" ); return undef; } elsif( $cmd eq 'send' ) { return "MASTER not connected" if( !$phash->{PID} ); readingsSingleUpdate( $hash, 'sent', join( ' ', @args ), 1 ); my $number = $hash->{NUMBER}; $number =~ s/\./-/; return yowsup_Write( $phash, "/message send $number '". join( ' ', @args ) ."'" ); return undef; } } else { $list .= "image send raw disconnect:noArg " if( $hash->{PID} ); $list .= "reconnect:noArg"; if( $cmd eq 'raw' ) { return yowsup_Write( $hash, join( ' ', @args ) ); return undef; } elsif( $cmd eq 'image' ) { readingsSingleUpdate( $hash, 'sent', 'image: '. join( ' ', @args ), 1 ); my $number = shift(@args); $number =~ s/\./-/; my $image = shift(@args); return yowsup_Write( $hash, "/image send $number $image '". join( ' ', @args ) ."'" ); return undef; } elsif( $cmd eq 'send' ) { readingsSingleUpdate( $hash, 'sent', join( ' ', @args ), 1 ); my $number = shift(@args); $number =~ s/\./-/; if( $number =~ m/,/ ) { return yowsup_Write( $hash, "/message broadcast $number '". join( ' ', @args ) ."'" ); } else { return yowsup_Write( $hash, "/message send $number '". join( ' ', @args ) ."'" ); } return undef; } elsif( $cmd eq 'disconnect' ) { yowsup_Disconnect($hash); return undef; } elsif( $cmd eq 'reconnect' ) { yowsup_Disconnect($hash); yowsup_Connect($hash); return undef; } } return "Unknown argument $cmd, choose one of $list"; } sub yowsup_Get($$@) { my ($hash, $name, $cmd) = @_; my $list = "devices:noArg"; if( $cmd eq "devices" ) { return undef; } return "Unknown argument $cmd, choose one of $list"; } sub yowsup_Parse($$) { my ($hash,$data) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "$name: parse: $data"; $hash->{TIME} = TimeNow(); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+60*10, "yowsup_reConnect", $hash, 0); if( $data =~ m/\[offline\]:/ ) { readingsSingleUpdate( $hash, "state", 'offline', 1 ) if( ReadingsVal($name,'state','' ) ne 'offline' ); if( $hash->{WAITING_FOR_LOGIN} ) { yowsup_Write( $hash, '/L' ); yowsup_Write( $hash, '/presence available' ); yowsup_Write( $hash, "/presence name '". AttrVal($name, 'nickname', "") ."'" ) if(defined(AttrVal($name, 'nickname', undef))); #yowsup_Write( $hash, '/ping' ); delete $hash->{WAITING_FOR_LOGIN}; } } elsif( $data =~ m/\[connected\]:/ ) { readingsSingleUpdate( $hash, "state", 'connected', 1 ) if( ReadingsVal($name,'state','' ) ne 'connected' ); } elsif( $data =~ m/Auth: Logged in!/ ) { readingsSingleUpdate( $hash, "state", 'logged in', 1 ) if( ReadingsVal($name,'state','' ) ne 'logged in' ); } if( $data =~ m/^CHATSTATE:.*State: (\S*).*From: ([\d-]*)/s ) { my $chatstate = $1; my $number = $2; $number =~ s/-/\./; if( my $chash = $modules{yowsup}{defptr}{$number} ) { readingsSingleUpdate( $chash, "chatstate", $chatstate, 1 ); } #} elsif( $data =~ m/\[(.*)@.*\((.*)\)\]:\[(.*)\]\s*(.*)/s ) { } elsif( $data =~ m/\[(.*)@.*\((.*)\)\]:\[([^\]]*)\]\s*(.*)(\nMessage)/s || $data =~ m/\[(.*)@.*\((.*)\)\]:\[([^\]]*)\]\s*(.*)/s ) { my $number = $1; my $time = $2; my $id = $3; my $message = $4; my $last_sender; if( $number =~ m/(\d*)\/(\d*)-(\d*)/ ) { $number = "$2.$3"; $last_sender = $1; } $message =~ s/\n$//; $message =~ s/[\b]*$//; my $chash = $modules{yowsup}{defptr}{$number}; if( !$chash ) { my $accept_from = AttrVal($name, "acceptFrom", undef ); if( !$accept_from || ",$accept_from," =~/,$number,/ ) { my $define = "$number yowsup $number"; my $cmdret = CommandDefine(undef,$define); if($cmdret) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for number '$number': $cmdret"; } else { #$cmdret = CommandAttr(undef,"$number alias ".$result->{$id}{name}); $cmdret = CommandAttr(undef,"$number room yowsup"); #$cmdret = CommandAttr(undef,"$number IODev $name"); } $chash = $modules{yowsup}{defptr}{$number}; } } if( $chash ) { readingsBeginUpdate($chash); if( $last_sender ) { readingsBulkUpdate( $chash, "chatstate", "received from: $last_sender" ); } else { readingsBulkUpdate( $chash, "chatstate", "received" ); } readingsBulkUpdate( $chash, "message", $message ); readingsEndUpdate($chash, 1); my $cname = $chash->{NAME}; if( my $prefix = AttrVal($cname, "commandPrefix", undef ) ) { my $cmd; if( $prefix eq '0' ) { } elsif( $prefix eq '1' ) { $cmd = $message; } elsif( $message =~ m/^$prefix(.*)/ ) { $cmd = $1; } if( $cmd ) { my $accept_from = AttrVal($cname, "acceptFrom", undef ); if( !$accept_from || $last_sender || ",$accept_from," =~/,$last_sender,/ ) { Log3 $name, 3, "$cname: received command: $cmd"; $chash->{SNAME} = $cname; my $ret = AnalyzeCommandChain( $chash, $cmd ); Log3 $name, 4, "$cname: command result: $ret"; my $number = $chash->{NUMBER}; $number =~ s/\./-/; yowsup_Write( $hash, "/message send $number '$ret'" ) if( $ret ); } else { Log3 $cname, 3, "$cname: commands: ". $last_sender?$last_sender:$number ." not allowed"; } } } else { Log3 $cname, 3, "$cname: commands not allowed"; } } else { Log3 $name, 3, "$name: sender: $number not allowed"; } } } sub yowsup_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; my $buf; my $ret = sysread($hash->{FH}, $buf, 65536 ); if(!defined($ret) || $ret <= 0) { yowsup_Disconnect( $hash ); Log3 $name, 3, "$name: read: error during sysread: $!" if(!defined($ret)); Log3 $name, 3, "$name: read: end of file reached while sysread" if( $ret <= 0); InternalTimer(gettimeofday()+10, "yowsup_Connect", $hash, 0); return undef; } yowsup_Parse($hash,$buf); return undef; my $data = $hash->{PARTIAL}; Log3 $name, 5, "yowsup/RAW: $data/$buf"; $data .= $buf; $hash->{PARTIAL} = $data; } sub yowsup_Write($$) { my ($hash, $data) = @_; my $name = $hash->{NAME}; return "not connected" if( !$hash->{PID} ); #my $ls = chr(226) . chr(128) . chr(168); #$data =~ s/\n/$ls/g; $data =~ s/\n/\r/g; Log3 $name, 3, "$name: sending $data"; syswrite $hash->{FH}, $data ."\n"; return undef; } sub yowsup_Attr($$$) { my ($cmd, $name, $attrName, @params) = @_; my ($attrVal) = @params; my $orig = $attrVal; if($attrName eq "allowedCommands" && $cmd eq "set") { my $aName = "allowed_$name"; my $exists = ($defs{$aName} ? 1 : 0); AnalyzeCommand(undef, "defmod $aName allowed"); AnalyzeCommand(undef, "attr $aName validFor $name"); AnalyzeCommand(undef, "attr $aName $attrName ".join(" ",@params)); return "$name: ".($exists ? "modifying":"creating"). " device $aName for attribute $attrName"; } elsif( $attrName eq "disable" ) { my $hash = $defs{$name}; yowsup_Disconnect($hash); if( $cmd eq "set" && $attrVal ne "0" ) { $attrVal = 1; } else { $attr{$name}{$attrName} = 0; yowsup_Connect($hash); } } elsif( $attrName eq "cmd" ) { my $hash = $defs{$name}; if( $cmd eq "set" ) { $attr{$name}{$attrName} = $attrVal; } else { delete $attr{$name}{$attrName}; } yowsup_Disconnect($hash); yowsup_Connect($hash); } if( $cmd eq "set" ) { if( !defined($orig) || $orig ne $attrVal ) { $attr{$name}{$attrName} = $attrVal; return $attrName ." set to ". $attrVal; } } return; } 1; =pod =item summary interface to the yowsup librbary (for whatsapp) =item summary_DE Interface zur yowsup Bibliothek (für WhatsApp) =begin html

yowsup

    Module to interface to the yowsup library to send and recive WhatsApp messages.

    Notes:
    • Probably only works on linux/unix systems.

    Define
      define <name> yowsup

      Defines a yowsup device.

      Examples:
        define WhatsApp yowsup

    Set
    • image [<number>] <path> [<text>]
      sends an image with optional text. <number> has to be given if sending via master device.
    • send [<numner>] <text>
      sends <text>. <number> has to be given if sending via master device.

    Attributes
    • cmd
      complette commandline to start the yowsup cli client
      eg: attr WhatsApp cmd /opt/local/bin/yowsup-cli demos -c /root/config.yowsup --yowsup
    • home
      set $HOME for the started yowsup process
      PWD -> set to $PWD
      anything else -> use as $HOME
    • nickname
      nickname that will be send as sender
    • acceptFrom
      comma separated list of contacts (numbers) from which messages will be accepted
    • commandPrefix
      not set -> don't accept commands
      0 -> don't accept commands
      1 -> allow commands, every message is interpreted as a fhem command
      anything else -> if the message starts with this prefix then everything after the prefix is taken as the command
    • allowedCommands
      A comma separated list of commands that are allowed from this contact.
      If set to an empty list , (i.e. comma only) no commands are accepted.
      Note: allowedCommands should work as intended, but no guarantee can be given that there is no way to circumvent it.
=end html =cut