#!/usr/bin/env node /** * Check Signal group membership status and update Zammad tickets * * This task queries the Signal CLI API to check if users have joined * their assigned groups. When a user joins (moves from pendingInvites to members), * it updates the ticket's group_joined flag in Zammad. */ import { db, getWorkerUtils } from "@link-stack/bridge-common"; import { createLogger } from "@link-stack/logger"; import * as signalApi from "@link-stack/signal-api"; const logger = createLogger("check-group-membership"); const { Configuration, GroupsApi } = signalApi; interface CheckGroupMembershipTaskOptions { // Optional: Check specific group. If not provided, checks all groups with group_joined=false groupId?: string; botToken?: string; } const checkGroupMembershipTask = async ( options: CheckGroupMembershipTaskOptions = {}, ): Promise => { const config = new Configuration({ basePath: process.env.BRIDGE_SIGNAL_URL, }); const groupsClient = new GroupsApi(config); const worker = await getWorkerUtils(); // Get all Signal bots const bots = await db.selectFrom("SignalBot").selectAll().execute(); for (const bot of bots) { try { logger.debug( { botId: bot.id, phoneNumber: bot.phoneNumber }, "Checking groups for bot", ); // Get all groups for this bot const groups = await groupsClient.v1GroupsNumberGet({ number: bot.phoneNumber, }); logger.debug( { botId: bot.id, groupCount: groups.length }, "Retrieved groups from Signal CLI", ); // For each group, check if we have tickets waiting for members to join for (const group of groups) { if (!group.id || !group.internalId) { logger.debug({ groupName: group.name }, "Skipping group without ID"); continue; } // Log info about each group temporarily for debugging logger.info( { groupId: group.id, groupName: group.name, membersCount: group.members?.length || 0, members: group.members, pendingInvitesCount: group.pendingInvites?.length || 0, pendingInvites: group.pendingInvites, pendingRequestsCount: group.pendingRequests?.length || 0, }, "Checking group membership", ); // Notify Zammad about each member who has joined // This handles both cases: // 1. New contacts who must accept invite (they move from pendingInvites to members) // 2. Existing contacts who are auto-added (they appear directly in members) if (group.members && group.members.length > 0) { for (const memberPhone of group.members) { // Check if this member was previously pending // We'll send the webhook and let Zammad decide if it needs to update await worker.addJob("common/notify-webhooks", { backendId: bot.id, payload: { event: "group_member_joined", group_id: group.id, member_phone: memberPhone, timestamp: new Date().toISOString(), }, }); logger.info( { groupId: group.id, memberPhone, }, "Notified Zammad about group member", ); } } } } catch (error: any) { logger.error( { botId: bot.id, error: error.message, stack: error.stack, }, "Error checking group membership for bot", ); } } logger.info("Completed group membership check"); }; export default checkGroupMembershipTask;