#include "JS8_UI/mainwindow.h" /** \file * @brief member function of the MainWindow class * processes JS8 commands */ void MainWindow::processCommandActivity() { #if 0 if (!m_txFrameQueue.isEmpty()) { return; } #endif if (m_rxCommandQueue.isEmpty()) { return; } #if 0 bool processed = false; int f = currentFreq(); #endif auto now = DriftingDateTime::currentDateTimeUtc(); while (!m_rxCommandQueue.isEmpty()) { auto d = m_rxCommandQueue.dequeue(); auto selectedCallsign = callsignSelected(); bool isAllCall = isAllCallIncluded(d.to); bool isGroupCall = isGroupCallIncluded(d.to) || d.to == "@APRSIS"; qCDebug(mainwindow_js8) << "try processing command" << d.from << d.to << d.cmd << d.dial << d.offset << d.grid << d.extra << isAllCall << isGroupCall; // if we need a compound callsign but never got one...skip if (d.from == "<....>" || d.to == "<....>") { continue; } // we're only processing a subset of queries at this point if (!Varicode::isCommandAllowed(d.cmd)) { continue; } // is this to me? bool toMe = d.to == m_config.my_callsign().trimmed() || d.to == Radio::base_callsign(m_config.my_callsign()).trimmed(); // log call activity... CallDetail cd = {}; cd.call = d.from; cd.grid = d.grid; cd.snr = d.snr; cd.dial = d.dial; cd.offset = d.offset; cd.bits = d.bits; cd.ackTimestamp = d.text.contains(": ACK") || toMe ? d.utcTimestamp : QDateTime{}; cd.utcTimestamp = d.utcTimestamp; cd.tdrift = d.tdrift; cd.submode = d.submode; logCallActivity(cd, true); logHeardGraph(d.from, d.to); // Placeholder for callbacks to be passed to confirmThenEnqueueMessage // and enqueueMessage Callback callback = nullptr; // PROCESS BUFFERED HEARING FOR EVERYONE if (d.cmd == " HEARING") { // 1. parse callsigns // 2. log it to the heard graph auto calls = Varicode::parseCallsigns(d.text); foreach (auto call, calls) { logHeardGraph(d.from, call); } } // PROCESS BUFFERED GRID FOR EVERYONE if (d.cmd == " GRID") { // 1. parse grids // 2. log it to our call activity auto grids = Varicode::parseGrids(d.text); foreach (auto grid, grids) { CallDetail cd = {}; cd.bits = d.bits; cd.call = d.from; cd.dial = d.dial; cd.offset = d.offset; cd.grid = grid; cd.snr = d.snr; cd.utcTimestamp = d.utcTimestamp; cd.tdrift = d.tdrift; cd.submode = d.submode; // PROCESS GRID SPOTS TO APRSIS FOR EVERYONE /** * APRS relay: inbound `MSG TO:` stores for our local station. * Dedupe within 60s using TO + TEXT + DE . */ if (d.to == "@APRSIS") { spotAprsGrid(cd.dial, cd.offset, cd.snr, cd.call, cd.grid); } logCallActivity(cd, true); } } // PROCESS @JS8NET, @APRSIS, AND OTHER GROUP SPOTS FOR EVERYONE if (d.to.startsWith("@")) { spotCmd(d); } // PROCESS @APRSIS CMD SPOTS FOR EVERYONE if (d.to == "@APRSIS") { spotAprsCmd(d); } // PREPARE CMD TEXT STRING QStringList textList = { QString("%1: %2%3").arg(d.from).arg(d.to).arg(d.cmd)}; if (!d.extra.isEmpty()) { textList.append(d.extra); } if (!d.text.isEmpty()) { textList.append(d.text); } QString text = textList.join(" "); bool isLast = (d.bits & Varicode::JS8CallLast) == Varicode::JS8CallLast; if (isLast) { // append the eot character to the text text = QString("%1 %2 ") .arg(Varicode::rstrip(text)) .arg(m_config.eot()); } // log the text to directed txt log writeMsgTxt(text, d.snr, d.offset); // write all directed messages to api if (canSendNetworkMessage()) { sendNetworkMessage( "RX.DIRECTED", text, {{"_ID", QVariant(-1)}, {"FROM", QVariant(d.from)}, {"TO", QVariant(d.to)}, {"CMD", QVariant(d.cmd)}, {"GRID", QVariant(d.grid)}, {"EXTRA", QVariant(d.extra)}, {"TEXT", QVariant(d.text)}, {"FREQ", QVariant(d.dial + d.offset)}, {"DIAL", QVariant(d.dial)}, {"OFFSET", QVariant(d.offset)}, {"SNR", QVariant(d.snr)}, {"SPEED", QVariant(d.submode)}, {"TDRIFT", QVariant(d.tdrift)}, {"UTC", QVariant(d.utcTimestamp.toMSecsSinceEpoch())}}); } // we're only responding to allcalls if we are participating in the // allcall group but, don't avoid for heartbeats...those are technically // allcalls but are processed differently if (isAllCall && m_config.avoid_allcall() && d.cmd != " CQ" && d.cmd != " HB" && d.cmd != " HEARTBEAT") { continue; } // we're only responding to allcall, groupcalls, and our callsign at // this point, so we'll end after logging the callsigns we've heard if (!isAllCall && !toMe && !isGroupCall) { continue; } ActivityDetail ad = {}; ad.isLowConfidence = false; ad.isDirected = true; ad.bits = d.bits; ad.dial = d.dial; ad.offset = d.offset; ad.snr = d.snr; ad.text = text; ad.utcTimestamp = d.utcTimestamp; // we'd be double printing here if were on frequency, so let's be // "smart" about this... bool shouldDisplay = true; // don't display ping allcalls if (isAllCall && (d.cmd != " " || ad.text.contains("@HB HEARTBEAT"))) { shouldDisplay = false; } if (shouldDisplay) { auto c = ui->textEditRX->textCursor(); c.movePosition(QTextCursor::End); ui->textEditRX->setTextCursor(c); // ACKs and SNRs are the most likely source of items to be // overwritten (multiple responses at once)... so don't overwrite // those (i.e., print each on a new line) bool shouldOverwrite = (!d.cmd.contains(" ACK") && !d.cmd.contains(" SNR")); /* && isRecentOffset(d.freq);*/ if (shouldOverwrite && ui->textEditRX->find(d.utcTimestamp.time().toString(), QTextDocument::FindBackward)) { // ... maybe we could delete the last line that had this message // on this frequency... c = ui->textEditRX->textCursor(); c.movePosition(QTextCursor::StartOfBlock); c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); qCDebug(mainwindow_js8) << "should display directed message, " "erasing last rx activity line..." << c.selectedText().toUpper(); c.removeSelectedText(); c.deletePreviousChar(); c.deletePreviousChar(); /* c.deleteChar(); c.deleteChar(); */ } // log it to the display! displayTextForFreq(ad.text, ad.offset, ad.utcTimestamp, false, true, false); /* // and send it to the network in case we want to interact with it from an external app... if(canSendNetworkMessage()){ sendNetworkMessage("RX.DIRECTED.ME", ad.text, { {"_ID", QVariant(-1)}, {"FROM", QVariant(d.from)}, {"TO", QVariant(d.to)}, {"CMD", QVariant(d.cmd)}, {"GRID", QVariant(d.grid)}, {"EXTRA", QVariant(d.extra)}, {"TEXT", QVariant(d.text)}, {"FREQ", QVariant(ad.dial+ad.offset)}, {"DIAL", QVariant(ad.dial)}, {"OFFSET", QVariant(ad.offset)}, {"SNR", QVariant(ad.snr)}, {"SPEED", QVariant(ad.submode)}, {"TDRIFT", QVariant(ad.tdrift)}, {"UTC", QVariant(ad.utcTimestamp.toMSecsSinceEpoch())} }); } */ if (!isAllCall) { // If we've received a message to be displayed, // we should no longer call CQ if (m_cq_loop->isActive()) { qCDebug(mainwindow_js8) << "Canceling CQ loop to prioritize incoming messages, " "case II"; m_cq_loop->onLoopCancel(); } // notification for directed message tryNotify("directed"); } } // we're only responding to callsigns in our whitelist if we have one // defined... make sure the whitelist is empty (no restrictions) or the // from callsign or its base callsign is on it auto whitelist = m_config.auto_whitelist(); if (!whitelist.isEmpty() && !(whitelist.contains(d.from) || whitelist.contains(Radio::base_callsign(d.from)))) { qCDebug(mainwindow_js8) << "skipping command for whitelist" << d.from; continue; } // we'll never reply to a blacklisted callsign or base callsign auto blacklist = m_config.auto_blacklist(); if (!blacklist.isEmpty() && (blacklist.contains(d.from) || blacklist.contains(Radio::base_callsign(d.from)))) { qCDebug(mainwindow_js8) << "skipping command for blacklist" << d.from; continue; } // if this is an allcall, check to make sure we haven't replied to their // allcall recently (in the past ten minutes) that way we never get // spammed by allcalls at too high of a frequency if (isAllCall && m_txAllcallCommandCache.contains(d.from) && m_txAllcallCommandCache[d.from]->secsTo(now) / 60 < 15) { qCDebug(mainwindow_js8) << "skipping command for allcall timeout" << d.from; continue; } // don't actually process any automatic message replies while in idle if (m_tx_watchdog) { qCDebug(mainwindow_js8) << "skipping command for idle timeout" << d.from; continue; } // HACK: if this is an autoreply cmd and relay path is populated and cmd // is not MSG or MSG TO:, then swap out the relay path if (Varicode::isCommandAutoreply(d.cmd) && !d.relayPath.isEmpty() && !d.cmd.startsWith(" MSG") && !d.cmd.startsWith(" QUERY")) { d.from = d.relayPath; } // construct a reply, if needed QString reply; int priority = PriorityNormal; int freq = -1; // QUERIED SNR // Only reply to a SNR request if there is no incoming MSG's and is not // directed to AllCall if (d.cmd == " SNR?" && !isAllCall && m_messageBuffer.isEmpty()) { reply = QString("%1 SNR %2") .arg(d.from) .arg(Varicode::formatSNR(d.snr)); } // QUERIED INFO // Only reply to a INFO request if there is no incoming MSG's and is not // directed to AllCall else if (d.cmd == " INFO?" && !isAllCall && m_messageBuffer.isEmpty()) { QString info = m_config.my_info(); if (info.isEmpty()) { continue; } reply = QString("%1 INFO %2") .arg(d.from) .arg(replaceMacros(info, buildMacroValues(), true)); } // QUERIED ACTIVE // Only reply to a STATUS request if there is no incoming MSG's and is // not directed to AllCall else if (d.cmd == " STATUS?" && !isAllCall && m_messageBuffer.isEmpty()) { QString status = m_config.my_status(); if (status.isEmpty()) { continue; } reply = QString("%1 STATUS %2") .arg(d.from) .arg(replaceMacros(status, buildMacroValues(), true)); } // QUERIED GRID // Only reply to a GRID request if there is no incoming MSG's and is not // directed to AllCall else if (d.cmd == " GRID?" && !isAllCall && m_messageBuffer.isEmpty()) { QString grid = m_config.my_grid(); if (grid.isEmpty()) { continue; } reply = QString("%1 GRID %2").arg(d.from).arg(grid); } // QUERIED STATIONS HEARD // Only reply to a HEARING request if there is no incoming MSG's and is // not directed to AllCall else if (d.cmd == " HEARING?" && !isAllCall && m_messageBuffer.isEmpty()) { auto calls = m_callActivity.keys(); std::stable_sort(calls.begin(), calls.end(), [this](QString const &lhs, QString const &rhs) { return m_callActivity[rhs].utcTimestamp < m_callActivity[lhs].utcTimestamp; }); auto const callsignAging = m_config.callsign_aging(); auto const maxStations = 4; auto i = 0; QStringList lines; for (auto const &call : calls) { if (i >= maxStations) break; if (call == d.from) continue; auto const &cd = m_callActivity[call]; if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) { continue; } lines.append(cd.call); i++; } lines.prepend(QString("%1 HEARING").arg(d.from)); reply = lines.join(' '); } // PROCESS RELAY else if (d.cmd == ">" && !isAllCall && !m_config.relay_off()) { // 1. see if there are any more hops to process // 2. if so, forward // 3. otherwise, display alert & reply dialog QString callToPattern = { R"(^(?\b(?[A-Z0-9]{1,4}\/)?(?([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?\/[A-Z0-9]{1,4})?(?[> ]))\b)"}; QRegularExpression re(callToPattern); auto text = d.text; auto match = re.match(text); // if the text starts with a callsign, and relay is not disabled, // and this is not a group callsign, then relay. if (match.hasMatch() && !isGroupCall) { // replace freetext with relayed free text if (match.captured("type") != ">") { text = text.replace(match.capturedStart("type"), match.capturedLength("type"), ">"); } reply = QString("%1 *DE* %2").arg(text).arg(d.from); // otherwise, as long as we're not an ACK...alert the user and // either send an ACK or Message } else if (!d.text.startsWith("ACK")) { // parse out the callsign path auto calls = parseRelayPathCallsigns(d.from, d.text); // put these third party calls in the heard list foreach (auto call, calls) { CallDetail cd = {}; cd.call = call; cd.snr = -64; cd.dial = d.dial; cd.offset = d.offset; cd.through = d.from; cd.utcTimestamp = DriftingDateTime::currentDateTimeUtc(); cd.tdrift = d.tdrift; cd.submode = d.submode; logCallActivity(cd, false); } d.relayPath = calls.join('>'); reply = QString("%1 ACK").arg(d.relayPath); // check to see if the relay text contains a command that should // be replied to instead of an ack. QStringList relayedCmds = d.text.split(" "); if (!relayedCmds.isEmpty()) { auto first = relayedCmds.first(); auto valid = Varicode::isCommandAllowed(first); if (!valid) { first = " " + first; valid = Varicode::isCommandAllowed(first); if (valid) { relayedCmds.removeFirst(); } } // HACK: "MSG TO:" should be supported but contains a space // :( if (!relayedCmds.isEmpty()) { if (first == " MSG") { auto second = relayedCmds.first(); if (second == "TO:") { first = " MSG TO:"; relayedCmds.removeFirst(); } else if (second.startsWith("TO:")) { first = " MSG TO:"; relayedCmds.replace(0, second.mid(3)); } } else if (first == " QUERY") { auto second = relayedCmds.first(); if (second == "MSGS" || second == "MSGS?") { first = " QUERY MSGS"; relayedCmds.removeFirst(); } else if (second == "CALL") { first = " QUERY CALL"; relayedCmds.removeFirst(); } } } if (Varicode::isCommandAllowed(first) && Varicode::isCommandAutoreply(first)) { CommandDetail rd = {}; rd.bits = d.bits; rd.cmd = first; rd.dial = d.dial; rd.offset = d.offset; rd.from = d.from; // note, MSG and QUERY commands are not set // with from as the relay path. rd.relayPath = d.relayPath; rd.text = relayedCmds.join(" "); // d.text; rd.to = d.to; rd.utcTimestamp = d.utcTimestamp; m_rxCommandQueue.insert(0, rd); continue; } } #if STORE_RELAY_MSGS_TO_INBOX // if we make it here, this is a message addCommandToMyInbox(d); #endif } } // PROCESS MESSAGE STORAGE else if (d.cmd == " MSG TO:" && !isAllCall) { // store message QStringList segs = d.text.split(" "); if (segs.isEmpty()) { continue; } auto to = segs.first(); segs.removeFirst(); auto text = segs.join(" ").trimmed(); /** * APRS relay: inbound `MSG TO:` stores for our local station. * Dedupe within 60s using TO + TEXT + DE . */ if (d.to == "@APRSIS") { if (to != m_config.my_callsign().trimmed()) { continue; } static QRegularExpression aprsDeRe("\\s+DE\\s+(\\S+)\\s*$"); auto aprsSender = QString("APRS"); auto deMatch = aprsDeRe.match(text); if (deMatch.hasMatch()) { aprsSender = deMatch.captured(1); } auto dedupeKey = QString("%1|%2|%3") .arg(to, text, aprsSender) .toUpper(); auto now = DriftingDateTime::currentDateTimeUtc(); auto cutoff = now.addSecs(-60); auto it = m_aprsRelayDedupCache.begin(); while (it != m_aprsRelayDedupCache.end()) { if (it.value() < cutoff) { it = m_aprsRelayDedupCache.erase(it); } else { ++it; } } if (m_aprsRelayDedupCache.contains(dedupeKey)) { continue; } CommandDetail cd = {}; cd.bits = d.bits; cd.cmd = " MSG "; cd.extra = d.extra; cd.dial = d.dial; cd.offset = d.offset; cd.from = "APRS"; cd.relayPath = "APRS"; cd.grid = d.grid; cd.snr = d.snr; cd.tdrift = d.tdrift; cd.text = text; cd.to = to; cd.utcTimestamp = d.utcTimestamp; cd.submode = d.submode; addCommandToMyInbox(cd); m_aprsRelayDedupCache.insert(dedupeKey, now); tryNotify("inbox"); continue; } if (m_config.relay_off()) { continue; } auto calls = parseRelayPathCallsigns(d.from, text); d.relayPath = calls.join(">"); CommandDetail cd = {}; cd.bits = d.bits; cd.cmd = d.cmd; cd.extra = d.extra; cd.dial = d.dial; cd.offset = d.offset; cd.from = d.from; cd.grid = d.grid; cd.relayPath = d.relayPath; cd.snr = d.snr; cd.tdrift = d.tdrift; cd.text = text; cd.to = Radio::base_callsign(to); cd.utcTimestamp = d.utcTimestamp; cd.submode = d.submode; qCDebug(mainwindow_js8) << "storing message to" << to << ":" << text; addCommandToStorage("STORE", cd); // we haven't replaced the from with the relay path, so we have to // use it for the ack if there is one reply = QString("%1 ACK").arg(calls.length() > 1 ? d.relayPath : d.from); } // PROCESS AGN else if (d.cmd == " AGN?" && !isAllCall && !isGroupCall && !m_lastTxMessage.isEmpty()) { reply = Varicode::rstrip(m_lastTxMessage); } // PROCESS ACTIVE HEARTBEAT // if we have hb mode enabled and auto reply enabled and auto ack // enabled and no callsign is selected update: if we're in HB // mode, doesn't matter if a callsign is selected. else if ((d.cmd == " HB" || d.cmd == " HEARTBEAT") && canCurrentModeSendHeartbeat() && ui->actionModeJS8HB->isChecked() && ui->actionModeAutoreply->isChecked() && ui->actionHeartbeatAcknowledgements->isChecked()) { // do not process HB activity if buffer is not empty, this prevents // broken incoming MSG's if (!m_messageBuffer.isEmpty()) { qCDebug(mainwindow_js8) << "hb paused for messageBuffer"; continue; } // check to make sure we aren't pausing HB transmissions (ACKs) // while a callsign is selected if (m_config.heartbeat_qso_pause() && !selectedCallsign.isEmpty()) { qCDebug(mainwindow_js8) << "hb paused during qso"; continue; } // check to make sure this callsign isn't blacklisted if (m_config.hb_blacklist().contains(d.from) || m_config.hb_blacklist().contains( Radio::base_callsign(d.from))) { qCDebug(mainwindow_js8) << "hb blacklist blocking" << d.from; continue; } // check to see if we have a message for a station who is // heartbeating QString extra; auto mid = getNextMessageIdForCallsign(d.from); if (mid != -1) { extra = QString("MSG ID %1").arg(mid); } // group messaging - if isGroupCall, check to see if there's a // message id for the group and return it if there's not an // individual message // TODO: include group name in response to differentiate from direct // messages? else if (isGroupCall) { mid = getNextGroupMessageIdForCallsign(d.to, d.from); if (mid != -1) { extra = QString("MSG ID %1").arg(mid); } } // TODO: require confirmation? sendHeartbeatAck(d.from, d.snr, extra); if (isAllCall) { // since all pings are technically @ALLCALL, let's bump the // allcall cache here... m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 5); } continue; } // PROCESS HEARTBEAT SNR else if (d.cmd == " HEARTBEAT SNR") { qCDebug(mainwindow_js8) << "skipping incoming hb snr" << d.text; continue; } // PROCESS CQ else if (d.cmd == " CQ") { qCDebug(mainwindow_js8) << "skipping incoming cq" << d.text; continue; } // PROCESS MSG else if (d.cmd == " MSG" && !isAllCall) { auto text = d.text; /** * APRS relay: inbound `MSG` where the payload starts with * `TO:` and includes `DE `. * Dedupe within 60s using TO + TEXT + DE . */ if (d.to == "@APRSIS") { static QRegularExpression aprsToRe( "^TO:\\s*(\\S+)\\s+(.*)$"); auto match = aprsToRe.match(text); if (match.hasMatch()) { auto dest = match.captured(1); auto aprsText = match.captured(2).trimmed(); if (dest == m_config.my_callsign().trimmed()) { static QRegularExpression aprsDeRe( "\\s+DE\\s+(\\S+)\\s*$"); auto aprsSender = QString("APRS"); auto deMatch = aprsDeRe.match(aprsText); if (deMatch.hasMatch()) { aprsSender = deMatch.captured(1); } auto dedupeKey = QString("%1|%2|%3") .arg(dest, aprsText, aprsSender) .toUpper(); auto now = DriftingDateTime::currentDateTimeUtc(); auto cutoff = now.addSecs(-60); auto it = m_aprsRelayDedupCache.begin(); while (it != m_aprsRelayDedupCache.end()) { if (it.value() < cutoff) { it = m_aprsRelayDedupCache.erase(it); } else { ++it; } } if (m_aprsRelayDedupCache.contains(dedupeKey)) { continue; } CommandDetail cd = d; cd.cmd = " MSG "; cd.from = "APRS"; cd.relayPath = "APRS"; cd.to = dest; cd.text = aprsText; addCommandToMyInbox(cd); m_aprsRelayDedupCache.insert(dedupeKey, now); tryNotify("inbox"); } } continue; } qCDebug(mainwindow_js8) << "adding message to inbox" << text; auto calls = parseRelayPathCallsigns(d.from, text); d.cmd = " MSG "; d.relayPath = calls.join(">"); d.text = text; addCommandToMyInbox(d); // notification tryNotify("inbox"); // we haven't replaced the from with the relay path, so we have to // use it for the ack if there is one reply = QString("%1 ACK").arg(calls.length() > 1 ? d.relayPath : d.from); #define SHOW_ALERT_FOR_MSG 1 #if SHOW_ALERT_FOR_MSG SelfDestructMessageBox *m = new SelfDestructMessageBox( 300, "New Message Received", QString("A new message was received at %1 UTC from %2") .arg(d.utcTimestamp.time().toString()) .arg(d.from), QMessageBox::Information, QMessageBox::Ok, QMessageBox::Ok, false, this); m->show(); #endif } // PROCESS ACKS else if (d.cmd == " ACK" && !isAllCall) { qCDebug(mainwindow_js8) << "skipping incoming ack" << d.text; // notification for ack tryNotify("ack"); // make sure this is explicit continue; } // PROCESS BUFFERED CMD else if (d.cmd == " CMD" && !isAllCall) { qCDebug(mainwindow_js8) << "skipping incoming command" << d.text; // make sure this is explicit continue; } // PROCESS BUFFERED QUERY else if (d.cmd == " QUERY" && !isAllCall) { auto who = d.from; // keep in mind, this is the sender, not the // original requestor if relayed auto replyPath = d.from; if (d.relayPath.contains(">")) { auto path = d.relayPath.split(">"); who = path.last(); replyPath = d.relayPath; } QStringList segs = d.text.split(" "); if (segs.isEmpty()) { continue; } auto cmd = segs.first(); segs.removeFirst(); if (cmd == "MSG" && !segs.isEmpty()) { auto inbox = Inbox(inboxPath()); if (!inbox.open()) { continue; } bool ok = false; int mid = QString(segs.first()).toInt(&ok); if (!ok) { continue; } auto msg = inbox.value(mid); auto params = msg.params(); if (params.isEmpty()) { continue; } auto from = params.value("FROM").toString().trimmed(); auto to = params.value("TO").toString().trimmed(); // group messaging - allow any message to a @GROUP to be // retrieved by anybody bool isGroupMsg = to.startsWith("@"); if (!isGroupMsg && to != who && to != Radio::base_callsign(who)) { continue; } auto text = params.value("TEXT").toString().trimmed(); if (text.isEmpty()) { continue; } /* * mark as delivered (so subsequent HBs and QUERY MSGS don't * receive this message) * * Do this in callbacks so that messages are only marked read * after any potential confirmations have been made by the user, * and the message has been processed in the transaction queue. */ if (!isGroupMsg) { callback = [this, mid, msg]() { this->markMsgDelivered(mid, msg); }; } else { callback = [this, mid, who]() { this->markGroupMsgDeliveredForCallsign(mid, who); }; } auto lookaheadMid = getLookaheadMessageIdForCallsign(who, mid); if (lookaheadMid == -1 && isGroupMsg) { lookaheadMid = getLookaheadGroupMessageIdForCallsign(d.to, who, mid); } // and reply if (lookaheadMid != -1) { reply = QString("%1 MSG %2 FROM %3 NEXT MSG ID %4"); reply = reply.arg(replyPath); reply = reply.arg(text); reply = reply.arg(from); reply = reply.arg(lookaheadMid); } else { reply = QString("%1 MSG %2 FROM %3"); reply = reply.arg(replyPath); reply = reply.arg(text); reply = reply.arg(from); } } } // PROCESS BUFFERED QUERY MSGS // Do not process this request if there is incoming MSG as it breaks the // incoming else if (d.cmd == " QUERY MSGS" && ui->actionModeAutoreply->isChecked() && m_messageBuffer.isEmpty()) { auto who = d.from; // keep in mind, this is the sender, not the // original requestor if relayed auto replyPath = d.from; if (d.relayPath.contains(">")) { auto path = d.relayPath.split(">"); who = path.last(); replyPath = d.relayPath; } // if this is an allcall or a directed call, check to see if we have // a stored message for user. we reply yes if the user would be able // to retreive a stored message auto mid = getNextMessageIdForCallsign(who); if (mid != -1) { reply = QString("%1 YES MSG ID %2").arg(replyPath).arg(mid); } // Group messaging - if isGroupCall, check to see if there's a // message id for the group and return it if there's not an // individual message // TODO: include group name in response to differentiate from direct // messages? else if (isGroupCall) { mid = getNextGroupMessageIdForCallsign(d.to, d.from); if (mid != -1) { reply = QString("%1 YES MSG ID %2").arg(replyPath).arg(mid); } } // if this is not an allcall and we have no messages, reply no. if (!isAllCall && reply.isEmpty()) { reply = QString("%1 NO").arg(replyPath); } } // PROCESS BUFFERED QUERY CALL // Do not process this request if there is incoming MSG as it breaks the // incoming else if (d.cmd == " QUERY CALL" && ui->actionModeAutoreply->isChecked() && m_messageBuffer.isEmpty()) { auto replyPath = d.from; if (d.relayPath.contains(">")) { replyPath = d.relayPath; } auto who = d.text; if (who.isEmpty()) { continue; } auto callsigns = Varicode::parseCallsigns(who); if (callsigns.isEmpty()) { continue; } QStringList replies; int callsignAging = m_config.callsign_aging(); auto baseCall = callsigns.first(); foreach (auto cd, m_callActivity.values()) { if (callsignAging && cd.utcTimestamp.secsTo(now) / 60 >= callsignAging) { continue; } if (baseCall == cd.call || baseCall == Radio::base_callsign(cd.call)) { auto r = QString("%1 (%2)") .arg(Varicode::formatSNR(cd.snr)) .arg(since(cd.utcTimestamp)) .trimmed(); replies.append(r); break; } } if (!replies.isEmpty()) { replies.prepend(QString("%1 YES").arg(replyPath)); } reply = replies.join(" "); if (!reply.isEmpty()) { if (isAllCall) { m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25); } } } #if 0 // PROCESS ALERT else if (d.cmd == "!" && !isAllCall) { // create alert dialog processAlertReplyForCommand(d, d.from, " "); // make sure this is explicit continue; } #endif // well, if there's no reply, don't do anything... if (reply.isEmpty()) { continue; } // do not queue @ALLCALL replies if auto-reply is not checked if (!ui->actionModeAutoreply->isChecked() && isAllCall) { continue; } #if 0 // TODO: jsherer - HB issue here // do not queue a reply if it's a HB and HB is not active // if((!ui->hbMacroButton->isChecked() || m_hbInterval <= 0) && d.cmd.contains("HB")){ // continue; // } #endif // do not queue for reply if there's text in the window if (!ui->extFreeTextMsgEdit->toPlainText().isEmpty()) { continue; } // do not queue for reply if there's a buffer open to us int bufferOffset = 0; if (hasExistingMessageBufferToMe(&bufferOffset)) { qCDebug(mainwindow_js8) << "skipping reply due to open buffer" << bufferOffset << m_messageBuffer.count(); continue; } // add @ALLCALLs to the @ALLCALL cache if (isAllCall) { m_txAllcallCommandCache.insert(d.from, new QDateTime(now), 25); } // queue the reply here to be sent when a free interval is available on // the frequency that was sent unless, this is an allcall, to which we // should be responding on a clear frequency offset we always want to // make sure that the directed cache has been updated at this point so // we have the most information available to make a frequency selection. if (m_config.autoreply_confirmation()) { confirmThenEnqueueMessage(90, priority, reply, freq, callback); } else { enqueueMessage(priority, reply, freq, callback); } } }