From 610227feec4809056fcb63ecab412fbce5e63936 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 06:31:16 -0500 Subject: [PATCH 1/7] minor update to reward system UI --- cmd/skywire-cli/commands/log/st.go | 2 +- cmd/skywire-cli/commands/rewards/ui.go | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/skywire-cli/commands/log/st.go b/cmd/skywire-cli/commands/log/st.go index 464c7accf..1b086affd 100644 --- a/cmd/skywire-cli/commands/log/st.go +++ b/cmd/skywire-cli/commands/log/st.go @@ -105,7 +105,7 @@ func makeTree() { var coloredFile string if kid == "proxy" { testtime, _ := script.File(proxyCSV).Match(dirNode).Replace(",", " ").Column(2).String() //nolint - testres, _ := script.File(proxyCSV).Match(dirNode).Replace(",", " ").Column(3).String() //nolint + testres, _ := script.File(proxyCSV).Match(dirNode).Replace(",", " ").Column(3).String() //nolint if testtime != "" && testres != "" { coloredFile = fmt.Sprintf("%s %s %s", pterm.Green("proxy"), strings.ReplaceAll(testtime, "\n", ""), strings.ReplaceAll(testres, "\n", "")) } else { diff --git a/cmd/skywire-cli/commands/rewards/ui.go b/cmd/skywire-cli/commands/rewards/ui.go index 45d4f508b..5fdd7686e 100644 --- a/cmd/skywire-cli/commands/rewards/ui.go +++ b/cmd/skywire-cli/commands/rewards/ui.go @@ -360,8 +360,8 @@ func server() { tpstats, _ := script.Exec("skywire cli tp tree -s").Match("Count of transports:").Replace("Count of transports: ", "").Replace("\n", "").String() //nolint tpcount, _ := strconv.Atoi(tpstats) //nolint if tpcount < 400 { - tpTree, _ := script.Exec("skywire cli tp tree").Bytes() //nolint - c.Writer.Write(ansihtml.ConvertToHTML(tpTree)) //nolint + tpTree, _ := script.Exec("skywire cli tp tree").Bytes() //nolint + c.Writer.Write(ansihtml.ConvertToHTML(tpTree)) //nolint c.Writer.Flush() } else { c.Writer.Write([]byte(fmt.Sprintf("Transport count: %v exceeds server resources to map", tpcount))) //nolint @@ -851,7 +851,9 @@ func server() { c.Writer.Header().Set("Transfer-Encoding", "chunked") _, err := time.Parse("2006-01-02", c.Param("date")) if err != nil { - if strings.Contains(c.Param("date"), "_rewardtxn0.csv") { + _, err1 := time.Parse("2006-01-02", strings.Replace(c.Param("date"), "_rewardtxn0.csv", "", -1)) + _, err2 := time.Parse("2006-01-02", strings.Replace(c.Param("date"), "_stats.txt", "", -1)) + if err1 != nil || err2 != nil { filetoserve, err := script.File("rewards/hist/" + c.Param("date")).Bytes() if err == nil { c.Writer.Header().Set("Content-Type", "text/plain") @@ -1261,7 +1263,7 @@ func serveSyntaxHighlighted(c *gin.Context) { return } c.Status(http.StatusOK) - c.Writer.Write(buf.Bytes()) //nolint + c.Writer.Write(buf.Bytes()) //nolint } type ginHandler struct { From 2bd8eb98c144812f71b1ee53ff3a560ff02a97a2 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 06:40:07 -0500 Subject: [PATCH 2/7] update scripts / add proxy test script --- scripts/rewards/getlogs.sh | 22 +++++++--------------- scripts/rewards/gettps.sh | 24 +++++++++++++++++++----- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/scripts/rewards/getlogs.sh b/scripts/rewards/getlogs.sh index a753e3e7b..4a95b3c93 100755 --- a/scripts/rewards/getlogs.sh +++ b/scripts/rewards/getlogs.sh @@ -1,10 +1,13 @@ #!/usr/bin/bash -timeout 30.0m unbuffer skywire-cli log --minv v1.3.19 -s $(tail -n1 survey-wl.conf) | tee skywire-cli-log.txt +timeout 30.0m unbuffer skywire-cli log --minv v1.3.21 -s $(tail -n1 survey-wl.conf) | tee skywire-cli-log.txt #echo -e "skywire survey and transport log collection $(date)\n\n$(cat skywire-cli-log.txt)\n" echo -e "skywire survey and transport log collection $(date)\n\n$(cat skywire-cli-log.txt)\n" | tee skywire-cli-log0.txt >> /dev/null echo "finished "$(date) | tee -a skywire-cli-log0.txt mv skywire-cli-log0.txt skywire-cli-log.txt +#Delete json files more than 1 week old +find log_backups/*/*.json -type f -mmin +$((168 * 60)) -delete +find log_collecting/*/*.json -type f -mmin +$((168 * 60)) -delete #remove empty files and dirs find log_collecting/*/ -empty -type f -delete && printf "removed empty files... \n" || true find log_collecting/*/ -type f -size 19c -delete && printf "removed files with http 404 errors... \n" || true @@ -22,20 +25,8 @@ find log_collecting/*/$(date +'%Y-%m-%d').csv -type f -print | while read _file find log_collecting/*/$(date --date="yesterday" +'%Y-%m-%d').csv -type f -print | while read _file ; do [[ $(head -n 1 $_file) == *"tp_id,recv,sent,time_stamp"* ]] && sed -i '1d' $_file ; done || true find log_backups/*/$(date +'%Y-%m-%d').csv -type f -print | while read _file ; do [[ $(head -n 1 $_file) == *"tp_id,recv,sent,time_stamp"* ]] && sed -i '1d' $_file ; done || true find log_backups/*/$(date --date="yesterday" +'%Y-%m-%d').csv -type f -print | while read _file ; do [[ $(head -n 1 $_file) == *"tp_id,recv,sent,time_stamp"* ]] && sed -i '1d' $_file ; done || true -grep -l "404 page not found" log_collecting/*/$(date +'%Y-%m-%d').csv | xargs rm -f || true -grep -l "404 page not found" log_collecting/*/$(date --date="yesterday" +'%Y-%m-%d').csv | xargs rm -f || true -grep -l "404 page not found" log_backups/*/$(date +'%Y-%m-%d').csv | xargs rm -f || true -grep -l "404 page not found" log_backups/*/$(date --date="yesterday" +'%Y-%m-%d').csv | xargs rm -f || true - - -#check / decrypt surveys -printf "checking surveys... \n" -find log_collecting/*/node-info.json -type f -print | xargs grep -l "404 page not found" | parallel rm || true -find log_collecting/*/node-info.json -type f -print | xargs grep -l "Not Found" | parallel rm || true -find log_collecting/*/node-info.json -type f -print | xargs grep -l "PGP MESSAGE" | parallel rm || true #xargs -I {} ./decrypt.sh {} #./decrypt.sh -find log_backups/*/node-info.json -type f -print | xargs grep -l "404 page not found" | parallel rm || true -find log_backups/*/node-info.json -type f -print | xargs grep -l "Not Found" | parallel rm || true -find log_backups/*/node-info.json -type f -print | xargs grep -l "PGP MESSAGE" | parallel rm || true #xargs -I {} ./decrypt.sh {} #./decrypt.sh +find log_collecting/*/*.json -type f -print | while read _file; do if ! jq '.' "$_file" >/dev/null 2>&1; then echo "invalid json $_file" ; rm $_file; fi; done +find log_backups/*/*.json -type f -print | while read _file; do if ! jq '.' "$_file" >/dev/null 2>&1; then echo "invalid json $_file" ; rm $_file; fi; done printf "checking tp logs... \n" [[ -f log_collecting/*/$(date +'%Y-%m-%d').csv ]] && find log_collecting/*/$(date +'%Y-%m-%d').csv -type f -print | xargs grep -l "404 page not found" | parallel rm || true @@ -47,6 +38,7 @@ printf "checking tp logs... \n" [[ -f log_collecting/*/$(date +'%Y-%m-%d').csv ]] && find log_backups/*/$(date +'%Y-%m-%d').csv -type f -print | xargs grep -l "Not Found" | parallel rm || true [[ -f log_collecting/*/log_collecting/*/$(date --date="yesterday" +'%Y-%m-%d').csv ]] && find log_backups/*/$(date --date="yesterday" +'%Y-%m-%d').csv -type f -print | xargs grep -l "Not Found" | parallel rm || true + #back up the collected files rsync -r log_collecting/ log_backups || true [[ -f log_backups/*/*~ ]] && rm log_backups/*/*~ || true diff --git a/scripts/rewards/gettps.sh b/scripts/rewards/gettps.sh index b6e8079ea..b3b56f17f 100755 --- a/scripts/rewards/gettps.sh +++ b/scripts/rewards/gettps.sh @@ -1,10 +1,24 @@ #!/usr/bin/bash -[[ ! -d tp_setup ]] && mkdir tp_setup +#refresh the cached uptime tracker data at /tmp/ut.json skywire cli ut -s -#jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"' | while read _pk ; do -printf "%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(find ./log_backups/ -type d -print0 | xargs -0 -n1 basename) | sort | uniq -c | sort -nr | grep "2 " | sed 's/ 2 //g' | tac | while read _pk ; do - mkdir -p tp_setup/${_pk} -skywire svc tps list -1 ${_pk} 2>&1 | tee tp_setup/${_pk}/tp.json +#online status alone is too broad of a qualifier for surveying. But there are sometimes errors with the dmsghttp logserver that wouldn't otherwise cause the transport setup node to fail. Some nodes may be offline though UT shows online and vice versa. +find ./log_backups/ -type d -print0 | xargs -0 -n1 basename | while read _pk ; do +mkdir -p tp_setup/${_pk} done +printf "%s\n%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(find ./log_backups/ -type d -print0 | xargs -0 -n1 basename) $(find log_backups/ -type f -name "health.json" -mmin -60 | cut -d '/' -f2) | sort | uniq -c | sort -nr | grep -v "1 " | sed 's/ [23] //g' | tac | while read _pk ; do +timeout 3.0m parallel -j 25 'skywire svc tps list -1 {} 2>&1 | tee tp_setup/{}/tp.json' +done +skywire cli ut -s +find ./log_backups/ -type d -print0 | xargs -0 -n1 basename | while read _pk ; do +mkdir -p tp_setup/${_pk} +done +printf "%s\n%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(find ./log_backups/ -type d -print0 | xargs -0 -n1 basename) $(find log_backups/ -type f -name "health.json" -mmin -60 | cut -d '/' -f2) | sort | uniq -c | sort -nr | grep -v "1 " | sed 's/ [23] //g' | while read _pk ; do +timeout 3.0m parallel -j 25 'skywire svc tps list -1 {} 2>&1 | tee tp_setup/{}/tp.json' +done +skywire cli ut -s +printf "%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(find tp_setup/ -type f -name "tp.json" -mmin -60 | cut -d '/' -f2) | sort | uniq -c | sort -nr | sed 's/ [12] //g' | while read _pk ; do +timeout 3.0m parallel -j 25 'jq -e . tp_setup/{}/tp.json > /dev/null 2>&1 || skywire svc tps list -1 {} 2>&1 | tee tp_setup/{}/tp.json' +done +echo "cleaning up" find ./tp_setup/ -type f -mtime +2 -delete find ./tp_setup/ -type d -empty -delete From d825311b5a668385531dfd9486d88ed58ba0ae58 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 07:14:42 -0500 Subject: [PATCH 3/7] add / update reward system scripts and systemd services ; update reward system administration documentation --- REWARDS.md | 60 +++++++------------ scripts/rewards/services/fiberreward.service | 17 ++++++ scripts/rewards/services/rewardbot.service | 17 ++++++ .../rewards/services/skywire-reward.service | 13 ++++ scripts/rewards/services/skywire-reward.timer | 10 ++++ scripts/rewards/testproxies.sh | 43 +++++++++++++ 6 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 scripts/rewards/services/fiberreward.service create mode 100644 scripts/rewards/services/rewardbot.service create mode 100644 scripts/rewards/services/skywire-reward.service create mode 100644 scripts/rewards/services/skywire-reward.timer create mode 100755 scripts/rewards/testproxies.sh diff --git a/REWARDS.md b/REWARDS.md index d6521b5fa..4ad0bbf30 100644 --- a/REWARDS.md +++ b/REWARDS.md @@ -35,7 +35,7 @@ It should be noted that the system survey generation requires root for many of i ### Log & Survey Collection -The log collection and [reward processing](#reward-processing) happens hourly. +The log collection and [reward processing](#reward-processing) happens hourly via [skywire-reward.service](/scripts/rewards/services/skywire-reward.service) - triggered to run hourly by [skywire-reward.timer](/scripts/rewards/services/skywire-reward.timer). The log collection run can be viewed here: https://fiber.skywire.dev/log-collection @@ -52,11 +52,16 @@ These `survey_whitelist` keys are specified by the [conf service](https://conf.s The collected surveys are then checked and backed up. -The collection of surveys and the reward calculation happens hourly - as configured by the systemd service and timer +The following scripts are used by the reward system: + +[`getlogs.sh`](/scripts/rewards/getlogs.sh) - a wrapper script for survey and transport bandwidth log collection via `skywire cli log` +[`reward.sh`](/scripts/rewards/reward.sh) - a wrapper script for reward calculation via `skywire cli rewards` +[`gettps.sh`](/scripts/rewards/gettps.sh) - a wrapper script for collecting responses to transport setup-node requests via `skywire svc tps ls` +[`testproxies.sh`](/scripts/rewards/testproxies.sh) - WIP - a wrapper script for testing curl response time over the skywire socks5 proxy (not used for reward calculation) ### Reward Processing -The rewards are calculated by `skywire cli rewards calc` with the aid of [reward.sh](scripts/rewards/reward.sh) to produce the reward distribution data for the previous day's uptime. +The rewards are calculated by `skywire cli rewards calc` with the aid of [`reward.sh`](scripts/rewards/reward.sh) to produce the reward distribution data for the previous day's uptime. ### Per-IP reward limit @@ -70,53 +75,27 @@ To avoid a user running multiple instances of skywire on virtual machines, the M ## Automation via systemd service -Automation of the hourly log & survey collection is accomplished via systemd service and timer executing [getlogs.sh](scripts/rewards/getlogs.sh) and [reward.sh](scripts/rewards/reward.sh) - -/etc/systemd/system/skywire-reward.service -``` -[Unit] -Description=skywire reward service -After=network.target - -[Service] -Type=simple -User=user -Group=user -WorkingDirectory=/path/to/github.com/reward/rewards -ExecStart=/usr/bin/bash -c './getlogs.sh && ./reward.sh' - -[Install] -WantedBy=multi-user.target +Automation of the hourly log & survey collection is accomplished via systemd service and timer -``` +/etc/systemd/system/[`skywire-reward.service`](/scripts/rewards/services/skywire-reward.service) **Note: change the user and working directory in the above systemd service** -This service is called by a timer +This service is called by a timer which triggers it to run hourly -/etc/systemd/system/skywire-reward.timer -``` -[Unit] -Description=skywire reward timer -After=network.target - -[Timer] -OnUnitActiveSec=1h -Unit=skywire-reward.service - -[Install] -WantedBy=multi-user.target - -``` +/etc/systemd/system/[`skywire-reward.timer`](/scripts/rewards/services/skywire-reward.timer). ## fiber.skywire.dev -The 'frontend' of the reward system, is currently running at [fiber.skywire.dev](https://fiber.skywire.dev) and is reliant upon on the output of certain cli commands and some scripts +The 'frontend' of the reward system, is currently running at [fiber.skywire.dev](https://fiber.skywire.dev) and is reliant upon on the output of certain cli commands ~~and some scripts~~ [`skywire cli rewards ui`](cmd/skywire-cli/commands/rewards/ui.go) serves the reward system frontend or user interface - via http and dmsghttp. -A wrapper script [`scripts/getlogs.sh`](scripts/getlogs.sh) is used to redirect the output of `skywire cli log` to a file, which is displayed at: +The service which runs the reward system UI: +/etc/systemd/system/[`fiberreward.service`](/scripts/rewards/services/fiberreward.service) + +A wrapper script [`getlogs.sh`](scripts/rewards/getlogs.sh) is used to redirect the output of `skywire cli log` to a file, which is displayed at: https://fiber.skywire.dev/log-collection @@ -126,7 +105,7 @@ Here shows links to the reward calculations and distribution data by day: https://fiber.skywire.dev/skycoin-rewards -on each linked page, the distribution data is displayed, and a link to the explorer for that transaction if it was broadcast. Also displayed are the public keys and their reward shares, or the reason why they were not rewarded +on each linked page, the distribution data is displayed with a link to the explorer for that transaction if it was broadcast. Also displayed are the public keys and their reward shares, or the reason why they were not rewarded The frontend may be run either with flags or by using a conf file such as the following: fr.conf @@ -166,5 +145,6 @@ REWARD_SYS_URL="dmsg://:80" before the script is run and the transaction is attempted to be broadcast, it's crucial to check that the hourly [log collection and reward calculation](https://fiber.skywire.dev/log-collection) is not ongoing. +### Reward Notifications -When the transaction is then broadcast, it's transaction ID recorded in a file which is monitored by the reward telegram bot, which generates a notification in https://t.me/skywire_reward. **Note: this will eventually be supplemented with or replaced by a notification via skychat.** +When the transaction is broadcast by the reward system, it's transaction ID is recorded by appending a file which is monitored by the reward telegram bot. The telegram bot will then generate a notification in https://t.me/skywire_reward when a change to that file is detected. **Note: this will eventually be supplemented with or replaced by a notification via skychat.** diff --git a/scripts/rewards/services/fiberreward.service b/scripts/rewards/services/fiberreward.service new file mode 100644 index 000000000..3c96183d3 --- /dev/null +++ b/scripts/rewards/services/fiberreward.service @@ -0,0 +1,17 @@ +[Unit] +Description=skywire cli rewards ui +After=network.target network-online.target + +[Service] +Type=simple +User=youruser +Group=youruser +WorkingDirectory=/path/to/reward +Environment='SKYENV=fr.conf' +ExecStart=/usr/bin/bash -c 'skywire cli rewards ui' +Restart=always +RestartSec=20 +TimeoutSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/rewards/services/rewardbot.service b/scripts/rewards/services/rewardbot.service new file mode 100644 index 000000000..358882f3d --- /dev/null +++ b/scripts/rewards/services/rewardbot.service @@ -0,0 +1,17 @@ +[Unit] +Description=reward notification bot +After=network.target network-online.target + +[Service] +Type=simple +User=youruser +Group=youruser +Environment=TG_BOT_TOKEN="" TG_CHAT_ID="-1001751553086" +WorkingDirectory=/path/to/reward-tgbot +ExecStart=/usr/bin/go run main.go +Restart=on-failure +RestartSec=20 +TimeoutSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/rewards/services/skywire-reward.service b/scripts/rewards/services/skywire-reward.service new file mode 100644 index 000000000..b5f1fbbab --- /dev/null +++ b/scripts/rewards/services/skywire-reward.service @@ -0,0 +1,13 @@ +[Unit] +Description=skywire reward service +After=network.target + +[Service] +Type=simple +User=youruser +Group=youruser +WorkingDirectory=/path/to/reward/rewards +ExecStart=/bin/bash -c '(unbuffer skywire svc tps -l debug -c /etc/tpsn.json | tee transport-setup-node.txt) & (./getlogs.sh && ./gettps.sh && ./reward.sh ; exit 0)' + +[Install] +WantedBy=multi-user.target diff --git a/scripts/rewards/services/skywire-reward.timer b/scripts/rewards/services/skywire-reward.timer new file mode 100644 index 000000000..024ea6767 --- /dev/null +++ b/scripts/rewards/services/skywire-reward.timer @@ -0,0 +1,10 @@ +[Unit] +Description=skywire reward timer +After=network.target + +[Timer] +OnUnitActiveSec=1h +Unit=skywire-reward.service + +[Install] +WantedBy=multi-user.target diff --git a/scripts/rewards/testproxies.sh b/scripts/rewards/testproxies.sh new file mode 100755 index 000000000..e077d5730 --- /dev/null +++ b/scripts/rewards/testproxies.sh @@ -0,0 +1,43 @@ +#!/bin/bash +_testpk="$(skywire cli visor pk || exit 1)" +skywire-cli proxy stop --all > /dev/null ; skywire cli tp rm -a || exit 1 +skywire cli ut -s > /dev/null +[[ -f /tmp/proxy1234567.json ]] && rm /tmp/proxy1234567.json > /dev/null +#echo '00pk, time, time_namelookup, time_connect, time_appconnect, time_pretransfer, time_redirect, time_starttransfer, time_total, ip_address, latitude, longitude, postal_code, continent_code, country_code, country_name, region_code, region_name, province_code, province_name, city_name, timezone' | tee proxy_test/proxies-tmp.csv +echo '00pk, time_now, time_total' | tee proxy_test/proxies-tmp.csv +awk -F',' 'NF == 3 {print $0}' proxy_test/proxies.csv | grep -v "00pk" | cut -d "," -f1 | sort | uniq -c | sort -nr | sed 's/ [0-9]\+ //g' | sort | tac | while read _pk ; do grep -m1 "$_pk" proxy_test/proxies.csv | tee -a proxy_test/proxies-tmp.csv ; done ; mv proxy_test/proxies-tmp.csv proxy_test/proxies.csv +#_res="$(curl -o /tmp/proxy123456.json -w "%{time_namelookup}s, %{time_connect}s, %{time_appconnect}s, %{time_pretransfer}s, %{time_redirect}s, %{time_starttransfer}s, %{time_total}s" -sL http://ip.skycoin.com/ || exit 1)" +#[[ "$(grep -m1 "027087fe40d97f7f0be4a0dc768462ddbb371d4b9e7679d4f11f117d757b9856ed" proxy_test/proxies.csv)" == "" ]] && echo "027087fe40d97f7f0be4a0dc768462ddbb371d4b9e7679d4f11f117d757b9856ed,$(date "+%D_%T"),${_res//$'\n'/},$(jq -r '[.ip_address, .latitude, .longitude, .postal_code, .continent_code, .country_code, .country_name, .region_code, .region_name, .province_code, .province_name, .city_name, .timezone] | @csv' /tmp/proxy123456.json | tr -d '"')" | tee -a proxy_test/proxies.csv +_res="$(curl -o /tmp/proxy123456.json -w "%{time_total}s" -sL http://ip.skycoin.com/ || exit 1)" +[[ "$(grep -m1 "N/A" proxy_test/proxies.csv)" == "" ]] && echo "N/A,$(date "+%D_%T"),${_res//$'\n'/}" | tee -a proxy_test/proxies.csv +[[ -f /tmp/proxy1234567.json ]] && rm /tmp/proxy1234567.json > /dev/null || true +#skywire cli tp add -t dmsg $_testpk || exit 1 +#timeout 11s skywire cli proxy start -k $_testpk -t 10 || exit 1 +#[[ $(skywire cli proxy status | grep -m1 "Status:" ) != "Status: running" ]] && exit 1 +# _res="$(timeout 10s curl -o /tmp/proxy123456.json -w "%{time_namelookup}s, %{time_connect}s, %{time_appconnect}s, %{time_pretransfer}s, %{time_redirect}s, %{time_starttransfer}s, %{time_total}s" -sLx socks5h://127.0.0.1:1080 http://ip.skycoin.com/)" && +# echo "$_pk,$(date "+%D_%T"),${_res//$'\n'/},$(jq -r '[.ip_address, .latitude, .longitude, .postal_code, .continent_code, .country_code, .country_name, .region_code, .region_name, .province_code, .province_name, .city_name, .timezone] | @csv' /tmp/proxy123456.json | tr -d '"')" | tee -a proxy_test/proxies.csv +#_res="$(timeout 10s curl -o /tmp/proxy123456.json -w "%{time_total}s" -sLx socks5h://127.0.0.1:1080 http://haltingstate.net/204)" && +#echo "self_transport,$(date "+%D_%T"),${_res//$'\n'/}" | tee -a proxy_test/proxies.csv +#skywire-cli proxy stop --all > /dev/null ; skywire cli tp rm -a || exit 1 +printf "%s\n%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(skywire cli proxy list -v v1.3.21) $(find log_backups/ -type f -name "health.json" | cut -d '/' -f2) | sort | uniq -c | sort -nr | grep "3 " | sed 's/ 3 //g' | grep -v "${_testpk/"\n"/}" | tac > proxy_test/list.txt +cat proxy_test/list.txt | grep -vFf <(tail -n+2 proxy_test/proxies.csv | awk -F',' 'NF == 22 {print $1}') | shuf -n 25 | sort | while read _pk ; do + timeout 30s parallel -j 4 "skywire svc tps add -z http://127.0.0.1:8078 -t dmsg -1 {} -2 ${_testpk}" +done +skywire cli tp | tr -s " " | cut -d " " -f3 | sort | while read _pk ; do +( + skywire cli proxy stop --all > /dev/null || true + [[ -f /tmp/proxy1234567.json ]] && rm /tmp/proxy1234567.json > /dev/null || true + [[ $(skywire cli tp | grep "$_pk" ) != *"$_pk"* ]] && echo "pk not found in transports $_pk" && exit #continue + timeout 11s skywire cli proxy start -k $_pk -t 10 || exit # continue + [[ $(skywire cli proxy status | grep -m1 "Status:" ) != "Status: running" ]] && exit # continue +# _res="$(timeout 10s curl -o /tmp/proxy123456.json -w "%{time_namelookup}s, %{time_connect}s, %{time_appconnect}s, %{time_pretransfer}s, %{time_redirect}s, %{time_starttransfer}s, %{time_total}s" -sLx socks5h://127.0.0.1:1080 http://ip.skycoin.com/)" && +# echo "$_pk,$(date "+%D_%T"),${_res//$'\n'/},$(jq -r '[.ip_address, .latitude, .longitude, .postal_code, .continent_code, .country_code, .country_name, .region_code, .region_name, .province_code, .province_name, .city_name, .timezone] | @csv' /tmp/proxy123456.json | tr -d '"')" | tee -a proxy_test/proxies.csv + _res="$(timeout 10s curl -o /tmp/proxy123456.json -w "%{time_total}s" -sLx socks5h://127.0.0.1:1080 http://haltingstate.net/204)" && + echo "$_pk,$(date "+%D_%T"),${_res//$'\n'/}" | tee -a proxy_test/proxies.csv +) & _pid="$!" +( sleep 25 ; [[ $(kill -0 "${_pid}" 2>/dev/null) ]] && kill "${_pid}" || true ) & +wait "${_pid}" +done +skywire cli ut -s > /dev/null +printf "%s\n%s\n%s\n" $(jq '.[] | select(.version=="v1.3.21" and .on==true ) | .pk' /tmp/ut.json | tr -d '"') $(skywire cli proxy list -v v1.3.21) $(find log_backups/ -type f -name "health.json" | cut -d '/' -f2) | sort | uniq -c | sort -nr | grep "3 " | sed 's/ 3 //g' | grep -v "${_testpk}" | tac > proxy_test/list.txt +[[ $(cat proxy_test/list.txt | grep -vFf <(tail -n+2 proxy_test/proxies.csv | awk -F',' 'NF == 3 {print $1}') | wc -l) -gt 0 ]] && ./testproxies.sh From 70677713c4eba7735729bea7d930b65852edb392 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 07:39:46 -0500 Subject: [PATCH 4/7] add telegram reward notification bot --- cmd/skywire-cli/commands/rewards/tgbot.go | 117 ++ go.mod | 1 + go.sum | 640 +++++++++- vendor/gopkg.in/telebot.v3/.gitignore | 34 + vendor/gopkg.in/telebot.v3/LICENSE | 22 + vendor/gopkg.in/telebot.v3/README.md | 489 ++++++++ vendor/gopkg.in/telebot.v3/admin.go | 313 +++++ vendor/gopkg.in/telebot.v3/api.go | 330 ++++++ vendor/gopkg.in/telebot.v3/bot.go | 1153 +++++++++++++++++++ vendor/gopkg.in/telebot.v3/callback.go | 89 ++ vendor/gopkg.in/telebot.v3/chat.go | 467 ++++++++ vendor/gopkg.in/telebot.v3/commands.go | 85 ++ vendor/gopkg.in/telebot.v3/context.go | 505 ++++++++ vendor/gopkg.in/telebot.v3/editable.go | 30 + vendor/gopkg.in/telebot.v3/errors.go | 260 +++++ vendor/gopkg.in/telebot.v3/file.go | 87 ++ vendor/gopkg.in/telebot.v3/game.go | 99 ++ vendor/gopkg.in/telebot.v3/inline.go | 139 +++ vendor/gopkg.in/telebot.v3/inline_types.go | 373 ++++++ vendor/gopkg.in/telebot.v3/input_types.go | 73 ++ vendor/gopkg.in/telebot.v3/markup.go | 365 ++++++ vendor/gopkg.in/telebot.v3/media.go | 358 ++++++ vendor/gopkg.in/telebot.v3/message.go | 463 ++++++++ vendor/gopkg.in/telebot.v3/middleware.go | 38 + vendor/gopkg.in/telebot.v3/options.go | 226 ++++ vendor/gopkg.in/telebot.v3/payments.go | 188 +++ vendor/gopkg.in/telebot.v3/payments_data.go | 14 + vendor/gopkg.in/telebot.v3/poll.go | 75 ++ vendor/gopkg.in/telebot.v3/poller.go | 115 ++ vendor/gopkg.in/telebot.v3/sendable.go | 407 +++++++ vendor/gopkg.in/telebot.v3/stickers.go | 212 ++++ vendor/gopkg.in/telebot.v3/telebot.go | 150 +++ vendor/gopkg.in/telebot.v3/topic.go | 172 +++ vendor/gopkg.in/telebot.v3/update.go | 375 ++++++ vendor/gopkg.in/telebot.v3/video_chat.go | 31 + vendor/gopkg.in/telebot.v3/web_app.go | 24 + vendor/gopkg.in/telebot.v3/webhook.go | 207 ++++ vendor/modules.txt | 3 + 38 files changed, 8728 insertions(+), 1 deletion(-) create mode 100644 cmd/skywire-cli/commands/rewards/tgbot.go create mode 100644 vendor/gopkg.in/telebot.v3/.gitignore create mode 100644 vendor/gopkg.in/telebot.v3/LICENSE create mode 100644 vendor/gopkg.in/telebot.v3/README.md create mode 100644 vendor/gopkg.in/telebot.v3/admin.go create mode 100644 vendor/gopkg.in/telebot.v3/api.go create mode 100644 vendor/gopkg.in/telebot.v3/bot.go create mode 100644 vendor/gopkg.in/telebot.v3/callback.go create mode 100644 vendor/gopkg.in/telebot.v3/chat.go create mode 100644 vendor/gopkg.in/telebot.v3/commands.go create mode 100644 vendor/gopkg.in/telebot.v3/context.go create mode 100644 vendor/gopkg.in/telebot.v3/editable.go create mode 100644 vendor/gopkg.in/telebot.v3/errors.go create mode 100644 vendor/gopkg.in/telebot.v3/file.go create mode 100644 vendor/gopkg.in/telebot.v3/game.go create mode 100644 vendor/gopkg.in/telebot.v3/inline.go create mode 100644 vendor/gopkg.in/telebot.v3/inline_types.go create mode 100644 vendor/gopkg.in/telebot.v3/input_types.go create mode 100644 vendor/gopkg.in/telebot.v3/markup.go create mode 100644 vendor/gopkg.in/telebot.v3/media.go create mode 100644 vendor/gopkg.in/telebot.v3/message.go create mode 100644 vendor/gopkg.in/telebot.v3/middleware.go create mode 100644 vendor/gopkg.in/telebot.v3/options.go create mode 100644 vendor/gopkg.in/telebot.v3/payments.go create mode 100644 vendor/gopkg.in/telebot.v3/payments_data.go create mode 100644 vendor/gopkg.in/telebot.v3/poll.go create mode 100644 vendor/gopkg.in/telebot.v3/poller.go create mode 100644 vendor/gopkg.in/telebot.v3/sendable.go create mode 100644 vendor/gopkg.in/telebot.v3/stickers.go create mode 100644 vendor/gopkg.in/telebot.v3/telebot.go create mode 100644 vendor/gopkg.in/telebot.v3/topic.go create mode 100644 vendor/gopkg.in/telebot.v3/update.go create mode 100644 vendor/gopkg.in/telebot.v3/video_chat.go create mode 100644 vendor/gopkg.in/telebot.v3/web_app.go create mode 100644 vendor/gopkg.in/telebot.v3/webhook.go diff --git a/cmd/skywire-cli/commands/rewards/tgbot.go b/cmd/skywire-cli/commands/rewards/tgbot.go new file mode 100644 index 000000000..ea698665f --- /dev/null +++ b/cmd/skywire-cli/commands/rewards/tgbot.go @@ -0,0 +1,117 @@ +// Package clirewards cmd/skywire-cli/commands/rewards/tgbot.go +package clirewards + +import ( + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/bitfield/script" + "github.com/spf13/cobra" + tele "gopkg.in/telebot.v3" +) + +var filePath string + +func init() { + RootCmd.AddCommand( + tgbotCmd, + ) + tgbotCmd.Flags().StringVarP(&filePath, "watch", "w", "../reward/rewards/transactions0.txt", "File to watch - file where reward transaction IDs are recorded") + +} + +var tgbotCmd = &cobra.Command{ + Use: "bot", + Short: "reward notification telegram bot", + Long: "reward notification telegram bot", + Run: func(_ *cobra.Command, _ []string) { + chatIDStr := os.Getenv("TG_CHAT_ID") + chatID, err := strconv.ParseInt(chatIDStr, 10, 64) + if err != nil { + log.Fatalf("failed to parse chat ID: %v", err) + } + pref := tele.Settings{ + Token: os.Getenv("TG_BOT_TOKEN"), + Poller: &tele.LongPoller{Timeout: 10 * time.Second}, + } + + b, err := tele.NewBot(pref) + if err != nil { + log.Fatal(err) + return + } + + tgbotscript := `#!/bin/bash + _stats() { + # [[ $1 == "" ]] && return + cat ` + strings.TrimSuffix(filePath, "/transactions0.txt") + `/hist/$(find ` + strings.TrimSuffix(filePath, "/transactions0.txt") + `/hist/ -name "*.txt" -type f -exec grep -l "$1" {} + | xargs -I{} basename {} | tr -d ".txt")_stats.txt + } + ` + + // var lastModTime time.Time + lastModTime, err := os.Stat(filePath) + if err != nil { + log.Fatal(err) + return + } + // Use a goroutine to periodically check the file for changes + go func() { + for { + time.Sleep(2 * time.Second) + + fileInfo, err := os.Stat(filePath) + if err != nil { + log.Printf("Error checking file info: %s", err) + continue + } + + if fileInfo.ModTime().After(lastModTime.ModTime()) { + // The file has been modified since the last check, get the last line which is the most recent txid + lastLine, err := script.File(filePath).Last(1).String() + if err != nil { + log.Printf("Error getting last line of file: %v", err) + continue + } + if lastLine != "" { + tmpFile, err := os.CreateTemp(os.TempDir(), "*.sh") + if err != nil { + return + } + if err := tmpFile.Close(); err != nil { + return + } + _, _ = script.Exec(`chmod +x ` + tmpFile.Name()).String() //nolint + _, _ = script.Echo(tgbotscript).WriteFile(tmpFile.Name()) //nolint + stats, err := script.Exec(`bash -c 'source ` + tmpFile.Name() + ` ; _stats ` + lastLine + `'`).String() //nolint + if err != nil { + log.Printf("Error getting statistics: %v", err) + continue + } + os.Remove(tmpFile.Name()) //nolint + + dateforlink, err := script.Echo(stats).First(1).Replace("date: ", "").String() + if err != nil { + log.Printf("Error getting date for link: %v", err) + continue + } + msg := fmt.Sprintf("Rewards have been distributed!\n\nhttps://explorer.skycoin.com/app/transaction/%s\n\n%s\n\nhttps://fiber.skywire.dev/skycoin-rewards/hist/%s", lastLine, stats, dateforlink) + // Send the last line to the Telegram chat + _, err = b.Send(&tele.Chat{ID: chatID}, msg) + if err != nil { + log.Printf("Error sending message to Telegram chat: %s", err) + continue + } + } + + lastModTime = fileInfo + } + } + }() + + b.Start() + }, +} diff --git a/go.mod b/go.mod index 8ac0de506..ce20465f4 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( golang.org/x/sync v0.3.0 golang.org/x/sys v0.17.0 golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 + gopkg.in/telebot.v3 v3.2.1 ) require ( diff --git a/go.sum b/go.sum index 2a043cc7c..096ff966b 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,61 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/ActiveState/termtest/conpty v0.5.0 h1:JLUe6YDs4Jw4xNPCU+8VwTpniYOGeKzQg4SM2YHQNA8= github.com/ActiveState/termtest/conpty v0.5.0/go.mod h1:LO4208FLsxw6DcNZ1UtuGUMW+ga9PFtX4ntv8Ymg9og= github.com/AudriusButkevicius/pfilter v0.0.11 h1:6emuvqNeH1gGlqkML35pEizyPcaxdAN4JO9sdgwcx78= @@ -17,6 +72,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -41,14 +98,29 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anatol/smart.go v0.0.0-20220917195147-c0b00d90f8cc h1:UH+K+oojIu7jWqJrmPXCb33A/ZIfCLBIorj3KQGJxgs= github.com/anatol/smart.go v0.0.0-20220917195147-c0b00d90f8cc/go.mod h1:H/rz4ePNwdNiEdxv+NRWuqONKHe2N5n7rCQftsmStNE= github.com/anatol/vmtest v0.0.0-20220413190228-7a42f1f6d7b8 h1:t4JGeY9oaF5LB4Rdx9e2wARRRPAYt8Ow4eCf5SwO3fA= github.com/anatol/vmtest v0.0.0-20220413190228-7a42f1f6d7b8/go.mod h1:oPm5wWoqTSkeoPe1Q3sPryTK8o24Jcbwh8dKOiiIobk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitfield/script v0.22.1 h1:DphxoC5ssYciwd0ZS+N0Xae46geAD/0mVWh6a2NUxM4= github.com/bitfield/script v0.22.1/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -71,12 +143,25 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:Yg2hDs4b13Evkpj42FU2idX2cVXVFqQSheXYKM86Qsk= github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:MgJyK38wkzZbiZSKeIeFankxxSA8gayko/nr5x5bgBA= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/confiant-inc/go-socks5 v0.0.0-20210816151940-c1124825b1d6 h1:sRQemCQ+r6Ht7uIT0D9Xcyjed4lKpDhNKarBEPFZp3c= @@ -86,12 +171,15 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -120,15 +208,26 @@ github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d h1:dHYKX8CBAs1zSGXm3q3M15CLAEwPEkwrK1ed8FCo+Xo= @@ -148,7 +247,14 @@ github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -166,6 +272,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= @@ -188,23 +295,36 @@ github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04v github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -215,29 +335,66 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= @@ -248,12 +405,43 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -328,9 +516,17 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -349,7 +545,12 @@ github.com/klauspost/reedsolomon v1.11.8/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -369,13 +570,22 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -386,21 +596,32 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= @@ -421,17 +642,42 @@ github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/orandin/lumberjackrus v1.0.1 h1:7ysDQ0MHD79zIFN9/EiDHjUcgopNi5ehtxFDy8rUkWo= github.com/orandin/lumberjackrus v1.0.1/go.mod h1:xYLt6H8W93pKnQgUQaxsApS0Eb4BwHLOkxk5DVzf5H0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -448,8 +694,10 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc= github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= @@ -459,7 +707,10 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= @@ -469,8 +720,10 @@ github.com/shirou/gopsutil v3.20.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -494,18 +747,22 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -520,11 +777,13 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= @@ -539,6 +798,7 @@ github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef h1:7D6Nm4D6f0ci9yttWaKjM1T github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef/go.mod h1:WLFStEdnJXpjK8kd4qKLwQKX/1vrDzp5BcDyiZJBHJM= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -562,59 +822,107 @@ github.com/xxxserxxx/gotop/v4 v4.2.0 h1:M6fiSCx666qEVuKOYrHjvsLm+eq7jKpWLe0joJYQ github.com/xxxserxxx/gotop/v4 v4.2.0/go.mod h1:CuZB7ftL/ye6p34q0Yq+LifoW2KLY6soj0YrNRO0olk= github.com/xxxserxxx/lingo/v2 v2.0.1 h1:6uLLKzPqL0XpdFmNMmpSfu+uIzQk344ebfdpFWbGuxs= github.com/xxxserxxx/lingo/v2 v2.0.1/go.mod h1:Hr6LTxpwirwJ2Qe83MvgSQARPFDzZ4S6DKd6ciuED7A= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zcalusic/sysinfo v1.0.1 h1:cVh8q3codjh43AGRTa54dJ2Zq+qPejv8n2VWpxKViwc= github.com/zcalusic/sysinfo v1.0.1/go.mod h1:LxwKwtQdbTIQc65drhjQzYzt0o7jfB80LrrZm7SWn8o= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -623,69 +931,187 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -706,16 +1132,21 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -723,16 +1154,62 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= @@ -744,23 +1221,167 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU= golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -769,11 +1390,15 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -782,14 +1407,19 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= +gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -804,8 +1434,12 @@ gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= @@ -814,4 +1448,8 @@ mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/vendor/gopkg.in/telebot.v3/.gitignore b/vendor/gopkg.in/telebot.v3/.gitignore new file mode 100644 index 000000000..c81da31db --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/.gitignore @@ -0,0 +1,34 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea +.DS_Store +coverage.txt + +# Terraform artifacts +*.zip +.terraform* +terraform* +/examples/awslambdaechobot/awslambdaechobot diff --git a/vendor/gopkg.in/telebot.v3/LICENSE b/vendor/gopkg.in/telebot.v3/LICENSE new file mode 100644 index 000000000..2965b8423 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 llya Kowalewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/gopkg.in/telebot.v3/README.md b/vendor/gopkg.in/telebot.v3/README.md new file mode 100644 index 000000000..060a70d0d --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/README.md @@ -0,0 +1,489 @@ +# Telebot +>"I never knew creating Telegram bots could be so _sexy_!" + +[![GoDoc](https://godoc.org/gopkg.in/telebot.v3?status.svg)](https://godoc.org/gopkg.in/telebot.v3) +[![GitHub Actions](https://github.com/tucnak/telebot/actions/workflows/go.yml/badge.svg)](https://github.com/tucnak/telebot/actions) +[![codecov.io](https://codecov.io/gh/tucnak/telebot/coverage.svg?branch=v3)](https://codecov.io/gh/tucnak/telebot) +[![Discuss on Telegram](https://img.shields.io/badge/telegram-discuss-0088cc.svg)](https://t.me/go_telebot) + +```bash +go get -u gopkg.in/telebot.v3 +``` + +* [Overview](#overview) +* [Getting Started](#getting-started) + - [Context](#context) + - [Middleware](#middleware) + - [Poller](#poller) + - [Commands](#commands) + - [Files](#files) + - [Sendable](#sendable) + - [Editable](#editable) + - [Keyboards](#keyboards) + - [Inline mode](#inline-mode) +* [Contributing](#contributing) +* [Donate](#donate) +* [License](#license) + +# Overview +Telebot is a bot framework for [Telegram Bot API](https://core.telegram.org/bots/api). +This package provides the best of its kind API for command routing, inline query requests and keyboards, as well +as callbacks. Actually, I went a couple steps further, so instead of making a 1:1 API wrapper I chose to focus on +the beauty of API and performance. Some strong sides of Telebot are: + +* Real concise API +* Command routing +* Middleware +* Transparent File API +* Effortless bot callbacks + +All the methods of Telebot API are _extremely_ easy to memorize and get used to. Also, consider Telebot a +highload-ready solution. I'll test and benchmark the most popular actions and if necessary, optimize +against them without sacrificing API quality. + +# Getting Started +Let's take a look at the minimal Telebot setup: + +```go +package main + +import ( + "log" + "os" + "time" + + tele "gopkg.in/telebot.v3" +) + +func main() { + pref := tele.Settings{ + Token: os.Getenv("TOKEN"), + Poller: &tele.LongPoller{Timeout: 10 * time.Second}, + } + + b, err := tele.NewBot(pref) + if err != nil { + log.Fatal(err) + return + } + + b.Handle("/hello", func(c tele.Context) error { + return c.Send("Hello!") + }) + + b.Start() +} + +``` + +Simple, innit? Telebot's routing system takes care of delivering updates +to their endpoints, so in order to get to handle any meaningful event, +all you got to do is just plug your function into one of the Telebot-provided +endpoints. You can find the full list +[here](https://godoc.org/gopkg.in/telebot.v3#pkg-constants). + +There are dozens of supported endpoints (see package consts). Let me know +if you'd like to see some endpoint or endpoint ideas implemented. This system +is completely extensible, so I can introduce them without breaking +backwards compatibility. + +## Context +Context is a special type that wraps a huge update structure and represents +the context of the current event. It provides several helpers, which allow +getting, for example, the chat that this update had been sent in, no matter +what kind of update this is. + +```go +b.Handle(tele.OnText, func(c tele.Context) error { + // All the text messages that weren't + // captured by existing handlers. + + var ( + user = c.Sender() + text = c.Text() + ) + + // Use full-fledged bot's functions + // only if you need a result: + msg, err := b.Send(user, text) + if err != nil { + return err + } + + // Instead, prefer a context short-hand: + return c.Send(text) +}) + +b.Handle(tele.OnChannelPost, func(c tele.Context) error { + // Channel posts only. + msg := c.Message() +}) + +b.Handle(tele.OnPhoto, func(c tele.Context) error { + // Photos only. + photo := c.Message().Photo +}) + +b.Handle(tele.OnQuery, func(c tele.Context) error { + // Incoming inline queries. + return c.Answer(...) +}) +``` + +## Middleware +Telebot has a simple and recognizable way to set up middleware — chained functions with access to `Context`, called before the handler execution. + +Import a `middleware` package to get some basic out-of-box middleware +implementations: +```go +import "gopkg.in/telebot.v3/middleware" +``` + +```go +// Global-scoped middleware: +b.Use(middleware.Logger()) +b.Use(middleware.AutoRespond()) + +// Group-scoped middleware: +adminOnly := b.Group() +adminOnly.Use(middleware.Whitelist(adminIDs...)) +adminOnly.Handle("/ban", onBan) +adminOnly.Handle("/kick", onKick) + +// Handler-scoped middleware: +b.Handle(tele.OnText, onText, middleware.IgnoreVia()) +``` + +Custom middleware example: +```go +// AutoResponder automatically responds to every callback update. +func AutoResponder(next tele.HandlerFunc) tele.HandlerFunc { + return func(c tele.Context) error { + if c.Callback() != nil { + defer c.Respond() + } + return next(c) // continue execution chain + } +} +``` + +## Poller +Telebot doesn't really care how you provide it with incoming updates, as long +as you set it up with a Poller, or call ProcessUpdate for each update: + +```go +// Poller is a provider of Updates. +// +// All pollers must implement Poll(), which accepts bot +// pointer and subscription channel and start polling +// synchronously straight away. +type Poller interface { + // Poll is supposed to take the bot object + // subscription channel and start polling + // for Updates immediately. + // + // Poller must listen for stop constantly and close + // it as soon as it's done polling. + Poll(b *Bot, updates chan Update, stop chan struct{}) +} +``` + +## Commands +When handling commands, Telebot supports both direct (`/command`) and group-like +syntax (`/command@botname`) and will never deliver messages addressed to some +other bot, even if [privacy mode](https://core.telegram.org/bots#privacy-mode) is off. + +For simplified deep-linking, Telebot also extracts payload: +```go +// Command: /start +b.Handle("/start", func(c tele.Context) error { + fmt.Println(c.Message().Payload) // +}) +``` + +For multiple arguments use: +```go +// Command: /tags <...> +b.Handle("/tags", func(c tele.Context) error { + tags := c.Args() // list of arguments splitted by a space + for _, tag := range tags { + // iterate through passed arguments + } +}) +``` + +## Files +>Telegram allows files up to 50 MB in size. + +Telebot allows to both upload (from disk or by URL) and download (from Telegram) +files in bot's scope. Also, sending any kind of media with a File created +from disk will upload the file to Telegram automatically: +```go +a := &tele.Audio{File: tele.FromDisk("file.ogg")} + +fmt.Println(a.OnDisk()) // true +fmt.Println(a.InCloud()) // false + +// Will upload the file from disk and send it to the recipient +b.Send(recipient, a) + +// Next time you'll be sending this very *Audio, Telebot won't +// re-upload the same file but rather utilize its Telegram FileID +b.Send(otherRecipient, a) + +fmt.Println(a.OnDisk()) // true +fmt.Println(a.InCloud()) // true +fmt.Println(a.FileID) // +``` + +You might want to save certain `File`s in order to avoid re-uploading. Feel free +to marshal them into whatever format, `File` only contain public fields, so no +data will ever be lost. + +## Sendable +Send is undoubtedly the most important method in Telebot. `Send()` accepts a +`Recipient` (could be user, group or a channel) and a `Sendable`. Other types other than +the Telebot-provided media types (`Photo`, `Audio`, `Video`, etc.) are `Sendable`. +If you create composite types of your own, and they satisfy the `Sendable` interface, +Telebot will be able to send them out. + +```go +// Sendable is any object that can send itself. +// +// This is pretty cool, since it lets bots implement +// custom Sendables for complex kinds of media or +// chat objects spanning across multiple messages. +type Sendable interface { + Send(*Bot, Recipient, *SendOptions) (*Message, error) +} +``` + +The only type at the time that doesn't fit `Send()` is `Album` and there is a reason +for that. Albums were added not so long ago, so they are slightly quirky for backwards +compatibilities sake. In fact, an `Album` can be sent, but never received. Instead, +Telegram returns a `[]Message`, one for each media object in the album: +```go +p := &tele.Photo{File: tele.FromDisk("chicken.jpg")} +v := &tele.Video{File: tele.FromURL("http://video.mp4")} + +msgs, err := b.SendAlbum(user, tele.Album{p, v}) +``` + +### Send options +Send options are objects and flags you can pass to `Send()`, `Edit()` and friends +as optional arguments (following the recipient and the text/media). The most +important one is called `SendOptions`, it lets you control _all_ the properties of +the message supported by Telegram. The only drawback is that it's rather +inconvenient to use at times, so `Send()` supports multiple shorthands: +```go +// regular send options +b.Send(user, "text", &tele.SendOptions{ + // ... +}) + +// ReplyMarkup is a part of SendOptions, +// but often it's the only option you need +b.Send(user, "text", &tele.ReplyMarkup{ + // ... +}) + +// flags: no notification && no web link preview +b.Send(user, "text", tele.Silent, tele.NoPreview) +``` + +Full list of supported option-flags you can find +[here](https://pkg.go.dev/gopkg.in/telebot.v3#Option). + +## Editable +If you want to edit some existing message, you don't really need to store the +original `*Message` object. In fact, upon edit, Telegram only requires `chat_id` +and `message_id`. So you don't really need the Message as a whole. Also, you +might want to store references to certain messages in the database, so I thought +it made sense for *any* Go struct to be editable as a Telegram message, to implement +`Editable`: +```go +// Editable is an interface for all objects that +// provide "message signature", a pair of 32-bit +// message ID and 64-bit chat ID, both required +// for edit operations. +// +// Use case: DB model struct for messages to-be +// edited with, say two columns: msg_id,chat_id +// could easily implement MessageSig() making +// instances of stored messages editable. +type Editable interface { + // MessageSig is a "message signature". + // + // For inline messages, return chatID = 0. + MessageSig() (messageID int, chatID int64) +} +``` + +For example, `Message` type is Editable. Here is the implementation of `StoredMessage` +type, provided by Telebot: +```go +// StoredMessage is an example struct suitable for being +// stored in the database as-is or being embedded into +// a larger struct, which is often the case (you might +// want to store some metadata alongside, or might not.) +type StoredMessage struct { + MessageID int `sql:"message_id" json:"message_id"` + ChatID int64 `sql:"chat_id" json:"chat_id"` +} + +func (x StoredMessage) MessageSig() (int, int64) { + return x.MessageID, x.ChatID +} +``` + +Why bother at all? Well, it allows you to do things like this: +```go +// just two integer columns in the database +var msgs []tele.StoredMessage +db.Find(&msgs) // gorm syntax + +for _, msg := range msgs { + bot.Edit(&msg, "Updated text") + // or + bot.Delete(&msg) +} +``` + +I find it incredibly neat. Worth noting, at this point of time there exists +another method in the Edit family, `EditCaption()` which is of a pretty +rare use, so I didn't bother including it to `Edit()`, just like I did with +`SendAlbum()` as it would inevitably lead to unnecessary complications. +```go +var m *Message + +// change caption of a photo, audio, etc. +bot.EditCaption(m, "new caption") +``` + +## Keyboards +Telebot supports both kinds of keyboards Telegram provides: reply and inline +keyboards. Any button can also act as endpoints for `Handle()`. + +```go +var ( + // Universal markup builders. + menu = &tele.ReplyMarkup{ResizeKeyboard: true} + selector = &tele.ReplyMarkup{} + + // Reply buttons. + btnHelp = menu.Text("ℹ Help") + btnSettings = menu.Text("⚙ Settings") + + // Inline buttons. + // + // Pressing it will cause the client to + // send the bot a callback. + // + // Make sure Unique stays unique as per button kind + // since it's required for callback routing to work. + // + btnPrev = selector.Data("⬅", "prev", ...) + btnNext = selector.Data("➡", "next", ...) +) + +menu.Reply( + menu.Row(btnHelp), + menu.Row(btnSettings), +) +selector.Inline( + selector.Row(btnPrev, btnNext), +) + +b.Handle("/start", func(c tele.Context) error { + return c.Send("Hello!", menu) +}) + +// On reply button pressed (message) +b.Handle(&btnHelp, func(c tele.Context) error { + return c.Edit("Here is some help: ...") +}) + +// On inline button pressed (callback) +b.Handle(&btnPrev, func(c tele.Context) error { + return c.Respond() +}) +``` + +You can use markup constructor for every type of possible button: +```go +r := b.NewMarkup() + +// Reply buttons: +r.Text("Hello!") +r.Contact("Send phone number") +r.Location("Send location") +r.Poll(tele.PollQuiz) + +// Inline buttons: +r.Data("Show help", "help") // data is optional +r.Data("Delete item", "delete", item.ID) +r.URL("Visit", "https://google.com") +r.Query("Search", query) +r.QueryChat("Share", query) +r.Login("Login", &tele.Login{...}) +``` + +## Inline mode +So if you want to handle incoming inline queries you better plug the `tele.OnQuery` +endpoint and then use the `Answer()` method to send a list of inline queries +back. I think at the time of writing, Telebot supports all of the provided result +types (but not the cached ones). This is what it looks like: + +```go +b.Handle(tele.OnQuery, func(c tele.Context) error { + urls := []string{ + "http://photo.jpg", + "http://photo2.jpg", + } + + results := make(tele.Results, len(urls)) // []tele.Result + for i, url := range urls { + result := &tele.PhotoResult{ + URL: url, + ThumbURL: url, // required for photos + } + + results[i] = result + // needed to set a unique string ID for each result + results[i].SetResultID(strconv.Itoa(i)) + } + + return c.Answer(&tele.QueryResponse{ + Results: results, + CacheTime: 60, // a minute + }) +}) +``` + +There's not much to talk about really. It also supports some form of authentication +through deep-linking. For that, use fields `SwitchPMText` and `SwitchPMParameter` +of `QueryResponse`. + +# Contributing + +1. Fork it +2. Clone v3: `git clone -b v3 https://github.com/tucnak/telebot` +3. Create your feature branch: `git checkout -b v3-feature` +4. Make changes and add them: `git add .` +5. Commit: `git commit -m "add some feature"` +6. Push: `git push origin v3-feature` +7. Pull request + +# Donate + +I do coding for fun, but I also try to search for interesting solutions and +optimize them as much as possible. +If you feel like it's a good piece of software, I wouldn't mind a tip! + +Litecoin: `ltc1qskt5ltrtyg7esfjm0ftx6jnacwffhpzpqmerus` + +Ethereum: `0xB78A2Ac1D83a0aD0b993046F9fDEfC5e619efCAB` + +# License + +Telebot is distributed under MIT. diff --git a/vendor/gopkg.in/telebot.v3/admin.go b/vendor/gopkg.in/telebot.v3/admin.go new file mode 100644 index 000000000..cc8b33a28 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/admin.go @@ -0,0 +1,313 @@ +package telebot + +import ( + "encoding/json" + "strconv" + "time" +) + +// Rights is a list of privileges available to chat members. +type Rights struct { + // Anonymous is true, if the user's presence in the chat is hidden. + Anonymous bool `json:"is_anonymous"` + + CanBeEdited bool `json:"can_be_edited"` + CanChangeInfo bool `json:"can_change_info"` + CanPostMessages bool `json:"can_post_messages"` + CanEditMessages bool `json:"can_edit_messages"` + CanDeleteMessages bool `json:"can_delete_messages"` + CanPinMessages bool `json:"can_pin_messages"` + CanInviteUsers bool `json:"can_invite_users"` + CanRestrictMembers bool `json:"can_restrict_members"` + CanPromoteMembers bool `json:"can_promote_members"` + CanSendMessages bool `json:"can_send_messages"` + CanSendPolls bool `json:"can_send_polls"` + CanSendOther bool `json:"can_send_other_messages"` + CanAddPreviews bool `json:"can_add_web_page_previews"` + CanManageVideoChats bool `json:"can_manage_video_chats"` + CanManageChat bool `json:"can_manage_chat"` + CanManageTopics bool `json:"can_manage_topics"` + + CanSendMedia bool `json:"can_send_media_messages,omitempty"` // deprecated + CanSendAudios bool `json:"can_send_audios"` + CanSendDocuments bool `json:"can_send_documents"` + CanSendPhotos bool `json:"can_send_photos"` + CanSendVideos bool `json:"can_send_videos"` + CanSendVideoNotes bool `json:"can_send_video_notes"` + CanSendVoiceNotes bool `json:"can_send_voice_notes"` + + // Independent defines whether the chat permissions are set independently. + // If not, the can_send_other_messages and can_add_web_page_previews permissions + // will imply the can_send_messages, can_send_audios, can_send_documents, can_send_photos, + // can_send_videos, can_send_video_notes, and can_send_voice_notes permissions; + // the can_send_polls permission will imply the can_send_messages permission. + // + // Works for Restrict and SetGroupPermissions methods only. + Independent bool `json:"-"` +} + +// NoRights is the default Rights{}. +func NoRights() Rights { return Rights{} } + +// NoRestrictions should be used when un-restricting or +// un-promoting user. +// +// member.Rights = tele.NoRestrictions() +// b.Restrict(chat, member) +func NoRestrictions() Rights { + return Rights{ + CanBeEdited: true, + CanChangeInfo: false, + CanPostMessages: false, + CanEditMessages: false, + CanDeleteMessages: false, + CanInviteUsers: false, + CanRestrictMembers: false, + CanPinMessages: false, + CanPromoteMembers: false, + CanSendMessages: true, + CanSendPolls: true, + CanSendOther: true, + CanAddPreviews: true, + CanManageVideoChats: false, + CanManageChat: false, + CanManageTopics: false, + CanSendAudios: true, + CanSendDocuments: true, + CanSendPhotos: true, + CanSendVideos: true, + CanSendVideoNotes: true, + CanSendVoiceNotes: true, + } +} + +// AdminRights could be used to promote user to admin. +func AdminRights() Rights { + return Rights{ + CanBeEdited: true, + CanChangeInfo: true, + CanPostMessages: true, + CanEditMessages: true, + CanDeleteMessages: true, + CanInviteUsers: true, + CanRestrictMembers: true, + CanPinMessages: true, + CanPromoteMembers: true, + CanSendMessages: true, + CanSendPolls: true, + CanSendOther: true, + CanAddPreviews: true, + CanManageVideoChats: true, + CanManageChat: true, + CanManageTopics: true, + CanSendAudios: true, + CanSendDocuments: true, + CanSendPhotos: true, + CanSendVideos: true, + CanSendVideoNotes: true, + CanSendVoiceNotes: true, + } +} + +// Forever is a ExpireUnixtime of "forever" banning. +func Forever() int64 { + return time.Now().Add(367 * 24 * time.Hour).Unix() +} + +// Ban will ban user from chat until `member.RestrictedUntil`. +func (b *Bot) Ban(chat *Chat, member *ChatMember, revokeMessages ...bool) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(member.RestrictedUntil, 10), + } + if len(revokeMessages) > 0 { + params["revoke_messages"] = strconv.FormatBool(revokeMessages[0]) + } + + _, err := b.Raw("kickChatMember", params) + return err +} + +// Unban will unban user from chat, who would have thought eh? +// forBanned does nothing if the user is not banned. +func (b *Bot) Unban(chat *Chat, user *User, forBanned ...bool) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + if len(forBanned) > 0 { + params["only_if_banned"] = strconv.FormatBool(forBanned[0]) + } + + _, err := b.Raw("unbanChatMember", params) + return err +} + +// Restrict lets you restrict a subset of member's rights until +// member.RestrictedUntil, such as: +// +// - can send messages +// - can send media +// - can send other +// - can add web page previews +func (b *Bot) Restrict(chat *Chat, member *ChatMember) error { + perms, until := member.Rights, member.RestrictedUntil + + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(until, 10), + "permissions": perms, + } + if perms.Independent { + params["use_independent_chat_permissions"] = true + } + + _, err := b.Raw("restrictChatMember", params) + return err +} + +// Promote lets you update member's admin rights, such as: +// +// - can change info +// - can post messages +// - can edit messages +// - can delete messages +// - can invite users +// - can restrict members +// - can pin messages +// - can promote members +func (b *Bot) Promote(chat *Chat, member *ChatMember) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "is_anonymous": member.Anonymous, + } + embedRights(params, member.Rights) + + _, err := b.Raw("promoteChatMember", params) + return err +} + +// AdminsOf returns a member list of chat admins. +// +// On success, returns an Array of ChatMember objects that +// contains information about all chat administrators except other bots. +// +// If the chat is a group or a supergroup and +// no administrators were appointed, only the creator will be returned. +func (b *Bot) AdminsOf(chat *Chat) ([]ChatMember, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("getChatAdministrators", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []ChatMember + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// Len returns the number of members in a chat. +func (b *Bot) Len(chat *Chat) (int, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("getChatMembersCount", params) + if err != nil { + return 0, err + } + + var resp struct { + Result int + } + if err := json.Unmarshal(data, &resp); err != nil { + return 0, wrapError(err) + } + return resp.Result, nil +} + +// SetAdminTitle sets a custom title for an administrator. +// A title should be 0-16 characters length, emoji are not allowed. +func (b *Bot) SetAdminTitle(chat *Chat, user *User, title string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + "custom_title": title, + } + + _, err := b.Raw("setChatAdministratorCustomTitle", params) + return err +} + +// BanSenderChat will use this method to ban a channel chat in a supergroup or a channel. +// Until the chat is unbanned, the owner of the banned chat won't be able +// to send messages on behalf of any of their channels. +func (b *Bot) BanSenderChat(chat *Chat, sender Recipient) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sender_chat_id": sender.Recipient(), + } + + _, err := b.Raw("banChatSenderChat", params) + return err +} + +// UnbanSenderChat will use this method to unban a previously banned channel chat in a supergroup or channel. +// The bot must be an administrator for this to work and must have the appropriate administrator rights. +func (b *Bot) UnbanSenderChat(chat *Chat, sender Recipient) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sender_chat_id": sender.Recipient(), + } + + _, err := b.Raw("unbanChatSenderChat", params) + return err +} + +// DefaultRights returns the current default administrator rights of the bot. +func (b *Bot) DefaultRights(forChannels bool) (*Rights, error) { + params := map[string]bool{ + "for_channels": forChannels, + } + + data, err := b.Raw("getMyDefaultAdministratorRights", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Rights + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// SetDefaultRights changes the default administrator rights requested by the bot +// when it's added as an administrator to groups or channels. +func (b *Bot) SetDefaultRights(rights Rights, forChannels bool) error { + params := map[string]interface{}{ + "rights": rights, + "for_channels": forChannels, + } + + _, err := b.Raw("setMyDefaultAdministratorRights", params) + return err +} + +func embedRights(p map[string]interface{}, rights Rights) { + data, _ := json.Marshal(rights) + _ = json.Unmarshal(data, &p) +} diff --git a/vendor/gopkg.in/telebot.v3/api.go b/vendor/gopkg.in/telebot.v3/api.go new file mode 100644 index 000000000..6ed4eb146 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/api.go @@ -0,0 +1,330 @@ +package telebot + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +// Raw lets you call any method of Bot API manually. +// It also handles API errors, so you only need to unwrap +// result field from json data. +func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) { + url := b.URL + "/bot" + b.Token + "/" + method + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(payload); err != nil { + return nil, err + } + + // Cancel the request immediately without waiting for the timeout when bot is about to stop. + // This may become important if doing long polling with long timeout. + exit := make(chan struct{}) + defer close(exit) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + select { + case <-b.stopClient: + cancel() + case <-exit: + } + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) + if err != nil { + return nil, wrapError(err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := b.client.Do(req) + if err != nil { + return nil, wrapError(err) + } + resp.Close = true + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, wrapError(err) + } + + if b.verbose { + verbose(method, payload, data) + } + + // returning data as well + return data, extractOk(data) +} + +func (b *Bot) sendFiles(method string, files map[string]File, params map[string]string) ([]byte, error) { + rawFiles := make(map[string]interface{}) + for name, f := range files { + switch { + case f.InCloud(): + params[name] = f.FileID + case f.FileURL != "": + params[name] = f.FileURL + case f.OnDisk(): + rawFiles[name] = f.FileLocal + case f.FileReader != nil: + rawFiles[name] = f.FileReader + default: + return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name) + } + } + + if len(rawFiles) == 0 { + return b.Raw(method, params) + } + + pipeReader, pipeWriter := io.Pipe() + writer := multipart.NewWriter(pipeWriter) + + go func() { + defer pipeWriter.Close() + + for field, file := range rawFiles { + if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil { + pipeWriter.CloseWithError(err) + return + } + } + for field, value := range params { + if err := writer.WriteField(field, value); err != nil { + pipeWriter.CloseWithError(err) + return + } + } + if err := writer.Close(); err != nil { + pipeWriter.CloseWithError(err) + return + } + }() + + url := b.URL + "/bot" + b.Token + "/" + method + + resp, err := b.client.Post(url, writer.FormDataContentType(), pipeReader) + if err != nil { + err = wrapError(err) + pipeReader.CloseWithError(err) + return nil, err + } + resp.Close = true + defer resp.Body.Close() + + if resp.StatusCode == http.StatusInternalServerError { + return nil, ErrInternal + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, wrapError(err) + } + + return data, extractOk(data) +} + +func addFileToWriter(writer *multipart.Writer, filename, field string, file interface{}) error { + var reader io.Reader + if r, ok := file.(io.Reader); ok { + reader = r + } else if path, ok := file.(string); ok { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + reader = f + } else { + return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field) + } + + part, err := writer.CreateFormFile(field, filename) + if err != nil { + return err + } + + _, err = io.Copy(part, reader) + return err +} + +func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "text": text, + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendMessage", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +func (b *Bot) sendMedia(media Media, params map[string]string, files map[string]File) (*Message, error) { + kind := media.MediaType() + what := "send" + strings.Title(kind) + + if kind == "videoNote" { + kind = "video_note" + } + + sendFiles := map[string]File{kind: *media.MediaFile()} + for k, v := range files { + sendFiles[k] = v + } + + data, err := b.sendFiles(what, sendFiles, params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +func (b *Bot) getMe() (*User, error) { + data, err := b.Raw("getMe", nil) + if err != nil { + return nil, err + } + + var resp struct { + Result *User + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []string) ([]Update, error) { + params := map[string]string{ + "offset": strconv.Itoa(offset), + "timeout": strconv.Itoa(int(timeout / time.Second)), + } + + data, _ := json.Marshal(allowed) + params["allowed_updates"] = string(data) + + if limit != 0 { + params["limit"] = strconv.Itoa(limit) + } + + data, err := b.Raw("getUpdates", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Update + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// extractOk checks given result for error. If result is ok returns nil. +// In other cases it extracts API error. If error is not presented +// in errors.go, it will be prefixed with `unknown` keyword. +func extractOk(data []byte) error { + var e struct { + Ok bool `json:"ok"` + Code int `json:"error_code"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + } + if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil { + return nil // FIXME + } + if e.Ok { + return nil + } + + err := Err(e.Description) + switch err { + case nil: + case ErrGroupMigrated: + migratedTo, ok := e.Parameters["migrate_to_chat_id"] + if !ok { + return NewError(e.Code, e.Description) + } + + return GroupError{ + err: err.(*Error), + MigratedTo: int64(migratedTo.(float64)), + } + default: + return err + } + + switch e.Code { + case http.StatusTooManyRequests: + retryAfter, ok := e.Parameters["retry_after"] + if !ok { + return NewError(e.Code, e.Description) + } + + err = FloodError{ + err: NewError(e.Code, e.Description), + RetryAfter: int(retryAfter.(float64)), + } + default: + err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code) + } + + return err +} + +// extractMessage extracts common Message result from given data. +// Should be called after extractOk or b.Raw() to handle possible errors. +func extractMessage(data []byte) (*Message, error) { + var resp struct { + Result *Message + } + if err := json.Unmarshal(data, &resp); err != nil { + var resp struct { + Result bool + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + if resp.Result { + return nil, ErrTrueResult + } + return nil, wrapError(err) + } + return resp.Result, nil +} + +func verbose(method string, payload interface{}, data []byte) { + body, _ := json.Marshal(payload) + body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`)) + body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`)) + body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`)) + + indent := func(b []byte) string { + var buf bytes.Buffer + json.Indent(&buf, b, "", " ") + return buf.String() + } + + log.Printf( + "[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v", + method, indent(body), indent(data), + ) +} diff --git a/vendor/gopkg.in/telebot.v3/bot.go b/vendor/gopkg.in/telebot.v3/bot.go new file mode 100644 index 000000000..e3a9a41f2 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/bot.go @@ -0,0 +1,1153 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" +) + +// NewBot does try to build a Bot with token `token`, which +// is a secret API key assigned to particular bot. +func NewBot(pref Settings) (*Bot, error) { + if pref.Updates == 0 { + pref.Updates = 100 + } + + client := pref.Client + if client == nil { + client = &http.Client{Timeout: time.Minute} + } + + if pref.URL == "" { + pref.URL = DefaultApiURL + } + if pref.Poller == nil { + pref.Poller = &LongPoller{} + } + if pref.OnError == nil { + pref.OnError = defaultOnError + } + + bot := &Bot{ + Token: pref.Token, + URL: pref.URL, + Poller: pref.Poller, + onError: pref.OnError, + + Updates: make(chan Update, pref.Updates), + handlers: make(map[string]HandlerFunc), + stop: make(chan chan struct{}), + + synchronous: pref.Synchronous, + verbose: pref.Verbose, + parseMode: pref.ParseMode, + client: client, + } + + if pref.Offline { + bot.Me = &User{} + } else { + user, err := bot.getMe() + if err != nil { + return nil, err + } + bot.Me = user + } + + bot.group = bot.Group() + return bot, nil +} + +// Bot represents a separate Telegram bot instance. +type Bot struct { + Me *User + Token string + URL string + Updates chan Update + Poller Poller + onError func(error, Context) + + group *Group + handlers map[string]HandlerFunc + synchronous bool + verbose bool + parseMode ParseMode + stop chan chan struct{} + client *http.Client + stopClient chan struct{} +} + +// Settings represents a utility struct for passing certain +// properties of a bot around and is required to make bots. +type Settings struct { + URL string + Token string + + // Updates channel capacity, defaulted to 100. + Updates int + + // Poller is the provider of Updates. + Poller Poller + + // Synchronous prevents handlers from running in parallel. + // It makes ProcessUpdate return after the handler is finished. + Synchronous bool + + // Verbose forces bot to log all upcoming requests. + // Use for debugging purposes only. + Verbose bool + + // ParseMode used to set default parse mode of all sent messages. + // It attaches to every send, edit or whatever method. You also + // will be able to override the default mode by passing a new one. + ParseMode ParseMode + + // OnError is a callback function that will get called on errors + // resulted from the handler. It is used as post-middleware function. + // Notice that context can be nil. + OnError func(error, Context) + + // HTTP Client used to make requests to telegram api + Client *http.Client + + // Offline allows to create a bot without network for testing purposes. + Offline bool +} + +var defaultOnError = func(err error, c Context) { + if c != nil { + log.Println(c.Update().ID, err) + } else { + log.Println(err) + } +} + +func (b *Bot) OnError(err error, c Context) { + b.onError(err, c) +} + +func (b *Bot) debug(err error) { + if b.verbose { + b.OnError(err, nil) + } +} + +// Group returns a new group. +func (b *Bot) Group() *Group { + return &Group{b: b} +} + +// Use adds middleware to the global bot chain. +func (b *Bot) Use(middleware ...MiddlewareFunc) { + b.group.Use(middleware...) +} + +var ( + cmdRx = regexp.MustCompile(`^(/\w+)(@(\w+))?(\s|$)(.+)?`) + cbackRx = regexp.MustCompile(`^\f([-\w]+)(\|(.+))?$`) +) + +// Handle lets you set the handler for some command name or +// one of the supported endpoints. It also applies middleware +// if such passed to the function. +// +// Example: +// +// b.Handle("/start", func (c tele.Context) error { +// return c.Reply("Hello!") +// }) +// +// b.Handle(&inlineButton, func (c tele.Context) error { +// return c.Respond(&tele.CallbackResponse{Text: "Hello!"}) +// }) +// +// Middleware usage: +// +// b.Handle("/ban", onBan, middleware.Whitelist(ids...)) +func (b *Bot) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) { + if len(b.group.middleware) > 0 { + m = appendMiddleware(b.group.middleware, m) + } + + handler := func(c Context) error { + return applyMiddleware(h, m...)(c) + } + + switch end := endpoint.(type) { + case string: + b.handlers[end] = handler + case CallbackEndpoint: + b.handlers[end.CallbackUnique()] = handler + default: + panic("telebot: unsupported endpoint") + } +} + +// Start brings bot into motion by consuming incoming +// updates (see Bot.Updates channel). +func (b *Bot) Start() { + if b.Poller == nil { + panic("telebot: can't start without a poller") + } + + // do nothing if called twice + if b.stopClient != nil { + return + } + b.stopClient = make(chan struct{}) + + stop := make(chan struct{}) + stopConfirm := make(chan struct{}) + + go func() { + b.Poller.Poll(b, b.Updates, stop) + close(stopConfirm) + }() + + for { + select { + // handle incoming updates + case upd := <-b.Updates: + b.ProcessUpdate(upd) + // call to stop polling + case confirm := <-b.stop: + close(stop) + <-stopConfirm + close(confirm) + b.stopClient = nil + return + } + } +} + +// Stop gracefully shuts the poller down. +func (b *Bot) Stop() { + if b.stopClient != nil { + close(b.stopClient) + } + confirm := make(chan struct{}) + b.stop <- confirm + <-confirm +} + +// NewMarkup simply returns newly created markup instance. +func (b *Bot) NewMarkup() *ReplyMarkup { + return &ReplyMarkup{} +} + +// NewContext returns a new native context object, +// field by the passed update. +func (b *Bot) NewContext(u Update) Context { + return &nativeContext{ + b: b, + u: u, + } +} + +// Send accepts 2+ arguments, starting with destination chat, followed by +// some Sendable (or string!) and optional send options. +// +// NOTE: +// +// Since most arguments are of type interface{}, but have pointer +// method receivers, make sure to pass them by-pointer, NOT by-value. +// +// What is a send option exactly? It can be one of the following types: +// +// - *SendOptions (the actual object accepted by Telegram API) +// - *ReplyMarkup (a component of SendOptions) +// - Option (a shortcut flag for popular options) +// - ParseMode (HTML, Markdown, etc) +func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + + sendOpts := extractOptions(opts) + + switch object := what.(type) { + case string: + return b.sendText(to, object, sendOpts) + case Sendable: + return object.Send(b, to, sendOpts) + default: + return nil, ErrUnsupportedWhat + } +} + +// SendAlbum sends multiple instances of media as a single message. +// To include the caption, make sure the first Inputtable of an album has it. +// From all existing options, it only supports tele.Silent. +func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + + sendOpts := extractOptions(opts) + media := make([]string, len(a)) + files := make(map[string]File) + + for i, x := range a { + var ( + repr string + data []byte + file = x.MediaFile() + ) + + switch { + case file.InCloud(): + repr = file.FileID + case file.FileURL != "": + repr = file.FileURL + case file.OnDisk() || file.FileReader != nil: + repr = "attach://" + strconv.Itoa(i) + files[strconv.Itoa(i)] = *file + default: + return nil, fmt.Errorf("telebot: album entry #%d does not exist", i) + } + + im := x.InputMedia() + im.Media = repr + + if len(sendOpts.Entities) > 0 { + im.Entities = sendOpts.Entities + } else { + im.ParseMode = sendOpts.ParseMode + } + + data, _ = json.Marshal(im) + media[i] = string(data) + } + + params := map[string]string{ + "chat_id": to.Recipient(), + "media": "[" + strings.Join(media, ",") + "]", + } + b.embedSendOptions(params, sendOpts) + + data, err := b.sendFiles("sendMediaGroup", files, params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Message + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + for attachName := range files { + i, _ := strconv.Atoi(attachName) + r := resp.Result[i] + + var newID string + switch { + case r.Photo != nil: + newID = r.Photo.FileID + case r.Video != nil: + newID = r.Video.FileID + case r.Audio != nil: + newID = r.Audio.FileID + case r.Document != nil: + newID = r.Document.FileID + } + + a[i].MediaFile().FileID = newID + } + + return resp.Result, nil +} + +// Reply behaves just like Send() with an exception of "reply-to" indicator. +// This function will panic upon nil Message. +func (b *Bot) Reply(to *Message, what interface{}, opts ...interface{}) (*Message, error) { + sendOpts := extractOptions(opts) + if sendOpts == nil { + sendOpts = &SendOptions{} + } + + sendOpts.ReplyTo = to + return b.Send(to.Chat, what, sendOpts) +} + +// Forward behaves just like Send() but of all options it only supports Silent (see Bots API). +// This function will panic upon nil Editable. +func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": to.Recipient(), + "from_chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("forwardMessage", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Copy behaves just like Forward() but the copied message doesn't have a link to the original message (see Bots API). +// +// This function will panic upon nil Editable. +func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": to.Recipient(), + "from_chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(options) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("copyMessage", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Edit is magic, it lets you change already sent message. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +// Use cases: +// +// b.Edit(m, m.Text, newMarkup) +// b.Edit(m, "new text", tele.ModeHTML) +// b.Edit(m, &tele.ReplyMarkup{...}) +// b.Edit(m, &tele.Photo{File: ...}) +// b.Edit(m, tele.Location{42.1337, 69.4242}) +// b.Edit(c, "edit inline message from the callback") +// b.Edit(r, "edit message from chosen inline result") +func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Message, error) { + var ( + method string + params = make(map[string]string) + ) + + switch v := what.(type) { + case *ReplyMarkup: + return b.EditReplyMarkup(msg, v) + case Inputtable: + return b.EditMedia(msg, v, opts...) + case string: + method = "editMessageText" + params["text"] = v + case Location: + method = "editMessageLiveLocation" + params["latitude"] = fmt.Sprintf("%f", v.Lat) + params["longitude"] = fmt.Sprintf("%f", v.Lng) + + if v.HorizontalAccuracy != nil { + params["horizontal_accuracy"] = fmt.Sprintf("%f", *v.HorizontalAccuracy) + } + if v.Heading != 0 { + params["heading"] = strconv.Itoa(v.Heading) + } + if v.AlertRadius != 0 { + params["proximity_alert_radius"] = strconv.Itoa(v.AlertRadius) + } + default: + return nil, ErrUnsupportedWhat + } + + msgID, chatID := msg.MessageSig() + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw(method, params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditReplyMarkup edits reply markup of already sent message. +// This function will panic upon nil Editable. +// Pass nil or empty ReplyMarkup to delete it from the message. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +func (b *Bot) EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, error) { + msgID, chatID := msg.MessageSig() + params := make(map[string]string) + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + if markup == nil { + // will delete reply markup + markup = &ReplyMarkup{} + } + + processButtons(markup.InlineKeyboard) + data, _ := json.Marshal(markup) + params["reply_markup"] = string(data) + + data, err := b.Raw("editMessageReplyMarkup", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditCaption edits already sent photo caption with known recipient and message id. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "caption": caption, + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("editMessageCaption", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditMedia edits already sent media with known recipient and message id. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +// Use cases: +// +// b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")}) +// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")}) +func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error) { + var ( + repr string + file = media.MediaFile() + files = make(map[string]File) + + thumb *Photo + thumbName = "thumb" + ) + + switch { + case file.InCloud(): + repr = file.FileID + case file.FileURL != "": + repr = file.FileURL + case file.OnDisk() || file.FileReader != nil: + s := file.FileLocal + if file.FileReader != nil { + s = "0" + } else if s == thumbName { + thumbName = "thumb2" + } + + repr = "attach://" + s + files[s] = *file + default: + return nil, fmt.Errorf("telebot: cannot edit media, it does not exist") + } + + switch m := media.(type) { + case *Video: + thumb = m.Thumbnail + case *Audio: + thumb = m.Thumbnail + case *Document: + thumb = m.Thumbnail + case *Animation: + thumb = m.Thumbnail + } + + msgID, chatID := msg.MessageSig() + params := make(map[string]string) + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + im := media.InputMedia() + im.Media = repr + + if len(sendOpts.Entities) > 0 { + im.Entities = sendOpts.Entities + } else { + im.ParseMode = sendOpts.ParseMode + } + + if thumb != nil { + im.Thumbnail = "attach://" + thumbName + files[thumbName] = *thumb.MediaFile() + } + + data, _ := json.Marshal(im) + params["media"] = string(data) + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.sendFiles("editMessageMedia", files, params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Delete removes the message, including service messages. +// This function will panic upon nil Editable. +// +// - A message can only be deleted if it was sent less than 48 hours ago. +// - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. +// - Bots can delete outgoing messages in private chats, groups, and supergroups. +// - Bots can delete incoming messages in private chats. +// - Bots granted can_post_messages permissions can delete outgoing messages in channels. +// - If the bot is an administrator of a group, it can delete any message there. +// - If the bot has can_delete_messages permission in a supergroup or a +// channel, it can delete any message there. +func (b *Bot) Delete(msg Editable) error { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + _, err := b.Raw("deleteMessage", params) + return err +} + +// Notify updates the chat action for recipient. +// +// Chat action is a status message that recipient would see where +// you typically see "Harry is typing" status message. The only +// difference is that bots' chat actions live only for 5 seconds +// and die just once the client receives a message from the bot. +// +// Currently, Telegram supports only a narrow range of possible +// actions, these are aligned as constants of this package. +func (b *Bot) Notify(to Recipient, action ChatAction, threadID ...int) error { + if to == nil { + return ErrBadRecipient + } + + params := map[string]string{ + "chat_id": to.Recipient(), + "action": string(action), + } + + if len(threadID) > 0 { + params["message_thread_id"] = strconv.Itoa(threadID[0]) + } + + _, err := b.Raw("sendChatAction", params) + return err +} + +// Ship replies to the shipping query, if you sent an invoice +// requesting an address and the parameter is_flexible was specified. +// +// Example: +// +// b.Ship(query) // OK +// b.Ship(query, opts...) // OK with options +// b.Ship(query, "Oops!") // Error message +func (b *Bot) Ship(query *ShippingQuery, what ...interface{}) error { + params := map[string]string{ + "shipping_query_id": query.ID, + } + + if len(what) == 0 { + params["ok"] = "true" + } else if s, ok := what[0].(string); ok { + params["ok"] = "false" + params["error_message"] = s + } else { + var opts []ShippingOption + for _, v := range what { + opt, ok := v.(ShippingOption) + if !ok { + return ErrUnsupportedWhat + } + opts = append(opts, opt) + } + + params["ok"] = "true" + data, _ := json.Marshal(opts) + params["shipping_options"] = string(data) + } + + _, err := b.Raw("answerShippingQuery", params) + return err +} + +// Accept finalizes the deal. +func (b *Bot) Accept(query *PreCheckoutQuery, errorMessage ...string) error { + params := map[string]string{ + "pre_checkout_query_id": query.ID, + } + + if len(errorMessage) == 0 { + params["ok"] = "true" + } else { + params["ok"] = "False" + params["error_message"] = errorMessage[0] + } + + _, err := b.Raw("answerPreCheckoutQuery", params) + return err +} + +// Respond sends a response for a given callback query. A callback can +// only be responded to once, subsequent attempts to respond to the same callback +// will result in an error. +// +// Example: +// +// b.Respond(c) +// b.Respond(c, response) +func (b *Bot) Respond(c *Callback, resp ...*CallbackResponse) error { + var r *CallbackResponse + if resp == nil { + r = &CallbackResponse{} + } else { + r = resp[0] + } + + r.CallbackID = c.ID + _, err := b.Raw("answerCallbackQuery", r) + return err +} + +// Answer sends a response for a given inline query. A query can only +// be responded to once, subsequent attempts to respond to the same query +// will result in an error. +func (b *Bot) Answer(query *Query, resp *QueryResponse) error { + resp.QueryID = query.ID + + for _, result := range resp.Results { + result.Process(b) + } + + _, err := b.Raw("answerInlineQuery", resp) + return err +} + +// AnswerWebApp sends a response for a query from Web App and returns +// information about an inline message sent by a Web App on behalf of a user +func (b *Bot) AnswerWebApp(query *Query, r Result) (*WebAppMessage, error) { + r.Process(b) + + params := map[string]interface{}{ + "web_app_query_id": query.ID, + "result": r, + } + + data, err := b.Raw("answerWebAppQuery", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *WebAppMessage + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return resp.Result, err +} + +// FileByID returns full file object including File.FilePath, allowing you to +// download the file from the server. +// +// Usually, Telegram-provided File objects miss FilePath so you might need to +// perform an additional request to fetch them. +func (b *Bot) FileByID(fileID string) (File, error) { + params := map[string]string{ + "file_id": fileID, + } + + data, err := b.Raw("getFile", params) + if err != nil { + return File{}, err + } + + var resp struct { + Result File + } + if err := json.Unmarshal(data, &resp); err != nil { + return File{}, wrapError(err) + } + return resp.Result, nil +} + +// Download saves the file from Telegram servers locally. +// Maximum file size to download is 20 MB. +func (b *Bot) Download(file *File, localFilename string) error { + reader, err := b.File(file) + if err != nil { + return err + } + defer reader.Close() + + out, err := os.Create(localFilename) + if err != nil { + return wrapError(err) + } + defer out.Close() + + _, err = io.Copy(out, reader) + if err != nil { + return wrapError(err) + } + + file.FileLocal = localFilename + return nil +} + +// File gets a file from Telegram servers. +func (b *Bot) File(file *File) (io.ReadCloser, error) { + f, err := b.FileByID(file.FileID) + if err != nil { + return nil, err + } + + url := b.URL + "/file/bot" + b.Token + "/" + f.FilePath + file.FilePath = f.FilePath // saving file path + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, wrapError(err) + } + + resp, err := b.client.Do(req) + if err != nil { + return nil, wrapError(err) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("telebot: expected status 200 but got %s", resp.Status) + } + + return resp.Body, nil +} + +// StopLiveLocation stops broadcasting live message location +// before Location.LivePeriod expires. +// +// It supports ReplyMarkup. +// This function will panic upon nil Editable. +// +// If the message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +func (b *Bot) StopLiveLocation(msg Editable, opts ...interface{}) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("stopMessageLiveLocation", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// StopPoll stops a poll which was sent by the bot and returns +// the stopped Poll object with the final results. +// +// It supports ReplyMarkup. +// This function will panic upon nil Editable. +func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("stopPoll", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Poll + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// Leave makes bot leave a group, supergroup or channel. +func (b *Bot) Leave(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("leaveChat", params) + return err +} + +// Pin pins a message in a supergroup or a channel. +// +// It supports Silent option. +// This function will panic upon nil Editable. +func (b *Bot) Pin(msg Editable, opts ...interface{}) error { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + _, err := b.Raw("pinChatMessage", params) + return err +} + +// Unpin unpins a message in a supergroup or a channel. +// It supports tb.Silent option. +func (b *Bot) Unpin(chat *Chat, messageID ...int) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if len(messageID) > 0 { + params["message_id"] = strconv.Itoa(messageID[0]) + } + + _, err := b.Raw("unpinChatMessage", params) + return err +} + +// UnpinAll unpins all messages in a supergroup or a channel. +// It supports tb.Silent option. +func (b *Bot) UnpinAll(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("unpinAllChatMessages", params) + return err +} + +// ChatByID fetches chat info of its ID. +// +// Including current name of the user for one-on-one conversations, +// current username of a user, group or channel, etc. +func (b *Bot) ChatByID(id int64) (*Chat, error) { + return b.ChatByUsername(strconv.FormatInt(id, 10)) +} + +// ChatByUsername fetches chat info by its username. +func (b *Bot) ChatByUsername(name string) (*Chat, error) { + params := map[string]string{ + "chat_id": name, + } + + data, err := b.Raw("getChat", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Chat + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + if resp.Result.Type == ChatChannel && resp.Result.Username == "" { + resp.Result.Type = ChatChannelPrivate + } + return resp.Result, nil +} + +// ProfilePhotosOf returns list of profile pictures for a user. +func (b *Bot) ProfilePhotosOf(user *User) ([]Photo, error) { + params := map[string]string{ + "user_id": user.Recipient(), + } + + data, err := b.Raw("getUserProfilePhotos", params) + if err != nil { + return nil, err + } + + var resp struct { + Result struct { + Count int `json:"total_count"` + Photos []Photo `json:"photos"` + } + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result.Photos, nil +} + +// ChatMemberOf returns information about a member of a chat. +func (b *Bot) ChatMemberOf(chat, user Recipient) (*ChatMember, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("getChatMember", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *ChatMember + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// MenuButton returns the current value of the bot's menu button in a private chat, +// or the default menu button. +func (b *Bot) MenuButton(chat *User) (*MenuButton, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("getChatMenuButton", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *MenuButton + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// SetMenuButton changes the bot's menu button in a private chat, +// or the default menu button. +// +// It accepts two kinds of menu button arguments: +// +// - MenuButtonType for simple menu buttons (default, commands) +// - MenuButton complete structure for web_app menu button type +func (b *Bot) SetMenuButton(chat *User, mb interface{}) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + } + + switch v := mb.(type) { + case MenuButtonType: + params["menu_button"] = MenuButton{Type: v} + case *MenuButton: + params["menu_button"] = v + } + + _, err := b.Raw("setChatMenuButton", params) + return err +} + +// Logout logs out from the cloud Bot API server before launching the bot locally. +func (b *Bot) Logout() (bool, error) { + data, err := b.Raw("logOut", nil) + if err != nil { + return false, err + } + + var resp struct { + Result bool `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return false, wrapError(err) + } + + return resp.Result, nil +} + +// Close closes the bot instance before moving it from one local server to another. +func (b *Bot) Close() (bool, error) { + data, err := b.Raw("close", nil) + if err != nil { + return false, err + } + + var resp struct { + Result bool `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return false, wrapError(err) + } + + return resp.Result, nil +} diff --git a/vendor/gopkg.in/telebot.v3/callback.go b/vendor/gopkg.in/telebot.v3/callback.go new file mode 100644 index 000000000..4bce60a1e --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/callback.go @@ -0,0 +1,89 @@ +package telebot + +// CallbackEndpoint is an interface any element capable +// of responding to a callback `\f`. +type CallbackEndpoint interface { + CallbackUnique() string +} + +// Callback object represents a query from a callback button in an +// inline keyboard. +type Callback struct { + ID string `json:"id"` + + // For message sent to channels, Sender may be empty + Sender *User `json:"from"` + + // Message will be set if the button that originated the query + // was attached to a message sent by a bot. + Message *Message `json:"message"` + + // MessageID will be set if the button was attached to a message + // sent via the bot in inline mode. + MessageID string `json:"inline_message_id"` + + // Data associated with the callback button. Be aware that + // a bad client can send arbitrary data in this field. + Data string `json:"data"` + + // Unique displays an unique of the button from which the + // callback was fired. Sets immediately before the handling, + // while the Data field stores only with payload. + Unique string `json:"-"` +} + +// MessageSig satisfies Editable interface. +func (c *Callback) MessageSig() (string, int64) { + if c.IsInline() { + return c.MessageID, 0 + } + return c.Message.MessageSig() +} + +// IsInline says whether message is an inline message. +func (c *Callback) IsInline() bool { + return c.MessageID != "" +} + +// CallbackResponse builds a response to a Callback query. +type CallbackResponse struct { + // The ID of the callback to which this is a response. + // + // Note: Telebot sets this field automatically! + CallbackID string `json:"callback_query_id"` + + // Text of the notification. If not specified, nothing will be + // shown to the user. + Text string `json:"text,omitempty"` + + // (Optional) If true, an alert will be shown by the client instead + // of a notification at the top of the chat screen. Defaults to false. + ShowAlert bool `json:"show_alert,omitempty"` + + // (Optional) URL that will be opened by the user's client. + // If you have created a Game and accepted the conditions via + // @BotFather, specify the URL that opens your game. + // + // Note: this will only work if the query comes from a game + // callback button. Otherwise, you may use deep-linking: + // https://telegram.me/your_bot?start=XXXX + URL string `json:"url,omitempty"` +} + +// CallbackUnique returns ReplyButton.Text. +func (t *ReplyButton) CallbackUnique() string { + return t.Text +} + +// CallbackUnique returns InlineButton.Unique. +func (t *InlineButton) CallbackUnique() string { + return "\f" + t.Unique +} + +// CallbackUnique implements CallbackEndpoint. +func (t *Btn) CallbackUnique() string { + if t.Unique != "" { + return "\f" + t.Unique + } + return t.Text +} diff --git a/vendor/gopkg.in/telebot.v3/chat.go b/vendor/gopkg.in/telebot.v3/chat.go new file mode 100644 index 000000000..74d6333bf --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/chat.go @@ -0,0 +1,467 @@ +package telebot + +import ( + "encoding/json" + "strconv" + "time" +) + +// User object represents a Telegram user, bot. +type User struct { + ID int64 `json:"id"` + + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + IsForum bool `json:"is_forum"` + Username string `json:"username"` + LanguageCode string `json:"language_code"` + IsBot bool `json:"is_bot"` + IsPremium bool `json:"is_premium"` + AddedToMenu bool `json:"added_to_attachment_menu"` + Usernames []string `json:"active_usernames"` + CustomEmojiStatus string `json:"emoji_status_custom_emoji_id"` + + // Returns only in getMe + CanJoinGroups bool `json:"can_join_groups"` + CanReadMessages bool `json:"can_read_all_group_messages"` + SupportsInline bool `json:"supports_inline_queries"` +} + +// Recipient returns user ID (see Recipient interface). +func (u *User) Recipient() string { + return strconv.FormatInt(u.ID, 10) +} + +// Chat object represents a Telegram user, bot, group or a channel. +type Chat struct { + ID int64 `json:"id"` + + // See ChatType and consts. + Type ChatType `json:"type"` + + // Won't be there for ChatPrivate. + Title string `json:"title"` + + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + + // Returns only in getChat + Bio string `json:"bio,omitempty"` + Photo *ChatPhoto `json:"photo,omitempty"` + Description string `json:"description,omitempty"` + InviteLink string `json:"invite_link,omitempty"` + PinnedMessage *Message `json:"pinned_message,omitempty"` + Permissions *Rights `json:"permissions,omitempty"` + SlowMode int `json:"slow_mode_delay,omitempty"` + StickerSet string `json:"sticker_set_name,omitempty"` + CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` + LinkedChatID int64 `json:"linked_chat_id,omitempty"` + ChatLocation *ChatLocation `json:"location,omitempty"` + Private bool `json:"has_private_forwards,omitempty"` + Protected bool `json:"has_protected_content,omitempty"` + NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"` + HiddenMembers bool `json:"has_hidden_members,omitempty"` + AggressiveAntiSpam bool `json:"has_aggressive_anti_spam_enabled,omitempty"` +} + +// Recipient returns chat ID (see Recipient interface). +func (c *Chat) Recipient() string { + return strconv.FormatInt(c.ID, 10) +} + +// ChatType represents one of the possible chat types. +type ChatType string + +const ( + ChatPrivate ChatType = "private" + ChatGroup ChatType = "group" + ChatSuperGroup ChatType = "supergroup" + ChatChannel ChatType = "channel" + ChatChannelPrivate ChatType = "privatechannel" +) + +// ChatLocation represents a location to which a chat is connected. +type ChatLocation struct { + Location Location `json:"location,omitempty"` + Address string `json:"address,omitempty"` +} + +// ChatPhoto object represents a chat photo. +type ChatPhoto struct { + // File identifiers of small (160x160) chat photo + SmallFileID string `json:"small_file_id"` + SmallUniqueID string `json:"small_file_unique_id"` + + // File identifiers of big (640x640) chat photo + BigFileID string `json:"big_file_id"` + BigUniqueID string `json:"big_file_unique_id"` +} + +// ChatMember object represents information about a single chat member. +type ChatMember struct { + Rights + + User *User `json:"user"` + Role MemberStatus `json:"status"` + Title string `json:"custom_title"` + Anonymous bool `json:"is_anonymous"` + Member bool `json:"is_member,omitempty"` + + // Date when restrictions will be lifted for the user, unix time. + // + // If user is restricted for more than 366 days or less than + // 30 seconds from the current time, they are considered to be + // restricted forever. + // + // Use tele.Forever(). + // + RestrictedUntil int64 `json:"until_date,omitempty"` + + JoinToSend string `json:"join_to_send_messages"` + JoinByRequest string `json:"join_by_request"` +} + +// MemberStatus is one's chat status. +type MemberStatus string + +const ( + Creator MemberStatus = "creator" + Administrator MemberStatus = "administrator" + Member MemberStatus = "member" + Restricted MemberStatus = "restricted" + Left MemberStatus = "left" + Kicked MemberStatus = "kicked" +) + +// ChatMemberUpdate object represents changes in the status of a chat member. +type ChatMemberUpdate struct { + // Chat where the user belongs to. + Chat *Chat `json:"chat"` + + // Sender which user the action was triggered. + Sender *User `json:"from"` + + // Unixtime, use Date() to get time.Time. + Unixtime int64 `json:"date"` + + // Previous information about the chat member. + OldChatMember *ChatMember `json:"old_chat_member"` + + // New information about the chat member. + NewChatMember *ChatMember `json:"new_chat_member"` + + // (Optional) InviteLink which was used by the user to + // join the chat; for joining by invite link events only. + InviteLink *ChatInviteLink `json:"invite_link"` +} + +// Time returns the moment of the change in local time. +func (c *ChatMemberUpdate) Time() time.Time { + return time.Unix(c.Unixtime, 0) +} + +// ChatID represents a chat or an user integer ID, which can be used +// as recipient in bot methods. It is very useful in cases where +// you have special group IDs, for example in your config, and don't +// want to wrap it into *tele.Chat every time you send messages. +// +// Example: +// +// group := tele.ChatID(-100756389456) +// b.Send(group, "Hello!") +// +// type Config struct { +// AdminGroup tele.ChatID `json:"admin_group"` +// } +// b.Send(conf.AdminGroup, "Hello!") +type ChatID int64 + +// Recipient returns chat ID (see Recipient interface). +func (i ChatID) Recipient() string { + return strconv.FormatInt(int64(i), 10) +} + +// ChatJoinRequest represents a join request sent to a chat. +type ChatJoinRequest struct { + // Chat to which the request was sent. + Chat *Chat `json:"chat"` + + // Sender is the user that sent the join request. + Sender *User `json:"from"` + + // UserChatID is an ID of a private chat with the user + // who sent the join request. The bot can use this ID + // for 5 minutes to send messages until the join request + // is processed, assuming no other administrator contacted the user. + UserChatID int64 `json:"user_chat_id"` + + // Unixtime, use ChatJoinRequest.Time() to get time.Time. + Unixtime int64 `json:"date"` + + // Bio of the user, optional. + Bio string `json:"bio"` + + // InviteLink is the chat invite link that was used by + //the user to send the join request, optional. + InviteLink *ChatInviteLink `json:"invite_link"` +} + +// ChatInviteLink object represents an invite for a chat. +type ChatInviteLink struct { + // The invite link. + InviteLink string `json:"invite_link"` + + // Invite link name. + Name string `json:"name"` + + // The creator of the link. + Creator *User `json:"creator"` + + // If the link is primary. + IsPrimary bool `json:"is_primary"` + + // If the link is revoked. + IsRevoked bool `json:"is_revoked"` + + // (Optional) Point in time when the link will expire, + // use ExpireDate() to get time.Time. + ExpireUnixtime int64 `json:"expire_date,omitempty"` + + // (Optional) Maximum number of users that can be members of + // the chat simultaneously. + MemberLimit int `json:"member_limit,omitempty"` + + // (Optional) True, if users joining the chat via the link need to + // be approved by chat administrators. If True, member_limit can't be specified. + JoinRequest bool `json:"creates_join_request"` + + // (Optional) Number of pending join requests created using this link. + PendingCount int `json:"pending_join_request_count"` +} + +// ExpireDate returns the moment of the link expiration in local time. +func (c *ChatInviteLink) ExpireDate() time.Time { + return time.Unix(c.ExpireUnixtime, 0) +} + +// Time returns the moment of chat join request sending in local time. +func (r ChatJoinRequest) Time() time.Time { + return time.Unix(r.Unixtime, 0) +} + +// InviteLink should be used to export chat's invite link. +func (b *Bot) InviteLink(chat *Chat) (string, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("exportChatInviteLink", params) + if err != nil { + return "", err + } + + var resp struct { + Result string + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", wrapError(err) + } + return resp.Result, nil +} + +// CreateInviteLink creates an additional invite link for a chat. +func (b *Bot) CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if link != nil { + params["name"] = link.Name + + if link.ExpireUnixtime != 0 { + params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10) + } + if link.MemberLimit > 0 { + params["member_limit"] = strconv.Itoa(link.MemberLimit) + } else if link.JoinRequest { + params["creates_join_request"] = "true" + } + } + + data, err := b.Raw("createChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// EditInviteLink edits a non-primary invite link created by the bot. +func (b *Bot) EditInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if link != nil { + params["invite_link"] = link.InviteLink + params["name"] = link.Name + + if link.ExpireUnixtime != 0 { + params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10) + } + if link.MemberLimit > 0 { + params["member_limit"] = strconv.Itoa(link.MemberLimit) + } else if link.JoinRequest { + params["creates_join_request"] = "true" + } + } + + data, err := b.Raw("editChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// RevokeInviteLink revokes an invite link created by the bot. +func (b *Bot) RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "invite_link": link, + } + + data, err := b.Raw("revokeChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// ApproveJoinRequest approves a chat join request. +func (b *Bot) ApproveJoinRequest(chat Recipient, user *User) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("approveChatJoinRequest", params) + if err != nil { + return err + } + + return extractOk(data) +} + +// DeclineJoinRequest declines a chat join request. +func (b *Bot) DeclineJoinRequest(chat Recipient, user *User) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("declineChatJoinRequest", params) + if err != nil { + return err + } + + return extractOk(data) +} + +// SetGroupTitle should be used to update group title. +func (b *Bot) SetGroupTitle(chat *Chat, title string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "title": title, + } + + _, err := b.Raw("setChatTitle", params) + return err +} + +// SetGroupDescription should be used to update group description. +func (b *Bot) SetGroupDescription(chat *Chat, description string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "description": description, + } + + _, err := b.Raw("setChatDescription", params) + return err +} + +// SetGroupPhoto should be used to update group photo. +func (b *Bot) SetGroupPhoto(chat *Chat, p *Photo) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params) + return err +} + +// SetGroupStickerSet should be used to update group's group sticker set. +func (b *Bot) SetGroupStickerSet(chat *Chat, setName string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sticker_set_name": setName, + } + + _, err := b.Raw("setChatStickerSet", params) + return err +} + +// SetGroupPermissions sets default chat permissions for all members. +func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "permissions": perms, + } + if perms.Independent { + params["use_independent_chat_permissions"] = true + } + + _, err := b.Raw("setChatPermissions", params) + return err +} + +// DeleteGroupPhoto should be used to just remove group photo. +func (b *Bot) DeleteGroupPhoto(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("deleteChatPhoto", params) + return err +} + +// DeleteGroupStickerSet should be used to just remove group sticker set. +func (b *Bot) DeleteGroupStickerSet(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("deleteChatStickerSet", params) + return err +} diff --git a/vendor/gopkg.in/telebot.v3/commands.go b/vendor/gopkg.in/telebot.v3/commands.go new file mode 100644 index 000000000..36a515afd --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/commands.go @@ -0,0 +1,85 @@ +package telebot + +import "encoding/json" + +// Command represents a bot command. +type Command struct { + // Text is a text of the command, 1-32 characters. + // Can contain only lowercase English letters, digits and underscores. + Text string `json:"command"` + + // Description of the command, 3-256 characters. + Description string `json:"description"` +} + +// CommandParams controls parameters for commands-related methods (setMyCommands, deleteMyCommands and getMyCommands). +type CommandParams struct { + Commands []Command `json:"commands,omitempty"` + Scope *CommandScope `json:"scope,omitempty"` + LanguageCode string `json:"language_code,omitempty"` +} + +type CommandScopeType = string + +const ( + CommandScopeDefault CommandScopeType = "default" + CommandScopeAllPrivateChats CommandScopeType = "all_private_chats" + CommandScopeAllGroupChats CommandScopeType = "all_group_chats" + CommandScopeAllChatAdmin CommandScopeType = "all_chat_administrators" + CommandScopeChat CommandScopeType = "chat" + CommandScopeChatAdmin CommandScopeType = "chat_administrators" + CommandScopeChatMember CommandScopeType = "chat_member" +) + +// CommandScope object represents a scope to which bot commands are applied. +type CommandScope struct { + Type CommandScopeType `json:"type"` + ChatID int64 `json:"chat_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` +} + +// Commands returns the current list of the bot's commands for the given scope and user language. +func (b *Bot) Commands(opts ...interface{}) ([]Command, error) { + params := extractCommandsParams(opts...) + data, err := b.Raw("getMyCommands", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Command + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// SetCommands changes the list of the bot's commands. +func (b *Bot) SetCommands(opts ...interface{}) error { + params := extractCommandsParams(opts...) + _, err := b.Raw("setMyCommands", params) + return err +} + +// DeleteCommands deletes the list of the bot's commands for the given scope and user language. +func (b *Bot) DeleteCommands(opts ...interface{}) error { + params := extractCommandsParams(opts...) + _, err := b.Raw("deleteMyCommands", params) + return err +} + +// extractCommandsParams extracts parameters for commands-related methods from the given options. +func extractCommandsParams(opts ...interface{}) (params CommandParams) { + for _, opt := range opts { + switch value := opt.(type) { + case []Command: + params.Commands = value + case string: + params.LanguageCode = value + case CommandScope: + params.Scope = &value + } + } + return +} diff --git a/vendor/gopkg.in/telebot.v3/context.go b/vendor/gopkg.in/telebot.v3/context.go new file mode 100644 index 000000000..3306c8f73 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/context.go @@ -0,0 +1,505 @@ +package telebot + +import ( + "errors" + "strings" + "sync" + "time" +) + +// HandlerFunc represents a handler function, which is +// used to handle actual endpoints. +type HandlerFunc func(Context) error + +// Context wraps an update and represents the context of current event. +type Context interface { + // Bot returns the bot instance. + Bot() *Bot + + // Update returns the original update. + Update() Update + + // Message returns stored message if such presented. + Message() *Message + + // Callback returns stored callback if such presented. + Callback() *Callback + + // Query returns stored query if such presented. + Query() *Query + + // InlineResult returns stored inline result if such presented. + InlineResult() *InlineResult + + // ShippingQuery returns stored shipping query if such presented. + ShippingQuery() *ShippingQuery + + // PreCheckoutQuery returns stored pre checkout query if such presented. + PreCheckoutQuery() *PreCheckoutQuery + + // Poll returns stored poll if such presented. + Poll() *Poll + + // PollAnswer returns stored poll answer if such presented. + PollAnswer() *PollAnswer + + // ChatMember returns chat member changes. + ChatMember() *ChatMemberUpdate + + // ChatJoinRequest returns the chat join request. + ChatJoinRequest() *ChatJoinRequest + + // Migration returns both migration from and to chat IDs. + Migration() (int64, int64) + + // Topic returns the topic changes. + Topic() *Topic + + // Sender returns the current recipient, depending on the context type. + // Returns nil if user is not presented. + Sender() *User + + // Chat returns the current chat, depending on the context type. + // Returns nil if chat is not presented. + Chat() *Chat + + // Recipient combines both Sender and Chat functions. If there is no user + // the chat will be returned. The native context cannot be without sender, + // but it is useful in the case when the context created intentionally + // by the NewContext constructor and have only Chat field inside. + Recipient() Recipient + + // Text returns the message text, depending on the context type. + // In the case when no related data presented, returns an empty string. + Text() string + + // Entities returns the message entities, whether it's media caption's or the text's. + // In the case when no entities presented, returns a nil. + Entities() Entities + + // Data returns the current data, depending on the context type. + // If the context contains command, returns its arguments string. + // If the context contains payment, returns its payload. + // In the case when no related data presented, returns an empty string. + Data() string + + // Args returns a raw slice of command or callback arguments as strings. + // The message arguments split by space, while the callback's ones by a "|" symbol. + Args() []string + + // Send sends a message to the current recipient. + // See Send from bot.go. + Send(what interface{}, opts ...interface{}) error + + // SendAlbum sends an album to the current recipient. + // See SendAlbum from bot.go. + SendAlbum(a Album, opts ...interface{}) error + + // Reply replies to the current message. + // See Reply from bot.go. + Reply(what interface{}, opts ...interface{}) error + + // Forward forwards the given message to the current recipient. + // See Forward from bot.go. + Forward(msg Editable, opts ...interface{}) error + + // ForwardTo forwards the current message to the given recipient. + // See Forward from bot.go + ForwardTo(to Recipient, opts ...interface{}) error + + // Edit edits the current message. + // See Edit from bot.go. + Edit(what interface{}, opts ...interface{}) error + + // EditCaption edits the caption of the current message. + // See EditCaption from bot.go. + EditCaption(caption string, opts ...interface{}) error + + // EditOrSend edits the current message if the update is callback, + // otherwise the content is sent to the chat as a separate message. + EditOrSend(what interface{}, opts ...interface{}) error + + // EditOrReply edits the current message if the update is callback, + // otherwise the content is replied as a separate message. + EditOrReply(what interface{}, opts ...interface{}) error + + // Delete removes the current message. + // See Delete from bot.go. + Delete() error + + // DeleteAfter waits for the duration to elapse and then removes the + // message. It handles an error automatically using b.OnError callback. + // It returns a Timer that can be used to cancel the call using its Stop method. + DeleteAfter(d time.Duration) *time.Timer + + // Notify updates the chat action for the current recipient. + // See Notify from bot.go. + Notify(action ChatAction) error + + // Ship replies to the current shipping query. + // See Ship from bot.go. + Ship(what ...interface{}) error + + // Accept finalizes the current deal. + // See Accept from bot.go. + Accept(errorMessage ...string) error + + // Answer sends a response to the current inline query. + // See Answer from bot.go. + Answer(resp *QueryResponse) error + + // Respond sends a response for the current callback query. + // See Respond from bot.go. + Respond(resp ...*CallbackResponse) error + + // Get retrieves data from the context. + Get(key string) interface{} + + // Set saves data in the context. + Set(key string, val interface{}) +} + +// nativeContext is a native implementation of the Context interface. +// "context" is taken by context package, maybe there is a better name. +type nativeContext struct { + b *Bot + u Update + lock sync.RWMutex + store map[string]interface{} +} + +func (c *nativeContext) Bot() *Bot { + return c.b +} + +func (c *nativeContext) Update() Update { + return c.u +} + +func (c *nativeContext) Message() *Message { + switch { + case c.u.Message != nil: + return c.u.Message + case c.u.Callback != nil: + return c.u.Callback.Message + case c.u.EditedMessage != nil: + return c.u.EditedMessage + case c.u.ChannelPost != nil: + if c.u.ChannelPost.PinnedMessage != nil { + return c.u.ChannelPost.PinnedMessage + } + return c.u.ChannelPost + case c.u.EditedChannelPost != nil: + return c.u.EditedChannelPost + default: + return nil + } +} + +func (c *nativeContext) Callback() *Callback { + return c.u.Callback +} + +func (c *nativeContext) Query() *Query { + return c.u.Query +} + +func (c *nativeContext) InlineResult() *InlineResult { + return c.u.InlineResult +} + +func (c *nativeContext) ShippingQuery() *ShippingQuery { + return c.u.ShippingQuery +} + +func (c *nativeContext) PreCheckoutQuery() *PreCheckoutQuery { + return c.u.PreCheckoutQuery +} + +func (c *nativeContext) ChatMember() *ChatMemberUpdate { + switch { + case c.u.ChatMember != nil: + return c.u.ChatMember + case c.u.MyChatMember != nil: + return c.u.MyChatMember + default: + return nil + } +} + +func (c *nativeContext) ChatJoinRequest() *ChatJoinRequest { + return c.u.ChatJoinRequest +} + +func (c *nativeContext) Poll() *Poll { + return c.u.Poll +} + +func (c *nativeContext) PollAnswer() *PollAnswer { + return c.u.PollAnswer +} + +func (c *nativeContext) Migration() (int64, int64) { + return c.u.Message.MigrateFrom, c.u.Message.MigrateTo +} + +func (c *nativeContext) Topic() *Topic { + m := c.u.Message + if m == nil { + return nil + } + switch { + case m.TopicCreated != nil: + return m.TopicCreated + case m.TopicReopened != nil: + return m.TopicReopened + case m.TopicEdited != nil: + return m.TopicEdited + } + return nil +} + +func (c *nativeContext) Sender() *User { + switch { + case c.u.Callback != nil: + return c.u.Callback.Sender + case c.Message() != nil: + return c.Message().Sender + case c.u.Query != nil: + return c.u.Query.Sender + case c.u.InlineResult != nil: + return c.u.InlineResult.Sender + case c.u.ShippingQuery != nil: + return c.u.ShippingQuery.Sender + case c.u.PreCheckoutQuery != nil: + return c.u.PreCheckoutQuery.Sender + case c.u.PollAnswer != nil: + return c.u.PollAnswer.Sender + case c.u.MyChatMember != nil: + return c.u.MyChatMember.Sender + case c.u.ChatMember != nil: + return c.u.ChatMember.Sender + case c.u.ChatJoinRequest != nil: + return c.u.ChatJoinRequest.Sender + default: + return nil + } +} + +func (c *nativeContext) Chat() *Chat { + switch { + case c.Message() != nil: + return c.Message().Chat + case c.u.MyChatMember != nil: + return c.u.MyChatMember.Chat + case c.u.ChatMember != nil: + return c.u.ChatMember.Chat + case c.u.ChatJoinRequest != nil: + return c.u.ChatJoinRequest.Chat + default: + return nil + } +} + +func (c *nativeContext) Recipient() Recipient { + chat := c.Chat() + if chat != nil { + return chat + } + return c.Sender() +} + +func (c *nativeContext) Text() string { + m := c.Message() + if m == nil { + return "" + } + if m.Caption != "" { + return m.Caption + } + return m.Text +} + +func (c *nativeContext) Entities() Entities { + m := c.Message() + if m == nil { + return nil + } + if len(m.CaptionEntities) > 0 { + return m.CaptionEntities + } + return m.Entities +} + +func (c *nativeContext) Data() string { + switch { + case c.u.Message != nil: + return c.u.Message.Payload + case c.u.Callback != nil: + return c.u.Callback.Data + case c.u.Query != nil: + return c.u.Query.Text + case c.u.InlineResult != nil: + return c.u.InlineResult.Query + case c.u.ShippingQuery != nil: + return c.u.ShippingQuery.Payload + case c.u.PreCheckoutQuery != nil: + return c.u.PreCheckoutQuery.Payload + default: + return "" + } +} + +func (c *nativeContext) Args() []string { + switch { + case c.u.Message != nil: + payload := strings.Trim(c.u.Message.Payload, " ") + if payload != "" { + return strings.Split(payload, " ") + } + case c.u.Callback != nil: + return strings.Split(c.u.Callback.Data, "|") + case c.u.Query != nil: + return strings.Split(c.u.Query.Text, " ") + case c.u.InlineResult != nil: + return strings.Split(c.u.InlineResult.Query, " ") + } + return nil +} + +func (c *nativeContext) Send(what interface{}, opts ...interface{}) error { + _, err := c.b.Send(c.Recipient(), what, opts...) + return err +} + +func (c *nativeContext) SendAlbum(a Album, opts ...interface{}) error { + _, err := c.b.SendAlbum(c.Recipient(), a, opts...) + return err +} + +func (c *nativeContext) Reply(what interface{}, opts ...interface{}) error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + _, err := c.b.Reply(msg, what, opts...) + return err +} + +func (c *nativeContext) Forward(msg Editable, opts ...interface{}) error { + _, err := c.b.Forward(c.Recipient(), msg, opts...) + return err +} + +func (c *nativeContext) ForwardTo(to Recipient, opts ...interface{}) error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + _, err := c.b.Forward(to, msg, opts...) + return err +} + +func (c *nativeContext) Edit(what interface{}, opts ...interface{}) error { + if c.u.InlineResult != nil { + _, err := c.b.Edit(c.u.InlineResult, what, opts...) + return err + } + if c.u.Callback != nil { + _, err := c.b.Edit(c.u.Callback, what, opts...) + return err + } + return ErrBadContext +} + +func (c *nativeContext) EditCaption(caption string, opts ...interface{}) error { + if c.u.InlineResult != nil { + _, err := c.b.EditCaption(c.u.InlineResult, caption, opts...) + return err + } + if c.u.Callback != nil { + _, err := c.b.EditCaption(c.u.Callback, caption, opts...) + return err + } + return ErrBadContext +} + +func (c *nativeContext) EditOrSend(what interface{}, opts ...interface{}) error { + err := c.Edit(what, opts...) + if err == ErrBadContext { + return c.Send(what, opts...) + } + return err +} + +func (c *nativeContext) EditOrReply(what interface{}, opts ...interface{}) error { + err := c.Edit(what, opts...) + if err == ErrBadContext { + return c.Reply(what, opts...) + } + return err +} + +func (c *nativeContext) Delete() error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + return c.b.Delete(msg) +} + +func (c *nativeContext) DeleteAfter(d time.Duration) *time.Timer { + return time.AfterFunc(d, func() { + if err := c.Delete(); err != nil { + c.b.OnError(err, c) + } + }) +} + +func (c *nativeContext) Notify(action ChatAction) error { + return c.b.Notify(c.Recipient(), action) +} + +func (c *nativeContext) Ship(what ...interface{}) error { + if c.u.ShippingQuery == nil { + return errors.New("telebot: context shipping query is nil") + } + return c.b.Ship(c.u.ShippingQuery, what...) +} + +func (c *nativeContext) Accept(errorMessage ...string) error { + if c.u.PreCheckoutQuery == nil { + return errors.New("telebot: context pre checkout query is nil") + } + return c.b.Accept(c.u.PreCheckoutQuery, errorMessage...) +} + +func (c *nativeContext) Respond(resp ...*CallbackResponse) error { + if c.u.Callback == nil { + return errors.New("telebot: context callback is nil") + } + return c.b.Respond(c.u.Callback, resp...) +} + +func (c *nativeContext) Answer(resp *QueryResponse) error { + if c.u.Query == nil { + return errors.New("telebot: context inline query is nil") + } + return c.b.Answer(c.u.Query, resp) +} + +func (c *nativeContext) Set(key string, value interface{}) { + c.lock.Lock() + defer c.lock.Unlock() + + if c.store == nil { + c.store = make(map[string]interface{}) + } + c.store[key] = value +} + +func (c *nativeContext) Get(key string) interface{} { + c.lock.RLock() + defer c.lock.RUnlock() + return c.store[key] +} diff --git a/vendor/gopkg.in/telebot.v3/editable.go b/vendor/gopkg.in/telebot.v3/editable.go new file mode 100644 index 000000000..ec1fb5b93 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/editable.go @@ -0,0 +1,30 @@ +package telebot + +// Editable is an interface for all objects that +// provide "message signature", a pair of 32-bit +// message ID and 64-bit chat ID, both required +// for edit operations. +// +// Use case: DB model struct for messages to-be +// edited with, say two columns: msg_id,chat_id +// could easily implement MessageSig() making +// instances of stored messages editable. +type Editable interface { + // MessageSig is a "message signature". + // + // For inline messages, return chatID = 0. + MessageSig() (messageID string, chatID int64) +} + +// StoredMessage is an example struct suitable for being +// stored in the database as-is or being embedded into +// a larger struct, which is often the case (you might +// want to store some metadata alongside, or might not.) +type StoredMessage struct { + MessageID string `sql:"message_id" json:"message_id"` + ChatID int64 `sql:"chat_id" json:"chat_id"` +} + +func (x StoredMessage) MessageSig() (string, int64) { + return x.MessageID, x.ChatID +} diff --git a/vendor/gopkg.in/telebot.v3/errors.go b/vendor/gopkg.in/telebot.v3/errors.go new file mode 100644 index 000000000..0197e1934 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/errors.go @@ -0,0 +1,260 @@ +package telebot + +import ( + "fmt" + "strings" +) + +type ( + Error struct { + Code int + Description string + Message string + } + + FloodError struct { + err *Error + RetryAfter int + } + + GroupError struct { + err *Error + MigratedTo int64 + } +) + +// ʔ returns description of error. +// A tiny shortcut to make code clearer. +func (err *Error) ʔ() string { + return err.Description +} + +// Error implements error interface. +func (err *Error) Error() string { + msg := err.Message + if msg == "" { + split := strings.Split(err.Description, ": ") + if len(split) == 2 { + msg = split[1] + } else { + msg = err.Description + } + } + return fmt.Sprintf("telegram: %s (%d)", msg, err.Code) +} + +// Error implements error interface. +func (err FloodError) Error() string { + return err.err.Error() +} + +// Error implements error interface. +func (err GroupError) Error() string { + return err.err.Error() +} + +// NewError returns new Error instance with given description. +// First element of msgs is Description. The second is optional Message. +func NewError(code int, msgs ...string) *Error { + err := &Error{Code: code} + if len(msgs) >= 1 { + err.Description = msgs[0] + } + if len(msgs) >= 2 { + err.Message = msgs[1] + } + return err +} + +// General errors +var ( + ErrTooLarge = NewError(400, "Request Entity Too Large") + ErrUnauthorized = NewError(401, "Unauthorized") + ErrNotFound = NewError(404, "Not Found") + ErrInternal = NewError(500, "Internal Server Error") +) + +// Bad request errors +var ( + ErrBadButtonData = NewError(400, "Bad Request: BUTTON_DATA_INVALID") + ErrBadUserID = NewError(400, "Bad Request: USER_ID_INVALID") + ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options") + ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content") + ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited") + ErrCantRemoveOwner = NewError(400, "Bad Request: can't remove chat owner") + ErrCantUploadFile = NewError(400, "Bad Request: can't upload file by URL") + ErrCantUseMediaInAlbum = NewError(400, "Bad Request: can't use the media of the specified type in the album") + ErrChatAboutNotModified = NewError(400, "Bad Request: chat description is not modified") + ErrChatNotFound = NewError(400, "Bad Request: chat not found") + ErrEmptyChatID = NewError(400, "Bad Request: chat_id is empty") + ErrEmptyMessage = NewError(400, "Bad Request: message must be non-empty") + ErrEmptyText = NewError(400, "Bad Request: text is empty") + ErrFailedImageProcess = NewError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed") + ErrGroupMigrated = NewError(400, "Bad Request: group chat was upgraded to a supergroup chat") + ErrMessageNotModified = NewError(400, "Bad Request: message is not modified") + ErrNoRightsToDelete = NewError(400, "Bad Request: message can't be deleted") + ErrNoRightsToRestrict = NewError(400, "Bad Request: not enough rights to restrict/unrestrict chat member") + ErrNoRightsToSend = NewError(400, "Bad Request: have no rights to send a message") + ErrNoRightsToSendGifs = NewError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat") + ErrNoRightsToSendPhoto = NewError(400, "Bad Request: not enough rights to send photos to the chat") + ErrNoRightsToSendStickers = NewError(400, "Bad Request: not enough rights to send stickers to the chat") + ErrNotFoundToDelete = NewError(400, "Bad Request: message to delete not found") + ErrNotFoundToForward = NewError(400, "Bad Request: message to forward not found") + ErrNotFoundToReply = NewError(400, "Bad Request: reply message not found") + ErrQueryTooOld = NewError(400, "Bad Request: query is too old and response timeout expired or query ID is invalid") + ErrSameMessageContent = NewError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") + ErrStickerEmojisInvalid = NewError(400, "Bad Request: invalid sticker emojis") + ErrStickerSetInvalid = NewError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid") + ErrStickerSetInvalidName = NewError(400, "Bad Request: invalid sticker set name is specified") + ErrStickerSetNameOccupied = NewError(400, "Bad Request: sticker set name is already occupied") + ErrTooLongMarkup = NewError(400, "Bad Request: reply markup is too long") + ErrTooLongMessage = NewError(400, "Bad Request: message is too long") + ErrUserIsAdmin = NewError(400, "Bad Request: user is an administrator of the chat") + ErrWrongFileID = NewError(400, "Bad Request: wrong file identifier/HTTP URL specified") + ErrWrongFileIDCharacter = NewError(400, "Bad Request: wrong remote file id specified: Wrong character in the string") + ErrWrongFileIDLength = NewError(400, "Bad Request: wrong remote file id specified: Wrong string length") + ErrWrongFileIDPadding = NewError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string") + ErrWrongFileIDSymbol = NewError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol") + ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content") + ErrWrongURL = NewError(400, "Bad Request: wrong HTTP URL specified") + ErrForwardMessage = NewError(400, "Bad Request: administrators of the chat restricted message forwarding") + ErrUserAlreadyParticipant = NewError(400, "Bad Request: USER_ALREADY_PARTICIPANT", "User is already a participant") + ErrHideRequesterMissing = NewError(400, "Bad Request: HIDE_REQUESTER_MISSING") + ErrChannelsTooMuch = NewError(400, "Bad Request: CHANNELS_TOO_MUCH") + ErrChannelsTooMuchUser = NewError(400, "Bad Request: USER_CHANNELS_TOO_MUCH") +) + +// Forbidden errors +var ( + ErrBlockedByUser = NewError(403, "Forbidden: bot was blocked by the user") + ErrKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat") + ErrKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat") + ErrKickedFromChannel = NewError(403, "Forbidden: bot was kicked from the channel chat") + ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user") + ErrUserIsDeactivated = NewError(403, "Forbidden: user is deactivated") +) + +// Err returns Error instance by given description. +func Err(s string) error { + switch s { + case ErrTooLarge.ʔ(): + return ErrTooLarge + case ErrUnauthorized.ʔ(): + return ErrUnauthorized + case ErrNotFound.ʔ(): + return ErrNotFound + case ErrInternal.ʔ(): + return ErrInternal + case ErrBadButtonData.ʔ(): + return ErrBadButtonData + case ErrBadUserID.ʔ(): + return ErrBadUserID + case ErrBadPollOptions.ʔ(): + return ErrBadPollOptions + case ErrBadURLContent.ʔ(): + return ErrBadURLContent + case ErrCantEditMessage.ʔ(): + return ErrCantEditMessage + case ErrCantRemoveOwner.ʔ(): + return ErrCantRemoveOwner + case ErrCantUploadFile.ʔ(): + return ErrCantUploadFile + case ErrCantUseMediaInAlbum.ʔ(): + return ErrCantUseMediaInAlbum + case ErrChatAboutNotModified.ʔ(): + return ErrChatAboutNotModified + case ErrChatNotFound.ʔ(): + return ErrChatNotFound + case ErrEmptyChatID.ʔ(): + return ErrEmptyChatID + case ErrEmptyMessage.ʔ(): + return ErrEmptyMessage + case ErrEmptyText.ʔ(): + return ErrEmptyText + case ErrFailedImageProcess.ʔ(): + return ErrFailedImageProcess + case ErrGroupMigrated.ʔ(): + return ErrGroupMigrated + case ErrMessageNotModified.ʔ(): + return ErrMessageNotModified + case ErrNoRightsToDelete.ʔ(): + return ErrNoRightsToDelete + case ErrNoRightsToRestrict.ʔ(): + return ErrNoRightsToRestrict + case ErrNoRightsToSend.ʔ(): + return ErrNoRightsToSend + case ErrNoRightsToSendGifs.ʔ(): + return ErrNoRightsToSendGifs + case ErrNoRightsToSendPhoto.ʔ(): + return ErrNoRightsToSendPhoto + case ErrNoRightsToSendStickers.ʔ(): + return ErrNoRightsToSendStickers + case ErrNotFoundToDelete.ʔ(): + return ErrNotFoundToDelete + case ErrNotFoundToForward.ʔ(): + return ErrNotFoundToForward + case ErrNotFoundToReply.ʔ(): + return ErrNotFoundToReply + case ErrQueryTooOld.ʔ(): + return ErrQueryTooOld + case ErrSameMessageContent.ʔ(): + return ErrSameMessageContent + case ErrStickerEmojisInvalid.ʔ(): + return ErrStickerEmojisInvalid + case ErrStickerSetInvalid.ʔ(): + return ErrStickerSetInvalid + case ErrStickerSetInvalidName.ʔ(): + return ErrStickerSetInvalidName + case ErrStickerSetNameOccupied.ʔ(): + return ErrStickerSetNameOccupied + case ErrTooLongMarkup.ʔ(): + return ErrTooLongMarkup + case ErrTooLongMessage.ʔ(): + return ErrTooLongMessage + case ErrUserIsAdmin.ʔ(): + return ErrUserIsAdmin + case ErrWrongFileID.ʔ(): + return ErrWrongFileID + case ErrWrongFileIDCharacter.ʔ(): + return ErrWrongFileIDCharacter + case ErrWrongFileIDLength.ʔ(): + return ErrWrongFileIDLength + case ErrWrongFileIDPadding.ʔ(): + return ErrWrongFileIDPadding + case ErrWrongFileIDSymbol.ʔ(): + return ErrWrongFileIDSymbol + case ErrWrongTypeOfContent.ʔ(): + return ErrWrongTypeOfContent + case ErrWrongURL.ʔ(): + return ErrWrongURL + case ErrBlockedByUser.ʔ(): + return ErrBlockedByUser + case ErrKickedFromGroup.ʔ(): + return ErrKickedFromGroup + case ErrKickedFromSuperGroup.ʔ(): + return ErrKickedFromSuperGroup + case ErrKickedFromChannel.ʔ(): + return ErrKickedFromChannel + case ErrNotStartedByUser.ʔ(): + return ErrNotStartedByUser + case ErrUserIsDeactivated.ʔ(): + return ErrUserIsDeactivated + case ErrForwardMessage.ʔ(): + return ErrForwardMessage + case ErrUserAlreadyParticipant.ʔ(): + return ErrUserAlreadyParticipant + case ErrHideRequesterMissing.ʔ(): + return ErrHideRequesterMissing + case ErrChannelsTooMuch.ʔ(): + return ErrChannelsTooMuch + case ErrChannelsTooMuchUser.ʔ(): + return ErrChannelsTooMuchUser + default: + return nil + } +} + +// wrapError returns new wrapped telebot-related error. +func wrapError(err error) error { + return fmt.Errorf("telebot: %w", err) +} diff --git a/vendor/gopkg.in/telebot.v3/file.go b/vendor/gopkg.in/telebot.v3/file.go new file mode 100644 index 000000000..14c40f97f --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/file.go @@ -0,0 +1,87 @@ +package telebot + +import ( + "io" + "os" +) + +// File object represents any sort of file. +type File struct { + FileID string `json:"file_id"` + UniqueID string `json:"file_unique_id"` + FileSize int64 `json:"file_size"` + + // FilePath is used for files on Telegram server. + FilePath string `json:"file_path"` + + // FileLocal is used for files on local file system. + FileLocal string `json:"file_local"` + + // FileURL is used for file on the internet. + FileURL string `json:"file_url"` + + // FileReader is used for file backed with io.Reader. + FileReader io.Reader `json:"-"` + + fileName string +} + +// FromDisk constructs a new local (on-disk) file object. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromDisk("chicken.jpg")} +// +func FromDisk(filename string) File { + return File{FileLocal: filename} +} + +// FromURL constructs a new file on provided HTTP URL. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromURL("https://site.com/picture.jpg")} +// +func FromURL(url string) File { + return File{FileURL: url} +} + +// FromReader constructs a new file from io.Reader. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromReader(bytes.NewReader(...))} +// +func FromReader(reader io.Reader) File { + return File{FileReader: reader} +} + +func (f *File) stealRef(g *File) { + if g.OnDisk() { + f.FileLocal = g.FileLocal + } + + if g.FileURL != "" { + f.FileURL = g.FileURL + } +} + +// InCloud tells whether the file is present on Telegram servers. +func (f *File) InCloud() bool { + return f.FileID != "" +} + +// OnDisk will return true if file is present on disk. +func (f *File) OnDisk() bool { + _, err := os.Stat(f.FileLocal) + return err == nil +} diff --git a/vendor/gopkg.in/telebot.v3/game.go b/vendor/gopkg.in/telebot.v3/game.go new file mode 100644 index 000000000..0a1276d55 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/game.go @@ -0,0 +1,99 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +// Game object represents a game. +// Their short names acts as unique identifiers. +type Game struct { + Name string `json:"game_short_name"` + + Title string `json:"title"` + Description string `json:"description"` + Photo *Photo `json:"photo"` + + // (Optional) + Text string `json:"text"` + Entities []MessageEntity `json:"text_entities"` + Animation *Animation `json:"animation"` +} + +// GameHighScore object represents one row +// of the high scores table for a game. +type GameHighScore struct { + User *User `json:"user"` + Position int `json:"position"` + + Score int `json:"score"` + Force bool `json:"force"` + NoEdit bool `json:"disable_edit_message"` +} + +// GameScores returns the score of the specified user +// and several of their neighbors in a game. +// +// This function will panic upon nil Editable. +// +// Currently, it returns scores for the target user, +// plus two of their closest neighbors on each side. +// Will also return the top three users +// if the user and his neighbors are not among them. +// +func (b *Bot) GameScores(user Recipient, msg Editable) ([]GameHighScore, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "user_id": user.Recipient(), + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.Raw("getGameHighScores", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []GameHighScore + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + return resp.Result, nil +} + +// SetGameScore sets the score of the specified user in a game. +// +// If the message was sent by the bot, returns the edited Message, +// otherwise returns nil and ErrTrueResult. +// +func (b *Bot) SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "user_id": user.Recipient(), + "score": strconv.Itoa(score.Score), + "force": strconv.FormatBool(score.Force), + "disable_edit_message": strconv.FormatBool(score.NoEdit), + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.Raw("setGameScore", params) + if err != nil { + return nil, err + } + return extractMessage(data) +} diff --git a/vendor/gopkg.in/telebot.v3/inline.go b/vendor/gopkg.in/telebot.v3/inline.go new file mode 100644 index 000000000..2a8895822 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/inline.go @@ -0,0 +1,139 @@ +package telebot + +import ( + "encoding/json" + "fmt" +) + +// Query is an incoming inline query. When the user sends +// an empty query, your bot could return some default or +// trending results. +type Query struct { + // Unique identifier for this query. 1-64 bytes. + ID string `json:"id"` + + // Sender. + Sender *User `json:"from"` + + // Sender location, only for bots that request user location. + Location *Location `json:"location"` + + // Text of the query (up to 512 characters). + Text string `json:"query"` + + // Offset of the results to be returned, can be controlled by the bot. + Offset string `json:"offset"` + + // ChatType of the type of the chat, from which the inline query was sent. + ChatType string `json:"chat_type"` +} + +// QueryResponse builds a response to an inline Query. +type QueryResponse struct { + // The ID of the query to which this is a response. + // + // Note: Telebot sets this field automatically! + QueryID string `json:"inline_query_id"` + + // The results for the inline query. + Results Results `json:"results"` + + // (Optional) The maximum amount of time in seconds that the result + // of the inline query may be cached on the server. + CacheTime int `json:"cache_time,omitempty"` + + // (Optional) Pass True, if results may be cached on the server side + // only for the user that sent the query. By default, results may + // be returned to any user who sends the same query. + IsPersonal bool `json:"is_personal"` + + // (Optional) Pass the offset that a client should send in the next + // query with the same text to receive more results. Pass an empty + // string if there are no more results or if you don‘t support + // pagination. Offset length can’t exceed 64 bytes. + NextOffset string `json:"next_offset"` + + // (Optional) If passed, clients will display a button with specified + // text that switches the user to a private chat with the bot and sends + // the bot a start message with the parameter switch_pm_parameter. + SwitchPMText string `json:"switch_pm_text,omitempty"` + + // (Optional) Parameter for the start message sent to the bot when user + // presses the switch button. + SwitchPMParameter string `json:"switch_pm_parameter,omitempty"` +} + +// InlineResult represents a result of an inline query that was chosen +// by the user and sent to their chat partner. +type InlineResult struct { + Sender *User `json:"from"` + Location *Location `json:"location,omitempty"` + ResultID string `json:"result_id"` + Query string `json:"query"` + MessageID string `json:"inline_message_id"` // inline messages only! +} + +// MessageSig satisfies Editable interface. +func (ir *InlineResult) MessageSig() (string, int64) { + return ir.MessageID, 0 +} + +// Result represents one result of an inline query. +type Result interface { + ResultID() string + SetResultID(string) + SetParseMode(ParseMode) + SetContent(InputMessageContent) + SetReplyMarkup(*ReplyMarkup) + Process(*Bot) +} + +// Results is a slice wrapper for convenient marshalling. +type Results []Result + +// MarshalJSON makes sure IQRs have proper IDs and Type variables set. +func (results Results) MarshalJSON() ([]byte, error) { + for i, result := range results { + if result.ResultID() == "" { + result.SetResultID(fmt.Sprintf("%d", &results[i])) + } + if err := inferIQR(result); err != nil { + return nil, err + } + } + + return json.Marshal([]Result(results)) +} + +func inferIQR(result Result) error { + switch r := result.(type) { + case *ArticleResult: + r.Type = "article" + case *AudioResult: + r.Type = "audio" + case *ContactResult: + r.Type = "contact" + case *DocumentResult: + r.Type = "document" + case *GifResult: + r.Type = "gif" + case *LocationResult: + r.Type = "location" + case *Mpeg4GifResult: + r.Type = "mpeg4_gif" + case *PhotoResult: + r.Type = "photo" + case *VenueResult: + r.Type = "venue" + case *VideoResult: + r.Type = "video" + case *VoiceResult: + r.Type = "voice" + case *StickerResult: + r.Type = "sticker" + default: + return fmt.Errorf("telebot: result %v is not supported", result) + } + + return nil +} diff --git a/vendor/gopkg.in/telebot.v3/inline_types.go b/vendor/gopkg.in/telebot.v3/inline_types.go new file mode 100644 index 000000000..d93cffc20 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/inline_types.go @@ -0,0 +1,373 @@ +package telebot + +// ResultBase must be embedded into all IQRs. +type ResultBase struct { + // Unique identifier for this result, 1-64 Bytes. + // If left unspecified, a 64-bit FNV-1 hash will be calculated + ID string `json:"id"` + + // Ignore. This field gets set automatically. + Type string `json:"type"` + + // Optional. Send Markdown or HTML, if you want Telegram apps to show + // bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode ParseMode `json:"parse_mode,omitempty"` + + // Optional. Content of the message to be sent. + Content InputMessageContent `json:"input_message_content,omitempty"` + + // Optional. Inline keyboard attached to the message. + ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` +} + +// ResultID returns ResultBase.ID. +func (r *ResultBase) ResultID() string { + return r.ID +} + +// SetResultID sets ResultBase.ID. +func (r *ResultBase) SetResultID(id string) { + r.ID = id +} + +// SetParseMode sets ResultBase.ParseMode. +func (r *ResultBase) SetParseMode(mode ParseMode) { + r.ParseMode = mode +} + +// SetContent sets ResultBase.Content. +func (r *ResultBase) SetContent(content InputMessageContent) { + r.Content = content +} + +// SetReplyMarkup sets ResultBase.ReplyMarkup. +func (r *ResultBase) SetReplyMarkup(markup *ReplyMarkup) { + r.ReplyMarkup = markup +} + +func (r *ResultBase) Process(b *Bot) { + if r.ParseMode == ModeDefault { + r.ParseMode = b.parseMode + } + if r.Content != nil { + c, ok := r.Content.(*InputTextMessageContent) + if ok && c.ParseMode == ModeDefault { + c.ParseMode = r.ParseMode + } + } + if r.ReplyMarkup != nil { + processButtons(r.ReplyMarkup.InlineKeyboard) + } +} + +// ArticleResult represents a link to an article or web page. +type ArticleResult struct { + ResultBase + + // Title of the result. + Title string `json:"title"` + + // Message text. Shortcut (and mutually exclusive to) specifying + // InputMessageContent. + Text string `json:"message_text,omitempty"` + + // Optional. URL of the result. + URL string `json:"url,omitempty"` + + // Optional. Pass True, if you don't want the URL to be shown in the message. + HideURL bool `json:"hide_url,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// AudioResult represents a link to an mp3 audio file. +type AudioResult struct { + ResultBase + + // Title. + Title string `json:"title"` + + // A valid URL for the audio file. + URL string `json:"audio_url"` + + // Optional. Performer. + Performer string `json:"performer,omitempty"` + + // Optional. Audio duration in seconds. + Duration int `json:"audio_duration,omitempty"` + + // Optional. Caption, 0-1024 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"audio_file_id,omitempty"` +} + +// ContactResult represents a contact with a phone number. +type ContactResult struct { + ResultBase + + // Contact's phone number. + PhoneNumber string `json:"phone_number"` + + // Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes. + VCard string `json:"vcard,omitempty"` + + // Contact's first name. + FirstName string `json:"first_name"` + + // Optional. Contact's last name. + LastName string `json:"last_name,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// DocumentResult represents a link to a file. +type DocumentResult struct { + ResultBase + + // Title for the result. + Title string `json:"title"` + + // A valid URL for the file + URL string `json:"document_url"` + + // Mime type of the content of the file, either “application/pdf” or + // “application/zip”. + MIME string `json:"mime_type"` + + // Optional. Caption of the document to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. URL of the thumbnail (jpeg only) for the file. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"document_file_id,omitempty"` +} + +// GifResult represents a link to an animated GIF file. +type GifResult struct { + ResultBase + + // A valid URL for the GIF file. File size must not exceed 1MB. + URL string `json:"gif_url"` + + // Optional. Width of the GIF. + Width int `json:"gif_width,omitempty"` + + // Optional. Height of the GIF. + Height int `json:"gif_height,omitempty"` + + // Optional. Duration of the GIF. + Duration int `json:"gif_duration,omitempty"` + + // URL of the static thumbnail for the result (jpeg or gif). + ThumbURL string `json:"thumb_url"` + + // Optional. MIME type of the thumbnail, must be one of + // “image/jpeg”, “image/gif”, or “video/mp4”. + ThumbMIME string `json:"thumb_mime_type,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Caption of the GIF file to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"gif_file_id,omitempty"` +} + +// LocationResult represents a location on a map. +type LocationResult struct { + ResultBase + + Location + + // Location title. + Title string `json:"title"` + + // Optional. Url of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` +} + +// Mpeg4GifResult represents a link to a video animation +// (H.264/MPEG-4 AVC video without sound). +type Mpeg4GifResult struct { + ResultBase + + // A valid URL for the MP4 file. + URL string `json:"mpeg4_url"` + + // Optional. Video width. + Width int `json:"mpeg4_width,omitempty"` + + // Optional. Video height. + Height int `json:"mpeg4_height,omitempty"` + + // Optional. Video duration. + Duration int `json:"mpeg4_duration,omitempty"` + + // URL of the static thumbnail (jpeg or gif) for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. MIME type of the thumbnail, must be one of + // “image/jpeg”, “image/gif”, or “video/mp4”. + ThumbMIME string `json:"thumb_mime_type,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Caption of the MPEG-4 file to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"mpeg4_file_id,omitempty"` +} + +// PhotoResult represents a link to a photo. +type PhotoResult struct { + ResultBase + + // A valid URL of the photo. Photo must be in jpeg format. + // Photo size must not exceed 5MB. + URL string `json:"photo_url"` + + // Optional. Width of the photo. + Width int `json:"photo_width,omitempty"` + + // Optional. Height of the photo. + Height int `json:"photo_height,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. Caption of the photo to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // URL of the thumbnail for the photo. + ThumbURL string `json:"thumb_url"` + + // If Cache != "", it'll be used instead + Cache string `json:"photo_file_id,omitempty"` +} + +// VenueResult represents a venue. +type VenueResult struct { + ResultBase + + Location + + // Title of the venue. + Title string `json:"title"` + + // Address of the venue. + Address string `json:"address"` + + // Optional. Foursquare identifier of the venue if known. + FoursquareID string `json:"foursquare_id,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// VideoResult represents a link to a page containing an embedded +// video player or a video file. +type VideoResult struct { + ResultBase + + // A valid URL for the embedded video player or video file. + URL string `json:"video_url"` + + // Mime type of the content of video url, “text/html” or “video/mp4”. + MIME string `json:"mime_type"` + + // URL of the thumbnail (jpeg only) for the video. + ThumbURL string `json:"thumb_url"` + + // Title for the result. + Title string `json:"title"` + + // Optional. Caption of the video to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // Optional. Video width. + Width int `json:"video_width,omitempty"` + + // Optional. Video height. + Height int `json:"video_height,omitempty"` + + // Optional. Video duration in seconds. + Duration int `json:"video_duration,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"video_file_id,omitempty"` +} + +// VoiceResult represents a link to a voice recording in an .ogg +// container encoded with OPUS. +type VoiceResult struct { + ResultBase + + // A valid URL for the voice recording. + URL string `json:"voice_url"` + + // Recording title. + Title string `json:"title"` + + // Optional. Recording duration in seconds. + Duration int `json:"voice_duration"` + + // Optional. Caption, 0-1024 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"voice_file_id,omitempty"` +} + +// StickerResult represents an inline cached sticker response. +type StickerResult struct { + ResultBase + + // If Cache != "", it'll be used instead + Cache string `json:"sticker_file_id,omitempty"` +} diff --git a/vendor/gopkg.in/telebot.v3/input_types.go b/vendor/gopkg.in/telebot.v3/input_types.go new file mode 100644 index 000000000..8186c0727 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/input_types.go @@ -0,0 +1,73 @@ +package telebot + +// InputMessageContent objects represent the content of a message to be sent +// as a result of an inline query. +type InputMessageContent interface { + IsInputMessageContent() bool +} + +// InputTextMessageContent represents the content of a text message to be +// sent as the result of an inline query. +type InputTextMessageContent struct { + // Text of the message to be sent, 1-4096 characters. + Text string `json:"message_text"` + + // Optional. Send Markdown or HTML, if you want Telegram apps to show + // bold, italic, fixed-width text or inline URLs in your bot's message. + ParseMode string `json:"parse_mode,omitempty"` + + // Optional. Disables link previews for links in the sent message. + DisablePreview bool `json:"disable_web_page_preview"` +} + +func (input *InputTextMessageContent) IsInputMessageContent() bool { + return true +} + +// InputLocationMessageContent represents the content of a location message +// to be sent as the result of an inline query. +type InputLocationMessageContent struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` +} + +func (input *InputLocationMessageContent) IsInputMessageContent() bool { + return true +} + +// InputVenueMessageContent represents the content of a venue message to +// be sent as the result of an inline query. +type InputVenueMessageContent struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` + + // Name of the venue. + Title string `json:"title"` + + // Address of the venue. + Address string `json:"address"` + + // Optional. Foursquare identifier of the venue, if known. + FoursquareID string `json:"foursquare_id,omitempty"` +} + +func (input *InputVenueMessageContent) IsInputMessageContent() bool { + return true +} + +// InputContactMessageContent represents the content of a contact +// message to be sent as the result of an inline query. +type InputContactMessageContent struct { + // Contact's phone number. + PhoneNumber string `json:"phone_number"` + + // Contact's first name. + FirstName string `json:"first_name"` + + // Optional. Contact's last name. + LastName string `json:"last_name,omitempty"` +} + +func (input *InputContactMessageContent) IsInputMessageContent() bool { + return true +} diff --git a/vendor/gopkg.in/telebot.v3/markup.go b/vendor/gopkg.in/telebot.v3/markup.go new file mode 100644 index 000000000..29236db34 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/markup.go @@ -0,0 +1,365 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "strings" +) + +// ReplyMarkup controls two convenient options for bot-user communications +// such as reply keyboard and inline "keyboard" (a grid of buttons as a part +// of the message). +type ReplyMarkup struct { + // InlineKeyboard is a grid of InlineButtons displayed in the message. + // + // Note: DO NOT confuse with ReplyKeyboard and other keyboard properties! + InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"` + + // ReplyKeyboard is a grid, consisting of keyboard buttons. + // + // Note: you don't need to set HideCustomKeyboard field to show custom keyboard. + ReplyKeyboard [][]ReplyButton `json:"keyboard,omitempty"` + + // ForceReply forces Telegram clients to display + // a reply interface to the user (act as if the user + // has selected the bot‘s message and tapped "Reply"). + ForceReply bool `json:"force_reply,omitempty"` + + // Requests clients to resize the keyboard vertically for optimal fit + // (e.g. make the keyboard smaller if there are just two rows of buttons). + // + // Defaults to false, in which case the custom keyboard is always of the + // same height as the app's standard keyboard. + ResizeKeyboard bool `json:"resize_keyboard,omitempty"` + + // Requests clients to hide the reply keyboard as soon as it's been used. + // + // Defaults to false. + OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"` + + // Requests clients to remove the reply keyboard. + // + // Defaults to false. + RemoveKeyboard bool `json:"remove_keyboard,omitempty"` + + // Use this param if you want to force reply from + // specific users only. + // + // Targets: + // 1) Users that are @mentioned in the text of the Message object; + // 2) If the bot's message is a reply (has SendOptions.ReplyTo), + // sender of the original message. + Selective bool `json:"selective,omitempty"` + + // Placeholder will be shown in the input field when the reply is active. + Placeholder string `json:"input_field_placeholder,omitempty"` + + // IsPersistent allows to control when the keyboard is shown. + IsPersistent bool `json:"is_persistent,omitempty"` +} + +func (r *ReplyMarkup) copy() *ReplyMarkup { + cp := *r + + if len(r.ReplyKeyboard) > 0 { + cp.ReplyKeyboard = make([][]ReplyButton, len(r.ReplyKeyboard)) + for i, row := range r.ReplyKeyboard { + cp.ReplyKeyboard[i] = make([]ReplyButton, len(row)) + copy(cp.ReplyKeyboard[i], row) + } + } + + if len(r.InlineKeyboard) > 0 { + cp.InlineKeyboard = make([][]InlineButton, len(r.InlineKeyboard)) + for i, row := range r.InlineKeyboard { + cp.InlineKeyboard[i] = make([]InlineButton, len(row)) + copy(cp.InlineKeyboard[i], row) + } + } + + return &cp +} + +// Btn is a constructor button, which will later become either a reply, or an inline button. +type Btn struct { + Unique string `json:"unique,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + Data string `json:"callback_data,omitempty"` + InlineQuery string `json:"switch_inline_query,omitempty"` + InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"` + Login *Login `json:"login_url,omitempty"` + WebApp *WebApp `json:"web_app,omitempty"` + Contact bool `json:"request_contact,omitempty"` + Location bool `json:"request_location,omitempty"` + Poll PollType `json:"request_poll,omitempty"` + User *ReplyRecipient `json:"request_user,omitempty"` + Chat *ReplyRecipient `json:"request_chat,omitempty"` +} + +// Row represents an array of buttons, a row. +type Row []Btn + +// Row creates a row of buttons. +func (r *ReplyMarkup) Row(many ...Btn) Row { + return many +} + +// Split splits the keyboard into the rows with N maximum number of buttons. +// For example, if you pass six buttons and 3 as the max, you get two rows with +// three buttons in each. +// +// `Split(3, []Btn{six buttons...}) -> [[1, 2, 3], [4, 5, 6]]` +// `Split(2, []Btn{six buttons...}) -> [[1, 2],[3, 4],[5, 6]]` +func (r *ReplyMarkup) Split(max int, btns []Btn) []Row { + rows := make([]Row, (max-1+len(btns))/max) + for i, b := range btns { + i /= max + rows[i] = append(rows[i], b) + } + return rows +} + +func (r *ReplyMarkup) Inline(rows ...Row) { + inlineKeys := make([][]InlineButton, 0, len(rows)) + for i, row := range rows { + keys := make([]InlineButton, 0, len(row)) + for j, btn := range row { + btn := btn.Inline() + if btn == nil { + panic(fmt.Sprintf( + "telebot: button row %d column %d is not an inline button", + i, j)) + } + keys = append(keys, *btn) + } + inlineKeys = append(inlineKeys, keys) + } + + r.InlineKeyboard = inlineKeys +} + +func (r *ReplyMarkup) Reply(rows ...Row) { + replyKeys := make([][]ReplyButton, 0, len(rows)) + for i, row := range rows { + keys := make([]ReplyButton, 0, len(row)) + for j, btn := range row { + btn := btn.Reply() + if btn == nil { + panic(fmt.Sprintf( + "telebot: button row %d column %d is not a reply button", + i, j)) + } + keys = append(keys, *btn) + } + replyKeys = append(replyKeys, keys) + } + + r.ReplyKeyboard = replyKeys +} + +func (r *ReplyMarkup) Text(text string) Btn { + return Btn{Text: text} +} + +func (r *ReplyMarkup) Data(text, unique string, data ...string) Btn { + return Btn{ + Unique: unique, + Text: text, + Data: strings.Join(data, "|"), + } +} + +func (r *ReplyMarkup) URL(text, url string) Btn { + return Btn{Text: text, URL: url} +} + +func (r *ReplyMarkup) Query(text, query string) Btn { + return Btn{Text: text, InlineQuery: query} +} + +func (r *ReplyMarkup) QueryChat(text, query string) Btn { + return Btn{Text: text, InlineQueryChat: query} +} + +func (r *ReplyMarkup) Contact(text string) Btn { + return Btn{Contact: true, Text: text} +} + +func (r *ReplyMarkup) Location(text string) Btn { + return Btn{Location: true, Text: text} +} + +func (r *ReplyMarkup) Poll(text string, poll PollType) Btn { + return Btn{Poll: poll, Text: text} +} + +func (r *ReplyMarkup) User(text string, user *ReplyRecipient) Btn { + return Btn{Text: text, User: user} +} + +func (r *ReplyMarkup) Chat(text string, chat *ReplyRecipient) Btn { + return Btn{Text: text, Chat: chat} +} + +func (r *ReplyMarkup) Login(text string, login *Login) Btn { + return Btn{Login: login, Text: text} +} + +func (r *ReplyMarkup) WebApp(text string, app *WebApp) Btn { + return Btn{Text: text, WebApp: app} +} + +// ReplyButton represents a button displayed in reply-keyboard. +// +// Set either Contact or Location to true in order to request +// sensitive info, such as user's phone number or current location. +type ReplyButton struct { + Text string `json:"text"` + + Contact bool `json:"request_contact,omitempty"` + Location bool `json:"request_location,omitempty"` + Poll PollType `json:"request_poll,omitempty"` + User *ReplyRecipient `json:"request_user,omitempty"` + Chat *ReplyRecipient `json:"request_chat,omitempty"` + WebApp *WebApp `json:"web_app,omitempty"` +} + +// MarshalJSON implements json.Marshaler. It allows passing PollType as a +// keyboard's poll type instead of KeyboardButtonPollType object. +func (pt PollType) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + }{ + Type: string(pt), + }) +} + +// ReplyRecipient combines both KeyboardButtonRequestUser +// and KeyboardButtonRequestChat objects. Use inside ReplyButton +// to request the user or chat sharing with respective settings. +// +// To pass the pointers to bool use a special tele.Flag function, +// that way you will be able to reflect the three-state bool (nil, false, true). +type ReplyRecipient struct { + ID int32 `json:"request_id"` + + Bot *bool `json:"user_is_bot,omitempty"` // user only, optional + Premium *bool `json:"user_is_premium,omitempty"` // user only, optional + + Channel bool `json:"chat_is_channel,omitempty"` // chat only, required + Forum *bool `json:"chat_is_forum,omitempty"` // chat only, optional + WithUsername *bool `json:"chat_has_username,omitempty"` // chat only, optional + Created *bool `json:"chat_is_created,omitempty"` // chat only, optional + UserRights *Rights `json:"user_administrator_rights,omitempty"` // chat only, optional + BotRights *Rights `json:"bot_administrator_rights,omitempty"` // chat only, optional + BotMember *bool `json:"bot_is_member,omitempty"` // chat only, optional +} + +// RecipientShared combines both UserShared and ChatShared objects. +type RecipientShared struct { + ID int32 `json:"request_id"` + UserID int64 `json:"user_id"` + ChatID int64 `json:"chat_id"` +} + +// InlineButton represents a button displayed in the message. +type InlineButton struct { + // Unique slagish name for this kind of button, + // try to be as specific as possible. + // + // It will be used as a callback endpoint. + Unique string `json:"unique,omitempty"` + + Text string `json:"text"` + URL string `json:"url,omitempty"` + Data string `json:"callback_data,omitempty"` + InlineQuery string `json:"switch_inline_query,omitempty"` + InlineQueryChat string `json:"switch_inline_query_current_chat"` + Login *Login `json:"login_url,omitempty"` + WebApp *WebApp `json:"web_app,omitempty"` +} + +// MarshalJSON implements json.Marshaler interface. +// It needed to avoid InlineQueryChat and Login or WebApp fields conflict. +// If you have Login or WebApp field in your button, InlineQueryChat must be skipped. +func (t *InlineButton) MarshalJSON() ([]byte, error) { + type IB InlineButton + + if t.Login != nil || t.WebApp != nil { + return json.Marshal(struct { + IB + InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"` + }{ + IB: IB(*t), + }) + } + return json.Marshal(IB(*t)) +} + +// With returns a copy of the button with data. +func (t *InlineButton) With(data string) *InlineButton { + return &InlineButton{ + Unique: t.Unique, + Text: t.Text, + URL: t.URL, + InlineQuery: t.InlineQuery, + InlineQueryChat: t.InlineQueryChat, + Login: t.Login, + Data: data, + } +} + +func (b Btn) Reply() *ReplyButton { + if b.Unique != "" { + return nil + } + + return &ReplyButton{ + Text: b.Text, + Contact: b.Contact, + Location: b.Location, + Poll: b.Poll, + User: b.User, + Chat: b.Chat, + WebApp: b.WebApp, + } +} + +func (b Btn) Inline() *InlineButton { + return &InlineButton{ + Unique: b.Unique, + Text: b.Text, + URL: b.URL, + Data: b.Data, + InlineQuery: b.InlineQuery, + InlineQueryChat: b.InlineQueryChat, + Login: b.Login, + WebApp: b.WebApp, + } +} + +// Login represents a parameter of the inline keyboard button +// used to automatically authorize a user. Serves as a great replacement +// for the Telegram Login Widget when the user is coming from Telegram. +type Login struct { + URL string `json:"url"` + Text string `json:"forward_text,omitempty"` + Username string `json:"bot_username,omitempty"` + WriteAccess bool `json:"request_write_access,omitempty"` +} + +// MenuButton describes the bot's menu button in a private chat. +type MenuButton struct { + Type MenuButtonType `json:"type"` + Text string `json:"text,omitempty"` + WebApp *WebApp `json:"web_app,omitempty"` +} + +type MenuButtonType = string + +const ( + MenuButtonDefault MenuButtonType = "default" + MenuButtonCommands MenuButtonType = "commands" + MenuButtonWebApp MenuButtonType = "web_app" +) diff --git a/vendor/gopkg.in/telebot.v3/media.go b/vendor/gopkg.in/telebot.v3/media.go new file mode 100644 index 000000000..d161aa58a --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/media.go @@ -0,0 +1,358 @@ +package telebot + +import ( + "encoding/json" +) + +// Media is a generic type for all kinds of media that includes File. +type Media interface { + // MediaType returns string-represented media type. + MediaType() string + + // MediaFile returns a pointer to the media file. + MediaFile() *File +} + +// InputMedia represents a composite InputMedia struct that is +// used by Telebot in sending and editing media methods. +type InputMedia struct { + Type string `json:"type"` + Media string `json:"media"` + Caption string `json:"caption"` + Thumbnail string `json:"thumb,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` + Entities Entities `json:"caption_entities,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Duration int `json:"duration,omitempty"` + Title string `json:"title,omitempty"` + Performer string `json:"performer,omitempty"` + Streaming bool `json:"supports_streaming,omitempty"` + DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"` + HasSpoiler bool `json:"is_spoiler,omitempty"` +} + +// Inputtable is a generic type for all kinds of media you +// can put into an album. +type Inputtable interface { + Media + + // InputMedia returns already marshalled InputMedia type + // ready to be used in sending and editing media methods. + InputMedia() InputMedia +} + +// Album lets you group multiple media into a single message. +type Album []Inputtable + +// Photo object represents a single photo file. +type Photo struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` +} + +type photoSize struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` +} + +func (p *Photo) MediaType() string { + return "photo" +} + +func (p *Photo) MediaFile() *File { + return &p.File +} + +func (p *Photo) InputMedia() InputMedia { + return InputMedia{ + Type: p.MediaType(), + Caption: p.Caption, + } +} + +// UnmarshalJSON is custom unmarshaller required to abstract +// away the hassle of treating different thumbnail sizes. +// Instead, Telebot chooses the hi-res one and just sticks to it. +// +// I really do find it a beautiful solution. +func (p *Photo) UnmarshalJSON(data []byte) error { + var hq photoSize + + if data[0] == '{' { + if err := json.Unmarshal(data, &hq); err != nil { + return err + } + } else { + var sizes []photoSize + if err := json.Unmarshal(data, &sizes); err != nil { + return err + } + + hq = sizes[len(sizes)-1] + } + + p.File = hq.File + p.Width = hq.Width + p.Height = hq.Height + + return nil +} + +// Audio object represents an audio file. +type Audio struct { + File + + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + Title string `json:"title,omitempty"` + Performer string `json:"performer,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (a *Audio) MediaType() string { + return "audio" +} + +func (a *Audio) MediaFile() *File { + a.fileName = a.FileName + return &a.File +} + +func (a *Audio) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Caption: a.Caption, + Duration: a.Duration, + Title: a.Title, + Performer: a.Performer, + } +} + +// Document object represents a general file (as opposed to Photo or Audio). +// Telegram users can send files of any type of up to 1.5 GB in size. +type Document struct { + File + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` + Caption string `json:"caption,omitempty"` + MIME string `json:"mime_type"` + FileName string `json:"file_name,omitempty"` + DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"` +} + +func (d *Document) MediaType() string { + return "document" +} + +func (d *Document) MediaFile() *File { + d.fileName = d.FileName + return &d.File +} + +func (d *Document) InputMedia() InputMedia { + return InputMedia{ + Type: d.MediaType(), + Caption: d.Caption, + DisableTypeDetection: d.DisableTypeDetection, + } +} + +// Video object represents a video file. +type Video struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + Streaming bool `json:"supports_streaming,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (v *Video) MediaType() string { + return "video" +} + +func (v *Video) MediaFile() *File { + v.fileName = v.FileName + return &v.File +} + +func (v *Video) InputMedia() InputMedia { + return InputMedia{ + Type: v.MediaType(), + Caption: v.Caption, + Width: v.Width, + Height: v.Height, + Duration: v.Duration, + Streaming: v.Streaming, + } +} + +// Animation object represents a animation file. +type Animation struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (a *Animation) MediaType() string { + return "animation" +} + +func (a *Animation) MediaFile() *File { + a.fileName = a.FileName + return &a.File +} + +func (a *Animation) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Caption: a.Caption, + Width: a.Width, + Height: a.Height, + Duration: a.Duration, + } +} + +// Voice object represents a voice note. +type Voice struct { + File + + Duration int `json:"duration"` + + // (Optional) + Caption string `json:"caption,omitempty"` + MIME string `json:"mime_type,omitempty"` +} + +func (v *Voice) MediaType() string { + return "voice" +} + +func (v *Voice) MediaFile() *File { + return &v.File +} + +// VideoNote represents a video message. +type VideoNote struct { + File + + Duration int `json:"duration"` + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` + Length int `json:"length,omitempty"` +} + +func (v *VideoNote) MediaType() string { + return "videoNote" +} + +func (v *VideoNote) MediaFile() *File { + return &v.File +} + +// Sticker object represents a WebP image, so-called sticker. +type Sticker struct { + File + Width int `json:"width"` + Height int `json:"height"` + Animated bool `json:"is_animated"` + Video bool `json:"is_video"` + Thumbnail *Photo `json:"thumb"` + Emoji string `json:"emoji"` + SetName string `json:"set_name"` + MaskPosition *MaskPosition `json:"mask_position"` + PremiumAnimation *File `json:"premium_animation"` + Type StickerSetType `json:"type"` + CustomEmoji string `json:"custom_emoji_id"` +} + +func (s *Sticker) MediaType() string { + return "sticker" +} + +func (s *Sticker) MediaFile() *File { + return &s.File +} + +// Contact object represents a contact to Telegram user. +type Contact struct { + PhoneNumber string `json:"phone_number"` + FirstName string `json:"first_name"` + + // (Optional) + LastName string `json:"last_name"` + UserID int64 `json:"user_id,omitempty"` +} + +// Location object represents geographic position. +type Location struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` + HorizontalAccuracy *float32 `json:"horizontal_accuracy,omitempty"` + Heading int `json:"heading,omitempty"` + AlertRadius int `json:"proximity_alert_radius,omitempty"` + + // Period in seconds for which the location will be updated + // (see Live Locations, should be between 60 and 86400.) + LivePeriod int `json:"live_period,omitempty"` +} + +// Venue object represents a venue location with name, address and +// optional foursquare ID. +type Venue struct { + Location Location `json:"location"` + Title string `json:"title"` + Address string `json:"address"` + + // (Optional) + FoursquareID string `json:"foursquare_id,omitempty"` + FoursquareType string `json:"foursquare_type,omitempty"` + GooglePlaceID string `json:"google_place_id,omitempty"` + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// Dice object represents a dice with a random value +// from 1 to 6 for currently supported base emoji. +type Dice struct { + Type DiceType `json:"emoji"` + Value int `json:"value"` +} + +// DiceType defines dice types. +type DiceType string + +var ( + Cube = &Dice{Type: "🎲"} + Dart = &Dice{Type: "🎯"} + Ball = &Dice{Type: "🏀"} + Goal = &Dice{Type: "⚽"} + Slot = &Dice{Type: "🎰"} + Bowl = &Dice{Type: "🎳"} +) diff --git a/vendor/gopkg.in/telebot.v3/message.go b/vendor/gopkg.in/telebot.v3/message.go new file mode 100644 index 000000000..82997569a --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/message.go @@ -0,0 +1,463 @@ +package telebot + +import ( + "strconv" + "time" + "unicode/utf16" +) + +// Message object represents a message. +type Message struct { + ID int `json:"message_id"` + + // (Optional) Unique identifier of a message thread to which the message belongs; for supergroups only + ThreadID int `json:"message_thread_id"` + + // For message sent to channels, Sender will be nil + Sender *User `json:"from"` + + // Unixtime, use Message.Time() to get time.Time + Unixtime int64 `json:"date"` + + // Conversation the message belongs to. + Chat *Chat `json:"chat"` + + // Sender of the message, sent on behalf of a chat. + SenderChat *Chat `json:"sender_chat"` + + // For forwarded messages, sender of the original message. + OriginalSender *User `json:"forward_from"` + + // For forwarded messages, chat of the original message when + // forwarded from a channel. + OriginalChat *Chat `json:"forward_from_chat"` + + // For forwarded messages, identifier of the original message + // when forwarded from a channel. + OriginalMessageID int `json:"forward_from_message_id"` + + // For forwarded messages, signature of the post author. + OriginalSignature string `json:"forward_signature"` + + // For forwarded messages, sender's name from users who + // disallow adding a link to their account. + OriginalSenderName string `json:"forward_sender_name"` + + // For forwarded messages, unixtime of the original message. + OriginalUnixtime int `json:"forward_date"` + + // Message is a channel post that was automatically forwarded to the connected discussion group. + AutomaticForward bool `json:"is_automatic_forward"` + + // For replies, ReplyTo represents the original message. + // + // Note that the Message object in this field will not + // contain further ReplyTo fields even if it + // itself is a reply. + ReplyTo *Message `json:"reply_to_message"` + + // Shows through which bot the message was sent. + Via *User `json:"via_bot"` + + // (Optional) Time of last edit in Unix. + LastEdit int64 `json:"edit_date"` + + // (Optional) True, if the message is sent to a forum topic. + TopicMessage bool `json:"is_topic_message"` + + // (Optional) Message can't be forwarded. + Protected bool `json:"has_protected_content,omitempty"` + + // AlbumID is the unique identifier of a media message group + // this message belongs to. + AlbumID string `json:"media_group_id"` + + // Author signature (in channels). + Signature string `json:"author_signature"` + + // For a text message, the actual UTF-8 text of the message. + Text string `json:"text"` + + // For registered commands, will contain the string payload: + // + // Ex: `/command ` or `/command@botname ` + Payload string `json:"-"` + + // For text messages, special entities like usernames, URLs, bot commands, + // etc. that appear in the text. + Entities Entities `json:"entities,omitempty"` + + // Some messages containing media, may as well have a caption. + Caption string `json:"caption,omitempty"` + + // For messages with a caption, special entities like usernames, URLs, + // bot commands, etc. that appear in the caption. + CaptionEntities Entities `json:"caption_entities,omitempty"` + + // For an audio recording, information about it. + Audio *Audio `json:"audio"` + + // For a general file, information about it. + Document *Document `json:"document"` + + // For a photo, all available sizes (thumbnails). + Photo *Photo `json:"photo"` + + // For a sticker, information about it. + Sticker *Sticker `json:"sticker"` + + // For a voice message, information about it. + Voice *Voice `json:"voice"` + + // For a video note, information about it. + VideoNote *VideoNote `json:"video_note"` + + // For a video, information about it. + Video *Video `json:"video"` + + // For a animation, information about it. + Animation *Animation `json:"animation"` + + // For a contact, contact information itself. + Contact *Contact `json:"contact"` + + // For a location, its longitude and latitude. + Location *Location `json:"location"` + + // For a venue, information about it. + Venue *Venue `json:"venue"` + + // For a poll, information the native poll. + Poll *Poll `json:"poll"` + + // For a game, information about it. + Game *Game `json:"game"` + + // For a dice, information about it. + Dice *Dice `json:"dice"` + + // For a service message, represents a user, + // that just got added to chat, this message came from. + // + // Sender leads to User, capable of invite. + // + // UserJoined might be the Bot itself. + UserJoined *User `json:"new_chat_member"` + + // For a service message, represents a user, + // that just left chat, this message came from. + // + // If user was kicked, Sender leads to a User, + // capable of this kick. + // + // UserLeft might be the Bot itself. + UserLeft *User `json:"left_chat_member"` + + // For a service message, represents a new title + // for chat this message came from. + // + // Sender would lead to a User, capable of change. + NewGroupTitle string `json:"new_chat_title"` + + // For a service message, represents all available + // thumbnails of the new chat photo. + // + // Sender would lead to a User, capable of change. + NewGroupPhoto *Photo `json:"new_chat_photo"` + + // For a service message, new members that were added to + // the group or supergroup and information about them + // (the bot itself may be one of these members). + UsersJoined []User `json:"new_chat_members"` + + // For a service message, true if chat photo just + // got removed. + // + // Sender would lead to a User, capable of change. + GroupPhotoDeleted bool `json:"delete_chat_photo"` + + // For a service message, true if group has been created. + // + // You would receive such a message if you are one of + // initial group chat members. + // + // Sender would lead to creator of the chat. + GroupCreated bool `json:"group_chat_created"` + + // For a service message, true if supergroup has been created. + // + // You would receive such a message if you are one of + // initial group chat members. + // + // Sender would lead to creator of the chat. + SuperGroupCreated bool `json:"supergroup_chat_created"` + + // For a service message, true if channel has been created. + // + // You would receive such a message if you are one of + // initial channel administrators. + // + // Sender would lead to creator of the chat. + ChannelCreated bool `json:"channel_chat_created"` + + // For a service message, the destination (supergroup) you + // migrated to. + // + // You would receive such a message when your chat has migrated + // to a supergroup. + // + // Sender would lead to creator of the migration. + MigrateTo int64 `json:"migrate_to_chat_id"` + + // For a service message, the Origin (normal group) you migrated + // from. + // + // You would receive such a message when your chat has migrated + // to a supergroup. + // + // Sender would lead to creator of the migration. + MigrateFrom int64 `json:"migrate_from_chat_id"` + + // Specified message was pinned. Note that the Message object + // in this field will not contain further ReplyTo fields even + // if it is itself a reply. + PinnedMessage *Message `json:"pinned_message"` + + // Message is an invoice for a payment. + Invoice *Invoice `json:"invoice"` + + // Message is a service message about a successful payment. + Payment *Payment `json:"successful_payment"` + + // For a service message, a user was shared with the bot. + UserShared *RecipientShared `json:"user_shared,omitempty"` + + // For a service message, a chat was shared with the bot. + ChatShared *RecipientShared `json:"chat_shared,omitempty"` + + // The domain name of the website on which the user has logged in. + ConnectedWebsite string `json:"connected_website,omitempty"` + + // For a service message, a video chat started in the chat. + VideoChatStarted *VideoChatStarted `json:"video_chat_started,omitempty"` + + // For a service message, a video chat ended in the chat. + VideoChatEnded *VideoChatEnded `json:"video_chat_ended,omitempty"` + + // For a service message, some users were invited in the video chat. + VideoChatParticipants *VideoChatParticipants `json:"video_chat_participants_invited,omitempty"` + + // For a service message, a video chat schedule in the chat. + VideoChatScheduled *VideoChatScheduled `json:"video_chat_scheduled,omitempty"` + + // For a data sent by a Web App. + WebAppData *WebAppData `json:"web_app_data,omitempty"` + + // For a service message, represents the content of a service message, + // sent whenever a user in the chat triggers a proximity alert set by another user. + ProximityAlert *ProximityAlert `json:"proximity_alert_triggered,omitempty"` + + // For a service message, represents about a change in auto-delete timer settings. + AutoDeleteTimer *AutoDeleteTimer `json:"message_auto_delete_timer_changed,omitempty"` + + // Inline keyboard attached to the message. + ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` + + // Service message: forum topic created + TopicCreated *Topic `json:"forum_topic_created,omitempty"` + + // Service message: forum topic closed + TopicClosed *struct{} `json:"forum_topic_closed,omitempty"` + + // Service message: forum topic reopened + TopicReopened *Topic `json:"forum_topic_reopened,omitempty"` + + // Service message: forum topic deleted + TopicEdited *Topic `json:"forum_topic_edited,omitempty"` + + // Service message: general forum topic hidden + GeneralTopicHidden *struct{} `json:"general_topic_hidden,omitempty"` + + // Service message: general forum topic unhidden + GeneralTopicUnhidden *struct{} `json:"general_topic_unhidden,omitempty"` + + // Service message: represents spoiler information about the message. + HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` + + // Service message: the user allowed the bot added to the attachment menu to write messages + WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` +} + +// MessageEntity object represents "special" parts of text messages, +// including hashtags, usernames, URLs, etc. +type MessageEntity struct { + // Specifies entity type. + Type EntityType `json:"type"` + + // Offset in UTF-16 code units to the start of the entity. + Offset int `json:"offset"` + + // Length of the entity in UTF-16 code units. + Length int `json:"length"` + + // (Optional) For EntityTextLink entity type only. + // + // URL will be opened after user taps on the text. + URL string `json:"url,omitempty"` + + // (Optional) For EntityTMention entity type only. + User *User `json:"user,omitempty"` + + // (Optional) For EntityCodeBlock entity type only. + Language string `json:"language,omitempty"` + + // (Optional) For EntityCustomEmoji entity type only. + CustomEmoji string `json:"custom_emoji_id"` +} + +// EntityType is a MessageEntity type. +type EntityType string + +const ( + EntityMention EntityType = "mention" + EntityTMention EntityType = "text_mention" + EntityHashtag EntityType = "hashtag" + EntityCashtag EntityType = "cashtag" + EntityCommand EntityType = "bot_command" + EntityURL EntityType = "url" + EntityEmail EntityType = "email" + EntityPhone EntityType = "phone_number" + EntityBold EntityType = "bold" + EntityItalic EntityType = "italic" + EntityUnderline EntityType = "underline" + EntityStrikethrough EntityType = "strikethrough" + EntityCode EntityType = "code" + EntityCodeBlock EntityType = "pre" + EntityTextLink EntityType = "text_link" + EntitySpoiler EntityType = "spoiler" + EntityCustomEmoji EntityType = "custom_emoji" +) + +// Entities is used to set message's text entities as a send option. +type Entities []MessageEntity + +// ProximityAlert sent whenever a user in the chat triggers +// a proximity alert set by another user. +type ProximityAlert struct { + Traveler *User `json:"traveler,omitempty"` + Watcher *User `json:"watcher,omitempty"` + Distance int `json:"distance"` +} + +// AutoDeleteTimer represents a service message about a change in auto-delete timer settings. +type AutoDeleteTimer struct { + Unixtime int `json:"message_auto_delete_time"` +} + +// MessageSig satisfies Editable interface (see Editable.) +func (m *Message) MessageSig() (string, int64) { + return strconv.Itoa(m.ID), m.Chat.ID +} + +// Time returns the moment of message creation in local time. +func (m *Message) Time() time.Time { + return time.Unix(m.Unixtime, 0) +} + +// LastEdited returns time.Time of last edit. +func (m *Message) LastEdited() time.Time { + return time.Unix(m.LastEdit, 0) +} + +// IsForwarded says whether message is forwarded copy of another +// message or not. +func (m *Message) IsForwarded() bool { + return m.OriginalSender != nil || m.OriginalChat != nil +} + +// IsReply says whether message is a reply to another message. +func (m *Message) IsReply() bool { + return m.ReplyTo != nil +} + +// Private returns true, if it's a personal message. +func (m *Message) Private() bool { + return m.Chat.Type == ChatPrivate +} + +// FromGroup returns true, if message came from a group OR a supergroup. +func (m *Message) FromGroup() bool { + return m.Chat.Type == ChatGroup || m.Chat.Type == ChatSuperGroup +} + +// FromChannel returns true, if message came from a channel. +func (m *Message) FromChannel() bool { + return m.Chat.Type == ChatChannel +} + +// IsService returns true, if message is a service message, +// returns false otherwise. +// +// Service messages are automatically sent messages, which +// typically occur on some global action. For instance, when +// anyone leaves the chat or chat title changes. +func (m *Message) IsService() bool { + fact := false + + fact = fact || m.UserJoined != nil + fact = fact || len(m.UsersJoined) > 0 + fact = fact || m.UserLeft != nil + fact = fact || m.NewGroupTitle != "" + fact = fact || m.NewGroupPhoto != nil + fact = fact || m.GroupPhotoDeleted + fact = fact || m.GroupCreated || m.SuperGroupCreated + fact = fact || (m.MigrateTo != m.MigrateFrom) + + return fact +} + +// EntityText returns the substring of the message identified by the +// given MessageEntity. +// +// It's safer than manually slicing Text because Telegram uses +// UTF-16 indices whereas Go string are []byte. +func (m *Message) EntityText(e MessageEntity) string { + text := m.Text + if text == "" { + text = m.Caption + } + + a := utf16.Encode([]rune(text)) + off, end := e.Offset, e.Offset+e.Length + + if off < 0 || end > len(a) { + return "" + } + + return string(utf16.Decode(a[off:end])) +} + +// Media returns the message's media if it contains either photo, +// voice, audio, animation, sticker, document, video or video note. +func (m *Message) Media() Media { + switch { + case m.Photo != nil: + return m.Photo + case m.Voice != nil: + return m.Voice + case m.Audio != nil: + return m.Audio + case m.Animation != nil: + return m.Animation + case m.Sticker != nil: + return m.Sticker + case m.Document != nil: + return m.Document + case m.Video != nil: + return m.Video + case m.VideoNote != nil: + return m.VideoNote + default: + return nil + } +} diff --git a/vendor/gopkg.in/telebot.v3/middleware.go b/vendor/gopkg.in/telebot.v3/middleware.go new file mode 100644 index 000000000..a8e2912a5 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/middleware.go @@ -0,0 +1,38 @@ +package telebot + +// MiddlewareFunc represents a middleware processing function, +// which get called before the endpoint group or specific handler. +type MiddlewareFunc func(HandlerFunc) HandlerFunc + +func appendMiddleware(a, b []MiddlewareFunc) []MiddlewareFunc { + if len(a) == 0 { + return b + } + + m := make([]MiddlewareFunc, 0, len(a)+len(b)) + return append(m, append(a, b...)...) +} + +func applyMiddleware(h HandlerFunc, m ...MiddlewareFunc) HandlerFunc { + for i := len(m) - 1; i >= 0; i-- { + h = m[i](h) + } + return h +} + +// Group is a separated group of handlers, united by the general middleware. +type Group struct { + b *Bot + middleware []MiddlewareFunc +} + +// Use adds middleware to the chain. +func (g *Group) Use(middleware ...MiddlewareFunc) { + g.middleware = append(g.middleware, middleware...) +} + +// Handle adds endpoint handler to the bot, combining group's middleware +// with the optional given middleware. +func (g *Group) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) { + g.b.Handle(endpoint, h, appendMiddleware(g.middleware, m)...) +} diff --git a/vendor/gopkg.in/telebot.v3/options.go b/vendor/gopkg.in/telebot.v3/options.go new file mode 100644 index 000000000..56e0d9c29 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/options.go @@ -0,0 +1,226 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +// Option is a shortcut flag type for certain message features +// (so-called options). It means that instead of passing +// fully-fledged SendOptions* to Send(), you can use these +// flags instead. +// +// Supported options are defined as iota-constants. +type Option int + +const ( + // NoPreview = SendOptions.DisableWebPagePreview + NoPreview Option = iota + + // Silent = SendOptions.DisableNotification + Silent + + // AllowWithoutReply = SendOptions.AllowWithoutReply + AllowWithoutReply + + // Protected = SendOptions.Protected + Protected + + // ForceReply = ReplyMarkup.ForceReply + ForceReply + + // OneTimeKeyboard = ReplyMarkup.OneTimeKeyboard + OneTimeKeyboard + + // RemoveKeyboard = ReplyMarkup.RemoveKeyboard + RemoveKeyboard +) + +// Placeholder is used to set input field placeholder as a send option. +func Placeholder(text string) *SendOptions { + return &SendOptions{ + ReplyMarkup: &ReplyMarkup{ + ForceReply: true, + Placeholder: text, + }, + } +} + +// SendOptions has most complete control over in what way the message +// must be sent, providing an API-complete set of custom properties +// and options. +// +// Despite its power, SendOptions is rather inconvenient to use all +// the way through bot logic, so you might want to consider storing +// and re-using it somewhere or be using Option flags instead. +type SendOptions struct { + // If the message is a reply, original message. + ReplyTo *Message + + // See ReplyMarkup struct definition. + ReplyMarkup *ReplyMarkup + + // For text messages, disables previews for links in this message. + DisableWebPagePreview bool + + // Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound. + DisableNotification bool + + // ParseMode controls how client apps render your message. + ParseMode ParseMode + + // Entities is a list of special entities that appear in message text, which can be specified instead of parse_mode. + Entities Entities + + // AllowWithoutReply allows sending messages not a as reply if the replied-to message has already been deleted. + AllowWithoutReply bool + + // Protected protects the contents of sent message from forwarding and saving. + Protected bool + + // ThreadID supports sending messages to a thread. + ThreadID int + + // HasSpoiler marks the message as containing a spoiler. + HasSpoiler bool + +} + +func (og *SendOptions) copy() *SendOptions { + cp := *og + if cp.ReplyMarkup != nil { + cp.ReplyMarkup = cp.ReplyMarkup.copy() + } + return &cp +} + +func extractOptions(how []interface{}) *SendOptions { + opts := &SendOptions{} + + for _, prop := range how { + switch opt := prop.(type) { + case *SendOptions: + opts = opt.copy() + case *ReplyMarkup: + if opt != nil { + opts.ReplyMarkup = opt.copy() + } + case Option: + switch opt { + case NoPreview: + opts.DisableWebPagePreview = true + case Silent: + opts.DisableNotification = true + case AllowWithoutReply: + opts.AllowWithoutReply = true + case ForceReply: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.ForceReply = true + case OneTimeKeyboard: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.OneTimeKeyboard = true + case RemoveKeyboard: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.RemoveKeyboard = true + case Protected: + opts.Protected = true + default: + panic("telebot: unsupported flag-option") + } + case ParseMode: + opts.ParseMode = opt + case Entities: + opts.Entities = opt + default: + panic("telebot: unsupported send-option") + } + } + + return opts +} + +func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) { + if b.parseMode != ModeDefault { + params["parse_mode"] = b.parseMode + } + + if opt == nil { + return + } + + if opt.ReplyTo != nil && opt.ReplyTo.ID != 0 { + params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID) + } + + if opt.DisableWebPagePreview { + params["disable_web_page_preview"] = "true" + } + + if opt.DisableNotification { + params["disable_notification"] = "true" + } + + if opt.ParseMode != ModeDefault { + params["parse_mode"] = opt.ParseMode + } + + if len(opt.Entities) > 0 { + delete(params, "parse_mode") + entities, _ := json.Marshal(opt.Entities) + + if params["caption"] != "" { + params["caption_entities"] = string(entities) + } else { + params["entities"] = string(entities) + } + } + + if opt.AllowWithoutReply { + params["allow_sending_without_reply"] = "true" + } + + if opt.ReplyMarkup != nil { + processButtons(opt.ReplyMarkup.InlineKeyboard) + replyMarkup, _ := json.Marshal(opt.ReplyMarkup) + params["reply_markup"] = string(replyMarkup) + } + + if opt.Protected { + params["protect_content"] = "true" + } + + if opt.ThreadID != 0 { + params["message_thread_id"] = strconv.Itoa(opt.ThreadID) + } + + if opt.HasSpoiler { + params["spoiler"] = "true" + } +} + +func processButtons(keys [][]InlineButton) { + if keys == nil || len(keys) < 1 || len(keys[0]) < 1 { + return + } + + for i := range keys { + for j := range keys[i] { + key := &keys[i][j] + if key.Unique != "" { + // Format: "\f|" + data := key.Data + if data == "" { + key.Data = "\f" + key.Unique + } else { + key.Data = "\f" + key.Unique + "|" + data + } + } + } + } +} diff --git a/vendor/gopkg.in/telebot.v3/payments.go b/vendor/gopkg.in/telebot.v3/payments.go new file mode 100644 index 000000000..c32f8a13a --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/payments.go @@ -0,0 +1,188 @@ +package telebot + +import ( + "encoding/json" + "math" + "strconv" +) + +// ShippingQuery contains information about an incoming shipping query. +type ShippingQuery struct { + Sender *User `json:"from"` + ID string `json:"id"` + Payload string `json:"invoice_payload"` + Address ShippingAddress `json:"shipping_address"` +} + +// ShippingAddress represents a shipping address. +type ShippingAddress struct { + CountryCode string `json:"country_code"` + State string `json:"state"` + City string `json:"city"` + StreetLine1 string `json:"street_line1"` + StreetLine2 string `json:"street_line2"` + PostCode string `json:"post_code"` +} + +// ShippingOption represents one shipping option. +type ShippingOption struct { + ID string `json:"id"` + Title string `json:"title"` + Prices []Price `json:"prices"` +} + +// Payment contains basic information about a successful payment. +type Payment struct { + Currency string `json:"currency"` + Total int `json:"total_amount"` + Payload string `json:"invoice_payload"` + OptionID string `json:"shipping_option_id"` + Order Order `json:"order_info"` + TelegramChargeID string `json:"telegram_payment_charge_id"` + ProviderChargeID string `json:"provider_payment_charge_id"` +} + +// PreCheckoutQuery contains information about an incoming pre-checkout query. +type PreCheckoutQuery struct { + Sender *User `json:"from"` + ID string `json:"id"` + Currency string `json:"currency"` + Payload string `json:"invoice_payload"` + Total int `json:"total_amount"` + OptionID string `json:"shipping_option_id"` + Order Order `json:"order_info"` +} + +// Order represents information about an order. +type Order struct { + Name string `json:"name"` + PhoneNumber string `json:"phone_number"` + Email string `json:"email"` + Address ShippingAddress `json:"shipping_address"` +} + +// Invoice contains basic information about an invoice. +type Invoice struct { + Title string `json:"title"` + Description string `json:"description"` + Payload string `json:"payload"` + Currency string `json:"currency"` + Prices []Price `json:"prices"` + Token string `json:"provider_token"` + Data string `json:"provider_data"` + + Photo *Photo `json:"photo"` + PhotoSize int `json:"photo_size"` + + // Unique deep-linking parameter that can be used to + // generate this invoice when used as a start parameter (0). + Start string `json:"start_parameter"` + + // Shows the total price in the smallest units of the currency. + // For example, for a price of US$ 1.45 pass amount = 145. + Total int `json:"total_amount"` + + MaxTipAmount int `json:"max_tip_amount"` + SuggestedTipAmounts []int `json:"suggested_tip_amounts"` + + NeedName bool `json:"need_name"` + NeedPhoneNumber bool `json:"need_phone_number"` + NeedEmail bool `json:"need_email"` + NeedShippingAddress bool `json:"need_shipping_address"` + SendPhoneNumber bool `json:"send_phone_number_to_provider"` + SendEmail bool `json:"send_email_to_provider"` + Flexible bool `json:"is_flexible"` +} + +func (i Invoice) params() map[string]string { + params := map[string]string{ + "title": i.Title, + "description": i.Description, + "start_parameter": i.Start, + "payload": i.Payload, + "provider_token": i.Token, + "provider_data": i.Data, + "currency": i.Currency, + "max_tip_amount": strconv.Itoa(i.MaxTipAmount), + "need_name": strconv.FormatBool(i.NeedName), + "need_phone_number": strconv.FormatBool(i.NeedPhoneNumber), + "need_email": strconv.FormatBool(i.NeedEmail), + "need_shipping_address": strconv.FormatBool(i.NeedShippingAddress), + "send_phone_number_to_provider": strconv.FormatBool(i.SendPhoneNumber), + "send_email_to_provider": strconv.FormatBool(i.SendEmail), + "is_flexible": strconv.FormatBool(i.Flexible), + } + if i.Photo != nil { + if i.Photo.FileURL != "" { + params["photo_url"] = i.Photo.FileURL + } + if i.PhotoSize > 0 { + params["photo_size"] = strconv.Itoa(i.PhotoSize) + } + if i.Photo.Width > 0 { + params["photo_width"] = strconv.Itoa(i.Photo.Width) + } + if i.Photo.Height > 0 { + params["photo_height"] = strconv.Itoa(i.Photo.Height) + } + } + if len(i.Prices) > 0 { + data, _ := json.Marshal(i.Prices) + params["prices"] = string(data) + } + if len(i.SuggestedTipAmounts) > 0 { + var amounts []string + for _, n := range i.SuggestedTipAmounts { + amounts = append(amounts, strconv.Itoa(n)) + } + + data, _ := json.Marshal(amounts) + params["suggested_tip_amounts"] = string(data) + } + return params +} + +// Price represents a portion of the price for goods or services. +type Price struct { + Label string `json:"label"` + Amount int `json:"amount"` +} + +// Currency contains information about supported currency for payments. +type Currency struct { + Code string `json:"code"` + Title string `json:"title"` + Symbol string `json:"symbol"` + Native string `json:"native"` + ThousandsSep string `json:"thousands_sep"` + DecimalSep string `json:"decimal_sep"` + SymbolLeft bool `json:"symbol_left"` + SpaceBetween bool `json:"space_between"` + Exp int `json:"exp"` + MinAmount interface{} `json:"min_amount"` + MaxAmount interface{} `json:"max_amount"` +} + +func (c Currency) FromTotal(total int) float64 { + return float64(total) / math.Pow(10, float64(c.Exp)) +} + +func (c Currency) ToTotal(total float64) int { + return int(total) * int(math.Pow(10, float64(c.Exp))) +} + +// CreateInvoiceLink creates a link for a payment invoice. +func (b *Bot) CreateInvoiceLink(i Invoice) (string, error) { + data, err := b.Raw("createInvoiceLink", i.params()) + if err != nil { + return "", err + } + + var resp struct { + Result string + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", wrapError(err) + } + return resp.Result, nil +} diff --git a/vendor/gopkg.in/telebot.v3/payments_data.go b/vendor/gopkg.in/telebot.v3/payments_data.go new file mode 100644 index 000000000..a325c5b05 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/payments_data.go @@ -0,0 +1,14 @@ +package telebot + +import "encoding/json" + +const dataCurrencies = `{"AED":{"code":"AED","title":"United Arab Emirates Dirham","symbol":"AED","native":"\u062f.\u0625.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"367","max_amount":"3673200"},"AFN":{"code":"AFN","title":"Afghan Afghani","symbol":"AFN","native":"\u060b","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"7554","max_amount":"75540495"},"ALL":{"code":"ALL","title":"Albanian Lek","symbol":"ALL","native":"Lek","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":false,"exp":2,"min_amount":"10908","max_amount":"109085036"},"AMD":{"code":"AMD","title":"Armenian Dram","symbol":"AMD","native":"\u0564\u0580.","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"48398","max_amount":"483984962"},"ARS":{"code":"ARS","title":"Argentine Peso","symbol":"ARS","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3720","max_amount":"37202998"},"AUD":{"code":"AUD","title":"Australian Dollar","symbol":"AU$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"139","max_amount":"1392750"},"AZN":{"code":"AZN","title":"Azerbaijani Manat","symbol":"AZN","native":"\u043c\u0430\u043d.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"170","max_amount":"1702500"},"BAM":{"code":"BAM","title":"Bosnia & Herzegovina Convertible Mark","symbol":"BAM","native":"KM","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"171","max_amount":"1715550"},"BDT":{"code":"BDT","title":"Bangladeshi Taka","symbol":"BDT","native":"\u09f3","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"8336","max_amount":"83367500"},"BGN":{"code":"BGN","title":"Bulgarian Lev","symbol":"BGN","native":"\u043b\u0432.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"171","max_amount":"1716850"},"BND":{"code":"BND","title":"Brunei Dollar","symbol":"BND","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"134","max_amount":"1349850"},"BOB":{"code":"BOB","title":"Bolivian Boliviano","symbol":"BOB","native":"Bs","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"687","max_amount":"6877150"},"BRL":{"code":"BRL","title":"Brazilian Real","symbol":"R$","native":"R$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"377","max_amount":"3775397"},"CAD":{"code":"CAD","title":"Canadian Dollar","symbol":"CA$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"132","max_amount":"1321950"},"CHF":{"code":"CHF","title":"Swiss Franc","symbol":"CHF","native":"CHF","thousands_sep":"'","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"99","max_amount":"993220"},"CLP":{"code":"CLP","title":"Chilean Peso","symbol":"CLP","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":0,"min_amount":"666","max_amount":"6665199"},"CNY":{"code":"CNY","title":"Chinese Renminbi Yuan","symbol":"CN\u00a5","native":"CN\u00a5","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"674","max_amount":"6747298"},"COP":{"code":"COP","title":"Colombian Peso","symbol":"COP","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"315595","max_amount":"3155950000"},"CRC":{"code":"CRC","title":"Costa Rican Col\u00f3n","symbol":"CRC","native":"\u20a1","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"60113","max_amount":"601130282"},"CZK":{"code":"CZK","title":"Czech Koruna","symbol":"CZK","native":"K\u010d","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"2251","max_amount":"22510978"},"DKK":{"code":"DKK","title":"Danish Krone","symbol":"DKK","native":"kr","thousands_sep":"","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"654","max_amount":"6545403"},"DOP":{"code":"DOP","title":"Dominican Peso","symbol":"DOP","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"5032","max_amount":"50329504"},"DZD":{"code":"DZD","title":"Algerian Dinar","symbol":"DZD","native":"\u062f.\u062c.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"11872","max_amount":"118729869"},"EGP":{"code":"EGP","title":"Egyptian Pound","symbol":"EGP","native":"\u062c.\u0645.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"1791","max_amount":"17912012"},"EUR":{"code":"EUR","title":"Euro","symbol":"\u20ac","native":"\u20ac","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"87","max_amount":"877155"},"GBP":{"code":"GBP","title":"British Pound","symbol":"\u00a3","native":"\u00a3","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"75","max_amount":"757605"},"GEL":{"code":"GEL","title":"Georgian Lari","symbol":"GEL","native":"GEL","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"266","max_amount":"2663750"},"GTQ":{"code":"GTQ","title":"Guatemalan Quetzal","symbol":"GTQ","native":"Q","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"768","max_amount":"7689850"},"HKD":{"code":"HKD","title":"Hong Kong Dollar","symbol":"HK$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"784","max_amount":"7845505"},"HNL":{"code":"HNL","title":"Honduran Lempira","symbol":"HNL","native":"L","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"2427","max_amount":"24277502"},"HRK":{"code":"HRK","title":"Croatian Kuna","symbol":"HRK","native":"kn","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"650","max_amount":"6506302"},"HUF":{"code":"HUF","title":"Hungarian Forint","symbol":"HUF","native":"Ft","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"27844","max_amount":"278440341"},"IDR":{"code":"IDR","title":"Indonesian Rupiah","symbol":"IDR","native":"Rp","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"1406555","max_amount":"14065550000"},"ILS":{"code":"ILS","title":"Israeli New Sheqel","symbol":"\u20aa","native":"\u20aa","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"366","max_amount":"3668230"},"INR":{"code":"INR","title":"Indian Rupee","symbol":"\u20b9","native":"\u20b9","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"7090","max_amount":"70900503"},"ISK":{"code":"ISK","title":"Icelandic Kr\u00f3na","symbol":"ISK","native":"kr","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":0,"min_amount":"119","max_amount":"1195599"},"JMD":{"code":"JMD","title":"Jamaican Dollar","symbol":"JMD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"13153","max_amount":"131539958"},"JPY":{"code":"JPY","title":"Japanese Yen","symbol":"\u00a5","native":"\uffe5","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"109","max_amount":"1095549"},"KES":{"code":"KES","title":"Kenyan Shilling","symbol":"KES","native":"Ksh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"10032","max_amount":"100322011"},"KGS":{"code":"KGS","title":"Kyrgyzstani Som","symbol":"KGS","native":"KGS","thousands_sep":"\u00a0","decimal_sep":"-","symbol_left":false,"space_between":true,"exp":2,"min_amount":"6982","max_amount":"69820300"},"KRW":{"code":"KRW","title":"South Korean Won","symbol":"\u20a9","native":"\u20a9","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"1119","max_amount":"11190001"},"KZT":{"code":"KZT","title":"Kazakhstani Tenge","symbol":"KZT","native":"\u20b8","thousands_sep":"\u00a0","decimal_sep":"-","symbol_left":true,"space_between":false,"exp":2,"min_amount":"37767","max_amount":"377674954"},"LBP":{"code":"LBP","title":"Lebanese Pound","symbol":"LBP","native":"\u0644.\u0644.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"150080","max_amount":"1500802255"},"LKR":{"code":"LKR","title":"Sri Lankan Rupee","symbol":"LKR","native":"\u0dbb\u0dd4.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"18078","max_amount":"180789638"},"MAD":{"code":"MAD","title":"Moroccan Dirham","symbol":"MAD","native":"\u062f.\u0645.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"955","max_amount":"9554850"},"MDL":{"code":"MDL","title":"Moldovan Leu","symbol":"MDL","native":"MDL","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"1703","max_amount":"17038967"},"MNT":{"code":"MNT","title":"Mongolian T\u00f6gr\u00f6g","symbol":"MNT","native":"MNT","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"261750","max_amount":"2617500000"},"MUR":{"code":"MUR","title":"Mauritian Rupee","symbol":"MUR","native":"MUR","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3438","max_amount":"34384499"},"MVR":{"code":"MVR","title":"Maldivian Rufiyaa","symbol":"MVR","native":"MVR","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"1550","max_amount":"15501063"},"MXN":{"code":"MXN","title":"Mexican Peso","symbol":"MX$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"1898","max_amount":"18988704"},"MYR":{"code":"MYR","title":"Malaysian Ringgit","symbol":"MYR","native":"RM","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"412","max_amount":"4124501"},"MZN":{"code":"MZN","title":"Mozambican Metical","symbol":"MZN","native":"MTn","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"6188","max_amount":"61889913"},"NGN":{"code":"NGN","title":"Nigerian Naira","symbol":"NGN","native":"\u20a6","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"36174","max_amount":"361749532"},"NIO":{"code":"NIO","title":"Nicaraguan C\u00f3rdoba","symbol":"NIO","native":"C$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3241","max_amount":"32415503"},"NOK":{"code":"NOK","title":"Norwegian Krone","symbol":"NOK","native":"kr","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"851","max_amount":"8510100"},"NPR":{"code":"NPR","title":"Nepalese Rupee","symbol":"NPR","native":"\u0928\u0947\u0930\u0942","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"11299","max_amount":"112995016"},"NZD":{"code":"NZD","title":"New Zealand Dollar","symbol":"NZ$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"146","max_amount":"1461850"},"PAB":{"code":"PAB","title":"Panamanian Balboa","symbol":"PAB","native":"B\/.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"99","max_amount":"995290"},"PEN":{"code":"PEN","title":"Peruvian Nuevo Sol","symbol":"PEN","native":"S\/.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"333","max_amount":"3331250"},"PHP":{"code":"PHP","title":"Philippine Peso","symbol":"PHP","native":"\u20b1","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"5260","max_amount":"52602981"},"PKR":{"code":"PKR","title":"Pakistani Rupee","symbol":"PKR","native":"\u20a8","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"13921","max_amount":"139214990"},"PLN":{"code":"PLN","title":"Polish Z\u0142oty","symbol":"PLN","native":"z\u0142","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"376","max_amount":"3764026"},"PYG":{"code":"PYG","title":"Paraguayan Guaran\u00ed","symbol":"PYG","native":"\u20b2","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":0,"min_amount":"6013","max_amount":"60134502"},"QAR":{"code":"QAR","title":"Qatari Riyal","symbol":"QAR","native":"\u0631.\u0642.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"364","max_amount":"3641101"},"RON":{"code":"RON","title":"Romanian Leu","symbol":"RON","native":"RON","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"417","max_amount":"4172003"},"RSD":{"code":"RSD","title":"Serbian Dinar","symbol":"RSD","native":"\u0434\u0438\u043d.","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"10391","max_amount":"103910127"},"RUB":{"code":"RUB","title":"Russian Ruble","symbol":"RUB","native":"\u0440\u0443\u0431.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"6598","max_amount":"65986027"},"SAR":{"code":"SAR","title":"Saudi Riyal","symbol":"SAR","native":"\u0631.\u0633.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"373","max_amount":"3732650"},"SEK":{"code":"SEK","title":"Swedish Krona","symbol":"SEK","native":"kr","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"904","max_amount":"9047896"},"SGD":{"code":"SGD","title":"Singapore Dollar","symbol":"SGD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"135","max_amount":"1353897"},"THB":{"code":"THB","title":"Thai Baht","symbol":"\u0e3f","native":"\u0e3f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3156","max_amount":"31563499"},"TJS":{"code":"TJS","title":"Tajikistani Somoni","symbol":"TJS","native":"TJS","thousands_sep":"\u00a0","decimal_sep":";","symbol_left":false,"space_between":true,"exp":2,"min_amount":"938","max_amount":"9389950"},"TRY":{"code":"TRY","title":"Turkish Lira","symbol":"TRY","native":"TL","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"526","max_amount":"5267200"},"TTD":{"code":"TTD","title":"Trinidad and Tobago Dollar","symbol":"TTD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"675","max_amount":"6757850"},"TWD":{"code":"TWD","title":"New Taiwan Dollar","symbol":"NT$","native":"NT$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3072","max_amount":"30722993"},"TZS":{"code":"TZS","title":"Tanzanian Shilling","symbol":"TZS","native":"TSh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"230200","max_amount":"2302000188"},"UAH":{"code":"UAH","title":"Ukrainian Hryvnia","symbol":"UAH","native":"\u20b4","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":false,"exp":2,"min_amount":"2764","max_amount":"27648991"},"UGX":{"code":"UGX","title":"Ugandan Shilling","symbol":"UGX","native":"USh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"3657","max_amount":"36575502"},"USD":{"code":"USD","title":"United States Dollar","symbol":"$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"100","max_amount":1000000},"UYU":{"code":"UYU","title":"Uruguayan Peso","symbol":"UYU","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3246","max_amount":"32469503"},"UZS":{"code":"UZS","title":"Uzbekistani Som","symbol":"UZS","native":"UZS","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"832759","max_amount":"8327599915"},"VND":{"code":"VND","title":"Vietnamese \u0110\u1ed3ng","symbol":"\u20ab","native":"\u20ab","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":0,"min_amount":"23084","max_amount":"230840500"},"YER":{"code":"YER","title":"Yemeni Rial","symbol":"YER","native":"\u0631.\u064a.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"25030","max_amount":"250301249"},"ZAR":{"code":"ZAR","title":"South African Rand","symbol":"ZAR","native":"R","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"1362","max_amount":"13620106"}}` + +var SupportedCurrencies = make(map[string]Currency) + +func init() { + err := json.Unmarshal([]byte(dataCurrencies), &SupportedCurrencies) + if err != nil { + panic(err) + } +} diff --git a/vendor/gopkg.in/telebot.v3/poll.go b/vendor/gopkg.in/telebot.v3/poll.go new file mode 100644 index 000000000..8e2e5091d --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/poll.go @@ -0,0 +1,75 @@ +package telebot + +import "time" + +// PollType defines poll types. +type PollType string + +const ( + // NOTE: + // Despite "any" type isn't described in documentation, + // it needed for proper KeyboardButtonPollType marshaling. + PollAny PollType = "any" + + PollQuiz PollType = "quiz" + PollRegular PollType = "regular" +) + +// Poll contains information about a poll. +type Poll struct { + ID string `json:"id"` + Type PollType `json:"type"` + Question string `json:"question"` + Options []PollOption `json:"options"` + VoterCount int `json:"total_voter_count"` + + // (Optional) + Closed bool `json:"is_closed,omitempty"` + CorrectOption int `json:"correct_option_id,omitempty"` + MultipleAnswers bool `json:"allows_multiple_answers,omitempty"` + Explanation string `json:"explanation,omitempty"` + ParseMode ParseMode `json:"explanation_parse_mode,omitempty"` + Entities []MessageEntity `json:"explanation_entities"` + + // True by default, shouldn't be omitted. + Anonymous bool `json:"is_anonymous"` + + // (Mutually exclusive) + OpenPeriod int `json:"open_period,omitempty"` + CloseUnixdate int64 `json:"close_date,omitempty"` +} + +// PollOption contains information about one answer option in a poll. +type PollOption struct { + Text string `json:"text"` + VoterCount int `json:"voter_count"` +} + +// PollAnswer represents an answer of a user in a non-anonymous poll. +type PollAnswer struct { + PollID string `json:"poll_id"` + Sender *User `json:"user"` + Options []int `json:"option_ids"` +} + +// IsRegular says whether poll is a regular. +func (p *Poll) IsRegular() bool { + return p.Type == PollRegular +} + +// IsQuiz says whether poll is a quiz. +func (p *Poll) IsQuiz() bool { + return p.Type == PollQuiz +} + +// CloseDate returns the close date of poll in local time. +func (p *Poll) CloseDate() time.Time { + return time.Unix(p.CloseUnixdate, 0) +} + +// AddOptions adds text options to the poll. +func (p *Poll) AddOptions(opts ...string) { + for _, t := range opts { + p.Options = append(p.Options, PollOption{Text: t}) + } +} diff --git a/vendor/gopkg.in/telebot.v3/poller.go b/vendor/gopkg.in/telebot.v3/poller.go new file mode 100644 index 000000000..d45f2a543 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/poller.go @@ -0,0 +1,115 @@ +package telebot + +import "time" + +// Poller is a provider of Updates. +// +// All pollers must implement Poll(), which accepts bot +// pointer and subscription channel and start polling +// synchronously straight away. +// +type Poller interface { + // Poll is supposed to take the bot object + // subscription channel and start polling + // for Updates immediately. + // + // Poller must listen for stop constantly and close + // it as soon as it's done polling. + Poll(b *Bot, updates chan Update, stop chan struct{}) +} + +// LongPoller is a classic LongPoller with timeout. +type LongPoller struct { + Limit int + Timeout time.Duration + LastUpdateID int + + // AllowedUpdates contains the update types + // you want your bot to receive. + // + // Possible values: + // message + // edited_message + // channel_post + // edited_channel_post + // inline_query + // chosen_inline_result + // callback_query + // shipping_query + // pre_checkout_query + // poll + // poll_answer + // + AllowedUpdates []string `yaml:"allowed_updates"` +} + +// Poll does long polling. +func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) { + for { + select { + case <-stop: + return + default: + } + + updates, err := b.getUpdates(p.LastUpdateID+1, p.Limit, p.Timeout, p.AllowedUpdates) + if err != nil { + b.debug(err) + continue + } + + for _, update := range updates { + p.LastUpdateID = update.ID + dest <- update + } + } +} + +// MiddlewarePoller is a special kind of poller that acts +// like a filter for updates. It could be used for spam +// handling, banning or whatever. +// +// For heavy middleware, use increased capacity. +// +type MiddlewarePoller struct { + Capacity int // Default: 1 + Poller Poller + Filter func(*Update) bool +} + +// NewMiddlewarePoller wait for it... constructs a new middleware poller. +func NewMiddlewarePoller(original Poller, filter func(*Update) bool) *MiddlewarePoller { + return &MiddlewarePoller{ + Poller: original, + Filter: filter, + } +} + +// Poll sieves updates through middleware filter. +func (p *MiddlewarePoller) Poll(b *Bot, dest chan Update, stop chan struct{}) { + if p.Capacity < 1 { + p.Capacity = 1 + } + + middle := make(chan Update, p.Capacity) + stopPoller := make(chan struct{}) + stopConfirm := make(chan struct{}) + + go func() { + p.Poller.Poll(b, middle, stopPoller) + close(stopConfirm) + }() + + for { + select { + case <-stop: + close(stopPoller) + <-stopConfirm + return + case upd := <-middle: + if p.Filter(&upd) { + dest <- upd + } + } + } +} diff --git a/vendor/gopkg.in/telebot.v3/sendable.go b/vendor/gopkg.in/telebot.v3/sendable.go new file mode 100644 index 000000000..ecaae7fed --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/sendable.go @@ -0,0 +1,407 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" +) + +// Recipient is any possible endpoint you can send +// messages to: either user, group or a channel. +type Recipient interface { + Recipient() string // must return legit Telegram chat_id or username +} + +// Sendable is any object that can send itself. +// +// This is pretty cool, since it lets bots implement +// custom Sendables for complex kind of media or +// chat objects spanning across multiple messages. +type Sendable interface { + Send(*Bot, Recipient, *SendOptions) (*Message, error) +} + +// Send delivers media through bot b to recipient. +func (p *Photo) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": p.Caption, + } + b.embedSendOptions(params, opt) + + msg, err := b.sendMedia(p, params, nil) + if err != nil { + return nil, err + } + + msg.Photo.File.stealRef(&p.File) + *p = *msg.Photo + p.Caption = msg.Caption + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (a *Audio) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": a.Caption, + "performer": a.Performer, + "title": a.Title, + "file_name": a.FileName, + } + b.embedSendOptions(params, opt) + + if a.Duration != 0 { + params["duration"] = strconv.Itoa(a.Duration) + } + + msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail)) + if err != nil { + return nil, err + } + + if msg.Audio != nil { + msg.Audio.File.stealRef(&a.File) + *a = *msg.Audio + a.Caption = msg.Caption + } + + if msg.Document != nil { + msg.Document.File.stealRef(&a.File) + a.File = msg.Document.File + } + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": d.Caption, + "file_name": d.FileName, + } + b.embedSendOptions(params, opt) + + if d.FileSize != 0 { + params["file_size"] = strconv.FormatInt(d.FileSize, 10) + } + if d.DisableTypeDetection { + params["disable_content_type_detection"] = "true" + } + + msg, err := b.sendMedia(d, params, thumbnailToFilemap(d.Thumbnail)) + if err != nil { + return nil, err + } + + if doc := msg.Document; doc != nil { + doc.File.stealRef(&d.File) + *d = *doc + d.Caption = msg.Caption + } else if vid := msg.Video; vid != nil { + vid.File.stealRef(&d.File) + d.Caption = vid.Caption + d.MIME = vid.MIME + d.Thumbnail = vid.Thumbnail + } + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + } + b.embedSendOptions(params, opt) + + msg, err := b.sendMedia(s, params, nil) + if err != nil { + return nil, err + } + + msg.Sticker.File.stealRef(&s.File) + *s = *msg.Sticker + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": v.Caption, + "file_name": v.FileName, + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + if v.Width != 0 { + params["width"] = strconv.Itoa(v.Width) + } + if v.Height != 0 { + params["height"] = strconv.Itoa(v.Height) + } + if v.Streaming { + params["supports_streaming"] = "true" + } + + msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail)) + if err != nil { + return nil, err + } + + if vid := msg.Video; vid != nil { + vid.File.stealRef(&v.File) + *v = *vid + v.Caption = msg.Caption + } else if doc := msg.Document; doc != nil { + // If video has no sound, Telegram can turn it into Document (GIF) + doc.File.stealRef(&v.File) + + v.Caption = doc.Caption + v.MIME = doc.MIME + v.Thumbnail = doc.Thumbnail + } + + return msg, nil +} + +// Send delivers animation through bot b to recipient. +func (a *Animation) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": a.Caption, + "file_name": a.FileName, + } + b.embedSendOptions(params, opt) + + if a.Duration != 0 { + params["duration"] = strconv.Itoa(a.Duration) + } + if a.Width != 0 { + params["width"] = strconv.Itoa(a.Width) + } + if a.Height != 0 { + params["height"] = strconv.Itoa(a.Height) + } + + // file_name is required, without it animation sends as a document + if params["file_name"] == "" && a.File.OnDisk() { + params["file_name"] = filepath.Base(a.File.FileLocal) + } + + msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail)) + if err != nil { + return nil, err + } + + if anim := msg.Animation; anim != nil { + anim.File.stealRef(&a.File) + *a = *msg.Animation + } else if doc := msg.Document; doc != nil { + *a = Animation{ + File: doc.File, + Thumbnail: doc.Thumbnail, + MIME: doc.MIME, + FileName: doc.FileName, + } + } + + a.Caption = msg.Caption + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *Voice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": v.Caption, + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + + msg, err := b.sendMedia(v, params, nil) + if err != nil { + return nil, err + } + + msg.Voice.File.stealRef(&v.File) + *v = *msg.Voice + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *VideoNote) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + if v.Length != 0 { + params["length"] = strconv.Itoa(v.Length) + } + + msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail)) + if err != nil { + return nil, err + } + + msg.VideoNote.File.stealRef(&v.File) + *v = *msg.VideoNote + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (x *Location) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "latitude": fmt.Sprintf("%f", x.Lat), + "longitude": fmt.Sprintf("%f", x.Lng), + "live_period": strconv.Itoa(x.LivePeriod), + } + if x.HorizontalAccuracy != nil { + params["horizontal_accuracy"] = fmt.Sprintf("%f", *x.HorizontalAccuracy) + } + if x.Heading != 0 { + params["heading"] = strconv.Itoa(x.Heading) + } + if x.AlertRadius != 0 { + params["proximity_alert_radius"] = strconv.Itoa(x.Heading) + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendLocation", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers media through bot b to recipient. +func (v *Venue) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "latitude": fmt.Sprintf("%f", v.Location.Lat), + "longitude": fmt.Sprintf("%f", v.Location.Lng), + "title": v.Title, + "address": v.Address, + "foursquare_id": v.FoursquareID, + "foursquare_type": v.FoursquareType, + "google_place_id": v.GooglePlaceID, + "google_place_type": v.GooglePlaceType, + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendVenue", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers invoice through bot b to recipient. +func (i *Invoice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := i.params() + params["chat_id"] = to.Recipient() + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendInvoice", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers poll through bot b to recipient. +func (p *Poll) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "question": p.Question, + "type": string(p.Type), + "is_closed": strconv.FormatBool(p.Closed), + "is_anonymous": strconv.FormatBool(p.Anonymous), + "allows_multiple_answers": strconv.FormatBool(p.MultipleAnswers), + "correct_option_id": strconv.Itoa(p.CorrectOption), + } + if p.Explanation != "" { + params["explanation"] = p.Explanation + params["explanation_parse_mode"] = p.ParseMode + } + if p.OpenPeriod != 0 { + params["open_period"] = strconv.Itoa(p.OpenPeriod) + } else if p.CloseUnixdate != 0 { + params["close_date"] = strconv.FormatInt(p.CloseUnixdate, 10) + } + b.embedSendOptions(params, opt) + + var options []string + for _, o := range p.Options { + options = append(options, o.Text) + } + + opts, _ := json.Marshal(options) + params["options"] = string(opts) + + data, err := b.Raw("sendPoll", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers dice through bot b to recipient. +func (d *Dice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "emoji": string(d.Type), + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendDice", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers game through bot b to recipient. +func (g *Game) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "game_short_name": g.Name, + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendGame", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +func thumbnailToFilemap(thumb *Photo) map[string]File { + if thumb != nil { + return map[string]File{"thumb": thumb.File} + } + return nil +} diff --git a/vendor/gopkg.in/telebot.v3/stickers.go b/vendor/gopkg.in/telebot.v3/stickers.go new file mode 100644 index 000000000..3e0a6264b --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/stickers.go @@ -0,0 +1,212 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +type StickerSetType = string + +const ( + StickerRegular = "regular" + StickerMask = "mask" + StickerCustomEmoji = "custom_emoji" +) + +// StickerSet represents a sticker set. +type StickerSet struct { + Type StickerSetType `json:"sticker_type"` + Name string `json:"name"` + Title string `json:"title"` + Animated bool `json:"is_animated"` + Video bool `json:"is_video"` + Stickers []Sticker `json:"stickers"` + Thumbnail *Photo `json:"thumb"` + PNG *File `json:"png_sticker"` + TGS *File `json:"tgs_sticker"` + WebM *File `json:"webm_sticker"` + Emojis string `json:"emojis"` + ContainsMasks bool `json:"contains_masks"` // FIXME: can be removed + MaskPosition *MaskPosition `json:"mask_position"` +} + +// MaskPosition describes the position on faces where +// a mask should be placed by default. +type MaskPosition struct { + Feature MaskFeature `json:"point"` + XShift float32 `json:"x_shift"` + YShift float32 `json:"y_shift"` + Scale float32 `json:"scale"` +} + +// MaskFeature defines sticker mask position. +type MaskFeature string + +const ( + FeatureForehead MaskFeature = "forehead" + FeatureEyes MaskFeature = "eyes" + FeatureMouth MaskFeature = "mouth" + FeatureChin MaskFeature = "chin" +) + +// UploadSticker uploads a PNG file with a sticker for later use. +func (b *Bot) UploadSticker(to Recipient, png *File) (*File, error) { + files := map[string]File{ + "png_sticker": *png, + } + params := map[string]string{ + "user_id": to.Recipient(), + } + + data, err := b.sendFiles("uploadStickerFile", files, params) + if err != nil { + return nil, err + } + + var resp struct { + Result File + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return &resp.Result, nil +} + +// StickerSet returns a sticker set on success. +func (b *Bot) StickerSet(name string) (*StickerSet, error) { + data, err := b.Raw("getStickerSet", map[string]string{"name": name}) + if err != nil { + return nil, err + } + + var resp struct { + Result *StickerSet + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// CreateStickerSet creates a new sticker set. +func (b *Bot) CreateStickerSet(to Recipient, s StickerSet) error { + files := make(map[string]File) + if s.PNG != nil { + files["png_sticker"] = *s.PNG + } + if s.TGS != nil { + files["tgs_sticker"] = *s.TGS + } + if s.WebM != nil { + files["webm_sticker"] = *s.WebM + } + + params := map[string]string{ + "user_id": to.Recipient(), + "sticker_type": s.Type, + "name": s.Name, + "title": s.Title, + "emojis": s.Emojis, + "contains_masks": strconv.FormatBool(s.ContainsMasks), + } + + if s.MaskPosition != nil { + data, _ := json.Marshal(&s.MaskPosition) + params["mask_position"] = string(data) + } + + _, err := b.sendFiles("createNewStickerSet", files, params) + return err +} + +// AddSticker adds a new sticker to the existing sticker set. +func (b *Bot) AddSticker(to Recipient, s StickerSet) error { + files := make(map[string]File) + if s.PNG != nil { + files["png_sticker"] = *s.PNG + } else if s.TGS != nil { + files["tgs_sticker"] = *s.TGS + } else if s.WebM != nil { + files["webm_sticker"] = *s.WebM + } + + params := map[string]string{ + "user_id": to.Recipient(), + "name": s.Name, + "emojis": s.Emojis, + } + + if s.MaskPosition != nil { + data, _ := json.Marshal(&s.MaskPosition) + params["mask_position"] = string(data) + } + + _, err := b.sendFiles("addStickerToSet", files, params) + return err +} + +// SetStickerPosition moves a sticker in set to a specific position. +func (b *Bot) SetStickerPosition(sticker string, position int) error { + params := map[string]string{ + "sticker": sticker, + "position": strconv.Itoa(position), + } + + _, err := b.Raw("setStickerPositionInSet", params) + return err +} + +// DeleteSticker deletes a sticker from a set created by the bot. +func (b *Bot) DeleteSticker(sticker string) error { + _, err := b.Raw("deleteStickerFromSet", map[string]string{"sticker": sticker}) + return err + +} + +// SetStickerSetThumb sets a thumbnail of the sticker set. +// Animated thumbnails can be set for animated sticker sets only. +// +// Thumbnail must be a PNG image, up to 128 kilobytes in size +// and have width and height exactly 100px, or a TGS animation +// up to 32 kilobytes in size. +// +// Animated sticker set thumbnail can't be uploaded via HTTP URL. +// +func (b *Bot) SetStickerSetThumb(to Recipient, s StickerSet) error { + files := make(map[string]File) + if s.PNG != nil { + files["thumb"] = *s.PNG + } else if s.TGS != nil { + files["thumb"] = *s.TGS + } + + params := map[string]string{ + "name": s.Name, + "user_id": to.Recipient(), + } + + _, err := b.sendFiles("setStickerSetThumb", files, params) + return err +} + +// CustomEmojiStickers returns the information about custom emoji stickers by their ids. +func (b *Bot) CustomEmojiStickers(ids []string) ([]Sticker, error) { + data, _ := json.Marshal(ids) + + params := map[string]string{ + "custom_emoji_ids": string(data), + } + + data, err := b.Raw("getCustomEmojiStickers", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Sticker + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} diff --git a/vendor/gopkg.in/telebot.v3/telebot.go b/vendor/gopkg.in/telebot.v3/telebot.go new file mode 100644 index 000000000..b8271e9a4 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/telebot.go @@ -0,0 +1,150 @@ +// Package telebot is a framework for Telegram bots. +// +// Example: +// +// package main +// +// import ( +// "time" +// tele "gopkg.in/telebot.v3" +// ) +// +// func main() { +// b, err := tele.NewBot(tele.Settings{ +// Token: "...", +// Poller: &tele.LongPoller{Timeout: 10 * time.Second}, +// }) +// if err != nil { +// return +// } +// +// b.Handle("/start", func(c tele.Context) error { +// return c.Send("Hello world!") +// }) +// +// b.Start() +// } +package telebot + +import "errors" + +var ( + ErrBadRecipient = errors.New("telebot: recipient is nil") + ErrUnsupportedWhat = errors.New("telebot: unsupported what argument") + ErrCouldNotUpdate = errors.New("telebot: could not fetch new updates") + ErrTrueResult = errors.New("telebot: result is True") + ErrBadContext = errors.New("telebot: context does not contain message") +) + +const DefaultApiURL = "https://api.telegram.org" + +// These are one of the possible events Handle() can deal with. +// +// For convenience, all Telebot-provided endpoints start with +// an "alert" character \a. +const ( + // Basic message handlers. + OnText = "\atext" + OnEdited = "\aedited" + OnPhoto = "\aphoto" + OnAudio = "\aaudio" + OnAnimation = "\aanimation" + OnDocument = "\adocument" + OnSticker = "\asticker" + OnVideo = "\avideo" + OnVoice = "\avoice" + OnVideoNote = "\avideo_note" + OnContact = "\acontact" + OnLocation = "\alocation" + OnVenue = "\avenue" + OnDice = "\adice" + OnInvoice = "\ainvoice" + OnPayment = "\apayment" + OnGame = "\agame" + OnPoll = "\apoll" + OnPollAnswer = "\apoll_answer" + OnPinned = "\apinned" + OnChannelPost = "\achannel_post" + OnEditedChannelPost = "\aedited_channel_post" + OnTopicCreated = "\atopic_created" + OnTopicReopened = "\atopic_reopened" + OnTopicClosed = "\atopic_closed" + OnTopicEdited = "\atopic_edited" + OnGeneralTopicHidden = "\ageneral_topic_hidden" + OnGeneralTopicUnhidden = "\ageneral_topic_unhidden" + OnWriteAccessAllowed = "\awrite_access_allowed" + + OnAddedToGroup = "\aadded_to_group" + OnUserJoined = "\auser_joined" + OnUserLeft = "\auser_left" + OnUserShared = "\auser_shared" + OnChatShared = "\achat_shared" + OnNewGroupTitle = "\anew_chat_title" + OnNewGroupPhoto = "\anew_chat_photo" + OnGroupPhotoDeleted = "\achat_photo_deleted" + OnGroupCreated = "\agroup_created" + OnSuperGroupCreated = "\asupergroup_created" + OnChannelCreated = "\achannel_created" + + // OnMigration happens when group switches to + // a supergroup. You might want to update + // your internal references to this chat + // upon switching as its ID will change. + OnMigration = "\amigration" + + OnMedia = "\amedia" + OnCallback = "\acallback" + OnQuery = "\aquery" + OnInlineResult = "\ainline_result" + OnShipping = "\ashipping_query" + OnCheckout = "\apre_checkout_query" + OnMyChatMember = "\amy_chat_member" + OnChatMember = "\achat_member" + OnChatJoinRequest = "\achat_join_request" + OnProximityAlert = "\aproximity_alert_triggered" + OnAutoDeleteTimer = "\amessage_auto_delete_timer_changed" + OnWebApp = "\aweb_app" + + OnVideoChatStarted = "\avideo_chat_started" + OnVideoChatEnded = "\avideo_chat_ended" + OnVideoChatParticipants = "\avideo_chat_participants_invited" + OnVideoChatScheduled = "\avideo_chat_scheduled" +) + +// ChatAction is a client-side status indicating bot activity. +type ChatAction string + +const ( + Typing ChatAction = "typing" + UploadingPhoto ChatAction = "upload_photo" + UploadingVideo ChatAction = "upload_video" + UploadingAudio ChatAction = "upload_audio" + UploadingDocument ChatAction = "upload_document" + UploadingVNote ChatAction = "upload_video_note" + RecordingVideo ChatAction = "record_video" + RecordingAudio ChatAction = "record_audio" + RecordingVNote ChatAction = "record_video_note" + FindingLocation ChatAction = "find_location" + ChoosingSticker ChatAction = "choose_sticker" +) + +// ParseMode determines the way client applications treat the text of the message +type ParseMode = string + +const ( + ModeDefault ParseMode = "" + ModeMarkdown ParseMode = "Markdown" + ModeMarkdownV2 ParseMode = "MarkdownV2" + ModeHTML ParseMode = "HTML" +) + +// M is a shortcut for map[string]interface{}. +// Useful for passing arguments to the layout functions. +type M = map[string]interface{} + +// Flag returns a pointer to the given bool. +// Useful for passing the three-state flags to a Bot API. +// For example, see ReplyRecipient type. +func Flag(b bool) *bool { + return &b +} diff --git a/vendor/gopkg.in/telebot.v3/topic.go b/vendor/gopkg.in/telebot.v3/topic.go new file mode 100644 index 000000000..d81fe9a94 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/topic.go @@ -0,0 +1,172 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +type Topic struct { + Name string `json:"name"` + IconColor int `json:"icon_color"` + IconCustomEmojiID string `json:"icon_custom_emoji_id"` + ThreadID int `json:"message_thread_id"` +} + +// CreateTopic creates a topic in a forum supergroup chat. +func (b *Bot) CreateTopic(chat *Chat, topic *Topic) (*Topic, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "name": topic.Name, + } + + if topic.IconColor != 0 { + params["icon_color"] = strconv.Itoa(topic.IconColor) + } + if topic.IconCustomEmojiID != "" { + params["icon_custom_emoji_id"] = topic.IconCustomEmojiID + } + + data, err := b.Raw("createForumTopic", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Topic + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, err +} + +// EditTopic edits name and icon of a topic in a forum supergroup chat. +func (b *Bot) EditTopic(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": topic.ThreadID, + } + + if topic.Name != "" { + params["name"] = topic.Name + } + if topic.IconCustomEmojiID != "" { + params["icon_custom_emoji_id"] = topic.IconCustomEmojiID + } + + _, err := b.Raw("editForumTopic", params) + return err +} + +// CloseTopic closes an open topic in a forum supergroup chat. +func (b *Bot) CloseTopic(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": topic.ThreadID, + } + + _, err := b.Raw("closeForumTopic", params) + return err +} + +// ReopenTopic reopens a closed topic in a forum supergroup chat. +func (b *Bot) ReopenTopic(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": topic.ThreadID, + } + + _, err := b.Raw("reopenForumTopic", params) + return err +} + +// DeleteTopic deletes a forum topic along with all its messages in a forum supergroup chat. +func (b *Bot) DeleteTopic(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": topic.ThreadID, + } + + _, err := b.Raw("deleteForumTopic", params) + return err +} + +// UnpinAllTopicMessages clears the list of pinned messages in a forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. +func (b *Bot) UnpinAllTopicMessages(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": topic.ThreadID, + } + + _, err := b.Raw("unpinAllForumTopicMessages", params) + return err +} + +// TopicIconStickers gets custom emoji stickers, which can be used as a forum topic icon by any user. +func (b *Bot) TopicIconStickers() ([]Sticker, error) { + params := map[string]string{} + + data, err := b.Raw("getForumTopicIconStickers", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Sticker + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// EditGeneralTopic edits name of the 'General' topic in a forum supergroup chat. +func (b *Bot) EditGeneralTopic(chat *Chat, topic *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "name": topic.Name, + } + + _, err := b.Raw("editGeneralForumTopic", params) + return err +} + +// CloseGeneralTopic closes an open 'General' topic in a forum supergroup chat. +func (b *Bot) CloseGeneralTopic(chat *Chat) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("closeGeneralForumTopic", params) + return err +} + +// ReopenGeneralTopic reopens a closed 'General' topic in a forum supergroup chat. +func (b *Bot) ReopenGeneralTopic(chat *Chat) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("reopenGeneralForumTopic", params) + return err +} + +// HideGeneralTopic hides the 'General' topic in a forum supergroup chat. +func (b *Bot) HideGeneralTopic(chat *Chat) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("hideGeneralForumTopic", params) + return err +} + +// UnhideGeneralTopic unhides the 'General' topic in a forum supergroup chat. +func (b *Bot) UnhideGeneralTopic(chat *Chat) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("unhideGeneralForumTopic", params) + return err +} diff --git a/vendor/gopkg.in/telebot.v3/update.go b/vendor/gopkg.in/telebot.v3/update.go new file mode 100644 index 000000000..12a065ab2 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/update.go @@ -0,0 +1,375 @@ +package telebot + +import "strings" + +// Update object represents an incoming update. +type Update struct { + ID int `json:"update_id"` + + Message *Message `json:"message,omitempty"` + EditedMessage *Message `json:"edited_message,omitempty"` + ChannelPost *Message `json:"channel_post,omitempty"` + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` + Callback *Callback `json:"callback_query,omitempty"` + Query *Query `json:"inline_query,omitempty"` + InlineResult *InlineResult `json:"chosen_inline_result,omitempty"` + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + Poll *Poll `json:"poll,omitempty"` + PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"` + ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"` + ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` +} + +// ProcessUpdate processes a single incoming update. +// A started bot calls this function automatically. +func (b *Bot) ProcessUpdate(u Update) { + c := b.NewContext(u) + + if u.Message != nil { + m := u.Message + + if m.PinnedMessage != nil { + b.handle(OnPinned, c) + return + } + + // Commands + if m.Text != "" { + // Filtering malicious messages + if m.Text[0] == '\a' { + return + } + + match := cmdRx.FindAllStringSubmatch(m.Text, -1) + if match != nil { + // Syntax: "@ " + command, botName := match[0][1], match[0][3] + + if botName != "" && !strings.EqualFold(b.Me.Username, botName) { + return + } + + m.Payload = match[0][5] + if b.handle(command, c) { + return + } + } + + // 1:1 satisfaction + if b.handle(m.Text, c) { + return + } + + b.handle(OnText, c) + return + } + + if b.handleMedia(c) { + return + } + + if m.Contact != nil { + b.handle(OnContact, c) + return + } + if m.Location != nil { + b.handle(OnLocation, c) + return + } + if m.Venue != nil { + b.handle(OnVenue, c) + return + } + if m.Game != nil { + b.handle(OnGame, c) + return + } + if m.Dice != nil { + b.handle(OnDice, c) + return + } + if m.Invoice != nil { + b.handle(OnInvoice, c) + return + } + if m.Payment != nil { + b.handle(OnPayment, c) + return + } + + if m.TopicCreated != nil { + b.handle(OnTopicCreated, c) + return + } + if m.TopicReopened != nil { + b.handle(OnTopicReopened, c) + return + } + if m.TopicClosed != nil { + b.handle(OnTopicClosed, c) + return + } + if m.TopicEdited != nil { + b.handle(OnTopicEdited, c) + return + } + if m.GeneralTopicHidden != nil { + b.handle(OnGeneralTopicHidden, c) + return + } + if m.GeneralTopicUnhidden != nil { + b.handle(OnGeneralTopicUnhidden, c) + return + } + if m.WriteAccessAllowed != nil { + b.handle(OnWriteAccessAllowed, c) + return + } + + wasAdded := (m.UserJoined != nil && m.UserJoined.ID == b.Me.ID) || + (m.UsersJoined != nil && isUserInList(b.Me, m.UsersJoined)) + if m.GroupCreated || m.SuperGroupCreated || wasAdded { + b.handle(OnAddedToGroup, c) + return + } + + if m.UserJoined != nil { + b.handle(OnUserJoined, c) + return + } + if m.UsersJoined != nil { + for _, user := range m.UsersJoined { + m.UserJoined = &user + b.handle(OnUserJoined, c) + } + return + } + if m.UserLeft != nil { + b.handle(OnUserLeft, c) + return + } + + if m.UserShared != nil { + b.handle(OnUserShared, c) + return + } + if m.ChatShared != nil { + b.handle(OnChatShared, c) + return + } + + if m.NewGroupTitle != "" { + b.handle(OnNewGroupTitle, c) + return + } + if m.NewGroupPhoto != nil { + b.handle(OnNewGroupPhoto, c) + return + } + if m.GroupPhotoDeleted { + b.handle(OnGroupPhotoDeleted, c) + return + } + + if m.GroupCreated { + b.handle(OnGroupCreated, c) + return + } + if m.SuperGroupCreated { + b.handle(OnSuperGroupCreated, c) + return + } + if m.ChannelCreated { + b.handle(OnChannelCreated, c) + return + } + + if m.MigrateTo != 0 { + m.MigrateFrom = m.Chat.ID + b.handle(OnMigration, c) + return + } + + if m.VideoChatStarted != nil { + b.handle(OnVideoChatStarted, c) + return + } + if m.VideoChatEnded != nil { + b.handle(OnVideoChatEnded, c) + return + } + if m.VideoChatParticipants != nil { + b.handle(OnVideoChatParticipants, c) + return + } + if m.VideoChatScheduled != nil { + b.handle(OnVideoChatScheduled, c) + return + } + + if m.WebAppData != nil { + b.handle(OnWebApp, c) + return + } + + if m.ProximityAlert != nil { + b.handle(OnProximityAlert, c) + return + } + if m.AutoDeleteTimer != nil { + b.handle(OnAutoDeleteTimer, c) + return + } + } + + if u.EditedMessage != nil { + b.handle(OnEdited, c) + return + } + + if u.ChannelPost != nil { + m := u.ChannelPost + + if m.PinnedMessage != nil { + b.handle(OnPinned, c) + return + } + + b.handle(OnChannelPost, c) + return + } + + if u.EditedChannelPost != nil { + b.handle(OnEditedChannelPost, c) + return + } + + if u.Callback != nil { + if data := u.Callback.Data; data != "" && data[0] == '\f' { + match := cbackRx.FindAllStringSubmatch(data, -1) + if match != nil { + unique, payload := match[0][1], match[0][3] + if handler, ok := b.handlers["\f"+unique]; ok { + u.Callback.Unique = unique + u.Callback.Data = payload + b.runHandler(handler, c) + return + } + } + } + + b.handle(OnCallback, c) + return + } + + if u.Query != nil { + b.handle(OnQuery, c) + return + } + + if u.InlineResult != nil { + b.handle(OnInlineResult, c) + return + } + + if u.ShippingQuery != nil { + b.handle(OnShipping, c) + return + } + + if u.PreCheckoutQuery != nil { + b.handle(OnCheckout, c) + return + } + + if u.Poll != nil { + b.handle(OnPoll, c) + return + } + + if u.PollAnswer != nil { + b.handle(OnPollAnswer, c) + return + } + + if u.MyChatMember != nil { + b.handle(OnMyChatMember, c) + return + } + + if u.ChatMember != nil { + b.handle(OnChatMember, c) + return + } + + if u.ChatJoinRequest != nil { + b.handle(OnChatJoinRequest, c) + return + } +} + +func (b *Bot) handle(end string, c Context) bool { + if handler, ok := b.handlers[end]; ok { + b.runHandler(handler, c) + return true + } + return false +} + +func (b *Bot) handleMedia(c Context) bool { + var ( + m = c.Message() + fired = true + ) + + switch { + case m.Photo != nil: + fired = b.handle(OnPhoto, c) + case m.Voice != nil: + fired = b.handle(OnVoice, c) + case m.Audio != nil: + fired = b.handle(OnAudio, c) + case m.Animation != nil: + fired = b.handle(OnAnimation, c) + case m.Document != nil: + fired = b.handle(OnDocument, c) + case m.Sticker != nil: + fired = b.handle(OnSticker, c) + case m.Video != nil: + fired = b.handle(OnVideo, c) + case m.VideoNote != nil: + fired = b.handle(OnVideoNote, c) + default: + return false + } + + if !fired { + return b.handle(OnMedia, c) + } + + return true +} + +func (b *Bot) runHandler(h HandlerFunc, c Context) { + f := func() { + if err := h(c); err != nil { + b.OnError(err, c) + } + } + if b.synchronous { + f() + } else { + go f() + } +} + +func isUserInList(user *User, list []User) bool { + for _, user2 := range list { + if user.ID == user2.ID { + return true + } + } + return false +} diff --git a/vendor/gopkg.in/telebot.v3/video_chat.go b/vendor/gopkg.in/telebot.v3/video_chat.go new file mode 100644 index 000000000..4952e36b4 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/video_chat.go @@ -0,0 +1,31 @@ +package telebot + +import "time" + +type ( + // VideoChatStarted represents a service message about a video chat + // started in the chat. + VideoChatStarted struct{} + + // VideoChatEnded represents a service message about a video chat + // ended in the chat. + VideoChatEnded struct { + Duration int `json:"duration"` // in seconds + } + + // VideoChatParticipants represents a service message about new + // members invited to a video chat + VideoChatParticipants struct { + Users []User `json:"users"` + } + + // VideoChatScheduled represents a service message about a video chat scheduled in the chat. + VideoChatScheduled struct { + Unixtime int64 `json:"start_date"` + } +) + +// StartsAt returns the point when the video chat is supposed to be started by a chat administrator. +func (v *VideoChatScheduled) StartsAt() time.Time { + return time.Unix(v.Unixtime, 0) +} diff --git a/vendor/gopkg.in/telebot.v3/web_app.go b/vendor/gopkg.in/telebot.v3/web_app.go new file mode 100644 index 000000000..e5c9070f2 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/web_app.go @@ -0,0 +1,24 @@ +package telebot + +// WebApp represents a parameter of the inline keyboard button +// or the keyboard button used to launch Web App. +type WebApp struct { + URL string `json:"url"` +} + +// WebAppMessage describes an inline message sent by a Web App on behalf of a user. +type WebAppMessage struct { + InlineMessageID string `json:"inline_message_id"` +} + +// WebAppData object represents a data sent from a Web App to the bot +type WebAppData struct { + Data string `json:"data"` + Text string `json:"button_text"` +} + +// WebAppAccessAllowed represents a service message about a user allowing +// a bot to write messages after adding the bot to the attachment menu or launching a Web App from a link. +type WriteAccessAllowed struct { + WebAppName string `json:"web_app_name,omitempty"` +} diff --git a/vendor/gopkg.in/telebot.v3/webhook.go b/vendor/gopkg.in/telebot.v3/webhook.go new file mode 100644 index 000000000..5a48d6c3b --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/webhook.go @@ -0,0 +1,207 @@ +package telebot + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" +) + +// A WebhookTLS specifies the path to a key and a cert so the poller can open +// a TLS listener. +type WebhookTLS struct { + Key string `json:"key"` + Cert string `json:"cert"` +} + +// A WebhookEndpoint describes the endpoint to which telegram will send its requests. +// This must be a public URL and can be a loadbalancer or something similar. If the +// endpoint uses TLS and the certificate is self-signed you have to add the certificate +// path of this certificate so telegram will trust it. This field can be ignored if you +// have a trusted certificate (letsencrypt, ...). +type WebhookEndpoint struct { + PublicURL string `json:"public_url"` + Cert string `json:"cert"` +} + +// A Webhook configures the poller for webhooks. It opens a port on the given +// listen address. If TLS is filled, the listener will use the key and cert to open +// a secure port. Otherwise it will use plain HTTP. +// +// If you have a loadbalancer ore other infrastructure in front of your service, you +// must fill the Endpoint structure so this poller will send this data to telegram. If +// you leave these values empty, your local address will be sent to telegram which is mostly +// not what you want (at least while developing). If you have a single instance of your +// bot you should consider to use the LongPoller instead of a WebHook. +// +// You can also leave the Listen field empty. In this case it is up to the caller to +// add the Webhook to a http-mux. +// +type Webhook struct { + Listen string `json:"url"` + MaxConnections int `json:"max_connections"` + AllowedUpdates []string `json:"allowed_updates"` + IP string `json:"ip_address"` + DropUpdates bool `json:"drop_pending_updates"` + SecretToken string `json:"secret_token"` + + // (WebhookInfo) + HasCustomCert bool `json:"has_custom_certificate"` + PendingUpdates int `json:"pending_update_count"` + ErrorUnixtime int64 `json:"last_error_date"` + ErrorMessage string `json:"last_error_message"` + SyncErrorUnixtime int64 `json:"last_synchronization_error_date"` + + TLS *WebhookTLS + Endpoint *WebhookEndpoint + + dest chan<- Update + bot *Bot +} + +func (h *Webhook) getFiles() map[string]File { + m := make(map[string]File) + + if h.TLS != nil { + m["certificate"] = FromDisk(h.TLS.Cert) + } + // check if it is overwritten by an endpoint + if h.Endpoint != nil { + if h.Endpoint.Cert == "" { + // this can be the case if there is a loadbalancer or reverseproxy in + // front with a public cert. in this case we do not need to upload it + // to telegram. we delete the certificate from the map, because someone + // can have an internal TLS listener with a private cert + delete(m, "certificate") + } else { + // someone configured a certificate + m["certificate"] = FromDisk(h.Endpoint.Cert) + } + } + return m +} + +func (h *Webhook) getParams() map[string]string { + params := make(map[string]string) + + if h.MaxConnections != 0 { + params["max_connections"] = strconv.Itoa(h.MaxConnections) + } + if len(h.AllowedUpdates) > 0 { + data, _ := json.Marshal(h.AllowedUpdates) + params["allowed_updates"] = string(data) + } + if h.IP != "" { + params["ip_address"] = h.IP + } + if h.DropUpdates { + params["drop_pending_updates"] = strconv.FormatBool(h.DropUpdates) + } + if h.SecretToken != "" { + params["secret_token"] = h.SecretToken + } + + if h.TLS != nil { + params["url"] = "https://" + h.Listen + } else { + // this will not work with telegram, they want TLS + // but i allow this because telegram will send an error + // when you register this hook. in their docs they write + // that port 80/http is allowed ... + params["url"] = "http://" + h.Listen + } + if h.Endpoint != nil { + params["url"] = h.Endpoint.PublicURL + } + return params +} + +func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) { + if err := b.SetWebhook(h); err != nil { + b.OnError(err, nil) + close(stop) + return + } + + // store the variables so the HTTP-handler can use 'em + h.dest = dest + h.bot = b + + if h.Listen == "" { + h.waitForStop(stop) + return + } + + s := &http.Server{ + Addr: h.Listen, + Handler: h, + } + + go func(stop chan struct{}) { + h.waitForStop(stop) + s.Shutdown(context.Background()) + }(stop) + + if h.TLS != nil { + s.ListenAndServeTLS(h.TLS.Cert, h.TLS.Key) + } else { + s.ListenAndServe() + } +} + +func (h *Webhook) waitForStop(stop chan struct{}) { + <-stop + close(stop) +} + +// The handler simply reads the update from the body of the requests +// and writes them to the update channel. +func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.SecretToken != "" && r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != h.SecretToken { + h.bot.debug(fmt.Errorf("invalid secret token in request")) + return + } + + var update Update + if err := json.NewDecoder(r.Body).Decode(&update); err != nil { + h.bot.debug(fmt.Errorf("cannot decode update: %v", err)) + return + } + h.dest <- update +} + +// Webhook returns the current webhook status. +func (b *Bot) Webhook() (*Webhook, error) { + data, err := b.Raw("getWebhookInfo", nil) + if err != nil { + return nil, err + } + + var resp struct { + Result Webhook + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return &resp.Result, nil +} + +// SetWebhook configures a bot to receive incoming +// updates via an outgoing webhook. +func (b *Bot) SetWebhook(w *Webhook) error { + _, err := b.sendFiles("setWebhook", w.getFiles(), w.getParams()) + return err +} + +// RemoveWebhook removes webhook integration. +func (b *Bot) RemoveWebhook(dropPending ...bool) error { + drop := false + if len(dropPending) > 0 { + drop = dropPending[0] + } + _, err := b.Raw("deleteWebhook", map[string]bool{ + "drop_pending_updates": drop, + }) + return err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8e7cf126a..2116efcc8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -885,6 +885,9 @@ google.golang.org/protobuf/types/descriptorpb # gopkg.in/natefinch/lumberjack.v2 v2.2.1 ## explicit; go 1.13 gopkg.in/natefinch/lumberjack.v2 +# gopkg.in/telebot.v3 v3.2.1 +## explicit; go 1.13 +gopkg.in/telebot.v3 # gopkg.in/yaml.v2 v2.4.0 ## explicit; go 1.15 gopkg.in/yaml.v2 From af94d118f603edcadf45083627ece2c65ddecc74 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 08:22:37 -0500 Subject: [PATCH 5/7] add updated reward distribution script and .conf file template --- scripts/rewards/sendrewards.conf | 4 ++++ scripts/rewards/sendrewards.sh | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 scripts/rewards/sendrewards.conf create mode 100755 scripts/rewards/sendrewards.sh diff --git a/scripts/rewards/sendrewards.conf b/scripts/rewards/sendrewards.conf new file mode 100644 index 000000000..8837732fc --- /dev/null +++ b/scripts/rewards/sendrewards.conf @@ -0,0 +1,4 @@ +WALLET_FILE="$HOME/.skycoin/wallets/.wlt" +FROM_ADDRESS="HWteH5YSjqax9dnvkHsFPU4JzHiTxugbet" +REWARD_WL_SK= +REWARD_SYS_URL="dmsg://036a70e6956061778e1883e928c1236189db14dfd446df23d83e45c321b330c91f:80" diff --git a/scripts/rewards/sendrewards.sh b/scripts/rewards/sendrewards.sh new file mode 100755 index 000000000..cb8ec9aca --- /dev/null +++ b/scripts/rewards/sendrewards.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +## source configuration file +source sendrewards.conf +## check that skycoin wallet is running +skycoin-cli status ; [[ $? -ne 0 ]] && (echo 'skycoin wallet not running ; exiting' && exit 1) +## check that skywire-reward.service isn't running to ensure rewards calculation not ongoing +skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/s | jq -r '.' +[[ "$(skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/s | jq -r '.active')" == "active"* ]] && echo "skywire-rewards are calculating - reward service active ; not executing distribution to avoid partial file download" && exit 0 +## preview current reward statistics +source sendrewards.conf ; skywire dmsg curl $REWARD_SYS_URL/$(skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/csv | sed 's/_rewardtxn0.csv/_stats.txt/g') +## allow to accept or decline +read -n 1 -p "Send rewards? [Y/n]: " user_input +echo +if [[ "$user_input" != "Y" && "$user_input" != "y" ]]; then + echo "exiting" + exit 0 +fi +## get link to the latest CSV +#skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/csv -s $REWARD_WL_SK +## get reward csv data +# skywire dmsg curl $REWARD_SYS_URL/$(skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/csv -s $REWARD_WL_SK) -s $REWARD_WL_SK | tr -d ' ' | awk -F, '{printf "%s,%.3f\n", $1, int($2*1000)/1000}' | grep -v '^,0.000$' +skywire dmsg curl $REWARD_SYS_URL/reward -d "$(skycoin-cli createRawTransaction $WALLET_FILE -a $FROM_ADDRESS --csv <(skywire dmsg curl $REWARD_SYS_URL/$(skywire dmsg curl $REWARD_SYS_URL/skycoin-rewards/csv -s $REWARD_WL_SK) -s $REWARD_WL_SK | tr -d ' ' | awk -F, '{printf "%s,%.3f\n", $1, int($2*1000)/1000}' | grep -v '^,0.000$'))" -s $REWARD_WL_SK From a4997921715313af2fd8f4e40171f2d4638972c3 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 09:06:47 -0500 Subject: [PATCH 6/7] update reward distribution config template --- scripts/rewards/sendrewards.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rewards/sendrewards.conf b/scripts/rewards/sendrewards.conf index 8837732fc..62035f847 100644 --- a/scripts/rewards/sendrewards.conf +++ b/scripts/rewards/sendrewards.conf @@ -1,4 +1,4 @@ WALLET_FILE="$HOME/.skycoin/wallets/.wlt" FROM_ADDRESS="HWteH5YSjqax9dnvkHsFPU4JzHiTxugbet" -REWARD_WL_SK= +REWARD_WL_SK= REWARD_SYS_URL="dmsg://036a70e6956061778e1883e928c1236189db14dfd446df23d83e45c321b330c91f:80" From 709534a8d4361017223296c65fe110de91e42654 Mon Sep 17 00:00:00 2001 From: Moses Narrow Date: Fri, 24 May 2024 09:45:58 -0500 Subject: [PATCH 7/7] add updated skywire specs documents from skycoinpro/skywire-specs --- skywire-specs/Drafts.md | 28 + skywire-specs/README.md | 3 + skywire-specs/Specifications.md | 2379 +++++++++++++++++ skywire-specs/VPN/Client.md | 5 + skywire-specs/VPN/Handshake.md | 52 + skywire-specs/VPN/Server.md | 41 + skywire-specs/VPN/Specs.md | 113 + skywire-specs/img/routing-table-diagram.png | Bin 0 -> 33897 bytes skywire-specs/specifications/01-Overview.md | 19 + .../02-HTTP_Authorization_Middleware.md | 98 + skywire-specs/specifications/03-Transport.md | 124 + .../specifications/04-Transport_Discovery.md | 349 +++ .../specifications/05-Messaging_System.md | 514 ++++ skywire-specs/specifications/06-Packets.md | 212 ++ .../specifications/07-Transport_Management.md | 113 + .../specifications/08-Route_Finder.md | 89 + .../specifications/09-Route_Setup_Process.md | 9 + .../specifications/10-Routing_Table.md | 63 + skywire-specs/specifications/11-Router.md | 19 + skywire-specs/specifications/12-App_Server.md | 37 + skywire-specs/specifications/13-Setup_Node.md | 3 + .../specifications/14-Skywire_Visor.md | 429 +++ skywire-specs/specifications/15-Hypervisor.md | 18 + skywire-specs/specifications/16-Glossary.md | 104 + .../specifications/17-App2 module.md | 106 + .../specifications/18-Router2 module.md | 296 ++ skywire-specs/transports/stcpr/Specs.md | 22 + skywire-specs/transports/sudph/Specs.md | 77 + 28 files changed, 5322 insertions(+) create mode 100644 skywire-specs/Drafts.md create mode 100644 skywire-specs/README.md create mode 100644 skywire-specs/Specifications.md create mode 100644 skywire-specs/VPN/Client.md create mode 100644 skywire-specs/VPN/Handshake.md create mode 100644 skywire-specs/VPN/Server.md create mode 100644 skywire-specs/VPN/Specs.md create mode 100644 skywire-specs/img/routing-table-diagram.png create mode 100644 skywire-specs/specifications/01-Overview.md create mode 100644 skywire-specs/specifications/02-HTTP_Authorization_Middleware.md create mode 100644 skywire-specs/specifications/03-Transport.md create mode 100644 skywire-specs/specifications/04-Transport_Discovery.md create mode 100644 skywire-specs/specifications/05-Messaging_System.md create mode 100644 skywire-specs/specifications/06-Packets.md create mode 100644 skywire-specs/specifications/07-Transport_Management.md create mode 100644 skywire-specs/specifications/08-Route_Finder.md create mode 100644 skywire-specs/specifications/09-Route_Setup_Process.md create mode 100644 skywire-specs/specifications/10-Routing_Table.md create mode 100644 skywire-specs/specifications/11-Router.md create mode 100644 skywire-specs/specifications/12-App_Server.md create mode 100644 skywire-specs/specifications/13-Setup_Node.md create mode 100644 skywire-specs/specifications/14-Skywire_Visor.md create mode 100644 skywire-specs/specifications/15-Hypervisor.md create mode 100644 skywire-specs/specifications/16-Glossary.md create mode 100644 skywire-specs/specifications/17-App2 module.md create mode 100644 skywire-specs/specifications/18-Router2 module.md create mode 100644 skywire-specs/transports/stcpr/Specs.md create mode 100644 skywire-specs/transports/sudph/Specs.md diff --git a/skywire-specs/Drafts.md b/skywire-specs/Drafts.md new file mode 100644 index 000000000..daefd8145 --- /dev/null +++ b/skywire-specs/Drafts.md @@ -0,0 +1,28 @@ +# Drafts + +## Transport State + +The modifiable data associated with a *Transport* is stored in a different structure called the *Transport State*. As a *Transport* has two edges (and hence, two perspectives), a *Transport State* is also updated accordingly. + +## Packet types + +Transports, Routes and the *Route Setup Service* are to deliver data via Packets. Packets are responsible for setting up routes and streams, and delivering data within the constructed routes and streams. + +Here is a summary of all the *Packet Types*. + +| Type | Value | Description | +| ---- | ------ | ----------- | +| `Ping` | `0x0` | Sent between a Transport to check if connection is still open, and to determine latency of the Transport. | +| `InitiateRoute` | `0x1` | First packet sent via a route to have it initiated. | +| `RouteInitiated` | `0x2` | Confirms that a route is set up and functional. | +| `DestroyRoute` | `0x3` | Initiate the destruction of a route. | +| `RouteDestroyed` | `0x4` | Confirm the success of a route's destruction. | +| `OpenStream` | `0x5` | Opens a stream within a route. | +| `StreamOpened` | `0x6` | Confirms that a stream is successfully opened. | +| `CloseStream` | `0x7` | Closes a stream. | +| `StreamClosed` | `0x8` | Informs that a stream has successfully closed. | +| `Forward` | `0x9` | Forwards data via a specified stream. | + +## Route Setup Service + +The *Route Setup Service* is a service which communicates with ... \ No newline at end of file diff --git a/skywire-specs/README.md b/skywire-specs/README.md new file mode 100644 index 000000000..5dc194fff --- /dev/null +++ b/skywire-specs/README.md @@ -0,0 +1,3 @@ +# Skywire Specifications + +The Skywire specifications. diff --git a/skywire-specs/Specifications.md b/skywire-specs/Specifications.md new file mode 100644 index 000000000..287bd8d1c --- /dev/null +++ b/skywire-specs/Specifications.md @@ -0,0 +1,2379 @@ +- [Overview](#overview) +- [HTTP Authorization Middleware](#http-authorization-middleware) + - [Authorization Procedures](#authorization-procedures) +- [Transport](#transport) + - [Transport Module](#transport-module) +- [Transport Discovery](#transport-discovery) + - [Transport Discovery Procedures](#transport-discovery-procedures) + - [Security Procedures](#security-procedures) + - [Code Structure](#code-structure) + - [Database](#database) + - [Endpoint Definitions](#endpoint-definitions) + - [GET Incrementing Security Nonce](#get-incrementing-security-nonce) + - [GET Transport Entry via Transport ID](#get-transport-entry-via-transport-id) + - [GET Transport(s) via Edge Public Key](#get-transports-via-edge-public-key) + - [POST Register Transport(s)](#post-register-transports) + - [POST Status(es)](#post-statuses) +- [Dmsg](#dmsg-system) + - [Dmsg Modules](#dmsg-system-modules) + - [Dmsg Procedures](#dmsg-procedures) + - [Dmsg Discovery](#dmsg-discovery) + - [Instance Entry](#instance-entry) + - [Store Interface](#store-interface) + - [Endpoints](#endpoints) + - [GET Entry](#get-entry) + - [POST Entry](#post-entry) + - [GET Available Servers](#get-available-servers) + - [Dmsg Discovery Client Library](#dmsg-discovery-client-library) + - [Dmsg Discovery Integration Tests](#dmsg-discovery-integration-tests) + - [Dmsg Link](#dmsg-link) + - [Link Handshake Frames](#link-handshake-frames) + - [Dmsg Frames](#dmsg-frames) + - [Noise Implementation in Channels](#noise-implementation-in-channels) + - [Implementation in Code](#implementation-in-code) + - [Dmsg Instance](#dmsg-instance) + - [Configuring an Instance](#configuring-an-instance) + - [Instance Interaction with Dmsg Discovery](#instance-interaction-with-dmsg-discovery) + - [Channel Management](#channel-management) + - [Opening a Channel](#opening-a-channel) + - [Closing a Channel](#closing-a-channel) + - [Handling Disconnections](#handling-disconnections) +- [Packets](#packets) + - [Settlement Packets](#settlement-packets) + - [Foundational Packets](#foundational-packets) + - [`0x30 CreateLoop`](#0x30-createloop) + - [`0x31 LoopCreated`](#0x31-loopcreated) + - [`0x32 LoopNotCreated`](#0x32-loopnotcreated) + - [`0x33 ConfirmLoop`](#0x33-confirmloop) + - [`0x34 LoopConfirmed`](#0x34-loopconfirmed) + - [`0x35 SecureRIDs`](#0x35-securerids) + - [`0x36 RIDsSecured`](#0x36-ridssecured) + - [`0x37 RIDsNotSecured`](#0x37-ridsnotsecured) + - [`0x38 AddRules`](#0x38-addrules) + - [`0x39 RulesAdded`](#0x39-rulesadded) + - [`0x3A RemoveRules`](#0x3a-removerules) + - [`0x3B RulesRemoved`](#0x3b-rulesremoved) + - [Data Packets](#data-packets) + - [Loopback Packets](#loopback-packets) +- [Transport Management](#transport-management) + - [Transport Manager Procedures](#transport-manager-procedures) + - [Logging](#logging) +- [Route Finder](#route-finder) + - [Graph Algorithm](#graph-algorithm) + - [Routing algorithm](#routing-algorithm) + - [Code Structure](#code-structure-1) + - [Database](#database-1) + - [Endpoint Definitions](#endpoint-definitions-1) + - [GET Routes available for the defined start and end key](#get-routes-available-for-the-defined-start-and-end-key) +- [Route Setup Process](#route-setup-process) +- [Routing Table](#routing-table) +- [Router](#router) +- [App Server](#app-server) + - [Loop Encryption](#loop-encryption) +- [Setup Node](#setup-node) +- [App Node](#app-node) + - [App Node Configuration](#app-node-configuration) + - [App Node RPC Interface](#app-node-rpc-interface) + - [Commands](#commands) + - [Ports Management](#ports-management) +- [Manager Node](#manager-node) + - [Manager Node REST API (and User Interface)](#manager-node-rest-api-and-user-interface) +- [Glossary](#glossary) + +# Overview + +Skywire is an decentralized network that attempts to replace the current internet. The *Skywire Network* is made up of *Skywire Nodes*. There are currently two types of *Skywire Nodes*; *App Node* and *Setup Node*. + +Each *Skywire Node* is represented by a unique public key. A direct line of communication between two *Skywire Nodes* is called a *Transport*. Each *Transport* is represented by a unique *Transport ID* which is of a *Transport Type*, and the two *Skywire Nodes* that are connected via the *Transport* is named the *Transport Edges*. + +A *Route* is unidirectional and delivers data units called *Packets*. It is made up of multiple hops where each hop is a *Transport*. Two *Routes* of opposite directions make a *Loop* when associated with the given *Ports* at each *Loop Edge*. *Loops* handle the communication between two *Skywire Apps* and is represented via the *Loop's* source and destination node's public keys and the source and destination ports (similar to how TCP/UDP handles ports). + +A *Packet* is prefixed with a *Route ID* which helps *Skywire Nodes* identify how the *Packet* is to be handled (either to be forward to a remote node, or to be consumed internally). Every *Skywire Node* has a *Routing Table* that has the *Routing Rules* for that particular *Skywire Node*. + +In summary, + +- *Transports* are responsible for single-hop communication between two *Skywire Nodes* and are bidirectional (this may change later on). +- *Routes* are responsible for multi-hop communication between two *Skywire Nodes* and are unidirectional. +- *Loops* are responsible for communication between two *Skywire Apps* and are bidirectional. + +There are many ways in which we can implement a *Transport*. Each unique method is called a *Transport Type*. + +Initially, we need to implement a MVP in which we assume that there are no malicious nodes in the network and discovery of routes, transports and nodes are to be done in a centralized manner. However, basic authentication and encryption is still required. + +# HTTP Authorization Middleware + +Skywire is made up of multiple services and nodes. Some of these services/nodes communicate via restful interfaces, and some of the endpoints require authentication and authorization. + +As nodes in the Skywire network are identified via public keys, an appropriate approach to authentication and authorization is via public/private key cryptography. The curve to use is `secp256k1`, and when referenced by the RESTFUL endpoints, it is to be represented as a hexadecimal string format. + +These HTTP security middleware features should be implemented within the `/pkg/utils/httpauth` module of the `skywire` repository. This module not only provides server-side logic, but also client-side logic to make interaction with the server-side more streamlined. + +## Authorization Procedures + +To avoid replay attacks and unauthorized access, each remote entity (represented by it's public key) is assigned an *Security Nonce* by the `httpauth` module. The remote entity is required to sign the *Security Nonce* alongside the request body on every request. + +For each successful request, the next expected *Security Nonce* is to increment. The `httpauth` module is to provide an interface named `NonceStorer` to keep an record of "remote entity public key" to "next expected nonce" associations. The following is a proposed structure for `NonceStorer`; + +```golang +// NonceStorer stores Incrementing Security Nonces. +type NonceStorer interface { + + // IncrementNonce increments the nonce associated with the specified remote entity. + // It returns the next expected nonce after it has been incremented and returns error on failure. + IncrementNonce(ctx context.Context, remotePK cipher.PubKey) (nonce uint64, err error) + + // Nonce obtains the next expected nonce for a given remote entity (represented by public key). + // It returns error on failure. + Nonce(ctx context.Context, remotePK cipher.PubKey) (nonce uint64, err error) + + // Count obtains the number of entries stored in the underlying database. + Count(ctx context.Context) (n int, err error) +} +``` + +Take note that the only times the next-expected *Security Nonce* (for a given remote entity) is to increment, is when a successful request happens. + +Initially (when no successful requests has been processed for a given remote entity), the next expected *Security Nonce* should always be zero. When it is this value, the underlying database for the `NonceStorer` implementation should not need an entry for it. + +For every request that requires authentication and authorization in this manner, the structure `httpauth.Server` is to handle it. Specifically, it is to "wrap" the original `http.HandlerFunc` to add additional logic for checking the request. Consequently, the `httpauth.Client` appends the needed additional headers to the request. + +The following extra header values are required (`SW` stands for Skywire); + +- `SW-Public` - Specifies the public key (hexadecimal string representation) of the Skywire Node performing this operation. +- `SW-Nonce` - Specifies the incrementing nonce provided by this operation. +- `SW-Sig` - Specifies the of the signature (hexadecimal string representation) of the hash result of the concatenation of the Security Nonce + Body of the request. + +The `httpauth.Server` should also provide the `http.HandlerFunc` which obtains the next expected incrementing nonce for a given public key. This is required when a remote entity looses sync. A successful response of this call should look something of the following; + +```json +{ + "edge": "", + "next_nonce": 0 +} +``` + +The following is a proposed implementation of `httpauth.Server`; + +```golang +package httpauth + +// Server provides server-side logic for Skywire-related RESTFUL authorization and authentication. +type Server struct { + // implementation ... +} + +// NewServer creates a new authentication server with the provided NonceStorer. +func NewServer(store NonceStorer) *Server { + // implementation ... +} + +// WrapConfig configures the '(*Server).Wrap' function. +type WrapConfig struct { + // MaxHTTPBodyLen specifies the max body length that is acceptable. + // No limit is set if the value is 0. + MaxHTTPBodyLen int + + // PubKeyWhitelist specifies the whitelisted public keys. + // If value is nil, no whitelist rules are set. + PubKeyWhitelist []cipher.PubKey +} + +// Wrap wraps a http.HandlerFunc and adds authentication logic. +// The original http.HandlerFunc is responsible for setting the status code. +// The middleware logic should only increment the security nonce if the status code +// from the original http.HandlerFunc is of 2xx value (representing success). +func (as *Server) Wrap(config *WrapConfig, original http.HandlerFunc) http.HandlerFunc { + // implementation ... +} + +// HandleNextNonce returns a http handler that +func (as *Server) NextNonceHandler(remotePK cipher.PubKey) http.HandlerFunc { + // implementation ... +} +``` + +Take note that for the `(*Server).Wrap` function, we will need to define a custom `http.ResponseWriter` to obtain the status code (https://www.reddit.com/r/golang/comments/7p35s4/how_do_i_get_the_response_status_for_my_middleware/). + +The `httpauth.Client` implementation is responsible for providing logic for the following actions; + +- Keep a local record of the next expected *Security Nonce*. +- Adding security header values to a given request (`http.Request`). + +# Transport + +A *Transport* represents a bidirectional line of communication between two *Skywire Nodes* (or *Transport Edges*). A Transport is responsible for ensuring accurate delivery of data and providing symmetric encryption between the two nodes communicating. + +Each *Transport* is represented as a unique 16 byte (128 bit) UUID value called the *Transport ID* and has a *Transport Type* that identifies a specific implementation of the *Transport*. + +A *Transport* has the following information associated with it; + +- **Transport ID:** A `uuid.UUID` value that uniquely identifies the Transport. +- **Edges:** The public keys of the Transport's edge nodes (should only have 2 edges and the initiating edge should come first). +- **Type:** A `string` value that specifies the particular implementation of the *Transport*. +- **Public:** A `bool` that specifies whether the *Transport* is to be registered in the *Transport Discovery* or not. Only public transports are registered. +- **Registered:** A `int64` value that is the epoch time of when the *Transport* is registered in *Transport Discovery*. A value of `0` represents the state where the *Transport* is not (or not yet) registered in the *Transport Discovery*. + +This is a JSON representation of a *Transport Entry*; + +```json +{ + "t_id": "e1808c316b23d1d6119cad1795238ff0", + "edges": ["031d796272349d597d6d3130497ccd11cf8af12c7d186b1726358abfb49edad0c1", "03bd9724f335c5eb5a1011e7862d4af28488102c8edffc84585cf0826ac4864b38"], + "type": "dmsg", + "public": true +} +``` + +## Transport Module + +In code, `Transport` is an interface, and can have many implementations. + +The interface used to generate *Transports* of a certain *Transport Type* is named *Transport Factory* (represented by a `transport.Factory` interface in code). + +The representation of a *Transport* in *Transport Discovery* is of the type `transport.Entry`. + +A `transport.Status` type contains the status of a given *Transport*. Each *Transport Edge* provides such status, and the *Transport Discovery* compares the two statuses to derive the final status. + +```golang +package transport + +// Transport represents communication between two nodes via a single hop. +type Transport interface { + + // Read implements io.Reader + Read(p []byte) (n int, err error) + + // Write implements io.Writer + Write(p []byte) (n int, err error) + + // Close implements io.Closer + Close() error + + // Local returns the local transport edge's public key. + Local() cipher.PubKey + + // Remote returns the remote transport edge's public key. + Remote() cipher.PubKey + + // Type returns the string representation of the transport type. + Type() string + + // SetDeadline functions the same as that from net.Conn + // With a Transport, we don't have a distinction between write and read timeouts. + SetDeadline(t time.Time) error +} + +// Factory generates Transports of a certain type. +type Factory interface { + + // Accept accepts a remotely-initiated Transport. + Accept(ctx context.Context) (Transport, error) + + // Dial initiates a Transport with a remote node. + Dial(ctx context.Context, remote cipher.PubKey) (Transport, error) + + // Close implements io.Closer + Close() error + + // Local returns the local public key. + Local() cipher.PubKey + + // Type returns the Transport type. + Type() string +} + +// Entry is the unsigned representation of a Transport. +type Entry struct { + + // ID is the Transport ID that uniquely identifies the Transport. + ID uuid.UUID `json:"tid"` + + // Edges contains the public keys of the Transport's edge nodes (the public key of the node that initiated the transport should be on index 0). + Edges [2]string `json:"edges"` + + // Type represents the transport type. + Type string `json:"type"` + + // Public determines whether the transport is to be exposed to other nodes or not. + // Public transports are to be registered in the Transport Discovery. + Public bool `json:"public"` +} + +// SignedEntry holds an Entry and it's associated signatures. +// The signatures should be ordered as the contained 'Entry.Edges'. +type SignedEntry struct { + Entry *Entry `json:"entry"` + Signatures [2]string `json:"signatures"` + Registered int64 `json:"registered,omitempty"` +} + +// Status represents the current state of a Transport from the perspective +// from a Transport's single edge. Each Transport will have two perspectives; +// one from each of it's edges. +type Status struct { + + // ID is the Transport ID that identifies the Transport that this status is regarding. + ID uuid.UUID `json:"tid"` + + // IsUp represents whether the Transport is up. + // A Transport that is down will fail to forward Packets. + IsUp bool `json:"is_up"` + + // Updated is the epoch timestamp of when the status is last updated. + Updated int64 `json:"updated,omitempty"` +} +``` + +# Transport Discovery + +The Transport Discovery is a service that exposes a RESTful interface and interacts with a database on the back-end. + +The database stores *Transport Entries* that can be queried using their *Transport ID* or via a given *Transport Edge*. + +The process of submitting a *Transport Entry* is called *Registration* and a Transport cannot be deregistered. However, nodes that are an *Edge* of a *Transport*, can update their *Transport Status*, and specify whether the *Transport* is up or down. + +Any state-altering RESTful call to the *Transport Discovery* is authenticated using signatures, and replay attacks are avoided by expecting an incrementing security nonce (all communication should be encrypted with HTTPS anyhow). + +## Transport Discovery Procedures + +This is a summary of the procedures that the *Transport Discovery* is to handle. + +**Registering a Transport:** + +Technically, *Transports* are created by the Skywire Nodes themselves via an internal *Transport Factory* implementation. The *Transport Discovery* is only responsible for registering *Transports* in the form of a *Transport Entry*. + +When two Skywire Nodes establish a Transport connection between them, it is at first, unregistered in the *Transport Discovery*. The node that initiated the creation of the Transport (or the node that called the `(transport.Transport).Dial` method), is the node that is responsible for initiating the *Transport Settlement Handshake*. + +If two nodes; **A** and **B** establish a *Transport* between them (where **A** is the *Transport Initiator*), **A** is then also responsible for sending the first handshake packet for the *Transport Settlement Handshake*. The procedure is as follows: + +1. **A** sends **B** a proposed `transport.Entry` and also **A**'s signature of the Entry (in the form of `transport.SignedEntry`). + +2. **B** checks the `transport.SignedEntry` sent from **A**; + + 1. The `Entry.ID` field should be unique (check via *Transport Discovery*). + 2. The `Entry.Edges` field should be ordered correctly and contain public keys of **A** and **B**. + 3. The `Entry.Type` field should have the expected Transport Type. + 4. The `Signatures` field should contain **A**'s valid signature in the correct location (in the same index as **A**'s public key in `Entry.Edges`). + 5. The `Registered` field should be empty. + +3. **B** then adds it's only signature to the `transport.SignedEntry` and registers it to the *Transport Discovery*. Both public and private Transports are registered in the *Transport Discovery* (however only public *Transports* are publicly available). + +4. **B** then informs **A** on the success/failure of the registration, or just that the `transport.SignedEntry` is accepted by itself (depending on whether the Transport is to be public or not). + +**Submitting Transport Statuses:** + +If a given *Transport* is public, the associated *Transport Edges* is responsible for submitting their individual *Transport Statuses* to the *Transport Discovery* whenever the follow events occur; + +- Directly after a *Transport* is first successfully registered in the *Transport Discovery*. +- Whenever the *Transport* comes online/offline (connected/disconnected). + +**Obtaining Transports:** + +There are two ways to obtain transports; either via the assigned *Transport ID*, or via one of the *Transport Edges*. There is no restriction as who can access this information and results can be sorted by a given meta. + +## Security Procedures + +**Incrementing Security Nonce:** + +An *Incrementing Security Nonce* is represented by a `uint64` value. + +To avoid replay attacks and unauthorized access, each public key of a *Skywire Node* is assigned an *Incrementing Security Nonce*, and is expected to sign it with the rest of the body, and include the signature result in the http header. The *Incrementing Security Nonce* should increment every time an" endpoint is called (except for the endpoint that obtains the next expected incrementing security nonce). An *Incrementing Security Nonce* is not operation-specific, and increments every time any endpoint is called by the given Skywire Node. + +The *Transport Discovery* should store a table of expected next *Incrementing Security Nonce* for each public key of a *Skywire Node*. There is an endpoint `GET /security/nonces/{public-key}` that provides the next expected *Incrementing Security Nonce* for a given Node public key. This endpoint should be publicly accessible, but nevertheless, the *Skywire Nodes* themselves should keep a copy of their next expected *Incrementing Security Nonce*. + +The only times an *Incrementing Security Nonce* should not increment is when: + +- An invalid request is submitted (missing/extra fields, invalid signature). +- An internal server error occurs. + +Initially, the expected *Incrementing Security Nonce* should be 0. When it is this value, the *Transport Discovery* should not have an entry for it. + +Each operation should contain the following extra header entries: + +- `SW-Public` - Specifies the public key of the Skywire Node performing this operation. +- `SW-Nonce` - Specifies the incrementing nonce provided by this operation. +- `SW-Sig` - Specifies the hex-representation of the signature of the hash result of the concatenation of the *Incrementing Security Nonce* + Body of the request. + +If these values are not valid, the *Transport Discovery* should reject the request. + +## Code Structure + +The code should be in the `skywire-services` repository. + +- `/cmd/transport-discovery/transport-discovery.go` is the main executable for the *Transport Discovery*. +- `/pkg/transport-discovery/api/` contains the RESTFUL API definitions. +- `/pkg/transport-discovery/store/` contains the definition of the `Storer` interface and it's implementations. +- `/pkg/transport-discovery/client/` contains the client library that interacts with the *Transport Discovery* server's RESTFUL API. + +## Database + +The *Transport Discovery* should work with a variety of databases and the following interfaces should be defined for such implementations; + +- `TransportStorer` should store *Transport Signed Entries* and it's associated *Transport Statuses*. +- `NonceStorer` should store expected *Incrementing Nonces*. + +## Endpoint Definitions + +The following is a summary of all the *Transport Discovery* endpoints. + +- `GET /security/nonces/edge:` +- `GET /transports/id:` +- `GET /transports/edge:` +- `POST /transports` +- `POST /statuses` + +All endpoints should include an `Accept: application/json` field and the response header should include an `Content-Type: application/json` field. + +All requests (except for obtaining the next expected incrementing nonce) should include the following fields. + +``` +Accept: application/json +Content-Type: application/json +SW-Public: +SW-Nonce: +SW-Sig: +``` + +### GET Incrementing Security Nonce + +Obtains the next expected incrementing nonce for a given edge's public key. + +**Request:** + +``` +GET /security/nonces/ +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "edge": "", + "next_nonce": 0 + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### GET Transport Entry via Transport ID + +Obtains a *Transport* via a given *Transport ID*. + +Should only return a single `"transport"` result. + +**Request:** + +``` +GET /transports/id: +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "" + }, + "is_up": true, + "registered": 0 + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### GET Transport(s) via Edge Public Key + +Obtains *Transport(s)* via a given *Transport Edge* public key. + +**Request:** + +``` +GET /transports/edge: +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "entry": { + "t_id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "is_up": true, + "registered": 0 + }, + { + "entry": { + "t_id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "is_up": false, + "registered": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### POST Register Transport(s) + +Registers one or multiple Transports. + +**Request:** + +``` +POST /transports +``` + +```json +[ + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ] + }, + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ] + } +] +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ], + "registered": 0 + }, + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ], + "registered": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 401 Unauthorized (Invalid signature/nonce). +- 408 Request Timeout (Timed out). +- 500 Internal Server Error (Server error). + +### POST Status(es) + +Submits one or multiple *Transport Status(es)* from the perspective of the submitting node. The returned result is the final *Transport Status(es)* determined by the *Transport Discovery* that is generated using the submitted *Transport Status(es)* of the two edges. + +When a Transport is registered, it is considered to be *up*. Then after, every time a node's *Status* is submitted, the *Transport Discovery* alters the final state *Status* with the following rules: + +- If there is only one edge's *Status* submitted, the final status is of that of the submitted *Status*. +- If there are two *Status*es submitted and they both agree, final *Status* will also be the same. +- If the two submitted *Status*es disagree, then the final *Status* is always *Down*. + +**Request:** + +``` +POST /statuses +``` + +```json +[ + { + "id": "", + "is_up": true + }, + { + "id": "", + "is_up": true + } +] +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "id": "", + "is_up": true, + "updated": 0 + }, + { + "id": "", + "is_up": false, + "updated": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 401 Unauthorized (Invalid signature/nonce). +- 408 Request Timeout (Timed out). +- 500 Internal Server Error (Server error). + +# Dmsg + +[Dmsg](https://github.com/skycoin/dmsg) is an initial implementation of the `Transport` and associated interfaces. To work, dmsg requires an active internet connection and is designed to be horizontally scalable. + +Three services make up dmsg: *Dmsg Client* (or *Client Instance*), *Dmsg Server* (or *Service Instance*) and *Dmsg Discovery*. + +*Dmsg Clients* and *Dmsg Servers* are represented by public/private key pairs. *Dmsg Clients* deliver data to one another via *Dmsg Servers* which act as relays. + +The *Dmsg Discovery* is responsible for allowing *Dmsg Clients* to find other advertised *Dmsg Clients* via their public keys. It is also responsible for finding appropriate *Dmsg Servers* that either "advertise" them, or "advertise" other *Dmsg Clients*. + +``` + [D] + + S(1) S(2) + // \\ // \\ + // \\ // \\ + C(A) C(B) C(C) C(D) +``` + +Legend: +- ` [D]` - Discovery Service +- `S(X)` - Dmsg Server (Server Instance) +- `C(X)` - Dmsg Client (Client Instance) + +## Dmsg Modules + +There are two modules of dmsg. + +- `dmsg-discovery` contains the implementation of the *Dmsg Discovery*. +- `dmsg` contains the implementation of either a *Client Instance* or a *Server Instance* of a *Dmsg Client* or *Dmsg Server*. + +## Dmsg Procedures + +This is a summary of the procedures that the *Dmsg* is to handle. + +**Advertising a client:** + +To be discoverable by other clients, a client needs to advertise itself. + +1. Client queries the Discovery to find available Servers. +2. Client connects to some (or all) of the suggested Servers. +3. Client updates it's own record in Discovery to include it's delegated Servers. + +**Client creates a channel to another client:** + +In order for two clients to communicate, both clients need to be connected to the same dmsg server, and create a channel to each other via the server. + +1. Client queries discovery of the remote client's connected servers. The client will connect to one of these servers if it originally has no shared servers with the remote. +2. The client sends a `OpenChannel` frame to the remote client via the shared server. +3. If the remote client accepts, a `ChannelOpened` frame is sent back to the initiating client (also via the shared server). A channel is represented via two *Channel IDs* (between the initiating client and the server, and between the responding client and the server). The associated between the two channel IDs is defined within the server. +4. Once a channel is created, clients can communicate via one another via the channel. + +## Dmsg Discovery + +The *Dmsg Discovery* acts like a DNS for dmsg instances (*Dmsg Clients* or *Dmsg Servers*). + +### Instance Entry + +An entry within the *Dmsg Discovery* can either represent a *Dmsg Server* or a *Dmsg Client*. The *Dmsg Discovery* is a key-value store, in which entries (of either server or client) use their public keys as their "key". + +The following is the representation of an Entry in Golang. + +```golang +// Entry represents an Instance's entry in the Discovery database. +type Entry struct { + // The data structure's version. + Version string `json:"version"` + + // A Entry of a given public key may need to iterate. This is the iteration sequence. + Sequence uint64 `json:"sequence"` + + // Timestamp of the current iteration. + Timestamp int64 `json:"timestamp"` + + // Public key that represents the Instance. + Static string `json:"static"` + + // Contains the node's required client meta if it's to be advertised as a Dmsg Client. + Client *Client `json:"client,omitempty"` + + // Contains the node's required server meta if it's to be advertised as a Dmsg Server. + Server *Server `json:"server,omitempty"` + + // Signature for proving authenticity of of the Entry. + Signature string `json:"signature,omitempty"` +} + +// Client contains the node's required client meta, if it is to be advertised as a Dmsg Client. +type Client struct { + // DelegatedServers contains a list of delegated servers represented by their public keys. + DelegatedServers []string `json:"delegated_servers"` +} + +// Server contains the node's required server meta, if it is to be advertised as a Dmsg Server. +type Server struct { + // IPv4 or IPv6 public address of the Dmsg Server. + Address string `json:"address"` + + // Port in which the Dmsg Server is listening for connections. + Port string `json:"port"` + + // Number of connections still available. + AvailableConnections int `json:"available_connections"` +} +``` + +**Definition rules:** + +- A record **MUST** have either a "Server" field, a "Client" field, or both "Server" and "Client" fields. In other words, a Dmsg Node can be a Dmsg Server Node, a Dmsg Client Node, or both a Dmsg Server Node and a Dmsg Client Node. + +**Iteration rules:** + +- The first entry submitted of a given static public key, needs to have a "Sequence" value of `0`. Any future entries (of the same static public key) need to have a "Sequence" value of `{previous_sequence} + 1`. +- The "Timestamp" field of an entry, must be of a higher value than the "Timestamp" value of the previous entry. + +**Signature Rules:** + +The "Signature" field authenticates the entry. This is the process of generating a signature of the entry: +1. Obtain a JSON representation of the Entry, in which: + 1. There is no whitespace (no ` ` or `\n` characters). + 2. The `"signature"` field is non-existent. +2. Hash this JSON representation, ensuring the above rules. +3. Create a Signature of the hash using the node's static secret key. + +The process of verifying an entry's signature will be similar. + +### Store Interface + +The underlying database of the *Dmsg Discovery* is a key-value store. The `Store` interface allows many databases to be used with the *Dmsg Discovery*. + +```golang +type Store interface { + // Entry obtains a single dmsg instance entry. + // 'static' is a hex representation of the public key identifying the dmsg instance. + Entry(ctx context.Context, static string) (*Entry, error) + + // SetEntry set's an entry. + // This is unsafe and does not check signature. + SetEntry(ctx context.Context, entry *Entry) error + + // AvailableServers discovers available dmsg servers. + // Obtains at most 'maxCount' amount of available servers obtained randomly. + AvailableServers(ctx context.Context, maxCount int) ([]*Entry, error) +} +``` + +### Endpoints + +Only 3 endpoints need to be defined; Get Entry, Post Entry, and Get Available Servers. + +#### GET Entry +Obtains a dmsg node's entry. +> `GET {domain}/discovery/entries/{public_key}` + +**REQUEST** + +Header: +``` +Accept: application/json +``` + +**RESPONSE** + +Possible Status Codes: +- Success (200) - Successfully updated record. + - Header: + ``` + Content-Type: application/json + ``` + - Body: + > JSON-encoded entry. +- Not Found (404) - Entry of public key is not found. +- Unauthorized (401) - invalid signature. +- Internal Server Error (500) - something unexpected happened. + +#### POST Entry +Posts an entry and replaces the current entry if valid. +> `POST {domain}/discovery/entries` + +**REQUEST** + +Header: +``` +Content-Type: application/json +``` +Body: +> JSON-encoded, signed Entry. + +**RESPONSE** + +Possible Response Codes: +- Success (200) - Successfully registered record. +- Unauthorized (401) - invalid signature. +- Internal Server Error (500) - something unexpected happened. + +#### GET Available Servers +Obtains a subset of available server entries. +> `GET {domain}/discovery/available_servers` + +**REQUEST** + +Header: +``` +Accept: application/json +``` + +**RESPONSE** + +Possible Status Codes: +- Success (200) - Got results. + - Header: + ``` + Content-Type: application/json + ``` + - Body: + > JSON-encoded `[]Entry`. +- Not Found (404) - No results. +- Forbidden (403) - When access is forbidden. +- Internal Server Error (500) - Something unexpected happened. + +### Dmsg Discovery Client Library + +The module is named `client`. It contains a `HTTPClient` structure, that defines how the client will interact with the *Dmsg Discovery* API. + +A new `HTTPClient` object can be instantiated using the public function `New(address string)`. + +It exposes the following public methods: + +```go +// Entry retrieves an entry associated to the given public key from the discovery server. +func (*HTTPClient) Entry(ctx context.Context, static string) (*Entry, error) { + // definition ... +} + +// SetEntry tries to set the given entry on the discovery server. It must be signed. +// If the entry is modifying a previous one, must be signed by the same private key. +func (*HTTPClient) SetEntry(ctx context.Context, entry *Entry) error { + // definition ... +} + +// AvailableServers gets a list of server entries from the skywire discovery server. +// The amount is determined by the discovery server. +func (*HTTPClient) AvailableServers(ctx context.Context) ([]*Entry, error) { + // definition ... +} +``` + +The module also provides public functions to instantiate valid `Entry` objects. + +### Dmsg Discovery Integration Tests + +> **TODO:** Fix wording. + +This package does not uses another `messenger` package, so integration tests are defined for the external services that `discovery` is using. In this case, the external store and `discovery` itself for testing the `client` library. + +The cases for the store integration testing: + + 1. Its able to set an entry on the database without error by calling the `storer.SetEntry` method. + 2. Its able to retrieve the previously set entry by calling the `storer.Entry` method. + 3. Creates multiple service entries and store them by calling `storer.SetEntry`, then it should be able to retrieve them with `store.AvailableServers`. + 4. `store.AvailableServers` receives a `maxCount int` argument. We also test it passing an integer which value is less than the amount of server entries we have set in the database, it should return this exact amount of server entries. + 5. Same as in number 4, but we set `maxCount` to number bigger than the number of server entries we have set, now we should get an slice of the size of the server entries we have set. + +In order to run the test we preferably create a clean new instance of the store database using Docker, and the test code should connect to it. We remove it after we have tested. + +In order to test the client library we do integration test with an instance of the discovery server. +The test cases for the client integration testing: + +1. By using the method SetEntry the client can set a new entry on the discovery server. +2. By using the method SetEntry the client can update a previously set entry on the discovery server. +3. If using SetEntry to update a previously set Entry, but the new sequence is not the previous sequence + 1 it should return an error with status code 500, Something unexpected happened. +4. If using SetEntry to update a previously set Entry, but the signature of the new entry has been made by a different secret key it should return an error with status code 401, Invalid signature. +5. By calling the method Entry with the public key of a previously set Entry it should return that entry. +6. By calling the method Entry with the public key of a previously non-set Entry it should return an error with code 404, Entry of public key is not found. +7. By calling the method AvailableServers when there are previously set server entries it should return them. + +## Dmsg Link + +The `link` provides two *Dmsg Instances* a means to establish a connection with one another, and also handle a pool of connections. + +Using the *Dmsg Discovery*, a *Dmsg Instance* can discover other instances via only their public key. However, a *Link* requires both a public key and an address:port. + +Data sent via a *Link* is encapsulated in *Frames*. A *Link* is implemented using a TCP connection. + +### Link Handshake Frames + +When setting up a *Link* between two instances, the instance that initiates is called the *Initiator* and the instance that responds is called the *Responder*. Each instance is represented by a public key. + +To set up a *Link*, the *Initiator* first dials a TCP connection to the listening *Responder*. Once the TCP connection is established, the *Responder* sends the first *Frame*. It is expected that the *Initiator* knows the public key of the *Responder* + +Given a situation where instances 'A' and 'B' are to establish a link with one another (where 'A' is the initiator), the following *Frames* are delivered to perform a handshake. + +Link Handshake Frames are to be in JSON format. + +**Link Handshake Frame 1 (A -> B):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779" +} +``` + +The initiator is responsible for sending the first frame. +- `"version"` specifies the version of dmsg protocol that the initiator is using (`"0.1"` for now). +- `"initiator"` should contain the hex representation of the public key of the initiator (the instance that is sending the first handshake frame). +- `"responder"` should contain the hex representation of the public key of the expected responder (the responder should disconnect TCP if this is not their public key). +- `"nonce"` is the hex-string representation of a 16-byte nonce that the responder should sign (alongside the initiator's public key) to check authenticity of the responder and whether the responder. + +**Link Handshake Frame 2 (B -> A):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01" +} +``` + +This frame allows the responder agree with the initiator and prove it's ownership of it's claimed public key. + +The `"sig1"` field contains a hex representation of the result of signing the concatenation of the version, initiator, responder and nonce fields. Note that before concatenation, hex representations should be decoded and the concatenation result needs to be hashed before being signed. `"sig1"` should be signed by the responder. + +**Link Handshake Frame 3 (A -> B):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01", + "sig2": "fc17928d5a3f7691434282fb3108d1603889f996e8e45adc2e35362e08009b8611abb9f45e511b9931f0f04b37ff1057fd69554befe534ad28c77ff0c44121ab00" +} +``` + +This frame allows the initiator to inform the responder that `"sig1"` is accepted, and to prove the initiator's ownership of it's public key. + +`"sig2"` is the signature result of the concatenation of the version, initiator, responder, nonce and sig1 fields. Concatenation rules are the same as that of `"sig1"`. + +**Link Handshake Frame 4 (B -> A):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01", + "sig2": "fc17928d5a3f7691434282fb3108d1603889f996e8e45adc2e35362e08009b8611abb9f45e511b9931f0f04b37ff1057fd69554befe534ad28c77ff0c44121ab00", + "accepted": true +} +``` + +Sent by the responder, this frame concludes the handshake if the value of `"accepted"` is `true`. + +### Dmsg Frames + +After the handshake phase, frames have a reoccurring format. These are the *Dmsg Frames* of dmsg. + +``` +| FrameType | PayloadSize | Payload | +| 1 byte | 2 bytes | ~ bytes | +``` + +- The `Type` specifies the frame type. Different frame types are used for opening and closing channels as well as sending packets via the channels. +- `PayloadSize` contains an encoded `uint16` value that represents the Payload's length (the max size is 65535). +- `Payload` has a length determined by `PayloadSize`. + +The following is a summary of the frame types. + +| FrameTypeValue | FrameTypeName | FrameBody | +| -------------- | ------------- | --------- | +| `0x0` | `OpenChannel` | ChannelID + RemoteStatic + NoiseMessage1 | +| `0x1` | `ChannelOpened` | ChannelID + NoiseMessage2 | +| `0x2` | `CloseChannel` | ChannelID | +| `0x3` | `ChannelClosed` | ChannelID | +| `0x4` | `Send` | ChannelID + CipherText | + +The `FrameBody` has the following sub-fields. A `FrameBody` with multiple sub-fields have the sub-fields concatenated. + +- The `ChannelID` sub-field is represented by a single byte. This restricts a *Client Instance* to have at most 256 channels via a single *Server Instance*. +- The `RemoteStatic` sub-field is represented by 33 bytes. It contains a public key of a remote *Client Instance*. +- `NoiseMessage1` and `NoiseMessage2` are both represented by 49 bytes. It contains the noise handshake messages for establishing symmetric encryption between the two client instances of the channel. The noise handshake pattern used is KK. +- The `CipherText` sub-field is the only sub-field with a modular length. It contains size of the encrypted payload followed by payload that is to be delivered. + +### Noise Implementation in Channels + +As stated above, a channel is established using the `OpenChannel` and `ChannelOpened` frames. Then, after a channel is established, the two *Client Instances* of the channel can communicate with each over via `Send` frames (which includes a `CipherText` component). + +The protocol used to establish the symmetric encryption of the `CipherText` is the [Noise Protocol](http://noiseprotocol.org/). + +The curve used will be `secp256k1` for the key pair, and `chacha20poly1305` will be used for the symmetric encryption itself. + +Note that, the noise protocol requires the public key length and the ECDH result length (shared secret) to be equal. Because for `secp256k1`, public keys have a length of 33, and the ECDH result has a length of 32, so an empty byte (`0x0`) should be appended to all generated ECDH results. Hence, the `DHLEN` constant for the noise protocol should be 33. + +After the handshake, the CipherState object will be used by the *Client Instances* to encrypt and decrypt the `CipherText` contained within the `Send` frame. + +**Handshake pattern:** + +Only the `KK` [interactive handshake pattern (fundamental)](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) will be supported. + +``` +-> s +<- s +... +-> e, es, ss +<- e, ee, se +``` + +The `-> e, es, ss` message is the `NoiseMessage1` of a `OpenChannel` frame, while the `<- e, ee, se` message is the `NoiseMessage2` of a `ChannelOpened` frame. + +### Implementation in Code + +Within the `dmsg` module: + +- `Link` structure should represent a link between two instances. +- `Pool` structure should handle multiple `Links` (with different instances). +- `Client` which implements a *Client Instance*. +- `Server` which implements a *Server Instance*. + +*Client Instances* communicate with each other via a *Server Instance* (which acts as a relay). + +Both structs will use `link.Pool` to handle links, but *Frames* are handled differently. *Client Instances* are to implement `TransportFactory` while a *Server Instance* is not required to. A *Client Instance* should also represent an established *Channel* as a `Transport` implementation. + +### Configuring an Instance + +When creating an *Instance*, the following options should be available via the following structure. + +```golang +// Config configures an instance. +type Config struct { + // Public determines whether the instance is to advertise itself to the dmsg discovery servers. + Public bool + + // DiscoveryAddresses contains the dmsg discovery services to be used (in order of preference). + DiscoveryAddresses []string +} +``` + +The above structure is to be an input when creating a *Server Instance* or a *Client Instance*. + +### Instance Interaction with Dmsg Discovery + +On startup `Server` that is supposed to be publicly available should register itself in dmsg discovery. To do so it first has to fetch current version of an `Entry`, if entry doesn't exist it should create one. If entry exists it may update it if necessary. + +On startup `Client` may connect to necessary amount of servers by fetching list of available servers from the discovery. Once initial connections are established `Client` should update discovery entry to advertise it's relays. + +### Channel Management + +The following is a proposal of how a Channel can be represented in code. + +```golang +// Channel represents a channel that implements Transport. It can be from the perspective of a Server or Client Instance. +type Channel struct { + // ChannelID represents the ID that is associated of the adjacent link. + ChannelID uint8 + + // Destination is the public key of the instance that is the final destination. + // This should always contain the public key of a client instance (as a server cannot be the final destination). + Destination cipher.PubKey + + // Link contains the adjacent link of the channel. + Link *link.Link +} +``` + +Both the client and server instances needs to manage channels. Channels are associated with a channel ID and also the public key(s) of the remote instances that the channel interacts with. Channels are hence identified by *Link* + *Channel ID*. + +From the perspective of a *Client Instance*, the assignment of *Channel IDs* are unique to a given link with a *Server Instance*. For example, let's say client 'A' is connected with server 'B' and server 'C', hence we have links 'AB' and 'AC'. We can have 'AB' and 'AC' share the same Channel ID, but because the channel itself is associated with a different link, they are considered different channels. + +From the perspective of a *Server Instance*, the assignment of *Channel IDs* are unique to a given link with a *Client Instance*. + +### Opening a Channel + +A channel in it's entirety handles the communication between two client instances via a server instance (which acts as a relay). Within the link between a single client instance and the server instance, a channel is represented using a *Channel ID*. The *Channel ID* of the two *links* of the same "channel" can be different, and the *Server Instance* is responsible for recording this association of the *Channel IDs* (coupled with the client instance's public key). + +When a *Client Instance* wishes to communicate with another *Client Instance*, it is responsible for initiating the creation of a channel. To do so, t sends a `OpenChannel` frame to the *Server Instance* in which: + +- `ChannelID` contains a ChannelID that the client wishes to associate with the channel. +- `RemoteStatic` contains the public key of the remote *Client Instance* that the local client wishes to communicate via this channel. +- `NoiseMessage1` is the first noise handshake message (the handshake pattern used is KK). + +If the *Server Instance* wishes to reject the request to open channel, it can send a `ChannelClosed` frame back to the initiating client with the `ChannelID` sub-field containing the value of the channel ID suggested by the initiating client. + +If the *Server Instance* wishes to go forward with opening of a channel, it sends a `OpenChannel` frame to the second *Client Instance*, in which `ChannelID` is an ID that's unique between the server and the second client and public key of the first client. + +If the second *Client Instance* wishes to reject the request, it can send a `ChannelClosed` frame back to the server, and the server can subsequently send a `ChannelClosed` frame to the initiating client (the `ChannelID` sub-fields of these `ChannelClosed` frames should be the unique channel IDs of the associated links). + +If the second *Client Instance* accepts the request, it sends a `ChannelOpened` back to the *Server Instance* (with the `NoiseMessage2`). Subsequently, the *Server Instance* sends a `ChannelOpened` back to the initiating client (the `ChannelID` sub-fields of these `ChannelOpened` frames should be the unique channel IDs of the associated links). + +### Closing a Channel + +A *Client Instance* can safely close any of it's channels by sending a `CloseChannel` (with the associated `ChannelID`) to the *Server Instance*. + +After a *Client Instance* sends a `CloseChannel`, no more frames are to be sent by that instance. However, the remote instance can still send frames until it receives the `CloseChannel` to it. The "close-responding" client then sends a `ChannelClosed` instance back to the "close-initiating" client. Once the `ChannelClosed` channel is sent by the "close-responding" client, it will no longer send or receive frames. Once the "close-initiator" receives the `ChannelClosed` frame. it will no longer receive frames. + +In summary, + +- When a client instance sends a `CloseChannel` frame, the channel is "partially-closed" and the client instance will only receive and not send via the channel. If a `ChannelClosed` frame is not received after a given timeout, the channel sends a `ChannelClosed` itself and the channel is "fully-closed". +- When a client instance receives a `CloseChannel` frame, it delivers a `ChannelClosed` frame and the channel is "fully-closed" and the client will no longer receive or send via the channel. +- When a client instance receives a `ChannelClosed` frame, the channel is "fully-closed". + +### Handling Disconnections + +In any given situation, there may be a possibility that the *Server Instance* unexpectedly disconnects with a *Client Instance*, or that a *Client Instance* unexpectedly disconnects with a *Server Instance*. This should directly affect the channels associated with the *Link* in question. + +When a *Client Instance* detects that a *Server Instance* has disconnected from it. All associated channels with that *Server Instance* should be closed. When a channel closes, the associated *Transport* should also be closed. + +When a *Server Instance* detects a disconnection from a *Client Instance*, it should send a `ChannelClosed` frame to all the other *Client Instances* that shares a channel with the disconnected client. After so, the *Server Instance* should dissociate all relations with the closed channels. + +# Packets + +The *Node Module* handles data encapsulated within data units called *Packets*. *Packets* can be grouped within the following categories based on their use-case; + +- ***Settlement Packets*** are used by the *Transport Manager* to "settle" Transports. Settlement, allows the two nodes that are the edges of the transport to decide on the *Transport ID* to be used, and whether the Transport is to be public. Only after a *Transport* is settled, can the *Router* have access to the Transport. + + *Settlement Packets* contain `json` encoded payload. + +- ***Foundational Packets*** are used by a *Router* to communicate with a remote *Setup Node* and is used for setting up, establishing and destroying routes. + + *Foundational Packets* are prefixed by 3 bytes: the packet size (2 bytes) and a Type (1 byte) that contains the foundational packet type. + +- ***Data Packets*** are Packets that are actually used to encapsulate data delivered between two Apps. + + *Data Packets* are prefixed by 6 bytes; including the packet size (2 bytes) and the Route ID (4 bytes) which can have any value other than `0x00` or `0x01`. + +- ***Loopback Packets*** are packets that are consumed locally by the node. + + *Loopback Packets* are structurally similar to data packets but their Route ID links to a rule that specifies which app to forward the packet to. + +## Settlement Packets + +After a Transport is established between two nodes, the nodes needs to decide on the *Transport ID* that describes the Transport and whether the Transport is to be public or private (public Transports are to be registered in the *Transport Discovery*). This process is called the *Settlement Handshake*. + +The Packets of this handshake contain `json` encoded messages. + +*Settlement Handshake* packets do not need a field for Packet-type are they are expected in a specific order. + +- Request to settle transport is sent by the *Transport Initiator* to the *Transport Responder* after a *Transport* connection is established. + + JSON Body: Contains a `transport.SignedEntry` structure with the *Transport Initiator*'s signature. + +- *Transport Responder* should validate submitted `transport.SignedEntry`, and if entry is valid it should add sign it and perform transport registration in transport discovery. If registration was successful responder should send updated `transport.SignedEntry` back to initiator. + + JSON Body: Contains a `transport.SignedEntry` structure with signatures from both the *Transport Initiator* and the *Transport Responder*. If the transport is registered in *Transport Discovery*, the `SignedTransport.Registered` should contain the epoch time of registration. + +If transport will fail at any step participants can chose to stop handshake procedures and close corresponding transport. Transport disconnect during the handshake should be handled appropriately by participants. Optional handshake timeout should also be supported. + +## Foundational Packets + +Foundational packets are used for the communication between *App Nodes* and *Setup Nodes*. + +The *Setup Node* is responsible for fulfilling Route initiating and destroying requests by communicating with the initiating, responding and intermediate nodes of the proposed route. + +The following is the expected format of a Foundational Packet; + +``` +| Packet Len | Type | JSON Body | +| 2 bytes | 1 byte | ~ | +``` + +- ***Packet Len*** specifies the total packet length in bytes (exclusive of the *Packet Len* field). +- ***Type*** specifies the *Foundational Packet Type*. +- ***JSON Body*** is the packet body (in JSON format) that is unique depending on the packet type. + +**Foundational Packet Types Summary:** + +| Type | Name | +| ---- | ---- | +| 0x00 | `AddRules` | +| 0x01 | `RemoveRules` | +| 0x02 | `CreateLoop` | +| 0x03 | `ConfirmLoop` | +| 0x04 | `CloseLoop` | +| 0x05 | `LoopClosed` | +| 0xfe | `ResponseFailure` | +| 0xff | `ResponseSuccess` | + +### `0x00 AddRules` + +Sent by the *Setup Node* to all *Nodes* of the route. This packet informs nodes what rules are to be added to their internal routing table. + +**JSON Body:** + +```json +[, ] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + [, ] + ``` + +### `0x01 RemoveRules` + +Sent by the *Setup Node* to *Node* of the route. + +**JSON Body:** + +```json +["", "rid-2"] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + [, ] + ``` + +### `0x02 CreateLoop` + +Sent by the *Route Initiator* to a *Setup Node* to have a *Loop* created. + +**JSON Body:** + +```json +{ + "local-port": , + "remote-port": , + "forward": [ + { + "from": "", + "to": "", + "tid": "" + }, + { + "from": "", + "to": "", + "tid": "" + } + ], + "reverse": [ + { + "from": "", + "to": "", + "tid": "" + }, + { + "from": "", + "to": "", + "tid": "" + } + ], + "expiry": "" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x3 ConfirmLoop` + +Sent by the *Setup Node* to Responder and Initiator *Node* to confirm notify about route in opposite direction. + +**JSON Body:** + +```json +{ + "remote-pk": "", + "remote-port": , + "local-port": , + "resp-rid": +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x4 CloseLoop` + +Sent by a Responder or Initiator *Node* to a *Setup Node* to notify about closing a loop locally. + +**JSON Body:** + +```json +{ + "port": "", + "remote": { + "port": , + "pk": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x5 LoopClosed` + +Sent by a *Setup Node* to a Responder or Initiator to notify about closed loop on the opposite end. + +**JSON Body:** + +```json +{ + "port": "", + "remote": { + "port": , + "pk": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + + +## Data Packets + +The follow is the structure of a *Data Packet*. + +``` +| Packet Len | Route ID | Payload | +| 2 bytes | 4 bytes | ~ | +``` + +# Transport Management + +For all Skywire Node types, we need a universal way for managing and logging Transports. The structure that is responsible for this is the `TransportManager` (which should be within the `/pkg/node` package of the `skywire` package). + +As the `TransportManager` needs to interact with the *Transport Discovery* and other Skywire Nodes, it should have access to the local node's public and private key identity. + +The following is a proposed implementation of `TransportManager`; + +```golang +package node + +// Transport wraps a 'transport.Transport' implementation and contains +// associated/useful for the 'transport.Transport' implementation. +type Transport struct { + transport.Transport + ID uuid.UUID + // more fields ... +} + +// TransportManagerConfig configures a TransportManager. +type TransportManagerConfig struct { + PubKey cipher.PubKey // Local PubKey + SecKey cipher.SecKey // Local SecKey + DiscoveryClient client.Client // Transport discovery client + LogStore TransportLogStore // Store for transport's transfer rates +} + +// TransportManager manages Transports. +type TransportManager struct { + // Members... +} + +// NewTransportManager creates a TransportManager with the provided configuration and transport factories. +// 'factories' should be ordered by preference. +func NewTransportManager(config *TransportManagerConfig, factories ...transport.Factory) (*TransportManager, error) { /* ... */ } + +// Start starts the transport manager. +// - 'ctx' can end the transport listening operation. +func (tm *TransportManager) Serve(ctx context.Context) error { /* ... */ } + +// Observe returns channel for notifications about new Transport +// registration. Only single observer can listen for on a channel. +func (tm *TransportManager) Observe() <-chan *Transport { /* ... */ } + +// Factories returns all the factory types contained within the TransportManager. +func (tm *TransportManager) Factories() []string { /* ... */ } + +// Transport obtains a Transport via a given Transport ID. +func (tm *TransportManager) Transport(id uuid.UUID) (*Transport, bool) { /* ... */ } + +// RangeTransports ranges all Transports. +// Should return when 'action' returns a non-nil error. +func (tm *TransportManager) RangeAllTransports(action TransportAction) error { /* ... */ } + +// CreateTransport begins to attempt to establish transports to the given 'remote' node. +// This should be a non-blocking operation and any failures or future Transport disconnections +// should be dealt with with retries (under a given time interval). +// - 'remote' specifies the remote node to attempt to establish the Transports with. +// - 'tpType' is the transport type that is to be created. +// - 'public' determines whether the Transports established should be advertised to Transport Discovery. +// If a transport is not to be public, a random transport ID is assigned. +func (tm *TransportManager) CreateTransport(ctx context.Context, remote cipher.PubKey, tpType string, public bool) (*Transport, error) { /* ... */ } + +// DeleteTransport disconnects and removes the Transport of Transport ID. +func (tm *TransportManager) DeleteTransport(id uuid.UUID) error { /* ... */ } +``` + +## Transport Manager Procedures + +The transport manager is responsible for keeping track of established transports (via the `transport.Entry` and the `transport.Status` structures). The `transport.Entry` structure describes and identifies transports, while `transport.Status` keeps track of whether the transport is up or down (based on the perspective of the local node). + +If the *Transport Manager* wishes to confirm transport information, it can query the *Transport Discovery* via the `GET /transports/edge:` endpoint. Note that it is expected of the *Transport Manager* to call this endpoint on startup. + +When a transport is "closed" it is only considered "down", not "destroyed". + +The following highlights detailed startup and shutdown procedures of a *Transport Manager*; + +**Startup:** + +On startup, the `TransportManager` should call the *Transport Discovery* to ensure that it is up to date. Then it needs to attempt to establish (or re-establish) transports to the relevant remote nodes. + +When re-establishing a Transport, the `transport.Entry` used should be that also previously stored in the *Transport Discovery*. + +Once connected, the `TransportManager` should update it's *Status* of the given Transport and set `is_up` to `true`. + +The startup logic is triggered when `Start` is called. + +**Shutdown:** + +On shutdown, the first step is to update the *Transport Statuses* to "down" via the *Transport Discovery*. Then Transports to remote nodes is to be closed (with a timeout, in which after, the transport in question is forcefully closed). + +## Logging + +A *Transport Manager* is responsible for logging incoming and outgoing communication for each transport. Initially, only the total incoming and outgoing bandwidth (in bytes) is to be logged per transport. + +```golang +// TransportLogEntry represents a logging entry for a given Transport. +// The entry is updated every time a packet is received or sent. +type TransportLogEntry struct { + ReceivedBytes big.Int // Total received bytes. + SentBytes big.Int // Total sent bytes. +} +``` + +Logs for each transport is to be stored using `TransportLogStore`. `TransportLogStore` is to be specified within `TransportManagerConfig`. + +```golang +// TransportLogStore stores transport log entries. +type TransportLogStore interface { + Entry(id uuid.UUID) (*TransportLogEntry, error) + Record(id uuid.UUID, entry *TransportLogEntry) error +} +``` + +# Route Finder + +The *Route Finder* (or *Route Finding Service*) is responsible for finding and suggesting routes between two Skywire Nodes (identified by public keys). It is expected that an *App Node* is to use this service to find possible routes before contacting the *Setup Node*. + +In the initial version of the *Route Finder*, it should use a basic algorithm to choose and order the best routes. This algorithm should find the x amount (limited by the max routes parameter) of "fastest" routes determined by the least amount of hops needed, and order it by hops ascending. + +The implementation of *Route Finder* requires only a single REST API endpoint. + +## Graph Algorithm + +In order to explore routing we need to create a graph that represents the current skywire network, or at least the network formed by all the reachable nodes by `source node`. + +For this purpose, we use the `mark and sweep` algorithm. Such an algorithm consists of two phases. + +In the first phase, every object in the graph is explored in a `Deep First Search` order. This means that we need to explore every transport starting from route node, accessing the `transport-discovery` database each time that we need to retrieve new information. + +In the second phase, we remove nodes from the graph that have not been visited, and then mark every node as unvisited in preparation for the next iteration of the algorithm. + +An explanation and implementation of this algorithm can be found [here](https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/). + +## Routing algorithm + +Given the previous graph we can now explore it to find the best routes from every given starting node to destiny node. + +For this purpose we use a modification of `Dijkstra algorithm`. + +An implementation can be found [here](http://rosettacode.org/wiki/Dijkstra%27s_algorithm#Go). + +Route-finder modifies this algorithm by keeping track of all the nodes that reached to destination node. This allows the ability to backtrack every best route that arrives from a different node to destination node. + +## Code Structure + +The code should be in the `watercompany/skywire` repository; + +- `/cmd/route-finder/route-finder.go` is the main executable for the *Route Finder*. +- `/pkg/route-finder/api/` contains the RESTFUL API definitions. +- `/pkg/route-finder/store/` contains the definition of the `Storer` interface and it's implementations [**TODO**]. +- `/pkg/route-finder/client/` contains the client library that interacts with the *Route Finder* service's RESTFUL API. + +## Database + +The *Route Finder* only accesses the Transport database already defined in the *Transport Discovery* specification. + +## Endpoint Definitions + +All endpoint calls should include an `Accept: application/json` field in the request header, and the response header should include an `Content-Type: application/json` field. + +### GET Routes available for the defined start and end key + +Obtains the routes available for a specific start and end public key. Optionally with custom min and max hop parameters. + +Note that each transport is to be represented by the `transport.Entry` structure. + +**Request:** + +``` +GET /routes +``` + +```json +{ + "src_pk": "", + "dst_pk": "", + "min_hops": 0, + "max_hops": 0, +} +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "routes": [ + { + "transports": [ + { + "tid": "", + "edges": ["", ""], + "type": "", + "public": true, + } + ] + } + ] + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +# Route Setup Process + +1. Route paths are uni-directional. So, the whole route between 2 visors consists of forward and reverse paths. *Setup node* receives both of these paths in the routes setup request. +2. For each node along both paths *Setup node* calculates how many rules are to be applied. +3. *Setup node* connects to all the node along both paths and sends `ReserveIDs` request to reserve available rule IDs needed to setup the route. +4. *Setup node* creates rules the following way. Let's consider visor A setting up route to visor B. This way we have forward path `A->B` and reverse path `B->A`. For forward path we create `Forward` rule for visor `A`, `IntermediaryForward` rules for each node between `A` and `B`, and `Consume` rule for `B`. For reverse path we create `Forward` rule for visor `B`, `IntermediaryForward` rules for each visor between `B` and `A`, and `Consume` rule for `A`. +5. *Setup node* sends all the created `IntermediaryForward` rules to corresponding visors to be applied. +6. *Setup node* sends `Consume` and `Forward` rules to visor `B` (remote in our case). +7. *Setup node* sends `Forward` and `Consume` rules to visor `A` in response to the route setup request. + +## Loop Setup Process: + +In the code `Loop` is represented by the following structure: + +```golang +// Hop defines a route hop between 2 nodes. +type Hop struct { + From cipher.PubKey // Sender's pk + To cipher.PubKey // Receiver's pk + Transport uuid.UUID // Transport ID between sender and receiver +} + +// Route defines a route as a set of Hops. +type Route []*Hop + +// Loop defines a loop over a pair of routes. +type Loop struct { + LocalPort uint16 // Initiator's port + RemotePort uint16 // Responder's port + Forward Route // Initial route + Reverse Route // Route in opposite direction + ExpireAt time.Time // Expiration time +} + +``` + +Setup procedures: + +1. *Initiating Node* sends `CreateLoop` command to a *Setup Node*. +2. *Setup Node* contacts all nodes along a `Reverse` route using a separate dmsg channel and setups routing rules using `AddRules` command. +3. *Setup Node* performs the same operation for a `Forward` route. +4. *Setup Node* sends `ConfirmLoop` to initiator with reverse id for a `Forward` route. +5. *Setup Node* sends `ConfirmLoop` to responder with reverse id for a `Reverse` route. + +If at any point *Setup Node* will be unable to proceed it will issue requests to remove rules for the loop. + +## Loop Close Process: + +Loop can be closed by edge nodes at any time by sending `CloseLoop` command to a *Setup Node*. + +Close procedures: + +1. *Node* sends `CloseLoop` command to a *Setup Node*. +2. *Setup Node* sends `LoopClosed` to an opposite *Node*. +3. Forward routes are not removed since it's not possible to reconstruct the route. They will be removed by expiration timeout. + +# Routing Table + +A *Routing Table* (located within the `skywire/pkg/node` module) is unique for a given Node's public key. It is basically a key-value store in which the key is the *Route ID* and the value is the *Routing Rule* for the given *Route ID*. + +Initially, there will be two types of Routing Rules: *App* and *Forward*. + +- *App* rules are identified by their unique `` value of `0x00`. A packet which contains a *Route ID* that associates with a *App* rule is to be sent to a local App. +- *Forward* rules are identified by their unique `` value of `0x01`. A packet which contains a *Route ID* that associates with a *Forward* rule is to be forwarded. + +| Action | Key (Route ID) | Value (Routing Rule) | +| ------ | -------------- | -------------------- | +| *App* | ``
*4 bytes* | ``
*48 bytes* | +| *Forward* | ``
*4 bytes* | ``
*29 bytes* | + +- `` is the *Route ID* `uint32` key (represented by 4 bytes) that is used to obtain the routing rules for the Packet. +- `` contains the epoch time (8 bytes) of when the rule is to be discarded (or becomes invalid). +- `` specifies the type of Routing Rule (1 byte). Currently there are two possible routing rule types; *App* (`0x00`) and *Forward* (`0x01`). +- `` is the *Route ID* (4 bytes) that is the Route ID key for the reserve Route of the loop. +- `` identifies and classifies the loop. It contains the following sub-fields; ``. + - `` is the remote edge public key in which this route/loop is associated with. It is represented by 33 bytes. + - `` is the remote port in which this route/loop is associated with. It is represented by 2 bytes. + - `` is the local port in which this route/loop is associated with. It is represented by 2 bytes. +- `` is the *Route ID* that is to replace the `` before the Packet is to be forwarded. +- `` represents the transport which the packet is to be forwarded to. A Transport ID is 16 bytes long. + +Every time a Skywire Node receives a packet, it performs the following steps: + +1. Obtain the `` from the Packet, and uses this value to obtain a routing rule entry from the routing table. If no routing rule is found, or the routing rule has already expired (via checking the `` field), the Packet is then discarded. +2. Obtains the `` value to determine how the packet is to be dealt with. If the `` value is `0x00`, the packet is then to be sent to the local *App Server* with the Routing Rule. If the `` value is `0x01`, the packet is to be forwarded; continue on to step 3. +3. Obtain the `` from the *Routing Rule* and replace the `` from the *Route ID* field of the Packet. +4. Forward the Packet to the referenced transport specified within ``. + +The routing table is to be an interface. + +```golang +package node + +// RangeFunc is used by RangeRules to iterate over rules. +type RangeFunc func(routeID transport.RouteID, rule RoutingRule) (next bool) + +// RoutingTable represents a routing table implementation. +type RoutingTable interface { + // AddRule adds a new RoutingRules to the table and returns assigned RouteID. + AddRule(rule RoutingRule) (routeID transport.RouteID, err error) + + // SetRule sets RoutingRule for a given RouteID. + SetRule(routeID transport.RouteID, rule RoutingRule) error + + // Rule returns RoutingRule with a given RouteID. + Rule(routeID transport.RouteID) (RoutingRule, error) + + // DeleteRules removes RoutingRules with a given a RouteIDs. + DeleteRules(routeIDs ...transport.RouteID) error + + // RangeRules iterates over all rules and yields values to the rangeFunc until `next` is false. + RangeRules(rangeFunc RangeFunc) error + + // Count returns the number of RoutingRule entries stored. + Count() int +} +``` + +Potential improvement we could consider is to move ports from the rules into the data packet header, aligning this with `tcp`. By doing so we will be able to re-use intermediate forward rules across multiple loops which can drastically improve loop establishment time for complex loops. + +# Router + +The `Router` (located within the `skywire/pkg/node` module) uses the *Transport Manager* and the *Routing Table* internally, and is responsible for the handling incoming Packets (either from external nodes via transports, or internally via the `AppServer`), and also the process for setting up routes. + +Regarding the Route setup process, a router should be able to interact with multiple trusted *Setup Nodes*. + +Every transport created or accepted by the *Transport Manager* should be handled by the *Router*. All incoming packets should be cross-referenced against *Routing Table* and either be forwarded to next *Node* or to a local *App*. + +*Transport Manager* is also responsible for managing connections on local ports for node's apps. *App Node* will request new local connection from the *Router* on *App* startup. All incoming packets from the app's connections should be forwarded based on *App* rules defined in a local routing table. *Transport Manager* should also be capable of requesting new loops from a *Setup Node*. + +## Port management + +Router is responsible for port management. Port allocation algorithm should work similarly to how `tcp` manages ports: + +- Certain range of ports should be reserved and be inaccessible for general purpose apps. Default staring port is `10`. +- All allocated local ports should be unique. +- App should be able to allocate static ports that it will be accessible on for remote connections. Static port allocation is performed during `app` init. +- App should be able to dynamically allocate local port for newly created loops. +- Allocated ports should be closed on app shutdown or disconnect from the node. + +# App Server + +The `AppServer` (located within the `skywire/pkg/node` module) handles communication between local and remote Apps. It also manages and administers local Apps. It is to interact with a `Router` and identify loops via the *App* routing rule retrieved from the routing table. The *App* rule is structured as follows; + +``` +| expiry | r-type | resp-rid | remote-pk | remote-port | local-port | +| 8 bytes | 1 byte | 4 bytes | 33 bytes | 2 bytes | 2 bytes | +``` + +The *App Server* not only forwards Packets between Apps and remote entities, but it can also be instructed by the end user to send *Quit* signals to the Apps. *Apps* can also request to open *Loops* with remote/local Apps. + +Each *Loop* is identified uniquely via the local and remote public keys, paired with the local and remote ports. + +Within the `AppServerConfig` file, local ports are reserved for certain Apps. The following rules are to be opposed: +- Ports are either "reserved" or "unreserved". + - No two Apps are allowed to "reserve" the same port. + - Ports are reserved via the `AppServerConfig` file. +- Reserved ports are either "active" or "inactive". + - A port is "active" when the port is "reserved" for an App, and that App is running. + - A port is "inactive" either when the port is "unclaimed", or when the port is "claimed" but the App is not running. + +The following communication processes between a given App and the *App Server* is to exist: + +- **App requests to open loop** +- **App Server asks App whether it wishes to respond to a remotely-initiated loop** +- **App Server informs App that loop is to be closed (with given reason)** + - Reasons include: Route timeout, remotely closed, locally closed, etc. +- **App informs App Server it wishes to close a loop (with given reason)** + - Reasons include: App is shutting down, loop no-longer used, etc. +- **App Server forwards packets received from remote App** + - If local app does not exist or does not accept, the loop is to be closed, and routes destroyed. +- **App Server forwards packets received from local App** + - If the rule does not exist, or the remote app nodes not accept, the loop is to be closed and routes destroyed. + +## Loop Encryption + +Each loop is to be symmetrically encrypted with [Noise](http://noiseprotocol.org/noise.html). Specifically, we are to use the `KK` fundamental [handshake pattern](http://noiseprotocol.org/noise.html#handshake-patterns). The first noise message should be provided by initiator in the request to create a new loop, this messages will be setup to responder in a loop confirmation request. Responder should send second noise message which will be returned to initiator in a loop confirmation request. + +# Setup Node + +The *Setup Node* (located within the `skywire/pkg/node` module) uses *Transport Manager* internally and is responsible for propagation of routing rules to nodes along the *Route*. *Setup Node* should be only addressed by a public key and should work over dmsg transport using multiple channels. Each channel can be used to issue route setup commands by initiator. For *Loop* setup requests *Node* will be an initiator, for *Rule* setup related operations *Setup Node* will be a channel initiator. *Setup Node* is only responsible for handling *Foundational Packets* and doesn't perform any forwarding functions. + +# App Node + +An App Node is a node that is part of the Skywire network and is represented by a key pair (using the `secp256k1` curve). It handles Transports to remote nodes, sets up routes and loops (via Routing Rules and interaction with the *Setup Node*), and manages Apps. + +Each App is it's own executable that communicates with an *App Node* using a pair of *POSIX* pipes. Piped connection is setup on *App* startup and inherited by a forked *App* process using file descriptor `3` and `4`. Setup process for a forked *App* is handled by the `app` package. + +``` + [Skywire Node] + / | \ + / | \ +[App 1] [App 2] [App 3] +``` + +## Communication reliability + +Since dmsg and loop communication is dependent on intermediate servers we have to provide acknowledgment mechanism between edge nodes. This will be done via wrapper `io.ReadWriter` (`AckReadWriter`) that will be able to augment existing communication channels with `ack` packets. `Write` calls on `AckReadWrite` should block until corresponding `ack` packet is received. + +`ack` logic should be working in `tcp`-like way: all pending `ack` packets should be sent with subsequent write on the opposite edge. If no write is happened within a certain interval then all pending `ack` packets should be flushed. Outstanding `ack` packets should also be flushed on `Close` call. + +`AckReadWriter` should be able to send and receive 2 types of packets: `payload`(`0x0`) and `ack` (`0x1`): + +Format of a `payload` packet: + +``` +| Packet Type | Packet ID | Payload | +| 0x0 | 1 byte | ~ | +``` + +Format of an `ack` packet: + +``` +| Packet Type | Packet ID | SHA256 | +| 0x1 | 1 byte | 32 bytes | +``` + +`AckReadWriter` should be able to prepend any amount of `ack` packets to a `payload` packet. Sequences without `payload` packet should also be valid. Example packet sequence: + +``` +| 0x1 | 0x0 | hash | 0x1 | 0x1 | hash | 0x0 | 0x2 | payload | +``` + +This packet sequence will acknowledge received packets with ids `0` and `1` and will send packet with id `2`. + +Upon reading `ack` packets receiver should validate received hash for each packet. + +## App Programming Interface + +*App* programming interface (located within the `skywire/pkg/app` module) should expose methods for *Apps* to connect to a piped connection, perform handshake and exchange data with remote nodes. + +*App* interface should expose following methods: + +```golang +// Addr implements net.Addr for App connections. +type Addr struct { + PubKey transport.PubKey + Port uint16 +} + +// LoopAddr stores addressing parameters of a loop package. +type LoopAddr struct { + Port uint16 + Remote Addr +} + +// Packet represents message exchanged between App and Node. +type Packet struct { + Addr *LoopAddr + Payload []byte +} + +// Config defines configuration parameters for an App +type Config struct { + AppName string + AppVersion string + ProtocolVersion string +} + +// Setup sets up an app using default pair of pipes and performs handshake. +func Setup(config *Config) (*App, error) {} + +// Accept awaits for incoming loop confirmation request from a Node and +// returns net.Conn for a received loop. +func (app *App) Accept() (net.Conn, error) {} + +// Dial sends create loop request to a Node and returns net.Conn for created loop. +func (app *App) Dial(raddr *Addr) (net.Conn, error) {} + +// Addr returns empty Addr, implements net.Listener. +func (app *App) Addr() net.Addr {} + +// Close implements io.Closer for an App. +func (app *App) Close() error {} +``` + +## App to Node Communication protocol + +Communication between *Node* and an *App* happens over the piped connection using binary multiplexed protocol. + +The following is the expected format of a App Packet: + +``` +| Packet Len | Type | Message ID | JSON Body | +| 2 bytes | 1 byte | 1 byte | ~ | +``` + +- ***Packet Len*** specifies the total packet length in bytes (exclusive of the *Packet Len* field). +- ***Type*** specifies the *App Packet Type*. +- ***Message ID*** specifies multiplexing ID of a message, response for this message should contain the same ID. +- ***JSON Body*** is the packet body (in JSON format) that is unique depending on the packet type. + +**App Packet Types Summary:** + +| Type | Name | +| ---- | ---- | +| 0x00 | `Init` | +| 0x01 | `CreateLoop` | +| 0x02 | `ConfirmLoop` | +| 0x03 | `Send` | +| 0x04 | `Close` | +| 0xfe | `ResponseFailure` | +| 0xff | `ResponseSuccess` | + +### `0x00 Init` + +Sent by an *App* to a *Node*. This packet is used to handshake connection between an *App* and a *Node*. *Node* will typically check if app is allowed by the config file and which port should be statically allocated it. + +**JSON Body:** + +```json +{ + "app-name": "foo", + "app-version": "0.0.1", + "protocol-version": "0.0.1" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` without body. + +### `0x01 CreateLoop` + +Sent by an *App* to a *Node*. This packet is used to open new *Loop* to a remote *Node*. + +**JSON Body:** + +```json +{ + "pk": "", + "port": +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + { + "pk": "", + "port": + } + ``` + +### `0x02 ConfirmLoop` + +Sent by a *Node* to an *App* to notify about request to open new *Loop* from a remote *Node* + +**JSON Body:** + +```json +[ + { + "pk": "", + "port": + }, + { + "pk": "", + "port": + } +] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +### `0x03 Send` + +Sent by a *Node* and an *App*. This message is used to exchange messages through a previously established *Loop*. + +**JSON Body:** + +```json +{ + "addr": { + "port": , + "remote": { + "pk": "", + "port": + } + }, + "payload": "" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +### `0x04 Close` + +Sent by a *Node* and an *App*. *App* uses this message to notify about closed *Loop*. *Node* sends this message after remote node is requested to close established *Loop*. + +**JSON Body:** + +```json +{ + "port": , + "remote": { + "pk": "", + "port": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +## App Node Configuration + +The following is the JSON representation of a Skywire configuration. + +```json +{ + "version": "1.0", + "node": { + "static_public_key": "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7", + "static_secret_key": "42bca4df2f3189b28872d40e6c61aacd5e85b8e91f8fea65780af27c142419e5" + }, + "dmsg": { + "discovery_addresses": ["http://localhost:9090"], + "server_count": 1 + }, + "apps": [ + { + "app": "helloworld", + "version": "1.0", + "auto_start": true, + "port": 10, + "args": [] + } + ], + "transport_discovery": "http://localhost:9091", + "setup_nodes": ["02603d53d49b6575a0b8cee05b70dd23c86e42cd6cba99af769d61a6196ea2bcb1"], + "trusted_nodes": ["0348c941c5015a05c455ff238af2e57fb8f914c399aab604e9abb5b32b91a4c1fe"], + "dmsg_path": "./dmsg", + "apps_path": "./apps", + "local_path": "./local", + "log_level": "info", + "interfaces": { + "rpc": ":3436" + } +} +``` + +- `"version"` represents the version of the Skywire Node (and also the configuration format version). + +- `"node"` includes the public/private keys that identify the node. + +- `"dmsg"` configures the dmsg client instance included within the Skywire Node. + - When `"public"` is set, the Dmsg Client Instance will advertise itself to discovery. + - `"discovery_addresses"` specifies the Dmsg Discovery Services that the Skywire Node is to try. + - `"server_count"` specifies the number of servers to ensure connection with on first startup. + +- `"apps"` lists all available Skywire Apps. These configurations include; the App's name, whether the specified App should auto-start, and the ports that is reserved for the App. If these are not defined for an App, the App will not auto-start, nor have ports reserved for the App. + - If `"version"` is not specified, the highest stable version will be selected. + +- `"node_path"` stores logs, routing tables, and any data that the node may use. + +- `"dmsg_path"` holds the path which the Dmsg Client Instance can use to store cache or additional configurations. + +- `"apps_path"` holds all the app executables. App executable files should be named with no spaces or weird characters (TODO: define properly). They should also be appended with the semantic version of the App after a dot; `{app_name}.v{semantic_version}`. + +- `"local_path"` contains the working directories of the apps. An app named `chat` of version `v1.0` should have a working directory within `{root_dir}/{local_path}/chat/v1.0/`. The contents of the App's working directory is specified by the App. + +## App Node RPC Interface + +The App node should attempt to connect to the assigned *Manager Node* on startup. The connection is to be encrypted via Noise (KK handshake pattern) so that the nodes can identify one another. + +For the *App Node* to connect to the *Manager Node*, it needs the public key and tcp address of the *Manager Node* in it's configuration. + +After connection has been established, the *App Node* becomes the RPC Server and the *Manager Node* becomes the RPC client that can execute commands on the *App Node* (the RPC Server). + +Additionally, the App Node should listen on a port so that a local command-line executable (`skywire-cli`) can interact with it. This local port should hence, only accept connections from localhost. + +### Commands + +The following sub-commands should be supported. Note that command-line actions are listed below, but they should be served via RESTFUL interfaces. + +**General:** + +- **`skywire cli visor info`** obtains a summary of the current state of the ~~App Node~~ running skywire visor. + +**App Management:** + +- **`skywire cli visor app ls`** lists applications and applications stats (running/not running) (auto-start/non-auto-start) (local/remote ports). There should be flags for filtering (to be defined later). + +- **`skywire cli visor app start `** starts a Skywire app. + +- **`skywire cli visor app stop `** stops a Skywire app if running. + +- **`add-autostart-app [--start-now]`** adds a Skywire app to auto-start. After calling this command, the actual app does not actually start unless `--start-now` is set - **NOT IMPLEMENTED** - set autostart behavior with `skywire cli config gen` or `skywire cli config update` or directly in the config + +- **`rm-autostart-app [--stop-now]`** removes an app from auto-starting. After calling this command, the actual app does not stop running unless `--stop-now` is set - **NOT IMPLEMENTED** - set autostart behavior with `skywire cli config gen` or `skywire cli config update` or directly in the config + +**Dmsg Management:** + +- **`messaging list-discoveries`** lists saved discoveries and their statuses - **NOT IMPLEMENTED** + +- **`messaging add-discovery `** connects to and saves a discovery - **NOT IMPLEMENTED** + +- **`messaging rm-discovery `** disconnects from and removes a discovery - **NOT IMPLEMENTED** + +- **`skywire cli mdisc servers`** lists dmsg servers (just [available_servers](https://dmsgd.skywire.skycoin.com/dmsg-discovery/available_servers) not [all_servers](https://dmsgd.skywire.skycoin.com/dmsg-discovery/all_servers)) from dmsg discovery ~~and their statuses (connected/disconnected) (auto-connect/non-auto-connect)~~. + +- **`messaging connect-server (|--auto)`** connects to a dmsg server for this session (does not save server for auto-connect). If `--auto` is set, the transport discovery is queried for a random available dmsg server - **NOT IMPLEMENTED** + +- **`messaging disconnect-server `** disconnects from a dmsg server for this session (does not affect auto-connect settings) - **NOT IMPLEMENTED** + +- **`messaging add-autoconnect-server [--connect-now]`** adds a dmsg server to auto-connect. This command does not connect to the specified dmsg server unless `--connect-now` is set - **NOT IMPLEMENTED** + +- **`messaging rm-autoconnect-server [--disconnect-now]`** removes a dmsg server from auto-connecting. This command does not disconnect from the specified dmsg server unless `--disconnect-now` is set - **NOT IMPLEMENTED** + +**Transport Management:** + +- **`skywire cli tp --types `** lists all transports by type used by the visor (represented as strings). +- **`skywire cli tp`** lists all transports associated with the visor. +- **`skywire cli tp add -t `** adds a transport from the local visor to the remote public key of a given type. +- **`skywire cli tp rm [--tid=|--remote-pk=|--all]`** removes a transport; either for a given transport ID, or all transports connected to a remote node (identified via the remote node's public key). + +**Routes Management:** + +- **`skywire cli route`** lists all routing rules. A route ID range filter can be specified. +- **`skywire cli route add`** add routing rules +- **`skywire cli route rm`** removes routing rules; either via a list of route ID keys, or via a range of route ID keys (note that routing rules are identified via their `` key). This action may consequently destroy loops, and may cause the *Setup Node* to request destruction of more routing rules. +- **`skywire cli route find`** Query the route finder to find routes between two keys. + +**Loops Management:** + +- **`list-loops [--local-port=] [--remote-addr=[:]]`** lists all loops. A local port filter can be specified, where the returned loops will only be of the specified local port (there is an equivalent remote address filter) - **NOT IMPLEMENTED** +- **`add-loop --local-port= --remote-addr=: [--setup-node=]`** attempts to create a loop with the assigned setup node. The setup node is automatically chosen if not specified - **NOT IMPLEMENTED** + +## Ports Management + +Within the `AppsConfig` file, ports are reserved for certain Apps. The following rules are to be opposed: +- Ports are either "reserved" or "unreserved". + - No two Apps are allowed to "reserve" the same port. + - Ports are reserved via the `AppsConfig` file. +- Reserved ports are either "active" or "inactive". + - A port is "active" when the port is "reserved" for an App, and that App is running. + - A port is "inactive" either when the port is "unclaimed", or when the port is "claimed" but the App is not running. + +## App Example + +Simple `ping-pong` client and server apps can be implemented in such way: + +Server: + +```golang +package server + +import ( + "log" + + "github.com/watercompany/skywire/pkg/app" +) + +func main() { + // Open connection with Node + helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-server", AppVersion: "1.0", ProtocolVersion: "0.0.1"}) + if err != nil { + log.Fatal("Setup failure: ", err) + } + defer helloworldApp.Close() + + log.Println("listening for incoming connections") + // Start listening loop + for { + // Wait for new Loop + conn, err := helloworldApp.Accept() + if err != nil { + log.Fatal("Failed to accept conn: ", err) + } + + log.Println("got new connection from:", conn.RemoteAddr()) + // Handle incoming connection + go func() { + buf := make([]byte, 4) + if _, err := conn.Read(buf); err != nil { + log.Println("Failed to read remote data: ", err) + } + + log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf)) + if _, err := conn.Write([]byte("pong")); err != nil { + log.Println("Failed to write to a remote node: ", err) + } + }() + } +} +``` + +Client: + +```golang +package server + +import ( + "log" + "os" + + "github.com/watercompany/skywire/pkg/app" + "github.com/watercompany/skywire/pkg/cipher" +) + +func main() { + // Open connection with Node + helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-client", AppVersion: "1.0", ProtocolVersion: "0.0.1"}) + if err != nil { + log.Fatal("Setup failure: ", err) + } + defer helloworldApp.Close() + + // Read remote PK from stdin + remotePK := cipher.PubKey{} + if err := remotePK.UnmarshalText([]byte(os.Args[1])); err != nil { + log.Fatal("Failed to construct PubKey: ", err, os.Args[1]) + } + + // Dial to remote Node + conn, err := helloworldApp.Dial(&app.Addr{PubKey: remotePK, Port: 10}) + if err != nil { + log.Fatal("Failed to open remote conn: ", err) + } + + // Send payload + if _, err := conn.Write([]byte("ping")); err != nil { + log.Fatal("Failed to write to a remote node: ", err) + } + + // Receive payload + buf := make([]byte, 4) + if _, err = conn.Read(buf); err != nil { + log.Fatal("Failed to read remote data: ", err) + } + + log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf)) +} +``` + +# Manager Node + +__Note: this section is using old terminology to refer to a visor running with hypervisor UI and remote visors which are configured to connect to that hypervisor__ + +The *Manager Node* is responsible for managing *App Nodes* and is identified via it's public key and TCP address. + +The *App Node* is responsible for including a trusted *Manager Node* in it's configuration file, and attempt to connect to it on startup. The connection is authenticated and encrypted via the Noise protocol where the `XK` handshake pattern is used with the *App Node* being the initiator. + +After the connection is successfully established between an *App Node* and a *Manager Node*, the *App Node* acts as the RPC server while the *Manager Node* is the RPC client. In this way, the *Manager Node* can execute commands on the *App Node*. + +The *Manager Node* serves a REST API which the end user can interact with. To access the API, the user is required to log in via a username and password. + +The *Manager Node* should be implemented in `/pkg/manager-node`. + +## Manager Node REST API (and User Interface) + +- Login/logout. +- Change password. +- List connected *App Nodes*, each App Node should contain the following summary; Public key, local address, number of transports established, number of apps running, Uptime. +- The user can click into a listed *App Node* and perform node-specific actions; specifically, the [RPC commands as specified above.](#app-node-rpc-interface) + +# Glossary + +**Skywire Transport:** + +Identified via a Transport ID, a Skywire Transport is represented as an interface and can be implemented as different transport types. It is a bi-directional line of communication between two Skywire nodes (transport edges) and constructs a single hop of a route. + +It is the responsibility of a Skywire Factory to generate Skywire Transports. + +**Skywire Transport ID:** + +A Skywire Transport is an uint16 integer that refers to a Skywire Transport and identifies it in the transport discovery. Skywire Transport IDs are assigned uniquely by and for a transport edge. Therefore, the same transport can be referenced by two different Transport IDs, assigned by the 2 edges. + +**Transport Edge:** + +A transport edge is one of the two Skywire nodes that make up a transport. It is represented by a unique public key that identifies the Skywire node. + +**Transport Type:** + +A Transport Type (represented by a string) refers to the underlying implementation of a Transport Factory or a Transport. A Transport Factory of a certain type can only construct Transports of that type. + +Initially, we will have two transport types; dmsg and native. The dmsg transport is implemented by dmsg. The native transport is non-dependent on the current internet and in the future will be the main transport implementation for Skywire. + +**Transport Factory:** + +A transport factory is used by a Skywire node which constructs transports of a certain transport type. It is an interface that can either dial or listen for remotely initiated transports. + +**Transport Perspective:** + +A transport perspective is the assumed state of a transport (number of packets and bandwidth sent and received over that transport; whether transport is up or down). It only represents the perspective (on a transport) from a single transport edge and therefore the perspectives of two edges on the same transport might conflict. + +**Transport Discovery:** + +The transport discovery is a service that registers transports and the associated transport perspectives. It thereby provides the basis for the route finding service because it represents the public network topology. It is queried by the route finding service to discover routes. + +**Hop:** + +A hop is equivalent to a transport and is a single unit of a route. + +**Skywire Route:** + +A Skywire Route is a unidirectional network path that allows apps and services to communicate. It can be made up of one or more hops between Skywire Nodes. It is identified by route IDs, interpreted by individual Skywire Nodes via the routing table. + +**Skywire Route ID:** + +A route ID is represented by 32 bits and is the basis for a Skywire Node’s routing rules. Route IDs are changed along every hop of a route and there is a unique set of route IDs for every node. + +**Route Finder:** + +The route finder is a service that evaluates the network topology via the information of the transport discovery to provide possible routes to inquiring Skywire nodes. Currently it evaluates possible routes only on the basis of the hop metric. + + +**Routing Table:** + +The Routing Table is a key-value store that determines the action to be performed on an incoming Packet. + +Using the Routing ID (from the Packet) as the key, a Skywire Node can obtain either the next transport that the packet is to be sent over (and the new Routing ID) or whether the packet is to be consumed by the node itself. + +**Stream:** + +A stream represents a bi-directional line of communication between two Skywire Apps. A stream can use multiple Routes to establish itself. + +**Skywire App:** + +A Skywire App is an executable that interacts with the Skywire App Node via Unix pipes. Skywire apps provide services for the end user such as a proxy, ssh, chat, etc. + +**Dmsg:** + +dmsg is primitive fallback implementation of Transport and Transport Factory over the Internet. It consists of a dmsg discovery, client and server instances, where the server instance relays messages between clients. The communication between two clients is called a dmsg channel. + +**Dmsg Instance:** + +Dmsg instances (alongside the dmsg discovery) are the main components of dmsg. A dmsg instance can be either a dmsg server or dmsg client. + +**Dmsg Link:** + +A dmsg link is the direct line of communication between a server instance and a client instance. A dmsg channel between two client instances can be constructed from two dmsg links to a shared server instance. + +**Dmsg Discovery:** + +The dmsg discovery is a key value store that registers the messenger servers that a given messenger client is connected to. It allows clients to get the information necessary to establish a dmsg transport with another node by querying the public keys associated dmsg servers. + +**Dmsg Channel:** + +A Dmsg Channel represents the bi-directional connection-oriented line of communication between two client instances of dmsg. + +**Skywire Node:** + +A Skywire Node (now skywire __visor__) is the general term for nodes that make up the Skywire Network. Any entity that manages Transports and Packets is considered to be a Skywire Node. + +Examples of Skywire Nodes include; App Node (which manage Skywire Apps), Control Node (which administers App Nodes) and the Route Setup Node (which coordinates the construction of Routes with App Nodes). + +**Skywire App Node:** + +An App Node is a Skywire Node that runs, stops, monitors and sets permissions for Skywire Apps. Internally, it handles and coordinates packets incoming and outgoing between the Skywire App and the external Skywire network. + +An App Node can also forward packets to external Skywire Nodes (based on the set Routing Rules). + +**Skywire Control Nodes:** + +A Skywire Control node is similar to an app node, with the difference that it has administrative permissions for other nodes in a cluster and is being sent logs from other nodes. It should not run user-based Skywire Apps, nor should it forward Packets. + +**Skywire Route Setup Node:** + +The Route Setup Node is a Skywire Node which runs a service that allows it to set up routes for other Skywire nodes. It does this by relaying the routing rules to individual Skywire App Nodes. + +**Skywire Transsport Setup Node:** + +The Transport Setup Node is a service which is permissioned via inclusion of it's public key in the visor's config by other visors on the network that allows it to set up transports between remote visors or Skywire nodes. It does this via a special api which is exposed over dmsg to any keys which are whitelisted in the transport setup nodes array in the visor's config. diff --git a/skywire-specs/VPN/Client.md b/skywire-specs/VPN/Client.md new file mode 100644 index 000000000..b48106682 --- /dev/null +++ b/skywire-specs/VPN/Client.md @@ -0,0 +1,5 @@ +# VPN Client + +Client is mostly the same as a [server](./Server.md). What really differs server from the client is overall routing. Only outbound traffic should be going through the interface. Not sure if the inbound traffic matters. So, client reads all the outbound from the adapter and passes it to the remote VPN server through the open connection. + +Since internal Skywire traffic actually provides the tunnel functionality, we pass all the outbound traffic through the interface except for packets targeting Skywire services. To exclude, we pass IPs of our services to the client on startup via ENVs. During its work we add or remove such direct IPs via network hooks between visor and app. \ No newline at end of file diff --git a/skywire-specs/VPN/Handshake.md b/skywire-specs/VPN/Handshake.md new file mode 100644 index 000000000..23a5ea1a1 --- /dev/null +++ b/skywire-specs/VPN/Handshake.md @@ -0,0 +1,52 @@ +# Client/Server Handshake + +Before client and server start exchange the actual traffic the handshake process must take place. Client and server exchange their specific hello messages to agree on exchange details. Both messages are just JSON objects being sent over the app connection. Client sends its message first. + +As stated in the [general server description](./Server.md), we need to choose 4 different IP addresses in the same subnetwork to give these to client- and server-side TUN interfaces. These IPs must be in the same network. Interfaces of different clients must not share the same subnet. To make this process deterministic, server will be responsible for choosing these addresses for each connecting client. Obviously for the system to work these generated IPs must not clash neither with any of the IP of the local client network interfaces nor with its default network gateway. + +### Client Hello Message + +``` +type ClientHello struct { + UnavailablePrivateIPs []net.IP `json:"unavailable_private_ips"` + Passcode string `json:"passcode"` +} +``` + +Here we have only one field - IPs that server must exclude from the generation. Usually client includes in this field IPs of all its local network interfaces plus its default network gateway. Client may also want to include some of the IPs that it's going to connect to directly without VPN. + +### Server Hello Message + +``` +type ServerHello struct { + Status HandshakeStatus `json:"status"` + TUNIP net.IP `json:"tun_ip"` + TUNGateway net.IP `json:"tun_gateway"` +} +``` + +Status represents the handshake process result. May be one of the following: + +- 0 - OK +- 1 - Client message was malformed +- 2 - No free IPs left to give +- 3 - Internal server error +- 4 - Forbidden (invalid passcode) + +`TUNIP` and `TUNGateway` fields are used by the client to set up its local TUN interface + +### Server-Side IP Generation + +We need to generate 4 different IPs lying in the same network. For this we'll use the `/29` (`255.255.255.248`) mask. Server iterates over all private IP ranges: +- `192.168.0.0` - `192.168.255.255` +- `172.16.0.0` - `172.31.255.255` +- `10.0.0.0` - `10.255.255.255` + +Generation step is 8, so the IPs will be generate like: +`192.168.0.0, 192.168.0.8, 192.168.0.16, ...` + +This way the generated IP address will be the address of the subnet. Let's say we have the generated IP - `192.168.0.0`. Then server will assign the following addresses: +- `192.168.0.1` - Server-side TUN gateway +- `192.168.0.2` - Server-side TUN IP +- `192.168.0.3` - Client-side TUN gateway +- `192.168.0.4` - Client-side TUN IP \ No newline at end of file diff --git a/skywire-specs/VPN/Server.md b/skywire-specs/VPN/Server.md new file mode 100644 index 000000000..0fa5ced2d --- /dev/null +++ b/skywire-specs/VPN/Server.md @@ -0,0 +1,41 @@ +# VPN Server + +Server is responsible for listening to incoming VPN client connections over Skywire network, creating TUN/TAP adapter, setting routing up, reading packets from adapter and passing them to the remote VPN server. + +## Implementation Note + +Due to the firewall used on MacOS server cannot be implemented for the system. Windows needs to be investigated. + +## Routing + +We allocate TUN interface for each VPN client. This way we may easily distribute traffic between clients. For the system to work both client and server TUN interfaces must be in the same subnetwork. And they must have different IPs. Gateway probably may be the same, but just to be sure, we're giving different ones. For the generation process details please consult [handshake](./Handshake.md) section. + +Let's say we have server-side TUN `tun0` with IP `192.168.255.2` and gateway `192.168.255.1`. + +- Linux +``` +/sbin/ifconfig tun0 192.168.255.2 192.168.255.1 mtu 1500 netmask 255.255.255.248 up +``` + +Then we set up routing. First, we need to allow kernel pass packets from one interface to another. This is done like this: +- Linux +``` +sudo sysctl -w net.ipv4.ip_forward=1 // for IPv4 +sudo sysctl -w net.ipv6.conf.all.forwarding=1 // for IPv6 +``` + +Then we need to let the system work as NAT, so that packets flow as expected with their source IPs changed to the IP of the default interface. +``` +sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE +``` +Here `wlan0` is a default network interface in the system. May be fetched from the output of `netstat -rn` on Unix-like systems. + +For cleanup we may fetch the old value of forwarding like: +``` +sudo sysctl net.ipv4.ip_forward +sudo sysctl net.ipv6.conf.all.forwarding +``` +and then we may assign old values on cleanup. Routing rule may be removed like this: +``` +sudo iptables -t nat -D POSTROUTING -o wlan0 -j MASQUERADE +``` diff --git a/skywire-specs/VPN/Specs.md b/skywire-specs/VPN/Specs.md new file mode 100644 index 000000000..42624a701 --- /dev/null +++ b/skywire-specs/VPN/Specs.md @@ -0,0 +1,113 @@ +# VPN + +Basically VPN consists of 2 applications - [client](./Client.md) and [server](./Server.md). Both applications are made in form of a Skywire app (like skychat currently). They are run under control of a Skywire visor. + +## TUN/TAP adapter + +We use TUN/TAP to create a virtual interface for both client and server. Client and server are connected through a tunnel (SkyTunnel?). This is not a tunnel in common understanding like direct connection, apps are connected over Skywire network through visors like all other apps. + +All the system traffic is routed through created virtual network interface to the application which had created it. For the initial implementation I suggest that we go with TUN adapter which allows us to inspect and handle IP packets, so we may concentrate on the overall VPN functionality. + +### TUN/TAP creation + +To create the adapter we may use the following library. Linux is fully supported, MacOS is only for TUN adapter (initial implementation). + +https://github.com/songgao/water + +Also, these links may help: +- http://tuntaposx.sourceforge.net/ + +For Windows this can only be achieved with the installation of `WinTUN` driver and if running client with the `SYSTEM` account, apparently having administrator privileges is not enough. Library wrapper to create the actual interface we use: + +- https://golang.zx2c4.com/wireguard/tun + +The easiest way to get `WinTUN` installed is to install the `Wireguard` itself. Also acquiring `SYSTEM` rights is a plain pain, so, unfortunately I can not guide through these processes. This is not user-ready in any case. But the only thing I'm sure of is that it works, so this approach can be used as a proof of concept. + +The route setup trick is done by OpenVPN on all platforms, it's the same, and we do the same. + +- Linux/MacOS + +This one sets up the interface itself with `192.168.255.6` as interface address and `192.168.255.5` as a destination address for P2P connection: +``` +/sbin/ifconfig utun2 192.168.255.6 192.168.255.5 mtu 1500 netmask 255.255.255.255 up +``` + +Example of setting routing up: +``` +/sbin/route add -net 134.209.17.43 192.168.1.1 +/sbin/route add -net 0.0.0.0 192.168.255.5 128.0.0.1 +/sbin/route add -net 128.0.0.0 192.168.255.5 128.0.0.1 +``` + +And the corresponding cleanup: +``` +/sbin/route delete -net 134.209.17.43 192.168.1.1 +/sbin/route delete -net 0.0.0.0 192.168.255.5 +/sbin/route delete -net 128.0.0.0 192.168.255.5 +``` + +Setup: +- `134.209.17.43` is IP of one of Skywire services (we add all the IPs we need to connect directly. Like setup node, Dmsg servers, discoveries, etc). +- `192.168.1.1` is our router IP which serves as a default gateway when VPN is down. This IP can be fetched on Unix machines by: `netstat -rn | grep default | grep {DEFAULT_INTERFACE_NAME} | awk '{print $2}` +- `192.168.255.6` is a TUN interface's IP (question is how this one is being chosen. on this current run my laptop IP in the local network is 192.168.1.5. Probably OpenVPN just gets this addr from `ifconfig` and changes its 3rd octet to 255) +- `192.168.255.5` is a gateway for TUN interface (destination IP for P2P connection) (can't say for now how this one is determined, probably TUN interface's IP and plus 1 to the 4th octet) + +Basically in this example we do the following. Route all the traffic to the Skywire services through our router, like a usual connection does by default. Then we route all traffic from subnets `0.0.0.0` and `128.0.0.0` to the VPN gateway `192.168.255.5`. So, we cover all the IPv4 range of addresses with this. Netmask `128.0.0.0` should be applied to both half ranges. So first half range covers `0.0.0.0` through `127.255.255.255` and the second one covers `128.0.0.0` through `255.255.255.255`. We could use a single route `0.0.0.0/0` but this would override the default route in the system and will make cleanup more complicated. This way we will be routing all the IPv4 traffic from the system to gateway `192.168.255.5`, it will go to `192.168.255.6` by the P2P connection and we'll be reading this traffic out of TUN interface in the app. + +This command set should work for all Unix systems, the only difference is the binaries location. + +Localhost traffic shouldn't be affected by all this routing. So app/visor communication will be going on as usual. The part that bothers us is visor-to-other-services communication. All of the used services are put into visor's config. So, when visor starts apps, it's fully initialized itself. So, it may take all of external services and pass their domains/IPs to the VPN app. This way VPN app can resolve IPs and add needed routes. The problem for now is Dmsg servers and other visors that are being add to the local STCP table. These entities are being added at runtime, so we need to pass these to the VPN app and to update the routing table. Based on this link https://unix.stackexchange.com/questions/188584/which-order-is-the-route-table-analyzed-in , routing table is being consulted from the most specific rules to the least specific. We're adding highly specific routes, so it should work like a charm. App should have a mechanism to get new values from the visor on the fly. + +- Windows + +We provide just the examples of commands we use, without specific IPs, cause it is already demonstrated above. + +Setting up interface and its MTU requires 2 separate commands: + +``` +netsh interface ip set address name="${INTERFACE_NAME}" source=static addr=${IP} mask=${MASK} gateway=${GATEWAY} +netsh interface ipv4 set subinterface "${INTERFACE_NAME}" mtu=${MTU} +``` + +After we use these commands there's a lag before we can set up routes, cause interface doesn't get ready instantly (Windows, what can I say). Just be aware, that it may take several seconds (we wait for 10 in our code just to be sure). + +Setting and removing routes: +``` +route add ${IP} mask ${MASK} ${GATEWAY} +route delete ${IP} mask ${MASK} ${GATEWAY} +``` + +#### Cleanup + +Regardless of other cleanup routines that need to be run on app shutdown, I guess all the possible interruption signals should be caught so we could at least remove the routes and let the system network stack work as usual not to ruin UX. + +### MTU + +MTU setup is not yet clear. I see that my OpenVPN instance uses 1500 which is an Ethernet MTU. Is it fixed for all hardware configurations possible? We'll have it fixed for now. These links may be useful: +- https://community.spiceworks.com/topic/217130-mtu-issues-in-vpn-connections +- https://www.sonassi.com/help/troubleshooting/setting-correct-mtu-for-openvpn + +## Configuration + +Both client and server can be configured like any other VPN app. + +Server flags: +- `--pk` - server pub key; +- `--sk` - server secret key; +- `--passcode` - password for the client to authenticate; +- `--secure` - by default client can access machines in the server's local network (SSH in, for example). Some people may use this as a feature, while others consider this a security breach. So, setting this flag forbids access to the local network. + +Client flags: +- `--srv` - server's pub key; +- `--pk` - client's pub key; +- `--sk` - client's secret key; +- `--passcode` - password to authenticate; +- `--killswitch` - If VPN tunnel goes down and client tries to reconnect. By default during this process direct Internet access gets restored. If we set this flag, there won't be any direct Internet access, user will wait till VPN tunnel is up again. + +## Encryption + +We rely on underlying Skywire transports for encryption. + +## Authentication + +Authentication is implied by the Skywire protocol itself. No further actions needed. \ No newline at end of file diff --git a/skywire-specs/img/routing-table-diagram.png b/skywire-specs/img/routing-table-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..633e41268b68858f517f8d4bfb6beba793428747 GIT binary patch literal 33897 zcmeFYRajh2(=H4N1WAD4!3pl}P6+P7J-9Q#K=8qWySsaEcSvvga2?J?77y=>a|*{x~san?wT+aC22HNLR2_7I5b(A_aETko)5v&9mp?WzlA6v zGH`IHq}CD=DzXw1_x)C69vx#>d)wT!4#c{v4oy+6p}TV zSON&h8b60NQ7h;uC~}e8p5_10h0{@gkMlCZ@7XW3I%K+u{s6jC59~$HUmei#yYZXx zhcRB@6Eve0ntAhFEPSi5nC9CDII0Uy1aVT9NR185S1X)ia4zI1sb6Dz6_s7lkxvn2 z?nNGj*J9vggx0umY#%NjebKB9T?Fvq=-@VR3I&+GqdODA0m)cWs;tA zh;jWef4^%Minu^uMuc!y>qPL%(hR?Vz|gyDk|Q`RjQ%CuS(>&kg!$o6?)Af5P9w>C zxSux^aitd%=@ZZ3c4y*FDDLYAh@Xj3;QA4HwWbAB6s5a+!M}JNS1Q=T>Vi%9nN-a5JJ;n4T|ZSeg5qxsHRyJ)>OWd8AfX4(_tZKP zX1?6*ZQp=(CxX)4qxEC?l#S`LQ4f&Lk0Ni6&NcCEc#uiKy((tun?$%qM2%1e>Djz= zRgzM}8H2>v+<~{D1NmiEFP2{0hx~dUlH0n+b&2P~-iEd6=O78qJ;@*ZUcbls233l{ z)UUSR-B{DugncpfnF2qA49Hc0c4*Ce7gq=tEc42Bm-8Tnl;gKO#J0fsjSR zjHqNV8&H}Me4oYC_^Z6*liDL+d8_amFKp^P&1Ci)1qE^kTFuuPVe_FeVE|dG_!q;$ zI5Oe1_LLg2N(m0J12Sa89A-Wo86ClyvTKS}v~F=^vFjh`a7eDc*smnIVy^`&Sr;@Re7m%+?04Fh}2r+s{J4=O=7g6>nZ_3!meTi=) z=VsuhRZQYZ`jw)T%#tkf)vGd$ns!=gHiE7H}QD?-lPq)LyEZGSZW-dxn=(o+uBmd_H%Pmr(37;WIbO zSLry&I2anruE~n%cBynpg0w;6{&_!&V@qqa@)S41D0Eeg*j3o|&T2Nf`w6Ji-R8{3 z+$Lq#kj;!>Jd#QZu^@!KysF~3EW6CGe66~x^xF*blm3$8+UeUhEx9at zH#F9b)d-v3m1pG{V@}C1`%#EZ1CeOn6*-taw~c znoLS0+$O}HEt2geEZwQz(P9{7Sid&6meJ|uJtV^D!{C#Bb8v_1EqMP?6wvJhiqW#F z{!wjpf%Itjcz2(J!1jFV8R%8tix&u*h$0vqXiGRV?eT5S4qzYgDF~vdlW4=Q z_R*7G8DKBH5g>yQY7)}un|Ih;d|BQI-#FVi!fXls9=e4IEML_;F%qsFa8~@59@hwN zsp+RQ9B+CKUu9ldQnJSx$I()VI^FZTX13B*mmZYruw;1o-NoWbJFH5IL zDGw|RCJrd{FAW|?WJLob8DlcSEC;m)x|L29N@Y`(FO@5laj1i+8>zMuC=*yu&9B4m zvvFnibZJvQk0$NX>?NbKV26b=l&_rBwBf&^q1cO=mpS}EVDsp65AM*mfqtoIp$gRA|*amw?bFm#*8y< zd9F>&Rf-FwYa;I7?%uhD7!2N!MS&_#)K4nf=)7vTvCcj`Qcv!zi~OV|3B z?yKmltY7dM{?Xrd0#kyCL#kF>JG~yV=R22s8H16e-$@a7Tg zh-nfY5MuGBxGKCaJD+aLxdJ(0>Y}hlw?)rgEV z2EFt-zw5v0!9XP=6sZ&W#qyb1fyKhN$LGs+1+;Z5Vj^WQ-z<(UiSmbc<1;#{jnp@A zYctQqP>}ot{H{Z0<+G1?W1c_sAKfDvp1T_LqWW2wA!D^?u)rx%mer$8N`0^+2+V@B zb9j_~#M@_wSjg*|!J8yn^$qYM`#BLucB|jY$o) zmO1(<>b=wrFP_+QF+hhG4>K519A$KXaBz6kzrXOZAE-{@;NY{Y)wNx;6%_U~#f_5%;B3jt&d<-! z%ErOU!NCkG!3^|pa4~jgb^ucT*~s7Ryf+7$I$JxsSOXl$f46IF0&sN^rl9!U(LbL* z$7$|v{Xac90RJKjMj-3&99DJ~Hr9XIhE)~%{Z>H5+TGkv`@OZjxdRZ^hX_9ppU@xW z|CRGUJ^n{co&TxH&iB7+{zuMVHHBDz6Yw7d{mHFA-on@=f-1!N59>uxJ5mEuVY5MO z{a#rec7OJJYG5Bj*q83FJ1ot&=H)?T2fKffeJ`%=4*xR?se?rFYLM3D9WBDM&se&p zQ}6aVX}Kh%-`BpRz+=L}Revw3j>8lh)J-nwDT~|*jfcw%#)|)r@>xZq_36T3-Ogh7 zclqM;{U27h7jq8W7hfNzstj_e?%xGKjeN5kNK~Qk{nT|+8;U8@4v4%@bp8TX#YJh z*igSB%ZSg}{@0lPc$5-DtuuW7?>;cuo_&bDBK&tBp?;V!Qs)0n|Nl=?_S6tWZucmm z)xHm-2A=yx4$Z5dfJWCn_$xve;OUa{+`jO?>AF;cjiG&`H4`|_f6Uu`qeysR^mvZn zd^SK-a<`N!jM4#1IcyKb9d|<4p3 zsX@)kPhaj!AFs!Sq55v$#gU5OCxk8(p#7{M_SJbQAOcJGy)!f+=bkFcEXU`U(;%sr zUE6wi)CA~P{c)T$scb$WmsM>0&$`i>;Yrk*aEU)AGuQeVvsSD!{|*lCr=JU4#zpS3rYkK@$9L;bAorn%J!w&AJ%?6vh_=yEnvh4Go@JKn%H{rag4s#O#rW|- zdG)v%JSl7EM!{%VT(4?F+lL~dTW|Z$ZO5NiwTL?(JzC?UN;@_ zji%Z?pk?C)bkyjPqcb?_B}>a!=C-~3G}EYKp}U{;!@Qf-M)r-fE7;0H8*X<$t4Cs% zNLP17pFG^=bS#Vfp{s7|+YfRWmw8b(JFj(gtnz~5Z~X*}=MCIkN15ssnjxn-PxqD_ z3@3o|q-yW}hpTGet9Is_&i&$ym8NBfi~-lnN$GZ@`%V1)E}p+uYVII%qAt^%BfXBr zl&G3J@Z|J-lr<&U2z0|mJimY}=CIJ^@?8pTTIlL*NqS|LxIz;&!@Y2*S+fp;|YLWkGF@M)*i>LyEY(np$w1RtY$N`mZ=aj zP-+5q8V-_D@|$D#W(&EPmd4s+x79@H={=5`t=*ZM4G6E z&{oTA4;t)l`XSfBNg`I28&$m zwfiW9+dq@=gizLTuS;j!eY#^~S??&J-Y;*bY)UTHdbckbs# z*_plRxZaEqPVPtr)}z+>(h%9zhz~0~e|g~wW*;LV|-pl!udaMR0QSK^{%x%rXavw?bT6}*_4^Roc+Sk}P)QEj>Gq;#n zO^*3QKQyloR4mS{p=Vf4rj=y+OWieMS+#~;<%N)@^C{QL31KP}S2i!7&~$@w>rwfH zg9*y7h})@?wJC(>@OghN14NdedPx}-AohLAK9M3$^Bq>f5x5ktK6wc$}*UBVsk4-lqDiQ?#rx}Q701GLv~SU{5VF;r?!i5=h_g2p#k;0`JK_7N*7j@3R(>u| z<(kxa%Z74DXzp$sgfH8QPIvBg*> zLC3fSG;VQU76^-XzJm2XmKlB+lh45gRGn9<+=;!Zq)TJ{3~uJOTc}Y;tn@q~A`q`C zaX`nm2gz`F+>X0-VQ0@Z{k3T}WQpl1R>Z_oEyC)z@9`|!7L09lY-(jN4nY}1GiNTj$Msm zvWc1Xly&&$FA8_hAt}b1fC6f2;96p9v@O2A<2Nke0ugs-^+4-fFug}%Z(ru1`P7j9 z*ZkIvR8TN+*X?XImSPUBfK$(XO;mgwtwIWNFJ79~h5lHtm&5M!`HQu5(_DSY*7Sh; z>#Z2esTvsjcc~lANDDYOz;cwl`fgn%D=ItEK08Q2$6mL{2bDcDlnLL5LnCFS3xjk? zc?hxj%*}2NnW16Rm`6Q=Y;{5XR0UN$H0A61 z%XC&;;qQ#qjcfbyM%BAK#`WLWhvITcl^n(`&`)CcUibElmeV_I1Q{1Zc1*=a?=*Km z*l&uB)YILr0w;ulBvRbv@i^PRZ^fgr94Zk8I7_>IBS-G6D`^VKi+16DdUO9 z;GiQGp1dIL_Ay9#7$*NZb0}vQ&^zOs$YhKi3}z$5&+W z)TRY~EC^)7qlv(^sVFQSbK!c0x9bdz%;$Dl4b@p_ss*+=q^NA(n5xq%+#R={WEVHj zMcHJ+^m^+_C?+Mxe*Re|t-CeGbc~6CS!b-94?xJ`BAU}<$s~e#BtFdAH}!<#;s9nE zuwzHt+N~L*j7Ho*GOB;BNa7nLwrc&woz#(-V~yA{M(XtPD&uPT?3=kv*haj4!EcrC7GbhG;vV3ZS&1yNl7(;oJT5o=}~a=%

lYioD$li`HdszCP!H3H^gp z4u4`TJr|HJqA-0EQZ0BjqsF+U=F+@zlZZCB{}Hrfitk6L&W2m? z!0DC}5!OJyLu8`o>-P}e(E#H%l3;Q1mqDu97hf|_db#uZqH;SLBYIEIcqjvn2^Xrk zbl)&KL>7DOs^6P5Md*0geHs|Dp^N+w0yS{o2uhIBdlP)*4r=t68%MC#6i25lpOwXz zrw+27;PiP~UVltRsegLB3u*q+Mgok^%Hz%_=d|QeaHF;jbXV`|_^zo41fa)4nb||; zU!q;RgJ6~w_lA?pj-bZ*J<^H~bd_M-CHM0b3t}tDNf}Jql$++x3osFH!WpYlT`8A6 zceC6^ii)rJunSW@cB-_-}$j&T8U9@PIlr11Z`Jv{>|=u z*j6x3d=|?c-A?t_f$O^(LYsj&I%B6}MqG;xR*cY=V(N|DnbAsUmWYoh%>iYM5Qo~$ z<5`Ta!h~`L4>RZOsQ0`lwL^s|D5Oq>s&*64nSM#?Ogiu7PGN%5$o8SF(Veclz-sh- zdSp`6C&rXk;{)%$Xs?W|8r)^myhmF`rRiE2u+69e*C zAP8rU>_k72f<4Om!?Aj7EtA0Yk3e~sm~uem*R0!Y&{(Rz?7F_c zUQY!w;%q<15Mt}Ci>H2n?>!DXgXT1d+(mr&eF}N?T-28@`-{rHUVblaW{CcN8nB8M zXZurU32c^q&{vDdPudQv>K(A`e4y>?V;!S;0bnfPhF99|kMGjB%BC<;{B;H2T?@Tl zcs5kxzy4b}RAk6PVV2j$D_a4$|Gn-9*u!&TMmZGDI+pTXrQrjf9TCy-A>2;qJgQHx z*BcyO1gpdd|61T;*}Mb}pXwi+h?^E-dh8daO!0Z^!*rvev3e2?uN0!A6l7}y>a~7n z=%CN!iyds*S|Sv-yHr=GL020PNxLZ&Hk~!tn*4%gK4>38Qg*d?8y(eg**~~>$x}|g zm6kr9#C8OhWx-PD>8aflHMw_brsTkUTW}%s)#90J`enUm97ob&VwSaVfIe74oXxL&zk!d z_->^lfVj%uyBCeuY!lKRrp0+W9EQ6swdS??#lmEDq%G~P-&`)c8?)zc4^J-1Zv|0| zFOt}cA=*(<7n9<6wy}lTLa$bQt>*f~VS>vuWIZGLYr!k*RWw#KCxneG;%zD4Xt99vRjOjtq>a@%j%AYq8Fxd>`0SEQ#4FKH%;DPj6i8p&-o zT<}zGW|*~0K*$)PvT3PS9S~U{oP!4_IowO#7uU&M-xPyOHuO# zns$y*lohwI_cSe-L=;(5kJTpNYz{D%PhfPvWNuIq&?Q^T@TfE9gRq43AiKJYOmK1* zOdG#xvQ>?ZNPKs^vDN$KBg~^>96x2xce@*&(BxT2xT}pqDeQsDal&+Hd3Skhgae@v z-$xOIZ-JDRWHX?y?M8=C%50+{NoBw6P6C&k+C~eFhQLfkUTmXIR1VwTfjI_zLwDm~ z&e1;h5CH|ZQgZATk`sEUD(nREfnGB*Y$#SU~Ge za-G+AdR+PPwL2SB=kh~{J34Ox6zeZf+8+Hgd#%`EIysC2fN{aIixF)${Fl1|#)QNf z!zN8*LIz~`z^Dt)xiCR}?9>5HpznighFzVk_g&jB=z?hsOya<{mv_i8Yxi@+uGz*S zk6||Iu1u{-58*VZAuT#Uo8WD>G^s})E%lALL&fe7-~DgW{(bieMc#XTr@Qxn>;U2n z+^yG9lU`E?S^Q4ZE${=O$ZOu6r>{(3)S8_1;yq|?WtNaX-Awqh81sy+qtIrmnD{6W zOWz3H840k3LxK~sVGasfAlkhJZJ3~j5IzVdguQ;zRcu1QI7@J9+@^6@iM#NTC$1Sq z#IBsFgD1>oLAy3Z=DC+w*O)A;R`J`sp{`^Q%~fHT)de+3h|>tC-iMqpEb|@qPzsNp zsRH_U7Xgg{3c8!Bdd2kLG4bWyI-pKS_Kb%X0@V8}X#5cVqR5LfpTAkk@=Q3NJb^$V zvllh)sN*KnNrJc^&)f#)|1($W4h?Lrki=pw!CLDcdhVjCe|EO8LeT$3<_o=peh4;m zhjEDY3ZVO8B)>0CMd-r-H=V2TZa_>(t6^=&Y0pb@G#s+ECk|GDPj$&9OyRlh9{uhe)z)1rl}MC^F%sb{j6=jk`3=|kJa(gA*Dar6_NhA8~gd~0|SgOY4d z8=41q>+G!UHLsJgZB{dl;SRzVOh~xh^+HC;m3h6iBGM~SbVIezi`k+;g2Znj+JEpV zC~O3SE)nYl{J7ZH4Leh?AQE&xujN+GWa+5pw5JwYBSGaL8HIsm|A$@{c3tx)rh4LFi^_V1v~YU14Oe= zGLd5AHm`LFw?Q-9C+WJ;#*ah;@~ogZb+wgySEqtBmwf%co>EI8Bo>8;b&Ihj$3m7! zS{`2L{OPEV4z|l6i5qyEU0K{h$w5`O=LHMYqDx=M1ZQm?VER^NJCOz~(P0n;q(tR- zrTtiv4%-hO2{fa|0o9*!vW3VzgP{}Q>vE7;k{CzTyxMy;_XgCjvkU&Va9&OSwLqb%)-Y>ictn{mRMFzv)_)7oJ3MheS2{klI*HTySXxWTj$yEJ%}yRwwg z1K3%wj0xGyrCUpjz6D)|WrpeWnav#kW7jS2Unh3hK_Vu!GX99u1kKX&A}5occwEXli(Y&@ zF)n=?Fz%ZfRcO(Z<1|1pE+l3d&0S}T269_44k=1WzrY);{e&7bjU#G#k(5PGHlD2F zTx>O$iNHzk%ER%~4(EH?O&MniKetf^e(dEb80@{YDC1%nd$~Xa=-8l9NR9o>yR(#r zzt=XEEj80i#ACUvgCw4gy2cwVdyaC-yzK2@DOXd^e}3biE@V?+GoE?FzXs5gp3^Cj z>}CHn@Ac)p;683f4U}lK+C{nS&|b+IVzQ4`$@yK!QnhVeji}Q5qE`&J36F(q27+UEwOs#p5b_yN^?YIr*Ocu%6&U&+vU%+Qyr`Zo%Q@^~k)M5RD}lg(3G-!#DcwyNqhRU!E{q_J3+c5R_XhJb zp-9D$9@*WnSwe2DH^HNgoUHROxBWTF*NNg&K0Rpxl$viZh^Vr{R?Q{jb5KeCUM|kh zNb<-tc&#jf{UgqR?4Yc%G1p3-U5*lyafmrJwEiYKOqLJwBYrW%Z!F7=>)b9Zo~oc} zt-1axD{HJWtCO>8r7_WOBqn*&j)CLi=m%R44{bDezp3Fu)|l-)uhDbnV#*tO8Aw5o zaq_rK$KQI-GhIxYVF;{ap}shGdz2guh5;;U)L}@GvWE3Q53gIv$%k_6ukZ1K7G7SI zrm4|l`g*=905{~X%x}bBx|6{}QUz78jG}AjC-_Q=n zG*8^b9h0R3(MmQ)%l1I=fl6?!CW;I8S+G_8Jzd^Bo z5IHj#rZ@iMd-Q*UfT3a~Sg`>sWy*#(e>vR#140~v;gFf%@<{##`1vbQlOwjO<#e1g z|8L|HhEK%4B}e2Blo0&uboe)70ej4n6GL?_5%Jj3``6IPC16=ELO%UN=06F6BtM*O zC7<(==6?@87?$-4{pcSe|4Fzoz_87%+wCO4zlZ(`mPJ6kg!vzI{y9r~>M#IN@M@TG z{og|;fMs#=HBkPG+V5pB;MMIQL90vhUmzz3!tGJ(ZgvqE^Z#Rrp<+nC8Sjr_p!09W z)6tS+A3D-IYJu@C!r*!OW-(f5EJ`dD7whEc2tB+7D$*8@RUy`-Ndp<PG1dPdX3eEt0h2vfS>hEx->@$KUe5o?;yJ&73DgNy}tgSsh#G5OpEv)kgBsAxX zWuyA@+dz!AwCR#|4?KezG;KhqMZxqHY!Ue+ehmS1AFZYHvCBU6lviv*3?09Tq+?U- zA+)9V9c<`$m+1R6w(rMESdi>3E!`{oI!pXoEcw}j?t%_~fPH{_fOU{)VLnhl(bi0> z=v-wa+I%^u$4g{ze6{8KU<*A3fD-t52UmdYnDYj^cm}3XV5g%{F#yeX9aQJC?Quh! zvTcuwi`A1hSKrgX;+(Ms82Dek28_|OiC|ud#y47V@eYSgTrL31{ibPorLjPYI&6I- zKVl`9x%p=Xf$A5@TtSpl((&(|-t}Yg(cA)yA zYNul_+Hsd@jB8Ph=%>-+hJ@u`KkF}8CfPCfl3-HGd!r5eK~YJG_-gk~4E5aZoZL%? z=UDjG3i@bUO&ztJXLN7EDsS+7I4&Ub-QC`*+o}i5VfnNADhkkY<$J$%naOiNdAhA* z$v^VLW8aGJX8V@K!FW#-RPCcG9<91|-#XDcY&y_#WAA8l5<(WOY-@zz$?PAk7CT`8 zy4g0a@Y#-Y$SR1ox&0<1?m(DF@!Eee;OnT{%7(*&?-Ld4a+X`Rp3TLWWrR09wy^w< zDdXOiHRe1FQ3gS(inzHMbk`TM>j7W@dGT#Yu|*a>V4Y8|$m)MA9t z)K`xFsVSGlU^>us2-a+Jfd0ec9N|0I;|Jk9a%Gq&VkHn3v=GlnKQx;Y_wJJPnQ#?( zxAiE|M~lRH;O!b?b<6y5w`g8tB0qhqWo`e8^hxJ#Hm6#Gb&SxaNG5wPvlY#Bp#Axi z9~#e4(d~$V8-M1DzPcZ%j8yXjgBQOnyUv2Wh9OO{c#u~cZ15%e7p$#?j9*fR@kyT} zozw&#^3LmfOdg~ngPRZ-eCEw=dz;sstJmCeyi9u#2C--#QgjBGyT%2=bj~{h+zob&yHiHKd^;S+KAna48}tm~lm3~M2Xo(T%CV?k8Be6jFltk>h0@)bdg zcpm7xm#zJC`kd^wS+@R&@fUnz-}uRUI`Q5J@h!64N(;~{@3NJM^2v*5^FQ5F<;+$w zhjX4>x2u)VOw8nD03UDkPAb3(E=CQz+|9MxCcS76mGPFLgY%C(^HH~cyHDwzrm0;Q zk;W}Q#|7Ji+92M=_;_})Df6W6^}b zDGQRE$60NoWXI_%BAtQacHHd_=j$kGGDRT*(}T8(MrFAi@6ELa(?p4eay&wgzO!fy zE=%J^8A|h3){FRat3eX%5>*fA?OvAKN<{NYe5T1x9QQSW|^35p0RR}F6=z*9mRX95R?h{BlPB6;}) zU3WOncFa+F$5CBk7w(CxHC zME7H3BqHKO6cSk`@%kcR1ed@-^qV;M!HDO8(}bKBZVXW&k)JGY1ZMJn416fZKeXma z*%_Ds&2>VjyMwD-3yHF zd(a-HQaHp}ID*cFFMhPfkKr4B>-fBOe={LSh?~dMc6;EUXUnGkfpnbv3D<<6^72fL^Br?$H>)d^GI8~M~sGom0bfbG}(%K~IWvnb)jOWaD;`u?0JeY`cY z+%t)6dL*QbfR?DF_MtawiK>0Zo#9DNE?hiCfM=YNgIQ3g{)*3z)Ppa|i6()D?zdeC%dQsY~lL8a=9$cjQ{)LWHWm zlrZH0u_Cv&&==}G9sydzc}Vrhnt?9io35X7$H=0CEP0l2u#+A03O*Im&(6>qj@O%d z%Qwoou_6_Ew%p6=+sPr3&>8Y@bAC9u@eH~tns=#mU17jJwAncoZt_I3Wl_uiX>0yw zXFajjv|hH^*){9LyW~KQEGl`&<#E--c8fyNo_Bl|1dBS+A$pxBVT0cu444>=LL%8N zUYZhbM76I`1l=v`2F?>#!4{jsM{M~@!-{>re){ASA;qoQ-PU3H5-Jj)hhYVMf+HSp zimO#s&Ag$kPpsCbqO;lBIMvuniOj~hKlE~ghIqCG=}(pLoJMMuHxF0uBwRT&`pD$9 z*|&0kz}xao-3=p8=kpMt3=aVnYJDs8o80_m`+7hBipFP5(u}9C%Q2Z zA2&vFkGHa-nKHaU#g=k;V-T9+;duthSiQPCJ_Ou#yY4nEG0?kPW$r|Yfl(`%G4})! z4sNP+$ee^or4&)p<~?r-R?YhY0HFX>>QHV%Uf4lugo*6(#I$vNBly~I1&`?DqY z3u30s>}`s@3g;uVVs#7;1Ffgy-NNk!_}>up_A>W`-j_OUIZMlhKE1GY?;aBT*u!}` z_l|D8LPtL2xzkS@Imr$ldQ@&r$%>0nAL%vs0@F)coTru2=byUH<#<1KUiC~51JQ6V zh6|)=C$ceX{2>*z$+HUI%3K>*yPA@HW_x!Mb&JSD)*;b6RyB#tH#4O(y67dSb)6}r zM{VU^0|GnON_#logNdLena79avv_&bEHoayWX_harL0-bf_enFdhqd8f^9E^&jkeb z`_47ruoW;BNJO5qO;BJq(G6GPby-eCv}D{Erd3DGMxEI_27Xr^DcEaLFv z5J<&C8vsno`|?DZlcmqXM`wB7jri2ey|Io8=s4^Qcm-5w*Xj0ttM|&`w*^Hb#~!@(3Ruw~SDF zI`Plj5bBqB%zYsydnMi<1B4c(7>7>eEe2#j*tG-LG|5~vDV)^ln1{MW8Zhh>h^+{V z>LM;H$m8h~U@qhBnWUQ9#WibUr|nDNlPgMbq%!TzdE}bczX|JpTCG)h*;8+{PIzW) zNt0E;yU@Arvk_#r?Q{2%<#e*7n`^4N4oW<&MtBhU%F|=~AauD+z%@8+D}A>6-rV=g zX>-Xgc{wSIFIVM<*b~8HKIk=pB^B?&sDeo^wDG=bN1Bj6q%%Ol&gB$fKT4@e>LosD zrk-*BvBnhwFwL^5ZOCVmF6&qt!Cwjrqek3mbQOB5pxmpAjD46NJ*y=Bp_S`d#(^ly%`2cqlTrFB+gt=#r zkbYBD%{|Qw3B-%4h?{pa@}4ws5Psl_#KP_-d#rgSzEERQ2t=UyVj)1pFg_gmp3R6k zl1e=`0<6~gLAE2ONp;5^hb?jf0D~v7F)ZvAXpM0?@mi;(K&h5P9s34$E}VFfdIK|y zu*g>x<`zeXPkYi%v1VjJCj@H0RFW-WfG}aODoCo)nwYt6$!7m7Kgt5fY-)&({ptlJ zsiASng$n@@LEhTe_d@F0y)K_&5grEONF)6gHjnuPDHJ1Xi@e$Y_R9Ov_koe$=*S50Ay(FwblJvZ zC^HA`U_+B`C~{`ahJ=a?7HVkntu9NEtW+bNK12eswN&Aw4bc&+z-;OqrhNIxc@5{V zZ-ViSk~Hm(7xNB)mT}1Clc9+#+>WsQPfY&eUE_zoi%8tiL5_oPTE#A=5HD<5w~Ik45nSYS5)Zx)b_J(<)duxh`^`{ z51n4Bymx|+;1JtIYU{05SG*SqQfENqd3(%NqG>$-ODHXgj8!sm$V+JN3z@i0oM7lO z2qEpkvx-uvKsoZ=+xF!luZlhq3yV;wTa@34({2{GP6eB9)W9T_DVb7`F|*Q03GS~2 z$`k@?_x|H=#Kx=_3odUIHW*PR;%!?jt*j*sOF7-UxGKJcX~jkmUg=K^Y1dVi()WNd z(Sx{F%r96l$5nT6hJ!T8*f#6NRi#^9VbKRGA0yv$QjFb6mJABR5c$o^@@y$lOehVa z&eiD?Dfc?DYb(A$EL}ANN>(5?_%ojQ3tK)WO2?m)of9~sM~tWN8HT)4O<5|K zO$Y$Vo*2vCs~wHDdIrfM7LW==;Z+>^kxDbvXT|e~Bw0@Bf;!^GU<(_h4_0X=N9%=5 zwVxP2$cigHTM!jsEY_ArF$YdrTaU2x)Ej>xzY;g`Q7Mv0L0kz5{`hZyFs}r!$wx%_6aO%pB$ODcx z`5QTYqLJqo)R(1cO_VMzpV3D*-Eb0IaJIki>%75V7Q$BeK@fFzV%Zr5+APx&2YnTI ze|n)>%{C9xvVAp}@5x6qwIC8L3phGxDG+|I6NJJU9?Z*Jg#(1m=q3j4_H#v@k$U}? zC4@iiPgvI;I$fl7K-D^^!p3TtGB4NBcElT_m2ya*^r{2E`o`K^6p5^o=){K)hK#G- zn%K5do@dR0>gl}EJ1_CrWMB{*R?Q*V+D~k3yK;;g-SdDgPJz%bDD%qPXu#3xr`cMf73x`POrilf8WtH}FkT2ojJ4kr%3`89a4^9}_PAU_r zake`=`W^|_YfN%(yePq3%}X+oTCv#53y?DbWs9zx;`ZFpLO$g)aC@;1Vp<%rl*>WV zvfVz}y-*)8ve%j-BkNPTo^|)bL6iVHaA5E`9(SbQ+cliJ;~{63(=|2zpsAYTvAcdw z*G?dN>l)*bDr<0`MX9|+MNP!N^KM6Ld0A$R;0ENxqBW)(zj!}(cW>YV;y;E+KqRxsYg#5VawKSg4TH~fiuPvJiF&R z`0i_Uakm{GowBLpVon_D-95h8DtW+y9Gt#O6$LtVxDsw@W0!3|QJ{Q$YT13zd;$t5 zi@Wem4;1yrnxmF8m+fs$RwFkgh{@xcq0UNI<_?;Tzl`civhV77%}L2{{^GEApxd~= zBOz_+yEIkS3(u4BF4CAl=1BU6bLekI@&Q7_DNT?Un~VK1l|Qzu z5C^A7auDXZLvGk?2Ym-;m-S&nF7UZwVMPUf!IU0n9*GWwcF0Oq@i#*T(T1BfpmoNf zYw-mNOMdE}GsW!bozC=0g=D8PnlXkUKKQ=AHbZg$e)yL?Q(B-b)9&rp=EG}E0e(=B zf&4v_`1_mBf#Tq;jvxM)YIBYj==O}CZ>*t<2MCc0-|6E*(+-);#FMT&G7-=jZu5GpXf3j$+aTDrgmq}22={Y1omq1Y%!9AP9M|&ff><}-wBje zmo~Q?2Ud9hn9aGLrrZO95u0FSW;v}jtEz6j-{N+T$}uW*gwJPx4&8vYyh38mZyOrZ z2w2r4=ZG?uzi>2&nSq5q*?z08)t~S|*0b*X0h>iRXqARI=zDRk!k#XQMIyg2qm8Yp z!Mnj%AL;|_jw~{#MC`A=UGUr`))@_WK7!{^3L)h19J{Q?5l31PB|sw?ZITjbg-klYrZD%?OL_8ebC(-XLt5l=`;# zg6B{r_QBI7QtHUEvr96-^s`x72{G&388_X^M5@OKkSAb+NZ~W=yx&SJQSr?%qD+Ih z`j=$xG78s?0jI$?)~m1a_IdL&QqxK|J?SyXYUC<5wfc)A5FbsfYVw2^Q}MK1hO9qz zqfPAf)I2&_CI&o=A8KLow%zD;G1U+wFCOrTMZ=VrchNUl5o>Qw3VBvv?mPt(7u*Ou z97$F>B^R0s8w;1laiq)nx!VYjd#Xc(9pOB7NKi0NcCHgnu;md2(5nY#bD8E3m105p zhNPq!T*23u8n;Eq@`MjO42SE=LIG+i{iH37{nH~a+=}jSvebn2-xuF(fiKLvSnD#< zj_MnfB(3X+X^t&mk=}HpoxFgjtXL&TG5V3S%OHpwCVN$QMg_ZiH=VGJrd}UUi6Kr- zo_^JM8fX9d{TKQnK3oL>*)b1OzP_3ZUlC@7>!vUdw+nF9z;vAbS1k@3Je#jQ8XWzW& z0&+t`Ln%a_(;k>4Z<#gATz56+5JuwW0<5f++VZ>^ zG{raEV_7y&arBtJ7zY9?yFB@2y`Juhv)LwuHqvmRYs)^Tdg~)1zFTfxTgL*odyc*( z(qyjF<7^TfN5e0fx7R$j(0C_8M1S(Y&TlV@Jg&|LR|`4E9id*tBX5|Rj+z{Xm3^1S zYNzA^v!@Z?vVFiT<*d$WgAlq+@+^O*@wqN7STR|)pI5yw1YH}uJq8oC@xqL7=9;qs z2h1EOH!Q?<2zVud;X3O5c(Hgtq3#uuJpLx za40iyQ!I{P6>+_qJ*E&X$%`5nnjX80i|K2w@1B5O`5w>dt$JhAw4f=A3TIRu40MAY zfo`L3Lt3ow1+K!D9e@ed?c)Zu%MKfHF*mL*<4IadB8D9o(O#S9qpJ@k*$1Q#%j-S5 z7AU*==D5v?M!xu0_UR7EIzOE!x=U8Ps@q}lw!LfLv^mqwzag$}3jUw=&blwEuYdP4 z3?)cNgS2!G-5@9k(j(mrASEdv-5?FpFo2+dlyr9uUDDkk-CbvUes#aj{e1-I9R6dk z*|B1;wf1LS*L&?P6FsgoE)<2cnggfws1-u5cDhlJ1FJS4j{U!%_3U1pMBE-fvb&n?wEj45vMpIx0iV!AGD zEH?De+Md4Sa2RU=v@m9FlU9X72oa~?I-c<#UbU`YBwUWW4$9MAu~CW+yG5u(co_cuJlwLF?yGy^DL zxU=_l|LmaG<)FX#IkcJl}}c#}Aw%M8Ha&#%&wUe4K%n z&p3}_1!oO>ZhkE;+lR{SN5iQ=3q~KPA$p{Gno%5&093)*(x`8HP5x%t+2GoOw;zOA zTMWe3A3pA;c?Y|hFMSh36vbkMsn3#PKBPbG*;CY1QZFjH4>ts(*y<8SJ(t)cy=9fS zov}CDu=ANK2I7bZD4dOtgP4+sjSuy@)6q-EEw}n-uOGIq$v>`VoXX<_G6w$4V%YBp zz?0}_-BtA_FLv(4d*uLothu>AdnQne$-gA}@`ETB$Bs(jE|;4t3$?2gyX>)3##ZrS zDmOj4^O26-?RA@|+bHJYK2w~n@R6kJR*4VARRINHqQ#ETcgsyXtx`uK?V78DUjE!@ zd2p`jd+*0QlC1Vb=aZcuH<@_+j&0&tW3f{m+xpsZ6hGLARA4Y4?%^h1{kaRzu5m*A@Ob zthc9sfYq^s^!jnNuJhY{%`0d=lI2-6E(t4oC3neV-Q}XO(r4DF)$4|X0-GBVyJqdi zhk2)7-U0ea>=QSj*CMxEbidwME#jNPx_l_#Dd-~SY#ga2eiVcS5t2EbR(Ie;+XXEh zHlHdv90X#}L=p>Q4vS*R$JH+<7)N@px##>wObyLhd_S7+n)^s>XnT9P(QrU^f)s$i zl__!8e3}d^zrKpwmGOFPzTONXw#6KNZ8lcp*pYe1d-5Z?wS=4(FjB5=2HfflH6O7|4gv%eXMl3vHv$=ny-Aby`JdSLatE_sVI^Sx}T!odl?9!Su%J)`q- zT9RXYrZnD8ctw`6bvh!nReAK<`K!j{%I?JsV?3JS*Y6GY^X2YMcRh3gU!7s>mx-=H z#a?-wHG0Rh>%EbV84YVut3ObPv7CF34I?|DK3thaLr}`k-#k5f9u7A>n~VyJrP4U2 z9s6_7C49ydrDu6G^dNN{ZtsRr#g_@jbW0k4*LE*DbZv=iUd3&;Qyp~nTHRkvDn>RM zdTgfXxc#A_;d+7oIp^+ZbPVt?{#{h=s`A)RDCz_{mG?a)4n%Rf*kYA!OdW6AwB}3( zT;(Or9XyuL#?rWTdJ((XxFk8~>zjO_!~A!g6q|#V54R71F{KYnqP>efghGS5)3HTi z@vabZ$Uy1%_S)rmuegSYk)u@dZ5)2Q3DR}o_2#Xz5R?=tFk9=`d8(`%n}yERxE5ty zi@7@$j4ecu{IS+Fgd&U_TkzXa9!?!Yl~oy6er-4_T648>6yl^Rh<#xsR1*iOdFGtD#7j*Jzt#HzAFw zqZG1~Ge7bDF$N^sQT$!mJO%pm4!nT3K6W?JcR`}z1A`KqN-F=R9C2WiV@m#G zgOK4IUJRA+<-E)-C!oDu9b|@*O?aGThlj-x>HKt{f}2Q5%9Ocxmw2l;UYv~`7f1Ol zd3WMPIu>4hkHS<#mHO6?j+GMXypbj3jlG6=CjUF%a*FvXKS8f((9<+^JI~eAq#Y|N zdURJ=VqX-0JELVS${r)`&rG7hY||ev)1!&jfoQvfB*h}>x=?=Y#Tz0JjMA^O@dEUg zE697lmr?rkct6wmxT`}C?)Z3*JkT(C%+MMRz=V()I7QJ~U<#d^C*CPnrYlR@xzk&g zRRK6I{lMse{53iB+;g$}$}E%ddWh?G511;W*nxsAIRllC+!q^NXH(3{jzk}q4g4fD z6|PR7M?Xn*L6D+dTN;ah;nYs4UkOmih(A=8AZS9VLT(af2&Sn`F^ z!{&p7{M@nK+i7x4oAS^FE6+6npNvbqv-YZvK!=KVoX$K0{JLx2*J!Yv7gTE?r*Q)3 zjy#Ek68$l8o40q5cJrhLUVCMTpeiq{Cj%;T^fv^V>K4xqkz3BjYmxRhp()M0%EEkQ z)#bw4{l`vCJD7&(+U%DTCBmN3(Dh;y|3dVKp0t|}n|C82YQ1Drzb>rr146a9UTU2{ zGaaq(_dY~cHr=SYHXH182G z+bx@YNVVn=xCB55Ax*b<1Zi3!m|{0hW3DaadTxI@(y82D_U>N(v@l}@3riW9qW7dQ ze5!|p`0CkUvp;Vh@idWoVm{iIS+eqO4`s=1N?!o%Z>;JudpkY2_PBI4>=Q*bVMjv^ zevaOF-Hu6P(JlQUlj082FL&fcKk~f!=88lbot4;g4|Y&DXNS`k$$#l5gUjLgDdj^j zWy7Cr>d$sNUhiOtKCr20mgb2Ah~v`oc!gWaA$VzG$Kb6z#`2GV*fxnGirHzjkW>H- zfhzW!9VsP((LJb@0(#%z1e*Q!^wV*B%+sD6ch`5447ztzCASEH7vkAWlqMz$0~Hsu zZHAvPBwW~hVFPEJy8(VHbm??H^;;bCR)xzv&C9Y9JCnC-R8Ddo&hb)L5%@-EZDbNE z(1)l`NN#Kg9ShH6RI5KjS4Wc45#~Q%NRAHLV)U45e_&@6$Mmj`urxu(^{w*MGM?}{ zz1~`=Ge`++aaCcsUUmT!_v1Mw2oivIO)?sH2+KOne1qjBbBlye6P`qOmRTgAQuHZ! z#TW%#-KUl(_E&J!|4dt+B8f+Pl#*L^o7Qh!7A5%Px74qFsOEC@fBYrpto2lAlf#FFA)uD)slV^gwM#t>ApgsD8%SslYN*%X4$N{ABkd$ zNfew&Vf>{$lw}y>$l@h|OSA{W^DYol&ul2aFxlMPuJHHdm3?GrW2tGM@ItQi$Gsok~!cU%DMZ(Nem4Vux)GeP!lBu-g; z8(a9#H+##+`%4@zyrrqbhu6!13DH?#;Zxl0GMdC67XU{v^ol9i@2@9r6+lNhmI34~ z0r6<@8;j!9&xpn&rodr!O?`D%9I=mt&% zQJwcZ9o#q5lD`j;xT9GY8dKmQ%s6IWm}vQq;>VcrIUD#=V<+Fe1GqX`4LfA`RaxYd zXv$*eey)^gVS-O)HyneQkVZ4P)6%o{si-1ED`$A?3NV4A3# zqghpxujU<)X{JOy$6kaWxqz)++tpRCvN!kU1=Gz10GF^?AzX|U&+xw44p-OE&JhIg zF05tcX(de;IxB?6NZ&u(x+B$1Tjr&Q0f?TZuhsYWK$SMAmW1LnR^K;LlBA*Um3Cko z>)AA&PwHEK+m+||Xue8+i&!={_B!-f(xv8|${GVAk?VZ>m19IDjVJQyY{K4~Ln4Q4 z50^fV0-lZ4#cOx*ez+vP=&hgJpDp+biFRP|U|msduEQtTn zIeRs@JA@mE0no(M(IUd5z0bZ^@lasE+B5xwogaW&Fy41L8w*I%bCg4Z4T`t)(1sH% zT_FU*05B3SM%!PYnhAhivW+125!R%Bvh@c5@2~@AvUC}DXngn55z$trk2gnn-!8gh z;uL^$y6mfLst`Hbnq{kzb_Y6G(*-~>50{+zOCxm!lqZw+V(V}Epa_LT!GCYrZ0 zJWz8&mMY8I1fG)l^ra5Mco!s}TxgIW&k|$SRuaG20x()>A1472gBlwEDnmYzbhBLx z6kR4&ai7eJrKP6wzFIN%&H!rMITf-uTpB!k18Nz2C%ju!A*BrnxgMt~Ok|pTvBqrT zU|B&#Pg@ujl2ypL`1MY7EprmAwnn!D{_+-ncGy%5YSdYL(uv9-26aR}!AgVVzz--t zFZ=n`8C95$Nu^;|w8Bk!?ttd6R@hfqT&>fC%>HN}yXow;uKL+Tu2oR-4}>Q4SS?AW zJ$c&dZ`k3Ah);Hzj_UetD<%SP{zZQTFb6agYhB`t`Ye}k{ZPWikWZU07d?oR487fs zf4}1-1iWP|0Blo;}yxb!m$;hRYyVJoL50=Gz0-zl0$Ez~({zIh*lQpMmttrb92| zfvnep0C?^x0nK7jV=aI;3p<}OY=KbX01N|D&A7?5`H)YG`z-KAC)#H;F_034SAOR+ z0KzGGAa7^&Endw2HzNR%mtS!3rRfr2MG4$g{Gu(JK>YQ_WU(P+N-W-E_WX6jYbhAP zl|_#`jefPSo(CC$f7?mrr&+lOA9%7u?2Djg7dZ!+n2}E!Ky#=ySh68_$UDap1<5d zM{TnbAE5tub`E&i>B}wMz1EUpQ5epWBR_=|51qwQHxBPBq{V80e%M?tWJCNH<)W4@00n8h0!;lAR}zfWXwoo~=P=nmdmb}ZJ3T?eQ-+Y3_9TQTOVjmB zb>;oi#*UldAN$K~narhmGytH9(jv|G`*t{PGb}3TSoEOWtU}?1EO7v_W1WnD#Z~o= zi?*77-g!!YUwpCZEOqMqTjycKLxA7v`$GT(;q+Z%HGu}$$uNTjb)dDX=u%b3Njmn;~H*ey7T zNQ8uej*YBVAK;(_d`&S4jr+sGW3!5@s$uIz67TTo703}RgC;>wtr;Nj5>y_or;W=S z(0lZk(>RWvN%^o9rwcDAG$5#1bhoC%8HnaulYHBS1$Np3@OZx@D^f7W{p@;CfdIca zmxTK3<{$TGc@h*1_nbc0{fLTO`p1&Tn-WA7{JQrqz^Zim`5qEgA)$sA4OrS2@{^W` z?9L0_ruuI$$aZFPV`1~xBd;M=;34(R8XY%2TnMv6^-bngC!>_`z}RdKKs(%8(RR{q z{Sa}|6OR9#Z%VJ?3LuTejT=CT$OWCMQp{6ZS-B*C@F8-C8fC(Vm*!${f3gA4LjCxW;5N|8GkFhf6Gp<%}d<5t7aQ5mr zI8PA*=q;}@0F#Nj2&zHvla_0f^C28XGfxZ$I>MIJPPNIk|=)dny< z=n*Gh`sO*?$LOk~ESmmD-BR$^MKm$LCuip9Y7T?7^kD0k7 zeca<=+Qh&PZLzkxL!4I3+_iqh^1Rn|l$wb7V8g*Z+}gg0Ii-kTu0i%j77q}Tb2p{^ z4awl5&isrvw(^5CF$QnO)%D7f(8XCHrhis_@z z{fw_UMq$=bd!C$ zGF3gtYCi$MVX~}_tpcM@29sFTiKc{)_sLa~AMikg=*g2ceQ0rRUD=4*BJQ{W!Va0v z3Ub?8V_X+i)EtBuraQ}x*ejX0%G8AXIWHixa@}%_j=MOVN!f9Lx=Tg$CQYP zcuC5nr5(HQB|q*$V$(j}cqL{NjyC(QPWM-7+7fDkZ$gdLLLdqoDfKcET0+V8YvN{) z-AlrrSJ&0G=Lbp`S#a0xNw_xaI?As%s zq8dmQEp_*{Q_GA9R$;v#vx8VDU6Zc&MI>$tRD9V-S@^;fUwB`r`QEzEQN6@@Vc?-x z#92`Kz1x9tq5=IC-Z}i!9?rg+wxkTHqCq~+(-222B(PGr%45o~Cxfo2Q~$hGM3S+D zasq5h?KtL0Gn%j{ak5#p$k1&gq1U|>ZHCfnyMJjAo2`#DZC6r=>Q;jAHs^qZZzE+x z|3QX73}Y&};KTCnPoFTE1mA&dD=bDok7?cr8^`yM*+l-!k7QQK+)v(|1`Q))^+^D% zDG(HguDL8J*`D<>7D9&z>WB78%uQ{ZM;F zF@oCO3ZcY9^pQhZojdCNw~W6sW3r3G$Q1>1i#N#?$asI6g^?2R|0aMi=2N+GQAN(C zOrZx>$2k2Ckf6858KpL+WqZ!#l&SbjL0YK9gPrjuVac#l~3FZ=;)?x)?6a%<6-wt=yCI!SW6lmnW#Rx53#TW=^vzG z$TZ=`%u46V^im#OesGpG6M8! zA}0y6DBBXHHSs*VBK#wc_@D{+MtJ7OsDbwx7f}p5iM}>r8l~+8W%~Y_d~nq$2ls9a zj`l482CU8XYu{V`3pdh?QpAdLXlyGp4bc0}dw9|Vj{w0o5?V;$ut{*6Kw z<@^(>S6!g;-sf79T*`vRKlaTjm4&nt5_=We%1n&+74artXedJiX=O(~c-k0B`B_i( zm8AdW+QN!`8GV(hpM8`xD0B7?crsbR?LezY5rb0-|Ndf=Y4T^T;O2uvH-)A*@2$iw z+O=;}RrC{=$zVqn>0i3_F5OIP$y?i0C%#nWj#t&vP?qqV!1TWFyuKf(pIOtEKTmX9 z_j{4>^nkwrY)Yg@7Qo|8YR_vj{}*6fo<6-73}IXEz_7N8;Zd#4WbNneNDlgePScu^ z_r1?FRwwjl3SEbvr_}7HsQjc;C3qwCZ3?{P03T0inNzb*WNm{=+PJ5b=?YJ$xLn6)j^5(RYO;7{%+j!48G(vXbFqVtK-9r(x2=uVZv(;hA9QKE}q1o-I%bP->*sf~;^7;ajXrX13x zRBaMjf_3jAOOw^Yl1>%xhcUu7y2xU=Kgu$jo^QlgDWsne6_Ji-_3d;7LVs`2ZnqQL zQ@x@#m&rfu7QZO7$3bwvsUAA9`9-B@Im*WIhQ;Ss+mwC%hS~LwDOMors!y4 z@p>vVahfIB;#1S>abNAtz$Lz8+|+e}isP#I)SD+p2MYRf&1}FIqOmQ|&Rp56yZBs= z1cRwSNqw-hvgcNQE-VJT}7~irtH0Zm9*GBm_#<#?s$kC+2I9#FLI>0ec_-V|uIhRO{2=JR9y?j}n?KN(Dme_Y`Cp zFj}HBsCiIj;TXhNie5fpTn=Q$_1Fr@!P@i|8D~Wj@Aop%RXKfuxTaa6E6j3g&qTDClXzZa1(Q$ z^1V+Np(Zb?k`+&6CItT_gIWr@y`|r92B2MUKvwzZLYb+ z^bISxyXB~)*bJ=&t1>9~=1ZjMTERY=8IU7K%Go58nv{sFf~j+XTCVO@PnYRBw?3~a zwu2MovZ12uAWufb*7H6`1YS>+MRhZ-JERFy;5U94zj&gAOzC;lCV2bn@u1o_LJclt zrh(6tls33htIAYpAu-&|A)gqG59`EoJ0U_brBablhi9QZxJDkiBy#h{VL*Tqk*Q7wM~w#Yf0mMbZ5!U7%;l)L;K}zl<9xLJs;G>w26?q#@4RFe*E2KWa*US4<*9%D zoNr&%Hh?>e$V`&uIk{QNAp}3h2&pbwk#`3aKneBaZo9|8X8>1Bo&>*;_`W3dEe-wK zxJeY(13HN+Q7|sqNK9%kO&*nVGCOQqP|Q|_p(t;jjt!mq7cpdo)dI`&AU$Dllkars zq*aqwAg|6=0kmYcGtnqxFKJEbvzH!Mq<-vtn|`9qHdS-ivH~Wm8^gg=_^M_Sf7*X9 z0n|LR@E7UWY}?4tX*+CV_%kZ_Zbmay6D+g4bu|~S!&LyBJ!r6qJwwv5585o(ml@KF zbsV*W`JtMZ^~uu{mD3VH&5qKYoI8<&C$ZS}{Ic6JTdKq(SnIw4TAG_C>ou8xLI?VPE*@h(G;4S*`L$uyS$qH-g&v@oE%RICM3& z#nqR}d?T`dh7E9sILOUFARh)(i$sS9KtK9E&0Jl;L#wWJV6VD72fb{3vZA!~yG%8- z=GQuIzLfT6g!|56025$WScF=)3)nP1zDRL7#6O^Uc_J%$pIkYC?2t}J(!7JaMKt*j zeA&uR(86Bd1mC>$Ta}#*(HjxI5g1gFH$?_v)WJs0me*r|8=WgE%YSWSUvrW^dj)CZ zdX~L`gp3lb9t(0h|LsgO!UEzF$?fV?$d^Cb2y90%9q<%dP6Bfr2Jg{zvnen6fd$Ig_{K&pO(dxM zZ1%|FT!auK#aG2em}D!!IR=G+rH1_>6I%W^XpF~7?`%SuN5$N-`?P0M99;wc%UQ}u zL0lbT!{)O%#(>3>}~8 zlWU^w!5+Z{N2(lX+w&siqEaz>@_(*sBmv@as2MGFQUalbY$i2tVgl~QmeXU+f7GIP zA&5Kib+Fg`5z*L@@G4u&Aq(4nQAd;=3Dkr zav7W#*%((efwfM@Q!61WG}s=VU)6b~MPe&IXz?%wD2Hu_d&wwT z+aO&ELj1CZRV&;S1NB(Rr@}NgKM~q4WK3~^!Hf|SE~{U^dGHl^6gS&G^S= zG2b}@y(kzzZ{tjx2enWy1emS~HdVFq3bhr@!9Y2T{$ye1B9Tb_!gzVcj>Va4c9_`# zUxk`&TRtwv-@ZJTh=AlF;8@z3_>u+cVX&}u1qPVm#P}TLYWdITl}TG_mH44bB)=xo z6mFKTqGKEV#dg}aJj0HrSUd{Gtk$@e1y^kIlb`h?w1@nV8g+)YGTb4VA8l=bRS_B~ zyM{!xx{LE0cM7DGG0_Uj_RD%GcQ9pPtGQ|4LOIt0D-tQq=whWLRgzaG^c*m_Ah128 zc(@DWTwo}jAnvv5Fa>R;bTy461Tn5`Yth@X7c^qO4Adt@_|cpgd#ceKRS=Y;eT?Ei zS!=>QIz?Z#*&V*k%V0uwqtI_4=x54*(!M?w36U2sM(;5{QA)%88hbd6mQtwo6u0NM zQYF;-*F+V|BEGX_f?gqpu3Wbz>oe<1>8;JXdbJJ2rmW3Nbpq??=~igx>g#ko6B-_U ze}|rwP)O|FClbT&1P;$_w7693<*;mckg~AaTfze+IdMx-FffBPdi>Xqp8C7=D7cUg zHxA{8ksSVzR*e_VH}Rguc+L=qjzMG}{j*TW%FLhwZ>CWO_mJV-%D{CDO(^QYQn2zy z3}6pdF9VaUcF$1@m=f3SMzZ8`>6f1IEPZ-u>?7M-YAE(?PE$d_i&#EQoL}9LH;yGi zQi5d;iB6W~-1a%8l@a$Y=u?~-Z&Wqjpr0d<*R%gjuCOzW1!fhvhNRts7{eD{PRd^q zio9fM$nP}Mz7;lpyV4+dWU<9#`TIvLS(SGUD>WT;>hGMHHibe$?i?Hr%(FQgEO$$zkb^x`KHlv_c!du#iEB}## zhFT$N;4$;hAy6fJEF0}cZh)v1=A@=H=l%+K@fHYT-1LKs7u1(>zb$tz{@xfUb0PsE zyv7U(=+bq^OS=%il;ZU9WAR1X7)YzLQchuC&D7+&iR+IH`dQIa%H4_=Faos3SKI1u z;BRB8JY_^i9m_h)&oL1^-8?GQ%-DF;j;aZ}M??`2%z^6;st2w)g-P*Qt&PgDx|6HhKqQO&O2&Z|A(Dy3gWcPcRu2>>Kt7lfR?G&M?qk=dLGjD6DfI%5aVzVX$hn9=HnSBJSQx^2Y} zA@a4kq8-JF0t3sr*cG%Bj1Ei>17FkW5$d5JG8^rSxhh_HdKdmoa0B+SnX{<~Y79+G zJE@QoLL(a3Jz;$x3^QfaDb6zzFjnQyJLM z+I5N`y7gy%F$>Y>SR;L0!bB1xI+%rAS0vM_=V8L9vn{eXxpMmP821=zM-eXQhHp=5 zlsTS22Y5Bo8GzTz$g$WnV>p)C_H7!>R!G6<*=j&Z8D1C*g8XR*ydkt{DLJm6z*y?t zOi>5CeS&_{9-DOb`bSalEPD$e#crEW8iTY97bEQ_D!X4`;3K}o#;Nt#N4AC=MM`Q{ zXK<}>&~uIF$EDP9hd8cDByrl3ZvK92-^Eg7Z&>M&QPDn-fDe4Ts(N>qiFN0C%Fb}Wun^woaV}f;0?!&v-t)nIc_!ea=L#zA@&Ohb} z*P&0T56_BuT)uF*g1`K`=aI4hG;O412!sy{ZqD$wjFf*{`)KWz)dpe-LKkVDdh(H4 z3?HXh@cKC$mWVrvm4Z;{Z9B^oB8{7zm>QvpK8exY203^@MM)48oO{rRz$ICYu2ui5Q*IvCig zh{he8*o(btgiH1bIo88b=RiTFJr&xzMwkalYb03Rjg(eMDd!{hj0KhyWXv$y#dlwH z;2vEHj4;sV^pO-(`P2)DL7CMwu!Py}b=kB&r>`KqV1|Q{WhY<<2~( zWZkkq?9gDNpQX62%3l?HX%<-_)Hz+0D($d%Awm?Bf{Y$A09ATTGpXEHdOR$u$Xw1Y z(VTKPKC^0FX|iv6{InJ8)kI(PsTo$vIZoTrY8^snD8uAZNF^LYd?`={LFE}d2%V%Q zFya@7zWTD(=-Bdc|4mk;THFIOG!GVkN7BGQ6#K|PKmT&?2-%D`BsCYy`qW~sPe+xZ(h{4ULW)0?{WkMZ&)3)d4c>==l?t9V zYd-A}?b`iM;T{8Z=KdCSdi>AG$CBexwFRbd`_}3Ze4r3My2|oI#-)u*%-jsS4+eAj zNeON36vIxx@;vcFU zAEVUE#tl|UB)=$fLlikaGc>y?4%;~*TK|xn7T*!#($1C?Dx50BzpdpH;aB+qk6LZO z!iMy1Sg@@`fbJqUJeMb{EB^!1ccK;0Rs+kI=0<{v^HR{&i zM}rxXv&mah7s-I~qi#C!fm3#tVdN#Cjy?InzkVd1m}w& zWk#^`_4FTb$_j`2l>cmT3x;rvwJM&yr2UA&n7urQMEjff0;T;@m`|ui`(xy*MrSa5cj^{A;Pgw3D$qjrIGKtpc#(-9g(Wo#FNf|M> zW%$%rVTlzuabh~Lj;?K-fU(nJi0_xhp|oh>N3xv6OnMr#TRF16v$!88+AS-B5~?q@ z8!c;3XP5fUjurd9MSA18c&}hA9edpqfu#L>m)^oZn09EL^}XB*(%B+Z8%o;|89%trqEDYv(RvD720pEoQslg4iBATds%xq zO)n9CnzKTOi1j%ycrEbn&Y>5Cf)oFN6T3(?%&vIN<)dtrDg~r#Zr0o}og!GXUpTeOhPa56==KkP zjkylFTS>SXV&yL%{#~-X!frK}k+&ySBt1?Dgj3GSH^%GQQg;?KK-lWj&aeo7Lcy}? z9CXjtUe=k0#i93Z`PMKQNhQ8t3D#H|b{8v+`D#}e=E=pPxx9dmFTx1iSf@Hot!>)0 zIqC-8!qd}L|HyEiy!}U9{7Y&*U%>(W%(6Mj};g5?K2r&J}Ws#0FT)nqQ`BFam!1am9M_tW{>V01j6ezK)GX>_x$PF_*^%$z4+(I zQ(JUts2`qMpVm5i<8|Zd=8u)vY-jnifFKzgNy*x?m1_%G6x>GQhh8C} zB+`hZxwMjn>LJS#8EQ<5x>z!Ctw|LmMnpsEBuOxGF9}PikzC#~eFn8rfZcmoQ}_p7 z>h6Wus*`s!fgYZr451x8wI$=ldqZi9S1TVHC~5e#hizxkLE^00XFvWF51bw?Uoc7y z4s2z5^CZ*a4u|IMb`;`?7M-geZ@Kdd z*kKbJiQjX4#(ox3ulr-xHOv?BIuCv4+652SZj{4Bo)a95gTyv~&MQBn>+kL?((~u2 zN`c-L*{_)(rU40l@_$Ny|Ekx^{))WICNixe|4kVzi9Y@s(4#9ZpC$i8S_C^FVva6; zcyIkrf^-Hzb8ZdF>;8u{BO5>-?q+kIr}Hl=`u~=X^Wnw#>fsBDF@XR5RM=mMGptZz zg6toJ(?3IjjHI@116tRAJC#KDudrYDK7aDRgzx`S6q1TSMy9|@(Lh1}pQpA?O9A5k z=^f=NnSam+ThRhKcLsZRg?~sx5CJ97t>II^_iq|_Kv|CuWQ5Oe4E7IcT-AUcTeSKH jT8sz!w`9Lm^gD^V!%#fT#iaBnz@OY3rPsftjQsx>gghFl literal 0 HcmV?d00001 diff --git a/skywire-specs/specifications/01-Overview.md b/skywire-specs/specifications/01-Overview.md new file mode 100644 index 000000000..55d67b5e9 --- /dev/null +++ b/skywire-specs/specifications/01-Overview.md @@ -0,0 +1,19 @@ +# Overview + +Skywire is an decentralized network that attempts to replace the current internet. The *Skywire Network* is made up of physical *Skywire Nodes* which run the *Skywire-Visor*. There are currently two types of *Skywire Nodes*; *Skywire Visor* and *Setup Node*. + +Each *Skywire Visor* is represented by a unique public key. A direct line of communication between two *Skywire Visors* is called a *Transport*. Each *Transport* is represented by a unique *Transport ID* which is of a *Transport Type*, and the two *Skywire Visors* that are connected via the *Transport* are named the *Transport Edges*. + +A *Route* is unidirectional and delivers data units called *Packets*. It is made up of multiple hops where each hop is a *Transport*. Two *Routes* of opposite directions make a *Loop* when associated with the given *Ports* at each *Loop Edge*. *Loops* handle the communication between two *Skywire Apps* and are represented via the *Loop's* source and destination visor's public keys and the source and destination ports (similar to how TCP/UDP handles ports). + +A *Packet* is prefixed with a *Route ID* which helps *Skywire Visors* identify how the *Packet* is to be handled (either to be forward to a remote node, or to be consumed internally). Every *Skywire Visor* has a *Routing Table* that has the *Routing Rules* for that particular *Skywire Visor*. + +In summary, + +- *Transports* are responsible for single-hop communication between two *Skywire Visors* and are bidirectional. +- *Routes* are responsible for multi-hop communication between two *Skywire Visors* and are unidirectional. +- *Loops* are responsible for communication between two *Skywire Apps* and are bidirectional. + +There are many ways in which we can implement a *Transport*. Each unique method is called a *Transport Type*. + +Initially, we need to implement a MVP in which we assume that there are no malicious nodes in the network and discovery of routes, transports and nodes are to be done in a centralized manner. diff --git a/skywire-specs/specifications/02-HTTP_Authorization_Middleware.md b/skywire-specs/specifications/02-HTTP_Authorization_Middleware.md new file mode 100644 index 000000000..1cb11aacb --- /dev/null +++ b/skywire-specs/specifications/02-HTTP_Authorization_Middleware.md @@ -0,0 +1,98 @@ +# HTTP Authorization Middleware + +Skywire is made up of multiple services and nodes. Some of these services/nodes communicate via restful interfaces, and some of the endpoints require authentication and authorization. + +As nodes in the Skywire network are identified via public keys, an appropriate approach to authentication and authorization is via public/private key cryptography. The curve to use is `secp256k1`, and when referenced by the RESTFUL endpoints, it is to be represented as a hexadecimal string format. + +These HTTP security middleware features should be implemented within the `/pkg/utils/httpauth` module of the `skywire` repository. This module not only provides server-side logic, but also client-side logic to make interaction with the server-side more streamlined. + +## Authorization Procedures + +To avoid replay attacks and unauthorized access, each remote entity (represented by it's public key) is assigned an *Security Nonce* by the `httpauth` module. The remote entity is required to sign the *Security Nonce* alongside the request body on every request. + +For each successful request, the next expected *Security Nonce* is to increment. The `httpauth` module is to provide an interface named `NonceStorer` to keep an record of "remote entity public key" to "next expected nonce" associations. The following is a proposed structure for `NonceStorer`; + +```golang +// NonceStorer stores Incrementing Security Nonces. +type NonceStorer interface { + + // IncrementNonce increments the nonce associated with the specified remote entity. + // It returns the next expected nonce after it has been incremented and returns error on failure. + IncrementNonce(ctx context.Context, remotePK cipher.PubKey) (nonce uint64, err error) + + // Nonce obtains the next expected nonce for a given remote entity (represented by public key). + // It returns error on failure. + Nonce(ctx context.Context, remotePK cipher.PubKey) (nonce uint64, err error) + + // Count obtains the number of entries stored in the underlying database. + Count(ctx context.Context) (n int, err error) +} +``` + +Take note that the only times the next-expected *Security Nonce* (for a given remote entity) is to increment, is when a successful request happens. + +Initially (when no successful requests has been processed for a given remote entity), the next expected *Security Nonce* should always be zero. When it is this value, the underlying database for the `NonceStorer` implementation should not need an entry for it. + +For every request that requires authentication and authorization in this manner, the structure `httpauth.Server` is to handle it. Specifically, it is to "wrap" the original `http.HandlerFunc` to add additional logic for checking the request. Consequently, the `httpauth.Client` appends the needed additional headers to the request. + +The following extra header values are required (`SW` stands for Skywire); + +- `SW-Public` - Specifies the public key (hexadecimal string representation) of the Skywire Node performing this operation. +- `SW-Nonce` - Specifies the incrementing nonce provided by this operation. +- `SW-Sig` - Specifies the of the signature (hexadecimal string representation) of the hash result of the concatenation of the Security Nonce + Body of the request. + +The `httpauth.Server` should also provide the `http.HandlerFunc` which obtains the next expected incrementing nonce for a given public key. This is required when a remote entity looses sync. A successful response of this call should look something of the following; + +```json +{ + "edge": "", + "next_nonce": 0 +} +``` + +The following is a proposed implementation of `httpauth.Server`; + +```golang +package httpauth + +// Server provides server-side logic for Skywire-related RESTFUL authorization and authentication. +type Server struct { + // implementation ... +} + +// NewServer creates a new authentication server with the provided NonceStorer. +func NewServer(store NonceStorer) *Server { + // implementation ... +} + +// WrapConfig configures the '(*Server).Wrap' function. +type WrapConfig struct { + // MaxHTTPBodyLen specifies the max body length that is acceptable. + // No limit is set if the value is 0. + MaxHTTPBodyLen int + + // PubKeyWhitelist specifies the whitelisted public keys. + // If value is nil, no whitelist rules are set. + PubKeyWhitelist []cipher.PubKey +} + +// Wrap wraps a http.HandlerFunc and adds authentication logic. +// The original http.HandlerFunc is responsible for setting the status code. +// The middleware logic should only increment the security nonce if the status code +// from the original http.HandlerFunc is of 2xx value (representing success). +func (as *Server) Wrap(config *WrapConfig, original http.HandlerFunc) http.HandlerFunc { + // implementation ... +} + +// HandleNextNonce returns a http handler that +func (as *Server) NextNonceHandler(remotePK cipher.PubKey) http.HandlerFunc { + // implementation ... +} +``` + +Take note that for the `(*Server).Wrap` function, we will need to define a custom `http.ResponseWriter` to obtain the status code (https://www.reddit.com/r/golang/comments/7p35s4/how_do_i_get_the_response_status_for_my_middleware/). + +The `httpauth.Client` implementation is responsible for providing logic for the following actions; + +- Keep a local record of the next expected *Security Nonce*. +- Adding security header values to a given request (`http.Request`). diff --git a/skywire-specs/specifications/03-Transport.md b/skywire-specs/specifications/03-Transport.md new file mode 100644 index 000000000..84c5aafdc --- /dev/null +++ b/skywire-specs/specifications/03-Transport.md @@ -0,0 +1,124 @@ +# Transport + +A *Transport* represents a bidirectional line of communication between two *Skywire Nodes* (or *Transport Edges*). + +Each *Transport* is represented as a unique 16 byte (128 bit) UUID value called the *Transport ID* and has a *Transport Type* that identifies a specific implementation of the *Transport*. + +A *Transport* has the following information associated with it; + +- **Transport ID:** A `uuid.UUID` value that uniquely identifies the Transport. +- **Edges:** The public keys of the Transport's edge nodes (should only have 2 edges and the initiating edge should come first). +- **Type:** A `string` value that specifies the particular implementation of the *Transport*. +- **Public:** A `bool` that specifies whether the *Transport* is to be registered in the *Transport Discovery* or not. Only public transports are registered. +- **Registered:** A `int64` value that is the epoch time of when the *Transport* is registered in *Transport Discovery*. A value of `0` represents the state where the *Transport* is not (or not yet) registered in the *Transport Discovery*. + +This is a JSON representation of a *Transport Entry*; + +```json +{ + "t_id": "e1808c316b23d1d6119cad1795238ff0", + "edges": ["031d796272349d597d6d3130497ccd11cf8af12c7d186b1726358abfb49edad0c1", "03bd9724f335c5eb5a1011e7862d4af28488102c8edffc84585cf0826ac4864b38"], + "type": "messaging", + "public": true +} +``` + +## Transport Module + +In code, `Transport` is an interface, and can have many implementations. + +The interface used to generate *Transports* of a certain *Transport Type* is named *Transport Factory* (represented by a `transport.Factory` interface in code). + +The representation of a *Transport* in *Transport Discovery* is of the type `transport.Entry`. + +A `transport.Status` type contains the status of a given *Transport*. Each *Transport Edge* provides such status, and the *Transport Discovery* compares the two statuses to derive the final status. + +```golang +package transport + +// Transport represents communication between two nodes via a single hop. +type Transport interface { + + // Read implements io.Reader + Read(p []byte) (n int, err error) + + // Write implements io.Writer + Write(p []byte) (n int, err error) + + // Close implements io.Closer + Close() error + + // Local returns the local transport edge's public key. + Local() cipher.PubKey + + // Remote returns the remote transport edge's public key. + Remote() cipher.PubKey + + // Type returns the string representation of the transport type. + Type() string + + // SetDeadline functions the same as that from net.Conn + // With a Transport, we don't have a distinction between write and read timeouts. + SetDeadline(t time.Time) error +} + +// Factory generates Transports of a certain type. +type Factory interface { + + // Accept accepts a remotely-initiated Transport. + Accept(ctx context.Context) (Transport, error) + + // Dial initiates a Transport with a remote node. + Dial(ctx context.Context, remote cipher.PubKey) (Transport, error) + + // Close implements io.Closer + Close() error + + // Local returns the local public key. + Local() cipher.PubKey + + // Type returns the Transport type. + Type() string +} + +// Entry is the unsigned representation of a Transport. +type Entry struct { + + // ID is the Transport ID that uniquely identifies the Transport. + ID uuid.UUID `json:"tid"` + + // Edges contains the public keys of the Transport's edge nodes (the public key of the node that initiated the transport should be on index 0). + Edges [2]string `json:"edges"` + + // Type represents the transport type. + Type string `json:"type"` + + // Public determines whether the transport is to be exposed to other nodes or not. + // Public transports are to be registered in the Transport Discovery. + Public bool `json:"public"` +} + +// SignedEntry holds an Entry and it's associated signatures. +// The signatures should be ordered as the contained 'Entry.Edges'. +type SignedEntry struct { + Entry *Entry `json:"entry"` + Signatures [2]string `json:"signatures"` + Registered int64 `json:"registered,omitempty"` +} + +// Status represents the current state of a Transport from the perspective +// from a Transport's single edge. Each Transport will have two perspectives; +// one from each of it's edges. +type Status struct { + + // ID is the Transport ID that identifies the Transport that this status is regarding. + ID uuid.UUID `json:"tid"` + + // IsUp represents whether the Transport is up. + // A Transport that is down will fail to forward Packets. + IsUp bool `json:"is_up"` + + // Updated is the epoch timestamp of when the status is last updated. + Updated int64 `json:"updated,omitempty"` +} +``` diff --git a/skywire-specs/specifications/04-Transport_Discovery.md b/skywire-specs/specifications/04-Transport_Discovery.md new file mode 100644 index 000000000..3055c02eb --- /dev/null +++ b/skywire-specs/specifications/04-Transport_Discovery.md @@ -0,0 +1,349 @@ +# Transport Discovery + +The Transport Discovery is a service that exposes a RESTful interface and interacts with a database on the back-end. + +The database stores *Transport Entries* that can be queried using their *Transport ID* or via a given *Transport Edge*. + +The process of submitting a *Transport Entry* is called *Registration* and a Transport cannot be deregistered. However, nodes that are an *Edge* of a *Transport*, can update their *Transport Status*, and specify whether the *Transport* is up or down. + +Any state-altering RESTful call to the *Transport Discovery* is authenticated using signatures, and replay attacks are avoided by expecting an incrementing security nonce (all communication should be encrypted with HTTPS anyhow). + +## Transport Discovery Procedures + +This is a summary of the procedures that the *Transport Discovery* is to handle. + +**Registering a Transport:** + +Technically, *Transports* are created by the Skywire Nodes themselves via an internal *Transport Factory* implementation. The *Transport Discovery* is only responsible for registering *Transports* in the form of a *Transport Entry*. + +When two Skywire Nodes establish a Transport connection between them, it is at first, unregistered in the *Transport Discovery*. The node that initiated the creation of the Transport (or the node that called the `(transport.Transport).Dial` method), is the node that is responsible for initiating the *Transport Settlement Handshake*. + +If two nodes; **A** and **B** establish a *Transport* between them (where **A** is the *Transport Initiator*), **A** is then also responsible for sending the first handshake packet for the *Transport Settlement Handshake*. The procedure is as follows: + +1. **A** sends **B** a proposed `transport.Entry` and also **A**'s signature of the Entry (in the form of `transport.SignedEntry`). + +2. **B** checks the `transport.SignedEntry` sent from **A**; + + 1. The `Entry.ID` field should be unique (check via *Transport Discovery*). + 2. The `Entry.Edges` field should be ordered correctly and contain public keys of **A** and **B**. + 3. The `Entry.Type` field should have the expected Transport Type. + 4. The `Signatures` field should contain **A**'s valid signature in the correct location (in the same index as **A**'s public key in `Entry.Edges`). + 5. The `Registered` field should be empty. + +3. **B** then adds it's only signature to the `transport.SignedEntry` and registers it to the *Transport Discovery*. Both public and private Transports are registered in the *Transport Discovery* (however only public *Transports* are publicly available). + +4. **B** then informs **A** on the success/failure of the registration, or just that the `transport.SignedEntry` is accepted by itself (depending on whether the Transport is to be public or not). + +**Submitting Transport Statuses:** + +If a given *Transport* is public, the associated *Transport Edges* is responsible for submitting their individual *Transport Statuses* to the *Transport Discovery* whenever the follow events occur; + +- Directly after a *Transport* is first successfully registered in the *Transport Discovery*. +- Whenever the *Transport* comes online/offline (connected/disconnected). + +**Obtaining Transports:** + +There are two ways to obtain transports; either via the assigned *Transport ID*, or via one of the *Transport Edges*. There is no restriction as who can access this information and results can be sorted by a given meta. + +## Security Procedures + +**Incrementing Security Nonce:** + +An *Incrementing Security Nonce* is represented by a `uint64` value. + +To avoid replay attacks and unauthorized access, each public key of a *Skywire Node* is assigned an *Incrementing Security Nonce*, and is expected to sign it with the rest of the body, and include the signature result in the http header. The *Incrementing Security Nonce* should increment every time an" endpoint is called (except for the endpoint that obtains the next expected incrementing security nonce). An *Incrementing Security Nonce* is not operation-specific, and increments every time any endpoint is called by the given Skywire Node. + +The *Transport Discovery* should store a table of expected next *Incrementing Security Nonce* for each public key of a *Skywire Node*. There is an endpoint `GET /security/nonces/{public-key}` that provides the next expected *Incrementing Security Nonce* for a given Node public key. This endpoint should be publicly accessible, but nevertheless, the *Skywire Nodes* themselves should keep a copy of their next expected *Incrementing Security Nonce*. + +The only times an *Incrementing Security Nonce* should not increment is when: + +- An invalid request is submitted (missing/extra fields, invalid signature). +- An internal server error occurs. + +Initially, the expected *Incrementing Security Nonce* should be 0. When it is this value, the *Transport Discovery* should not have an entry for it. + +Each operation should contain the following extra header entries: + +- `SW-Public` - Specifies the public key of the Skywire Node performing this operation. +- `SW-Nonce` - Specifies the incrementing nonce provided by this operation. +- `SW-Sig` - Specifies the hex-representation of the signature of the hash result of the concatenation of the *Incrementing Security Nonce* + Body of the request. + +If these values are not valid, the *Transport Discovery* should reject the request. + +## Code Structure + +The code should be in the `skywire-services` repository. + +- `/cmd/transport-discovery/transport-discovery.go` is the main executable for the *Transport Discovery*. +- `/pkg/transport-discovery/api/` contains the RESTFUL API definitions. +- `/pkg/transport-discovery/store/` contains the definition of the `Storer` interface and it's implementations. +- `/pkg/transport-discovery/client/` contains the client library that interacts with the *Transport Discovery* server's RESTFUL API. + +## Database + +The *Transport Discovery* should work with a variety of databases and the following interfaces should be defined for such implementations; + +- `TransportStorer` should store *Transport Signed Entries* and it's associated *Transport Statuses*. +- `NonceStorer` should store expected *Incrementing Nonces*. + +## Endpoint Definitions + +The following is a summary of all the *Transport Discovery* endpoints. + +- `GET /security/nonces/edge:` +- `GET /transports/id:` +- `GET /transports/edge:` +- `POST /transports` +- `POST /statuses` + +All endpoints should include an `Accept: application/json` field and the response header should include an `Content-Type: application/json` field. + +All requests (except for obtaining the next expected incrementing nonce) should include the following fields. + +``` +Accept: application/json +Content-Type: application/json +SW-Public: +SW-Nonce: +SW-Sig: +``` + +### GET Incrementing Security Nonce + +Obtains the next expected incrementing nonce for a given edge's public key. + +**Request:** + +``` +GET /security/nonces/ +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "edge": "", + "next_nonce": 0 + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### GET Transport Entry via Transport ID + +Obtains a *Transport* via a given *Transport ID*. + +Should only return a single `"transport"` result. + +**Request:** + +``` +GET /transports/id: +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "" + }, + "is_up": true, + "registered": 0 + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### GET Transport(s) via Edge Public Key + +Obtains *Transport(s)* via a given *Transport Edge* public key. + +**Request:** + +``` +GET /transports/edge: +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "entry": { + "t_id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "is_up": true, + "registered": 0 + }, + { + "entry": { + "t_id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "is_up": false, + "registered": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). + +### POST Register Transport(s) + +Registers one or multiple Transports. + +**Request:** + +``` +POST /transports +``` + +```json +[ + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ] + }, + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ] + } +] +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ], + "registered": 0 + }, + { + "entry": { + "id": "", + "edges": [ + "", + "" + ], + "type": "", + "public": true + }, + "signatures": [ + "", + "" + ], + "registered": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 401 Unauthorized (Invalid signature/nonce). +- 408 Request Timeout (Timed out). +- 500 Internal Server Error (Server error). + +### POST Status(es) + +Submits one or multiple *Transport Status(es)* from the perspective of the submitting node. The returned result is the final *Transport Status(es)* determined by the *Transport Discovery* that is generated using the submitted *Transport Status(es)* of the two edges. + +When a Transport is registered, it is considered to be *up*. Then after, every time a node's *Status* is submitted, the *Transport Discovery* alters the final state *Status* with the following rules: + +- If there is only one edge's *Status* submitted, the final status is of that of the submitted *Status*. +- If there are two *Status*es submitted and they both agree, final *Status* will also be the same. +- If the two submitted *Status*es disagree, then the final *Status* is always *Down*. + +**Request:** + +``` +POST /statuses +``` + +```json +[ + { + "id": "", + "is_up": true + }, + { + "id": "", + "is_up": true + } +] +``` + +**Responses:** + +- 200 OK (Success). + ```json + [ + { + "id": "", + "is_up": true, + "updated": 0 + }, + { + "id": "", + "is_up": false, + "updated": 0 + } + ] + ``` +- 400 Bad Request (Malformed request). +- 401 Unauthorized (Invalid signature/nonce). +- 408 Request Timeout (Timed out). +- 500 Internal Server Error (Server error). diff --git a/skywire-specs/specifications/05-Messaging_System.md b/skywire-specs/specifications/05-Messaging_System.md new file mode 100644 index 000000000..f8d8f4a62 --- /dev/null +++ b/skywire-specs/specifications/05-Messaging_System.md @@ -0,0 +1,514 @@ +# Messaging System + +The messaging system is an initial implementation of the `Transport` and associated interfaces. To work, the messaging system requires an active internet connection and is designed to be horizontally scalable. + +Three services make up the messaging system: *Messaging Client* (or *Client Instance*), *Messaging Server* (or *Service Instance*) and *Messaging Discovery*. + +*Messaging Clients* and *Messaging Servers* are represented by public/private key pairs. *Messaging Clients* deliver data to one another via *Messaging Servers* which act as relays. + +The *Messaging Discovery* is responsible for allowing *Messaging Clients* to find other advertised *Messaging Clients* via their public keys. It is also responsible for finding appropriate *Messaging Servers* that either "advertise" them, or "advertise" other *Messaging Clients*. + +``` + [D] + + S(1) S(2) + // \\ // \\ + // \\ // \\ + C(A) C(B) C(C) C(D) +``` + +Legend: +- ` [D]` - Discovery Service +- `S(X)` - Messaging Server (Server Instance) +- `C(X)` - Messaging Client (Client Instance) + +## Messaging System Modules + +There are two modules of the messaging system. + +- `messaging-discovery` contains the implementation of the *Messaging Discovery*. +- `messaging` contains the implementation of either a *Client Instance* or a *Server Instance* of a *Messaging Client* or *Messaging Server*. + +## Messaging Procedures + +This is a summary of the procedures that the *Messaging System* is to handle. + +**Advertising a client:** + +To be discoverable by other clients, a client needs to advertise itself. + +1. Client queries the Discovery to find available Servers. +2. Client connects to some (or all) of the suggested Servers. +3. Client updates it's own record in Discovery to include it's delegated Servers. + +**Client creates a channel to another client:** + +In order for two clients to communicate, both clients need to be connected to the same messaging server, and create a channel to each other via the server. + +1. Client queries discovery of the remote client's connected servers. The client will connect to one of these servers if it originally has no shared servers with the remote. +2. The client sends a `OpenChannel` frame to the remote client via the shared server. +3. If the remote client accepts, a `ChannelOpened` frame is sent back to the initiating client (also via the shared server). A channel is represented via two *Channel IDs* (between the initiating client and the server, and between the responding client and the server). The associated between the two channel IDs is defined within the server. +4. Once a channel is created, clients can communicate via one another via the channel. + +## Messaging Discovery + +The *Messaging Discovery* acts like a DNS for messaging instances (*Messaging Clients* or *Messaging Servers*). + +### Instance Entry + +An entry within the *Messaging Discovery* can either represent a *Messaging Server* or a *Messaging Client*. The *Messaging Discovery* is a key-value store, in which entries (of either server or client) use their public keys as their "key". + +The following is the representation of an Entry in Golang. + +```golang +// Entry represents an Instance's entry in the Discovery database. +type Entry struct { + // The data structure's version. + Version string `json:"version"` + + // A Entry of a given public key may need to iterate. This is the iteration sequence. + Sequence uint64 `json:"sequence"` + + // Timestamp of the current iteration. + Timestamp int64 `json:"timestamp"` + + // Public key that represents the Instance. + Static string `json:"static"` + + // Contains the node's required client meta if it's to be advertised as a Messaging Client. + Client *Client `json:"client,omitempty"` + + // Contains the node's required server meta if it's to be advertised as a Messaging Server. + Server *Server `json:"server,omitempty"` + + // Signature for proving authenticity of of the Entry. + Signature string `json:"signature,omitempty"` +} + +// Client contains the node's required client meta, if it is to be advertised as a Messaging Client. +type Client struct { + // DelegatedServers contains a list of delegated servers represented by their public keys. + DelegatedServers []string `json:"delegated_servers"` +} + +// Server contains the node's required server meta, if it is to be advertised as a Messaging Server. +type Server struct { + // IPv4 or IPv6 public address of the Messaging Server. + Address string `json:"address"` + + // Port in which the Messaging Server is listening for connections. + Port string `json:"port"` + + // Number of connections still available. + AvailableConnections int `json:"available_connections"` +} +``` + +**Definition rules:** + +- A record **MUST** have either a "Server" field, a "Client" field, or both "Server" and "Client" fields. In other words, a Messaging Node can be a Messaging Server Node, a Messaging Client Node, or both a Messaging Server Node and a Messaging Client Node. + +**Iteration rules:** + +- The first entry submitted of a given static public key, needs to have a "Sequence" value of `0`. Any future entries (of the same static public key) need to have a "Sequence" value of `{previous_sequence} + 1`. +- The "Timestamp" field of an entry, must be of a higher value than the "Timestamp" value of the previous entry. + +**Signature Rules:** + +The "Signature" field authenticates the entry. This is the process of generating a signature of the entry: +1. Obtain a JSON representation of the Entry, in which: + 1. There is no whitespace (no ` ` or `\n` characters). + 2. The `"signature"` field is non-existent. +2. Hash this JSON representation, ensuring the above rules. +3. Create a Signature of the hash using the node's static secret key. + +The process of verifying an entry's signature will be similar. + +### Store Interface + +The underlying database of the *Messaging Discovery* is a key-value store. The `Store` interface allows many databases to be used with the *Messaging Discovery*. + +```golang +type Store interface { + // Entry obtains a single messaging instance entry. + // 'static' is a hex representation of the public key identifying the messaging instance. + Entry(ctx context.Context, static string) (*Entry, error) + + // SetEntry set's an entry. + // This is unsafe and does not check signature. + SetEntry(ctx context.Context, entry *Entry) error + + // AvailableServers discovers available messaging servers. + // Obtains at most 'maxCount' amount of available servers obtained randomly. + AvailableServers(ctx context.Context, maxCount int) ([]*Entry, error) +} +``` + +### Endpoints + +Only 3 endpoints need to be defined; Get Entry, Post Entry, and Get Available Servers. + +#### GET Entry +Obtains a messaging node's entry. +> `GET {domain}/discovery/entries/{public_key}` + +**REQUEST** + +Header: +``` +Accept: application/json +``` + +**RESPONSE** + +Possible Status Codes: +- Success (200) - Successfully updated record. + - Header: + ``` + Content-Type: application/json + ``` + - Body: + > JSON-encoded entry. +- Not Found (404) - Entry of public key is not found. +- Unauthorized (401) - invalid signature. +- Internal Server Error (500) - something unexpected happened. + +#### POST Entry +Posts an entry and replaces the current entry if valid. +> `POST {domain}/discovery/entries` + +**REQUEST** + +Header: +``` +Content-Type: application/json +``` +Body: +> JSON-encoded, signed Entry. + +**RESPONSE** + +Possible Response Codes: +- Success (200) - Successfully registered record. +- Unauthorized (401) - invalid signature. +- Internal Server Error (500) - something unexpected happened. + +#### GET Available Servers +Obtains a subset of available server entries. +> `GET {domain}/discovery/available_servers` + +**REQUEST** + +Header: +``` +Accept: application/json +``` + +**RESPONSE** + +Possible Status Codes: +- Success (200) - Got results. + - Header: + ``` + Content-Type: application/json + ``` + - Body: + > JSON-encoded `[]Entry`. +- Not Found (404) - No results. +- Forbidden (403) - When access is forbidden. +- Internal Server Error (500) - Something unexpected happened. + +### Messaging Discovery Client Library + +The module is named `client`. It contains a `HTTPClient` structure, that defines how the client will interact with the *Messaging Discovery* API. + +A new `HTTPClient` object can be instantiated using the public function `New(address string)`. + +It exposes the following public methods: + +```go +// Entry retrieves an entry associated to the given public key from the discovery server. +func (*HTTPClient) Entry(ctx context.Context, static string) (*Entry, error) { + // definition ... +} + +// SetEntry tries to set the given entry on the discovery server. It must be signed. +// If the entry is modifying a previous one, must be signed by the same private key. +func (*HTTPClient) SetEntry(ctx context.Context, entry *Entry) error { + // definition ... +} + +// AvailableServers gets a list of server entries from the skywire discovery server. +// The amount is determined by the discovery server. +func (*HTTPClient) AvailableServers(ctx context.Context) ([]*Entry, error) { + // definition ... +} +``` + +The module also provides public functions to instantiate valid `Entry` objects. + +### Messaging Discovery Integration Tests + +> **TODO:** Fix wording. + +This package does not uses another `messenger` package, so integration tests are defined for the external services that `discovery` is using. In this case, the external store and `discovery` itself for testing the `client` library. + +The cases for the store integration testing: + + 1. Its able to set an entry on the database without error by calling the `storer.SetEntry` method. + 2. Its able to retrieve the previously set entry by calling the `storer.Entry` method. + 3. Creates multiple service entries and store them by calling `storer.SetEntry`, then it should be able to retrieve them with `store.AvailableServers`. + 4. `store.AvailableServers` receives a `maxCount int` argument. We also test it passing an integer which value is less than the amount of server entries we have set in the database, it should return this exact amount of server entries. + 5. Same as in number 4, but we set `maxCount` to number bigger than the number of server entries we have set, now we should get an slice of the size of the server entries we have set. + +In order to run the test we preferably create a clean new instance of the store database using Docker, and the test code should connect to it. We remove it after we have tested. + +In order to test the client library we do integration test with an instance of the discovery server. +The test cases for the client integration testing: + +1. By using the method SetEntry the client can set a new entry on the discovery server. +2. By using the method SetEntry the client can update a previously set entry on the discovery server. +3. If using SetEntry to update a previously set Entry, but the new sequence is not the previous sequence + 1 it should return an error with status code 500, Something unexpected happened. +4. If using SetEntry to update a previously set Entry, but the signature of the new entry has been made by a different secret key it should return an error with status code 401, Invalid signature. +5. By calling the method Entry with the public key of a previously set Entry it should return that entry. +6. By calling the method Entry with the public key of a previously non-set Entry it should return an error with code 404, Entry of public key is not found. +7. By calling the method AvailableServers when there are previously set server entries it should return them. + +## Messaging Link + +The `link` provides two *Messaging Instances* a means to establish a connection with one another, and also handle a pool of connections. + +Using the *Messaging Discovery*, a *Messaging Instance* can discover other instances via only their public key. However, a *Link* requires both a public key and an address:port. + +Data sent via a *Link* is encapsulated in *Frames*. A *Link* is implemented using a TCP connection. + +### Link Handshake Frames + +When setting up a *Link* between two instances, the instance that initiates is called the *Initiator* and the instance that responds is called the *Responder*. Each instance is represented by a public key. + +To set up a *Link*, the *Initiator* first dials a TCP connection to the listening *Responder*. Once the TCP connection is established, the *Responder* sends the first *Frame*. It is expected that the *Initiator* knows the public key of the *Responder* + +Given a situation where instances 'A' and 'B' are to establish a link with one another (where 'A' is the initiator), the following *Frames* are delivered to perform a handshake. + +Link Handshake Frames are to be in JSON format. + +**Link Handshake Frame 1 (A -> B):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779" +} +``` + +The initiator is responsible for sending the first frame. +- `"version"` specifies the version of messaging protocol that the initiator is using (`"0.1"` for now). +- `"initiator"` should contain the hex representation of the public key of the initiator (the instance that is sending the first handshake frame). +- `"responder"` should contain the hex representation of the public key of the expected responder (the responder should disconnect TCP if this is not their public key). +- `"nonce"` is the hex-string representation of a 16-byte nonce that the responder should sign (alongside the initiator's public key) to check authenticity of the responder and whether the responder. + +**Link Handshake Frame 2 (B -> A):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01" +} +``` + +This frame allows the responder agree with the initiator and prove it's ownership of it's claimed public key. + +The `"sig1"` field contains a hex representation of the result of signing the concatenation of the version, initiator, responder and nonce fields. Note that before concatenation, hex representations should be decoded and the concatenation result needs to be hashed before being signed. `"sig1"` should be signed by the responder. + +**Link Handshake Frame 3 (A -> B):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01", + "sig2": "fc17928d5a3f7691434282fb3108d1603889f996e8e45adc2e35362e08009b8611abb9f45e511b9931f0f04b37ff1057fd69554befe534ad28c77ff0c44121ab00" +} +``` + +This frame allows the initiator to inform the responder that `"sig1"` is accepted, and to prove the initiator's ownership of it's public key. + +`"sig2"` is the signature result of the concatenation of the version, initiator, responder, nonce and sig1 fields. Concatenation rules are the same as that of `"sig1"`. + +**Link Handshake Frame 4 (B -> A):** + +```json +{ + "version": "0.1", + "initiator": "036b630436972743d4b3f5bb39cd451da29d222b5d30893684a40f34a66c692157", + "responder": "02e18998f0174631710e47052927e13bddc712ca0c11289e0150fbf57570e31151", + "nonce": "853ea8454d11bd3b59cb31f8572a3779", + "sig1": "df8a978f0ea681e218cfd8127692dbe4190441567181b9057ab15da34b08ff610d9060e5195419e1744bb57d50373c1dd444b5c2753a80dba32b292fa306e9df01", + "sig2": "fc17928d5a3f7691434282fb3108d1603889f996e8e45adc2e35362e08009b8611abb9f45e511b9931f0f04b37ff1057fd69554befe534ad28c77ff0c44121ab00", + "accepted": true +} +``` + +Sent by the responder, this frame concludes the handshake if the value of `"accepted"` is `true`. + +### Messaging Frames + +After the handshake phase, frames have a reoccurring format. These are the *Messaging Frames* of the messaging system. + +``` +| FrameType | PayloadSize | Payload | +| 1 byte | 2 bytes | ~ bytes | +``` + +- The `Type` specifies the frame type. Different frame types are used for opening and closing channels as well as sending packets via the channels. +- `PayloadSize` contains an encoded `uint16` value that represents the Payload's length (the max size is 65535). +- `Payload` has a length determined by `PayloadSize`. + +The following is a summary of the frame types. + +| FrameTypeValue | FrameTypeName | FrameBody | +| -------------- | ------------- | --------- | +| `0x0` | `OpenChannel` | ChannelID + RemoteStatic + NoiseMessage1 | +| `0x1` | `ChannelOpened` | ChannelID + NoiseMessage2 | +| `0x2` | `CloseChannel` | ChannelID | +| `0x3` | `ChannelClosed` | ChannelID | +| `0x4` | `Send` | ChannelID + CipherText | + +The `FrameBody` has the following sub-fields. A `FrameBody` with multiple sub-fields have the sub-fields concatenated. + +- The `ChannelID` sub-field is represented by a single byte. This restricts a *Client Instance* to have at most 256 channels via a single *Server Instance*. +- The `RemoteStatic` sub-field is represented by 33 bytes. It contains a public key of a remote *Client Instance*. +- `NoiseMessage1` and `NoiseMessage2` are both represented by 49 bytes. It contains the noise handshake messages for establishing symmetric encryption between the two client instances of the channel. The noise handshake pattern used is KK. +- The `CipherText` sub-field is the only sub-field with a modular length. It contains size of the encrypted payload followed by payload that is to be delivered. + +### Noise Implementation in Channels + +As stated above, a channel is established using the `OpenChannel` and `ChannelOpened` frames. Then, after a channel is established, the two *Client Instances* of the channel can communicate with each over via `Send` frames (which includes a `CipherText` component). + +The protocol used to establish the symmetric encryption of the `CipherText` is the [Noise Protocol](http://noiseprotocol.org/). + +The curve used will be `secp256k1` for the key pair, and `chacha20poly1305` will be used for the symmetric encryption itself. + +Note that, the noise protocol requires the public key length and the ECDH result length (shared secret) to be equal. Because for `secp256k1`, public keys have a length of 33, and the ECDH result has a length of 32, so an empty byte (`0x0`) should be appended to all generated ECDH results. Hence, the `DHLEN` constant for the noise protocol should be 33. + +After the handshake, the CipherState object will be used by the *Client Instances* to encrypt and decrypt the `CipherText` contained within the `Send` frame. + +**Handshake pattern:** + +Only the `KK` [interactive handshake pattern (fundamental)](http://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) will be supported. + +``` +-> s +<- s +... +-> e, es, ss +<- e, ee, se +``` + +The `-> e, es, ss` message is the `NoiseMessage1` of a `OpenChannel` frame, while the `<- e, ee, se` message is the `NoiseMessage2` of a `ChannelOpened` frame. + +### Implementation in Code + +Within the `messaging` module: + +- `Link` structure should represent a link between two instances. +- `Pool` structure should handle multiple `Links` (with different instances). +- `Client` which implements a *Client Instance*. +- `Server` which implements a *Server Instance*. + +*Client Instances* communicate with each other via a *Server Instance* (which acts as a relay). + +Both structs will use `link.Pool` to handle links, but *Frames* are handled differently. *Client Instances* are to implement `TransportFactory` while a *Server Instance* is not required to. A *Client Instance* should also represent an established *Channel* as a `Transport` implementation. + +### Configuring an Instance + +When creating an *Instance*, the following options should be available via the following structure. + +```golang +// Config configures an instance. +type Config struct { + // Public determines whether the instance is to advertise itself to the messaging discovery servers. + Public bool + + // DiscoveryAddresses contains the messaging discovery services to be used (in order of preference). + DiscoveryAddresses []string +} +``` + +The above structure is to be an input when creating a *Server Instance* or a *Client Instance*. + +### Instance Interaction with Messaging Discovery + +On startup `Server` that is supposed to be publicly available should register itself in messaging discovery. To do so it first has to fetch current version of an `Entry`, if entry doesn't exist it should create one. If entry exists it may update it if necessary. + +On startup `Client` may connect to necessary amount of servers by fetching list of available servers from the discovery. Once initial connections are established `Client` should update discovery entry to advertise it's relays. + +### Channel Management + +The following is a proposal of how a Channel can be represented in code. + +```golang +// Channel represents a channel that implements Transport. It can be from the perspective of a Server or Client Instance. +type Channel struct { + // ChannelID represents the ID that is associated of the adjacent link. + ChannelID uint8 + + // Destination is the public key of the instance that is the final destination. + // This should always contain the public key of a client instance (as a server cannot be the final destination). + Destination cipher.PubKey + + // Link contains the adjacent link of the channel. + Link *link.Link +} +``` + +Both the client and server instances needs to manage channels. Channels are associated with a channel ID and also the public key(s) of the remote instances that the channel interacts with. Channels are hence identified by *Link* + *Channel ID*. + +From the perspective of a *Client Instance*, the assignment of *Channel IDs* are unique to a given link with a *Server Instance*. For example, let's say client 'A' is connected with server 'B' and server 'C', hence we have links 'AB' and 'AC'. We can have 'AB' and 'AC' share the same Channel ID, but because the channel itself is associated with a different link, they are considered different channels. + +From the perspective of a *Server Instance*, the assignment of *Channel IDs* are unique to a given link with a *Client Instance*. + +### Opening a Channel + +A channel in it's entirety handles the communication between two client instances via a server instance (which acts as a relay). Within the link between a single client instance and the server instance, a channel is represented using a *Channel ID*. The *Channel ID* of the two *links* of the same "channel" can be different, and the *Server Instance* is responsible for recording this association of the *Channel IDs* (coupled with the client instance's public key). + +When a *Client Instance* wishes to communicate with another *Client Instance*, it is responsible for initiating the creation of a channel. To do so, t sends a `OpenChannel` frame to the *Server Instance* in which: + +- `ChannelID` contains a ChannelID that the client wishes to associate with the channel. +- `RemoteStatic` contains the public key of the remote *Client Instance* that the local client wishes to communicate via this channel. +- `NoiseMessage1` is the first noise handshake message (the handshake pattern used is KK). + +If the *Server Instance* wishes to reject the request to open channel, it can send a `ChannelClosed` frame back to the initiating client with the `ChannelID` sub-field containing the value of the channel ID suggested by the initiating client. + +If the *Server Instance* wishes to go forward with opening of a channel, it sends a `OpenChannel` frame to the second *Client Instance*, in which `ChannelID` is an ID that's unique between the server and the second client and public key of the first client. + +If the second *Client Instance* wishes to reject the request, it can send a `ChannelClosed` frame back to the server, and the server can subsequently send a `ChannelClosed` frame to the initiating client (the `ChannelID` sub-fields of these `ChannelClosed` frames should be the unique channel IDs of the associated links). + +If the second *Client Instance* accepts the request, it sends a `ChannelOpened` back to the *Server Instance* (with the `NoiseMessage2`). Subsequently, the *Server Instance* sends a `ChannelOpened` back to the initiating client (the `ChannelID` sub-fields of these `ChannelOpened` frames should be the unique channel IDs of the associated links). + +### Closing a Channel + +A *Client Instance* can safely close any of it's channels by sending a `CloseChannel` (with the associated `ChannelID`) to the *Server Instance*. + +After a *Client Instance* sends a `CloseChannel`, no more frames are to be sent by that instance. However, the remote instance can still send frames until it receives the `CloseChannel` to it. The "close-responding" client then sends a `ChannelClosed` instance back to the "close-initiating" client. Once the `ChannelClosed` channel is sent by the "close-responding" client, it will no longer send or receive frames. Once the "close-initiator" receives the `ChannelClosed` frame. it will no longer receive frames. + +In summary, + +- When a client instance sends a `CloseChannel` frame, the channel is "partially-closed" and the client instance will only receive and not send via the channel. If a `ChannelClosed` frame is not received after a given timeout, the channel sends a `ChannelClosed` itself and the channel is "fully-closed". +- When a client instance receives a `CloseChannel` frame, it delivers a `ChannelClosed` frame and the channel is "fully-closed" and the client will no longer receive or send via the channel. +- When a client instance receives a `ChannelClosed` frame, the channel is "fully-closed". + +### Handling Disconnections + +In any given situation, there may be a possibility that the *Server Instance* unexpectedly disconnects with a *Client Instance*, or that a *Client Instance* unexpectedly disconnects with a *Server Instance*. This should directly affect the channels associated with the *Link* in question. + +When a *Client Instance* detects that a *Server Instance* has disconnected from it. All associated channels with that *Server Instance* should be closed. When a channel closes, the associated *Transport* should also be closed. + +When a *Server Instance* detects a disconnection from a *Client Instance*, it should send a `ChannelClosed` frame to all the other *Client Instances* that shares a channel with the disconnected client. After so, the *Server Instance* should dissociate all relations with the closed channels. diff --git a/skywire-specs/specifications/06-Packets.md b/skywire-specs/specifications/06-Packets.md new file mode 100644 index 000000000..70b92d643 --- /dev/null +++ b/skywire-specs/specifications/06-Packets.md @@ -0,0 +1,212 @@ +# Packets + +The *Node Module* handles data encapsulated within data units called *Packets*. *Packets* can be grouped within the following categories based on their use-case; + +- ***Settlement Packets*** are used by the *Transport Manager* to "settle" Transports. Settlement, allows the two nodes that are the edges of the transport to decide on the *Transport ID* to be used, and whether the Transport is to be public. Only after a *Transport* is settled, can the *Router* have access to the Transport. + + *Settlement Packets* contain `json` encoded payload. + +- ***Foundational Packets*** are used by a *Router* to communicate with a remote *Setup Node* and is used for setting up, establishing and destroying routes. + + *Foundational Packets* are prefixed by 3 bytes: the packet size (2 bytes) and a Type (1 byte) that contains the foundational packet type. + +- ***Data Packets*** are Packets that are actually used to encapsulate data delivered between two Apps. + + *Data Packets* are prefixed by 6 bytes; including the packet size (2 bytes) and the Route ID (4 bytes) which can have any value other than `0x00` or `0x01`. + +- ***Loopback Packets*** are packets that are consumed locally by the node. + + *Loopback Packets* are structurally similar to data packets but their Route ID links to a rule that specifies which app to forward the packet to. + +## Settlement Packets + +After a Transport is established between two nodes, the nodes needs to decide on the *Transport ID* that describes the Transport and whether the Transport is to be public or private (public Transports are to be registered in the *Transport Discovery*). This process is called the *Settlement Handshake*. + +The Packets of this handshake contain `json` encoded messages. + +*Settlement Handshake* packets do not need a field for Packet-type are they are expected in a specific order. + +- Request to settle transport is sent by the *Transport Initiator* to the *Transport Responder* after a *Transport* connection is established. + + JSON Body: Contains a `transport.SignedEntry` structure with the *Transport Initiator*'s signature. + +- *Transport Responder* should validate submitted `transport.SignedEntry`, and if entry is valid it should add sign it and perform transport registration in transport discovery. If registration was successful responder should send updated `transport.SignedEntry` back to initiator. + + JSON Body: Contains a `transport.SignedEntry` structure with signatures from both the *Transport Initiator* and the *Transport Responder*. If the transport is registered in *Transport Discovery*, the `SignedTransport.Registered` should contain the epoch time of registration. + +If transport will fail at any step participants can chose to stop handshake procedures and close corresponding transport. Transport disconnect during the handshake should be handled appropriately by participants. Optional handshake timeout should also be supported. + +## Foundational Packets + +Foundational packets are used for the communication between *App Nodes* and *Setup Nodes*. + +The *Setup Node* is responsible for fulfilling Route initiating and destroying requests by communicating with the initiating, responding and intermediate nodes of the proposed route. + +The following is the expected format of a Foundational Packet; + +``` +| Packet Len | Type | JSON Body | +| 2 bytes | 1 byte | ~ | +``` + +- ***Packet Len*** specifies the total packet length in bytes (exclusive of the *Packet Len* field). +- ***Type*** specifies the *Foundational Packet Type*. +- ***JSON Body*** is the packet body (in JSON format) that is unique depending on the packet type. + +**Foundational Packet Types Summary:** + +| Type | Name | +| ---- | ---- | +| 0x00 | `AddRules` | +| 0x01 | `RemoveRules` | +| 0x02 | `CreateLoop` | +| 0x03 | `ConfirmLoop` | +| 0x04 | `CloseLoop` | +| 0x05 | `LoopClosed` | +| 0xfe | `ResponseFailure` | +| 0xff | `ResponseSuccess` | + +### `0x00 AddRules` + +Sent by the *Setup Node* to all *Nodes* of the route. This packet informs nodes what rules are to be added to their internal routing table. + +**JSON Body:** + +```json +[, ] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + [, ] + ``` + +### `0x01 RemoveRules` + +Sent by the *Setup Node* to *Node* of the route. + +**JSON Body:** + +```json +["", "rid-2"] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + [, ] + ``` + +### `0x02 CreateLoop` + +Sent by the *Route Initiator* to a *Setup Node* to have a *Loop* created. + +**JSON Body:** + +```json +{ + "local-port": , + "remote-port": , + "forward": [ + { + "from": "", + "to": "", + "tid": "" + }, + { + "from": "", + "to": "", + "tid": "" + } + ], + "reverse": [ + { + "from": "", + "to": "", + "tid": "" + }, + { + "from": "", + "to": "", + "tid": "" + } + ], + "expiry": "" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x3 ConfirmLoop` + +Sent by the *Setup Node* to Responder and Initiator *Node* to confirm notify about route in opposite direction. + +**JSON Body:** + +```json +{ + "remote-pk": "", + "remote-port": , + "local-port": , + "resp-rid": +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x4 CloseLoop` + +Sent by a Responder or Initiator *Node* to a *Setup Node* to notify about closing a loop locally. + +**JSON Body:** + +```json +{ + "port": "", + "remote": { + "port": , + "pk": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + +### `0x5 LoopClosed` + +Sent by a *Setup Node* to a Responder or Initiator to notify about closed loop on the opposite end. + +**JSON Body:** + +```json +{ + "port": "", + "remote": { + "port": , + "pk": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty payload. + + +## Data Packets + +The follow is the structure of a *Data Packet*. + +``` +| Packet Len | Route ID | Payload | +| 2 bytes | 4 bytes | ~ | +``` diff --git a/skywire-specs/specifications/07-Transport_Management.md b/skywire-specs/specifications/07-Transport_Management.md new file mode 100644 index 000000000..e2e9be976 --- /dev/null +++ b/skywire-specs/specifications/07-Transport_Management.md @@ -0,0 +1,113 @@ +# Transport Management + +For all Skywire Node types, we need a universal way for managing and logging Transports. The structure that is responsible for this is the `TransportManager` (which should be within the `/pkg/node` package of the `skywire` package). + +As the `TransportManager` needs to interact with the *Transport Discovery* and other Skywire Nodes, it should have access to the local node's public and private key identity. + +The following is a proposed implementation of `TransportManager`; + +```golang +package node + +// Transport wraps a 'transport.Transport' implementation and contains +// associated/useful for the 'transport.Transport' implementation. +type Transport struct { + transport.Transport + ID uuid.UUID + // more fields ... +} + +// TransportManagerConfig configures a TransportManager. +type TransportManagerConfig struct { + PubKey cipher.PubKey // Local PubKey + SecKey cipher.SecKey // Local SecKey + DiscoveryClient client.Client // Transport discovery client + LogStore TransportLogStore // Store for transport's transfer rates +} + +// TransportManager manages Transports. +type TransportManager struct { + // Members... +} + +// NewTransportManager creates a TransportManager with the provided configuration and transport factories. +// 'factories' should be ordered by preference. +func NewTransportManager(config *TransportManagerConfig, factories ...transport.Factory) (*TransportManager, error) { /* ... */ } + +// Start starts the transport manager. +// - 'ctx' can end the transport listening operation. +func (tm *TransportManager) Serve(ctx context.Context) error { /* ... */ } + +// Observe returns channel for notifications about new Transport +// registration. Only single observer can listen for on a channel. +func (tm *TransportManager) Observe() <-chan *Transport { /* ... */ } + +// Factories returns all the factory types contained within the TransportManager. +func (tm *TransportManager) Factories() []string { /* ... */ } + +// Transport obtains a Transport via a given Transport ID. +func (tm *TransportManager) Transport(id uuid.UUID) (*Transport, bool) { /* ... */ } + +// RangeTransports ranges all Transports. +// Should return when 'action' returns a non-nil error. +func (tm *TransportManager) RangeAllTransports(action TransportAction) error { /* ... */ } + +// CreateTransport begins to attempt to establish transports to the given 'remote' node. +// This should be a non-blocking operation and any failures or future Transport disconnections +// should be dealt with with retries (under a given time interval). +// - 'remote' specifies the remote node to attempt to establish the Transports with. +// - 'tpType' is the transport type that is to be created. +// - 'public' determines whether the Transports established should be advertised to Transport Discovery. +// If a transport is not to be public, a random transport ID is assigned. +func (tm *TransportManager) CreateTransport(ctx context.Context, remote cipher.PubKey, tpType string, public bool) (*Transport, error) { /* ... */ } + +// DeleteTransport disconnects and removes the Transport of Transport ID. +func (tm *TransportManager) DeleteTransport(id uuid.UUID) error { /* ... */ } +``` + +## Transport Manager Procedures + +The transport manager is responsible for keeping track of established transports (via the `transport.Entry` and the `transport.Status` structures). The `transport.Entry` structure describes and identifies transports, while `transport.Status` keeps track of whether the transport is up or down (based on the perspective of the local node). + +If the *Transport Manager* wishes to confirm transport information, it can query the *Transport Discovery* via the `GET /transports/edge:` endpoint. Note that it is expected of the *Transport Manager* to call this endpoint on startup. + +When a transport is "closed" it is only considered "down", not "destroyed". + +The following highlights detailed startup and shutdown procedures of a *Transport Manager*; + +**Startup:** + +On startup, the `TransportManager` should call the *Transport Discovery* to ensure that it is up to date. Then it needs to attempt to establish (or re-establish) transports to the relevant remote nodes. + +When re-establishing a Transport, the `transport.Entry` used should be that also previously stored in the *Transport Discovery*. + +Once connected, the `TransportManager` should update it's *Status* of the given Transport and set `is_up` to `true`. + +The startup logic is triggered when `Start` is called. + +**Shutdown:** + +On shutdown, the first step is to update the *Transport Statuses* to "down" via the *Transport Discovery*. Then Transports to remote nodes is to be closed (with a timeout, in which after, the transport in question is forcefully closed). + +## Logging + +A *Transport Manager* is responsible for logging incoming and outgoing communication for each transport. Initially, only the total incoming and outgoing bandwidth (in bytes) is to be logged per transport. + +```golang +// TransportLogEntry represents a logging entry for a given Transport. +// The entry is updated every time a packet is received or sent. +type TransportLogEntry struct { + ReceivedBytes big.Int // Total received bytes. + SentBytes big.Int // Total sent bytes. +} +``` + +Logs for each transport is to be stored using `TransportLogStore`. `TransportLogStore` is to be specified within `TransportManagerConfig`. + +```golang +// TransportLogStore stores transport log entries. +type TransportLogStore interface { + Entry(id uuid.UUID) (*TransportLogEntry, error) + Record(id uuid.UUID, entry *TransportLogEntry) error +} +``` diff --git a/skywire-specs/specifications/08-Route_Finder.md b/skywire-specs/specifications/08-Route_Finder.md new file mode 100644 index 000000000..b0c42df13 --- /dev/null +++ b/skywire-specs/specifications/08-Route_Finder.md @@ -0,0 +1,89 @@ +# Route Finder + +The *Route Finder* (or *Route Finding Service*) is responsible for finding and suggesting routes between two Skywire Nodes (identified by public keys). It is expected that an *App Node* is to use this service to find possible routes before contacting the *Setup Node*. + +In the initial version of the *Route Finder*, it should use a basic algorithm to choose and order the best routes. This algorithm should find the x amount (limited by the max routes parameter) of "fastest" routes determined by the least amount of hops needed, and order it by hops ascending. + +The implementation of *Route Finder* requires only a single REST API endpoint. + +## Graph Algorithm + +In order to explore routing we need to create a graph that represents the current skywire network, or at least the network formed by all the reachable nodes by `source node`. + +For this purpose, we use the `mark and sweep` algorithm. Such an algorithm consists of two phases. + +In the first phase, every object in the graph is explored in a `Deep First Search` order. This means that we need to explore every transport starting from route node, accessing the `transport-discovery` database each time that we need to retrieve new information. + +In the second phase, we remove nodes from the graph that have not been visited, and then mark every node as unvisited in preparation for the next iteration of the algorithm. + +An explanation and implementation of this algorithm can be found [here](https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/). + +## Routing algorithm + +Given the previous graph we can now explore it to find the best routes from every given starting node to destiny node. + +For this purpose we use a modification of `Dijkstra algorithm`. + +An implementation can be found [here](http://rosettacode.org/wiki/Dijkstra%27s_algorithm#Go). + +Route-finder modifies this algorithm by keeping track of all the nodes that reached to destination node. This allows the ability to backtrack every best route that arrives from a different node to destination node. + +## Code Structure + +The code should be in the `watercompany/skywire` repository; + +- `/cmd/route-finder/route-finder.go` is the main executable for the *Route Finder*. +- `/pkg/route-finder/api/` contains the RESTFUL API definitions. +- `/pkg/route-finder/store/` contains the definition of the `Storer` interface and it's implementations [**TODO**]. +- `/pkg/route-finder/client/` contains the client library that interacts with the *Route Finder* service's RESTFUL API. + +## Database + +The *Route Finder* only accesses the Transport database already defined in the *Transport Discovery* specification. + +## Endpoint Definitions + +All endpoint calls should include an `Accept: application/json` field in the request header, and the response header should include an `Content-Type: application/json` field. + +### GET Routes available for the defined start and end key + +Obtains the routes available for a specific start and end public key. Optionally with custom min and max hop parameters. + +Note that each transport is to be represented by the `transport.Entry` structure. + +**Request:** + +``` +GET /routes +``` + +```json +{ + "src_pk": "", + "dst_pk": "", + "min_hops": 0, + "max_hops": 0, +} +``` + +**Responses:** + +- 200 OK (Success). + ```json + { + "routes": [ + { + "transports": [ + { + "tid": "", + "edges": ["", ""], + "type": "", + "public": true, + } + ] + } + ] + } + ``` +- 400 Bad Request (Malformed request). +- 500 Internal Server Error (Server error). diff --git a/skywire-specs/specifications/09-Route_Setup_Process.md b/skywire-specs/specifications/09-Route_Setup_Process.md new file mode 100644 index 000000000..85218ffdb --- /dev/null +++ b/skywire-specs/specifications/09-Route_Setup_Process.md @@ -0,0 +1,9 @@ +# Route Setup Process + +1. Route paths are uni-directional. So, the whole route between 2 visors consists of forward and reverse paths. *Setup node* receives both of these paths in the routes setup request. +2. For each node along both paths *Setup node* calculates how many rules are to be applied. +3. *Setup node* connects to all the node along both paths and sends `ReserveIDs` request to reserve available rule IDs needed to setup the route. +4. *Setup node* creates rules the following way. Let's consider visor A setting up route to visor B. This way we have forward path `A->B` and reverse path `B->A`. For forward path we create `Forward` rule for visor `A`, `IntermediaryForward` rules for each node between `A` and `B`, and `Consume` rule for `B`. For reverse path we create `Forward` rule for visor `B`, `IntermediaryForward` rules for each visor between `B` and `A`, and `Consume` rule for `A`. +5. *Setup node* sends all the created `IntermediaryForward` rules to corresponding visors to be applied. +6. *Setup node* sends `Consume` and `Forward` rules to visor `B` (remote in our case). +7. *Setup node* sends `Forward` and `Consume` rules to visor `A` in response to the route setup request. diff --git a/skywire-specs/specifications/10-Routing_Table.md b/skywire-specs/specifications/10-Routing_Table.md new file mode 100644 index 000000000..08b06b4e7 --- /dev/null +++ b/skywire-specs/specifications/10-Routing_Table.md @@ -0,0 +1,63 @@ +# Routing Table + +A *Routing Table* (located within the `skywire/pkg/node` module) is unique for a given Node's public key. It is basically a key-value store in which the key is the *Route ID* and the value is the *Routing Rule* for the given *Route ID*. + +Initially, there will be two types of Routing Rules: *App* and *Forward*. + +- *App* rules are identified by their unique `` value of `0x00`. A packet which contains a *Route ID* that associates with a *App* rule is to be sent to a local App. +- *Forward* rules are identified by their unique `` value of `0x01`. A packet which contains a *Route ID* that associates with a *Forward* rule is to be forwarded. + +| Action | Key (Route ID) | Value (Routing Rule) | +| ------ | -------------- | -------------------- | +| *App* | ``
*4 bytes* | ``
*48 bytes* | +| *Forward* | ``
*4 bytes* | ``
*29 bytes* | + +- `` is the *Route ID* `uint32` key (represented by 4 bytes) that is used to obtain the routing rules for the Packet. +- `` contains the epoch time (8 bytes) of when the rule is to be discarded (or becomes invalid). +- `` specifies the type of Routing Rule (1 byte). Currently there are two possible routing rule types; *App* (`0x00`) and *Forward* (`0x01`). +- `` is the *Route ID* (4 bytes) that is the Route ID key for the reserve Route of the loop. +- `` identifies and classifies the loop. It contains the following sub-fields; ``. + - `` is the remote edge public key in which this route/loop is associated with. It is represented by 33 bytes. + - `` is the remote port in which this route/loop is associated with. It is represented by 2 bytes. + - `` is the local port in which this route/loop is associated with. It is represented by 2 bytes. +- `` is the *Route ID* that is to replace the `` before the Packet is to be forwarded. +- `` represents the transport which the packet is to be forwarded to. A Transport ID is 16 bytes long. + +Every time a Skywire Node receives a packet, it performs the following steps: + +1. Obtain the `` from the Packet, and uses this value to obtain a routing rule entry from the routing table. If no routing rule is found, or the routing rule has already expired (via checking the `` field), the Packet is then discarded. +2. Obtains the `` value to determine how the packet is to be dealt with. If the `` value is `0x00`, the packet is then to be sent to the local *App Server* with the Routing Rule. If the `` value is `0x01`, the packet is to be forwarded; continue on to step 3. +3. Obtain the `` from the *Routing Rule* and replace the `` from the *Route ID* field of the Packet. +4. Forward the Packet to the referenced transport specified within ``. + +The routing table is to be an interface. + +```golang +package node + +// RangeFunc is used by RangeRules to iterate over rules. +type RangeFunc func(routeID transport.RouteID, rule RoutingRule) (next bool) + +// RoutingTable represents a routing table implementation. +type RoutingTable interface { + // AddRule adds a new RoutingRules to the table and returns assigned RouteID. + AddRule(rule RoutingRule) (routeID transport.RouteID, err error) + + // SetRule sets RoutingRule for a given RouteID. + SetRule(routeID transport.RouteID, rule RoutingRule) error + + // Rule returns RoutingRule with a given RouteID. + Rule(routeID transport.RouteID) (RoutingRule, error) + + // DeleteRules removes RoutingRules with a given a RouteIDs. + DeleteRules(routeIDs ...transport.RouteID) error + + // RangeRules iterates over all rules and yields values to the rangeFunc until `next` is false. + RangeRules(rangeFunc RangeFunc) error + + // Count returns the number of RoutingRule entries stored. + Count() int +} +``` + +Potential improvement we could consider is to move ports from the rules into the data packet header, aligning this with `tcp`. By doing so we will be able to re-use intermediate forward rules across multiple loops which can drastically improve loop establishment time for complex loops. diff --git a/skywire-specs/specifications/11-Router.md b/skywire-specs/specifications/11-Router.md new file mode 100644 index 000000000..9adb5f915 --- /dev/null +++ b/skywire-specs/specifications/11-Router.md @@ -0,0 +1,19 @@ +# Router + +The `Router` (located within the `skywire/pkg/node` module) uses the *Transport Manager* and the *Routing Table* internally, and is responsible for the handling incoming Packets (either from external nodes via transports, or internally via the `AppServer`), and also the process for setting up routes. + +Regarding the Route setup process, a router should be able to interact with multiple trusted *Setup Nodes*. + +Every transport created or accepted by the *Transport Manager* should be handled by the *Router*. All incoming packets should be cross-referenced against *Routing Table* and either be forwarded to next *Node* or to a local *App*. + +*Transport Manager* is also responsible for managing connections on local ports for node's apps. *App Node* will request new local connection from the *Router* on *App* startup. All incoming packets from the app's connections should be forwarded based on *App* rules defined in a local routing table. *Transport Manager* should also be capable of requesting new loops from a *Setup Node*. + +## Port management + +Router is responsible for port management. Port allocation algorithm should work similarly to how `tcp` manages ports: + +- Certain range of ports should be reserved and be inaccessible for general purpose apps. Default staring port is `10`. +- All allocated local ports should be unique. +- App should be able to allocate static ports that it will be accessible on for remote connections. Static port allocation is performed during `app` init. +- App should be able to dynamically allocate local port for newly created loops. +- Allocated ports should be closed on app shutdown or disconnect from the node. diff --git a/skywire-specs/specifications/12-App_Server.md b/skywire-specs/specifications/12-App_Server.md new file mode 100644 index 000000000..25fb3ab9d --- /dev/null +++ b/skywire-specs/specifications/12-App_Server.md @@ -0,0 +1,37 @@ +# App Server + +The `AppServer` (located within the `skywire/pkg/node` module) handles communication between local and remote Apps. It also manages and administers local Apps. It is to interact with a `Router` and identify loops via the *App* routing rule retrieved from the routing table. The *App* rule is structured as follows; + +``` +| expiry | r-type | resp-rid | remote-pk | remote-port | local-port | +| 8 bytes | 1 byte | 4 bytes | 33 bytes | 2 bytes | 2 bytes | +``` + +The *App Server* not only forwards Packets between Apps and remote entities, but it can also be instructed by the end user to send *Quit* signals to the Apps. *Apps* can also request to open *Loops* with remote/local Apps. + +Each *Loop* is identified uniquely via the local and remote public keys, paired with the local and remote ports. + +Within the `AppServerConfig` file, local ports are reserved for certain Apps. The following rules are to be opposed: +- Ports are either "reserved" or "unreserved". + - No two Apps are allowed to "reserve" the same port. + - Ports are reserved via the `AppServerConfig` file. +- Reserved ports are either "active" or "inactive". + - A port is "active" when the port is "reserved" for an App, and that App is running. + - A port is "inactive" either when the port is "unclaimed", or when the port is "claimed" but the App is not running. + +The following communication processes between a given App and the *App Server* is to exist: + +- **App requests to open loop** +- **App Server asks App whether it wishes to respond to a remotely-initiated loop** +- **App Server informs App that loop is to be closed (with given reason)** + - Reasons include: Route timeout, remotely closed, locally closed, etc. +- **App informs App Server it wishes to close a loop (with given reason)** + - Reasons include: App is shutting down, loop no-longer used, etc. +- **App Server forwards packets received from remote App** + - If local app does not exist or does not accept, the loop is to be closed, and routes destroyed. +- **App Server forwards packets received from local App** + - If the rule does not exist, or the remote app nodes not accept, the loop is to be closed and routes destroyed. + +## Loop Encryption + +Each loop is to be symmetrically encrypted with [Noise](http://noiseprotocol.org/noise.html). Specifically, we are to use the `KK` fundamental [handshake pattern](http://noiseprotocol.org/noise.html#handshake-patterns). The first noise message should be provided by initiator in the request to create a new loop, this messages will be setup to responder in a loop confirmation request. Responder should send second noise message which will be returned to initiator in a loop confirmation request. diff --git a/skywire-specs/specifications/13-Setup_Node.md b/skywire-specs/specifications/13-Setup_Node.md new file mode 100644 index 000000000..de49eb20c --- /dev/null +++ b/skywire-specs/specifications/13-Setup_Node.md @@ -0,0 +1,3 @@ +# Setup Node + +The *Setup Node* (located within the `skywire/pkg/node` module) uses *Transport Manager* internally and is responsible for propagation of routing rules to nodes along the *Route*. *Setup Node* should be only addressed by a public key and should work over messaging transport using multiple channels. Each channel can be used to issue route setup commands by initiator. For *Loop* setup requests *Node* will be an initiator, for *Rule* setup related operations *Setup Node* will be a channel initiator. *Setup Node* is only responsible for handling *Foundational Packets* and doesn't perform any forwarding functions. diff --git a/skywire-specs/specifications/14-Skywire_Visor.md b/skywire-specs/specifications/14-Skywire_Visor.md new file mode 100644 index 000000000..2aa57b68f --- /dev/null +++ b/skywire-specs/specifications/14-Skywire_Visor.md @@ -0,0 +1,429 @@ +# Skywire Visor + +A visor is a node that is part of the Skywire network and is represented by a key pair (using the `secp256k1` curve). It handles Transports to remote visors, sets up routes and loops (via Routing Rules and interaction with the *Setup Node*), and manages Apps. + +Each App is it's own executable that communicates with an *App Node* using a pair of *POSIX* pipes. A piped connection is setup on *App* startup and inherited by a forked *App* process using file descriptor `3` and `4`. Setup process for a forked *App* is handled by the `app` package. + +``` + [Skywire Visor] + / | \ + / | \ +[App 1] [App 2] [App 3] +``` + +## Communication reliability + +Currently, loop ACK's are not implemented. They will need to be implemented at a later stage. ACK's are implemented for the dmsg implementation. + +## App Programming Interface + +*App* programming interface (located within the `skywire/pkg/app` module) should expose methods for *Apps* to connect to a piped connection, perform handshake and exchange data with remote nodes. + +*App* interface should expose following methods: + +```golang +// Addr implements net.Addr for App connections. +type Addr struct { + PubKey transport.PubKey + Port uint16 +} + +// LoopAddr stores addressing parameters of a loop package. +type LoopAddr struct { + Port uint16 + Remote Addr +} + +// Packet represents message exchanged between App and Visor. +type Packet struct { + Addr *LoopAddr + Payload []byte +} + +// Config defines configuration parameters for an App +type Config struct { + AppName string + AppVersion string + ProtocolVersion string +} + +// Setup sets up an app using default pair of pipes and performs handshake. +func Setup(config *Config) (*App, error) {} + +// Accept awaits for incoming loop confirmation request from a Visor and +// returns net.Conn for a received loop. +func (app *App) Accept() (net.Conn, error) {} + +// Dial sends create loop request to a Visor and returns net.Conn for created loop. +func (app *App) Dial(raddr *Addr) (net.Conn, error) {} + +// Addr returns empty Addr, implements net.Listener. +func (app *App) Addr() net.Addr {} + +// Close implements io.Closer for an App. +func (app *App) Close() error {} +``` + +## App to Visor Communication protocol + +Communication between *Visor* and an *App* happens over the piped connection using binary multiplexed protocol. + +The following is the expected format of a App Packet: + +``` +| Packet Len | Type | Message ID | JSON Body | +| 2 bytes | 1 byte | 1 byte | ~ | +``` + +- ***Packet Len*** specifies the total packet length in bytes (exclusive of the *Packet Len* field). +- ***Type*** specifies the *App Packet Type*. +- ***Message ID*** specifies multiplexing ID of a message, response for this message should contain the same ID. +- ***JSON Body*** is the packet body (in JSON format) that is unique depending on the packet type. + +**App Packet Types Summary:** + +| Type | Name | +| ---- | ---- | +| 0x00 | `Init` | +| 0x01 | `CreateLoop` | +| 0x02 | `ConfirmLoop` | +| 0x03 | `Send` | +| 0x04 | `Close` | +| 0xfe | `ResponseFailure` | +| 0xff | `ResponseSuccess` | + +### `0x00 Init` + +Sent by an *App* to a *Visor*. This packet is used to handshake connection between an *App* and a *Visor*. *Visor* will typically check if app is allowed by the config file and which port should be statically allocated it. + +**JSON Body:** + +```json +{ + "app-name": "foo", + "app-version": "0.0.1", + "protocol-version": "0.0.1" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` without body. + +### `0x01 CreateLoop` + +Sent by an *App* to a *Visor*. This packet is used to open new *Loop* to a remote *Visor*. + +**JSON Body:** + +```json +{ + "pk": "", + "port": +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with + ```json + { + "pk": "", + "port": + } + ``` + +### `0x02 ConfirmLoop` + +Sent by a *Visor* to an *App* to notify about request to open new *Loop* from a remote *Visor* + +**JSON Body:** + +```json +[ + { + "pk": "", + "port": + }, + { + "pk": "", + "port": + } +] +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +### `0x03 Send` + +Sent by a *Visor* and an *App*. This message is used to exchange messages through a previously established *Loop*. + +**JSON Body:** + +```json +{ + "addr": { + "port": , + "remote": { + "pk": "", + "port": + } + }, + "payload": "" +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +### `0x04 Close` + +Sent by a *Visor* and an *App*. *App* uses this message to notify about closed *Loop*. *Visor* sends this message after remote node is requested to close established *Loop*. + +**JSON Body:** + +```json +{ + "port": , + "remote": { + "pk": "", + "port": + } +} +``` + +Response: +- `ResponseFailure` with `error`. +- `ResponseSuccess` with empty body. + +## App Node Configuration + +The following is the JSON representation of a Skywire configuration. + +```json +{ + "version": "1.0", + "node": { + "static_public_key": "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7", + "static_secret_key": "42bca4df2f3189b28872d40e6c61aacd5e85b8e91f8fea65780af27c142419e5" + }, + "messaging": { + "discovery_addresses": ["http://localhost:9090"], + "server_count": 1 + }, + "apps": [ + { + "app": "helloworld", + "version": "1.0", + "auto_start": true, + "port": 10, + "args": [] + } + ], + "transport_discovery": "http://localhost:9091", + "setup_nodes": ["02603d53d49b6575a0b8cee05b70dd23c86e42cd6cba99af769d61a6196ea2bcb1"], + "trusted_nodes": ["0348c941c5015a05c455ff238af2e57fb8f914c399aab604e9abb5b32b91a4c1fe"], + "messaging_path": "./messaging", + "apps_path": "./apps", + "local_path": "./local", + "log_level": "info", + "interfaces": { + "rpc": ":3436" + } +} +``` + +- `"version"` represents the version of the Skywire Visor (and also the configuration format version). + +- `"node"` includes the public/private keys that identify the visor. + +- `"messaging"` configures the dmsg client instance included within the Skywire Visor. + - When `"public"` is set, the dmsg Client Instance will advertise itself to discovery. + - `"discovery_addresses"` specifies the dmsg Discovery Services that the Skywire Visor is to try. + - `"server_count"` specifies the number of servers to ensure connection with on first startup. + +- `"apps"` lists all available Skywire Apps. These configurations include; the App's name, whether the specified App should auto-start, and the ports that is reserved for the App. If these are not defined for an App, the App will not auto-start, nor have ports reserved for the App. + - If `"version"` is not specified, the highest stable version will be selected. + +- `"node_path"` stores logs, routing tables, and any data that the visor may use. + +- `"messaging_path"` holds the path which the dmsg Client Instance can use to store cache or additional configurations. + +- `"apps_path"` holds all the app executables. App executable files should be named with no spaces or weird characters (TODO: define properly). They should also be appended with the semantic version of the App after a dot; `{app_name}.v{semantic_version}`. + +- `"local_path"` contains the working directories of the apps. An app named `chat` of version `v1.0` should have a working directory within `{root_dir}/{local_path}/chat/v1.0/`. The contents of the App's working directory is specified by the App. + +## App Node RPC Interface + +The Visor should attempt to connect to the assigned *Hypervisor* on startup. The connection is to be encrypted via Noise (KK handshake pattern) so that the nodes can identify one another. + +For the *Visor* to connect to the *Hypervisor*, it needs the public key and tcp address of the *Hypervisor* in it's configuration. + +After connection has been established, the *Visor* becomes the RPC Server and the *Hypervisor* becomes the RPC client that can execute commands on the *Visor* (the RPC Server). + +Additionally, the Visor should listen on a port so that a local command-line executable (`skywire-cli`) can interact with it. This local port should hence, only accept connections from localhost. + +### Commands + +The following sub-commands should be supported. Note that command-line actions are listed below, but they should be served via RESTFUL interfaces. + +**General:** + +- **`summary`** obtains a summary of the current state of the Visor. + +**App Management:** + +- **`list-apps`** lists applications and applications stats (running/not running) (auto-start/non-auto-start) (local/remote ports). There should be flags for filtering (to be defined later). + +- **`start-app `** starts a Skywire app. + +- **`stop-app `** stops a Skywire app if running. + +- **`add-autostart-app [--start-now]`** adds a Skywire app to auto-start. After calling this command, the actual app does not actually start unless `--start-now` is set. + +- **`rm-autostart-app [--stop-now]`** removes an app from auto-starting. After calling this command, the actual app does not stop running unless `--stop-now` is set. + +**Messaging System Management:** + +- **`messaging list-discoveries`** lists saved discoveries and their statuses + +- **`messaging add-discovery `** connects to and saves a discovery. + +- **`messaging rm-discovery `** disconnects from and removes a discovery. + +- **`messaging list-servers`** lists connected messaging servers and their statuses (connected/disconnected) (auto-connect/non-auto-connect). + +- **`messaging connect-server (|--auto)`** connects to a messaging server for this session (does not save server for auto-connect). If `--auto` is set, the transport discovery is queried for a random available messaging server. + +- **`messaging disconnect-server `** disconnects from a messaging server for this session (does not affect auto-connect settings). + +- **`messaging add-autoconnect-server [--connect-now]`** adds a messaging server to auto-connect. This command does not connect to the specified messaging server unless `--connect-now` is set. + +- **`messaging rm-autoconnect-server [--disconnect-now]`** removes a messaging server from auto-connecting. This command does not disconnect from the specified messaging server unless `--disconnect-now` is set. + +**Transport Management:** + +- **`transport-types`** lists all transport types used by the visor (represented as strings). +- **`list-transports [--filter-types=,,...] [--filter-pks=,,...] [--no-logs]`** lists all transports associated with the visor. Filters can be used to only show visors associated with specified transport types or public keys. By default, transports are displayed with their logging information. +- **`add-transport [--timeout=]`** adds a transport of a given type and public key. If `--timeout` is not set, the default timeout is used. +- **`rm-transport [--tid=|--remote-pk=]`** removes a transport; either for a given transport ID, or all transports connected to a remote visor (identified via the remote visor's public key). + +**Routes Management:** + +- **`list-rules [--rid-range=:]`** lists all routing rules. A route ID range filter can be specified. +- **`rm-rules [--list=,,...|--range=:]`** removes routing rules; either via a list of route ID keys, or via a range of route ID keys (note that routing rules are identified via their `` key). This action may consequently destroy loops, and may cause the *Setup Node* to request destruction of more routing rules. + +**Loops Management:** + +- **`list-loops [--local-port=] [--remote-addr=[:]]`** lists all loops. A local port filter can be specified, where the returned loops will only be of the specified local port (there is an equivalent remote address filter). +- **`add-loop --local-port= --remote-addr=: [--setup-node=]`** attempts to create a loop with the assigned setup node. The setup node is automatically chosen if not specified. + +## Ports Management + +Within the `AppsConfig` file, ports are reserved for certain Apps. The following rules are to be opposed: +- Ports are either "reserved" or "unreserved". + - No two Apps are allowed to "reserve" the same port. + - Ports are reserved via the `AppsConfig` file. +- Reserved ports are either "active" or "inactive". + - A port is "active" when the port is "reserved" for an App, and that App is running. + - A port is "inactive" either when the port is "unclaimed", or when the port is "claimed" but the App is not running. + +## App Example + +Simple `ping-pong` client and server apps can be implemented in such way: + +Server: + +```golang +package server + +import ( + "log" + + "github.com/watercompany/skywire/pkg/app" +) + +func main() { + // Open connection with visor + helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-server", AppVersion: "1.0", ProtocolVersion: "0.0.1"}) + if err != nil { + log.Fatal("Setup failure: ", err) + } + defer helloworldApp.Close() + + log.Println("listening for incoming connections") + // Start listening loop + for { + // Wait for new Loop + conn, err := helloworldApp.Accept() + if err != nil { + log.Fatal("Failed to accept conn: ", err) + } + + log.Println("got new connection from:", conn.RemoteAddr()) + // Handle incoming connection + go func() { + buf := make([]byte, 4) + if _, err := conn.Read(buf); err != nil { + log.Println("Failed to read remote data: ", err) + } + + log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf)) + if _, err := conn.Write([]byte("pong")); err != nil { + log.Println("Failed to write to a remote visor: ", err) + } + }() + } +} +``` + +Client: + +```golang +package server + +import ( + "log" + "os" + + "github.com/watercompany/skywire/pkg/app" + "github.com/watercompany/skywire/pkg/cipher" +) + +func main() { + // Open connection with visor + helloworldApp, err := app.Setup(&app.Config{AppName: "helloworld-client", AppVersion: "1.0", ProtocolVersion: "0.0.1"}) + if err != nil { + log.Fatal("Setup failure: ", err) + } + defer helloworldApp.Close() + + // Read remote PK from stdin + remotePK := cipher.PubKey{} + if err := remotePK.UnmarshalText([]byte(os.Args[1])); err != nil { + log.Fatal("Failed to construct PubKey: ", err, os.Args[1]) + } + + // Dial to remote visor + conn, err := helloworldApp.Dial(&app.Addr{PubKey: remotePK, Port: 10}) + if err != nil { + log.Fatal("Failed to open remote conn: ", err) + } + + // Send payload + if _, err := conn.Write([]byte("ping")); err != nil { + log.Fatal("Failed to write to a remote visor: ", err) + } + + // Receive payload + buf := make([]byte, 4) + if _, err = conn.Read(buf); err != nil { + log.Fatal("Failed to read remote data: ", err) + } + + log.Printf("Message from %s: %s", conn.RemoteAddr().String(), string(buf)) +} +``` diff --git a/skywire-specs/specifications/15-Hypervisor.md b/skywire-specs/specifications/15-Hypervisor.md new file mode 100644 index 000000000..47927c2ed --- /dev/null +++ b/skywire-specs/specifications/15-Hypervisor.md @@ -0,0 +1,18 @@ +# Hypervisor + +The *Hypervisor* is responsible for managing *Visors* and is identified via it's public key and TCP address. + +The *Visor* is responsible for including a trusted *Hypervisor* in it's configuration file, and attempt to connect to it on startup. The connection is authenticated and encrypted via the Noise protocol where the `XK` handshake pattern is used with the *Visor* being the initiator. + +After the connection is successfully established between an *Visor* and a *Hypervisor*, the *Visor* acts as the RPC server while the *Hypervisor* is the RPC client. In this way, the *Hypervisor* can execute commands on the *Visor*. + +The *Hypervisor* serves a REST API which the end user can interact with. To access the API, the user is required to log in via a username and password. + +The *Hypervisor* should be implemented in `/pkg/hypervisor`. + +## Hypervisor REST API (and User Interface) + +- Login/logout. +- Change password. +- List connected *Visor*, each Visor should contain the following summary; Public key, local address, number of transports established, number of apps running, Uptime. +- The user can click into a listed *Visor* and perform node-specific actions; specifically, the [RPC commands as specified above.](#visor-rpc-interface) diff --git a/skywire-specs/specifications/16-Glossary.md b/skywire-specs/specifications/16-Glossary.md new file mode 100644 index 000000000..5ac9ff00b --- /dev/null +++ b/skywire-specs/specifications/16-Glossary.md @@ -0,0 +1,104 @@ +# Glossary + +**Skywire Transport:** + +Identified via a Transport ID, a Skywire Transport is represented as an interface and can be implemented as different transport types. It is a bi-directional line of communication between two Skywire nodes (transport edges) and constructs a single hop of a route. + +It is the responsibility of a Skywire Factory to generate Skywire Transports. + +**Skywire Transport ID:** + +A Skywire Transport is an uint16 integer that refers to a Skywire Transport and identifies it in the transport discovery. Skywire Transport IDs are assigned uniquely by and for a transport edge. Therefore, the same transport can be referenced by two different Transport IDs, assigned by the 2 edges. + +**Transport Edge:** + +A transport edge is one of the two Skywire nodes that make up a transport. It is represented by a unique public key that identifies the Skywire node. + +**Transport Type:** + +A Transport Type (represented by a string) refers to the underlying implementation of a Transport Factory or a Transport. A Transport Factory of a certain type can only construct Transports of that type. + +Initially, we will have two transport types; messaging and native. The messaging transport is implemented by the messaging system. The native transport is non-dependent on the current internet and in the future will be the main transport implementation for Skywire. + +**Transport Factory:** + +A transport factory is used by a Skywire node which constructs transports of a certain transport type. It is an interface that can either dial or listen for remotely initiated transports. + +**Transport Perspective:** + +A transport perspective is the assumed state of a transport (number of packets and bandwidth sent and received over that transport; whether transport is up or down). It only represents the perspective (on a transport) from a single transport edge and therefore the perspectives of two edges on the same transport might conflict. + +**Transport Discovery:** + +The transport discovery is a service that registers transports and the associated transport perspectives. It thereby provides the basis for the route finding service because it represents the public network topology. It is queried by the route finding service to discover routes. + +**Hop:** + +A hop is equivalent to a transport and is a single unit of a route. + +**Skywire Route:** + +A Skywire Route is a unidirectional network path that allows apps and services to communicate. It can be made up of one or more hops between Skywire Nodes. It is identified by route IDs, interpreted by individual Skywire Nodes via the routing table. + +**Skywire Route ID:** + +A route ID is represented by 32 bits and is the basis for a Skywire Node’s routing rules. Route IDs are changed along every hop of a route and there is a unique set of route IDs for every node. + +**Route Finder:** + +The route finder is a service that evaluates the network topology via the information of the transport discovery to provide possible routes to inquiring Skywire nodes. Currently it evaluates possible routes only on the basis of the hop metric. + + +**Routing Table:** + +The Routing Table is a key-value store that determines the action to be performed on an incoming Packet. + +Using the Routing ID (from the Packet) as the key, a Skywire Node can obtain either the next transport that the packet is to be sent over (and the new Routing ID) or whether the packet is to be consumed by the node itself. + +**Stream:** + +A stream represents a bi-directional line of communication between two Skywire Apps. A stream can use multiple Routes to establish itself. + +**Skywire App:** + +A Skywire App is an executable that interacts with the Skywire App Node via Unix pipes. Skywire apps provide services for the end user such as a proxy, ssh, chat, etc. + +**Messaging System:** + +The messaging system is primitive fallback implementation of Transport and Transport Factory over the Internet. It consists of a messaging discovery, client and server instances, where the server instance relays messages between clients. The communication between two clients is called a messaging channel. + +**Messaging Instance:** + +Messaging instances (alongside the messaging discovery) are the main components of the messaging system. A messaging instance can be either a messaging server or messaging client. + +**Messaging Link:** + +A messaging link is the direct line of communication between a server instance and a client instance. A messaging channel between two client instances can be constructed from two messaging links to a shared server instance. + +**Messaging Discovery:** + +The messaging discovery is a key value store that registers the messenger servers that a given messenger client is connected to. It allows clients to get the information necessary to establish a messaging transport with another node by querying the public keys associated messaging servers. + +**Messaging Channel:** + +A Messaging Channel represents the bi-directional connection-oriented line of communication between two client instances of the messaging system. + +**Skywire Node:** + +A Skywire Node is the general term for nodes that make up the Skywire Network. Any entity that manages Transports and Packets is considered to be a Skywire Node. + +Examples of Skywire Nodes include; App Node (which manage Skywire Apps), Control Node (which administers App Nodes) and the Route Setup Node (which coordinates the construction of Routes with App Nodes). + +**Skywire App Node:** + +An App Node is a Skywire Node that runs, stops, monitors and sets permissions for Skywire Apps. Internally, it handles and coordinates packets incoming and outgoing between the Skywire App and the external Skywire network. + +An App Node can also forward packets to external Skywire Nodes (based on the set Routing Rules). + +**Skywire Control Nodes:** + +A Skywire Control node is similar to an app node, with the difference that it has administrative permissions for other nodes in a cluster and is being sent logs from other nodes. It should not run user-based Skywire Apps, nor should it forward Packets. + +**Skywire Route Setup Node:** + +The Route Setup Node is a Skywire Node which runs a service that allows it to set up routes for other Skywire nodes. It does this by relaying the routing rules to individual Skywire App Nodes. diff --git a/skywire-specs/specifications/17-App2 module.md b/skywire-specs/specifications/17-App2 module.md new file mode 100644 index 000000000..a30489e4a --- /dev/null +++ b/skywire-specs/specifications/17-App2 module.md @@ -0,0 +1,106 @@ +# `app2` + +The current `app` module of Skywire is a mess. It is hard to test and it's actual communication logic is split across multiple packages. This proposal details how I would envision an *ideal* `app` package, and also how we can migrate and eventually scrap the old `app` module. + +## Overview + +The visor node is the intermediary of communication between apps and the network(s). The `app2` package will provide structures for use in `visor` and skywire apps. It will facilitate the communication between them. All networks used by skywire should implement the `net.Conn` and `net.Listener` interfaces. Currently, apps only need to dial/accept connections on `dmsg` and `router`. + +The goal of `app2` is to be simple and retain as much of the original form of the communication as possible. This should improve code readability and performance (when compared to the original `app` package). + +`app2` will have two major components: `app2.Client` (which will be used by skywire apps) and `app2.Server` (which will run on the visor node). The communication between `app2.Client` and `app2.Server` should be via unix socket where the `app2.Server` is responsible for listening on a socket file. + +## Types + +The following structures should identify the components that facilitate interation between `app2.Client` and `app2.Server`. + +### Summary + +```golang +package app2 + +// ProcID identifies the current instance of an app (an app process). +// The visor node is responsible for starting apps, and the started process +// should be provided with a ProcID. +type ProcID uint16 + +// HSFrameType identifies the type of handshake frame. +type HSFrameType byte + +// HSFrame is the data unit for socket connection handshakes between Server and Client. +type HSFrame []byte + +// Client is used by skywire apps. +type Client struct { + // TODO: define. +} + +// NewClient creates a new Client. The Client needs to be provided with: +// - localPK: The local public key of the parent skywire visor. +// - pid: The procID assigned for the process that Client is being used by. +// - sockAddr: The socket address to connect to Server. +func NewClient(localPK cipher.PubKey, pid ProcID, sockAddr string) (*Client, error) { + // TODO: define. +} + +// Server is used by skywire visor. +type Server struct { + // TODO: define. +} +``` + +The reason for only having handshake frames (`HSFrame`), is because a connection on a network of choice between a visor node and a remote node, is represented by a socket connection between the visor node (via `app2.Server`) to a skywire app (which uses `app2.Client`). So after the handshake (which is used for determining how we map the socket connection), the data thereafter can just be forwarded as-is to and from the socket connection and the network connection. + +### `HSFrame` + +A `HSFrame` consists of a header and a body. The contents of the body changes based on the `HSFrameType` (specified in the header). + +**`HSFrame` header:** + +The `HSFrame` header consists of a `ProcID`, a `HSFrameType` and a `BodyLen` (5 bytes in total). + +``` +| ProcID (2 bytes) | HSFrameType (1 byte) | BodyLen (2 bytes) | +``` + +**`HSFrame` body:** + +The `HSFrame` body should be a JSON structure. + +| HSFrameType Name | HSFrameType Value | HSFrame Body Contents | Usage(s) | +| --- | --- | --- | --- | +| `DmsgListen` | `10` | localPK, localPort | `Client -> Server` to request listening on a given pk/port pair. | +| `DmsgListening` | `11` | localPK, localPort | `Server -> Client` to inform that the client is now listening the pk/port pair. | +| `DmsgDial` | `12` | localPK, (localPort), remotePK, remotePort | `Client -> Server` to request dialing to a remote.
`Server -> Client` to inform that a remote has dialed.
**Note:** localPort must be `0` when `Client -> Server` as the `app2.Server` will provide an ephemeral port on `DmsgAccept`. | +| `DmsgAccept` | `13` | localPK, localPort, remotePK, remotePort | `Client -> Server` to inform the server that the remote connection is accepted.
`Server -> Client` to inform the client that the connection is accepted by remote. | + +Note that there are no *close* frame types, as one can just close the underlying socket connection. + +## Procedures + +### `app2.Client` listens on given pk/port. + +- `app2.Client` dials a new socket connection to `app2.Server` and sends a `DmsgListen` type `HSFrame` with local pk/port in which the client wishes to listen on. +- `app2.Server` calls `(*dmsg.Client).Listen` and creates a `dmsg.Listener`. + - If there is no error, `app2.Server` sends `DmsgListening` back to the client. + - On failure, the `app2.Server` just closes the socket connection. + +### `app2.Client` accepts a connection from remote. + +Given that an `app2.Client` is listening on a given pk/port, when `app2.Server` accepts a remote connection, the following should happen: + +- `app2.Server` sends `DmsgDial` to `app2.Client` via the socket connection that is initiated with `DmsgListen`. +- `app2.Client` dials a new socket connection to `app2.Server` and sends `DmsgAccept` (with the same body as the `DmsgDial`) via the new socket connection. +- Further data to/from the related remote connection should be directly forwarded by this new socket connection. + +### `app2.Client` dials to a remote pk/port. + +- `app2.Client` dials a new socket connection to the `app2.Server` and sends a `DmsgDial` with an empty localPort. +- `app2.Server` attempts the requested dial. + - On success, the `app2.Server` sends `DmsgAccept` to `app2.Client` via the same socket connection. + - On failure, the socket connection gets closed. + +## Implementation notes + +### `app2.Server` +- We will need to move most of the app-management logic here. Currently, they are mostly located in [`visor`](https://github.com/skycoin/skywire/blob/mainnet-milestone1/pkg/visor/visor.go). On top of this, we need to introduce the idea of `ProcID`. We should probably add a new structure (named `Process`) that holds both the `os.Process` and `net.Conn` socket connection to the app pocess. \ No newline at end of file diff --git a/skywire-specs/specifications/18-Router2 module.md b/skywire-specs/specifications/18-Router2 module.md new file mode 100644 index 000000000..26a43b69b --- /dev/null +++ b/skywire-specs/specifications/18-Router2 module.md @@ -0,0 +1,296 @@ +# `router2` + +The goal of this proposal is to split out and define the responsiblities within `router` to allow for easier integration of future features, as well as make the codebase easier to understand. + +The current implementation of `routing.Rule` has fields that have different meanings, based on the rule type. A clearer definition of rule types are addressed in this proposal. + +Currently, setup nodes create *loops* (which is the combination of a forward and a reverse route). We want setup nodes to create *routes* (which is a uni-directional line of communication across x-number of hops). A single `CreateRoute` packet can request to create multiple routes. How routes are to be used is to be handled in an additional structure called `multiplexer`. + +## `routing.Rule` changes + +As the `Router` is to be split up into router/multiplexer, we are to remove the concept of `App` rule types, and work with the concepts of forward/consume. + +### New fields for `routing.Rule` + +A routing rule is structured as follows (note that the `body` field is action-specific). +``` +| keepAlive | rType | keyRtID | body | +| 8[0:8] | 1[8:9] | 4[9:13] | [13:~] | +``` + +#### field `keepAlive` (8 bytes) + +`keepAlive` is the duration of non-use before the rule is to be disregarded/deleted. It is the responsibility of the src edge to send ping packets and the dst edge to respond with pong packets. + +#### field `rType` (1 byte) + +`rType` is the `RuleType`. + +```golang +type RuleType byte ` + +const ( + // ConsumeRule represents a hop to the route's destination node. + // A packet referencing this rule is to be consumed localy. + ConsumeRule = RuleType(0) + + // ForwardRule represents a hop from the route's source node. + // A packet referencing this rule is to be sent to a remote node. + ForwardRule = RuleType(1) + + // IntermediaryForwardRule represents a hop which is not from the route's source, + // nor to the route's destination. + IntermediaryForwardRule = RuleType(2) +) +``` + +#### field `keyRtID` (4 bytes) + +`keyRtID` is the route ID that is used as the key to retreive the rule. + +#### field `body` (~ bytes) + +The contents and size of this field is dependent on the `rType`. + +- `ConsumeRule` `body` field contents: + ``` + | rDesc | + | 70[13:83] | + ``` + +- `ForwardRule` `body` field contents: + ``` + | rDesc | nxtRtID | nxtTpID | + | 70[13:83] | 4[83:87] | 16[87:103] | + ``` +- `IntermediaryForwardRule` `body` field contents: + ``` + | nxtRtID | nxtTpID | + | 4[13:17] | 16[17:33] | + ``` + +#### field `rDesc` (70 bytes) + +```golang +// RouteDescriptor describes a route (from the perspective of the source and destination edges). +type RouteDescriptor [70]byte + +func (d RouteDescriptor) SrcPK() cipher.PubKey { /*TODO*/ } +func (d RouteDescriptor) DstPK() cipher.PubKey { /*TODO*/ } +func (d RouteDescriptor) SrcPort() uint16 { /*TODO*/ } +func (d RouteDescriptor) DstPort() uint16 { /*TODO*/ } +``` + +The decision to go with src/dst instead of local/remote is because it will have the same values independent of the residing edge. + +#### field `nxtRtID` (4 bytes) + +Provides the next `routeID` value for `Forward`-esk type rules. + +#### field `nxtTpID` (16 bytes) + +Provides the next transport ID for `Forward`-esk type rules. + +## `routing.Table` changes + +The nature of rules is going to be temporary and there will no longer be any need to store rules to disk. The only implementation for `routing.Table` should be in memory. + +The new structure of `routing.Table` should have an internal garbage collector, removing the need for `router.managedRoutingTable`. The internal garbage collector should only delete `IntermediaryForwardRule` types. All other rule types should be deleted by external structures. + +```golang +type Table interface { + ReserveKey() (key RouteID, err error) + SaveRule(r Rule) error + Rule(key RouteID) (r Rule, err error) + RulesWithDesc(desc RouteDescriptor) (rs []Rule, err error) + AllRules() []Rule + DelRules([]RouteID) + Count() int +} + +type memTable struct { + // All rules, referenced with 'keyRtID'. + rules map[RouteID]Rule +} +``` + +## Setup node changes + +### Packet types + +We should only have the following packet types: + +| Packet Type | Packet Body | +| --- | --- | +| `CreateRoutes` | `routes []Route` | +| `RoutesCreated` | `routes []Route` | +| `ReserveRtIDs` | `n int` | +| `RtIDsReserved` | `rtIDs []routing.RouteID` | +| `AddRules` | `rules []routing.Rule` | +| `RulesAdded` | `rtIDs []routing.RouteID` | +| `Failure` | `failureCode byte, msg string` | + +Notes: +- We are removing `Close~` packet types as it's no longer needed. Instead, we will use shorter `keepAlive` durations and introduce ping/pong to keep routes alive. + +### `Route` + +```golang +type Hop struct { + TpID uuid.UUID + From cipher.PubKey + To cipher.PubKey +} + +type Route struct { + Desc RouteDescriptor + Hops []Hop + KeepAlive time.Duration +} +``` + +### Use one connection per visor, per request + +Previously, a single request is split between multiple connections per visor. We should only use one connection per request per visor. + +### Failure codes + +We need to define a new type. + +```golang +type FailureCode byte + +func (fc *FailureCode) String() string { + // TODO: define. +} +``` + +And have a list of constants for failure codes. + +## Route Finder Changes + +Instead of finding *paired routes*, we should be able to search for routes independently. + +```golang +type RouteOptions struct { + MinHops int + MaxHops int +} + +// FindRoutes find routes specified in 'rts'. +// As routes are uni-directional, rts[i][0] is the source edge, and rts[i][1] is the destination edge. +// If 'opts' is nil, MinHops is 1 and MaxHops is 3. +func (c *apiClient) FindRoutes(ctx context.Context, rts [][2]cipher.PubKey, opts *RouteOptions) +``` + +Therefore the route finder API also needs modifications. + +## Router Changes + +The `router` is responsible for creating and keeping track of these uni-directional routes. Internally, it uses the *routing table*, *route finder client* and *setup client*. + +```golang +// DialOptions are options when dialing routes. +type DialOptions struct { + MinForwardRts int + MaxForwardRts int + MinConsumeRts int + MaxConsumeRts int +} + +type Router interface { + io.Closer + + // DialRoutes dials to a given visor of 'rPK'. + // 'lPort'/'rPort' specifies the local/remote ports respectively. + // A nil 'opts' input results in a value of '1' for all DialOptions fields. + // A single call to DialRoutes should perform the following: + // - Find routes via RouteFinder (in one call). + // - Setup routes via SetupNode (in one call). + // - Save to routing.Table and internal RouteGroup map. + // - Return RouteGroup if successful. + DialRoutes(ctx context.Context, rPK cipher.PubKey, lPort, rPort uint16, opts *DialOptions) (*RouteGroup, error) + + // AcceptsRoutes should block until we recieve an AddRules packet from SetupNode that contains ConsumeRule(s) or ForwardRule(s). Then the following should happen: + // - Save to routing.Table and internal RouteGroup map. + // - Return the RoutingGroup. + AcceptRoutes() (*RouteGroup, error) +} + +type router { + rt RoutingTable // routing table + tm transport.Manager // transport manager + rfc rfclient.Client // route finder client + rsc rsclient.Client // route setup client + rgs map[RouteDescriptor]*RouteGroup // route groups to push incoming reads from transports. +} +``` + +### `RouteGroup` structure + +A `RouteGroup` is responsible for input/output via rules. In the future, we may read/write via multiple transports, hence we need a way of reordering the packets. + +A `RouteGroup` is created either when we: +- Initiate a set a routes. +- Receive a set of routes. + +```golang +// RouteGroup should implement 'io.ReadWriteCloser'. +type RouteGroup struct { + desc RouteDescriptor // describes the route group + fwd []Rule // forward rules (for writing) + rvs []Rule // reverse rules (for reading) + + // The following fields are used for writing: + // - fwd/tps should have the same number of elements. + // - the corresponding element of tps should have tpID of the corresponding rule in fwd. + // - rg.fwd references 'ForwardRule' rules for writes. + + // 'tps' is transports used for writing/forward rules. + // It should have the same number of elements as 'fwd' + // where each element corresponds with the adjacent element in 'fwd'. + tps []*transport.ManagedTransport + + // 'readCh' reads in incoming packets of this route group. + // - Router should serve call '(*transport.Manager).ReadPacket' in a loop, + // and push to the appropriate '(RouteGroup).readCh'. + readCh <-chan []byte // push reads from Router + readBuf bytes.Buffer // for read overflow +} +``` + +#### Routing packets + +The unit of communication for routing/router is called packets. + +``` +| type (byte) | route ID (uint32) | payload size (uint16) | payload (~) | +| 1[0:1] | 4[1:5] | 2[5:7] | [7:~] | +``` + +packet types: +- `DataPacket` - Payload is just the underlying data. +- `ClosePacket` - Payload is a `type CloseCode byte`. +- `KeepAlivePacket` - Payload is empty. + +#### Reading mechanism of `RouteGroup` + +The `Router`, via `transport.Manager`, is responsible for reading incoming packets and pushing it to the appropriate `RouteGroup` via `(*RouteGroup).readCh`. + +To help with implementing the read logic, within the `dmsg` repo, we have [`ioutil.BufRead`](https://github.com/skycoin/dmsg/blob/master/ioutil/buf_read.go), just in case the read buffer is short. + +#### Writing mechanism of `RouteGroup` + +For the first version, only the first `ForwardRule` (`fwd[0]`) is used for writing. + +#### Closing the `RouteGroup` + +- Send `Close` packet for all `ForwardRule`s. +- Delete all rules (`ForwardRule`s and `ConsumeRule`s) from routing table. +- Close all go channels. + +#### KeepAlive mechanism + +The keepAlive value for routing rules is rather short to ensure that rules are deleted when unused. It is the responsibility for the source edge of each route (or the node with the `ForwardRule`) to send keepAlive packets. + +The `RouteGroup` is responsible for ensuring that at least one packet is sent every `keepAlive/2`. This can be a data packet or a keepAlive packet. If data packets are delivered with a high frequency, we will not need to send keepAlive packets. \ No newline at end of file diff --git a/skywire-specs/transports/stcpr/Specs.md b/skywire-specs/transports/stcpr/Specs.md new file mode 100644 index 000000000..05dd83e83 --- /dev/null +++ b/skywire-specs/transports/stcpr/Specs.md @@ -0,0 +1,22 @@ +# STCP transport with address resolving + +STCPR transport work the same way as STCP, +but uses the address-resolver service instead of PK table to determine an address by a PK. + +### STCPR description + +Address-resolver has the following HTTP methods for resolving PKs to IPs: + +- `POST` `/bind/stcpr` + +It is used to bind PKs that visors send on start with their addresses. It requires PK authorization. + +The request format is a JSON with a port visor listens on and with a list of visor local addresses. + +- `GET` `/resolve/stcpr/{pk}` + +It is used by dialing visor to resolve public key to address of dialed visor. It requires PK authorization. + +- `/security/nonces/` + +It is used by `httpauth` middleware for public key authorization for both binding and resolving addresses. diff --git a/skywire-specs/transports/sudph/Specs.md b/skywire-specs/transports/sudph/Specs.md new file mode 100644 index 000000000..1e5fe8fdc --- /dev/null +++ b/skywire-specs/transports/sudph/Specs.md @@ -0,0 +1,77 @@ +# Hole-punch transport + +Hole-punch transport uses a point-to-point connection between two parties that are behind NATs or firewalls. + +### UDP hole-punch transport description + +To be able to establish a connection between two visors (`A` and `B`) using UDP hole-punching, +an intermediate server (`S`) between two nodes is necessary. + +The connection to `S` and then connections between `A` and `B` should be created from the same address. + +On start, `A` and `B` listen on ports `AP` and `BP` and send UDP requests to `S` to port `SP` +to register their addresses in `S`. + +This connection should be kept alive within the lifecycle of hole-punching transport to keep NAT mapping. +When `A` tries to hole-punch `B`, +it needs to send packets from the same port `AP` (to reuse existing NAT mapping) to port `BP`. +What about `B`, it listens on the same port `BP` for a connection from `AP`. +So, we need to have 2 connections on `A` (`AP` -> `SP`, `AP` -> `BP`) +and 2 connections on `B` (`BP` -> `SP`, `BP` -> `AP`). + +The local port is same for both connections, therefore UDP listener (`*net.UDPConn`) is shared between both connections. +So in fact, connections to server and other visor reuse the same UDP connection, but remote ports and addresses differ. +When a new [KCP](https://github.com/xtaci/kcp-go) connection is created from a `*net.UDPConn`, +it requires setting remote address and drops packets from different addresses. +Therefore, if `*net.UDPConn` is wrapped by KCP, there's a question how to receive packets from different remote addresses, +which is required for UDP hole punching. + +To solve that, a middleware between `*net.UDPConn` and KCP is used. +It extracts packets sent from different remote addresses. +Packets sent from an expected by KCP address would be passed further to KCP. + +#### Connection from visor to address resolving server + +As written above, visors use the connection for sending a public key they want to connect to +and for receiving an IP they are asked to connect to. Therefore, the connection is bi-directional. +Visor sends an HTTP request which is upgraded to websocket connection. +When visor sends its public key, it should be authenticated. For that, `httpauth` is used for the HTTP request. + +#### Address resolving server + +Address resolving server needs to store visor address and its corresponding address. +The server has two types of storage: + +- non-persistent in-memory storage +- persistent `Redis` storage + +Address-resolver has two types of API that work with UDP hole-punching + +- HTTP +- UDP + +The `--addr` argument specifies an address address-resolver listens both HTTP and UDP on. + +The UDP server is used to bind PKs that visors send on start with their addresses. +PKs are authorized by a handshake procedure same with other handshakes (using noise). + +- `GET` `/resolve/sudph/{pk}` + +It is used by dialing visor to resolve public key to address of dialed visor. It requires PK authorization. + +- `/security/nonces/` + +It is used by `httpauth` middleware for public key authorization for resolving addresses. + +#### Communication format + +Visor starts UDP communication with address-resolver and performs a noise handshake with it. +Then it sends a JSON with a port it listens UDP connections on +and with local IPs to be able to connect with local IPs locally. + +If address resolver sees that external IPs of visors trying to connect to each other are same, +it allows them to try to connect to a local IP first. + +## Useful links + +- https://bford.info/pub/net/p2pnat/index.html \ No newline at end of file