mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
💐 Sync 2026-09-29 21:24:56
This commit is contained in:
1 parent
ac66545478
commit
ed79a5268f
87 files changed
+351
-75
No files matched your search
@@ -374,8 +374,13 @@ if [ "$DO_RDNS" = "1" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Listed as blocked yet associated right now = the radio never got the block.
|
||||
WIFI_PENDING=false
|
||||
tctl_wifi_block_pending "$([ "$WIFI_BLOCKED" = "true" ] && echo 1 || echo 0)" "$CONN_TYPE" \
|
||||
&& WIFI_PENDING=true
|
||||
|
||||
# Output final JSON
|
||||
printf '{"ip":"%s","name":"%s","mac":"%s","conn_type":"%s","conn_last":"%s","timestamp":%d,"blocked":%s,"block_packets":%d,"block_bytes":%.0f,"wifi_blocked":%s,"total":%.0f,"protocols":{"tcp":%d,"udp":%d,"other":%d},"tcp_states":{"established":%d,"time_wait":%d,"syn_sent":%d,"close_wait":%d},"connections":[%s],"rate_limit_kbit":%d,"shape_kbit":%d}\n' \
|
||||
printf '{"ip":"%s","name":"%s","mac":"%s","conn_type":"%s","conn_last":"%s","timestamp":%d,"blocked":%s,"block_packets":%d,"block_bytes":%.0f,"wifi_blocked":%s,"wifi_block_pending":%s,"total":%.0f,"protocols":{"tcp":%d,"udp":%d,"other":%d},"tcp_states":{"established":%d,"time_wait":%d,"syn_sent":%d,"close_wait":%d},"connections":[%s],"rate_limit_kbit":%d,"shape_kbit":%d}\n' \
|
||||
"$IP" "$NAME" "$MAC" "$CONN_TYPE" "$CONN_LAST" "$TIMESTAMP" "$BLOCKED" "$BLOCK_PACKETS" "$BLOCK_BYTES" \
|
||||
"$WIFI_BLOCKED" "$TOTAL" "$N_TCP" "$N_UDP" "$N_OTHER" \
|
||||
"$WIFI_BLOCKED" "$WIFI_PENDING" "$TOTAL" "$N_TCP" "$N_UDP" "$N_OTHER" \
|
||||
"$EST" "$TW" "$SS" "$CW" "$CONNS_OUT" "$RATE_LIM" "$SHAPE_KBIT"
|
||||
@@ -418,12 +418,24 @@ tctl_get_wifi_interfaces() {
|
||||
uci show wireless 2>/dev/null | grep '=wifi-iface' | cut -d. -f2 | cut -d= -f1
|
||||
}
|
||||
|
||||
# Get running WiFi interface names (e.g. wlan0, wlan1)
|
||||
# Running AP interface names, one per line (e.g. phy0-ap0), taken from the
|
||||
# hostapd ubus objects.
|
||||
#
|
||||
# Returns non-zero when ubus itself could not be consulted. That is NOT the
|
||||
# same as "no AP is running", and callers must not read an empty list as
|
||||
# "nothing to enforce" unless the query actually succeeded — doing so would
|
||||
# turn a broken router into a silent "blocked".
|
||||
tctl_get_hostapd_ifaces() {
|
||||
ubus list 2>/dev/null | grep '^hostapd\.' | cut -d. -f2
|
||||
local out
|
||||
command -v ubus >/dev/null 2>&1 || return 1
|
||||
out=$(ubus list 2>/dev/null) || return 1
|
||||
# An empty result is a valid answer -- "no AP is running" -- so the grep
|
||||
# matching nothing must not be reported as a failed query. Only ubus itself
|
||||
# failing, above, is that.
|
||||
printf '%s\n' "$out" | grep '^hostapd\.' | cut -d. -f2-
|
||||
return 0
|
||||
}
|
||||
|
||||
# Add MAC to hostapd deny ACL at runtime + deauth the client (no wifi reload)
|
||||
# Which ACL policy a wifi-iface uses: "allow" (whitelist — only listed MACs may
|
||||
# associate) or "deny" (blacklist — listed MACs are rejected). Anything else,
|
||||
# including unset, means no filtering is configured yet, reported as "deny"
|
||||
@@ -434,51 +446,187 @@ tctl_get_wifi_filter_mode() {
|
||||
[ "$mode" = "allow" ] && echo "allow" || echo "deny"
|
||||
}
|
||||
|
||||
# Block a MAC at runtime. In deny mode that means adding it to the deny ACL; in
|
||||
# allow (whitelist) mode it means dropping it from the accept ACL. Either way
|
||||
# the client is deauthenticated so the change takes effect immediately.
|
||||
# ── Runtime WiFi enforcement ──────────────────────────────────────────────
|
||||
#
|
||||
# The uci maclist is the durable half of a WiFi block; hostapd's running ACL is
|
||||
# the half that decides whether the device is on the air *now*. The two drift
|
||||
# apart whenever the runtime call cannot be made, and for a long time that
|
||||
# drift was invisible: the calls below were fire-and-forget, their exit status
|
||||
# discarded, so a router without hostapd-utils reported every block as done
|
||||
# while the device kept browsing.
|
||||
#
|
||||
# So every entry point here reports how far enforcement actually got, as one
|
||||
# of these words, and proves it by reading the state back rather than trusting
|
||||
# a return code:
|
||||
#
|
||||
# acl - the running ACL was changed and the change was read back
|
||||
# ban - no usable hostapd_cli; hostapd's ubus deauthed and banned the
|
||||
# client, which expires on its own (see TCTL_WIFI_BAN_MS)
|
||||
# none - nothing could be applied or verified on the running radio
|
||||
# no-radio - ubus answered and no AP is running, so there is nothing to
|
||||
# enforce; the uci maclist applies when wifi next starts
|
||||
#
|
||||
# Only "acl" and "no-radio" mean the operator's intent is in force, and the
|
||||
# functions return 0 for exactly those two.
|
||||
|
||||
# How long a ubus ban lasts, in milliseconds. Used only where hostapd_cli is
|
||||
# unavailable: hostapd's ubus object has no ACL method (del_client, list_bans,
|
||||
# get_clients, reload), so a timed ban is the strongest immediate measure it
|
||||
# can offer. One hour is long enough to be worth doing and short enough that
|
||||
# callers must keep calling it temporary rather than done.
|
||||
TCTL_WIFI_BAN_MS=3600000
|
||||
|
||||
# hostapd_cli exists AND this AP's control socket answers. Both halves matter:
|
||||
# the package can be absent (the router this bug was found on), or present
|
||||
# while hostapd is not listening, and only a PONG shows the ACL commands will
|
||||
# reach anything.
|
||||
tctl_hostapd_cli_alive() {
|
||||
command -v hostapd_cli >/dev/null 2>&1 || return 1
|
||||
hostapd_cli -i "$1" ping 2>/dev/null | grep -q PONG
|
||||
}
|
||||
|
||||
# Does the running ACL currently keep this MAC off the air? Read back from
|
||||
# hostapd instead of inferred from our own call: hostapd_cli exits 0 for
|
||||
# "command delivered", and an ACL that stays empty afterwards is exactly the
|
||||
# failure being guarded against. hostapd also reloads its maclist file at times
|
||||
# of its own choosing, so the entry present may not be the one we added — which
|
||||
# is fine, and another reason to ask rather than assume.
|
||||
tctl_hostapd_acl_blocks() {
|
||||
local iface="$1" mac="$2" mode="$3" acl
|
||||
if [ "$mode" = "allow" ]; then
|
||||
acl=$(hostapd_cli -i "$iface" accept_acl SHOW 2>/dev/null)
|
||||
! printf '%s\n' "$acl" | grep -qi "$mac"
|
||||
else
|
||||
acl=$(hostapd_cli -i "$iface" deny_acl SHOW 2>/dev/null)
|
||||
printf '%s\n' "$acl" | grep -qi "$mac"
|
||||
fi
|
||||
}
|
||||
|
||||
tctl_hostapd_ubus_banned() {
|
||||
local iface="$1" mac="$2"
|
||||
command -v ubus >/dev/null 2>&1 || return 1
|
||||
ubus call "hostapd.$iface" list_bans 2>/dev/null | grep -qi "$mac"
|
||||
}
|
||||
|
||||
# Fallback when hostapd_cli is unusable. del_client deauthenticates and refuses
|
||||
# the client for ban_time, which is not an ACL entry — the durable half stays
|
||||
# the uci maclist. The call's own exit status is deliberately ignored: it can
|
||||
# fail for a client that is not currently associated while the ban still lands,
|
||||
# and list_bans is the only answer worth having.
|
||||
tctl_hostapd_ubus_ban() {
|
||||
local iface="$1" mac="$2"
|
||||
command -v ubus >/dev/null 2>&1 || return 1
|
||||
ubus call "hostapd.$iface" del_client \
|
||||
"{\"addr\":\"$mac\",\"reason\":1,\"deauth\":true,\"ban_time\":$TCTL_WIFI_BAN_MS}" \
|
||||
>/dev/null 2>&1
|
||||
tctl_hostapd_ubus_banned "$iface" "$mac"
|
||||
}
|
||||
|
||||
# acl > ban > none, so a loop over several APs can keep the worst result.
|
||||
tctl_enforce_rank() {
|
||||
case "$1" in
|
||||
acl) echo 3 ;;
|
||||
ban) echo 2 ;;
|
||||
*) echo 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
tctl_hostapd_block_iface() {
|
||||
local iface="$1" mac="$2" mode="$3"
|
||||
if tctl_hostapd_cli_alive "$iface"; then
|
||||
if [ "$mode" = "allow" ]; then
|
||||
hostapd_cli -i "$iface" accept_acl DEL_MAC "$mac" >/dev/null 2>&1
|
||||
else
|
||||
hostapd_cli -i "$iface" deny_acl ADD_MAC "$mac" >/dev/null 2>&1
|
||||
fi
|
||||
hostapd_cli -i "$iface" deauthenticate "$mac" >/dev/null 2>&1
|
||||
if tctl_hostapd_acl_blocks "$iface" "$mac" "$mode"; then
|
||||
echo acl
|
||||
return
|
||||
fi
|
||||
fi
|
||||
if tctl_hostapd_ubus_ban "$iface" "$mac"; then
|
||||
echo ban
|
||||
return
|
||||
fi
|
||||
echo none
|
||||
}
|
||||
|
||||
tctl_hostapd_unblock_iface() {
|
||||
local iface="$1" mac="$2" mode="$3"
|
||||
if tctl_hostapd_cli_alive "$iface"; then
|
||||
if [ "$mode" = "allow" ]; then
|
||||
hostapd_cli -i "$iface" accept_acl ADD_MAC "$mac" >/dev/null 2>&1
|
||||
else
|
||||
hostapd_cli -i "$iface" deny_acl DEL_MAC "$mac" >/dev/null 2>&1
|
||||
fi
|
||||
if ! tctl_hostapd_acl_blocks "$iface" "$mac" "$mode"; then
|
||||
# The ACL permits the MAC again, but a ban left over from an
|
||||
# earlier hostapd_cli-less block would still keep it off the air,
|
||||
# and there is no ubus method to lift one.
|
||||
if tctl_hostapd_ubus_banned "$iface" "$mac"; then
|
||||
echo ban
|
||||
else
|
||||
echo acl
|
||||
fi
|
||||
return
|
||||
fi
|
||||
fi
|
||||
if tctl_hostapd_ubus_banned "$iface" "$mac"; then
|
||||
echo ban
|
||||
return
|
||||
fi
|
||||
echo none
|
||||
}
|
||||
|
||||
# Applies $2 ("allow"/"deny" ACL policy) to every running AP and echoes the
|
||||
# worst per-AP outcome. Always safe to re-run: it enforces from the runtime
|
||||
# state, so calling it on a MAC that uci already lists is how a block that was
|
||||
# only ever written to config gets applied for real.
|
||||
tctl_hostapd_apply_mac() {
|
||||
local op="$1" mac="$2" mode="$3"
|
||||
local ifaces iface state rank worst=3
|
||||
ifaces=$(tctl_get_hostapd_ifaces) || { echo none; return 1; }
|
||||
if [ -z "$ifaces" ]; then
|
||||
echo no-radio
|
||||
return 0
|
||||
fi
|
||||
for iface in $ifaces; do
|
||||
case "$op" in
|
||||
block) state=$(tctl_hostapd_block_iface "$iface" "$mac" "$mode") ;;
|
||||
*) state=$(tctl_hostapd_unblock_iface "$iface" "$mac" "$mode") ;;
|
||||
esac
|
||||
rank=$(tctl_enforce_rank "$state")
|
||||
[ "$rank" -lt "$worst" ] && worst="$rank"
|
||||
done
|
||||
case "$worst" in
|
||||
3) echo acl; return 0 ;;
|
||||
2) echo ban ;;
|
||||
*) echo none ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
tctl_hostapd_block_mac() {
|
||||
local mac="$1" mode="$2"
|
||||
local iface
|
||||
for iface in $(tctl_get_hostapd_ifaces); do
|
||||
if [ "$mode" = "allow" ]; then
|
||||
hostapd_cli -i "$iface" accept_acl DEL_MAC "$mac" 2>/dev/null
|
||||
else
|
||||
hostapd_cli -i "$iface" deny_acl ADD_MAC "$mac" 2>/dev/null
|
||||
fi
|
||||
hostapd_cli -i "$iface" deauthenticate "$mac" 2>/dev/null
|
||||
done
|
||||
tctl_hostapd_apply_mac block "$1" "$2"
|
||||
}
|
||||
|
||||
# Unblock a MAC at runtime — the inverse of tctl_hostapd_block_mac.
|
||||
tctl_hostapd_unblock_mac() {
|
||||
local mac="$1" mode="$2"
|
||||
local iface
|
||||
for iface in $(tctl_get_hostapd_ifaces); do
|
||||
if [ "$mode" = "allow" ]; then
|
||||
hostapd_cli -i "$iface" accept_acl ADD_MAC "$mac" 2>/dev/null
|
||||
else
|
||||
hostapd_cli -i "$iface" deny_acl DEL_MAC "$mac" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
tctl_hostapd_apply_mac unblock "$1" "$2"
|
||||
}
|
||||
|
||||
tctl_hostapd_deny_mac() {
|
||||
local mac="$1"
|
||||
local iface
|
||||
for iface in $(tctl_get_hostapd_ifaces); do
|
||||
hostapd_cli -i "$iface" deny_acl ADD_MAC "$mac" 2>/dev/null
|
||||
hostapd_cli -i "$iface" deauthenticate "$mac" 2>/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
# Remove MAC from hostapd deny ACL at runtime (client can reassociate immediately)
|
||||
tctl_hostapd_allow_mac() {
|
||||
local mac="$1"
|
||||
local iface
|
||||
for iface in $(tctl_get_hostapd_ifaces); do
|
||||
hostapd_cli -i "$iface" deny_acl DEL_MAC "$mac" 2>/dev/null
|
||||
done
|
||||
# A device listed as WiFi-blocked while it is associated on a radio right now
|
||||
# is proof the running ACL does not carry the block — the state a silent
|
||||
# enforcement failure leaves a router in. Keyed off conn_type rather than the
|
||||
# station dump so it still holds where iw is missing and the connection type
|
||||
# came from the bridge port instead.
|
||||
tctl_wifi_block_pending() {
|
||||
local blocked="$1" conn_type="$2"
|
||||
[ "$blocked" = "1" ] || return 1
|
||||
case "$conn_type" in
|
||||
wifi|2.4G|5G|6G) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Persistence ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=dash
|
||||
# Block device WiFi access by adding its MAC to deny maclist on all interfaces.
|
||||
# Uses hostapd to deauth only the target client — no wifi reload needed.
|
||||
# The uci maclist is the durable half; hostapd is then programmed at runtime so
|
||||
# only the target client is kicked, with no wifi reload. Reports how far that
|
||||
# runtime half actually got — see the enforcement words in trafficctl-fw.sh.
|
||||
# Usage: trafficctl-macfilter-add.sh <ip>
|
||||
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
@@ -76,11 +78,41 @@ for iface in $IFACES; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CHANGED" = "1" ]; then
|
||||
uci commit wireless
|
||||
# Apply at runtime and deauth just this client — no wifi reload.
|
||||
tctl_hostapd_block_mac "$MAC" "$MODE"
|
||||
fi
|
||||
[ "$CHANGED" = "1" ] && uci commit wireless
|
||||
|
||||
tctl_log "wifi_block" "$IP" "MAC=$MAC" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
echo "{\"ok\":true,\"msg\":\"MAC $MAC blocked on wifi for $IP\"}"
|
||||
# Applied unconditionally, not just when uci needed editing. Config and runtime
|
||||
# are two pieces of state: a router whose maclist already names the MAC but
|
||||
# whose radio never got the ACL entry used to make this a no-op that still
|
||||
# reported success, so pressing Block again — the obvious reaction to "it did
|
||||
# not work" — could never recover it.
|
||||
ENFORCE=$(tctl_hostapd_block_mac "$MAC" "$MODE")
|
||||
BAN_MIN=$(( TCTL_WIFI_BAN_MS / 60000 ))
|
||||
|
||||
# The uci entry is kept even when the radio could not be programmed. It is the
|
||||
# only durable record of the operator's intent and it does take effect at the
|
||||
# next wifi restart, so discarding it would throw away the half that worked and
|
||||
# leave the device unblocked forever. What made keeping it dangerous was the
|
||||
# claim of success, not the entry — so the claim is what goes. A wifi reload
|
||||
# would apply it now, but it disconnects every client on the radio, which is
|
||||
# not something a per-device click should do behind the operator's back.
|
||||
case "$ENFORCE" in
|
||||
acl)
|
||||
OK=true
|
||||
MSG="MAC $MAC blocked on wifi for $IP"
|
||||
;;
|
||||
no-radio)
|
||||
OK=true
|
||||
MSG="MAC $MAC added to the wifi deny list for $IP; no radio is running, it applies when wifi starts"
|
||||
;;
|
||||
ban)
|
||||
OK=false
|
||||
MSG="MAC $MAC is NOT permanently blocked: hostapd_cli is missing, so the block is temporary ($BAN_MIN min). Install hostapd-utils, or restart wifi (drops all clients)"
|
||||
;;
|
||||
*)
|
||||
OK=false
|
||||
MSG="MAC $MAC saved to the wifi deny list but NOT applied to the radio, so the device stays online. Install hostapd-utils, or restart wifi (drops all clients)"
|
||||
;;
|
||||
esac
|
||||
|
||||
tctl_log "wifi_block" "$IP" "MAC=$MAC enforce=$ENFORCE" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
printf '{"ok":%s,"enforcement":"%s","msg":"%s"}\n' "$OK" "$ENFORCE" "$MSG"
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=dash
|
||||
# Remove device WiFi MAC filter (unblock from wifi deny list).
|
||||
# Uses hostapd beacon update — no wifi reload, other clients stay connected.
|
||||
# Updates the uci maclist and the running hostapd ACL, so the client can
|
||||
# reassociate without a wifi reload and other clients stay connected. Reports
|
||||
# how far the runtime half got — see the enforcement words in trafficctl-fw.sh.
|
||||
# Usage: trafficctl-macfilter-remove.sh <ip>
|
||||
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
@@ -68,11 +70,32 @@ for iface in $IFACES; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CHANGED" = "1" ]; then
|
||||
uci commit wireless
|
||||
# Update the runtime ACL — the client can reassociate immediately.
|
||||
tctl_hostapd_unblock_mac "$MAC" "$MODE"
|
||||
fi
|
||||
[ "$CHANGED" = "1" ] && uci commit wireless
|
||||
|
||||
tctl_log "wifi_unblock" "$IP" "MAC=$MAC" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
echo "{\"ok\":true,\"msg\":\"MAC $MAC removed from wifi filter for $IP\"}"
|
||||
# Unconditional for the same reason as the add path, and the stakes are higher
|
||||
# here: a MAC that uci no longer lists but the radio still denies leaves a real
|
||||
# person off the network while the UI says they are not blocked.
|
||||
ENFORCE=$(tctl_hostapd_unblock_mac "$MAC" "$MODE")
|
||||
BAN_MIN=$(( TCTL_WIFI_BAN_MS / 60000 ))
|
||||
|
||||
case "$ENFORCE" in
|
||||
acl)
|
||||
OK=true
|
||||
MSG="MAC $MAC removed from wifi filter for $IP"
|
||||
;;
|
||||
no-radio)
|
||||
OK=true
|
||||
MSG="MAC $MAC removed from the wifi filter for $IP; no radio is running"
|
||||
;;
|
||||
ban)
|
||||
OK=false
|
||||
MSG="MAC $MAC removed from the wifi filter, but hostapd still bans it for up to $BAN_MIN min. Restart wifi (drops all clients) to clear it now"
|
||||
;;
|
||||
*)
|
||||
OK=false
|
||||
MSG="MAC $MAC removed from the wifi filter, but the radio ACL could not be read back: it may still be blocked. Install hostapd-utils, or restart wifi"
|
||||
;;
|
||||
esac
|
||||
|
||||
tctl_log "wifi_unblock" "$IP" "MAC=$MAC enforce=$ENFORCE" "${TCTL_VIA:-cli}" "${TCTL_SRC:-local}"
|
||||
printf '{"ok":%s,"enforcement":"%s","msg":"%s"}\n' "$OK" "$ENFORCE" "$MSG"
|
||||
@@ -436,16 +436,21 @@ for ip in $ACTIVE_IPS; do
|
||||
fi
|
||||
fi
|
||||
|
||||
# Needs CONN_TYPE, so it is decided here rather than next to WIFI_BLK.
|
||||
WIFI_PENDING=0
|
||||
tctl_wifi_block_pending "$WIFI_BLK" "$CONN_TYPE" && WIFI_PENDING=1
|
||||
|
||||
if [ "$FIRST" = "1" ]; then
|
||||
FIRST=0
|
||||
else
|
||||
printf ","
|
||||
fi
|
||||
printf '{"ip":"%s","name":"%s","mac":"%s","conn_type":"%s","conn_last":"%s","app":"%s","conns":%d,"total":%.0f,"tcp":%.0f,"udp":%.0f,"blocked":%s,"block_bytes":%.0f,"wifi_blocked":%s,"rate_limit_kbit":%.0f,"shape_kbit":%.0f}' \
|
||||
printf '{"ip":"%s","name":"%s","mac":"%s","conn_type":"%s","conn_last":"%s","app":"%s","conns":%d,"total":%.0f,"tcp":%.0f,"udp":%.0f,"blocked":%s,"block_bytes":%.0f,"wifi_blocked":%s,"wifi_block_pending":%s,"rate_limit_kbit":%.0f,"shape_kbit":%.0f}' \
|
||||
"$ip" "$NAME" "$MAC" "$CONN_TYPE" "$CONN_LAST" "$APP" "$CONNS" "$TOTAL" "$TCP" "$UDP" \
|
||||
"$([ "$BLOCKED" = "1" ] && echo true || echo false)" \
|
||||
"$BLOCK_BYTES" \
|
||||
"$([ "$WIFI_BLK" = "1" ] && echo true || echo false)" \
|
||||
"$([ "$WIFI_PENDING" = "1" ] && echo true || echo false)" \
|
||||
"$RATE_LIM" "$SHAPE"
|
||||
done
|
||||
printf "]\n"
|
||||
|
||||
@@ -97,6 +97,14 @@ tg_send() {
|
||||
|
||||
tg_answer_cb() {
|
||||
local cb_id="$1" text="$2"
|
||||
# The Bot API caps a callback toast at 200 characters and rejects the whole
|
||||
# call when it is longer — so an over-long message does not get trimmed, it
|
||||
# vanishes. Backend results carrying remediation advice can reach that
|
||||
# length, and a silently missing answer is exactly the failure mode this
|
||||
# plugin is being fixed for.
|
||||
if [ "${#text}" -gt 200 ]; then
|
||||
text="$(printf '%.196s' "$text")..."
|
||||
fi
|
||||
text=$(tg_json_escape "$text")
|
||||
tg_api "answerCallbackQuery" \
|
||||
"$(printf '{"callback_query_id":"%s","text":"%s"}' "$cb_id" "$text")" >/dev/null
|
||||
|
||||
Reference in new issue
Block a user