param ( [Parameter(Mandatory=$true)] [string] $HomeServer, [Parameter(Mandatory=$true)] [string] $UserId, [Parameter(Mandatory=$true)] [securestring] $AccessToken ) $account_data_type_prefix = 'de.lubiland.pingposh.' $account_data_types = @{ next_batch = $account_data_type_prefix+'next_batch' } function Send-MatrixEvent { param ( [Parameter(Mandatory=$true)] [string] $RoomId, [Parameter(Mandatory=$true)] [string] $Event, [Parameter(Mandatory=$true)] [string] $EventType ) #txn_id should be unique per client, so we use timestamp+random $txn_id = '{0}{1}' -f (Get-Date -UFormat '%s'),(Get-Random) $uri = '{0}/_matrix/client/r0/rooms/{1}/send/{2}/{3}' -f $HomeServer,$RoomId,$EventType,$txn_id $header_splat = @{ Authentication = 'Bearer' Token = $AccessToken ContentType = 'application/json' } $http_splat = @{ Uri = $uri Method = 'Put' Body = $Event } $response = Invoke-RestMethod @header_splat @http_splat if($response.event_id) { Write-Host ('Event {0} sent to room {1}' -f $response.event_id,$RoomId) } } function Send-Pong { param ( [Parameter(Mandatory=$true)] [string] $RoomId, [Parameter(Mandatory=$true)] [string] $Body, [Parameter(Mandatory=$false)] [string] $FormattedBody, [Parameter(Mandatory=$true)] [string] $OriginHomeServer, [Parameter(Mandatory=$true)] [int] $Duration, [Parameter(Mandatory=$true)] [string] $PingEventId ) $event = @{ msgtype = 'm.notice' body = $Body pong = @{ from = $OriginHomeServer ms = $Duration ping = $PingEventId } } if($FormattedBody) { $event += @{ format = 'org.matrix.custom.html' formatted_body = $FormattedBody } } $event_json = $event | ConvertTo-Json -Compress Send-MatrixEvent -RoomId $RoomId -Event $event_json -EventType 'm.room.message' } function Compare-Timestamps { param ( [Parameter(Mandatory=$true)] [Int64] $OriginServerTs ) $current_time = (Get-Date).ToUniversalTime() #$OriginServerTs is milliseconds since 1970-01-01 (epoch time in milliseconds) $original_time = Get-Date -Date ((Get-Date -Date '1970-01-01') + [timespan]::FromMilliseconds($OriginServerTs)) #return the difference Write-Output ($current_time - $original_time) } function ConvertTo-HumanReadableTimespan { param ( [parameter(Mandatory=$true)] [timespan] $TimeSpan ) switch($TimeSpan) { Default { $output = '{0} days' -f [System.Math]::Round($TimeSpan.TotalDays, 2) } {$PSItem.TotalHours -lt 24} { $output = '{0} h' -f [System.Math]::Round($TimeSpan.TotalHours, 2) } {$PSItem.TotalMinutes -lt 120} { $output = '{0} min' -f [System.Math]::Round($TimeSpan.TotalMinutes, 2) } {$PSItem.TotalSeconds -lt 120} { $output = '{0} s' -f [System.Math]::Round($TimeSpan.TotalSeconds, 2) } {$PSItem.TotalMilliseconds -lt 10000} { $output = '{0} ms' -f [System.Math]::Round($TimeSpan.TotalMilliseconds, 0) } } Write-Output $output } function Join-Pong { param ( [Parameter(Mandatory=$true)] [string] $RoomId, [Parameter(Mandatory=$true)] [string] $PingEventId, [Parameter(Mandatory=$true)] [string] $SenderMxId, [Parameter(Mandatory=$true)] [string] $ReadableTimespan, [Parameter(Mandatory=$false)] [string] $Ball ) if($Ball) { $padded_ball = $Ball+' ' } $body = '{0}: Pong! (ping {1}took {2} to arrive)' -f $SenderMxId,$padded_ball,$ReadableTimespan $formatted_body = '{0}: Pong! ' -f $SenderMxId $formatted_body += '(ping {2}took {3} to arrive)' -f $RoomId,$PingEventId,$padded_ball,$ReadableTimespan return @{ Body = $body FormattedBody = $formatted_body } } function Open-Event { param ( [Parameter(Mandatory=$true)] $Event, [Parameter(Mandatory=$true)] $RoomId ) #the "ball" is a string returned by the bot if($Event.content.msgtype -eq 'm.text' -and $Event.content.body -match '!ping( (?.*))?') { $difference = Compare-Timestamps -OriginServerTs $Event.origin_server_ts $readable_timespan = ConvertTo-HumanReadableTimespan -TimeSpan $difference #$bodies contains a hashtable with keys Body and FormattedBody $bodies = Join-Pong -RoomId $RoomId -PingEventId $Event.event_id -SenderMxId $Event.sender -ReadableTimespan $readable_timespan -Ball $Matches.ball $origin_homeserver = $Event.sender.Split(':')[1] Send-Pong -RoomId $RoomId @bodies -OriginHomeServer $origin_homeserver -Duration $difference.TotalMilliseconds -PingEventId $Event.event_id } } function Get-MatrixAccountData { param ( [Parameter(Mandatory=$true)] [string] $Type ) $uri = '{0}/_matrix/client/r0/user/{1}/account_data/{2}' -f $HomeServer,$UserId,$Type $header_splat = @{ Authentication = 'Bearer' Token = $AccessToken ContentType = 'application/json' } $http_splat = @{ Uri = $uri Method = 'Get' } $response = Invoke-RestMethod @header_splat @http_splat return $response } function Set-MatrixAccountData { param ( [Parameter(Mandatory=$true)] [string] $Type, [Parameter(Mandatory=$true)] [System.Object] $Content ) $uri = '{0}/_matrix/client/r0/user/{1}/account_data/{2}' -f $HomeServer,$UserId,$Type $header_splat = @{ Authentication = 'Bearer' Token = $AccessToken ContentType = 'application/json' } $http_splat = @{ Uri = $uri Method = 'Put' Body = $Content | ConvertTo-Json -Compress } Invoke-RestMethod @header_splat @http_splat | Out-Null } function Get-NextBatchToken { $account_data = Get-MatrixAccountData -Type $account_data_types.next_batch return $account_data.next_batch } function Set-NextBatchToken { param ( [Parameter(Mandatory=$true)] [string] $Token ) Set-MatrixAccountData -Type $account_data_types.next_batch -Content @{next_batch = $Token} } function Invoke-MatrixSync { param ( [string] $Token, [string] $FilterId, [int] $Timeout = 30000 ) $uri = '{0}/_matrix/client/r0/sync?timeout={1}' -f $HomeServer,$Timeout if($Token) { $uri += '&since={0}' -f $Token } if($FilterId) { $uri += '&filter={0}' -f $FilterId } $header_splat = @{ Authentication = 'Bearer' Token = $AccessToken ContentType = 'application/json' } $http_splat = @{ Uri = $uri } $response = Invoke-RestMethod @header_splat @http_splat #.PSObject.Properties because the rooms under .join are [NoteProperty] $room_ids = $response.rooms.join.PSObject.Properties.Name foreach($room_id in $room_ids) { $events = $response.rooms.join.$room_id.timeline.events foreach($event in $events) { Open-Event -Event $event -RoomId $room_id } } return $response.next_batch } function Get-MatrixFilterId { param ( [System.Object] $Filter ) $uri = '{0}/_matrix/client/r0/user/{1}/filter' -f $HomeServer,$UserId $header_splat = @{ Authentication = 'Bearer' Token = $AccessToken ContentType = 'application/json' } $http_splat = @{ Uri = $uri Method = 'Post' #a filter can be up to 3 layers deep according to spec #https://matrix.org/docs/spec/client_server/r0.6.0#filtering Body = $Filter | ConvertTo-Json -Compress -Depth 3 } $response = Invoke-RestMethod @header_splat @http_splat return $response.filter_id } function Get-FilterId { $filter = @{ event_fields = @( #all fields used in Open-Event 'event_id' 'sender' 'origin_server_ts' 'content.msgtype' 'content.body' ) presence = @{ #we don't use presence information types = @('invalid') } account_data = @{ #we only retrieve account_data via the account_data api endpoint types = @('invalid') } room = @{ ephemeral = @{ #we don't use ephemeral events like typing or read receipts types = @('invalid') } state = @{ #we don't use room state types = @('invalid') } timeline = @{ #exclude ourself not_senders = @( $UserId ) types = @( #to receive !ping commands 'm.room.message' ) } account_data = @{ #we don't use room specific account_data types = @('invalid') } } } $filter_id = Get-MatrixFilterId -Filter $filter return $filter_id } function Start-MatrixSync { #get last used next_batch from account_data $token = Get-NextBatchToken #get /sync filter $filter_id = Get-FilterId Try { #try first sync with the old token $token = Invoke-MatrixSync -Token $token -FilterId $filter_id } Catch { #if it fails start a fresh sync reset $token $token = $null } while($true) { #use $token of the previous sync Set-NextBatchToken -Token $token $token = Invoke-MatrixSync -Token $token -FilterId $filter_id } } Start-MatrixSync