This commit is contained in:
Bill 2026-01-30 13:39:51 +08:00 committed by GitHub
commit a98be90e08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 254 additions and 1 deletions

View File

@ -23,6 +23,11 @@ export const TlonAccountSchema = z.object({
dmAllowlist: z.array(ShipSchema).optional(),
autoDiscoverChannels: 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({
@ -37,6 +42,11 @@ export const TlonConfigSchema = z.object({
showModelSignature: z.boolean().optional(),
authorization: TlonAuthorizationSchema.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(),
});

View File

@ -7,7 +7,7 @@ import { resolveTlonAccount } from "../types.js";
import { normalizeShip, parseChannelNest } from "../targets.js";
import { authenticate } from "../urbit/auth.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 { createProcessedMessageTracker } from "./processed-messages.js";
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() {
try {
const dmShips = await api!.scry("/chat/dm.json");
@ -465,6 +644,12 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
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...");
await api!.connect();
runtime.log?.("[tlon] Connected! All subscriptions active");

View File

@ -12,6 +12,11 @@ export type TlonResolvedAccount = {
dmAllowlist: string[];
autoDiscoverChannels: 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 {
@ -26,6 +31,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
dmAllowlist?: string[];
autoDiscoverChannels?: boolean;
showModelSignature?: boolean;
autoAcceptGroupInvites?: boolean;
autoAcceptDmInvites?: boolean;
inviteAllowlist?: string[];
inviteBlocklist?: string[];
accounts?: Record<string, Record<string, unknown>>;
}
| undefined;
@ -43,6 +52,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
dmAllowlist: [],
autoDiscoverChannels: 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;
const showModelSignature =
(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);
return {
@ -72,6 +91,10 @@ export function resolveTlonAccount(cfg: OpenClawConfig, accountId?: string | nul
dmAllowlist,
autoDiscoverChannels,
showModelSignature,
autoAcceptGroupInvites,
autoAcceptDmInvites,
inviteAllowlist,
inviteBlocklist,
};
}

View File

@ -125,3 +125,38 @@ export function buildMediaText(text: string | undefined, mediaUrl: string | unde
if (cleanUrl) return cleanUrl;
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,
},
});
}