You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
12 KiB
PowerShell
457 lines
12 KiB
PowerShell
#!/usr/bin/pwsh
|
|
|
|
#alternatively read params from $env
|
|
#for more flexible Dockerfile support
|
|
param (
|
|
[ValidateNotNullOrEmpty()]
|
|
[uri]
|
|
$HomeServer = $env:HOMESERVER,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]
|
|
$UserId = $env:USERID,
|
|
|
|
[ValidateNotNullOrEmpty()]
|
|
[securestring]
|
|
$AccessToken = ((Get-Content -Path $env:ACCESSTOKEN_FILE -Raw).Trim() `
|
|
| ConvertTo-SecureString -AsPlainText -Force),
|
|
|
|
[switch]
|
|
$Unencrypted = $false #is alternatively set with $env:UNENCRYPTED
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
|
if($env:UNENCRYPTED -eq 'TRUE') {
|
|
$Unencrypted = $true
|
|
}
|
|
|
|
#$join_time contains rooms we joined during runtime
|
|
#it is used to ignore events sent before we joined
|
|
$join_time = @{}
|
|
|
|
|
|
#used in Invoke-RestMethod
|
|
$authentication_headers = @{
|
|
Authentication = 'Bearer'
|
|
Token = $AccessToken
|
|
ContentType = 'application/json'
|
|
AllowUnencryptedAuthentication = $Unencrypted
|
|
}
|
|
|
|
$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
|
|
|
|
$http_splat = @{
|
|
Uri = $uri
|
|
Method = 'Put'
|
|
Body = $Event
|
|
}
|
|
|
|
$response = Invoke-RestMethod @authentication_headers @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 ConvertFrom-MatrixTimestamp {
|
|
param (
|
|
[Parameter(Mandatory)]
|
|
[Int64]
|
|
$OriginServerTs
|
|
)
|
|
|
|
#$OriginServerTs is milliseconds since 1970-01-01 (epoch time in milliseconds)
|
|
return Get-Date -Date ((Get-Date -Date '1970-01-01') + [timespan]::FromMilliseconds($OriginServerTs))
|
|
}
|
|
function Measure-TimeDifference {
|
|
param (
|
|
[Parameter(Mandatory=$true)]
|
|
[datetime]
|
|
$OriginTime,
|
|
|
|
[datetime]
|
|
$ReferenceTime = (Get-Date).ToUniversalTime()
|
|
)
|
|
|
|
#return the difference
|
|
Write-Output ($ReferenceTime - $OriginTime)
|
|
}
|
|
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 = '"{0}" ' -f $Ball
|
|
}
|
|
|
|
$body = '{0}: Pong! (ping {1}took {2} to arrive)' -f $SenderMxId,$padded_ball,$ReadableTimespan
|
|
$formatted_body = '<a href=''https://matrix.to/#/{0}''>{0}</a>: Pong! ' -f $SenderMxId
|
|
$formatted_body += '(<a href="https://matrix.to/#/{0}/{1}">ping</a> {2}took {3} to arrive)' -f $RoomId,$PingEventId,$padded_ball,$ReadableTimespan
|
|
|
|
return @{
|
|
Body = $body
|
|
FormattedBody = $formatted_body
|
|
}
|
|
}
|
|
function Open-JoinEvent {
|
|
param (
|
|
[Parameter(Mandatory=$true)]
|
|
$Event,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
$RoomId
|
|
)
|
|
|
|
$origin_time = ConvertFrom-MatrixTimestamp -OriginServerTs $Event.origin_server_ts
|
|
|
|
#check if a $join_time for $RoomId exists and if it newer than our event
|
|
if($join_time.$RoomId) {
|
|
if((Measure-TimeDifference -OriginTime $origin_time -ReferenceTime $join_time.$RoomId) -gt 0) {
|
|
#ignore events sent before we joined the room
|
|
return
|
|
} else {
|
|
#first event with negative difference received
|
|
#probably all future events are after our join time
|
|
$join_time.Remove($RoomId)
|
|
}
|
|
}
|
|
|
|
#the "ball" is a string returned by the bot
|
|
if($Event.content.msgtype -eq 'm.text' -and $Event.content.body -match '^!ping( (?<ball>.*))?') {
|
|
$difference = Measure-TimeDifference -OriginTime $origin_time
|
|
$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 Join-MatrixRoom {
|
|
param (
|
|
[Parameter(Mandatory=$true)]
|
|
$RoomId
|
|
)
|
|
|
|
$uri = '{0}_matrix/client/r0/rooms/{1}/join' -f $HomeServer,$RoomId
|
|
|
|
$http_splat = @{
|
|
Uri = $uri
|
|
Method = 'Post'
|
|
}
|
|
|
|
$response = Invoke-RestMethod @authentication_headers @http_splat
|
|
if($response.room_id) {
|
|
Write-Host ('Joined room {0}' -f $RoomId)
|
|
} else {
|
|
Write-Error ('Error joining room {0}: {1}' -f $RoomId,$response.error)
|
|
}
|
|
}
|
|
function Get-MatrixAccountData {
|
|
param (
|
|
[Parameter(Mandatory=$true)]
|
|
[string]
|
|
$Type
|
|
)
|
|
|
|
$uri = '{0}_matrix/client/r0/user/{1}/account_data/{2}' -f $HomeServer,$UserId,$Type
|
|
|
|
$http_splat = @{
|
|
Uri = $uri
|
|
Method = 'Get'
|
|
}
|
|
|
|
$response = Invoke-RestMethod @authentication_headers @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
|
|
|
|
$http_splat = @{
|
|
Uri = $uri
|
|
Method = 'Put'
|
|
Body = $Content | ConvertTo-Json -Compress
|
|
}
|
|
|
|
Invoke-RestMethod @authentication_headers @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
|
|
}
|
|
|
|
$http_splat = @{
|
|
Uri = $uri
|
|
}
|
|
|
|
$response = Invoke-RestMethod @authentication_headers @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-JoinEvent -Event $event -RoomId $room_id
|
|
}
|
|
}
|
|
|
|
#.PSObject.Properties because the rooms under .invite are [NoteProperty]
|
|
$room_ids = $response.rooms.invite.PSObject.Properties.Name
|
|
foreach($room_id in $room_ids) {
|
|
Join-MatrixRoom -RoomId $room_id
|
|
$join_time.$room_id = (Get-Date).ToUniversalTime()
|
|
}
|
|
|
|
return $response.next_batch
|
|
}
|
|
function Get-MatrixFilterId {
|
|
param (
|
|
[System.Object]
|
|
$Filter
|
|
)
|
|
|
|
$uri = '{0}_matrix/client/r0/user/{1}/filter' -f $HomeServer,$UserId
|
|
|
|
$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 @authentication_headers @http_splat
|
|
|
|
return $response.filter_id
|
|
}
|
|
function Get-FilterId {
|
|
$filter = @{
|
|
event_fields = @(
|
|
#all fields used in Open-JoinEvent
|
|
'event_id'
|
|
'sender'
|
|
'origin_server_ts'
|
|
'content.msgtype'
|
|
'content.body'
|
|
#'content.membership' #invites only
|
|
)
|
|
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 = @{
|
|
types = @(
|
|
#to receive invites
|
|
'm.room.member'
|
|
)
|
|
lazy_load_members = $true
|
|
}
|
|
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 |