feat(linq): add read receipts, typing indicators, and User-Agent header

Send read receipt and typing indicator immediately on inbound messages
for a more natural iMessage experience. Add User-Agent header to all
Linq API requests. Fix delivery payload to use .text instead of .body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
George McCain
2026-02-14 07:49:45 -05:00
committed by Peter Steinberger
parent 60bd154e5a
commit 1d81cc4f1f
3 changed files with 13 additions and 7 deletions

View File

@@ -34,7 +34,7 @@ import {
import { resolveAgentRoute } from "../routing/resolve-route.js";
import { truncateUtf16Safe } from "../utils.js";
import { resolveLinqAccount } from "./accounts.js";
import { sendMessageLinq } from "./send.js";
import { markAsReadLinq, sendMessageLinq, startTypingLinq } from "./send.js";
export type MonitorLinqOpts = {
accountId?: string;
@@ -198,6 +198,10 @@ export async function monitorLinqProvider(opts: MonitorLinqOpts = {}): Promise<v
return;
}
// Send read receipt and typing indicator immediately (fire-and-forget).
markAsReadLinq(chatId, token).catch(() => {});
startTypingLinq(chatId, token).catch(() => {});
const storeAllowFrom = await readChannelAllowFromStore("linq").catch(() => []);
const effectiveDmAllowFrom = Array.from(new Set([...allowFrom, ...storeAllowFrom]))
.map((v) => String(v).trim())
@@ -342,8 +346,7 @@ export async function monitorLinqProvider(opts: MonitorLinqOpts = {}): Promise<v
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
const replyText =
typeof payload === "string" ? payload : ((payload as { body?: string }).body ?? "");
const replyText = typeof payload === "string" ? payload : (payload.text ?? "");
if (replyText) {
await sendMessageLinq(chatId, replyText, {
token,

View File

@@ -32,7 +32,7 @@ export async function probeLinq(
try {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${resolvedToken}` },
headers: { Authorization: `Bearer ${resolvedToken}`, "User-Agent": "OpenClaw/1.0" },
signal: controller.signal,
});
if (!response.ok) {

View File

@@ -3,6 +3,7 @@ import { loadConfig } from "../config/config.js";
import { resolveLinqAccount, type ResolvedLinqAccount } from "./accounts.js";
const LINQ_API_BASE = "https://api.linqapp.com/api/partner/v3";
const UA = "OpenClaw/1.0";
export type LinqSendOpts = {
accountId?: string;
@@ -55,6 +56,7 @@ export async function sendMessageLinq(
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": UA,
},
body: JSON.stringify({ message }),
});
@@ -79,7 +81,7 @@ export async function startTypingLinq(chatId: string, token: string): Promise<vo
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/typing`;
await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
});
}
@@ -88,7 +90,7 @@ export async function stopTypingLinq(chatId: string, token: string): Promise<voi
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/typing`;
await fetch(url, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
});
}
@@ -97,7 +99,7 @@ export async function markAsReadLinq(chatId: string, token: string): Promise<voi
const url = `${LINQ_API_BASE}/chats/${encodeURIComponent(chatId)}/read`;
await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
headers: { Authorization: `Bearer ${token}`, "User-Agent": UA },
});
}
@@ -114,6 +116,7 @@ export async function sendReactionLinq(
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": UA,
},
body: JSON.stringify({ operation, type }),
});