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(),
|
||||
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(),
|
||||
});
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user