Update Ikar server and Android client

This commit is contained in:
Курнат Андрей
2026-05-17 22:22:43 +03:00
commit 22e19cdfdb
258 changed files with 51047 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
param(
[string]$BaseUrl = 'http://localhost:5099',
[string]$AdminUsername = $env:Admin__Username,
[string]$AdminPassword = $env:Admin__Password
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Net.Http
if ([string]::IsNullOrWhiteSpace($AdminUsername) -or [string]::IsNullOrWhiteSpace($AdminPassword)) {
throw 'Admin__Username and Admin__Password must be provided.'
}
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$actorUsername = "adminsmoke$stamp"
$targetUsername = "adminpeer$stamp"
$password = 'Demo123!'
$attachmentName = "admin-attachment-$stamp.txt"
$actorPhone = "+7911$stamp"
$targetPhone = "+7922$stamp"
$actorSession = Invoke-RestMethod `
-Uri "$BaseUrl/api/auth/register" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $actorUsername
displayName = 'Admin Smoke Actor'
password = $password
phoneNumber = $actorPhone
} | ConvertTo-Json)
$targetSession = Invoke-RestMethod `
-Uri "$BaseUrl/api/auth/register" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $targetUsername
displayName = 'Admin Smoke Peer'
password = $password
phoneNumber = $targetPhone
} | ConvertTo-Json)
$actorHeaders = @{
Authorization = "Bearer $($actorSession.accessToken)"
}
$targetUser = Invoke-RestMethod `
-Uri "$BaseUrl/api/users/search?q=$targetUsername" `
-Headers $actorHeaders `
-Method Get | Select-Object -First 1
$chat = Invoke-RestMethod `
-Uri "$BaseUrl/api/chats/direct" `
-Headers $actorHeaders `
-Method Post `
-ContentType 'application/json' `
-Body (@{
userId = $targetUser.id
} | ConvertTo-Json)
$tempFile = Join-Path $env:TEMP $attachmentName
"admin attachment smoke $stamp" | Set-Content -Path $tempFile -NoNewline
$handler = New-Object System.Net.Http.HttpClientHandler
$client = New-Object System.Net.Http.HttpClient($handler)
$client.BaseAddress = [Uri]::new("$BaseUrl/")
$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $actorSession.accessToken)
$form = New-Object System.Net.Http.MultipartFormDataContent
$form.Add((New-Object System.Net.Http.StringContent('admin smoke attachment')), 'text')
$fileStream = [System.IO.File]::OpenRead($tempFile)
$fileContent = New-Object System.Net.Http.StreamContent($fileStream)
$fileContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue('text/plain')
$form.Add($fileContent, 'file', [System.IO.Path]::GetFileName($tempFile))
$uploadResponse = $client.PostAsync("api/chats/$($chat.id)/attachments", $form).GetAwaiter().GetResult()
$uploadPayload = $uploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$fileStream.Dispose()
$form.Dispose()
$client.Dispose()
$handler.Dispose()
if (-not $uploadResponse.IsSuccessStatusCode) {
throw $uploadPayload
}
$uploadedMessage = $uploadPayload | ConvertFrom-Json
$attachment = $uploadedMessage.attachments[0]
$adminSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/session/login" `
-WebSession $adminSession `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $AdminUsername
password = $AdminPassword
} | ConvertTo-Json) | Out-Null
$status = Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/status" `
-WebSession $adminSession `
-Method Get
$usersBeforeDeleteResponse = Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users?q=adminsmoke$stamp" `
-WebSession $adminSession `
-Method Get
[object[]]$usersBeforeDelete = if ($null -eq $usersBeforeDeleteResponse) { @() } else { @($usersBeforeDeleteResponse) }
$chatsBeforeDeleteResponse = Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/chats?q=adminsmoke$stamp" `
-WebSession $adminSession `
-Method Get
[object[]]$chatsBeforeDelete = if ($null -eq $chatsBeforeDeleteResponse) { @() } else { @($chatsBeforeDeleteResponse) }
$attachmentsBeforeDeleteResponse = Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/attachments?q=$attachmentName" `
-WebSession $adminSession `
-Method Get
[object[]]$attachmentsBeforeDelete = if ($null -eq $attachmentsBeforeDeleteResponse) { @() } else { @($attachmentsBeforeDeleteResponse) }
$downloadPath = Join-Path $env:TEMP "admin-download-$stamp.txt"
Invoke-WebRequest `
-Uri "$BaseUrl/admin/api/attachments/$($attachment.id)/content" `
-WebSession $adminSession `
-OutFile $downloadPath | Out-Null
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users/$($actorSession.user.id)/block" `
-WebSession $adminSession `
-Method Post | Out-Null
$blockedStatusCode = 0
try {
Invoke-WebRequest `
-Uri "$BaseUrl/api/auth/login" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $actorUsername
password = $password
} | ConvertTo-Json) | Out-Null
}
catch {
$blockedStatusCode = [int]$_.Exception.Response.StatusCode
}
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users/$($actorSession.user.id)/unblock" `
-WebSession $adminSession `
-Method Post | Out-Null
$actorSessionAfterUnblock = Invoke-RestMethod `
-Uri "$BaseUrl/api/auth/login" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $actorUsername
password = $password
} | ConvertTo-Json)
$actorHeadersAfterUnblock = @{
Authorization = "Bearer $($actorSessionAfterUnblock.accessToken)"
}
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/chats/$($chat.id)" `
-WebSession $adminSession `
-Method Delete | Out-Null
$chatDeleted = $false
try {
Invoke-RestMethod `
-Uri "$BaseUrl/api/chats/$($chat.id)" `
-Headers $actorHeadersAfterUnblock `
-Method Get | Out-Null
}
catch {
$chatDeleted = [int]$_.Exception.Response.StatusCode -eq 404
}
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users/$($actorSession.user.id)" `
-WebSession $adminSession `
-Method Delete | Out-Null
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users/$($targetSession.user.id)" `
-WebSession $adminSession `
-Method Delete | Out-Null
$usersAfterDeleteResponse = Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/users?q=adminsmoke$stamp" `
-WebSession $adminSession `
-Method Get
[object[]]$usersAfterDelete = if ($null -eq $usersAfterDeleteResponse) { @() } else { @($usersAfterDeleteResponse) }
Invoke-RestMethod `
-Uri "$BaseUrl/admin/api/session/logout" `
-WebSession $adminSession `
-Method Post | Out-Null
$logoutBlocked = $false
try {
Invoke-WebRequest `
-Uri "$BaseUrl/admin/api/status" `
-WebSession $adminSession `
-Method Get | Out-Null
}
catch {
$logoutBlocked = [int]$_.Exception.Response.StatusCode -eq 401
}
[pscustomobject]@{
ActorUsername = $actorUsername
TargetUsername = $targetUsername
TotalUsers = $status.totalUsers
DatabaseReachable = $status.databaseReachable
UsersBeforeCount = $usersBeforeDelete.Count
ChatsBeforeCount = $chatsBeforeDelete.Count
AttachmentsBeforeCount = $attachmentsBeforeDelete.Count
UsersListed = $usersBeforeDelete.Count -ge 1
ChatsListed = $chatsBeforeDelete.Count -ge 1
AttachmentsListed = $attachmentsBeforeDelete.Count -ge 1
AttachmentDownloaded = (Get-Item $downloadPath).Length -gt 0
BlockedLoginStatus = $blockedStatusCode
ChatDeleted = $chatDeleted
UsersDeleted = $usersAfterDelete.Count -eq 0
AdminLogoutBlocked = $logoutBlocked
}
+123
View File
@@ -0,0 +1,123 @@
param(
[string]$BaseUrl = 'http://localhost:5099'
)
$ErrorActionPreference = 'Stop'
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$phoneA = "+7999$stamp"
$phoneB = "+7888$stamp"
function Invoke-JsonPost {
param(
[string]$Uri,
[hashtable]$Body,
[hashtable]$Headers = @{}
)
return Invoke-RestMethod `
-Uri $Uri `
-Method Post `
-Headers $Headers `
-ContentType 'application/json' `
-Body ($Body | ConvertTo-Json)
}
$challengeA = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/request-code" `
-Body @{ phoneNumber = $phoneA }
$sessionA = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/verify-code" `
-Body @{
challengeId = $challengeA.challengeId
phoneNumber = $phoneA
code = $challengeA.testCode
displayName = 'Phone Smoke A'
}
$challengeB = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/request-code" `
-Body @{ phoneNumber = $phoneB }
$sessionB = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/verify-code" `
-Body @{
challengeId = $challengeB.challengeId
phoneNumber = $phoneB
code = $challengeB.testCode
displayName = 'Phone Smoke B'
}
$headersA = @{
Authorization = "Bearer $($sessionA.accessToken)"
}
$meA = Invoke-RestMethod `
-Uri "$BaseUrl/api/users/me" `
-Method Get `
-Headers $headersA
$resolve = Invoke-JsonPost `
-Uri "$BaseUrl/api/users/contacts/resolve" `
-Headers $headersA `
-Body @{
phoneNumbers = @(
$phoneB,
'+7 (777) 000-00-00'
)
}
if ($null -eq $resolve) {
$resolve = @()
}
else {
$resolve = @($resolve)
}
$repeatChallengeA = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/request-code" `
-Body @{ phoneNumber = $phoneA }
$repeatSessionA = Invoke-JsonPost `
-Uri "$BaseUrl/api/auth/verify-code" `
-Body @{
challengeId = $repeatChallengeA.challengeId
phoneNumber = $phoneA
code = $repeatChallengeA.testCode
displayName = $null
}
$chat = Invoke-JsonPost `
-Uri "$BaseUrl/api/chats/direct" `
-Headers $headersA `
-Body @{
userId = $sessionB.user.id
}
$chats = Invoke-RestMethod `
-Uri "$BaseUrl/api/chats" `
-Method Get `
-Headers $headersA
if ($null -eq $chats) {
$chats = @()
}
else {
$chats = @($chats)
}
[pscustomobject]@{
PhoneARegistered = -not $challengeA.isRegistered
PhoneBRegistered = -not $challengeB.isRegistered
PhoneAMeDisplayName = $meA.displayName
PhoneAMePhone = $meA.phoneNumber
ResolvedCount = $resolve.Count
ResolvedPhone = if ($resolve.Count -gt 0) { $resolve[0].phoneNumber } else { $null }
ResolvedDisplayName = if ($resolve.Count -gt 0) { $resolve[0].user.displayName } else { $null }
ReLoginRegistered = $repeatChallengeA.isRegistered
ReLoginSameUser = $repeatSessionA.user.id -eq $sessionA.user.id
DirectChatType = $chat.type
DirectChatCreated = $chat.type -eq 1 -or $chat.type -eq 'Direct'
ChatsCount = $chats.Count
}
+222
View File
@@ -0,0 +1,222 @@
param(
[string]$Slug = 'ikar-android',
[string]$Name,
[string]$Summary = 'Android client build',
[string]$Description = 'Android release channel for the Ikar client.',
[string]$Version,
[string]$Channel = 'stable',
[string]$Notes,
[string]$RepositoryUrl,
[string]$HomepageUrl = 'https://ikar.kusoft.xyz',
[switch]$Unlisted,
[switch]$SkipBuild
)
$ErrorActionPreference = 'Stop'
function Require-Tool([string]$Path, [string]$Label) {
if (-not (Test-Path $Path)) {
throw "$Label was not found: $Path"
}
}
function Get-ApkBadgingValue([string]$BadgingText, [string]$Pattern) {
$match = [regex]::Match($BadgingText, $Pattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
if (-not $match.Success) {
return $null
}
return $match.Groups[1].Value
}
function Resolve-ArgusPlatform([string[]]$NativeCodes) {
$normalized = $NativeCodes | Where-Object { $_ } | ForEach-Object { $_.Trim().ToLowerInvariant() }
$allUniversalAbis = @('arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64')
if (@($allUniversalAbis | Where-Object { $normalized -contains $_ }).Count -eq $allUniversalAbis.Count) {
return 'android-universal'
}
if ($normalized -contains 'arm64-v8a') {
return 'android-arm64'
}
if ($normalized -contains 'armeabi-v7a') {
return 'android-armv7'
}
if ($normalized -contains 'x86_64') {
return 'android-x86_64'
}
if ($normalized -contains 'x86') {
return 'android-x86'
}
return 'android'
}
function Get-RepositoryUrl() {
$remote = git remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($remote)) {
return $remote.Trim()
}
return $null
}
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repoRoot
$javaHome = if ($env:JAVA_HOME) { $env:JAVA_HOME } else { 'C:\Program Files\Android\openjdk\jdk-21.0.8' }
$androidHome = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } else { 'C:\Users\seven\AppData\Local\Android\Sdk' }
$env:JAVA_HOME = $javaHome
$env:ANDROID_HOME = $androidHome
$projectPath = Join-Path $repoRoot 'src\Ikar.Client\Ikar.Client.csproj'
$publishDir = Join-Path $repoRoot 'src\Ikar.Client\bin\Release\net10.0-android'
$releaseApkPath = Join-Path $publishDir 'com.seven.ikar-Signed.apk'
$aaptPath = Join-Path $androidHome 'build-tools\36.1.0\aapt.exe'
$apksignerPath = Join-Path $androidHome 'build-tools\36.1.0\apksigner.bat'
Require-Tool $projectPath 'Ikar.Client project'
Require-Tool $aaptPath 'aapt'
Require-Tool $apksignerPath 'apksigner'
if (-not $SkipBuild) {
dotnet publish $projectPath -f net10.0-android -c Release -p:AndroidPackageFormat=apk -v minimal --no-restore
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed with exit code $LASTEXITCODE."
}
}
if (-not (Test-Path $releaseApkPath)) {
throw "Release APK was not found: $releaseApkPath"
}
$badging = & $aaptPath dump badging $releaseApkPath
if ($LASTEXITCODE -ne 0) {
throw "aapt badging failed with exit code $LASTEXITCODE."
}
$badgingText = ($badging | Out-String)
$packageName = Get-ApkBadgingValue $badgingText "package: name='([^']+)'"
$versionCode = Get-ApkBadgingValue $badgingText "versionCode='([^']+)'"
$versionName = Get-ApkBadgingValue $badgingText "versionName='([^']+)'"
$targetSdk = Get-ApkBadgingValue $badgingText "targetSdkVersion:'([^']+)'"
$applicationLabel = Get-ApkBadgingValue $badgingText "application-label:'([^']+)'"
$nativeCodeLine = ($badging | Where-Object { $_ -like 'native-code:*' } | Select-Object -First 1)
$nativeCodes = if ($nativeCodeLine) {
[regex]::Matches($nativeCodeLine, "'([^']+)'") | ForEach-Object { $_.Groups[1].Value }
} else {
@()
}
if (-not $packageName) {
throw 'Package name could not be extracted from APK.'
}
if (-not $versionCode) {
throw 'Version code could not be extracted from APK.'
}
if (-not $versionName) {
throw 'Version name could not be extracted from APK.'
}
$signatureOutput = & $apksignerPath verify --print-certs $releaseApkPath
if ($LASTEXITCODE -ne 0) {
throw "apksigner verification failed with exit code $LASTEXITCODE."
}
$signatureText = ($signatureOutput | Out-String)
$signerDn = Get-ApkBadgingValue $signatureText "Signer #1 certificate DN: (.+)"
$signerSha256 = Get-ApkBadgingValue $signatureText "Signer #1 certificate SHA-256 digest: ([A-Fa-f0-9]+)"
if ($signerDn) {
$signerDn = $signerDn.Trim()
}
if ($signerSha256) {
$signerSha256 = $signerSha256.Trim()
}
$isDebugSigned = $false
if ($signerDn -and $signerDn -like '*CN=Android Debug*') {
$isDebugSigned = $true
}
$resolvedName = if ($Name) { $Name } elseif ($applicationLabel) { $applicationLabel } else { 'Ikar' }
$resolvedVersion = if ($Version) { $Version } else { '{0}+{1}' -f $versionName, $versionCode }
$resolvedRepositoryUrl = if ($RepositoryUrl) { $RepositoryUrl } else { Get-RepositoryUrl }
$resolvedPlatform = Resolve-ArgusPlatform $nativeCodes
$apkItem = Get-Item $releaseApkPath
$apkHash = (Get-FileHash -Path $releaseApkPath -Algorithm SHA256).Hash.ToLowerInvariant()
$stageDirectory = Join-Path $repoRoot ("artifacts\argus\{0}\v{1}" -f $Slug, $versionCode)
if (-not (Test-Path $stageDirectory)) {
New-Item -ItemType Directory -Path $stageDirectory -Force | Out-Null
}
$stagedApkName = "{0}-{1}.apk" -f $Slug, $versionCode
$stagedApkPath = Join-Path $stageDirectory $stagedApkName
Copy-Item $releaseApkPath $stagedApkPath -Force
$metadata = [ordered]@{
slug = $Slug
name = $resolvedName
summary = $Summary
description = $Description
version = $resolvedVersion
channel = $Channel
platform = $resolvedPlatform
packageKind = 'apk'
notes = if ($Notes) { $Notes } else { $null }
repositoryUrl = if ($resolvedRepositoryUrl) { $resolvedRepositoryUrl } else { $null }
homepageUrl = if ($HomepageUrl) { $HomepageUrl } else { $null }
androidPackageName = $packageName
androidVersionCode = [int]$versionCode
targetSdkVersion = if ($targetSdk) { [int]$targetSdk } else { $null }
isListed = (-not $Unlisted)
packagePath = $stagedApkPath
packageFileName = $stagedApkName
packageSizeBytes = $apkItem.Length
packageSha256 = $apkHash
preparedAt = [DateTimeOffset]::UtcNow.ToString('O')
verifiedFromApk = [ordered]@{
sourceApk = $releaseApkPath
applicationLabel = if ($applicationLabel) { $applicationLabel } else { $null }
packageName = $packageName
versionCode = [int]$versionCode
versionName = $versionName
targetSdkVersion = if ($targetSdk) { [int]$targetSdk } else { $null }
nativeCodes = $nativeCodes
signerDn = if ($signerDn) { $signerDn } else { $null }
signerSha256 = if ($signerSha256) { $signerSha256.ToLowerInvariant() } else { $null }
isDebugSigned = $isDebugSigned
}
}
$metadataPath = Join-Path $stageDirectory 'argus-package.json'
$metadata | ConvertTo-Json -Depth 6 | Set-Content -Path $metadataPath -Encoding UTF8
$summaryLines = @(
"slug=$Slug"
"name=$resolvedName"
"version=$resolvedVersion"
"channel=$Channel"
"platform=$resolvedPlatform"
"packageKind=apk"
"androidPackageName=$packageName"
"androidVersionCode=$versionCode"
"targetSdkVersion=$targetSdk"
"packagePath=$stagedApkPath"
"packageSizeBytes=$($apkItem.Length)"
"packageSha256=$apkHash"
"signerDn=$signerDn"
"signerSha256=$($signerSha256.ToLowerInvariant())"
"isDebugSigned=$isDebugSigned"
)
$summaryPath = Join-Path $stageDirectory 'argus-package-summary.txt'
$summaryLines | Set-Content -Path $summaryPath -Encoding UTF8
Write-Output "Prepared Argus package files."
Write-Output "APK: $stagedApkPath"
Write-Output "Metadata: $metadataPath"
Write-Output "Summary: $summaryPath"
+10
View File
@@ -0,0 +1,10 @@
$ErrorActionPreference = 'Stop'
Set-Location (Join-Path $PSScriptRoot '..')
$publishDir = '.\src\Ikar.Client\bin\Release\net10.0-android\publish'
if (Test-Path $publishDir) {
Remove-Item $publishDir -Recurse -Force
}
dotnet publish .\src\Ikar.Client\Ikar.Client.csproj -f net10.0-android -c Release -p:AndroidPackageFormat=apk
Get-ChildItem $publishDir | Where-Object { $_.Name -like 'com.seven.massenger*' } | Remove-Item -Force
+298
View File
@@ -0,0 +1,298 @@
param(
[string]$ServerHost = '192.168.0.185',
[string]$Username = 'sevenhill',
[Parameter(Mandatory = $true)]
[string]$Password,
[string]$ArgusBaseUrl = 'https://argus.kusoft.xyz',
[string]$ArgusInternalBaseUrl = 'http://127.0.0.1:5105',
[string]$ArgusEnvPath = '/home/sevenhill/argus-deploy/.env.server',
[string]$Slug = 'ikar-android',
[string]$Notes = 'Android client update for Ikar',
[string]$AppName = 'Ikar Android',
[string]$Summary = 'Android client build',
[string]$Description = 'Android release channel for the Ikar client.',
[string]$HomepageUrl = 'https://ikar.kusoft.xyz',
[string]$Platform = 'android',
[string]$PackageKind = 'apk',
[string]$Channel = 'stable'
)
$ErrorActionPreference = 'Stop'
Set-Location (Join-Path $PSScriptRoot '..')
$repoRoot = (Get-Location).Path
$projectPath = '.\src\Ikar.Client\Ikar.Client.csproj'
[xml]$projectXml = Get-Content $projectPath
$versionName = $projectXml.Project.PropertyGroup.ApplicationDisplayVersion | Select-Object -First 1
$versionCode = [int]($projectXml.Project.PropertyGroup.ApplicationVersion | Select-Object -First 1)
$publishDir = Join-Path $repoRoot (".codex-temp\android-update-publish\v{0}" -f $versionCode)
if (Test-Path $publishDir) {
Remove-Item $publishDir -Recurse -Force
}
dotnet publish $projectPath -f net10.0-android -c Release -p:AndroidPackageFormat=apk -p:UseSharedCompilation=false -o $publishDir
$sourceApk = Join-Path $publishDir 'com.seven.ikar-Signed.apk'
if (-not (Test-Path $sourceApk)) {
throw "Signed APK not found: $sourceApk"
}
$remoteFileName = "ikar-android-$versionCode.apk"
$localCopy = Join-Path $publishDir $remoteFileName
Copy-Item $sourceApk $localCopy -Force
$manifest = [ordered]@{
versionCode = $versionCode
versionName = $versionName
packageFileName = $remoteFileName
publishedAt = [DateTimeOffset]::UtcNow.ToString('O')
notes = $Notes
}
$manifestPath = Join-Path $publishDir 'latest.json'
$manifest | ConvertTo-Json -Depth 4 | Set-Content $manifestPath -Encoding UTF8
$argusPublishResult = @"
import json
import os
import paramiko
import posixpath
host = os.environ['IKAR_UPDATE_HOST']
username = os.environ['IKAR_UPDATE_USERNAME']
password = os.environ['IKAR_UPDATE_PASSWORD']
env_path = os.environ['IKAR_UPDATE_ARGUS_ENV_PATH']
local_apk_path = os.environ['IKAR_UPDATE_APK_PATH']
remote_apk_name = os.environ['IKAR_UPDATE_REMOTE_APK_NAME']
argus_internal_base_url = os.environ['IKAR_UPDATE_ARGUS_INTERNAL_BASE_URL']
slug = os.environ['IKAR_UPDATE_ARGUS_SLUG']
app_name = os.environ['IKAR_UPDATE_ARGUS_NAME']
summary = os.environ['IKAR_UPDATE_ARGUS_SUMMARY']
description = os.environ['IKAR_UPDATE_ARGUS_DESCRIPTION']
homepage_url = os.environ['IKAR_UPDATE_ARGUS_HOMEPAGE_URL']
release_version = os.environ['IKAR_UPDATE_ARGUS_VERSION']
channel = os.environ['IKAR_UPDATE_ARGUS_CHANNEL']
platform = os.environ['IKAR_UPDATE_ARGUS_PLATFORM']
package_kind = os.environ['IKAR_UPDATE_ARGUS_PACKAGE_KIND']
notes = os.environ['IKAR_UPDATE_ARGUS_NOTES']
temp_root = f"/home/{username}/.argus-upload"
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, username=username, password=password, timeout=10)
sftp = client.open_sftp()
with sftp.open(env_path, 'r') as handle:
content = handle.read().decode('utf-8', errors='replace')
values = {}
for raw_line in content.splitlines():
line = raw_line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
values[key.strip()] = value.strip()
username = values.get('Admin__Username')
password = values.get('Admin__Password')
if not username or not password:
raise RuntimeError(f"Admin credentials were not found in {env_path}.")
parts = temp_root.strip('/').split('/')
current = '/'
for part in parts:
current = posixpath.join(current, part)
try:
sftp.stat(current)
except FileNotFoundError:
sftp.mkdir(current)
remote_apk_path = posixpath.join(temp_root, remote_apk_name)
sftp.put(local_apk_path, remote_apk_path)
sftp.close()
def run_remote_python(script: str) -> str:
stdin, stdout, stderr = client.exec_command("python3 -", timeout=600)
stdin.write(script)
stdin.channel.shutdown_write()
out = stdout.read().decode('utf-8', errors='replace')
err = stderr.read().decode('utf-8', errors='replace')
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0:
raise RuntimeError(f"Remote publish failed with exit status {exit_status}\\nSTDOUT:\\n{out}\\nSTDERR:\\n{err}")
return out
remote_payload = json.dumps({
'base_url': argus_internal_base_url.rstrip('/'),
'slug': slug,
'package_path': remote_apk_path,
'admin_username': username,
'admin_password': password,
'app_name': app_name,
'summary': summary,
'description': description,
'homepage_url': homepage_url,
'release_version': release_version,
'release_channel': channel,
'release_platform': platform,
'release_package_kind': package_kind,
'release_notes': notes,
})
remote_script = '''
import json
import os
import urllib.request
import uuid
payload = json.loads(%s)
base_url = payload['base_url']
slug = payload['slug']
package_path = payload['package_path']
admin_username = payload['admin_username']
admin_password = payload['admin_password']
app_name = payload['app_name']
summary = payload['summary']
description = payload['description']
homepage_url = payload['homepage_url']
release_version = payload['release_version']
release_channel = payload['release_channel']
release_platform = payload['release_platform']
release_package_kind = payload['release_package_kind']
release_notes = payload['release_notes']
def read_response(response):
body = response.read().decode('utf-8', errors='replace')
return response.getcode(), body
opener = urllib.request.build_opener()
login_payload = json.dumps({
'username': admin_username,
'password': admin_password,
}).encode('utf-8')
login_request = urllib.request.Request(
f"{base_url}/api/admin/session/login",
data=login_payload,
headers={'Content-Type': 'application/json'},
method='POST')
login_response = opener.open(login_request, timeout=600)
login_status, login_body = read_response(login_response)
if login_status < 200 or login_status >= 300:
raise RuntimeError(f"Login failed with status {login_status}: {login_body}")
cookie_header = login_response.headers.get('Set-Cookie', '')
auth_cookie = cookie_header.split(';', 1)[0].strip()
if not auth_cookie:
raise RuntimeError('Argus login did not return an auth cookie.')
metadata_payload = json.dumps({
'name': app_name,
'summary': summary,
'description': description,
'homepageUrl': homepage_url or None,
'repositoryUrl': None,
'isListed': True,
}).encode('utf-8')
metadata_request = urllib.request.Request(
f"{base_url}/api/admin/apps/{slug}",
data=metadata_payload,
headers={
'Content-Type': 'application/json',
'Cookie': auth_cookie,
},
method='PUT')
metadata_response = opener.open(metadata_request, timeout=600)
metadata_status, metadata_body = read_response(metadata_response)
if metadata_status < 200 or metadata_status >= 300:
raise RuntimeError(f"Metadata update failed with status {metadata_status}: {metadata_body}")
with open(package_path, 'rb') as handle:
file_bytes = handle.read()
boundary = f"----IkarArgusBoundary{uuid.uuid4().hex}"
parts: list[bytes] = []
def add_field(name: str, value: str):
parts.append(f"--{boundary}\\r\\n".encode('utf-8'))
parts.append(f'Content-Disposition: form-data; name="{name}"\\r\\n\\r\\n'.encode('utf-8'))
parts.append(value.encode('utf-8'))
parts.append(b"\\r\\n")
for field_name, field_value in [
('Version', release_version),
('Channel', release_channel),
('Platform', release_platform),
('PackageKind', release_package_kind),
]:
add_field(field_name, field_value)
if release_notes:
add_field('Notes', release_notes)
file_name = os.path.basename(package_path)
parts.append(f"--{boundary}\\r\\n".encode('utf-8'))
parts.append(
f'Content-Disposition: form-data; name="Package"; filename="{file_name}"\\r\\n'.encode('utf-8'))
parts.append(b"Content-Type: application/vnd.android.package-archive\\r\\n\\r\\n")
parts.append(file_bytes)
parts.append(b"\\r\\n")
parts.append(f"--{boundary}--\\r\\n".encode('utf-8'))
release_request = urllib.request.Request(
f"{base_url}/api/admin/apps/{slug}/releases",
data=b''.join(parts),
headers={
'Content-Type': f'multipart/form-data; boundary={boundary}',
'Cookie': auth_cookie,
},
method='POST')
release_response = opener.open(release_request, timeout=600)
release_status, release_body = read_response(release_response)
if release_status < 200 or release_status >= 300:
raise RuntimeError(f"Release upload failed with status {release_status}: {release_body}")
print(release_body)
''' % json.dumps(remote_payload)
publish_output = run_remote_python(remote_script)
try:
sftp = client.open_sftp()
sftp.remove(remote_apk_path)
sftp.close()
except Exception:
pass
client.close()
print(json.dumps({'status': 'UPLOAD_OK', 'release': publish_output.strip()}))
"@ | ForEach-Object {
$env:IKAR_UPDATE_HOST = $ServerHost
$env:IKAR_UPDATE_USERNAME = $Username
$env:IKAR_UPDATE_PASSWORD = $Password
$env:IKAR_UPDATE_ARGUS_ENV_PATH = $ArgusEnvPath
$env:IKAR_UPDATE_APK_PATH = (Resolve-Path $localCopy)
$env:IKAR_UPDATE_REMOTE_APK_NAME = $remoteFileName
$env:IKAR_UPDATE_ARGUS_INTERNAL_BASE_URL = $ArgusInternalBaseUrl
$env:IKAR_UPDATE_ARGUS_SLUG = $Slug
$env:IKAR_UPDATE_ARGUS_NAME = $AppName
$env:IKAR_UPDATE_ARGUS_SUMMARY = $Summary
$env:IKAR_UPDATE_ARGUS_DESCRIPTION = $Description
$env:IKAR_UPDATE_ARGUS_HOMEPAGE_URL = $HomepageUrl
$env:IKAR_UPDATE_ARGUS_VERSION = "$versionName+$versionCode"
$env:IKAR_UPDATE_ARGUS_CHANNEL = $Channel
$env:IKAR_UPDATE_ARGUS_PLATFORM = $Platform
$env:IKAR_UPDATE_ARGUS_PACKAGE_KIND = $PackageKind
$env:IKAR_UPDATE_ARGUS_NOTES = $Notes
$_
} | python -
$publishResult = $argusPublishResult | ConvertFrom-Json
if ($publishResult.status -ne 'UPLOAD_OK') {
throw "Argus publish failed."
}
Write-Output $publishResult.status
+15
View File
@@ -0,0 +1,15 @@
$ErrorActionPreference = 'Stop'
Set-Location (Join-Path $PSScriptRoot '..')
$project = '.\src\Ikar.WinUI\Ikar.WinUI.csproj'
$target = '.\artifacts\winui-win64'
dotnet build $project -c Release -f net10.0-windows10.0.19041.0 -p:Platform=x64
if (Test-Path $target) {
Remove-Item $target -Recurse -Force
}
New-Item -ItemType Directory -Path $target | Out-Null
Copy-Item '.\src\Ikar.WinUI\bin\x64\Release\net10.0-windows10.0.19041.0\win-x64\*' $target -Recurse -Force
Get-ChildItem $target -Recurse | Where-Object { $_.Name -like 'Massenger*' } | Remove-Item -Recurse -Force
Write-Host "Windows client prepared at $target\Ikar.WinUI.exe"
+3
View File
@@ -0,0 +1,3 @@
$ErrorActionPreference = 'Stop'
Set-Location (Join-Path $PSScriptRoot '..')
dotnet run --project .\src\Ikar.Server\Ikar.Server.csproj
+190
View File
@@ -0,0 +1,190 @@
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Net.Http
$baseUrl = 'http://localhost:5099'
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$username = "smoke$stamp"
$password = 'Demo123!'
$phoneNumber = "+7900$stamp"
$session = Invoke-RestMethod `
-Uri "$baseUrl/api/auth/register" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = $username
displayName = 'Smoke Test'
password = $password
phoneNumber = $phoneNumber
} | ConvertTo-Json)
$refreshed = Invoke-RestMethod `
-Uri "$baseUrl/api/auth/refresh" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
refreshToken = $session.refreshToken
} | ConvertTo-Json)
$headers = @{
Authorization = "Bearer $($refreshed.accessToken)"
}
$installationId = "push-$stamp"
$pushDevice = Invoke-RestMethod `
-Uri "$baseUrl/api/push/devices" `
-Headers $headers `
-Method Post `
-ContentType 'application/json' `
-Body (@{
platform = 1
installationId = $installationId
deviceToken = "token-$stamp"
deviceName = "Smoke Android $stamp"
notificationsEnabled = $true
} | ConvertTo-Json)
$pushDevicesAfterRegister = Invoke-RestMethod `
-Uri "$baseUrl/api/push/devices" `
-Headers $headers `
-Method Get
if ($null -eq $pushDevicesAfterRegister) {
$pushDevicesAfterRegister = @()
}
else {
$pushDevicesAfterRegister = @($pushDevicesAfterRegister)
}
$bob = Invoke-RestMethod -Uri "$baseUrl/api/users/search?q=bob" -Headers $headers -Method Get | Select-Object -First 1
$channel = Invoke-RestMethod `
-Uri "$baseUrl/api/chats/channel" `
-Headers $headers `
-Method Post `
-ContentType 'application/json' `
-Body (@{
title = "Smoke Channel $stamp"
memberIds = @($bob.id)
} | ConvertTo-Json)
$message = Invoke-RestMethod `
-Uri "$baseUrl/api/chats/$($channel.id)/messages" `
-Headers $headers `
-Method Post `
-ContentType 'application/json' `
-Body (@{ text = 'channel smoke message' } | ConvertTo-Json)
$edited = Invoke-RestMethod `
-Uri "$baseUrl/api/chats/$($channel.id)/messages/$($message.id)" `
-Headers $headers `
-Method Put `
-ContentType 'application/json' `
-Body (@{ text = 'channel smoke message edited' } | ConvertTo-Json)
$tempFile = Join-Path $env:TEMP "ikar-voice-$stamp.ogg"
"voice smoke test $stamp" | Set-Content -Path $tempFile -NoNewline
$handler = New-Object System.Net.Http.HttpClientHandler
$client = New-Object System.Net.Http.HttpClient($handler)
$client.BaseAddress = [Uri]::new("$baseUrl/")
$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $refreshed.accessToken)
$form = New-Object System.Net.Http.MultipartFormDataContent
$form.Add((New-Object System.Net.Http.StringContent('voice caption')), 'text')
$fileStream = [System.IO.File]::OpenRead($tempFile)
$fileContent = New-Object System.Net.Http.StreamContent($fileStream)
$fileContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue('audio/ogg')
$form.Add($fileContent, 'file', [System.IO.Path]::GetFileName($tempFile))
$uploadResponse = $client.PostAsync("api/chats/$($channel.id)/attachments", $form).GetAwaiter().GetResult()
$uploadPayload = $uploadResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$fileStream.Dispose()
$form.Dispose()
$client.Dispose()
$handler.Dispose()
if (-not $uploadResponse.IsSuccessStatusCode) {
throw $uploadPayload
}
$voiceMessage = $uploadPayload | ConvertFrom-Json
$attachment = $voiceMessage.attachments[0]
$downloadHandler = New-Object System.Net.Http.HttpClientHandler
$downloadClient = New-Object System.Net.Http.HttpClient($downloadHandler)
$downloadClient.BaseAddress = [Uri]::new("$baseUrl/")
$downloadClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $refreshed.accessToken)
$downloadResponse = $downloadClient.GetAsync($attachment.downloadPath.TrimStart('/')).GetAwaiter().GetResult()
$downloadBytes = $downloadResponse.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult()
$downloadClient.Dispose()
$downloadHandler.Dispose()
$deleted = Invoke-RestMethod `
-Uri "$baseUrl/api/chats/$($channel.id)/messages/$($message.id)" `
-Headers $headers `
-Method Delete
$bobSession = Invoke-RestMethod `
-Uri "$baseUrl/api/auth/login" `
-Method Post `
-ContentType 'application/json' `
-Body (@{
username = 'bob'
password = 'demo123'
} | ConvertTo-Json)
$bobHeaders = @{
Authorization = "Bearer $($bobSession.accessToken)"
}
$subscriberBlocked = $false
try {
Invoke-RestMethod `
-Uri "$baseUrl/api/chats/$($channel.id)/messages" `
-Headers $bobHeaders `
-Method Post `
-ContentType 'application/json' `
-Body (@{ text = 'subscriber should not publish' } | ConvertTo-Json) | Out-Null
}
catch {
$subscriberBlocked = $_.Exception.Response.StatusCode.value__ -eq 403
}
$channelForBob = Invoke-RestMethod -Uri "$baseUrl/api/chats/$($channel.id)" -Headers $bobHeaders -Method Get
$channelReload = Invoke-RestMethod -Uri "$baseUrl/api/chats/$($channel.id)" -Headers $headers -Method Get
Invoke-RestMethod -Uri "$baseUrl/api/push/devices/$installationId" -Headers $headers -Method Delete | Out-Null
$pushDevicesAfterDelete = Invoke-RestMethod `
-Uri "$baseUrl/api/push/devices" `
-Headers $headers `
-Method Get
if ($null -eq $pushDevicesAfterDelete) {
$pushDevicesAfterDelete = @()
}
else {
$pushDevicesAfterDelete = @($pushDevicesAfterDelete)
}
Invoke-RestMethod -Uri "$baseUrl/api/auth/logout" -Headers $headers -Method Post | Out-Null
$logoutBlocked = $false
try {
Invoke-RestMethod -Uri "$baseUrl/api/users/me" -Headers $headers -Method Get | Out-Null
}
catch {
$logoutBlocked = $_.Exception.Response.StatusCode.value__ -eq 401
}
[pscustomobject]@{
Username = $username
ChannelId = $channel.id
EditedMessageId = $edited.id
EditedText = $edited.text
DeletedFlag = $null -ne $deleted.deletedAt
VoiceAttachmentId = $attachment.id
VoiceKind = $attachment.kind
VoiceKindOk = ($attachment.kind -eq 2) -or ($attachment.kind -eq 'VoiceNote')
DownloadOk = [int]$downloadResponse.StatusCode -eq 200 -and $downloadBytes.Length -eq $attachment.fileSizeBytes
PushRegistered = $pushDevice.installationId -eq $installationId
PushListed = $pushDevicesAfterRegister.Count -eq 1 -and $pushDevicesAfterRegister[0].installationId -eq $installationId
PushDeleted = $pushDevicesAfterDelete.Count -eq 0
MessageCount = $channelReload.messages.Count
SubscriberBlocked = $subscriberBlocked
ChannelReadOnly = -not $channelForBob.canSendMessages
RefreshRotated = $session.refreshToken -ne $refreshed.refreshToken
LogoutBlocked = $logoutBlocked
}