Fix clock filtering and contact lookup behavior bugs

This commit is contained in:
Jack Kingsman committed 2026-01-26 22:51:02 -08:00
1 parent 2b681e1905
commit 00697e3c06
13 files changed
+133 -43

No files matched your search

+16 -8
View File
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
from meshcore import EventType
from app.models import Contact
from app.models import CONTACT_TYPE_REPEATER, Contact
from app.packet_processor import process_raw_packet
from app.repository import ContactRepository, MessageRepository
from app.websocket import broadcast_event
@@ -71,11 +71,20 @@ async def on_contact_message(event: "Event") -> None:
sender_pubkey = payload.get("public_key") or payload.get("pubkey_prefix", "")
received_at = int(time.time())
# Look up full public key from contact database if we only have prefix
if len(sender_pubkey) < 64:
contact = await ContactRepository.get_by_key_prefix(sender_pubkey)
if contact:
sender_pubkey = contact.public_key
# Look up contact from database - use prefix lookup only if needed
# (get_by_key_or_prefix does exact match first, then prefix fallback)
contact = await ContactRepository.get_by_key_or_prefix(sender_pubkey)
if contact:
sender_pubkey = contact.public_key
# Skip messages from repeaters - they only send CLI responses, not chat messages.
# CLI responses are handled by the command endpoint and txt_type filter above.
if contact.type == CONTACT_TYPE_REPEATER:
logger.debug(
"Skipping message from repeater %s (not stored in chat history)",
sender_pubkey[:12],
)
return
# Try to create message - INSERT OR IGNORE handles duplicates atomically
# If the packet processor already stored this message, this returns None
@@ -121,8 +130,7 @@ async def on_contact_message(event: "Event") -> None:
},
)
# Update contact last_contacted
contact = await ContactRepository.get_by_key_prefix(sender_pubkey)
# Update contact last_contacted (contact was already fetched above)
if contact:
await ContactRepository.update_last_contacted(contact.public_key, received_at)
+3
View File
@@ -204,6 +204,9 @@ class TelemetryResponse(BaseModel):
default_factory=list, description="List of neighbors seen by repeater"
)
acl: list[AclEntry] = Field(default_factory=list, description="Access control list")
clock_output: str | None = Field(
default=None, description="Output from 'clock' command (or error message)"
)
class CommandRequest(BaseModel):
+6 -5
View File
@@ -212,13 +212,14 @@ async def create_dm_message_from_decrypted(
Returns the message ID if created, None if duplicate.
"""
# Extract txt_type from flags (lower 4 bits)
# txt_type=1 is CLI response - don't store these in chat history
txt_type = decrypted.flags & 0x0F
if txt_type == 1:
# Check if sender is a repeater - repeaters only send CLI responses, not chat messages.
# CLI responses are handled by the command endpoint, not stored in chat history.
contact = await ContactRepository.get_by_key_or_prefix(their_public_key)
if contact and contact.type == CONTACT_TYPE_REPEATER:
logger.debug(
"Skipping CLI response from %s (txt_type=1)",
"Skipping message from repeater %s (CLI responses not stored): %s",
their_public_key[:12],
(decrypted.message or "")[:50],
)
return None
+42
View File
@@ -361,8 +361,49 @@ async def request_telemetry(public_key: str, request: TelemetryRequest) -> Telem
)
)
# Fetch clock output (up to 2 attempts)
# Must pause polling and stop auto-fetch to prevent race condition where
# the CLI response is consumed before we can call get_msg()
logger.info("Fetching clock from repeater %s", contact.public_key[:12])
clock_output: str | None = None
async with pause_polling():
await mc.stop_auto_message_fetching()
try:
for attempt in range(1, 3):
logger.debug("Clock request attempt %d/2", attempt)
try:
send_result = await mc.commands.send_cmd(contact.public_key, "clock")
if send_result.type == EventType.ERROR:
logger.debug("Clock command send error: %s", send_result.payload)
continue
# Wait for response
wait_result = await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=5.0)
if wait_result is None:
logger.debug("Clock request timeout, retrying...")
continue
response_event = await mc.commands.get_msg()
if response_event.type == EventType.ERROR:
logger.debug("Clock get_msg error: %s", response_event.payload)
continue
clock_output = response_event.payload.get("text", "")
logger.info("Received clock output: %s", clock_output)
break
except Exception as e:
logger.debug("Clock request exception: %s", e)
continue
finally:
await mc.start_auto_message_fetching()
if clock_output is None:
clock_output = "Unable to fetch `clock` output (repeater did not respond)"
# Convert raw telemetry to response format
# bat is in mV, convert to V (e.g., 3775 -> 3.775)
return TelemetryResponse(
pubkey_prefix=status.get("pubkey_pre", contact.public_key[:12]),
battery_volts=status.get("bat", 0) / 1000.0,
@@ -384,6 +425,7 @@ async def request_telemetry(public_key: str, request: TelemetryRequest) -> Telem
full_events=status.get("full_evts", 0),
neighbors=neighbors,
acl=acl_entries,
clock_output=clock_output,
)
@@ -38,7 +38,7 @@ var Ey=Object.defineProperty;var ky=(n,s,i)=>s in n?Ey(n,s,{enumerable:!0,config
`).replace(ee,"")}function he(e,r,a){if(r=le(r),le(e)!==r&&a)throw Error(i(425))}function be(){}var Me=null,et=null;function ht(e,r){return e==="textarea"||e==="noscript"||typeof r.children=="string"||typeof r.children=="number"||typeof r.dangerouslySetInnerHTML=="object"&&r.dangerouslySetInnerHTML!==null&&r.dangerouslySetInnerHTML.__html!=null}var dn=typeof setTimeout=="function"?setTimeout:void 0,ln=typeof clearTimeout=="function"?clearTimeout:void 0,On=typeof Promise=="function"?Promise:void 0,Tn=typeof queueMicrotask=="function"?queueMicrotask:typeof On<"u"?function(e){return On.resolve(null).then(e).catch(eo)}:dn;function eo(e){setTimeout(function(){throw e})}function za(e,r){var a=r,p=0;do{var y=a.nextSibling;if(e.removeChild(a),y&&y.nodeType===8)if(a=y.data,a==="/$"){if(p===0){e.removeChild(y),rr(r);return}p--}else a!=="$"&&a!=="$?"&&a!=="$!"||p++;a=y}while(a);rr(r)}function Jr(e){for(;e!=null;e=e.nextSibling){var r=e.nodeType;if(r===1||r===3)break;if(r===8){if(r=e.data,r==="$"||r==="$!"||r==="$?")break;if(r==="/$")return null}}return e}function ou(e){e=e.previousSibling;for(var r=0;e;){if(e.nodeType===8){var a=e.data;if(a==="$"||a==="$!"||a==="$?"){if(r===0)return e;r--}else a==="/$"&&r++}e=e.previousSibling}return null}var vi=Math.random().toString(36).slice(2),yr="__reactFiber$"+vi,to="__reactProps$"+vi,Ft="__reactContainer$"+vi,no="__reactEvents$"+vi,Gd="__reactListeners$"+vi,$d="__reactHandles$"+vi;function Ai(e){var r=e[yr];if(r)return r;for(var a=e.parentNode;a;){if(r=a[Ft]||a[yr]){if(a=r.alternate,r.child!==null||a!==null&&a.child!==null)for(e=ou(e);e!==null;){if(a=e[yr])return a;e=ou(e)}return r}e=a,a=e.parentNode}return null}function yi(e){return e=e[yr]||e[Ft],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function _i(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(i(33))}function Ls(e){return e[to]||null}var Kd=[],Ms=-1;function ro(e){return{current:e}}function qt(e){0>Ms||(e.current=Kd[Ms],Kd[Ms]=null,Ms--)}function Zt(e,r){Ms++,Kd[Ms]=e.current,e.current=r}var io={},Dn=ro(io),qn=ro(!1),Io=io;function Is(e,r){var a=e.type.contextTypes;if(!a)return io;var p=e.stateNode;if(p&&p.__reactInternalMemoizedUnmaskedChildContext===r)return p.__reactInternalMemoizedMaskedChildContext;var y={},S;for(S in a)y[S]=r[S];return p&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=r,e.__reactInternalMemoizedMaskedChildContext=y),y}function Gn(e){return e=e.childContextTypes,e!=null}function su(){qt(qn),qt(Dn)}function Ah(e,r,a){if(Dn.current!==io)throw Error(i(168));Zt(Dn,r),Zt(qn,a)}function yh(e,r,a){var p=e.stateNode;if(r=r.childContextTypes,typeof p.getChildContext!="function")return a;p=p.getChildContext();for(var y in p)if(!(y in r))throw Error(i(108,Ae(e)||"Unknown",y));return Z({},a,p)}function au(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||io,Io=Dn.current,Zt(Dn,e),Zt(qn,qn.current),!0}function _h(e,r,a){var p=e.stateNode;if(!p)throw Error(i(169));a?(e=yh(e,r,Io),p.__reactInternalMemoizedMergedChildContext=e,qt(qn),qt(Dn),Zt(Dn,e)):qt(qn),Zt(qn,a)}var wi=null,lu=!1,Qd=!1;function wh(e){wi===null?wi=[e]:wi.push(e)}function ZA(e){lu=!0,wh(e)}function oo(){if(!Qd&&wi!==null){Qd=!0;var e=0,r=Ct;try{var a=wi;for(Ct=1;e<a.length;e++){var p=a[e];do p=p(!0);while(p!==null)}wi=null,lu=!1}catch(y){throw wi!==null&&(wi=wi.slice(e+1)),Ll(ma,oo),y}finally{Ct=r,Qd=!1}}return null}var Os=[],js=0,uu=null,cu=0,_r=[],wr=0,Oo=null,bi=1,Ci="";function jo(e,r){Os[js++]=cu,Os[js++]=uu,uu=e,cu=r}function bh(e,r,a){_r[wr++]=bi,_r[wr++]=Ci,_r[wr++]=Oo,Oo=e;var p=bi;e=Ci;var y=32-ct(p)-1;p&=~(1<<y),a+=1;var S=32-ct(r)+y;if(30<S){var z=y-y%5;S=(p&(1<<z)-1).toString(32),p>>=z,y-=z,bi=1<<32-ct(r)+y|a<<y|p,Ci=S+e}else bi=1<<S|a<<y|p,Ci=e}function Yd(e){e.return!==null&&(jo(e,1),bh(e,1,0))}function Xd(e){for(;e===uu;)uu=Os[--js],Os[js]=null,cu=Os[--js],Os[js]=null;for(;e===Oo;)Oo=_r[--wr],_r[wr]=null,Ci=_r[--wr],_r[wr]=null,bi=_r[--wr],_r[wr]=null}var lr=null,ur=null,Kt=!1,Lr=null;function Ch(e,r){var a=kr(5,null,null,0);a.elementType="DELETED",a.stateNode=r,a.return=e,r=e.deletions,r===null?(e.deletions=[a],e.flags|=16):r.push(a)}function Eh(e,r){switch(e.tag){case 5:var a=e.type;return r=r.nodeType!==1||a.toLowerCase()!==r.nodeName.toLowerCase()?null:r,r!==null?(e.stateNode=r,lr=e,ur=Jr(r.firstChild),!0):!1;case 6:return r=e.pendingProps===""||r.nodeType!==3?null:r,r!==null?(e.stateNode=r,lr=e,ur=null,!0):!1;case 13:return r=r.nodeType!==8?null:r,r!==null?(a=Oo!==null?{id:bi,overflow:Ci}:null,e.memoizedState={dehydrated:r,treeContext:a,retryLane:1073741824},a=kr(18,null,null,0),a.stateNode=r,a.return=e,e.child=a,lr=e,ur=null,!0):!1;default:return!1}}function Jd(e){return(e.mode&1)!==0&&(e.flags&128)===0}function ef(e){if(Kt){var r=ur;if(r){var a=r;if(!Eh(e,r)){if(Jd(e))throw Error(i(418));r=Jr(a.nextSibling);var p=lr;r&&Eh(e,r)?Ch(p,a):(e.flags=e.flags&-4097|2,Kt=!1,lr=e)}}else{if(Jd(e))throw Error(i(418));e.flags=e.flags&-4097|2,Kt=!1,lr=e}}}function kh(e){for(e=e. Line truncated
Error generating stack: `+S.message+`
`+S.stack}return{value:e,source:r,stack:y,digest:null}}function bf(e,r,a){return{value:e,source:null,stack:a??null,digest:r??null}}function Cf(e,r){try{console.error(r.value)}catch(a){setTimeout(function(){throw a})}}var JA=typeof WeakMap=="function"?WeakMap:Map;function sp(e,r,a){a=ki(-1,a),a.tag=3,a.payload={element:null};var p=r.value;return a.callback=function(){Tu||(Tu=!0,jf=p),Cf(e,r)},a}function ap(e,r,a){a=ki(-1,a),a.tag=3;var p=e.type.getDerivedStateFromError;if(typeof p=="function"){var y=r.value;a.payload=function(){return p(y)},a.callback=function(){Cf(e,r)}}var S=e.stateNode;return S!==null&&typeof S.componentDidCatch=="function"&&(a.callback=function(){Cf(e,r),typeof p!="function"&&(uo===null?uo=new Set([this]):uo.add(this));var z=r.stack;this.componentDidCatch(r.value,{componentStack:z!==null?z:""})}),a}function lp(e,r,a){var p=e.pingCache;if(p===null){p=e.pingCache=new JA;var y=new Set;p.set(r,y)}else y=p.get(r),y===void 0&&(y=new Set,p.set(r,y));y.has(a)||(y.add(a),e=hy.bind(null,e,r,a),r.then(e,e))}function up(e){do{var r;if((r=e.tag===13)&&(r=e.memoizedState,r=r!==null?r.dehydrated!==null:!0),r)return e;e=e.return}while(e!==null);return null}function cp(e,r,a,p,y){return(e.mode&1)===0?(e===r?e.flags|=65536:(e.flags|=128,a.flags|=131072,a.flags&=-52805,a.tag===1&&(a.alternate===null?a.tag=17:(r=ki(-1,1),r.tag=2,ao(a,r,1))),a.lanes|=1),e):(e.flags|=65536,e.lanes=y,e)}var ey=T.ReactCurrentOwner,$n=!1;function jn(e,r,a,p){r.child=e===null?Th(r,null,a,p):Hs(r,e.child,a,p)}function dp(e,r,a,p,y){a=a.render;var S=r.ref;return Us(r,y),p=mf(e,r,a,p,S,y),a=gf(),e!==null&&!$n?(r.updateQueue=e.updateQueue,r.flags&=-2053,e.lanes&=~y,Si(e,r,y)):(Kt&&a&&Yd(r),r.flags|=1,jn(e,r,p,y),r.child)}function fp(e,r,a,p,y){if(e===null){var S=a.type;return typeof S=="function"&&!qf(S)&&S.defaultProps===void 0&&a.compare===null&&a.defaultProps===void 0?(r.tag=15,r.type=S,hp(e,r,S,p,y)):(e=Mu(a.type,null,p,r,r.mode,y),e.ref=r.ref,e.return=r,r.child=e)}if(S=e.child,(e.lanes&y)===0){var z=S.memoizedProps;if(a=a.compare,a=a!==null?a:Qr,a(z,p)&&e.ref===r.ref)return Si(e,r,y)}return r.flags|=1,e=po(S,p),e.ref=r.ref,e.return=r,r.child=e}function hp(e,r,a,p,y){if(e!==null){var S=e.memoizedProps;if(Qr(S,p)&&e.ref===r.ref)if($n=!1,r.pendingProps=p=S,(e.lanes&y)!==0)(e.flags&131072)!==0&&($n=!0);else return r.lanes=e.lanes,Si(e,r,y)}return Ef(e,r,a,p,y)}function pp(e,r,a){var p=r.pendingProps,y=p.children,S=e!==null?e.memoizedState:null;if(p.mode==="hidden")if((r.mode&1)===0)r.memoizedState={baseLanes:0,cachePool:null,transitions:null},Zt(Gs,cr),cr|=a;else{if((a&1073741824)===0)return e=S!==null?S.baseLanes|a:a,r.lanes=r.childLanes=1073741824,r.memoizedState={baseLanes:e,cachePool:null,transitions:null},r.updateQueue=null,Zt(Gs,cr),cr|=e,null;r.memoizedState={baseLanes:0,cachePool:null,transitions:null},p=S!==null?S.baseLanes:a,Zt(Gs,cr),cr|=p}else S!==null?(p=S.baseLanes|a,r.memoizedState=null):p=a,Zt(Gs,cr),cr|=p;return jn(e,r,y,a),r.child}function mp(e,r){var a=r.ref;(e===null&&a!==null||e!==null&&e.ref!==a)&&(r.flags|=512,r.flags|=2097152)}function Ef(e,r,a,p,y){var S=Gn(a)?Io:Dn.current;return S=Is(r,S),Us(r,y),a=mf(e,r,a,p,S,y),p=gf(),e!==null&&!$n?(r.updateQueue=e.updateQueue,r.flags&=-2053,e.lanes&=~y,Si(e,r,y)):(Kt&&p&&Yd(r),r.flags|=1,jn(e,r,a,y),r.child)}function gp(e,r,a,p,y){if(Gn(a)){var S=!0;au(r)}else S=!1;if(Us(r,y),r.stateNode===null)Eu(e,r),ip(r,a,p),wf(r,a,p,y),p=!0;else if(e===null){var z=r.stateNode,X=r.memoizedProps;z.props=X;var ne=z.context,me=a.contextType;typeof me=="object"&&me!==null?me=br(me):(me=Gn(a)?Io:Dn.current,me=Is(r,me));var Se=a.getDerivedStateFromProps,Te=typeof Se=="function"||typeof z.getSnapshotBeforeUpdate=="function";Te||typeof z.UNSAFE_componentWillReceiveProps!="function"&&typeof z.componentWillReceiveProps!="function"||(X!==p||ne!==me)&&op(r,z,p,me),so=!1;var ke=r.memoizedState;z.state=ke,gu(r,p,z,y),ne=r.memoizedState,X!==p||ke!==ne||qn.current||so?(typeof Se=="function"&&(_f(r,a,Se,p),ne=r.memoizedState),(X=so||rp(r,a,X,p,ke,ne,me))?(Te||typeof z.UNSAFE_componentWillMount!="function"&&typeof z.componentWillMount!="function"||(typeof z.componentWillMount=="function"&&z.componentWillMount(),typeof z.UNSAFE_componentWillMount=="function"&&z.UNSAFE_componentWillMount()),typeof z.componentDidMount=="function"&&(r.flags|=4194308)):(typeof z.componentDidMount=="function"&&(r.flags|=4194308),r.memoizedProps=p,r.memoizedState=ne),z.props=p,z.state=ne,z.context=me,p=X):(typeof z.componentDidMount=="function"&&(r.flags|=4194308),p=!1)}else{z=r.stateNode,Rh(e,r),X=r.memoizedProps,me=r.type===r.elementType?X:Mr(r.type,X),z.props=me,Te=r.pendingProps,ke=z.context,ne=a.contextType,typeof ne=="object"&&ne!==null?ne=br(ne):(ne=Gn(a)?Io:Dn.current,ne=Is(r,ne));var Ie=a.getDerivedStateFromProps;(Se=typeof Ie=="function"||typeof z.getSnapshotBeforeUpdate=="function")||typeof z.UNSAFE_componentWillReceiveProps!="function"&&typeof z.componentWillReceiveProps!="function"||(X!==Te||ke!==ne)&&op(r,z, Line truncated
`)}function Qs(n,s,i){const l=Math.floor(Date.now()/1e3);return{id:-Date.now(),type:"PRIV",conversation_key:n,text:s,sender_timestamp:l,received_at:l,paths:null,txt_type:0,signature:null,outgoing:i,acked:1}}function Vy(n){const s=N.useRef(null),i=N.useRef(null),l=N.useCallback(()=>{i.current!==null&&(clearInterval(i.current),i.current=null),s.current=null},[]),u=N.useCallback(async()=>{const h=s.current;if(!h)return;let d=null,g=null;for(let m=0;m<2;m++)try{d=await ut.requestTelemetry(h.conversationId,"");break}catch(x){g=x instanceof Error?x:new Error("Unknown error"),m===0&&await new Promise(v=>setTimeout(v,2e3))}if(d){const m=Qs(h.conversationId,Hy(h,d),!1);n(x=>[...x,m])}else{const m=Qs(h.conversationId,`Duty cycle poll failed after retry: ${(g==null?void 0:g.message)??"Unknown error"}`,!1);n(x=>[...x,m])}},[n]),c=N.useCallback(async(h,d)=>{const g=h.trim().toLowerCase();if(g==="dutycycle_start"){l();let m=null,x=null;for(let P=0;P<2;P++)try{m=await ut.requestTelemetry(d,"");break}catch(M){x=M instanceof Error?M:new Error("Unknown error"),P===0&&await new Promise(k=>setTimeout(k,2e3))}if(!m){const P=Qs(d,`Failed to start duty cycle tracking after retry: ${(x==null?void 0:x.message)??"Unknown error"}`,!1);return n(M=>[...M,P]),!0}const v=Math.floor(Date.now()/1e3);s.current={startTime:v,uptime:m.uptime_seconds,txAirtime:m.airtime_seconds,rxAirtime:m.rx_airtime_seconds,sentFlood:m.sent_flood,sentDirect:m.sent_direct,recvFlood:m.recv_flood,recvDirect:m.recv_direct,conversationId:d};const C=Qs(d,`Airtime/duty cycle statistics gathering begins at ${v}. Logs will follow every 5 minutes. To stop, run dutycycle_stop or navigate away from this conversation.`,!1);return n(P=>[...P,C]),i.current=window.setInterval(u,jy),!0}if(g==="dutycycle_stop"){if(s.current&&s.current.conversationId===d){await u(),l();const m=Qs(d,"Airtime/duty cycle statistics gathering stopped.",!1);n(x=>[...x,m])}else{const m=Qs(d,"Duty cycle tracking is not active.",!1);n(x=>[...x,m])}return!0}return!1},[n,l,u]);return N.useEffect(()=>()=>{i.current!==null&&clearInterval(i.current)},[]),{handleAirtimeCommand:c,stopTracking:l}}function oc(n){if(n<60)return`${n}s`;const s=Math.floor(n/86400),i=Math.floor(n%86400/3600),l=Math.floor(n%3600/60);return s>0?i>0&&l>0?`${s}d${i}h${l}m`:i>0?`${s}d${i}h`:l>0?`${s}d${l}m`:`${s}d`:i>0?l>0?`${i}h${l}m`:`${i}h`:`${l}m`}function Uy(n){return["Telemetry",`Battery Voltage: ${n.battery_volts.toFixed(3)}V`,`Uptime: ${oc(n.uptime_seconds)}`,`TX Airtime: ${oc(n.airtime_seconds)}`,`RX Airtime: ${oc(n.rx_airtime_seconds)}`,"",`Noise Floor: ${n.noise_floor_dbm} dBm`,`Last RSSI: ${n.last_rssi_dbm} dBm`,`Last SNR: ${n.last_snr_db.toFixed(1)} dB`,"",`Packets: ${n.packets_received.toLocaleString()} rx / ${n.packets_sent.toLocaleString()} tx`,`Flood: ${n.recv_flood.toLocaleString()} rx / ${n.sent_flood.toLocaleString()} tx`,`Direct: ${n.recv_direct.toLocaleString()} rx / ${n.sent_direct.toLocaleString()} tx`,`Duplicates: ${n.flood_dups.toLocaleString()} flood / ${n.direct_dups.toLocaleString()} direct`,"",`TX Queue: ${n.tx_queue_len}`,`Debug Flags: ${n.full_events}`].join(`
`)}function Qs(n,s,i){const l=Math.floor(Date.now()/1e3);return{id:-Date.now(),type:"PRIV",conversation_key:n,text:s,sender_timestamp:l,received_at:l,paths:null,txt_type:0,signature:null,outgoing:i,acked:1}}function Vy(n){const s=N.useRef(null),i=N.useRef(null),l=N.useCallback(()=>{i.current!==null&&(clearInterval(i.current),i.current=null),s.current=null},[]),u=N.useCallback(async()=>{const h=s.current;if(!h)return;let d=null,g=null;for(let m=0;m<2;m++)try{d=await ut.requestTelemetry(h.conversationId,"");break}catch(x){g=x instanceof Error?x:new Error("Unknown error"),m===0&&await new Promise(v=>setTimeout(v,2e3))}if(d){const m=Qs(h.conversationId,Hy(h,d),!1);n(x=>[...x,m])}else{const m=Qs(h.conversationId,`Duty cycle poll failed after retry: ${(g==null?void 0:g.message)??"Unknown error"}`,!1);n(x=>[...x,m])}},[n]),c=N.useCallback(async(h,d)=>{const g=h.trim().toLowerCase();if(g==="dutycycle_start"){l();let m=null,x=null;for(let P=0;P<2;P++)try{m=await ut.requestTelemetry(d,"");break}catch(M){x=M instanceof Error?M:new Error("Unknown error"),P===0&&await new Promise(k=>setTimeout(k,2e3))}if(!m){const P=Qs(d,`Failed to start duty cycle tracking after retry: ${(x==null?void 0:x.message)??"Unknown error"}`,!1);return n(M=>[...M,P]),!0}const v=Math.floor(Date.now()/1e3);s.current={startTime:v,uptime:m.uptime_seconds,txAirtime:m.airtime_seconds,rxAirtime:m.rx_airtime_seconds,sentFlood:m.sent_flood,sentDirect:m.sent_direct,recvFlood:m.recv_flood,recvDirect:m.recv_direct,conversationId:d};const C=Qs(d,`Airtime/duty cycle statistics gathering begins at ${v}. Logs will follow every 5 minutes. To stop, run dutycycle_stop or navigate away from this conversation.`,!1);return n(P=>[...P,C]),i.current=window.setInterval(u,jy),!0}if(g==="dutycycle_stop"){if(s.current&&s.current.conversationId===d){await u(),l();const m=Qs(d,"Airtime/duty cycle statistics gathering stopped.",!1);n(x=>[...x,m])}else{const m=Qs(d,"Duty cycle tracking is not active.",!1);n(x=>[...x,m])}return!0}return!1},[n,l,u]);return N.useEffect(()=>()=>{i.current!==null&&clearInterval(i.current)},[]),{handleAirtimeCommand:c,stopTracking:l}}function oc(n){if(n<60)return`${n}s`;const s=Math.floor(n/86400),i=Math.floor(n%86400/3600),l=Math.floor(n%3600/60);return s>0?i>0&&l>0?`${s}d${i}h${l}m`:i>0?`${s}d${i}h`:l>0?`${s}d${l}m`:`${s}d`:i>0?l>0?`${i}h${l}m`:`${i}h`:`${l}m`}function Uy(n){return["Telemetry",`Battery Voltage: ${n.battery_volts.toFixed(3)}V`,`Uptime: ${oc(n.uptime_seconds)}`,...n.clock_output?[`Clock: ${n.clock_output}`]:[],`TX Airtime: ${oc(n.airtime_seconds)}`,`RX Airtime: ${oc(n.rx_airtime_seconds)}`,"",`Noise Floor: ${n.noise_floor_dbm} dBm`,`Last RSSI: ${n.last_rssi_dbm} dBm`,`Last SNR: ${n.last_snr_db.toFixed(1)} dB`,"",`Packets: ${n.packets_received.toLocaleString()} rx / ${n.packets_sent.toLocaleString()} tx`,`Flood: ${n.recv_flood.toLocaleString()} rx / ${n.sent_flood.toLocaleString()} tx`,`Direct: ${n.recv_direct.toLocaleString()} rx / ${n.sent_direct.toLocaleString()} tx`,`Duplicates: ${n.flood_dups.toLocaleString()} flood / ${n.direct_dups.toLocaleString()} direct`,"",`TX Queue: ${n.tx_queue_len}`,`Debug Flags: ${n.full_events}`].join(`
`)}function Zy(n){if(n.length===0)return`Neighbors
No neighbors reported`;const s=[...n].sort((l,u)=>u.snr-l.snr),i=[`Neighbors (${s.length})`];for(const l of s){const u=l.name||l.pubkey_prefix,c=l.snr>=0?`+${l.snr.toFixed(1)}`:l.snr.toFixed(1);i.push(`${u}, ${c} dB [${oc(l.last_heard_seconds)} ago]`)}return i.join(`
`)}function Wy(n){if(n.length===0)return`ACL
@@ -599,4 +599,4 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
`),this.noSleepTimer=window.setInterval(()=>{document.hidden||(window.location.href=window.location.href.split("#")[0],window.setTimeout(window.stop,0))},15e3),this.enabled=!0,Promise.resolve()):this.noSleepVideo.play().then(d=>(this.enabled=!0,d)).catch(d=>{throw this.enabled=!1,d})}disable(){l()?(this._wakeLock&&this._wakeLock.release(),this._wakeLock=null):i()?this.noSleepTimer&&(console.warn(`
NoSleep now disabled for older iOS devices.
`),window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause(),this.enabled=!1}}return _0=u,_0}var CS=bS();const ES=ls(CS);function kS(n){if(n.length<4)return null;try{const i=parseInt(n.slice(0,2),16)&3;let l=2;if(i===0||i===3){if(n.length<l+8)return null;l+=8}if(n.length<l+2)return null;const u=parseInt(n.slice(l,l+2),16);l+=2;const c=u*2;return n.length<l+c?null:(l+=c,n.slice(l))}catch{return null}}function SS({packets:n,channels:s,onChannelCreate:i,onRunningChange:l,visible:u=!1}){const[c,h]=N.useState(!1),[d,g]=N.useState(6),[m,x]=N.useState(!1),[v,C]=N.useState(!0),[P,M]=N.useState(!1),[k,w]=N.useState(null),[B,b]=N.useState(new Map),[E,T]=N.useState([]),[F,I]=N.useState(!1),[j,O]=N.useState(null),[G,U]=N.useState(null),[K,J]=N.useState(0),Y=N.useRef(null),ie=N.useRef(null),ge=N.useRef(!1),$=N.useRef(!1),se=N.useRef(!1),W=N.useRef(new Map),te=N.useRef(!1),Z=N.useRef(6),D=N.useRef(!0),H=N.useRef(!1),q=N.useRef(new Set),Q=N.useRef(new Set);N.useEffect(()=>{const Ee=new _S;Y.current=Ee,O(Ee.isGpuAvailable());const je=new ES;return ie.current=je,()=>{Ee.destroy(),Y.current=null,je.disable(),ie.current=null}},[]),N.useEffect(()=>{!u||F||oS(async()=>{const{ENGLISH_WORDLIST:Ee}=await import("./wordlist-BtmChKSf.js");return{ENGLISH_WORDLIST:Ee}},[]).then(({ENGLISH_WORDLIST:Ee})=>{Y.current&&(Y.current.setWordlist(Ee),I(!0))}).catch(Ee=>{console.error("Failed to load wordlist:",Ee),At.error("Failed to load wordlist",{description:"Cracking will not be available"})})},[u,F]),N.useEffect(()=>{const Ee=()=>{ut.getUndecryptedPacketCount().then(({count:at})=>U(at)).catch(()=>U(null))};Ee();const je=setInterval(Ee,36e5);return()=>clearInterval(je)},[]);const ae=N.useMemo(()=>new Set(s.map(Ee=>Ee.key.toUpperCase())),[s]),ce=n.filter(Ee=>Ee.payload_type==="GROUP_TEXT"&&!Ee.decrypted);N.useEffect(()=>{let Ee=0;b(je=>{const at=new Map(je);let mt=!1;for(const ot of ce)if(!at.has(ot.id)){const qe=kS(ot.data);if(qe&&Q.current.has(qe)){Ee++;continue}qe&&Q.current.add(qe),at.set(ot.id,{packet:ot,attempts:0,lastAttemptLength:0,status:"pending"}),mt=!0}return mt?(W.current=at,at):je}),Ee>0&&J(je=>je+Ee)},[ce.length]),N.useEffect(()=>{W.current=B},[B]),N.useEffect(()=>{te.current=m},[m]),N.useEffect(()=>{Z.current=d},[d]),N.useEffect(()=>{D.current=v},[v]),N.useEffect(()=>{H.current=P},[P]),N.useEffect(()=>{q.current=new Set(ce.map(Ee=>Ee.id))},[ce]),N.useEffect(()=>{l==null||l(c)},[c,l]);const Ae=Array.from(B.values()).filter(Ee=>Ee.status==="pending").length,pe=Array.from(B.values()).filter(Ee=>Ee.status==="cracked").length,xe=Array.from(B.values()).filter(Ee=>Ee.status==="failed").length,Be=N.useCallback(async()=>{if(se.current||!Y.current||!ge.current)return;const Ee=W.current;let je=null,at=null;for(const[Ke,Qe]of Ee.entries())if(Qe.status==="pending"){je=Qe,at=Ke;break}if(!je&&te.current){const Ke=Array.from(Ee.entries()).filter(([,Qe])=>Qe.status==="failed"&&Qe.lastAttemptLength<10);Ke.length>0&&(Ke.sort((Qe,rt)=>Qe[1].lastAttemptLength-rt[1].lastAttemptLength),[at,je]=Ke[0])}if(!je||at===null){ge.current&&setTimeout(()=>Be(),1e3);return}if(!q.current.has(at)){b(Ke=>{const Qe=new Map(Ke);return Qe.delete(at),Qe}),ge.current&&setTimeout(()=>Be(),10);return}se.current=!0;const mt=Z.current,ot=je.lastAttemptLength>0,qe=ot?je.lastAttemptLength+1:mt;try{const Ke=await Y.current.crack(je.packet.data,{maxLength:qe,useSenderFilter:!0,useTimestampFilter:!0,useUtf8Filter:!0,...H.current&&{gpuDispatchMs:1e4},...ot&&{useDictionary:!1,startingLength:qe}},Qe=>{w(Qe)});if($.current){$.current=!1,se.current=!1,w(null);return}if(Ke.found&&Ke.roomName&&Ke.key){b(He=>{const Et=new Map(He),zt=Et.get(at);return zt&&Et.set(at,{...zt,status:"cracked",attempts:zt.attempts+1,lastAttemptLength:qe}),Et});const Qe={roomName:Ke.roomName,key:Ke.key,packetId:at,message:Ke.decryptedMessage||"",crackedAt:Date.now()};T(He=>[...He,Qe]);const rt=Ke.key.toUpperCase();if(!ae.has(rt))try{const He="#"+Ke.roomName;await i(He,Ke.key),D.current&&await ut.decryptHistoricalPackets({key_type:"channel",channel_name:He})}catch(He){console.error("Failed to create channel or decrypt historical:",He),At.error("Failed to save cracked channel",{description:He instanceof Error?He.message:"Channel discovered but could not be saved"})}}else b(Qe=>{const rt=new Map(Qe),He=rt.get(at);return He&&rt.set(at,{...He,status:"failed",attempts:He.attempts+1,lastAttemptLength:qe}),rt})}catch(Ke){console.error("Cracking error:",Ke),b(Qe=>{const rt=new Map(Qe),He=rt.get(at);return He&&rt.set(at,{...He,status:"failed",attempts:He.attempts+1,lastAttemptLength:qe}),rt})}se.current=!1,w(null),ge.current&&setTimeout(()=>Be(),100)},[ae,i]),Oe=()=>{var Ee;if(!j){At.error("WebGPU not available",{description:"Cracking requires Chrome 113+ or Edge 113+ with WebGPU support."});return}h(!0),ge.current=!0,$.current=!1,(Ee=ie.current)==null||Ee.enable(),Be()},Xe=()=>{var Ee,je;h(!1),ge.current=!1,$.current=!0,(Ee=Y.current)==null||Ee.abort(),(je=ie.current)==null|| Line truncated
//# sourceMappingURL=index-D5ozmcKB.js.map
//# sourceMappingURL=index-COA8MjNX.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"mappings":"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 truncated
-1
View File
@@ -1 +0,0 @@
{"version":3,"mappings":"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 truncated
+1 -1
View File
@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-D5ozmcKB.js"></script>
<script type="module" crossorigin src="/assets/index-COA8MjNX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-H2C92sGV.css">
</head>
<body>
+1
View File
@@ -37,6 +37,7 @@ export function formatTelemetry(telemetry: TelemetryResponse): string {
`Telemetry`,
`Battery Voltage: ${telemetry.battery_volts.toFixed(3)}V`,
`Uptime: ${formatDuration(telemetry.uptime_seconds)}`,
...(telemetry.clock_output ? [`Clock: ${telemetry.clock_output}`] : []),
`TX Airtime: ${formatDuration(telemetry.airtime_seconds)}`,
`RX Airtime: ${formatDuration(telemetry.rx_airtime_seconds)}`,
'',
@@ -78,6 +78,7 @@ describe('formatTelemetry', () => {
full_events: 0,
neighbors: [],
acl: [],
clock_output: null,
};
const result = formatTelemetry(telemetry);
@@ -119,6 +120,7 @@ describe('formatTelemetry', () => {
full_events: 0,
neighbors: [],
acl: [],
clock_output: null,
};
const result = formatTelemetry(telemetry);
+1
View File
@@ -197,6 +197,7 @@ export interface TelemetryResponse {
full_events: number;
neighbors: NeighborInfo[];
acl: AclEntry[];
clock_output: string | null;
}
export interface CommandResponse {
+3 -3
View File
@@ -195,7 +195,7 @@ class TestContactMessageCLIFiltering:
patch("app.bot.run_bot_for_message", new_callable=AsyncMock),
):
mock_repo.create = AsyncMock(return_value=42)
mock_contact_repo.get_by_key_prefix = AsyncMock(return_value=None)
mock_contact_repo.get_by_key_or_prefix = AsyncMock(return_value=None)
class MockEvent:
payload = {
@@ -224,7 +224,7 @@ class TestContactMessageCLIFiltering:
patch("app.bot.run_bot_for_message", new_callable=AsyncMock),
):
mock_repo.create = AsyncMock(return_value=42)
mock_contact_repo.get_by_key_prefix = AsyncMock(return_value=None)
mock_contact_repo.get_by_key_or_prefix = AsyncMock(return_value=None)
class MockEvent:
payload = {
@@ -259,7 +259,7 @@ class TestContactMessageCLIFiltering:
patch("app.bot.run_bot_for_message", new_callable=AsyncMock),
):
mock_repo.create = AsyncMock(return_value=42)
mock_contact_repo.get_by_key_prefix = AsyncMock(return_value=None)
mock_contact_repo.get_by_key_or_prefix = AsyncMock(return_value=None)
class MockEvent:
payload = {
+55 -23
View File
@@ -943,26 +943,47 @@ class TestDMDecryptionFunction:
assert packet_id not in [p.id for p in undecrypted]
class TestCLIResponseFiltering:
"""Test that CLI responses (txt_type=1) are not stored in chat history."""
class TestRepeaterMessageFiltering:
"""Test that messages from repeaters are not stored in chat history.
A1B2C3_PUB = "a1b2c3d3ba9f5fa8705b9845fe11cc6f01d1d49caaf4d122ac7121663c5beec7"
FACE12_PUB = "FACE123334789E2B81519AFDBC39A3C9EB7EA3457AD367D3243597A484847E46"
Repeaters only send CLI responses (not chat messages), and these are handled
by the command endpoint. The packet processor filters them out based on
contact type to prevent duplicate storage.
"""
# A repeater contact
REPEATER_PUB = "a1b2c3d3ba9f5fa8705b9845fe11cc6f01d1d49caaf4d122ac7121663c5beec7"
# A normal client contact
CLIENT_PUB = "b2c3d4e4cb0a6fb9816ca956ff22dd7f12e2e5adbbf5e233bd8232774d6cffe8"
# Our public key
OUR_PUB = "FACE123334789E2B81519AFDBC39A3C9EB7EA3457AD367D3243597A484847E46"
@pytest.mark.asyncio
async def test_cli_response_not_stored(self, test_db, captured_broadcasts):
"""CLI responses (flags & 0x0F == 1) should not be stored in database."""
async def test_repeater_message_not_stored(self, test_db, captured_broadcasts):
"""Messages from repeaters should not be stored in database."""
from app.decoder import DecryptedDirectMessage
from app.models import CONTACT_TYPE_REPEATER
from app.packet_processor import create_dm_message_from_decrypted
from app.repository import MessageRepository, RawPacketRepository
from app.repository import ContactRepository, MessageRepository, RawPacketRepository
# Store a raw packet first
# Create a repeater contact first
await ContactRepository.upsert(
{
"public_key": self.REPEATER_PUB,
"name": "Test Repeater",
"type": CONTACT_TYPE_REPEATER, # type=2 is repeater
"flags": 0,
"on_radio": False,
}
)
# Store a raw packet
packet_id, _ = await RawPacketRepository.create(b"\x09\x00test", 1700000000)
# Create a DecryptedDirectMessage with flags=1 (CLI response)
# Create a DecryptedDirectMessage (simulating a CLI response from repeater)
decrypted = DecryptedDirectMessage(
timestamp=1700000000,
flags=1, # txt_type=1 (CLI response)
flags=0, # flags don't matter - we filter by contact type
message="cli response: version 1.0",
dest_hash="fa",
src_hash="a1",
@@ -974,13 +995,13 @@ class TestCLIResponseFiltering:
msg_id = await create_dm_message_from_decrypted(
packet_id=packet_id,
decrypted=decrypted,
their_public_key=self.A1B2C3_PUB,
our_public_key=self.FACE12_PUB,
their_public_key=self.REPEATER_PUB,
our_public_key=self.OUR_PUB,
received_at=1700000001,
outgoing=False,
)
# Should return None (not stored)
# Should return None (not stored because sender is a repeater)
assert msg_id is None
# Should not broadcast
@@ -988,25 +1009,36 @@ class TestCLIResponseFiltering:
# Should not be in database
messages = await MessageRepository.get_all(
msg_type="PRIV", conversation_key=self.A1B2C3_PUB.lower(), limit=10
msg_type="PRIV", conversation_key=self.REPEATER_PUB.lower(), limit=10
)
assert len(messages) == 0
@pytest.mark.asyncio
async def test_normal_message_still_stored(self, test_db, captured_broadcasts):
"""Normal messages (flags & 0x0F == 0) should still be stored."""
async def test_client_message_still_stored(self, test_db, captured_broadcasts):
"""Messages from normal clients should still be stored."""
from app.decoder import DecryptedDirectMessage
from app.packet_processor import create_dm_message_from_decrypted
from app.repository import MessageRepository, RawPacketRepository
from app.repository import ContactRepository, MessageRepository, RawPacketRepository
# Create a normal client contact (type=1)
await ContactRepository.upsert(
{
"public_key": self.CLIENT_PUB,
"name": "Test Client",
"type": 1, # type=1 is client
"flags": 0,
"on_radio": False,
}
)
packet_id, _ = await RawPacketRepository.create(b"\x09\x00test2", 1700000000)
decrypted = DecryptedDirectMessage(
timestamp=1700000000,
flags=0, # txt_type=0 (normal message)
flags=0,
message="Hello, world!",
dest_hash="fa",
src_hash="a1",
src_hash="b2",
)
broadcasts, mock_broadcast = captured_broadcasts
@@ -1015,13 +1047,13 @@ class TestCLIResponseFiltering:
msg_id = await create_dm_message_from_decrypted(
packet_id=packet_id,
decrypted=decrypted,
their_public_key=self.A1B2C3_PUB,
our_public_key=self.FACE12_PUB,
their_public_key=self.CLIENT_PUB,
our_public_key=self.OUR_PUB,
received_at=1700000001,
outgoing=False,
)
# Should return message ID
# Should return message ID (stored because sender is a client)
assert msg_id is not None
# Should broadcast
@@ -1030,6 +1062,6 @@ class TestCLIResponseFiltering:
# Should be in database
messages = await MessageRepository.get_all(
msg_type="PRIV", conversation_key=self.A1B2C3_PUB.lower(), limit=10
msg_type="PRIV", conversation_key=self.CLIENT_PUB.lower(), limit=10
)
assert len(messages) == 1