mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 14:57:42 +00:00
ci: give the desktop test directory full-bleed letter avatars, and resolve a test SimpleX name on a local SMP server
The avatars are a two-shade gradient with a large white letter, noisy enough to stay near the size the apps make. backend.sh starts an SMP server built from the pinned simplexmq with its [NAMES] resolver on a local registry, and ci_contact claims seeded.testing for its address there; the entries' descriptions now read "CI seeded ..." so the name is also a directory search.
This commit is contained in:
1 parent
e67fda0371
commit
dfa87309bb
4 files changed
+232
-21
No files matched your search
@@ -211,8 +211,8 @@ jobs:
|
||||
- name: Install tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3
|
||||
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3
|
||||
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
||||
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
|
||||
|
||||
- name: Download core library
|
||||
uses: actions/download-artifact@v7
|
||||
@@ -275,7 +275,9 @@ jobs:
|
||||
|
||||
- name: Run smoke test
|
||||
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.link.outcome == 'success' }}
|
||||
run: scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test"
|
||||
run: |
|
||||
export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server")
|
||||
scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test"
|
||||
|
||||
- name: Stop test backend
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
# and one channel registered and approved by a CLI peer, and a search request that must find both.
|
||||
# Both have profile images as large as the apps make them, so their search results don't fit in one service
|
||||
# response, and the search must page through them.
|
||||
# For SimpleX names, an SMP server on 127.0.0.1 resolves .testing names through a local resolver, and a CLI peer
|
||||
# with an address on it claims the name seeded.testing.
|
||||
# Usage:
|
||||
# scripts/ci/desktop-linux/backend.sh start <backend .tar.gz> <work dir> writes the directory's short address to <work dir>/directory-link
|
||||
# scripts/ci/desktop-linux/backend.sh stop <work dir> stops the relay and the directory, prints their logs
|
||||
# The relay and the directory keep running between start and stop.
|
||||
# and the names server's address to <work dir>/names-server
|
||||
# scripts/ci/desktop-linux/backend.sh stop <work dir> stops the servers, prints their logs
|
||||
# The relay, the directory, the SMP server and the resolver keep running between start and stop.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
directory_name="SimpleX Directory"
|
||||
admin=ci_admin
|
||||
search_text="ci seed"
|
||||
@@ -18,8 +22,8 @@ search_text="ci seed"
|
||||
max_image_size=12500
|
||||
# the largest service response body, compressed if it has to be (maxServiceBodyLength)
|
||||
max_service_body=10737
|
||||
group_colour="rgb(200,40,40)"
|
||||
channel_colour="rgb(40,170,60)"
|
||||
name_label=seeded
|
||||
resolver_port=8090
|
||||
|
||||
# Runs one chat command in a CLI client that stays online for <secs> after it; the output is in $work/out.
|
||||
chat() {
|
||||
@@ -63,18 +67,21 @@ db_query() {
|
||||
}
|
||||
|
||||
# Prints a profile image data URI made as the apps make one: JPEG at quality 85, scaled down by the square root
|
||||
# of its excess, at most halved per step, until it is at most max_image_size characters. The source is noise
|
||||
# over the colour, which JPEG can't compress much, so the image ends up near the limit.
|
||||
# of its excess, at most halved per step, until it is at most max_image_size characters. The picture fills the
|
||||
# whole square, a gradient between two shades of one colour with a large white letter; noise keeps JPEG from
|
||||
# compressing it much, so the image ends up near the limit.
|
||||
avatar() {
|
||||
local colour=$1 source=$work/avatar-source.png file=$work/avatar.jpg size=512 uri
|
||||
convert -size 512x512 xc:gray50 -seed 1 +noise Random -modulate 100,0 -fill "$colour" -colorize 70% "$source"
|
||||
local top=$1 bottom=$2 letter=$3 source=$work/avatar-source.png file=$work/avatar.jpg size=512 uri font
|
||||
font=$(fc-match -f '%{file}' 'DejaVu Sans:bold')
|
||||
convert -size 512x512 gradient:"$top-$bottom" -seed 1 -attenuate 0.5 +noise Gaussian \
|
||||
-font "$font" -pointsize 320 -fill white -gravity center -annotate +0+0 "$letter" "$source"
|
||||
while :; do
|
||||
convert "$source" -resize "${size}x${size}" -quality 85 "$file"
|
||||
uri="data:image/jpg;base64,$(base64 -w 0 "$file")"
|
||||
[ "${#uri}" -gt "$max_image_size" ] || break
|
||||
size=$(awk -v s="$size" -v n="${#uri}" -v m="$max_image_size" 'BEGIN { r = sqrt(n / m); if (r > 2) r = 2; print int(s / r) }')
|
||||
done
|
||||
echo "avatar over $colour: ${size}x${size} px, ${#uri} characters, mean colour $(convert "$file" -resize '1x1!' -format '%[pixel:p{0,0}]' info:)" >&2
|
||||
echo "avatar \"$letter\" over $top to $bottom: ${size}x${size} px, ${#uri} characters, mean colour $(convert "$file" -resize '1x1!' -format '%[pixel:p{0,0}]' info:)" >&2
|
||||
echo "$uri"
|
||||
}
|
||||
|
||||
@@ -169,9 +176,9 @@ group_json() {
|
||||
seed_group() {
|
||||
local user_id
|
||||
user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'")
|
||||
group_image=$(avatar "$group_colour")
|
||||
admin 1 "/_group $user_id $(group_json ci_group "CI seed group" "$group_image" "")"
|
||||
expect_out "group #ci_group \(CI seed group\) is created"
|
||||
group_image=$(avatar "rgb(235,80,70)" "rgb(150,25,30)" G)
|
||||
admin 1 "/_group $user_id $(group_json ci_group "CI seeded group" "$group_image" "")"
|
||||
expect_out "group #ci_group \(CI seeded group\) is created"
|
||||
admin 5 "/a ci_group '$directory_name' admin"
|
||||
expect_out "invitation to join the group #ci_group sent to"
|
||||
approve ci_group "Group approved!"
|
||||
@@ -183,9 +190,9 @@ seed_channel() {
|
||||
local user_id relay_id
|
||||
user_id=$(db_query "$admin" "SELECT user_id FROM users WHERE local_display_name = '$admin'")
|
||||
relay_id=$(db_query "$admin" "SELECT chat_relay_id FROM chat_relays WHERE display_name = 'ci_relay' AND deleted = 0")
|
||||
channel_image=$(avatar "$channel_colour")
|
||||
admin 10 "/_public group $user_id $relay_id $(group_json ci_channel "CI seed channel" "$channel_image" ",\"support\":{\"enable\":\"off\"}")"
|
||||
expect_out "group #ci_channel \(CI seed channel\) is created"
|
||||
channel_image=$(avatar "rgb(80,200,90)" "rgb(25,120,45)" C)
|
||||
admin 10 "/_public group $user_id $relay_id $(group_json ci_channel "CI seeded channel" "$channel_image" ",\"support\":{\"enable\":\"off\"}")"
|
||||
expect_out "group #ci_channel \(CI seeded channel\) is created"
|
||||
local group_id
|
||||
group_id=$(db_query "$admin" "SELECT group_id FROM groups WHERE local_display_name = 'ci_channel'")
|
||||
admin_until "relay id [0-9]+: active" 180 "/_get relays #$group_id"
|
||||
@@ -194,6 +201,71 @@ seed_channel() {
|
||||
approve ci_channel "Channel approved!"
|
||||
}
|
||||
|
||||
# An SMP server on 127.0.0.1 whose [NAMES] section asks the local resolver, which answers from names.json.
|
||||
start_names_server() {
|
||||
local cfg=$work/smp/etc logs=$work/smp/var ini deadline
|
||||
echo '{}' > "$work/names.json"
|
||||
nohup python3 "$here/names-resolver.py" "$resolver_port" "$work/names.json" < /dev/null > "$work/resolver.log" 2>&1 &
|
||||
echo $! > "$work/resolver.pid"
|
||||
SMP_SERVER_CFG_PATH=$cfg SMP_SERVER_LOG_PATH=$logs "$bin/smp-server" init -y --ip 127.0.0.1 --no-password --disable-web
|
||||
ini=$cfg/smp-server.ini
|
||||
# only 5223: 443 needs root
|
||||
sed -i -e 's/^port = 5223,443$/port = 5223/' \
|
||||
-e '/^\[NAMES\]/,/^\[/{s/^enable: off$/enable: on/;s|^# resolver_endpoint: http://127.0.0.1:8000$|resolver_endpoint: http://127.0.0.1:'"$resolver_port"'|}' "$ini"
|
||||
if ! grep -qx 'port = 5223' "$ini" ||
|
||||
! sed -n '/^\[NAMES\]/,/^\[/p' "$ini" | grep -qx 'enable: on' ||
|
||||
! sed -n '/^\[NAMES\]/,/^\[/p' "$ini" | grep -qx "resolver_endpoint: http://127.0.0.1:$resolver_port"; then
|
||||
echo "error: $ini has a different port or [NAMES] than expected:"
|
||||
grep -E '^port|^\[NAMES\]|enable|resolver' "$ini"
|
||||
return 1
|
||||
fi
|
||||
SMP_SERVER_CFG_PATH=$cfg SMP_SERVER_LOG_PATH=$logs nohup "$bin/smp-server" start < /dev/null > "$work/smp-server.log" 2>&1 &
|
||||
echo $! > "$work/smp-server.pid"
|
||||
deadline=$((SECONDS + 60))
|
||||
until (exec 3<> /dev/tcp/127.0.0.1/5223) 2> /dev/null; do
|
||||
if ! kill -0 "$(cat "$work/smp-server.pid")" 2> /dev/null || [ "$SECONDS" -ge "$deadline" ]; then
|
||||
echo "error: the SMP server is not listening on 5223"
|
||||
cat "$work/smp-server.log"
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
names_server="smp://$(cat "$cfg/fingerprint")@127.0.0.1"
|
||||
echo "$names_server" > "$work/names-server"
|
||||
echo "names server: $names_server"
|
||||
}
|
||||
|
||||
# A CLI client started with -s uses only that server and, once its user exists, gives it every role, names included.
|
||||
names_client() {
|
||||
local db=$1
|
||||
shift
|
||||
chat "$db" "$@" --user-display-name "$db" -s "$names_server"
|
||||
}
|
||||
|
||||
# ci_contact's address is on the names server; the resolver maps seeded.testing to it, and ci_contact claims the name.
|
||||
seed_name() {
|
||||
local image link
|
||||
image=$(avatar "rgb(70,130,240)" "rgb(30,50,170)" N)
|
||||
names_client ci_contact 1 "/_profile 1 {\"displayName\":\"ci_contact\",\"fullName\":\"\",\"shortDescr\":\"CI seeded contact\",\"image\":\"$image\"}"
|
||||
names_client ci_contact 3 "/ad"
|
||||
link=$(awk '/Your new chat address is created!/ {getline; getline; print; exit}' "$work/out")
|
||||
case "$link" in
|
||||
https://*/a#*) ;;
|
||||
*)
|
||||
echo "error: no short address for ci_contact in the output"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
jq -n --arg n "$name_label" --arg l "$link" '{($n): {simplexContact: [$l]}}' > "$work/names.json"
|
||||
names_client ci_contact 5 "/_set domain 1 $name_label.testing"
|
||||
# what the app does on the Search key, from a client that knows nothing but the names server; it finds the
|
||||
# address only if ci_contact's claim is in the address data
|
||||
names_client name_probe 1 "/user"
|
||||
names_client name_probe 5 "/_connect plan 1 @$name_label.testing"
|
||||
expect_out "contact address: ok to connect"
|
||||
echo "@$name_label.testing resolves to ci_contact's address"
|
||||
}
|
||||
|
||||
page_summary() {
|
||||
local resp=$1 compressed=""
|
||||
if command -v zstd > /dev/null; then
|
||||
@@ -266,17 +338,19 @@ start() {
|
||||
seed_group
|
||||
seed_channel
|
||||
probe_search
|
||||
start_names_server
|
||||
seed_name
|
||||
}
|
||||
|
||||
stop() {
|
||||
local p
|
||||
for p in directory relay; do
|
||||
for p in directory relay smp-server resolver; do
|
||||
if [ -f "$work/$p.pid" ]; then
|
||||
kill -INT "$(cat "$work/$p.pid")" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
sleep 3
|
||||
for p in directory relay; do
|
||||
for p in directory relay smp-server resolver; do
|
||||
echo "::group::$p log"
|
||||
cat "$work/$p.log" 2> /dev/null || true
|
||||
echo "::endgroup::"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Builds the CLI and the directory service that the app job runs as its test backend.
|
||||
# Builds the CLI, the directory service and the SMP server that the app job runs as its test backend.
|
||||
# Usage: scripts/ci/desktop-linux/build-backend.sh <output .tar.gz>
|
||||
# They need the server-capable configuration (no client_library), as build.yml's Linux job builds the CLI.
|
||||
# The SMP server, which resolves the test's SimpleX names, comes from a clone of the simplexmq commit that
|
||||
# cabal.project pins: cabal builds only the libraries of a source-repository-package.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -23,10 +25,19 @@ EOF
|
||||
|
||||
cabal build exe:simplex-chat exe:simplex-directory-service
|
||||
|
||||
simplexmq_tag=$(sed -n '/simplex-chat\/simplexmq.git/{n;s/^ *tag: *//p}' cabal.project)
|
||||
smq=$(mktemp -d)
|
||||
git -C "$smq" init -q
|
||||
git -C "$smq" fetch -q --depth 1 https://github.com/simplex-chat/simplexmq.git "$simplexmq_tag"
|
||||
git -C "$smq" checkout -q FETCH_HEAD
|
||||
cp cabal.project.local "$smq"
|
||||
(cd "$smq" && cabal build exe:smp-server)
|
||||
|
||||
pkg=$(mktemp -d)
|
||||
for exe in simplex-chat simplex-directory-service; do
|
||||
cp "$(cabal list-bin "exe:$exe")" "$pkg"
|
||||
done
|
||||
cp "$(cd "$smq" && cabal list-bin exe:smp-server)" "$pkg"
|
||||
ldd "$pkg"/*
|
||||
|
||||
tar -C "$pkg" -czf "$out" .
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# A SimpleX name resolver for the test SMP server's [NAMES] section, as simplexmq's test resolver answers:
|
||||
# GET /v2/resolve/<query> with a NameResponse for every name, registered or not, and GET /health.
|
||||
# Names are read from the registry file on each request: {"<label>": {"simplexContact": [...], "simplexChannel": [...]}},
|
||||
# all under .testing. A v22 router asks by label hash, "[<keccak-256 hex>].testing", so names are matched by hash.
|
||||
# Usage: names-resolver.py <port> <registry .json>
|
||||
|
||||
import json
|
||||
import sys
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import unquote
|
||||
|
||||
MASK = (1 << 64) - 1
|
||||
ROTATIONS = [[0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61], [28, 55, 25, 21, 56], [27, 20, 39, 8, 14]]
|
||||
|
||||
|
||||
def round_constants():
|
||||
r, constants = 1, []
|
||||
for _ in range(24):
|
||||
c = 0
|
||||
for j in range(7):
|
||||
if r & 1:
|
||||
c |= 1 << ((1 << j) - 1)
|
||||
r = ((r << 1) ^ 0x71) & 0xFF if r & 0x80 else r << 1
|
||||
constants.append(c)
|
||||
return constants
|
||||
|
||||
|
||||
ROUND_CONSTANTS = round_constants()
|
||||
|
||||
|
||||
def rotate(x, n):
|
||||
return ((x << n) | (x >> (64 - n))) & MASK if n else x
|
||||
|
||||
|
||||
def keccak_f(a):
|
||||
for rc in ROUND_CONSTANTS:
|
||||
c = [a[x][0] ^ a[x][1] ^ a[x][2] ^ a[x][3] ^ a[x][4] for x in range(5)]
|
||||
d = [c[x - 1] ^ rotate(c[(x + 1) % 5], 1) for x in range(5)]
|
||||
a = [[a[x][y] ^ d[x] for y in range(5)] for x in range(5)]
|
||||
b = [[0] * 5 for _ in range(5)]
|
||||
for x in range(5):
|
||||
for y in range(5):
|
||||
b[y][(2 * x + 3 * y) % 5] = rotate(a[x][y], ROTATIONS[x][y])
|
||||
a = [[b[x][y] ^ (~b[(x + 1) % 5][y] & b[(x + 2) % 5][y]) for y in range(5)] for x in range(5)]
|
||||
a[0][0] ^= rc
|
||||
return a
|
||||
|
||||
|
||||
# Keccak-256 as Ethereum uses it (padding 0x01, not SHA3-256's 0x06), which labelHash in SimplexName.hs is
|
||||
def keccak256(data):
|
||||
rate = 136
|
||||
padded = bytearray(data) + b"\x01" + bytes(-(len(data) + 1) % rate)
|
||||
padded[-1] |= 0x80
|
||||
a = [[0] * 5 for _ in range(5)]
|
||||
for block in range(0, len(padded), rate):
|
||||
for i in range(rate // 8):
|
||||
a[i % 5][i // 5] ^= int.from_bytes(padded[block + 8 * i:block + 8 * i + 8], "little")
|
||||
a = keccak_f(a)
|
||||
return b"".join(a[i % 5][i // 5].to_bytes(8, "little") for i in range(4))
|
||||
|
||||
|
||||
def query_key(label):
|
||||
return "[" + keccak256(label.lower().encode()).hex() + "].testing"
|
||||
|
||||
|
||||
def record(label, links):
|
||||
return {
|
||||
"name": label + ".testing",
|
||||
"nickname": "",
|
||||
"website": "",
|
||||
"location": "",
|
||||
"simplexContact": links.get("simplexContact", []),
|
||||
"simplexChannel": links.get("simplexChannel", []),
|
||||
"eth": None,
|
||||
"btc": None,
|
||||
"xmr": None,
|
||||
"dot": None,
|
||||
"owner": "",
|
||||
"resolver": "",
|
||||
}
|
||||
|
||||
|
||||
def name_response(query, registry_file):
|
||||
with open(registry_file) as f:
|
||||
registry = json.load(f)
|
||||
for label, links in registry.items():
|
||||
if query in (query_key(label), label.lower() + ".testing"):
|
||||
return {"registration": {"type": "registered", "nameRecord": record(label, links)}}
|
||||
return {"registration": {"type": "available", "pricing": {"registrationPrices": {}, "basePrice": 1000, "minLabelLength": 1}}}
|
||||
|
||||
|
||||
class Resolver(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
prefix = "/v2/resolve/"
|
||||
if self.path == "/health":
|
||||
self.reply(200, {})
|
||||
elif self.path.startswith(prefix):
|
||||
query = unquote(self.path[len(prefix):])
|
||||
response = name_response(query, self.server.registry_file)
|
||||
print(f"resolve {query}: {response['registration']['type']}", flush=True)
|
||||
self.reply(200, response)
|
||||
else:
|
||||
self.reply(404, {})
|
||||
|
||||
def reply(self, status, body):
|
||||
data = json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
|
||||
def main():
|
||||
server = ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), Resolver)
|
||||
server.registry_file = sys.argv[2]
|
||||
print(f"names resolver on 127.0.0.1:{sys.argv[1]}, registry {sys.argv[2]}", flush=True)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user