Merge 121a674a54 into 613724c26e
This commit is contained in:
commit
a98be90e08
@ -23,6 +23,11 @@ export const TlonAccountSchema = z.object({
|
|||||||
dmAllowlist: z.array(ShipSchema).optional(),
|
dmAllowlist: z.array(ShipSchema).optional(),
|
||||||
autoDiscoverChannels: z.boolean().optional(),
|
autoDiscoverChannels: z.boolean().optional(),
|
||||||
showModelSignature: z.boolean().optional(),
|
showModelSignature: z.boolean().optional(),
|
||||||
|
// Auto-accept invites settings
|
||||||
|
autoAcceptGroupInvites: z.boolean().optional(),
|
||||||
|
autoAcceptDmInvites: z.boolean().optional(),
|
||||||
|
inviteAllowlist: z.array(ShipSchema).optional(),
|
||||||
|
inviteBlocklist: z.array(ShipSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TlonConfigSchema = z.object({
|
export const TlonConfigSchema = z.object({
|
||||||
@ -37,6 +42,11 @@ export const TlonConfigSchema = z.object({
|
|||||||
showModelSignature: z.boolean().optional(),
|
showModelSignature: z.boolean().optional(),
|
||||||
authorization: TlonAuthorizationSchema.optional(),
|
authorization: TlonAuthorizationSchema.optional(),
|
||||||
defaultAuthorizedShips: z.array(ShipSchema).optional(),
|
defaultAuthorizedShips: z.array(ShipSchema).optional(),
|
||||||
|
// Auto-accept invites settings
|
||||||
|
autoAcceptGroupInvites: z.boolean().optional(),
|
||||||
|
autoAcceptDmInvites: z.boolean().optional(),
|
||||||
|
inviteAllowlist: z.array(ShipSchema).optional(),
|
||||||
|
inviteBlocklist: z.array(ShipSchema).optional(),
|
||||||
accounts: z.record(z.string(), TlonAccountSchema).optional(),
|
accounts: z.record(z.string(), TlonAccountSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { resolveTlonAccount } from "../types.js";
|
|||||||
import { normalizeShip, parseChannelNest } from "../targets.js";
|
import { normalizeShip, parseChannelNest } from "../targets.js";
|
||||||
import { authenticate } from "../urbit/auth.js";
|
import { authenticate } from "../urbit/auth.js";
|
||||||
import { UrbitSSEClient } from "../urbit/sse-client.js";
|
import { UrbitSSEClient } from "../urbit/sse-client.js";
|
||||||
import { sendDm, sendGroupMessage } from "../urbit/send.js";
|
import { sendDm, sendGroupMessage, acceptGroupInvite, acceptDmInvite } from "../urbit/send.js";
|
||||||
import { cacheMessage, getChannelHistory } from "./history.js";
|
import { cacheMessage, getChannelHistory } from "./history.js";
|
||||||
import { createProcessedMessageTracker } from "./processed-messages.js";
|
import { createProcessedMessageTracker } from "./processed-messages.js";
|
||||||
import {
|
import {
|
||||||
@ -423,6 +423,185 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track processed invites to avoid duplicates
|
||||||
|
const processedInvites = new Set<string>();
|
||||||
|
const processedDmInvites = new Set<string>();
|
||||||
|
|
||||||
|
// Check if an invite should be auto-accepted based on config
|
||||||
|
function shouldAutoAcceptInvite(inviterShip: string, isGroupInvite: boolean): boolean {
|
||||||
|
const autoAcceptEnabled = isGroupInvite ? account.autoAcceptGroupInvites : account.autoAcceptDmInvites;
|
||||||
|
if (!autoAcceptEnabled) return false;
|
||||||
|
|
||||||
|
const normalizedInviter = normalizeShip(inviterShip);
|
||||||
|
|
||||||
|
// Check blocklist first (blocklist takes priority)
|
||||||
|
if (account.inviteBlocklist.length > 0) {
|
||||||
|
const normalizedBlocklist = account.inviteBlocklist.map(normalizeShip);
|
||||||
|
if (normalizedBlocklist.includes(normalizedInviter)) {
|
||||||
|
runtime.log?.(`[tlon] Invite from ${inviterShip} blocked by blocklist`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If allowlist is set, only accept from those ships
|
||||||
|
if (account.inviteAllowlist.length > 0) {
|
||||||
|
const normalizedAllowlist = account.inviteAllowlist.map(normalizeShip);
|
||||||
|
if (!normalizedAllowlist.includes(normalizedInviter)) {
|
||||||
|
runtime.log?.(`[tlon] Invite from ${inviterShip} not in allowlist, skipping`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming foreign groups updates (includes invites)
|
||||||
|
async function handleForeignGroupsUpdate(update: any) {
|
||||||
|
try {
|
||||||
|
if (!update || typeof update !== "object") return;
|
||||||
|
|
||||||
|
for (const [groupId, foreignData] of Object.entries(update)) {
|
||||||
|
if (!foreignData || typeof foreignData !== "object") continue;
|
||||||
|
|
||||||
|
const foreign = foreignData as {
|
||||||
|
invites?: Array<{ from: string; valid: boolean; time?: number }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validInvites = foreign.invites?.filter((inv) => inv.valid) ?? [];
|
||||||
|
if (validInvites.length === 0) continue;
|
||||||
|
|
||||||
|
for (const invite of validInvites) {
|
||||||
|
const inviteKey = `${groupId}:${invite.from}:${invite.time ?? "unknown"}`;
|
||||||
|
if (processedInvites.has(inviteKey)) continue;
|
||||||
|
processedInvites.add(inviteKey);
|
||||||
|
|
||||||
|
runtime.log?.(`[tlon] Received group invite: ${groupId} from ${invite.from}`);
|
||||||
|
|
||||||
|
if (shouldAutoAcceptInvite(invite.from, true)) {
|
||||||
|
try {
|
||||||
|
runtime.log?.(`[tlon] Auto-accepting invite to ${groupId} from ${invite.from}`);
|
||||||
|
await acceptGroupInvite(api!, groupId);
|
||||||
|
runtime.log?.(`[tlon] Successfully joined group ${groupId}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshChannelSubscriptions().catch((error) => {
|
||||||
|
runtime.error?.(`[tlon] Failed to refresh channels after join: ${error?.message ?? String(error)}`);
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Failed to accept invite to ${groupId}: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtime.log?.(`[tlon] Invite to ${groupId} not auto-accepted (auto-accept disabled or filtered)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Error handling foreign groups update: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to foreign groups updates to detect invites
|
||||||
|
let foreignGroupsSubscribed = false;
|
||||||
|
async function subscribeToForeignGroups() {
|
||||||
|
if (foreignGroupsSubscribed) return;
|
||||||
|
if (!account.autoAcceptGroupInvites) {
|
||||||
|
runtime.log?.("[tlon] Auto-accept group invites disabled, skipping foreign groups subscription");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api!.subscribe({
|
||||||
|
app: "groups",
|
||||||
|
path: "/v1/foreigns",
|
||||||
|
event: handleForeignGroupsUpdate,
|
||||||
|
err: (error) => {
|
||||||
|
runtime.error?.(`[tlon] Foreign groups subscription error: ${String(error)}`);
|
||||||
|
},
|
||||||
|
quit: () => {
|
||||||
|
runtime.log?.("[tlon] Foreign groups subscription ended");
|
||||||
|
foreignGroupsSubscribed = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
foreignGroupsSubscribed = true;
|
||||||
|
runtime.log?.("[tlon] Subscribed to foreign groups (invite detection enabled)");
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Failed to subscribe to foreign groups: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming DM invite updates from /v3 subscription
|
||||||
|
async function handleDmInviteUpdate(update: any) {
|
||||||
|
try {
|
||||||
|
if (!update || typeof update !== "object") return;
|
||||||
|
|
||||||
|
let pendingShips: string[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(update)) {
|
||||||
|
pendingShips = update.filter((item) => typeof item === "string");
|
||||||
|
} else if (update.ship && update.pending) {
|
||||||
|
pendingShips = [update.ship];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ship of pendingShips) {
|
||||||
|
const normalizedShip = normalizeShip(ship);
|
||||||
|
if (processedDmInvites.has(normalizedShip)) continue;
|
||||||
|
processedDmInvites.add(normalizedShip);
|
||||||
|
|
||||||
|
runtime.log?.(`[tlon] Received DM invite from ${normalizedShip}`);
|
||||||
|
|
||||||
|
if (shouldAutoAcceptInvite(normalizedShip, false)) {
|
||||||
|
try {
|
||||||
|
runtime.log?.(`[tlon] Auto-accepting DM invite from ${normalizedShip}`);
|
||||||
|
await acceptDmInvite(api!, normalizedShip);
|
||||||
|
runtime.log?.(`[tlon] Successfully accepted DM from ${normalizedShip}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
subscribeToDM(normalizedShip).catch((error) => {
|
||||||
|
runtime.error?.(`[tlon] Failed to subscribe to new DM: ${error?.message ?? String(error)}`);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Failed to accept DM invite from ${normalizedShip}: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtime.log?.(`[tlon] DM invite from ${normalizedShip} not auto-accepted (auto-accept disabled or filtered)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Error handling DM invite update: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to chat updates for DM invite detection
|
||||||
|
let dmInvitesSubscribed = false;
|
||||||
|
async function subscribeToDmInvites() {
|
||||||
|
if (dmInvitesSubscribed) return;
|
||||||
|
if (!account.autoAcceptDmInvites) {
|
||||||
|
runtime.log?.("[tlon] Auto-accept DM invites disabled, skipping DM invite subscription");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api!.subscribe({
|
||||||
|
app: "chat",
|
||||||
|
path: "/v3",
|
||||||
|
event: handleDmInviteUpdate,
|
||||||
|
err: (error) => {
|
||||||
|
runtime.error?.(`[tlon] DM invite subscription error: ${String(error)}`);
|
||||||
|
},
|
||||||
|
quit: () => {
|
||||||
|
runtime.log?.("[tlon] DM invite subscription ended");
|
||||||
|
dmInvitesSubscribed = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dmInvitesSubscribed = true;
|
||||||
|
runtime.log?.("[tlon] Subscribed to chat updates (DM invite detection enabled)");
|
||||||
|
} catch (error: any) {
|
||||||
|
runtime.error?.(`[tlon] Failed to subscribe to DM invites: ${error?.message ?? String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshChannelSubscriptions() {
|
async function refreshChannelSubscriptions() {
|
||||||
try {
|
try {
|
||||||
const dmShips = await api!.scry("/chat/dm.json");
|
const dmShips = await api!.scry("/chat/dm.json");
|
||||||
@ -465,6 +644,12 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
|
|||||||
await subscribeToChannel(channelNest);
|
await subscribeToChannel(channelNest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to foreign groups for invite detection (if auto-accept enabled)
|
||||||
|
await subscribeToForeignGroups();
|
||||||
|
|
||||||
|
// Subscribe to DM invites (if auto-accept enabled)
|
||||||
|
await subscribeToDmInvites();
|
||||||
|
|
||||||
runtime.log?.("[tlon] All subscriptions registered, connecting to SSE stream...");
|
runtime.log?.("[tlon] All subscriptions registered, connecting to SSE stream...");
|
||||||
await api!.connect();
|
await api!.connect();
|
||||||
runtime.log?.("[tlon] Connected! All subscriptions active");
|
runtime.log?.("[tlon] Connected! All subscriptions active");
|
||||||
|
|||||||
@ -12,6 +12,11 @@ export type TlonResolvedAccount = {
|
|||||||
dmAllowlist: string[];
|
dmAllowlist: string[];
|
||||||
autoDiscoverChannels: boolean | null;
|
autoDiscoverChannels: boolean | null;
|
||||||
showModelSignature: boolean | null;
|
showModelSignature: boolean | null;
|
||||||
|
// Auto-accept invites settings
|
||||||
|
autoAcceptGroupInvites: boolean | null;
|
||||||
|
autoAcceptDmInvites: boolean | null;
|
||||||
|
inviteAllowlist: string[];
|
||||||
|
inviteBlocklist: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | null): TlonResolvedAccount {
|
export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | null): TlonResolvedAccount {
|
||||||
@ -26,6 +31,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
|
|||||||
dmAllowlist?: string[];
|
dmAllowlist?: string[];
|
||||||
autoDiscoverChannels?: boolean;
|
autoDiscoverChannels?: boolean;
|
||||||
showModelSignature?: boolean;
|
showModelSignature?: boolean;
|
||||||
|
autoAcceptGroupInvites?: boolean;
|
||||||
|
autoAcceptDmInvites?: boolean;
|
||||||
|
inviteAllowlist?: string[];
|
||||||
|
inviteBlocklist?: string[];
|
||||||
accounts?: Record<string, Record<string, unknown>>;
|
accounts?: Record<string, Record<string, unknown>>;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
@ -43,6 +52,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
|
|||||||
dmAllowlist: [],
|
dmAllowlist: [],
|
||||||
autoDiscoverChannels: null,
|
autoDiscoverChannels: null,
|
||||||
showModelSignature: null,
|
showModelSignature: null,
|
||||||
|
autoAcceptGroupInvites: null,
|
||||||
|
autoAcceptDmInvites: null,
|
||||||
|
inviteAllowlist: [],
|
||||||
|
inviteBlocklist: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +71,12 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
|
|||||||
(account?.autoDiscoverChannels ?? base.autoDiscoverChannels ?? null) as boolean | null;
|
(account?.autoDiscoverChannels ?? base.autoDiscoverChannels ?? null) as boolean | null;
|
||||||
const showModelSignature =
|
const showModelSignature =
|
||||||
(account?.showModelSignature ?? base.showModelSignature ?? null) as boolean | null;
|
(account?.showModelSignature ?? base.showModelSignature ?? null) as boolean | null;
|
||||||
|
const autoAcceptGroupInvites =
|
||||||
|
(account?.autoAcceptGroupInvites ?? base.autoAcceptGroupInvites ?? null) as boolean | null;
|
||||||
|
const autoAcceptDmInvites =
|
||||||
|
(account?.autoAcceptDmInvites ?? base.autoAcceptDmInvites ?? null) as boolean | null;
|
||||||
|
const inviteAllowlist = (account?.inviteAllowlist ?? base.inviteAllowlist ?? []) as string[];
|
||||||
|
const inviteBlocklist = (account?.inviteBlocklist ?? base.inviteBlocklist ?? []) as string[];
|
||||||
const configured = Boolean(ship && url && code);
|
const configured = Boolean(ship && url && code);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -72,6 +91,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
|
|||||||
dmAllowlist,
|
dmAllowlist,
|
||||||
autoDiscoverChannels,
|
autoDiscoverChannels,
|
||||||
showModelSignature,
|
showModelSignature,
|
||||||
|
autoAcceptGroupInvites,
|
||||||
|
autoAcceptDmInvites,
|
||||||
|
inviteAllowlist,
|
||||||
|
inviteBlocklist,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -125,3 +125,38 @@ export function buildMediaText(text: string | undefined, mediaUrl: string | unde
|
|||||||
if (cleanUrl) return cleanUrl;
|
if (cleanUrl) return cleanUrl;
|
||||||
return cleanText;
|
return cleanText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept a group invite by sending a group-join poke
|
||||||
|
export async function acceptGroupInvite(api: TlonPokeApi, groupId: string): Promise<void> {
|
||||||
|
await api.poke({
|
||||||
|
app: "groups",
|
||||||
|
mark: "group-join",
|
||||||
|
json: {
|
||||||
|
flag: groupId,
|
||||||
|
"join-all": true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decline a group invite
|
||||||
|
export async function declineGroupInvite(api: TlonPokeApi, groupId: string): Promise<void> {
|
||||||
|
await api.poke({
|
||||||
|
app: "groups",
|
||||||
|
mark: "invite-decline",
|
||||||
|
json: groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept a DM invite by sending a chat-dm-rsvp poke
|
||||||
|
export async function acceptDmInvite(api: TlonPokeApi, ship: string, accept: boolean = true): Promise<void> {
|
||||||
|
// Ship MUST have the ~ prefix for this poke
|
||||||
|
const shipName = ship.startsWith("~") ? ship : `~${ship}`;
|
||||||
|
await api.poke({
|
||||||
|
app: "chat",
|
||||||
|
mark: "chat-dm-rsvp",
|
||||||
|
json: {
|
||||||
|
ship: shipName,
|
||||||
|
ok: accept,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user